diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py
new file mode 100644
index 00000000..f9614299
--- /dev/null
+++ b/tubesync/sync/choices.py
@@ -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 https://www.youtube.com/CHANNELNAME '
+ 'where CHANNELNAME 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 '
+ 'https://www.youtube.com/channel/BiGLoNgUnIqUeId '
+ 'where BiGLoNgUnIqUeId 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 https://www.youtube.com/playlist?list='
+ 'BiGLoNgUnIqUeId where BiGLoNgUnIqUeId is the '
+ 'unique ID of the playlist you want to add.'
+ ),
+ ),
+ )),
+}
+
diff --git a/tubesync/sync/fields.py b/tubesync/sync/fields.py
index 5dfde5b4..2910b7cc 100644
--- a/tubesync/sync/fields.py
+++ b/tubesync/sync/fields.py
@@ -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',
diff --git a/tubesync/sync/management/commands/import-existing-media.py b/tubesync/sync/management/commands/import-existing-media.py
index 9a524129..6b723e70 100644
--- a/tubesync/sync/management/commands/import-existing-media.py
+++ b/tubesync/sync/management/commands/import-existing-media.py
@@ -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))
diff --git a/tubesync/sync/matching.py b/tubesync/sync/matching.py
index 5993a4d1..9390e6fa 100644
--- a/tubesync/sync/matching.py
+++ b/tubesync/sync/matching.py
@@ -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
diff --git a/tubesync/sync/migrations/0028_alter_source_source_resolution.py b/tubesync/sync/migrations/0028_alter_source_source_resolution.py
new file mode 100644
index 00000000..d3535892
--- /dev/null
+++ b/tubesync/sync/migrations/0028_alter_source_source_resolution.py
@@ -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'),
+ ),
+ ]
+
diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py
index c914534a..d8a43649 100644
--- a/tubesync/sync/models.py
+++ b/tubesync/sync/models.py
@@ -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: '',
- SOURCE_TYPE_YOUTUBE_CHANNEL_ID: '',
- SOURCE_TYPE_YOUTUBE_PLAYLIST: '',
- }
+ ICONS = _srctype_dict('')
+
# 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: '',
- STATE_SCHEDULED: '',
- STATE_DOWNLOADING: '',
- STATE_DOWNLOADED: '',
- STATE_SKIPPED: '',
- STATE_DISABLED_AT_SOURCE: '',
- STATE_ERROR: '',
+ **(_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,
+ (
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ )
+ ))
+
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: '',
+ Val(MediaServerType.PLEX): '',
}
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(
diff --git a/tubesync/sync/signals.py b/tubesync/sync/signals.py
index e019e188..ce847881 100644
--- a/tubesync/sync/signals.py
+++ b/tubesync/sync/signals.py
@@ -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,
diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py
index a123a585..1c99e82b 100644
--- a/tubesync/sync/tests.py
+++ b/tubesync/sync/tests.py
@@ -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(
diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py
index 84514262..53c46f73 100644
--- a/tubesync/sync/views.py
+++ b/tubesync/sync/views.py
@@ -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 https://www.youtube.com/CHANNELNAME '
- 'where CHANNELNAME 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 '
- 'https://www.youtube.com/channel/BiGLoNgUnIqUeId '
- 'where BiGLoNgUnIqUeId 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 https://www.youtube.com/playlist?list='
- 'BiGLoNgUnIqUeId where BiGLoNgUnIqUeId 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):