mirror of
https://github.com/meeb/tubesync.git
synced 2025-06-22 13:06:34 +00:00
Merge pull request #711 from tcely/patch-17
Move things into `choices.py`
This commit is contained in:
commit
7d55bfdd83
216
tubesync/sync/choices.py
Normal file
216
tubesync/sync/choices.py
Normal file
@ -0,0 +1,216 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
DOMAINS = dict({
|
||||
'youtube': frozenset({
|
||||
'youtube.com',
|
||||
'm.youtube.com',
|
||||
'www.youtube.com',
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
def Val(*args):
|
||||
results = list(
|
||||
a.value if isinstance(a, models.enums.Choices) else a for a in args
|
||||
)
|
||||
return results.pop(0) if 1 == len(results) else (*results,)
|
||||
|
||||
|
||||
class CapChoices(models.IntegerChoices):
|
||||
CAP_NOCAP = 0, _('No cap')
|
||||
CAP_7DAYS = 604800, _('1 week (7 days)')
|
||||
CAP_30DAYS = 2592000, _('1 month (30 days)')
|
||||
CAP_90DAYS = 7776000, _('3 months (90 days)')
|
||||
CAP_6MONTHS = 15552000, _('6 months (180 days)')
|
||||
CAP_1YEAR = 31536000, _('1 year (365 days)')
|
||||
CAP_2YEARs = 63072000, _('2 years (730 days)')
|
||||
CAP_3YEARs = 94608000, _('3 years (1095 days)')
|
||||
CAP_5YEARs = 157680000, _('5 years (1825 days)')
|
||||
CAP_10YEARS = 315360000, _('10 years (3650 days)')
|
||||
|
||||
|
||||
class Fallback(models.TextChoices):
|
||||
FAIL = 'f', _('Fail, do not download any media')
|
||||
NEXT_BEST = 'n', _('Get next best resolution or codec instead')
|
||||
NEXT_BEST_HD = 'h', _('Get next best resolution but at least HD')
|
||||
|
||||
|
||||
class FileExtension(models.TextChoices):
|
||||
M4A = 'm4a', _('MPEG-4 Part 14 (MP4) Audio Container')
|
||||
OGG = 'ogg', _('Ogg Container')
|
||||
MKV = 'mkv', _('Matroska Multimedia Container')
|
||||
|
||||
|
||||
class FilterSeconds(models.IntegerChoices):
|
||||
MIN = True, _('Minimum Length')
|
||||
MAX = False, _('Maximum Length')
|
||||
|
||||
|
||||
class IndexSchedule(models.IntegerChoices):
|
||||
EVERY_HOUR = 3600, _('Every hour')
|
||||
EVERY_2_HOURS = 7200, _('Every 2 hours')
|
||||
EVERY_3_HOURS = 10800, _('Every 3 hours')
|
||||
EVERY_4_HOURS = 14400, _('Every 4 hours')
|
||||
EVERY_5_HOURS = 18000, _('Every 5 hours')
|
||||
EVERY_6_HOURS = 21600, _('Every 6 hours')
|
||||
EVERY_12_HOURS = 43200, _('Every 12 hours')
|
||||
EVERY_24_HOURS = 86400, _('Every 24 hours')
|
||||
EVERY_3_DAYS = 259200, _('Every 3 days')
|
||||
EVERY_7_DAYS = 604800, _('Every 7 days')
|
||||
NEVER = 0, _('Never')
|
||||
|
||||
|
||||
class MediaServerType(models.TextChoices):
|
||||
PLEX = 'p', _('Plex')
|
||||
|
||||
|
||||
class MediaState(models.TextChoices):
|
||||
UNKNOWN = 'unknown'
|
||||
SCHEDULED = 'scheduled'
|
||||
DOWNLOADING = 'downloading'
|
||||
DOWNLOADED = 'downloaded'
|
||||
SKIPPED = 'skipped'
|
||||
DISABLED_AT_SOURCE = 'source-disabled'
|
||||
ERROR = 'error'
|
||||
|
||||
|
||||
class SourceResolution(models.TextChoices):
|
||||
AUDIO = 'audio', _('Audio only')
|
||||
VIDEO_360P = '360p', _('360p (SD)')
|
||||
VIDEO_480P = '480p', _('480p (SD)')
|
||||
VIDEO_720P = '720p', _('720p (HD)')
|
||||
VIDEO_1080P = '1080p', _('1080p (Full HD)')
|
||||
VIDEO_1440P = '1440p', _('1440p (2K)')
|
||||
VIDEO_2160P = '2160p', _('4320p (8K)')
|
||||
VIDEO_4320P = '4320p', _('4320p (8K)')
|
||||
|
||||
@classmethod
|
||||
def _integer_mapping(cls):
|
||||
int_height = lambda s: int(s[:-1], base=10)
|
||||
video = list(filter(lambda s: s.endswith('0p'), cls.values))
|
||||
return dict(zip( video, map(int_height, video) ))
|
||||
|
||||
|
||||
# as stolen from:
|
||||
# - https://wiki.sponsor.ajay.app/w/Types
|
||||
# - https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/postprocessor/sponsorblock.py
|
||||
#
|
||||
# The spacing is a little odd, it is for easy copy/paste selection.
|
||||
# Please don't change it.
|
||||
# Every possible category fits in a string < 128 characters
|
||||
class SponsorBlock_Category(models.TextChoices):
|
||||
SPONSOR = 'sponsor', _( 'Sponsor' )
|
||||
INTRO = 'intro', _( 'Intermission/Intro Animation' )
|
||||
OUTRO = 'outro', _( 'Endcards/Credits' )
|
||||
SELFPROMO = 'selfpromo', _( 'Unpaid/Self Promotion' )
|
||||
PREVIEW = 'preview', _( 'Preview/Recap' )
|
||||
FILLER = 'filler', _( 'Filler Tangent' )
|
||||
INTERACTION = 'interaction', _( 'Interaction Reminder' )
|
||||
MUSIC_OFFTOPIC = 'music_offtopic', _( 'Non-Music Section' )
|
||||
|
||||
|
||||
class YouTube_SourceType(models.TextChoices):
|
||||
CHANNEL = 'c', _('YouTube channel')
|
||||
CHANNEL_ID = 'i', _('YouTube channel by ID')
|
||||
PLAYLIST = 'p', _('YouTube playlist')
|
||||
|
||||
@classmethod
|
||||
def _validation_urls(cls):
|
||||
defaults = {
|
||||
'scheme': 'https',
|
||||
'domains': DOMAINS['youtube'],
|
||||
'qs_args': [],
|
||||
}
|
||||
update_and_return = lambda c, d: c.update(d) or c
|
||||
return dict(zip(
|
||||
cls.values,
|
||||
(
|
||||
update_and_return(deepcopy(defaults), {
|
||||
'path_regex': r'^\/(c\/)?([^\/]+)(\/videos)?$',
|
||||
'path_must_not_match': ('/playlist', '/c/playlist'),
|
||||
'extract_key': ('path_regex', 1),
|
||||
'example': 'https://www.youtube.com/SOMECHANNEL',
|
||||
}),
|
||||
update_and_return(deepcopy(defaults), {
|
||||
'path_regex': r'^\/channel\/([^\/]+)(\/videos)?$',
|
||||
'path_must_not_match': ('/playlist', '/c/playlist'),
|
||||
'extract_key': ('path_regex', 0),
|
||||
'example': 'https://www.youtube.com/channel/CHANNELID',
|
||||
}),
|
||||
update_and_return(deepcopy(defaults), {
|
||||
'path_regex': r'^\/(playlist|watch)$',
|
||||
'path_must_not_match': (),
|
||||
'qs_args': ('list',),
|
||||
'extract_key': ('qs_args', 'list'),
|
||||
'example': 'https://www.youtube.com/playlist?list=PLAYLISTID',
|
||||
}),
|
||||
),
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def _long_type_mapping(cls):
|
||||
return dict(zip(
|
||||
(
|
||||
'youtube-channel',
|
||||
'youtube-channel-id',
|
||||
'youtube-playlist',
|
||||
),
|
||||
cls.values,
|
||||
))
|
||||
|
||||
|
||||
class YouTube_AudioCodec(models.TextChoices):
|
||||
OPUS = 'OPUS', _('OPUS')
|
||||
MP4A = 'MP4A', _('MP4A')
|
||||
|
||||
|
||||
class YouTube_VideoCodec(models.TextChoices):
|
||||
AV1 = 'AV1', _('AV1')
|
||||
VP9 = 'VP9', _('VP9')
|
||||
AVC1 = 'AVC1', _('AVC1 (H.264)')
|
||||
|
||||
|
||||
SourceResolutionInteger = SourceResolution._integer_mapping()
|
||||
youtube_long_source_types = YouTube_SourceType._long_type_mapping()
|
||||
youtube_validation_urls = YouTube_SourceType._validation_urls()
|
||||
youtube_help = {
|
||||
'examples': dict(zip(
|
||||
YouTube_SourceType.values,
|
||||
(
|
||||
('https://www.youtube.com/google'),
|
||||
('https://www.youtube.com/channel/'
|
||||
'UCK8sQmJBp8GCxrOtXWBpyEA'),
|
||||
('https://www.youtube.com/playlist?list='
|
||||
'PL590L5WQmH8dpP0RyH5pCfIaDEdt9nk7r'),
|
||||
),
|
||||
)),
|
||||
'texts': dict(zip(
|
||||
YouTube_SourceType.values,
|
||||
(
|
||||
_(
|
||||
'Enter a YouTube channel URL into the box below. A channel URL will be in '
|
||||
'the format of <strong>https://www.youtube.com/CHANNELNAME</strong> '
|
||||
'where <strong>CHANNELNAME</strong> is the name of the channel you want '
|
||||
'to add.'
|
||||
),
|
||||
_(
|
||||
'Enter a YouTube channel URL by channel ID into the box below. A channel '
|
||||
'URL by channel ID will be in the format of <strong>'
|
||||
'https://www.youtube.com/channel/BiGLoNgUnIqUeId</strong> '
|
||||
'where <strong>BiGLoNgUnIqUeId</strong> is the ID of the channel you want '
|
||||
'to add.'
|
||||
),
|
||||
_(
|
||||
'Enter a YouTube playlist URL into the box below. A playlist URL will be '
|
||||
'in the format of <strong>https://www.youtube.com/playlist?list='
|
||||
'BiGLoNgUnIqUeId</strong> where <strong>BiGLoNgUnIqUeId</strong> is the '
|
||||
'unique ID of the playlist you want to add.'
|
||||
),
|
||||
),
|
||||
)),
|
||||
}
|
||||
|
@ -5,24 +5,6 @@ from django.db import connection, models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
# as stolen from:
|
||||
# - https://wiki.sponsor.ajay.app/w/Types
|
||||
# - https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/postprocessor/sponsorblock.py
|
||||
#
|
||||
# The spacing is a little odd, it is for easy copy/paste selection.
|
||||
# Please don't change it.
|
||||
# Every possible category fits in a string < 128 characters
|
||||
class SponsorBlock_Category(models.TextChoices):
|
||||
SPONSOR = 'sponsor', _( 'Sponsor' )
|
||||
INTRO = 'intro', _( 'Intermission/Intro Animation' )
|
||||
OUTRO = 'outro', _( 'Endcards/Credits' )
|
||||
SELFPROMO = 'selfpromo', _( 'Unpaid/Self Promotion' )
|
||||
PREVIEW = 'preview', _( 'Preview/Recap' )
|
||||
FILLER = 'filler', _( 'Filler Tangent' )
|
||||
INTERACTION = 'interaction', _( 'Interaction Reminder' )
|
||||
MUSIC_OFFTOPIC = 'music_offtopic', _( 'Non-Music Section' )
|
||||
|
||||
|
||||
CommaSepChoice = namedtuple(
|
||||
'CommaSepChoice', [
|
||||
'allow_all',
|
||||
|
@ -2,6 +2,7 @@ import os
|
||||
from pathlib import Path
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from common.logger import log
|
||||
from sync.choices import FileExtension
|
||||
from sync.models import Source, Media
|
||||
|
||||
|
||||
@ -17,7 +18,7 @@ class Command(BaseCommand):
|
||||
for s in Source.objects.all():
|
||||
dirmap[s.directory_path] = s
|
||||
log.info(f'Scanning sources...')
|
||||
file_extensions = list(Source.EXTENSIONS) + self.extra_extensions
|
||||
file_extensions = list(FileExtension.values) + self.extra_extensions
|
||||
for sourceroot, source in dirmap.items():
|
||||
media = list(Media.objects.filter(source=source, downloaded=False,
|
||||
skip=False))
|
||||
|
@ -5,6 +5,7 @@
|
||||
'''
|
||||
|
||||
|
||||
from .choices import Val, Fallback
|
||||
from .utils import multi_key_sort
|
||||
from django.conf import settings
|
||||
|
||||
@ -389,10 +390,10 @@ def get_best_video_format(media):
|
||||
return True, best_match['id']
|
||||
elif media.source.can_fallback:
|
||||
# Allow the fallback if it meets requirements
|
||||
if (media.source.fallback == media.source.FALLBACK_NEXT_BEST_HD and
|
||||
if (media.source.fallback == Val(Fallback.NEXT_BEST_HD) and
|
||||
best_match['height'] >= fallback_hd_cutoff):
|
||||
return False, best_match['id']
|
||||
elif media.source.fallback == media.source.FALLBACK_NEXT_BEST:
|
||||
elif media.source.fallback == Val(Fallback.NEXT_BEST):
|
||||
return False, best_match['id']
|
||||
# Nope, failed to find match
|
||||
return False, False
|
||||
|
@ -0,0 +1,17 @@
|
||||
# Generated by Django 3.2.25 on 2025-02-12 18:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('sync', '0027_alter_source_sponsorblock_categories'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='source',
|
||||
name='source_resolution',
|
||||
field=models.CharField(choices=[('audio', 'Audio only'), ('360p', '360p (SD)'), ('480p', '480p (SD)'), ('720p', '720p (HD)'), ('1080p', '1080p (Full HD)'), ('1440p', '1440p (2K)'), ('2160p', '4320p (8K)'), ('4320p', '4320p (8K)')], db_index=True, default='1080p', help_text='Source resolution, desired video resolution to download', max_length=8, verbose_name='source resolution'),
|
||||
),
|
||||
]
|
||||
|
@ -25,9 +25,15 @@ from .utils import (seconds_to_timestr, parse_media_format, filter_response,
|
||||
from .matching import (get_best_combined_format, get_best_audio_format,
|
||||
get_best_video_format)
|
||||
from .mediaservers import PlexMediaServer
|
||||
from .fields import CommaSepChoiceField, SponsorBlock_Category
|
||||
from .fields import CommaSepChoiceField
|
||||
from .choices import (Val, CapChoices, Fallback, FileExtension,
|
||||
FilterSeconds, IndexSchedule, MediaServerType,
|
||||
MediaState, SourceResolution, SourceResolutionInteger,
|
||||
SponsorBlock_Category, YouTube_AudioCodec,
|
||||
YouTube_SourceType, YouTube_VideoCodec)
|
||||
|
||||
media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/')
|
||||
_srctype_dict = lambda n: dict(zip( YouTube_SourceType.values, (n,) * len(YouTube_SourceType.values) ))
|
||||
|
||||
class Source(models.Model):
|
||||
'''
|
||||
@ -35,87 +41,6 @@ class Source(models.Model):
|
||||
or a YouTube playlist.
|
||||
'''
|
||||
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL = 'c'
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL_ID = 'i'
|
||||
SOURCE_TYPE_YOUTUBE_PLAYLIST = 'p'
|
||||
SOURCE_TYPES = (SOURCE_TYPE_YOUTUBE_CHANNEL, SOURCE_TYPE_YOUTUBE_CHANNEL_ID,
|
||||
SOURCE_TYPE_YOUTUBE_PLAYLIST)
|
||||
SOURCE_TYPE_CHOICES = (
|
||||
(SOURCE_TYPE_YOUTUBE_CHANNEL, _('YouTube channel')),
|
||||
(SOURCE_TYPE_YOUTUBE_CHANNEL_ID, _('YouTube channel by ID')),
|
||||
(SOURCE_TYPE_YOUTUBE_PLAYLIST, _('YouTube playlist')),
|
||||
)
|
||||
|
||||
SOURCE_RESOLUTION_360P = '360p'
|
||||
SOURCE_RESOLUTION_480P = '480p'
|
||||
SOURCE_RESOLUTION_720P = '720p'
|
||||
SOURCE_RESOLUTION_1080P = '1080p'
|
||||
SOURCE_RESOLUTION_1440P = '1440p'
|
||||
SOURCE_RESOLUTION_2160P = '2160p'
|
||||
SOURCE_RESOLUTION_4320P = '4320p'
|
||||
SOURCE_RESOLUTION_AUDIO = 'audio'
|
||||
SOURCE_RESOLUTIONS = (SOURCE_RESOLUTION_360P, SOURCE_RESOLUTION_480P,
|
||||
SOURCE_RESOLUTION_720P, SOURCE_RESOLUTION_1080P,
|
||||
SOURCE_RESOLUTION_1440P, SOURCE_RESOLUTION_2160P,
|
||||
SOURCE_RESOLUTION_4320P, SOURCE_RESOLUTION_AUDIO)
|
||||
SOURCE_RESOLUTION_CHOICES = (
|
||||
(SOURCE_RESOLUTION_360P, _('360p (SD)')),
|
||||
(SOURCE_RESOLUTION_480P, _('480p (SD)')),
|
||||
(SOURCE_RESOLUTION_720P, _('720p (HD)')),
|
||||
(SOURCE_RESOLUTION_1080P, _('1080p (Full HD)')),
|
||||
(SOURCE_RESOLUTION_1440P, _('1440p (2K)')),
|
||||
(SOURCE_RESOLUTION_2160P, _('2160p (4K)')),
|
||||
(SOURCE_RESOLUTION_4320P, _('4320p (8K)')),
|
||||
(SOURCE_RESOLUTION_AUDIO, _('Audio only')),
|
||||
)
|
||||
RESOLUTION_MAP = {
|
||||
SOURCE_RESOLUTION_360P: 360,
|
||||
SOURCE_RESOLUTION_480P: 480,
|
||||
SOURCE_RESOLUTION_720P: 720,
|
||||
SOURCE_RESOLUTION_1080P: 1080,
|
||||
SOURCE_RESOLUTION_1440P: 1440,
|
||||
SOURCE_RESOLUTION_2160P: 2160,
|
||||
SOURCE_RESOLUTION_4320P: 4320,
|
||||
}
|
||||
|
||||
SOURCE_VCODEC_AVC1 = 'AVC1'
|
||||
SOURCE_VCODEC_VP9 = 'VP9'
|
||||
SOURCE_VCODECS = (SOURCE_VCODEC_AVC1, SOURCE_VCODEC_VP9)
|
||||
SOURCE_VCODECS_PRIORITY = (SOURCE_VCODEC_VP9, SOURCE_VCODEC_AVC1)
|
||||
SOURCE_VCODEC_CHOICES = (
|
||||
(SOURCE_VCODEC_AVC1, _('AVC1 (H.264)')),
|
||||
(SOURCE_VCODEC_VP9, _('VP9')),
|
||||
)
|
||||
|
||||
SOURCE_ACODEC_MP4A = 'MP4A'
|
||||
SOURCE_ACODEC_OPUS = 'OPUS'
|
||||
SOURCE_ACODECS = (SOURCE_ACODEC_MP4A, SOURCE_ACODEC_OPUS)
|
||||
SOURCE_ACODEC_PRIORITY = (SOURCE_ACODEC_OPUS, SOURCE_ACODEC_MP4A)
|
||||
SOURCE_ACODEC_CHOICES = (
|
||||
(SOURCE_ACODEC_MP4A, _('MP4A')),
|
||||
(SOURCE_ACODEC_OPUS, _('OPUS')),
|
||||
)
|
||||
|
||||
FALLBACK_FAIL = 'f'
|
||||
FALLBACK_NEXT_BEST = 'n'
|
||||
FALLBACK_NEXT_BEST_HD = 'h'
|
||||
FALLBACKS = (FALLBACK_FAIL, FALLBACK_NEXT_BEST, FALLBACK_NEXT_BEST_HD)
|
||||
FALLBACK_CHOICES = (
|
||||
(FALLBACK_FAIL, _('Fail, do not download any media')),
|
||||
(FALLBACK_NEXT_BEST, _('Get next best resolution or codec instead')),
|
||||
(FALLBACK_NEXT_BEST_HD, _('Get next best resolution but at least HD'))
|
||||
)
|
||||
|
||||
FILTER_SECONDS_CHOICES = (
|
||||
(True, _('Minimum Length')),
|
||||
(False, _('Maximum Length')),
|
||||
)
|
||||
|
||||
EXTENSION_M4A = 'm4a'
|
||||
EXTENSION_OGG = 'ogg'
|
||||
EXTENSION_MKV = 'mkv'
|
||||
EXTENSIONS = (EXTENSION_M4A, EXTENSION_OGG, EXTENSION_MKV)
|
||||
|
||||
sponsorblock_categories = CommaSepChoiceField(
|
||||
_(''),
|
||||
max_length=128,
|
||||
@ -143,60 +68,33 @@ class Source(models.Model):
|
||||
)
|
||||
|
||||
# Fontawesome icons used for the source on the front end
|
||||
ICONS = {
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL: '<i class="fab fa-youtube"></i>',
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL_ID: '<i class="fab fa-youtube"></i>',
|
||||
SOURCE_TYPE_YOUTUBE_PLAYLIST: '<i class="fab fa-youtube"></i>',
|
||||
}
|
||||
ICONS = _srctype_dict('<i class="fab fa-youtube"></i>')
|
||||
|
||||
# Format to use to display a URL for the source
|
||||
URLS = {
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/c/{key}',
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'https://www.youtube.com/channel/{key}',
|
||||
SOURCE_TYPE_YOUTUBE_PLAYLIST: 'https://www.youtube.com/playlist?list={key}',
|
||||
}
|
||||
URLS = dict(zip(
|
||||
YouTube_SourceType.values,
|
||||
(
|
||||
'https://www.youtube.com/c/{key}',
|
||||
'https://www.youtube.com/channel/{key}',
|
||||
'https://www.youtube.com/playlist?list={key}',
|
||||
),
|
||||
))
|
||||
|
||||
# Format used to create indexable URLs
|
||||
INDEX_URLS = {
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/c/{key}/{type}',
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'https://www.youtube.com/channel/{key}/{type}',
|
||||
SOURCE_TYPE_YOUTUBE_PLAYLIST: 'https://www.youtube.com/playlist?list={key}',
|
||||
}
|
||||
INDEX_URLS = dict(zip(
|
||||
YouTube_SourceType.values,
|
||||
(
|
||||
'https://www.youtube.com/c/{key}/{type}',
|
||||
'https://www.youtube.com/channel/{key}/{type}',
|
||||
'https://www.youtube.com/playlist?list={key}',
|
||||
),
|
||||
))
|
||||
|
||||
# Callback functions to get a list of media from the source
|
||||
INDEXERS = {
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL: get_youtube_media_info,
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL_ID: get_youtube_media_info,
|
||||
SOURCE_TYPE_YOUTUBE_PLAYLIST: get_youtube_media_info,
|
||||
}
|
||||
INDEXERS = _srctype_dict(get_youtube_media_info)
|
||||
|
||||
# Field names to find the media ID used as the key when storing media
|
||||
KEY_FIELD = {
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL: 'id',
|
||||
SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'id',
|
||||
SOURCE_TYPE_YOUTUBE_PLAYLIST: 'id',
|
||||
}
|
||||
|
||||
class CapChoices(models.IntegerChoices):
|
||||
CAP_NOCAP = 0, _('No cap')
|
||||
CAP_7DAYS = 604800, _('1 week (7 days)')
|
||||
CAP_30DAYS = 2592000, _('1 month (30 days)')
|
||||
CAP_90DAYS = 7776000, _('3 months (90 days)')
|
||||
CAP_6MONTHS = 15552000, _('6 months (180 days)')
|
||||
CAP_1YEAR = 31536000, _('1 year (365 days)')
|
||||
CAP_2YEARs = 63072000, _('2 years (730 days)')
|
||||
CAP_3YEARs = 94608000, _('3 years (1095 days)')
|
||||
CAP_5YEARs = 157680000, _('5 years (1825 days)')
|
||||
CAP_10YEARS = 315360000, _('10 years (3650 days)')
|
||||
|
||||
class IndexSchedule(models.IntegerChoices):
|
||||
EVERY_HOUR = 3600, _('Every hour')
|
||||
EVERY_2_HOURS = 7200, _('Every 2 hours')
|
||||
EVERY_3_HOURS = 10800, _('Every 3 hours')
|
||||
EVERY_4_HOURS = 14400, _('Every 4 hours')
|
||||
EVERY_5_HOURS = 18000, _('Every 5 hours')
|
||||
EVERY_6_HOURS = 21600, _('Every 6 hours')
|
||||
EVERY_12_HOURS = 43200, _('Every 12 hours')
|
||||
EVERY_24_HOURS = 86400, _('Every 24 hours')
|
||||
EVERY_3_DAYS = 259200, _('Every 3 days')
|
||||
EVERY_7_DAYS = 604800, _('Every 7 days')
|
||||
NEVER = 0, _('Never')
|
||||
KEY_FIELD = _srctype_dict('id')
|
||||
|
||||
uuid = models.UUIDField(
|
||||
_('uuid'),
|
||||
@ -222,8 +120,8 @@ class Source(models.Model):
|
||||
_('source type'),
|
||||
max_length=1,
|
||||
db_index=True,
|
||||
choices=SOURCE_TYPE_CHOICES,
|
||||
default=SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
choices=YouTube_SourceType.choices,
|
||||
default=YouTube_SourceType.CHANNEL,
|
||||
help_text=_('Source type')
|
||||
)
|
||||
key = models.CharField(
|
||||
@ -312,8 +210,8 @@ class Source(models.Model):
|
||||
)
|
||||
filter_seconds_min = models.BooleanField(
|
||||
_('filter seconds min/max'),
|
||||
choices=FILTER_SECONDS_CHOICES,
|
||||
default=True,
|
||||
choices=FilterSeconds.choices,
|
||||
default=Val(FilterSeconds.MIN),
|
||||
help_text=_('When Filter Seconds is > 0, do we skip on minimum (video shorter than limit) or maximum (video '
|
||||
'greater than maximum) video duration')
|
||||
)
|
||||
@ -331,24 +229,24 @@ class Source(models.Model):
|
||||
_('source resolution'),
|
||||
max_length=8,
|
||||
db_index=True,
|
||||
choices=SOURCE_RESOLUTION_CHOICES,
|
||||
default=SOURCE_RESOLUTION_1080P,
|
||||
choices=SourceResolution.choices,
|
||||
default=SourceResolution.VIDEO_1080P,
|
||||
help_text=_('Source resolution, desired video resolution to download')
|
||||
)
|
||||
source_vcodec = models.CharField(
|
||||
_('source video codec'),
|
||||
max_length=8,
|
||||
db_index=True,
|
||||
choices=SOURCE_VCODEC_CHOICES,
|
||||
default=SOURCE_VCODEC_VP9,
|
||||
choices=list(reversed(YouTube_VideoCodec.choices[1:])),
|
||||
default=YouTube_VideoCodec.VP9,
|
||||
help_text=_('Source video codec, desired video encoding format to download (ignored if "resolution" is audio only)')
|
||||
)
|
||||
source_acodec = models.CharField(
|
||||
_('source audio codec'),
|
||||
max_length=8,
|
||||
db_index=True,
|
||||
choices=SOURCE_ACODEC_CHOICES,
|
||||
default=SOURCE_ACODEC_OPUS,
|
||||
choices=list(reversed(YouTube_AudioCodec.choices)),
|
||||
default=YouTube_AudioCodec.OPUS,
|
||||
help_text=_('Source audio codec, desired audio encoding format to download')
|
||||
)
|
||||
prefer_60fps = models.BooleanField(
|
||||
@ -365,8 +263,8 @@ class Source(models.Model):
|
||||
_('fallback'),
|
||||
max_length=1,
|
||||
db_index=True,
|
||||
choices=FALLBACK_CHOICES,
|
||||
default=FALLBACK_NEXT_BEST_HD,
|
||||
choices=Fallback.choices,
|
||||
default=Fallback.NEXT_BEST_HD,
|
||||
help_text=_('What do do when media in your source resolution and codecs is not available')
|
||||
)
|
||||
copy_channel_images = models.BooleanField(
|
||||
@ -437,7 +335,11 @@ class Source(models.Model):
|
||||
|
||||
@property
|
||||
def is_audio(self):
|
||||
return self.source_resolution == self.SOURCE_RESOLUTION_AUDIO
|
||||
return self.source_resolution == SourceResolution.AUDIO.value
|
||||
|
||||
@property
|
||||
def is_playlist(self):
|
||||
return self.source_type == YouTube_SourceType.PLAYLIST.value
|
||||
|
||||
@property
|
||||
def is_video(self):
|
||||
@ -469,14 +371,14 @@ class Source(models.Model):
|
||||
depending on audio codec.
|
||||
'''
|
||||
if self.is_audio:
|
||||
if self.source_acodec == self.SOURCE_ACODEC_MP4A:
|
||||
return self.EXTENSION_M4A
|
||||
elif self.source_acodec == self.SOURCE_ACODEC_OPUS:
|
||||
return self.EXTENSION_OGG
|
||||
if self.source_acodec == Val(YouTube_AudioCodec.MP4A):
|
||||
return Val(FileExtension.M4A)
|
||||
elif self.source_acodec == Val(YouTube_AudioCodec.OPUS):
|
||||
return Val(FileExtension.OGG)
|
||||
else:
|
||||
raise ValueError('Unable to choose audio extension, uknown acodec')
|
||||
else:
|
||||
return self.EXTENSION_MKV
|
||||
return Val(FileExtension.MKV)
|
||||
|
||||
@classmethod
|
||||
def create_url(obj, source_type, key):
|
||||
@ -497,7 +399,7 @@ class Source(models.Model):
|
||||
|
||||
@property
|
||||
def format_summary(self):
|
||||
if self.source_resolution == Source.SOURCE_RESOLUTION_AUDIO:
|
||||
if self.is_audio:
|
||||
vc = 'none'
|
||||
else:
|
||||
vc = self.source_vcodec
|
||||
@ -514,7 +416,7 @@ class Source(models.Model):
|
||||
@property
|
||||
def type_directory_path(self):
|
||||
if settings.SOURCE_DOWNLOAD_DIRECTORY_PREFIX:
|
||||
if self.source_resolution == self.SOURCE_RESOLUTION_AUDIO:
|
||||
if self.is_audio:
|
||||
return Path(settings.DOWNLOAD_AUDIO_DIR) / self.directory
|
||||
else:
|
||||
return Path(settings.DOWNLOAD_VIDEO_DIR) / self.directory
|
||||
@ -526,7 +428,7 @@ class Source(models.Model):
|
||||
|
||||
@property
|
||||
def get_image_url(self):
|
||||
if self.source_type == self.SOURCE_TYPE_YOUTUBE_PLAYLIST:
|
||||
if self.is_playlist:
|
||||
raise SuspiciousOperation('This source is a playlist so it doesn\'t have thumbnail.')
|
||||
|
||||
return get_youtube_channel_image_info(self.url)
|
||||
@ -542,11 +444,11 @@ class Source(models.Model):
|
||||
|
||||
@property
|
||||
def source_resolution_height(self):
|
||||
return self.RESOLUTION_MAP.get(self.source_resolution, 0)
|
||||
return SourceResolutionInteger.get(self.source_resolution, 0)
|
||||
|
||||
@property
|
||||
def can_fallback(self):
|
||||
return self.fallback != self.FALLBACK_FAIL
|
||||
return self.fallback != Val(Fallback.FAIL)
|
||||
|
||||
@property
|
||||
def example_media_format_dict(self):
|
||||
@ -620,7 +522,7 @@ class Source(models.Model):
|
||||
if self.index_videos:
|
||||
entries += self.get_index('videos')
|
||||
# Playlists do something different that I have yet to figure out
|
||||
if self.source_type != Source.SOURCE_TYPE_YOUTUBE_PLAYLIST:
|
||||
if not self.is_playlist:
|
||||
if self.index_streams:
|
||||
entries += self.get_index('streams')
|
||||
|
||||
@ -646,109 +548,42 @@ class Media(models.Model):
|
||||
'''
|
||||
|
||||
# Format to use to display a URL for the media
|
||||
URLS = {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/watch?v={key}',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'https://www.youtube.com/watch?v={key}',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'https://www.youtube.com/watch?v={key}',
|
||||
}
|
||||
URLS = _srctype_dict('https://www.youtube.com/watch?v={key}')
|
||||
|
||||
# Callback functions to get a list of media from the source
|
||||
INDEXERS = {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: get_youtube_media_info,
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: get_youtube_media_info,
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: get_youtube_media_info,
|
||||
}
|
||||
INDEXERS = _srctype_dict(get_youtube_media_info)
|
||||
|
||||
# Maps standardised names to names used in source metdata
|
||||
_same_name = lambda n, k=None: {k or n: _srctype_dict(n) }
|
||||
METADATA_FIELDS = {
|
||||
'upload_date': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'upload_date',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'upload_date',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'upload_date',
|
||||
},
|
||||
'timestamp': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'timestamp',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'timestamp',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'timestamp',
|
||||
},
|
||||
'title': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'title',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'title',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'title',
|
||||
},
|
||||
'thumbnail': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'thumbnail',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'thumbnail',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'thumbnail',
|
||||
},
|
||||
'description': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'description',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'description',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'description',
|
||||
},
|
||||
'duration': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'duration',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'duration',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'duration',
|
||||
},
|
||||
'formats': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'formats',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'formats',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'formats',
|
||||
},
|
||||
'categories': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'categories',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'categories',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'categories',
|
||||
},
|
||||
'rating': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'average_rating',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'average_rating',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'average_rating',
|
||||
},
|
||||
'age_limit': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'age_limit',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'age_limit',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'age_limit',
|
||||
},
|
||||
'uploader': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'uploader',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'uploader',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'uploader',
|
||||
},
|
||||
'upvotes': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'like_count',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'like_count',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'like_count',
|
||||
},
|
||||
'downvotes': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'dislike_count',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'dislike_count',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'dislike_count',
|
||||
},
|
||||
'playlist_title': {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'playlist_title',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: 'playlist_title',
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: 'playlist_title',
|
||||
},
|
||||
}
|
||||
STATE_UNKNOWN = 'unknown'
|
||||
STATE_SCHEDULED = 'scheduled'
|
||||
STATE_DOWNLOADING = 'downloading'
|
||||
STATE_DOWNLOADED = 'downloaded'
|
||||
STATE_SKIPPED = 'skipped'
|
||||
STATE_DISABLED_AT_SOURCE = 'source-disabled'
|
||||
STATE_ERROR = 'error'
|
||||
STATES = (STATE_UNKNOWN, STATE_SCHEDULED, STATE_DOWNLOADING, STATE_DOWNLOADED,
|
||||
STATE_SKIPPED, STATE_DISABLED_AT_SOURCE, STATE_ERROR)
|
||||
STATE_ICONS = {
|
||||
STATE_UNKNOWN: '<i class="far fa-question-circle" title="Unknown download state"></i>',
|
||||
STATE_SCHEDULED: '<i class="far fa-clock" title="Scheduled to download"></i>',
|
||||
STATE_DOWNLOADING: '<i class="fas fa-download" title="Downloading now"></i>',
|
||||
STATE_DOWNLOADED: '<i class="far fa-check-circle" title="Downloaded"></i>',
|
||||
STATE_SKIPPED: '<i class="fas fa-exclamation-circle" title="Skipped"></i>',
|
||||
STATE_DISABLED_AT_SOURCE: '<i class="fas fa-stop-circle" title="Media downloading disabled at source"></i>',
|
||||
STATE_ERROR: '<i class="fas fa-exclamation-triangle" title="Error downloading"></i>',
|
||||
**(_same_name('upload_date')),
|
||||
**(_same_name('timestamp')),
|
||||
**(_same_name('title')),
|
||||
**(_same_name('description')),
|
||||
**(_same_name('duration')),
|
||||
**(_same_name('formats')),
|
||||
**(_same_name('categories')),
|
||||
**(_same_name('average_rating', 'rating')),
|
||||
**(_same_name('age_limit')),
|
||||
**(_same_name('uploader')),
|
||||
**(_same_name('like_count', 'upvotes')),
|
||||
**(_same_name('dislike_count', 'downvotes')),
|
||||
**(_same_name('playlist_title')),
|
||||
}
|
||||
|
||||
STATE_ICONS = dict(zip(
|
||||
MediaState.values,
|
||||
(
|
||||
'<i class="far fa-question-circle" title="Unknown download state"></i>',
|
||||
'<i class="far fa-clock" title="Scheduled to download"></i>',
|
||||
'<i class="fas fa-download" title="Downloading now"></i>',
|
||||
'<i class="far fa-check-circle" title="Downloaded"></i>',
|
||||
'<i class="fas fa-exclamation-circle" title="Skipped"></i>',
|
||||
'<i class="fas fa-stop-circle" title="Media downloading disabled at source"></i>',
|
||||
'<i class="fas fa-exclamation-triangle" title="Error downloading"></i>',
|
||||
)
|
||||
))
|
||||
|
||||
uuid = models.UUIDField(
|
||||
_('uuid'),
|
||||
primary_key=True,
|
||||
@ -1028,12 +863,12 @@ class Media(models.Model):
|
||||
resolution = self.downloaded_format.lower()
|
||||
elif self.downloaded_height:
|
||||
resolution = f'{self.downloaded_height}p'
|
||||
if self.downloaded_format != 'audio':
|
||||
if self.downloaded_format != Val(SourceResolution.AUDIO):
|
||||
vcodec = self.downloaded_video_codec.lower()
|
||||
fmt.append(vcodec)
|
||||
acodec = self.downloaded_audio_codec.lower()
|
||||
fmt.append(acodec)
|
||||
if self.downloaded_format != 'audio':
|
||||
if self.downloaded_format != Val(SourceResolution.AUDIO):
|
||||
fps = str(self.downloaded_fps)
|
||||
fmt.append(f'{fps}fps')
|
||||
if self.downloaded_hdr:
|
||||
@ -1357,19 +1192,19 @@ class Media(models.Model):
|
||||
acodec = self.downloaded_audio_codec
|
||||
if acodec is None:
|
||||
raise TypeError() # nothing here.
|
||||
acodec = acodec.lower()
|
||||
if acodec == "mp4a":
|
||||
acodec = acodec.upper()
|
||||
if acodec == Val(YouTube_AudioCodec.MP4A):
|
||||
return "audio/mp4"
|
||||
elif acodec == "opus":
|
||||
elif acodec == Val(YouTube_AudioCodec.OPUS):
|
||||
return "audio/opus"
|
||||
else:
|
||||
# fall-fall-back.
|
||||
return 'audio/ogg'
|
||||
vcodec = vcodec.lower()
|
||||
if vcodec == 'vp9':
|
||||
return 'video/webm'
|
||||
else:
|
||||
vcodec = vcodec.upper()
|
||||
if vcodec == Val(YouTube_VideoCodec.AVC1):
|
||||
return 'video/mp4'
|
||||
else:
|
||||
return 'video/matroska'
|
||||
|
||||
@property
|
||||
def nfoxml(self):
|
||||
@ -1390,7 +1225,7 @@ class Media(models.Model):
|
||||
nfo.append(showtitle)
|
||||
# season = upload date year
|
||||
season = nfo.makeelement('season', {})
|
||||
if self.source.source_type == Source.SOURCE_TYPE_YOUTUBE_PLAYLIST:
|
||||
if self.source.is_playlist:
|
||||
# If it's a playlist, set season to 1
|
||||
season.text = '1'
|
||||
else:
|
||||
@ -1487,23 +1322,23 @@ class Media(models.Model):
|
||||
|
||||
def get_download_state(self, task=None):
|
||||
if self.downloaded:
|
||||
return self.STATE_DOWNLOADED
|
||||
return Val(MediaState.DOWNLOADED)
|
||||
if task:
|
||||
if task.locked_by_pid_running():
|
||||
return self.STATE_DOWNLOADING
|
||||
return Val(MediaState.DOWNLOADING)
|
||||
elif task.has_error():
|
||||
return self.STATE_ERROR
|
||||
return Val(MediaState.ERROR)
|
||||
else:
|
||||
return self.STATE_SCHEDULED
|
||||
return Val(MediaState.SCHEDULED)
|
||||
if self.skip:
|
||||
return self.STATE_SKIPPED
|
||||
return Val(MediaState.SKIPPED)
|
||||
if not self.source.download_media:
|
||||
return self.STATE_DISABLED_AT_SOURCE
|
||||
return self.STATE_UNKNOWN
|
||||
return Val(MediaState.DISABLED_AT_SOURCE)
|
||||
return Val(MediaState.UNKNOWN)
|
||||
|
||||
def get_download_state_icon(self, task=None):
|
||||
state = self.get_download_state(task)
|
||||
return self.STATE_ICONS.get(state, self.STATE_ICONS[self.STATE_UNKNOWN])
|
||||
return self.STATE_ICONS.get(state, self.STATE_ICONS[Val(MediaState.UNKNOWN)])
|
||||
|
||||
def download_media(self):
|
||||
format_str = self.get_format_str()
|
||||
@ -1539,7 +1374,7 @@ class Media(models.Model):
|
||||
return response
|
||||
|
||||
def calculate_episode_number(self):
|
||||
if self.source.source_type == Source.SOURCE_TYPE_YOUTUBE_PLAYLIST:
|
||||
if self.source.is_playlist:
|
||||
sorted_media = Media.objects.filter(source=self.source)
|
||||
else:
|
||||
self_year = self.upload_date.year if self.upload_date else self.created.year
|
||||
@ -1643,24 +1478,19 @@ class MediaServer(models.Model):
|
||||
A remote media server, such as a Plex server.
|
||||
'''
|
||||
|
||||
SERVER_TYPE_PLEX = 'p'
|
||||
SERVER_TYPES = (SERVER_TYPE_PLEX,)
|
||||
SERVER_TYPE_CHOICES = (
|
||||
(SERVER_TYPE_PLEX, _('Plex')),
|
||||
)
|
||||
ICONS = {
|
||||
SERVER_TYPE_PLEX: '<i class="fas fa-server"></i>',
|
||||
Val(MediaServerType.PLEX): '<i class="fas fa-server"></i>',
|
||||
}
|
||||
HANDLERS = {
|
||||
SERVER_TYPE_PLEX: PlexMediaServer,
|
||||
Val(MediaServerType.PLEX): PlexMediaServer,
|
||||
}
|
||||
|
||||
server_type = models.CharField(
|
||||
_('server type'),
|
||||
max_length=1,
|
||||
db_index=True,
|
||||
choices=SERVER_TYPE_CHOICES,
|
||||
default=SERVER_TYPE_PLEX,
|
||||
choices=MediaServerType.choices,
|
||||
default=MediaServerType.PLEX,
|
||||
help_text=_('Server type')
|
||||
)
|
||||
host = models.CharField(
|
||||
|
@ -17,6 +17,7 @@ from .tasks import (delete_task_by_source, delete_task_by_media, index_source_ta
|
||||
get_media_metadata_task, get_media_download_task)
|
||||
from .utils import delete_file, glob_quote
|
||||
from .filtering import filter_media
|
||||
from .choices import Val, YouTube_SourceType
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Source)
|
||||
@ -52,7 +53,7 @@ def source_post_save(sender, instance, created, **kwargs):
|
||||
priority=0,
|
||||
verbose_name=verbose_name.format(instance.name)
|
||||
)
|
||||
if instance.source_type != Source.SOURCE_TYPE_YOUTUBE_PLAYLIST and instance.copy_channel_images:
|
||||
if instance.source_type != Val(YouTube_SourceType.PLAYLIST) and instance.copy_channel_images:
|
||||
download_source_images(
|
||||
str(instance.pk),
|
||||
priority=2,
|
||||
|
@ -19,6 +19,9 @@ from .models import Source, Media
|
||||
from .tasks import cleanup_old_media
|
||||
from .filtering import filter_media
|
||||
from .utils import filter_response
|
||||
from .choices import (Val, Fallback, IndexSchedule, SourceResolution,
|
||||
YouTube_AudioCodec, YouTube_VideoCodec,
|
||||
YouTube_SourceType, youtube_long_source_types)
|
||||
|
||||
|
||||
class FrontEndTestCase(TestCase):
|
||||
@ -33,11 +36,6 @@ class FrontEndTestCase(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_validate_source(self):
|
||||
test_source_types = {
|
||||
'youtube-channel': Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
'youtube-channel-id': Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID,
|
||||
'youtube-playlist': Source.SOURCE_TYPE_YOUTUBE_PLAYLIST,
|
||||
}
|
||||
test_sources = {
|
||||
'youtube-channel': {
|
||||
'valid': (
|
||||
@ -121,7 +119,7 @@ class FrontEndTestCase(TestCase):
|
||||
}
|
||||
}
|
||||
c = Client()
|
||||
for source_type in test_sources.keys():
|
||||
for source_type in youtube_long_source_types.keys():
|
||||
response = c.get(f'/source-validate/{source_type}')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = c.get('/source-validate/invalid')
|
||||
@ -129,7 +127,7 @@ class FrontEndTestCase(TestCase):
|
||||
for (source_type, tests) in test_sources.items():
|
||||
for test, urls in tests.items():
|
||||
for url in urls:
|
||||
source_type_char = test_source_types.get(source_type)
|
||||
source_type_char = youtube_long_source_types.get(source_type)
|
||||
data = {'source_url': url, 'source_type': source_type_char}
|
||||
response = c.post(f'/source-validate/{source_type}', data)
|
||||
if test == 'valid':
|
||||
@ -182,7 +180,7 @@ class FrontEndTestCase(TestCase):
|
||||
'media_format': settings.MEDIA_FORMATSTR_DEFAULT,
|
||||
'download_cap': 0,
|
||||
'filter_text': '.*',
|
||||
'filter_seconds_min': True,
|
||||
'filter_seconds_min': int(True),
|
||||
'index_schedule': 3600,
|
||||
'delete_old_media': False,
|
||||
'days_to_keep': 14,
|
||||
@ -231,23 +229,23 @@ class FrontEndTestCase(TestCase):
|
||||
expected_categories)
|
||||
# Update the source key
|
||||
data = {
|
||||
'source_type': Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
'source_type': Val(YouTube_SourceType.CHANNEL),
|
||||
'key': 'updatedkey', # changed
|
||||
'name': 'testname',
|
||||
'directory': 'testdirectory',
|
||||
'media_format': settings.MEDIA_FORMATSTR_DEFAULT,
|
||||
'download_cap': 0,
|
||||
'filter_text': '.*',
|
||||
'filter_seconds_min': True,
|
||||
'index_schedule': Source.IndexSchedule.EVERY_HOUR,
|
||||
'filter_seconds_min': int(True),
|
||||
'index_schedule': Val(IndexSchedule.EVERY_HOUR),
|
||||
'delete_old_media': False,
|
||||
'days_to_keep': 14,
|
||||
'source_resolution': Source.SOURCE_RESOLUTION_1080P,
|
||||
'source_vcodec': Source.SOURCE_VCODEC_VP9,
|
||||
'source_acodec': Source.SOURCE_ACODEC_OPUS,
|
||||
'source_resolution': Val(SourceResolution.VIDEO_1080P),
|
||||
'source_vcodec': Val(YouTube_VideoCodec.VP9),
|
||||
'source_acodec': Val(YouTube_AudioCodec.OPUS),
|
||||
'prefer_60fps': False,
|
||||
'prefer_hdr': False,
|
||||
'fallback': Source.FALLBACK_FAIL,
|
||||
'fallback': Val(Fallback.FAIL),
|
||||
'sponsorblock_categories': data_categories,
|
||||
'sub_langs': 'en',
|
||||
}
|
||||
@ -268,23 +266,23 @@ class FrontEndTestCase(TestCase):
|
||||
expected_categories)
|
||||
# Update the source index schedule which should recreate the scheduled task
|
||||
data = {
|
||||
'source_type': Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
'source_type': Val(YouTube_SourceType.CHANNEL),
|
||||
'key': 'updatedkey',
|
||||
'name': 'testname',
|
||||
'directory': 'testdirectory',
|
||||
'media_format': settings.MEDIA_FORMATSTR_DEFAULT,
|
||||
'download_cap': 0,
|
||||
'filter_text': '.*',
|
||||
'filter_seconds_min': True,
|
||||
'index_schedule': Source.IndexSchedule.EVERY_2_HOURS, # changed
|
||||
'filter_seconds_min': int(True),
|
||||
'index_schedule': Val(IndexSchedule.EVERY_2_HOURS), # changed
|
||||
'delete_old_media': False,
|
||||
'days_to_keep': 14,
|
||||
'source_resolution': Source.SOURCE_RESOLUTION_1080P,
|
||||
'source_vcodec': Source.SOURCE_VCODEC_VP9,
|
||||
'source_acodec': Source.SOURCE_ACODEC_OPUS,
|
||||
'source_resolution': Val(SourceResolution.VIDEO_1080P),
|
||||
'source_vcodec': Val(YouTube_VideoCodec.VP9),
|
||||
'source_acodec': Val(YouTube_AudioCodec.OPUS),
|
||||
'prefer_60fps': False,
|
||||
'prefer_hdr': False,
|
||||
'fallback': Source.FALLBACK_FAIL,
|
||||
'fallback': Val(Fallback.FAIL),
|
||||
'sponsorblock_categories': data_categories,
|
||||
'sub_langs': 'en',
|
||||
}
|
||||
@ -338,19 +336,19 @@ class FrontEndTestCase(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Add a test source
|
||||
test_source = Source.objects.create(
|
||||
source_type=Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
source_type=Val(YouTube_SourceType.CHANNEL),
|
||||
key='testkey',
|
||||
name='testname',
|
||||
directory='testdirectory',
|
||||
index_schedule=Source.IndexSchedule.EVERY_HOUR,
|
||||
index_schedule=Val(IndexSchedule.EVERY_HOUR),
|
||||
delete_old_media=False,
|
||||
days_to_keep=14,
|
||||
source_resolution=Source.SOURCE_RESOLUTION_1080P,
|
||||
source_vcodec=Source.SOURCE_VCODEC_VP9,
|
||||
source_acodec=Source.SOURCE_ACODEC_OPUS,
|
||||
source_resolution=Val(SourceResolution.VIDEO_1080P),
|
||||
source_vcodec=Val(YouTube_VideoCodec.VP9),
|
||||
source_acodec=Val(YouTube_AudioCodec.OPUS),
|
||||
prefer_60fps=False,
|
||||
prefer_hdr=False,
|
||||
fallback=Source.FALLBACK_FAIL
|
||||
fallback=Val(Fallback.FAIL)
|
||||
)
|
||||
# Add some media
|
||||
test_minimal_metadata = '''
|
||||
@ -519,7 +517,7 @@ class FilepathTestCase(TestCase):
|
||||
logging.disable(logging.CRITICAL)
|
||||
# Add a test source
|
||||
self.source = Source.objects.create(
|
||||
source_type=Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
source_type=Val(YouTube_SourceType.CHANNEL),
|
||||
key='testkey',
|
||||
name='testname',
|
||||
directory='testdirectory',
|
||||
@ -527,12 +525,12 @@ class FilepathTestCase(TestCase):
|
||||
index_schedule=3600,
|
||||
delete_old_media=False,
|
||||
days_to_keep=14,
|
||||
source_resolution=Source.SOURCE_RESOLUTION_1080P,
|
||||
source_vcodec=Source.SOURCE_VCODEC_VP9,
|
||||
source_acodec=Source.SOURCE_ACODEC_OPUS,
|
||||
source_resolution=Val(SourceResolution.VIDEO_1080P),
|
||||
source_vcodec=Val(YouTube_VideoCodec.VP9),
|
||||
source_acodec=Val(YouTube_AudioCodec.OPUS),
|
||||
prefer_60fps=False,
|
||||
prefer_hdr=False,
|
||||
fallback=Source.FALLBACK_FAIL
|
||||
fallback=Val(Fallback.FAIL)
|
||||
)
|
||||
# Add some test media
|
||||
self.media = Media.objects.create(
|
||||
@ -658,11 +656,11 @@ class FilepathTestCase(TestCase):
|
||||
self.assertTrue(isinstance(settings.SOURCE_DOWNLOAD_DIRECTORY_PREFIX, bool))
|
||||
# Test the default behavior for "True", forced "audio" or "video" parent directories for sources
|
||||
settings.SOURCE_DOWNLOAD_DIRECTORY_PREFIX = True
|
||||
self.source.source_resolution = Source.SOURCE_RESOLUTION_AUDIO
|
||||
self.source.source_resolution = Val(SourceResolution.AUDIO)
|
||||
test_audio_prefix_path = Path(self.source.directory_path)
|
||||
self.assertEqual(test_audio_prefix_path.parts[-2], 'audio')
|
||||
self.assertEqual(test_audio_prefix_path.parts[-1], 'testdirectory')
|
||||
self.source.source_resolution = Source.SOURCE_RESOLUTION_1080P
|
||||
self.source.source_resolution = Val(SourceResolution.VIDEO_1080P)
|
||||
test_video_prefix_path = Path(self.source.directory_path)
|
||||
self.assertEqual(test_video_prefix_path.parts[-2], 'video')
|
||||
self.assertEqual(test_video_prefix_path.parts[-1], 'testdirectory')
|
||||
@ -679,7 +677,7 @@ class MediaTestCase(TestCase):
|
||||
logging.disable(logging.CRITICAL)
|
||||
# Add a test source
|
||||
self.source = Source.objects.create(
|
||||
source_type=Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
source_type=Val(YouTube_SourceType.CHANNEL),
|
||||
key='testkey',
|
||||
name='testname',
|
||||
directory='testdirectory',
|
||||
@ -687,12 +685,12 @@ class MediaTestCase(TestCase):
|
||||
index_schedule=3600,
|
||||
delete_old_media=False,
|
||||
days_to_keep=14,
|
||||
source_resolution=Source.SOURCE_RESOLUTION_1080P,
|
||||
source_vcodec=Source.SOURCE_VCODEC_VP9,
|
||||
source_acodec=Source.SOURCE_ACODEC_OPUS,
|
||||
source_resolution=Val(SourceResolution.VIDEO_1080P),
|
||||
source_vcodec=Val(YouTube_VideoCodec.VP9),
|
||||
source_acodec=Val(YouTube_AudioCodec.OPUS),
|
||||
prefer_60fps=False,
|
||||
prefer_hdr=False,
|
||||
fallback=Source.FALLBACK_FAIL
|
||||
fallback=Val(Fallback.FAIL)
|
||||
)
|
||||
# Add some test media
|
||||
self.media = Media.objects.create(
|
||||
@ -752,7 +750,7 @@ class MediaFilterTestCase(TestCase):
|
||||
# logging.disable(logging.CRITICAL)
|
||||
# Add a test source
|
||||
self.source = Source.objects.create(
|
||||
source_type=Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
source_type=Val(YouTube_SourceType.CHANNEL),
|
||||
key="testkey",
|
||||
name="testname",
|
||||
directory="testdirectory",
|
||||
@ -760,12 +758,12 @@ class MediaFilterTestCase(TestCase):
|
||||
index_schedule=3600,
|
||||
delete_old_media=False,
|
||||
days_to_keep=14,
|
||||
source_resolution=Source.SOURCE_RESOLUTION_1080P,
|
||||
source_vcodec=Source.SOURCE_VCODEC_VP9,
|
||||
source_acodec=Source.SOURCE_ACODEC_OPUS,
|
||||
source_resolution=Val(SourceResolution.VIDEO_1080P),
|
||||
source_vcodec=Val(YouTube_VideoCodec.VP9),
|
||||
source_acodec=Val(YouTube_AudioCodec.OPUS),
|
||||
prefer_60fps=False,
|
||||
prefer_hdr=False,
|
||||
fallback=Source.FALLBACK_FAIL,
|
||||
fallback=Val(Fallback.FAIL),
|
||||
)
|
||||
# Add some test media
|
||||
self.media = Media.objects.create(
|
||||
@ -922,19 +920,19 @@ class FormatMatchingTestCase(TestCase):
|
||||
logging.disable(logging.CRITICAL)
|
||||
# Add a test source
|
||||
self.source = Source.objects.create(
|
||||
source_type=Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
source_type=Val(YouTube_SourceType.CHANNEL),
|
||||
key='testkey',
|
||||
name='testname',
|
||||
directory='testdirectory',
|
||||
index_schedule=3600,
|
||||
delete_old_media=False,
|
||||
days_to_keep=14,
|
||||
source_resolution=Source.SOURCE_RESOLUTION_1080P,
|
||||
source_vcodec=Source.SOURCE_VCODEC_VP9,
|
||||
source_acodec=Source.SOURCE_ACODEC_OPUS,
|
||||
source_resolution=Val(SourceResolution.VIDEO_1080P),
|
||||
source_vcodec=Val(YouTube_VideoCodec.VP9),
|
||||
source_acodec=Val(YouTube_AudioCodec.OPUS),
|
||||
prefer_60fps=False,
|
||||
prefer_hdr=False,
|
||||
fallback=Source.FALLBACK_FAIL
|
||||
fallback=Val(Fallback.FAIL)
|
||||
)
|
||||
# Add some media
|
||||
self.media = Media.objects.create(
|
||||
@ -944,7 +942,7 @@ class FormatMatchingTestCase(TestCase):
|
||||
)
|
||||
|
||||
def test_combined_exact_format_matching(self):
|
||||
self.source.fallback = Source.FALLBACK_FAIL
|
||||
self.source.fallback = Val(Fallback.FAIL)
|
||||
self.media.metadata = all_test_metadata['boring']
|
||||
self.media.save()
|
||||
expected_matches = {
|
||||
@ -1074,7 +1072,7 @@ class FormatMatchingTestCase(TestCase):
|
||||
self.assertEqual(match_type, expected_match_type)
|
||||
|
||||
def test_audio_exact_format_matching(self):
|
||||
self.source.fallback = Source.FALLBACK_FAIL
|
||||
self.source.fallback = Val(Fallback.FAIL)
|
||||
self.media.metadata = all_test_metadata['boring']
|
||||
self.media.save()
|
||||
expected_matches = {
|
||||
@ -1220,7 +1218,7 @@ class FormatMatchingTestCase(TestCase):
|
||||
self.assertEqual(match_type, expeceted_match_type)
|
||||
|
||||
def test_video_exact_format_matching(self):
|
||||
self.source.fallback = Source.FALLBACK_FAIL
|
||||
self.source.fallback = Val(Fallback.FAIL)
|
||||
# Test no 60fps, no HDR metadata
|
||||
self.media.metadata = all_test_metadata['boring']
|
||||
self.media.save()
|
||||
@ -1430,7 +1428,7 @@ class FormatMatchingTestCase(TestCase):
|
||||
self.assertEqual(match_type, expeceted_match_type)
|
||||
|
||||
def test_video_next_best_format_matching(self):
|
||||
self.source.fallback = Source.FALLBACK_NEXT_BEST
|
||||
self.source.fallback = Val(Fallback.NEXT_BEST)
|
||||
# Test no 60fps, no HDR metadata
|
||||
self.media.metadata = all_test_metadata['boring']
|
||||
self.media.save()
|
||||
@ -1740,19 +1738,19 @@ class ResponseFilteringTestCase(TestCase):
|
||||
logging.disable(logging.CRITICAL)
|
||||
# Add a test source
|
||||
self.source = Source.objects.create(
|
||||
source_type=Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
source_type=Val(YouTube_SourceType.CHANNEL),
|
||||
key='testkey',
|
||||
name='testname',
|
||||
directory='testdirectory',
|
||||
index_schedule=3600,
|
||||
delete_old_media=False,
|
||||
days_to_keep=14,
|
||||
source_resolution=Source.SOURCE_RESOLUTION_1080P,
|
||||
source_vcodec=Source.SOURCE_VCODEC_VP9,
|
||||
source_acodec=Source.SOURCE_ACODEC_OPUS,
|
||||
source_resolution=Val(SourceResolution.VIDEO_1080P),
|
||||
source_vcodec=Val(YouTube_VideoCodec.VP9),
|
||||
source_acodec=Val(YouTube_AudioCodec.OPUS),
|
||||
prefer_60fps=False,
|
||||
prefer_hdr=False,
|
||||
fallback=Source.FALLBACK_FAIL
|
||||
fallback=Val(Fallback.FAIL)
|
||||
)
|
||||
# Add some media
|
||||
self.media = Media.objects.create(
|
||||
|
@ -31,6 +31,9 @@ from .utils import validate_url, delete_file
|
||||
from .tasks import (map_task_to_instance, get_error_message,
|
||||
get_source_completed_tasks, get_media_download_task,
|
||||
delete_task_by_media, index_source_task)
|
||||
from .choices import (Val, MediaServerType, SourceResolution,
|
||||
YouTube_SourceType, youtube_long_source_types,
|
||||
youtube_help, youtube_validation_urls)
|
||||
from . import signals
|
||||
from . import youtube
|
||||
|
||||
@ -48,7 +51,7 @@ class DashboardView(TemplateView):
|
||||
# Sources
|
||||
data['num_sources'] = Source.objects.all().count()
|
||||
data['num_video_sources'] = Source.objects.filter(
|
||||
~Q(source_resolution=Source.SOURCE_RESOLUTION_AUDIO)
|
||||
~Q(source_resolution=Val(SourceResolution.AUDIO))
|
||||
).count()
|
||||
data['num_audio_sources'] = data['num_sources'] - data['num_video_sources']
|
||||
data['num_failed_sources'] = Source.objects.filter(has_failed=True).count()
|
||||
@ -161,82 +164,15 @@ class ValidateSourceView(FormView):
|
||||
'invalid_url': _('Invalid URL, the URL must for a "{item}" must be in '
|
||||
'the format of "{example}". The error was: {error}.'),
|
||||
}
|
||||
source_types = {
|
||||
'youtube-channel': Source.SOURCE_TYPE_YOUTUBE_CHANNEL,
|
||||
'youtube-channel-id': Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID,
|
||||
'youtube-playlist': Source.SOURCE_TYPE_YOUTUBE_PLAYLIST,
|
||||
}
|
||||
help_item = {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: _('YouTube channel'),
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: _('YouTube channel ID'),
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: _('YouTube playlist'),
|
||||
}
|
||||
help_texts = {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: _(
|
||||
'Enter a YouTube channel URL into the box below. A channel URL will be in '
|
||||
'the format of <strong>https://www.youtube.com/CHANNELNAME</strong> '
|
||||
'where <strong>CHANNELNAME</strong> is the name of the channel you want '
|
||||
'to add.'
|
||||
),
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: _(
|
||||
'Enter a YouTube channel URL by channel ID into the box below. A channel '
|
||||
'URL by channel ID will be in the format of <strong>'
|
||||
'https://www.youtube.com/channel/BiGLoNgUnIqUeId</strong> '
|
||||
'where <strong>BiGLoNgUnIqUeId</strong> is the ID of the channel you want '
|
||||
'to add.'
|
||||
),
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: _(
|
||||
'Enter a YouTube playlist URL into the box below. A playlist URL will be '
|
||||
'in the format of <strong>https://www.youtube.com/playlist?list='
|
||||
'BiGLoNgUnIqUeId</strong> where <strong>BiGLoNgUnIqUeId</strong> is the '
|
||||
'unique ID of the playlist you want to add.'
|
||||
),
|
||||
}
|
||||
help_examples = {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: 'https://www.youtube.com/google',
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: ('https://www.youtube.com/channel/'
|
||||
'UCK8sQmJBp8GCxrOtXWBpyEA'),
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: ('https://www.youtube.com/playlist?list='
|
||||
'PL590L5WQmH8dpP0RyH5pCfIaDEdt9nk7r')
|
||||
}
|
||||
_youtube_domains = frozenset({
|
||||
'youtube.com',
|
||||
'm.youtube.com',
|
||||
'www.youtube.com',
|
||||
})
|
||||
validation_urls = {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: {
|
||||
'scheme': 'https',
|
||||
'domains': _youtube_domains,
|
||||
'path_regex': '^\/(c\/)?([^\/]+)(\/videos)?$',
|
||||
'path_must_not_match': ('/playlist', '/c/playlist'),
|
||||
'qs_args': [],
|
||||
'extract_key': ('path_regex', 1),
|
||||
'example': 'https://www.youtube.com/SOMECHANNEL'
|
||||
},
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: {
|
||||
'scheme': 'https',
|
||||
'domains': _youtube_domains,
|
||||
'path_regex': '^\/channel\/([^\/]+)(\/videos)?$',
|
||||
'path_must_not_match': ('/playlist', '/c/playlist'),
|
||||
'qs_args': [],
|
||||
'extract_key': ('path_regex', 0),
|
||||
'example': 'https://www.youtube.com/channel/CHANNELID'
|
||||
},
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: {
|
||||
'scheme': 'https',
|
||||
'domains': _youtube_domains,
|
||||
'path_regex': '^\/(playlist|watch)$',
|
||||
'path_must_not_match': (),
|
||||
'qs_args': ('list',),
|
||||
'extract_key': ('qs_args', 'list'),
|
||||
'example': 'https://www.youtube.com/playlist?list=PLAYLISTID'
|
||||
},
|
||||
}
|
||||
source_types = youtube_long_source_types
|
||||
help_item = dict(YouTube_SourceType.choices)
|
||||
help_texts = youtube_help.get('texts')
|
||||
help_examples = youtube_help.get('examples')
|
||||
validation_urls = youtube_validation_urls
|
||||
prepopulate_fields = {
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL: ('source_type', 'key', 'name', 'directory'),
|
||||
Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: ('source_type', 'key'),
|
||||
Source.SOURCE_TYPE_YOUTUBE_PLAYLIST: ('source_type', 'key'),
|
||||
Val(YouTube_SourceType.CHANNEL): ('source_type', 'key', 'name', 'directory'),
|
||||
Val(YouTube_SourceType.CHANNEL_ID): ('source_type', 'key'),
|
||||
Val(YouTube_SourceType.PLAYLIST): ('source_type', 'key'),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -269,7 +205,7 @@ class ValidateSourceView(FormView):
|
||||
# Perform extra validation on the URL, we need to extract the channel name or
|
||||
# playlist ID and check they are valid
|
||||
source_type = form.cleaned_data['source_type']
|
||||
if source_type not in self.source_types.values():
|
||||
if source_type not in YouTube_SourceType.values:
|
||||
form.add_error(
|
||||
'source_type',
|
||||
ValidationError(self.errors['invalid_source'])
|
||||
@ -391,7 +327,7 @@ class AddSourceView(EditSourceMixin, CreateView):
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
source_type = request.GET.get('source_type', '')
|
||||
if source_type and source_type in Source.SOURCE_TYPES:
|
||||
if source_type and source_type in YouTube_SourceType.values:
|
||||
self.prepopulated_data['source_type'] = source_type
|
||||
key = request.GET.get('key', '')
|
||||
if key:
|
||||
@ -962,13 +898,11 @@ class AddMediaServerView(FormView):
|
||||
|
||||
template_name = 'sync/mediaserver-add.html'
|
||||
server_types = {
|
||||
'plex': MediaServer.SERVER_TYPE_PLEX,
|
||||
}
|
||||
server_type_names = {
|
||||
MediaServer.SERVER_TYPE_PLEX: _('Plex'),
|
||||
'plex': Val(MediaServerType.PLEX),
|
||||
}
|
||||
server_type_names = dict(MediaServerType.choices)
|
||||
forms = {
|
||||
MediaServer.SERVER_TYPE_PLEX: PlexMediaServerForm,
|
||||
Val(MediaServerType.PLEX): PlexMediaServerForm,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@ -1085,7 +1019,7 @@ class UpdateMediaServerView(FormView, SingleObjectMixin):
|
||||
template_name = 'sync/mediaserver-update.html'
|
||||
model = MediaServer
|
||||
forms = {
|
||||
MediaServer.SERVER_TYPE_PLEX: PlexMediaServerForm,
|
||||
Val(MediaServerType.PLEX): PlexMediaServerForm,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
Loading…
Reference in New Issue
Block a user