From 6f49474ade83f94d5b145425ba184cc457fcc8ab Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 11 Feb 2025 02:16:29 -0500 Subject: [PATCH 01/54] Create choices.py This is a start to moving various choices and mappings out of other files. --- tubesync/sync/choices.py | 106 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tubesync/sync/choices.py diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py new file mode 100644 index 00000000..98517a17 --- /dev/null +++ b/tubesync/sync/choices.py @@ -0,0 +1,106 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +DOMAINS = dict({ + 'youtube': frozenset({ + 'youtube.com', + 'm.youtube.com', + 'www.youtube.com', + }), +}) + + +# 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 ID') + PLAYLIST = 'p', _('YouTube playlist') + + +youtube_long_source_types = { + 'youtube-channel': YouTube_SourceType.CHANNEL, + 'youtube-channel-id': YouTube_SourceType.CHANNEL_ID, + 'youtube-playlist': YouTube_SourceType.PLAYLIST, +} + + +youtube_help = { + 'examples': { + YouTube_SourceType.CHANNEL: 'https://www.youtube.com/google', + YouTube_SourceType.CHANNEL_ID: ('https://www.youtube.com/channel/' + 'UCK8sQmJBp8GCxrOtXWBpyEA'), + YouTube_SourceType.PLAYLIST: ('https://www.youtube.com/playlist?list=' + 'PL590L5WQmH8dpP0RyH5pCfIaDEdt9nk7r'), + }, + 'texts': { + YouTube_SourceType.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.' + ), + YouTube_SourceType.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.' + ), + YouTube_SourceType.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.' + ), + }, +} + + +youtube_validation_urls = { + YouTube_SourceType.CHANNEL: { + 'scheme': 'https', + 'domains': DOMAINS['youtube'], + 'path_regex': '^\/(c\/)?([^\/]+)(\/videos)?$', + 'path_must_not_match': ('/playlist', '/c/playlist'), + 'qs_args': [], + 'extract_key': ('path_regex', 1), + 'example': 'https://www.youtube.com/SOMECHANNEL' + }, + YouTube_SourceType.CHANNEL_ID: { + 'scheme': 'https', + 'domains': DOMAINS['youtube'], + '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' + }, + YouTube_SourceType.PLAYLIST: { + 'scheme': 'https', + 'domains': DOMAINS['youtube'], + 'path_regex': '^\/(playlist|watch)$', + 'path_must_not_match': (), + 'qs_args': ('list',), + 'extract_key': ('qs_args', 'list'), + 'example': 'https://www.youtube.com/playlist?list=PLAYLISTID' + }, +} + From 0c282d457358bf0151aa779c4e00d7604823e265 Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 11 Feb 2025 02:51:23 -0500 Subject: [PATCH 02/54] Use the `.value` strings --- tubesync/sync/choices.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index 98517a17..c259aa0d 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -36,35 +36,35 @@ class YouTube_SourceType(models.TextChoices): youtube_long_source_types = { - 'youtube-channel': YouTube_SourceType.CHANNEL, - 'youtube-channel-id': YouTube_SourceType.CHANNEL_ID, - 'youtube-playlist': YouTube_SourceType.PLAYLIST, + 'youtube-channel': YouTube_SourceType.CHANNEL.value, + 'youtube-channel-id': YouTube_SourceType.CHANNEL_ID.value, + 'youtube-playlist': YouTube_SourceType.PLAYLIST.value, } youtube_help = { 'examples': { - YouTube_SourceType.CHANNEL: 'https://www.youtube.com/google', - YouTube_SourceType.CHANNEL_ID: ('https://www.youtube.com/channel/' + YouTube_SourceType.CHANNEL.value: 'https://www.youtube.com/google', + YouTube_SourceType.CHANNEL_ID.value: ('https://www.youtube.com/channel/' 'UCK8sQmJBp8GCxrOtXWBpyEA'), - YouTube_SourceType.PLAYLIST: ('https://www.youtube.com/playlist?list=' + YouTube_SourceType.PLAYLIST.value: ('https://www.youtube.com/playlist?list=' 'PL590L5WQmH8dpP0RyH5pCfIaDEdt9nk7r'), }, 'texts': { - YouTube_SourceType.CHANNEL: _( + YouTube_SourceType.CHANNEL.value: _( '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.' ), - YouTube_SourceType.CHANNEL_ID: _( + YouTube_SourceType.CHANNEL_ID.value: _( '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.' ), - YouTube_SourceType.PLAYLIST: _( + YouTube_SourceType.PLAYLIST.value: _( '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 ' @@ -75,7 +75,7 @@ youtube_help = { youtube_validation_urls = { - YouTube_SourceType.CHANNEL: { + YouTube_SourceType.CHANNEL.value: { 'scheme': 'https', 'domains': DOMAINS['youtube'], 'path_regex': '^\/(c\/)?([^\/]+)(\/videos)?$', @@ -84,7 +84,7 @@ youtube_validation_urls = { 'extract_key': ('path_regex', 1), 'example': 'https://www.youtube.com/SOMECHANNEL' }, - YouTube_SourceType.CHANNEL_ID: { + YouTube_SourceType.CHANNEL_ID.value: { 'scheme': 'https', 'domains': DOMAINS['youtube'], 'path_regex': '^\/channel\/([^\/]+)(\/videos)?$', @@ -93,7 +93,7 @@ youtube_validation_urls = { 'extract_key': ('path_regex', 0), 'example': 'https://www.youtube.com/channel/CHANNELID' }, - YouTube_SourceType.PLAYLIST: { + YouTube_SourceType.PLAYLIST.value: { 'scheme': 'https', 'domains': DOMAINS['youtube'], 'path_regex': '^\/(playlist|watch)$', From 96737c4de358c6fdd716d0b2f1ddb45804ecd7e8 Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 11 Feb 2025 22:32:43 -0500 Subject: [PATCH 03/54] Add choices from models.py --- tubesync/sync/choices.py | 87 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index c259aa0d..b442b149 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -11,6 +11,82 @@ DOMAINS = dict({ }) +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.TextChoices): + 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)') + + +SourceResolutionInteger = dict() +for name in SourceResolution.names: + if name.endswith('0P'): + value = SourceResolution.__getattr__(name).value[: -1] + SourceResolutionInteger.update({name: int(value)}) + + # as stolen from: # - https://wiki.sponsor.ajay.app/w/Types # - https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/postprocessor/sponsorblock.py @@ -35,6 +111,17 @@ class YouTube_SourceType(models.TextChoices): PLAYLIST = 'p', _('YouTube playlist') +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)') + + youtube_long_source_types = { 'youtube-channel': YouTube_SourceType.CHANNEL.value, 'youtube-channel-id': YouTube_SourceType.CHANNEL_ID.value, From e0c60365caa6479e94052086cd60dacc85c6ed0e Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 11 Feb 2025 22:56:01 -0500 Subject: [PATCH 04/54] Start using `YouTube_SourceType` --- tubesync/sync/models.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index c914534a..deed07d2 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -26,6 +26,7 @@ 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 .choices import YouTube_SourceType media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/') @@ -35,16 +36,10 @@ 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_TYPE_YOUTUBE_CHANNEL = YouTube_SourceType.CHANNEL.value + SOURCE_TYPE_YOUTUBE_CHANNEL_ID = YouTube_SourceType.CHANNEL_ID.value + SOURCE_TYPE_YOUTUBE_PLAYLIST = YouTube_SourceType.PLAYLIST.value + SOURCE_TYPES = YouTube_SourceType.values SOURCE_RESOLUTION_360P = '360p' SOURCE_RESOLUTION_480P = '480p' @@ -222,8 +217,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( From 44a6e0c69a0e8ce0c5362f0e7ce4187388819850 Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 11 Feb 2025 23:08:02 -0500 Subject: [PATCH 05/54] Replace `Source.SOURCE_TYPES` --- tubesync/sync/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index 24ff6866..319f1bf8 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -31,6 +31,7 @@ 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 YouTube_SourceType from . import signals from . import youtube @@ -391,7 +392,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: From 47f3341b00e660badd594493493522b40362c30f Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 11 Feb 2025 23:10:23 -0500 Subject: [PATCH 06/54] Remoce `Source.SOURCE_TYPES` --- tubesync/sync/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index deed07d2..83af1200 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -39,7 +39,6 @@ class Source(models.Model): SOURCE_TYPE_YOUTUBE_CHANNEL = YouTube_SourceType.CHANNEL.value SOURCE_TYPE_YOUTUBE_CHANNEL_ID = YouTube_SourceType.CHANNEL_ID.value SOURCE_TYPE_YOUTUBE_PLAYLIST = YouTube_SourceType.PLAYLIST.value - SOURCE_TYPES = YouTube_SourceType.values SOURCE_RESOLUTION_360P = '360p' SOURCE_RESOLUTION_480P = '480p' From bb3d5fcdff211c05c85d5e6976796fb51dc9dc71 Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 11 Feb 2025 23:24:21 -0500 Subject: [PATCH 07/54] Remove `CapChoices` and `IndexSchedule` --- tubesync/sync/models.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 83af1200..55887ed6 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -26,7 +26,7 @@ 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 .choices import YouTube_SourceType +from .choices import CapChoices, IndexSchedule, YouTube_SourceType media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/') @@ -167,31 +167,6 @@ class Source(models.Model): 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') - uuid = models.UUIDField( _('uuid'), primary_key=True, From 934cb2ed5850922a6cfca94c605db9d4e5e6f463 Mon Sep 17 00:00:00 2001 From: tcely Date: Tue, 11 Feb 2025 23:31:13 -0500 Subject: [PATCH 08/54] Use `IndexSchedule` from `choices.py` --- tubesync/sync/tests.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index a123a585..00cff06e 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -19,6 +19,7 @@ from .models import Source, Media from .tasks import cleanup_old_media from .filtering import filter_media from .utils import filter_response +from .choices import IndexSchedule class FrontEndTestCase(TestCase): @@ -239,7 +240,7 @@ class FrontEndTestCase(TestCase): 'download_cap': 0, 'filter_text': '.*', 'filter_seconds_min': True, - 'index_schedule': Source.IndexSchedule.EVERY_HOUR, + 'index_schedule': IndexSchedule.EVERY_HOUR, 'delete_old_media': False, 'days_to_keep': 14, 'source_resolution': Source.SOURCE_RESOLUTION_1080P, @@ -276,7 +277,7 @@ class FrontEndTestCase(TestCase): 'download_cap': 0, 'filter_text': '.*', 'filter_seconds_min': True, - 'index_schedule': Source.IndexSchedule.EVERY_2_HOURS, # changed + 'index_schedule': IndexSchedule.EVERY_2_HOURS, # changed 'delete_old_media': False, 'days_to_keep': 14, 'source_resolution': Source.SOURCE_RESOLUTION_1080P, @@ -342,7 +343,7 @@ class FrontEndTestCase(TestCase): key='testkey', name='testname', directory='testdirectory', - index_schedule=Source.IndexSchedule.EVERY_HOUR, + index_schedule=IndexSchedule.EVERY_HOUR, delete_old_media=False, days_to_keep=14, source_resolution=Source.SOURCE_RESOLUTION_1080P, From c493e3c92ab221aaf47559a1f357bfb6e2bd7216 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 00:02:39 -0500 Subject: [PATCH 09/54] Use `youtube_long_source_types` --- tubesync/sync/tests.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index 00cff06e..ceeb5afc 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -19,7 +19,7 @@ from .models import Source, Media from .tasks import cleanup_old_media from .filtering import filter_media from .utils import filter_response -from .choices import IndexSchedule +from .choices import IndexSchedule, youtube_long_source_types class FrontEndTestCase(TestCase): @@ -34,11 +34,7 @@ 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_source_types = youtube_long_source_types test_sources = { 'youtube-channel': { 'valid': ( From d5508120229922ef5ba9890611ba41a17625a688 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 00:06:46 -0500 Subject: [PATCH 10/54] Use `youtube_long_source_types` --- tubesync/sync/views.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index 319f1bf8..9028417b 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -31,7 +31,7 @@ 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 YouTube_SourceType +from .choices import YouTube_SourceType, youtube_long_source_types from . import signals from . import youtube @@ -162,11 +162,7 @@ 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, - } + source_types = youtube_long_source_types help_item = { Source.SOURCE_TYPE_YOUTUBE_CHANNEL: _('YouTube channel'), Source.SOURCE_TYPE_YOUTUBE_CHANNEL_ID: _('YouTube channel ID'), From 3337dcae08bd34be1a653e218aaca7516e3fb2d1 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 00:59:38 -0500 Subject: [PATCH 11/54] Fixes from testing --- tubesync/sync/choices.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index b442b149..6f1d4401 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -83,8 +83,13 @@ class SourceResolution(models.TextChoices): SourceResolutionInteger = dict() for name in SourceResolution.names: if name.endswith('0P'): - value = SourceResolution.__getattr__(name).value[: -1] - SourceResolutionInteger.update({name: int(value)}) + value = None + try: + value = SourceResolution.__getattr__(name).value[: -1] + except AttributeError: + value = name[6:-1] + if value is not None: + SourceResolutionInteger.update({name: int(value)}) # as stolen from: @@ -165,7 +170,7 @@ youtube_validation_urls = { YouTube_SourceType.CHANNEL.value: { 'scheme': 'https', 'domains': DOMAINS['youtube'], - 'path_regex': '^\/(c\/)?([^\/]+)(\/videos)?$', + 'path_regex': r'^\/(c\/)?([^\/]+)(\/videos)?$', 'path_must_not_match': ('/playlist', '/c/playlist'), 'qs_args': [], 'extract_key': ('path_regex', 1), @@ -174,7 +179,7 @@ youtube_validation_urls = { YouTube_SourceType.CHANNEL_ID.value: { 'scheme': 'https', 'domains': DOMAINS['youtube'], - 'path_regex': '^\/channel\/([^\/]+)(\/videos)?$', + 'path_regex': r'^\/channel\/([^\/]+)(\/videos)?$', 'path_must_not_match': ('/playlist', '/c/playlist'), 'qs_args': [], 'extract_key': ('path_regex', 0), @@ -183,7 +188,7 @@ youtube_validation_urls = { YouTube_SourceType.PLAYLIST.value: { 'scheme': 'https', 'domains': DOMAINS['youtube'], - 'path_regex': '^\/(playlist|watch)$', + 'path_regex': r'^\/(playlist|watch)$', 'path_must_not_match': (), 'qs_args': ('list',), 'extract_key': ('qs_args', 'list'), From 116b78db08e19460940636eb175d5e803f73381e Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 01:04:29 -0500 Subject: [PATCH 12/54] Make `migrate` happy again --- tubesync/sync/choices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index 6f1d4401..a1275cdd 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -112,7 +112,7 @@ class SponsorBlock_Category(models.TextChoices): class YouTube_SourceType(models.TextChoices): CHANNEL = 'c', _('YouTube channel') - CHANNEL_ID = 'i', _('YouTube channel ID') + CHANNEL_ID = 'i', _('YouTube channel by ID') PLAYLIST = 'p', _('YouTube playlist') From 8bedd4f9ffa2021fddbc912637ac997554710f61 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 01:18:45 -0500 Subject: [PATCH 13/54] Use a way that works for all versions --- tubesync/sync/choices.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index a1275cdd..d7bc891f 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -1,3 +1,5 @@ +from enum import Enum + from django.db import models from django.utils.translation import gettext_lazy as _ @@ -83,13 +85,10 @@ class SourceResolution(models.TextChoices): SourceResolutionInteger = dict() for name in SourceResolution.names: if name.endswith('0P'): - value = None - try: - value = SourceResolution.__getattr__(name).value[: -1] - except AttributeError: - value = name[6:-1] - if value is not None: - SourceResolutionInteger.update({name: int(value)}) + value = SourceResolution.__dict__.get(name, None) + if isinstance(value, Enum): + value = int(value.value) + SourceResolutionInteger.update({name: value}) # as stolen from: From b64ca284e7d7106b6c6230722d504b3dcb13c2d8 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 01:22:11 -0500 Subject: [PATCH 14/54] fixup: the 'p' must still be removed --- tubesync/sync/choices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index d7bc891f..b92f16bb 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -87,7 +87,7 @@ for name in SourceResolution.names: if name.endswith('0P'): value = SourceResolution.__dict__.get(name, None) if isinstance(value, Enum): - value = int(value.value) + value = int(value.value[: -1]) SourceResolutionInteger.update({name: value}) From e79218c2082531e12f152ef405a394d2f3a2c32b Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 01:33:11 -0500 Subject: [PATCH 15/54] Use `youtube_validation_urls` --- tubesync/sync/views.py | 37 ++----------------------------------- 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index 9028417b..c5840e29 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -31,7 +31,7 @@ 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 YouTube_SourceType, youtube_long_source_types +from .choices import (YouTube_SourceType, youtube_long_source_types, youtube_validation_urls) from . import signals from . import youtube @@ -196,40 +196,7 @@ class ValidateSourceView(FormView): 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' - }, - } + 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'), From b45675b13a440f4c1d5ad4bdd33510f7d42e1da0 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 02:46:27 -0500 Subject: [PATCH 16/54] Use a class method to generate the mapping dictionary --- tubesync/sync/choices.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index b92f16bb..c907ba29 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -1,5 +1,3 @@ -from enum import Enum - from django.db import models from django.utils.translation import gettext_lazy as _ @@ -81,14 +79,19 @@ class SourceResolution(models.TextChoices): VIDEO_2160P = '2160p', _('4320p (8K)') VIDEO_4320P = '4320p', _('4320p (8K)') + @classmethod + def _integer_mapping(cls): + filter_func = lambda s: s.lower().endswith('0p') + return dict(zip( + filter(filter_func, cls.names), + map( + lambda s: int(s[:-1], base=10), + filter(filter_func, cls.values) + ) + )) -SourceResolutionInteger = dict() -for name in SourceResolution.names: - if name.endswith('0P'): - value = SourceResolution.__dict__.get(name, None) - if isinstance(value, Enum): - value = int(value.value[: -1]) - SourceResolutionInteger.update({name: value}) + +SourceResolutionInteger = SourceResolution._integer_mapping() # as stolen from: From 09c9d93372ddabd0de7825cf1570186e6fd5ce16 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 03:20:25 -0500 Subject: [PATCH 17/54] Remove unneeded `test_source_types` --- tubesync/sync/tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index ceeb5afc..2a7ef59c 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -34,7 +34,6 @@ class FrontEndTestCase(TestCase): self.assertEqual(response.status_code, 200) def test_validate_source(self): - test_source_types = youtube_long_source_types test_sources = { 'youtube-channel': { 'valid': ( @@ -126,7 +125,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': From aabe2495980993000f3e4c7c7013acec16c787df Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 04:10:52 -0500 Subject: [PATCH 18/54] Use `youtube_help` --- tubesync/sync/views.py | 47 ++++++++---------------------------------- 1 file changed, 9 insertions(+), 38 deletions(-) diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index c5840e29..5aa5095a 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -31,7 +31,8 @@ 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 (YouTube_SourceType, youtube_long_source_types, youtube_validation_urls) +from .choices import (YouTube_SourceType, youtube_long_source_types, + youtube_help, youtube_validation_urls) from . import signals from . import youtube @@ -163,44 +164,14 @@ class ValidateSourceView(FormView): 'the format of "{example}". The error was: {error}.'), } source_types = youtube_long_source_types - 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') - } + 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'), + YouTube_SourceType.CHANNEL.value: ('source_type', 'key', 'name', 'directory'), + YouTube_SourceType.CHANNEL_ID.value: ('source_type', 'key'), + YouTube_SourceType.PLAYLIST.value: ('source_type', 'key'), } def __init__(self, *args, **kwargs): @@ -233,7 +204,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']) From 6b4adbd59659486787c14dd35d21aa2be293a9a6 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 05:03:48 -0500 Subject: [PATCH 19/54] Add `YouTube_SourceType._validation_urls` class method --- tubesync/sync/choices.py | 147 +++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 67 deletions(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index c907ba29..d65c1273 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -1,6 +1,8 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from copy import deepcopy + DOMAINS = dict({ 'youtube': frozenset({ @@ -91,9 +93,6 @@ class SourceResolution(models.TextChoices): )) -SourceResolutionInteger = SourceResolution._integer_mapping() - - # as stolen from: # - https://wiki.sponsor.ajay.app/w/Types # - https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/postprocessor/sponsorblock.py @@ -117,6 +116,49 @@ class YouTube_SourceType(models.TextChoices): CHANNEL_ID = 'i', _('YouTube channel by ID') PLAYLIST = 'p', _('YouTube playlist') + @classmethod + def _validation_urls(cls): + defaults = { + 'scheme': 'https', + 'domains': DOMAINS['youtube'], + 'qs_args': [], + } + return dict(zip( + cls.values, + ( + deepcopy(defaults).update({ + 'path_regex': r'^\/(c\/)?([^\/]+)(\/videos)?$', + 'path_must_not_match': ('/playlist', '/c/playlist'), + 'extract_key': ('path_regex', 1), + 'example': 'https://www.youtube.com/SOMECHANNEL', + }), + deepcopy(defaults).update({ + 'path_regex': r'^\/channel\/([^\/]+)(\/videos)?$', + 'path_must_not_match': ('/playlist', '/c/playlist'), + 'extract_key': ('path_regex', 0), + 'example': 'https://www.youtube.com/channel/CHANNELID', + }), + deepcopy(defaults).update({ + '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') @@ -129,72 +171,43 @@ class YouTube_VideoCodec(models.TextChoices): AVC1 = 'AVC1', _('AVC1 (H.264)') -youtube_long_source_types = { - 'youtube-channel': YouTube_SourceType.CHANNEL.value, - 'youtube-channel-id': YouTube_SourceType.CHANNEL_ID.value, - 'youtube-playlist': YouTube_SourceType.PLAYLIST.value, -} - - +SourceResolutionInteger = SourceResolution._integer_mapping() +youtube_long_source_types = YouTube_SourceType._long_type_mapping() +youtube_validation_urls = YouTube_SourceType._validation_urls() youtube_help = { - 'examples': { - YouTube_SourceType.CHANNEL.value: 'https://www.youtube.com/google', - YouTube_SourceType.CHANNEL_ID.value: ('https://www.youtube.com/channel/' - 'UCK8sQmJBp8GCxrOtXWBpyEA'), - YouTube_SourceType.PLAYLIST.value: ('https://www.youtube.com/playlist?list=' - 'PL590L5WQmH8dpP0RyH5pCfIaDEdt9nk7r'), - }, - 'texts': { - YouTube_SourceType.CHANNEL.value: _( - '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.' + 'examples': dict(zip( + YouTube_SourceType.values, + ( + ('https://www.youtube.com/google'), + ('https://www.youtube.com/channel/' + 'UCK8sQmJBp8GCxrOtXWBpyEA'), + ('https://www.youtube.com/playlist?list=' + 'PL590L5WQmH8dpP0RyH5pCfIaDEdt9nk7r'), ), - YouTube_SourceType.CHANNEL_ID.value: _( - '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.' + )), + '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.' + ), ), - YouTube_SourceType.PLAYLIST.value: _( - '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.' - ), - }, -} - - -youtube_validation_urls = { - YouTube_SourceType.CHANNEL.value: { - 'scheme': 'https', - 'domains': DOMAINS['youtube'], - 'path_regex': r'^\/(c\/)?([^\/]+)(\/videos)?$', - 'path_must_not_match': ('/playlist', '/c/playlist'), - 'qs_args': [], - 'extract_key': ('path_regex', 1), - 'example': 'https://www.youtube.com/SOMECHANNEL' - }, - YouTube_SourceType.CHANNEL_ID.value: { - 'scheme': 'https', - 'domains': DOMAINS['youtube'], - 'path_regex': r'^\/channel\/([^\/]+)(\/videos)?$', - 'path_must_not_match': ('/playlist', '/c/playlist'), - 'qs_args': [], - 'extract_key': ('path_regex', 0), - 'example': 'https://www.youtube.com/channel/CHANNELID' - }, - YouTube_SourceType.PLAYLIST.value: { - 'scheme': 'https', - 'domains': DOMAINS['youtube'], - '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' - }, + )), } From 073e08e27c260161ca6094214e9704dfeb90065e Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 05:29:10 -0500 Subject: [PATCH 20/54] Actually return the updated dictionary --- tubesync/sync/choices.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index d65c1273..21646b4e 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -123,22 +123,23 @@ class YouTube_SourceType(models.TextChoices): 'domains': DOMAINS['youtube'], 'qs_args': [], } + update_and_return = lambda c, d: c.update(d) or c return dict(zip( cls.values, ( - deepcopy(defaults).update({ + 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', }), - deepcopy(defaults).update({ + 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', }), - deepcopy(defaults).update({ + update_and_return(deepcopy(defaults), { 'path_regex': r'^\/(playlist|watch)$', 'path_must_not_match': (), 'qs_args': ('list',), From deefe060ed8b1d530e61a8d22124bffa7d373088 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 05:44:33 -0500 Subject: [PATCH 21/54] Use `SponsorBlock_Category` from `choices.py` --- tubesync/sync/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 55887ed6..da2bcb9b 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -25,8 +25,8 @@ 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 .choices import CapChoices, IndexSchedule, YouTube_SourceType +from .fields import CommaSepChoiceField +from .choices import CapChoices, IndexSchedule, SponsorBlock_Category, YouTube_SourceType media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/') From ab9a6e6c766785a14fefa53874df72b637b2d30d Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 05:46:32 -0500 Subject: [PATCH 22/54] Remove `SponsorBlock_Category` from `fields.py` --- tubesync/sync/fields.py | 18 ------------------ 1 file changed, 18 deletions(-) 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', From f8229fff1192899c287b20d5cbe5d5f4516da982 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 06:07:37 -0500 Subject: [PATCH 23/54] Use `MediaServerType` --- tubesync/sync/models.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index da2bcb9b..a721bb98 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -26,7 +26,8 @@ from .matching import (get_best_combined_format, get_best_audio_format, get_best_video_format) from .mediaservers import PlexMediaServer from .fields import CommaSepChoiceField -from .choices import CapChoices, IndexSchedule, SponsorBlock_Category, YouTube_SourceType +from .choices import (CapChoices, IndexSchedule, MediaServerType, + SponsorBlock_Category, YouTube_SourceType) media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/') @@ -1612,11 +1613,7 @@ 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')), - ) + SERVER_TYPE_PLEX = MediaServerType.PLEX.value ICONS = { SERVER_TYPE_PLEX: '', } @@ -1628,8 +1625,8 @@ class MediaServer(models.Model): _('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( From 4a1c0016fd49e6d92c1c36d467fdfe5ce360e712 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 06:34:29 -0500 Subject: [PATCH 24/54] Use `MediaServerType` --- tubesync/sync/views.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index 5aa5095a..f36a63bc 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -31,7 +31,7 @@ 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 (YouTube_SourceType, youtube_long_source_types, +from .choices import (MediaServerType, YouTube_SourceType, youtube_long_source_types, youtube_help, youtube_validation_urls) from . import signals from . import youtube @@ -897,13 +897,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': MediaServerType.PLEX.value, } + server_type_names = dict(MediaServerType.choices) forms = { - MediaServer.SERVER_TYPE_PLEX: PlexMediaServerForm, + MediaServerType.PLEX.value: PlexMediaServerForm, } def __init__(self, *args, **kwargs): @@ -1020,7 +1018,7 @@ class UpdateMediaServerView(FormView, SingleObjectMixin): template_name = 'sync/mediaserver-update.html' model = MediaServer forms = { - MediaServer.SERVER_TYPE_PLEX: PlexMediaServerForm, + MediaServerType.PLEX.value: PlexMediaServerForm, } def __init__(self, *args, **kwargs): From 0f85803081b6f4e990ada2c5b93ba8190e47abf0 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 07:07:26 -0500 Subject: [PATCH 25/54] Use `Fallback` --- tubesync/sync/models.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index a721bb98..76feae19 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -26,7 +26,7 @@ from .matching import (get_best_combined_format, get_best_audio_format, get_best_video_format) from .mediaservers import PlexMediaServer from .fields import CommaSepChoiceField -from .choices import (CapChoices, IndexSchedule, MediaServerType, +from .choices import (CapChoices, Fallback, IndexSchedule, MediaServerType, SponsorBlock_Category, YouTube_SourceType) media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/') @@ -91,15 +91,9 @@ class Source(models.Model): (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')) - ) + FALLBACK_FAIL = Fallback.FAIL.value + FALLBACK_NEXT_BEST = Fallback.NEXT_BEST.value + FALLBACK_NEXT_BEST_HD = Fallback.NEXT_BEST_HD.value FILTER_SECONDS_CHOICES = ( (True, _('Minimum Length')), @@ -335,8 +329,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( From 0fdef5948c77f91bc0a20ed522685048ad8902be Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 07:32:23 -0500 Subject: [PATCH 26/54] Use `Fallback` --- tubesync/sync/matching.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/matching.py b/tubesync/sync/matching.py index 5993a4d1..a173197e 100644 --- a/tubesync/sync/matching.py +++ b/tubesync/sync/matching.py @@ -5,6 +5,7 @@ ''' +from .choices import 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 == Fallback.NEXT_BEST_HD.value 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 == Fallback.NEXT_BEST.value: return False, best_match['id'] # Nope, failed to find match return False, False From 668a3c6604e658dce6478f8144f42e40c2c75b43 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 09:49:18 -0500 Subject: [PATCH 27/54] Use `Fallback` --- tubesync/sync/tests.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index 2a7ef59c..b3bb5a3f 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -19,7 +19,8 @@ from .models import Source, Media from .tasks import cleanup_old_media from .filtering import filter_media from .utils import filter_response -from .choices import IndexSchedule, youtube_long_source_types +from .choices import (Fallback, IndexSchedule, + youtube_long_source_types) class FrontEndTestCase(TestCase): @@ -243,7 +244,7 @@ class FrontEndTestCase(TestCase): 'source_acodec': Source.SOURCE_ACODEC_OPUS, 'prefer_60fps': False, 'prefer_hdr': False, - 'fallback': Source.FALLBACK_FAIL, + 'fallback': Fallback.FAIL.value, 'sponsorblock_categories': data_categories, 'sub_langs': 'en', } @@ -280,7 +281,7 @@ class FrontEndTestCase(TestCase): 'source_acodec': Source.SOURCE_ACODEC_OPUS, 'prefer_60fps': False, 'prefer_hdr': False, - 'fallback': Source.FALLBACK_FAIL, + 'fallback': Fallback.FAIL.value, 'sponsorblock_categories': data_categories, 'sub_langs': 'en', } @@ -346,7 +347,7 @@ class FrontEndTestCase(TestCase): source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Source.FALLBACK_FAIL + fallback=Fallback.FAIL.value ) # Add some media test_minimal_metadata = ''' @@ -528,7 +529,7 @@ class FilepathTestCase(TestCase): source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Source.FALLBACK_FAIL + fallback=Fallback.FAIL.value ) # Add some test media self.media = Media.objects.create( @@ -688,7 +689,7 @@ class MediaTestCase(TestCase): source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Source.FALLBACK_FAIL + fallback=Fallback.FAIL.value ) # Add some test media self.media = Media.objects.create( @@ -761,7 +762,7 @@ class MediaFilterTestCase(TestCase): source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Source.FALLBACK_FAIL, + fallback=Fallback.FAIL.value, ) # Add some test media self.media = Media.objects.create( @@ -930,7 +931,7 @@ class FormatMatchingTestCase(TestCase): source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Source.FALLBACK_FAIL + fallback=Fallback.FAIL.value ) # Add some media self.media = Media.objects.create( @@ -940,7 +941,7 @@ class FormatMatchingTestCase(TestCase): ) def test_combined_exact_format_matching(self): - self.source.fallback = Source.FALLBACK_FAIL + self.source.fallback = Fallback.FAIL.value self.media.metadata = all_test_metadata['boring'] self.media.save() expected_matches = { @@ -1070,7 +1071,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 = Fallback.FAIL.value self.media.metadata = all_test_metadata['boring'] self.media.save() expected_matches = { @@ -1216,7 +1217,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 = Fallback.FAIL.value # Test no 60fps, no HDR metadata self.media.metadata = all_test_metadata['boring'] self.media.save() @@ -1426,7 +1427,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 = Fallback.NEXT_BEST.value # Test no 60fps, no HDR metadata self.media.metadata = all_test_metadata['boring'] self.media.save() @@ -1748,7 +1749,7 @@ class ResponseFilteringTestCase(TestCase): source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Source.FALLBACK_FAIL + fallback=Fallback.FAIL.value ) # Add some media self.media = Media.objects.create( From 3675496cfa815bd74fa21e1abc1d6629efe704c1 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 11:21:29 -0500 Subject: [PATCH 28/54] Use `SourceResolution` --- tubesync/sync/models.py | 46 +++++++++-------------------------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 76feae19..2ca423f8 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -27,6 +27,7 @@ from .matching import (get_best_combined_format, get_best_audio_format, from .mediaservers import PlexMediaServer from .fields import CommaSepChoiceField from .choices import (CapChoices, Fallback, IndexSchedule, MediaServerType, + SourceResolution, SourceResolutionInteger, SponsorBlock_Category, YouTube_SourceType) media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/') @@ -41,37 +42,10 @@ class Source(models.Model): SOURCE_TYPE_YOUTUBE_CHANNEL_ID = YouTube_SourceType.CHANNEL_ID.value SOURCE_TYPE_YOUTUBE_PLAYLIST = YouTube_SourceType.PLAYLIST.value - 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_RESOLUTION_1080P = SourceResolution.VIDEO_1080P.value + SOURCE_RESOLUTION_AUDIO = SourceResolution.AUDIO.value + SOURCE_RESOLUTIONS = SourceResolution.values + RESOLUTION_MAP = SourceResolutionInteger SOURCE_VCODEC_AVC1 = 'AVC1' SOURCE_VCODEC_VP9 = 'VP9' @@ -295,8 +269,8 @@ 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( @@ -401,7 +375,7 @@ 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_video(self): @@ -461,7 +435,7 @@ class Source(models.Model): @property def format_summary(self): - if self.source_resolution == Source.SOURCE_RESOLUTION_AUDIO: + if self.source_resolution == SourceResolution.AUDIO.value: vc = 'none' else: vc = self.source_vcodec @@ -478,7 +452,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.source_resolution == SourceResolution.AUDIO.value: return Path(settings.DOWNLOAD_AUDIO_DIR) / self.directory else: return Path(settings.DOWNLOAD_VIDEO_DIR) / self.directory From 2195ac34b7478666b41999500c64e140db8ea454 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 11:27:27 -0500 Subject: [PATCH 29/54] Remove `RESOLUTION_MAP` --- tubesync/sync/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 2ca423f8..8e011fab 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -45,7 +45,6 @@ class Source(models.Model): SOURCE_RESOLUTION_1080P = SourceResolution.VIDEO_1080P.value SOURCE_RESOLUTION_AUDIO = SourceResolution.AUDIO.value SOURCE_RESOLUTIONS = SourceResolution.values - RESOLUTION_MAP = SourceResolutionInteger SOURCE_VCODEC_AVC1 = 'AVC1' SOURCE_VCODEC_VP9 = 'VP9' @@ -480,7 +479,7 @@ 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): From 7bb29165e20584323bf2f9d30981b337b23bc31e Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 13:15:56 -0500 Subject: [PATCH 30/54] Correct the mapping keys --- tubesync/sync/choices.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index 21646b4e..f62e8aec 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -83,14 +83,9 @@ class SourceResolution(models.TextChoices): @classmethod def _integer_mapping(cls): - filter_func = lambda s: s.lower().endswith('0p') - return dict(zip( - filter(filter_func, cls.names), - map( - lambda s: int(s[:-1], base=10), - filter(filter_func, cls.values) - ) - )) + int_height = lambda s: int(s[:-1], base=10) + video = filter(lambda s: s.endswith('0p'), cls.values) + return dict(zip( video, map(int_height, video) )) # as stolen from: From 2e4ef80691d0ddb21eb1d1d9e8e9871d59eb8b4d Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 13:25:40 -0500 Subject: [PATCH 31/54] fixup: missed the `list()` --- tubesync/sync/choices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index f62e8aec..08beaf10 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -84,7 +84,7 @@ class SourceResolution(models.TextChoices): @classmethod def _integer_mapping(cls): int_height = lambda s: int(s[:-1], base=10) - video = filter(lambda s: s.endswith('0p'), cls.values) + video = list(filter(lambda s: s.endswith('0p'), cls.values)) return dict(zip( video, map(int_height, video) )) From 091c9fed7b7f0cb896d0cfbcc614d395dc28598c Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 13:37:51 -0500 Subject: [PATCH 32/54] Create 0028_alter_source_source_resolution.py --- .../0028_alter_source_source_resolution.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tubesync/sync/migrations/0028_alter_source_source_resolution.py 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'), + ), + ] + From a33527a6cd19fed8628037d7acb6f30773012b14 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 13:49:19 -0500 Subject: [PATCH 33/54] Use `SourceResolution` --- tubesync/sync/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index f36a63bc..b7d37747 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -31,7 +31,8 @@ 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 (MediaServerType, YouTube_SourceType, youtube_long_source_types, +from .choices import (MediaServerType, SourceResolution, + YouTube_SourceType, youtube_long_source_types, youtube_help, youtube_validation_urls) from . import signals from . import youtube @@ -50,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=SourceResolution.AUDIO.value) ).count() data['num_audio_sources'] = data['num_sources'] - data['num_video_sources'] data['num_failed_sources'] = Source.objects.filter(has_failed=True).count() From 10632b9e5d85e921836e0bb60665ea522cd77b1c Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 15:23:08 -0500 Subject: [PATCH 34/54] Use `SourceResolution` --- tubesync/sync/tests.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index b3bb5a3f..52aa1ebe 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -19,7 +19,7 @@ from .models import Source, Media from .tasks import cleanup_old_media from .filtering import filter_media from .utils import filter_response -from .choices import (Fallback, IndexSchedule, +from .choices import (Fallback, IndexSchedule, SourceResolution, youtube_long_source_types) @@ -239,7 +239,7 @@ class FrontEndTestCase(TestCase): 'index_schedule': IndexSchedule.EVERY_HOUR, 'delete_old_media': False, 'days_to_keep': 14, - 'source_resolution': Source.SOURCE_RESOLUTION_1080P, + 'source_resolution': SourceResolution.VIDEO_1080P.value, 'source_vcodec': Source.SOURCE_VCODEC_VP9, 'source_acodec': Source.SOURCE_ACODEC_OPUS, 'prefer_60fps': False, @@ -276,7 +276,7 @@ class FrontEndTestCase(TestCase): 'index_schedule': IndexSchedule.EVERY_2_HOURS, # changed 'delete_old_media': False, 'days_to_keep': 14, - 'source_resolution': Source.SOURCE_RESOLUTION_1080P, + 'source_resolution': SourceResolution.VIDEO_1080P.value, 'source_vcodec': Source.SOURCE_VCODEC_VP9, 'source_acodec': Source.SOURCE_ACODEC_OPUS, 'prefer_60fps': False, @@ -342,7 +342,7 @@ class FrontEndTestCase(TestCase): index_schedule=IndexSchedule.EVERY_HOUR, delete_old_media=False, days_to_keep=14, - source_resolution=Source.SOURCE_RESOLUTION_1080P, + source_resolution=SourceResolution.VIDEO_1080P.value, source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, @@ -524,7 +524,7 @@ class FilepathTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=Source.SOURCE_RESOLUTION_1080P, + source_resolution=SourceResolution.VIDEO_1080P.value, source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, @@ -655,11 +655,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 = SourceResolution.AUDIO.value 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 = SourceResolution.VIDEO_1080P.value 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') @@ -684,7 +684,7 @@ class MediaTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=Source.SOURCE_RESOLUTION_1080P, + source_resolution=SourceResolution.VIDEO_1080P.value, source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, @@ -757,7 +757,7 @@ class MediaFilterTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=Source.SOURCE_RESOLUTION_1080P, + source_resolution=SourceResolution.VIDEO_1080P.value, source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, @@ -926,7 +926,7 @@ class FormatMatchingTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=Source.SOURCE_RESOLUTION_1080P, + source_resolution=SourceResolution.VIDEO_1080P.value, source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, @@ -1744,7 +1744,7 @@ class ResponseFilteringTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=Source.SOURCE_RESOLUTION_1080P, + source_resolution=SourceResolution.VIDEO_1080P.value, source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, From fb1db9da592521ffbdd7526905a87f00fca7cbe7 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 18:04:00 -0500 Subject: [PATCH 35/54] Add `V` function to save myself some effort --- tubesync/sync/choices.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index 08beaf10..8c7e2d45 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -13,6 +13,13 @@ DOMAINS = dict({ }) +def V(*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)') From a81d946980017199973cfb8860fb9e96a0d14546 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 18:09:51 -0500 Subject: [PATCH 36/54] Use `V` in `matching.py` --- tubesync/sync/matching.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/matching.py b/tubesync/sync/matching.py index a173197e..62593516 100644 --- a/tubesync/sync/matching.py +++ b/tubesync/sync/matching.py @@ -5,7 +5,7 @@ ''' -from .choices import Fallback +from .choices import V, Fallback from .utils import multi_key_sort from django.conf import settings @@ -390,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 == Fallback.NEXT_BEST_HD.value and + if (media.source.fallback == V(Fallback.NEXT_BEST_HD) and best_match['height'] >= fallback_hd_cutoff): return False, best_match['id'] - elif media.source.fallback == Fallback.NEXT_BEST.value: + elif media.source.fallback == V(Fallback.NEXT_BEST): return False, best_match['id'] # Nope, failed to find match return False, False From 150bb9a5f894ccd65b81a0e823d74459e00352cc Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 19:40:55 -0500 Subject: [PATCH 37/54] Use `V` for `tests.py` --- tubesync/sync/tests.py | 72 +++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index 52aa1ebe..c04e9ef7 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -19,8 +19,8 @@ from .models import Source, Media from .tasks import cleanup_old_media from .filtering import filter_media from .utils import filter_response -from .choices import (Fallback, IndexSchedule, SourceResolution, - youtube_long_source_types) +from .choices import (V, Fallback, IndexSchedule, SourceResolution, + YouTube_SourceType, youtube_long_source_types) class FrontEndTestCase(TestCase): @@ -118,7 +118,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') @@ -228,7 +228,7 @@ class FrontEndTestCase(TestCase): expected_categories) # Update the source key data = { - 'source_type': Source.SOURCE_TYPE_YOUTUBE_CHANNEL, + 'source_type': V(YouTube_SourceType.CHANNEL), 'key': 'updatedkey', # changed 'name': 'testname', 'directory': 'testdirectory', @@ -236,15 +236,15 @@ class FrontEndTestCase(TestCase): 'download_cap': 0, 'filter_text': '.*', 'filter_seconds_min': True, - 'index_schedule': IndexSchedule.EVERY_HOUR, + 'index_schedule': V(IndexSchedule.EVERY_HOUR), 'delete_old_media': False, 'days_to_keep': 14, - 'source_resolution': SourceResolution.VIDEO_1080P.value, + 'source_resolution': V(SourceResolution.VIDEO_1080P), 'source_vcodec': Source.SOURCE_VCODEC_VP9, 'source_acodec': Source.SOURCE_ACODEC_OPUS, 'prefer_60fps': False, 'prefer_hdr': False, - 'fallback': Fallback.FAIL.value, + 'fallback': V(Fallback.FAIL), 'sponsorblock_categories': data_categories, 'sub_langs': 'en', } @@ -265,7 +265,7 @@ 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': V(YouTube_SourceType.CHANNEL), 'key': 'updatedkey', 'name': 'testname', 'directory': 'testdirectory', @@ -273,15 +273,15 @@ class FrontEndTestCase(TestCase): 'download_cap': 0, 'filter_text': '.*', 'filter_seconds_min': True, - 'index_schedule': IndexSchedule.EVERY_2_HOURS, # changed + 'index_schedule': V(IndexSchedule.EVERY_2_HOURS), # changed 'delete_old_media': False, 'days_to_keep': 14, - 'source_resolution': SourceResolution.VIDEO_1080P.value, + 'source_resolution': V(SourceResolution.VIDEO_1080P), 'source_vcodec': Source.SOURCE_VCODEC_VP9, 'source_acodec': Source.SOURCE_ACODEC_OPUS, 'prefer_60fps': False, 'prefer_hdr': False, - 'fallback': Fallback.FAIL.value, + 'fallback': V(Fallback.FAIL), 'sponsorblock_categories': data_categories, 'sub_langs': 'en', } @@ -335,19 +335,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=V(YouTube_SourceType.CHANNEL), key='testkey', name='testname', directory='testdirectory', - index_schedule=IndexSchedule.EVERY_HOUR, + index_schedule=V(IndexSchedule.EVERY_HOUR), delete_old_media=False, days_to_keep=14, - source_resolution=SourceResolution.VIDEO_1080P.value, + source_resolution=V(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Fallback.FAIL.value + fallback=V(Fallback.FAIL) ) # Add some media test_minimal_metadata = ''' @@ -516,7 +516,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=V(YouTube_SourceType.CHANNEL), key='testkey', name='testname', directory='testdirectory', @@ -524,12 +524,12 @@ class FilepathTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=SourceResolution.VIDEO_1080P.value, + source_resolution=V(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Fallback.FAIL.value + fallback=V(Fallback.FAIL) ) # Add some test media self.media = Media.objects.create( @@ -655,11 +655,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 = SourceResolution.AUDIO.value + self.source.source_resolution = V(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 = SourceResolution.VIDEO_1080P.value + self.source.source_resolution = V(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') @@ -676,7 +676,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=V(YouTube_SourceType.CHANNEL), key='testkey', name='testname', directory='testdirectory', @@ -684,12 +684,12 @@ class MediaTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=SourceResolution.VIDEO_1080P.value, + source_resolution=V(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Fallback.FAIL.value + fallback=V(Fallback.FAIL) ) # Add some test media self.media = Media.objects.create( @@ -749,7 +749,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=V(YouTube_SourceType.CHANNEL), key="testkey", name="testname", directory="testdirectory", @@ -757,12 +757,12 @@ class MediaFilterTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=SourceResolution.VIDEO_1080P.value, + source_resolution=V(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Fallback.FAIL.value, + fallback=V(Fallback.FAIL), ) # Add some test media self.media = Media.objects.create( @@ -919,19 +919,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=V(YouTube_SourceType.CHANNEL), key='testkey', name='testname', directory='testdirectory', index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=SourceResolution.VIDEO_1080P.value, + source_resolution=V(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Fallback.FAIL.value + fallback=V(Fallback.FAIL) ) # Add some media self.media = Media.objects.create( @@ -941,7 +941,7 @@ class FormatMatchingTestCase(TestCase): ) def test_combined_exact_format_matching(self): - self.source.fallback = Fallback.FAIL.value + self.source.fallback = V(Fallback.FAIL) self.media.metadata = all_test_metadata['boring'] self.media.save() expected_matches = { @@ -1071,7 +1071,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expected_match_type) def test_audio_exact_format_matching(self): - self.source.fallback = Fallback.FAIL.value + self.source.fallback = V(Fallback.FAIL) self.media.metadata = all_test_metadata['boring'] self.media.save() expected_matches = { @@ -1217,7 +1217,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expeceted_match_type) def test_video_exact_format_matching(self): - self.source.fallback = Fallback.FAIL.value + self.source.fallback = V(Fallback.FAIL) # Test no 60fps, no HDR metadata self.media.metadata = all_test_metadata['boring'] self.media.save() @@ -1427,7 +1427,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expeceted_match_type) def test_video_next_best_format_matching(self): - self.source.fallback = Fallback.NEXT_BEST.value + self.source.fallback = V(Fallback.NEXT_BEST) # Test no 60fps, no HDR metadata self.media.metadata = all_test_metadata['boring'] self.media.save() @@ -1737,19 +1737,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=V(YouTube_SourceType.CHANNEL), key='testkey', name='testname', directory='testdirectory', index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=SourceResolution.VIDEO_1080P.value, + source_resolution=V(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=Fallback.FAIL.value + fallback=V(Fallback.FAIL) ) # Add some media self.media = Media.objects.create( From 76385001886e097e23b52b7c0dbd22619dd5abbb Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 20:30:32 -0500 Subject: [PATCH 38/54] Use `V` for `views.py` --- tubesync/sync/views.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tubesync/sync/views.py b/tubesync/sync/views.py index b7d37747..dead8c51 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -31,7 +31,7 @@ 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 (MediaServerType, SourceResolution, +from .choices import (V, MediaServerType, SourceResolution, YouTube_SourceType, youtube_long_source_types, youtube_help, youtube_validation_urls) from . import signals @@ -51,7 +51,7 @@ class DashboardView(TemplateView): # Sources data['num_sources'] = Source.objects.all().count() data['num_video_sources'] = Source.objects.filter( - ~Q(source_resolution=SourceResolution.AUDIO.value) + ~Q(source_resolution=V(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() @@ -170,9 +170,9 @@ class ValidateSourceView(FormView): help_examples = youtube_help.get('examples') validation_urls = youtube_validation_urls prepopulate_fields = { - YouTube_SourceType.CHANNEL.value: ('source_type', 'key', 'name', 'directory'), - YouTube_SourceType.CHANNEL_ID.value: ('source_type', 'key'), - YouTube_SourceType.PLAYLIST.value: ('source_type', 'key'), + V(YouTube_SourceType.CHANNEL): ('source_type', 'key', 'name', 'directory'), + V(YouTube_SourceType.CHANNEL_ID): ('source_type', 'key'), + V(YouTube_SourceType.PLAYLIST): ('source_type', 'key'), } def __init__(self, *args, **kwargs): @@ -898,11 +898,11 @@ class AddMediaServerView(FormView): template_name = 'sync/mediaserver-add.html' server_types = { - 'plex': MediaServerType.PLEX.value, + 'plex': V(MediaServerType.PLEX), } server_type_names = dict(MediaServerType.choices) forms = { - MediaServerType.PLEX.value: PlexMediaServerForm, + V(MediaServerType.PLEX): PlexMediaServerForm, } def __init__(self, *args, **kwargs): @@ -1019,7 +1019,7 @@ class UpdateMediaServerView(FormView, SingleObjectMixin): template_name = 'sync/mediaserver-update.html' model = MediaServer forms = { - MediaServerType.PLEX.value: PlexMediaServerForm, + V(MediaServerType.PLEX): PlexMediaServerForm, } def __init__(self, *args, **kwargs): From 58b98739ee6582e36c4bad5af2ac9bfe73d3aa7b Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 22:59:29 -0500 Subject: [PATCH 39/54] Use `V` for `models.py` --- tubesync/sync/models.py | 93 ++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 53 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 8e011fab..ae5b9eb0 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -26,9 +26,11 @@ from .matching import (get_best_combined_format, get_best_audio_format, get_best_video_format) from .mediaservers import PlexMediaServer from .fields import CommaSepChoiceField -from .choices import (CapChoices, Fallback, IndexSchedule, MediaServerType, +from .choices import (V, CapChoices, Fallback, FileExtension, + IndexSchedule, MediaServerType, SourceResolution, SourceResolutionInteger, - SponsorBlock_Category, YouTube_SourceType) + SponsorBlock_Category, YouTube_AudioCodec, + YouTube_SourceType, YouTube_VideoCodec) media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/') @@ -38,46 +40,31 @@ class Source(models.Model): or a YouTube playlist. ''' - SOURCE_TYPE_YOUTUBE_CHANNEL = YouTube_SourceType.CHANNEL.value - SOURCE_TYPE_YOUTUBE_CHANNEL_ID = YouTube_SourceType.CHANNEL_ID.value - SOURCE_TYPE_YOUTUBE_PLAYLIST = YouTube_SourceType.PLAYLIST.value + EXTENSIONS = FileExtension.values - SOURCE_RESOLUTION_1080P = SourceResolution.VIDEO_1080P.value - SOURCE_RESOLUTION_AUDIO = SourceResolution.AUDIO.value + SOURCE_TYPE_YOUTUBE_CHANNEL = V(YouTube_SourceType.CHANNEL) + SOURCE_TYPE_YOUTUBE_CHANNEL_ID = V(YouTube_SourceType.CHANNEL_ID) + SOURCE_TYPE_YOUTUBE_PLAYLIST = V(YouTube_SourceType.PLAYLIST) + + SOURCE_RESOLUTION_1080P = V(SourceResolution.VIDEO_1080P) + SOURCE_RESOLUTION_AUDIO = V(SourceResolution.AUDIO) SOURCE_RESOLUTIONS = SourceResolution.values - 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_VCODEC_VP9 = V(YouTube_VideoCodec.VP9) + SOURCE_VCODEC_CHOICES = list(reversed(YouTube_VideoCodec.choices[1:])) - 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')), - ) + SOURCE_ACODEC_OPUS = V(YouTube_AudioCodec.OPUS) + SOURCE_ACODEC_CHOICES = list(reversed(YouTube_AudioCodec.choices)) - FALLBACK_FAIL = Fallback.FAIL.value - FALLBACK_NEXT_BEST = Fallback.NEXT_BEST.value - FALLBACK_NEXT_BEST_HD = Fallback.NEXT_BEST_HD.value + FALLBACK_FAIL = V(Fallback.FAIL) + FALLBACK_NEXT_BEST = V(Fallback.NEXT_BEST) + FALLBACK_NEXT_BEST_HD = V(Fallback.NEXT_BEST_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, @@ -277,7 +264,7 @@ class Source(models.Model): max_length=8, db_index=True, choices=SOURCE_VCODEC_CHOICES, - default=SOURCE_VCODEC_VP9, + 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( @@ -285,7 +272,7 @@ class Source(models.Model): max_length=8, db_index=True, choices=SOURCE_ACODEC_CHOICES, - default=SOURCE_ACODEC_OPUS, + default=YouTube_AudioCodec.OPUS, help_text=_('Source audio codec, desired audio encoding format to download') ) prefer_60fps = models.BooleanField( @@ -374,7 +361,7 @@ class Source(models.Model): @property def is_audio(self): - return self.source_resolution == SourceResolution.AUDIO.value + return self.source_resolution == V(SourceResolution.AUDIO) @property def is_video(self): @@ -406,14 +393,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 == V(YouTube_AudioCodec.MP4A): + return V(FileExtension.M4A) + elif self.source_acodec == V(YouTube_AudioCodec.OPUS): + return V(FileExtension.OGG) else: raise ValueError('Unable to choose audio extension, uknown acodec') else: - return self.EXTENSION_MKV + return V(FileExtension.MKV) @classmethod def create_url(obj, source_type, key): @@ -434,7 +421,7 @@ class Source(models.Model): @property def format_summary(self): - if self.source_resolution == SourceResolution.AUDIO.value: + if self.is_audio: vc = 'none' else: vc = self.source_vcodec @@ -451,7 +438,7 @@ class Source(models.Model): @property def type_directory_path(self): if settings.SOURCE_DOWNLOAD_DIRECTORY_PREFIX: - if self.source_resolution == SourceResolution.AUDIO.value: + if self.is_audio: return Path(settings.DOWNLOAD_AUDIO_DIR) / self.directory else: return Path(settings.DOWNLOAD_VIDEO_DIR) / self.directory @@ -965,12 +952,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 != V(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 != V(SourceResolution.AUDIO): fps = str(self.downloaded_fps) fmt.append(f'{fps}fps') if self.downloaded_hdr: @@ -1294,19 +1281,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 == V(YouTube_AudioCodec.MP4A): return "audio/mp4" - elif acodec == "opus": + elif acodec == V(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 == V(YouTube_VideoCodec.AVC1): return 'video/mp4' + else: + return 'video/matroska' @property def nfoxml(self): @@ -1580,12 +1567,12 @@ class MediaServer(models.Model): A remote media server, such as a Plex server. ''' - SERVER_TYPE_PLEX = MediaServerType.PLEX.value + SERVER_TYPE_PLEX = V(MediaServerType.PLEX) ICONS = { - SERVER_TYPE_PLEX: '', + V(MediaServerType.PLEX): '', } HANDLERS = { - SERVER_TYPE_PLEX: PlexMediaServer, + V(MediaServerType.PLEX): PlexMediaServer, } server_type = models.CharField( From 312b9275c13103fca82e3802ddbf4cecc6bd53e8 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 23:07:25 -0500 Subject: [PATCH 40/54] Use `FileExtension` for `import-existing-media.py` --- tubesync/sync/management/commands/import-existing-media.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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)) From 43e6598a3176eb03ba5a663409f1421358ecc454 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 23:08:28 -0500 Subject: [PATCH 41/54] Remove unused variables --- tubesync/sync/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index ae5b9eb0..451313b3 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -40,8 +40,6 @@ class Source(models.Model): or a YouTube playlist. ''' - EXTENSIONS = FileExtension.values - SOURCE_TYPE_YOUTUBE_CHANNEL = V(YouTube_SourceType.CHANNEL) SOURCE_TYPE_YOUTUBE_CHANNEL_ID = V(YouTube_SourceType.CHANNEL_ID) SOURCE_TYPE_YOUTUBE_PLAYLIST = V(YouTube_SourceType.PLAYLIST) @@ -1567,7 +1565,6 @@ class MediaServer(models.Model): A remote media server, such as a Plex server. ''' - SERVER_TYPE_PLEX = V(MediaServerType.PLEX) ICONS = { V(MediaServerType.PLEX): '', } From edd320b786dbb6959f92bb66ae58363cd3cd49c0 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 23:26:14 -0500 Subject: [PATCH 42/54] Replace `Source.SOURCE_TYPE_YOUTUBE_PLAYLIST` --- tubesync/sync/signals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tubesync/sync/signals.py b/tubesync/sync/signals.py index fe245be5..a47ba4ae 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 V, 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 != V(YouTube_SourceType.PLAYLIST) and instance.copy_channel_images: download_source_images( str(instance.pk), priority=2, From f4b34bafd0f06782f4a97d692586b08eaf7953e6 Mon Sep 17 00:00:00 2001 From: tcely Date: Wed, 12 Feb 2025 23:52:35 -0500 Subject: [PATCH 43/54] Use `FilterSeconds` in `models.py` --- tubesync/sync/models.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 451313b3..dc38d290 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -27,7 +27,7 @@ from .matching import (get_best_combined_format, get_best_audio_format, from .mediaservers import PlexMediaServer from .fields import CommaSepChoiceField from .choices import (V, CapChoices, Fallback, FileExtension, - IndexSchedule, MediaServerType, + FilterSeconds, IndexSchedule, MediaServerType, SourceResolution, SourceResolutionInteger, SponsorBlock_Category, YouTube_AudioCodec, YouTube_SourceType, YouTube_VideoCodec) @@ -58,11 +58,6 @@ class Source(models.Model): FALLBACK_NEXT_BEST = V(Fallback.NEXT_BEST) FALLBACK_NEXT_BEST_HD = V(Fallback.NEXT_BEST_HD) - FILTER_SECONDS_CHOICES = ( - (True, _('Minimum Length')), - (False, _('Maximum Length')), - ) - sponsorblock_categories = CommaSepChoiceField( _(''), max_length=128, @@ -234,8 +229,8 @@ class Source(models.Model): ) filter_seconds_min = models.BooleanField( _('filter seconds min/max'), - choices=FILTER_SECONDS_CHOICES, - default=True, + choices=FilterSeconds.choices, + default=V(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') ) From 6768fe43854380e6e261981320bae795ee712d35 Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 13 Feb 2025 01:47:45 -0500 Subject: [PATCH 44/54] Use `MediaState` in `models.py` --- tubesync/sync/models.py | 194 +++++++++++++--------------------------- 1 file changed, 63 insertions(+), 131 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index dc38d290..49f56893 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -28,11 +28,12 @@ from .mediaservers import PlexMediaServer from .fields import CommaSepChoiceField from .choices import (V, CapChoices, Fallback, FileExtension, FilterSeconds, IndexSchedule, MediaServerType, - SourceResolution, SourceResolutionInteger, + 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): ''' @@ -85,35 +86,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', - } + KEY_FIELD = _srctype_dict('id') uuid = models.UUIDField( _('uuid'), @@ -563,109 +562,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, @@ -1404,19 +1336,19 @@ class Media(models.Model): def get_download_state(self, task=None): if self.downloaded: - return self.STATE_DOWNLOADED + return V(MediaState.DOWNLOADED) if task: if task.locked_by_pid_running(): - return self.STATE_DOWNLOADING + return V(MediaState.DOWNLOADING) elif task.has_error(): - return self.STATE_ERROR + return V(MediaState.ERROR) else: - return self.STATE_SCHEDULED + return V(MediaState.SCHEDULED) if self.skip: - return self.STATE_SKIPPED + return V(MediaState.SKIPPED) if not self.source.download_media: - return self.STATE_DISABLED_AT_SOURCE - return self.STATE_UNKNOWN + return V(MediaState.DISABLED_AT_SOURCE) + return V(MediaState.UNKNOWN) def get_download_state_icon(self, task=None): state = self.get_download_state(task) From 811bbdf57f6af28cdb67de77b504bd2463e2f3d6 Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 13 Feb 2025 01:54:55 -0500 Subject: [PATCH 45/54] fixup: missed one of the users of a removed attribute --- tubesync/sync/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 49f56893..a0225b41 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -1352,7 +1352,7 @@ class Media(models.Model): 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[V(MediaState.UNKNOWN)]) def download_media(self): format_str = self.get_format_str() From 1ccdd2e58b05b63a2b0bfa1409cf778607e6dc3f Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 13 Feb 2025 02:26:56 -0500 Subject: [PATCH 46/54] Integer is a closer fit for bool --- tubesync/sync/choices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index 8c7e2d45..02fce158 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -45,7 +45,7 @@ class FileExtension(models.TextChoices): MKV = 'mkv', _('Matroska Multimedia Container') -class FilterSeconds(models.TextChoices): +class FilterSeconds(models.IntegerChoices): MIN = True, _('Minimum Length') MAX = False, _('Maximum Length') From c702889653d2d4931ad0e0fcdd7dd9ab10c7c6c6 Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 13 Feb 2025 05:31:06 -0500 Subject: [PATCH 47/54] Adjusted test data for the new `filter_seconds_min` choices --- tubesync/sync/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index c04e9ef7..4d627082 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -179,7 +179,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, @@ -235,7 +235,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': V(IndexSchedule.EVERY_HOUR), 'delete_old_media': False, 'days_to_keep': 14, @@ -272,7 +272,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': V(IndexSchedule.EVERY_2_HOURS), # changed 'delete_old_media': False, 'days_to_keep': 14, From 8a044567a5c1d785c029cb27a962224d697fd53d Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 13 Feb 2025 08:23:06 -0500 Subject: [PATCH 48/54] Remove `SOURCE_TYPE_YOUTUBE_PLAYLIST` --- tubesync/sync/models.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index a0225b41..57592f0e 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -41,10 +41,6 @@ class Source(models.Model): or a YouTube playlist. ''' - SOURCE_TYPE_YOUTUBE_CHANNEL = V(YouTube_SourceType.CHANNEL) - SOURCE_TYPE_YOUTUBE_CHANNEL_ID = V(YouTube_SourceType.CHANNEL_ID) - SOURCE_TYPE_YOUTUBE_PLAYLIST = V(YouTube_SourceType.PLAYLIST) - SOURCE_RESOLUTION_1080P = V(SourceResolution.VIDEO_1080P) SOURCE_RESOLUTION_AUDIO = V(SourceResolution.AUDIO) SOURCE_RESOLUTIONS = SourceResolution.values @@ -353,7 +349,11 @@ class Source(models.Model): @property def is_audio(self): - return self.source_resolution == V(SourceResolution.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): @@ -442,7 +442,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) @@ -536,7 +536,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') @@ -1239,7 +1239,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: @@ -1388,7 +1388,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 From bc284568df1a860692de1f9b039ea0d79aedc4fd Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 13 Feb 2025 08:43:08 -0500 Subject: [PATCH 49/54] Remove unused `SOURCE_RESOLUTION*` --- tubesync/sync/models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 57592f0e..2e95ba03 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -41,10 +41,6 @@ class Source(models.Model): or a YouTube playlist. ''' - SOURCE_RESOLUTION_1080P = V(SourceResolution.VIDEO_1080P) - SOURCE_RESOLUTION_AUDIO = V(SourceResolution.AUDIO) - SOURCE_RESOLUTIONS = SourceResolution.values - SOURCE_VCODEC_VP9 = V(YouTube_VideoCodec.VP9) SOURCE_VCODEC_CHOICES = list(reversed(YouTube_VideoCodec.choices[1:])) From 465d584b8a0a864fcd834e71f593f0548221a45f Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 13 Feb 2025 21:09:53 -0500 Subject: [PATCH 50/54] Search and replace of `V` to `Val` Fixes #722 --- tubesync/sync/choices.py | 2 +- tubesync/sync/matching.py | 6 ++-- tubesync/sync/models.py | 54 +++++++++++++++---------------- tubesync/sync/signals.py | 4 +-- tubesync/sync/tests.py | 68 +++++++++++++++++++-------------------- tubesync/sync/views.py | 16 ++++----- 6 files changed, 75 insertions(+), 75 deletions(-) diff --git a/tubesync/sync/choices.py b/tubesync/sync/choices.py index 02fce158..f9614299 100644 --- a/tubesync/sync/choices.py +++ b/tubesync/sync/choices.py @@ -13,7 +13,7 @@ DOMAINS = dict({ }) -def V(*args): +def Val(*args): results = list( a.value if isinstance(a, models.enums.Choices) else a for a in args ) diff --git a/tubesync/sync/matching.py b/tubesync/sync/matching.py index 62593516..9390e6fa 100644 --- a/tubesync/sync/matching.py +++ b/tubesync/sync/matching.py @@ -5,7 +5,7 @@ ''' -from .choices import V, Fallback +from .choices import Val, Fallback from .utils import multi_key_sort from django.conf import settings @@ -390,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 == V(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 == V(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/models.py b/tubesync/sync/models.py index 2e95ba03..d7c1033c 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -26,7 +26,7 @@ from .matching import (get_best_combined_format, get_best_audio_format, get_best_video_format) from .mediaservers import PlexMediaServer from .fields import CommaSepChoiceField -from .choices import (V, CapChoices, Fallback, FileExtension, +from .choices import (Val, CapChoices, Fallback, FileExtension, FilterSeconds, IndexSchedule, MediaServerType, MediaState, SourceResolution, SourceResolutionInteger, SponsorBlock_Category, YouTube_AudioCodec, @@ -41,15 +41,15 @@ class Source(models.Model): or a YouTube playlist. ''' - SOURCE_VCODEC_VP9 = V(YouTube_VideoCodec.VP9) + SOURCE_VCODEC_VP9 = Val(YouTube_VideoCodec.VP9) SOURCE_VCODEC_CHOICES = list(reversed(YouTube_VideoCodec.choices[1:])) - SOURCE_ACODEC_OPUS = V(YouTube_AudioCodec.OPUS) + SOURCE_ACODEC_OPUS = Val(YouTube_AudioCodec.OPUS) SOURCE_ACODEC_CHOICES = list(reversed(YouTube_AudioCodec.choices)) - FALLBACK_FAIL = V(Fallback.FAIL) - FALLBACK_NEXT_BEST = V(Fallback.NEXT_BEST) - FALLBACK_NEXT_BEST_HD = V(Fallback.NEXT_BEST_HD) + FALLBACK_FAIL = Val(Fallback.FAIL) + FALLBACK_NEXT_BEST = Val(Fallback.NEXT_BEST) + FALLBACK_NEXT_BEST_HD = Val(Fallback.NEXT_BEST_HD) sponsorblock_categories = CommaSepChoiceField( _(''), @@ -221,7 +221,7 @@ class Source(models.Model): filter_seconds_min = models.BooleanField( _('filter seconds min/max'), choices=FilterSeconds.choices, - default=V(FilterSeconds.MIN), + 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') ) @@ -381,14 +381,14 @@ class Source(models.Model): depending on audio codec. ''' if self.is_audio: - if self.source_acodec == V(YouTube_AudioCodec.MP4A): - return V(FileExtension.M4A) - elif self.source_acodec == V(YouTube_AudioCodec.OPUS): - return V(FileExtension.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 V(FileExtension.MKV) + return Val(FileExtension.MKV) @classmethod def create_url(obj, source_type, key): @@ -873,12 +873,12 @@ class Media(models.Model): resolution = self.downloaded_format.lower() elif self.downloaded_height: resolution = f'{self.downloaded_height}p' - if self.downloaded_format != V(SourceResolution.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 != V(SourceResolution.AUDIO): + if self.downloaded_format != Val(SourceResolution.AUDIO): fps = str(self.downloaded_fps) fmt.append(f'{fps}fps') if self.downloaded_hdr: @@ -1203,15 +1203,15 @@ class Media(models.Model): if acodec is None: raise TypeError() # nothing here. acodec = acodec.upper() - if acodec == V(YouTube_AudioCodec.MP4A): + if acodec == Val(YouTube_AudioCodec.MP4A): return "audio/mp4" - elif acodec == V(YouTube_AudioCodec.OPUS): + elif acodec == Val(YouTube_AudioCodec.OPUS): return "audio/opus" else: # fall-fall-back. return 'audio/ogg' vcodec = vcodec.upper() - if vcodec == V(YouTube_VideoCodec.AVC1): + if vcodec == Val(YouTube_VideoCodec.AVC1): return 'video/mp4' else: return 'video/matroska' @@ -1332,23 +1332,23 @@ class Media(models.Model): def get_download_state(self, task=None): if self.downloaded: - return V(MediaState.DOWNLOADED) + return Val(MediaState.DOWNLOADED) if task: if task.locked_by_pid_running(): - return V(MediaState.DOWNLOADING) + return Val(MediaState.DOWNLOADING) elif task.has_error(): - return V(MediaState.ERROR) + return Val(MediaState.ERROR) else: - return V(MediaState.SCHEDULED) + return Val(MediaState.SCHEDULED) if self.skip: - return V(MediaState.SKIPPED) + return Val(MediaState.SKIPPED) if not self.source.download_media: - return V(MediaState.DISABLED_AT_SOURCE) - return V(MediaState.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[V(MediaState.UNKNOWN)]) + return self.STATE_ICONS.get(state, self.STATE_ICONS[Val(MediaState.UNKNOWN)]) def download_media(self): format_str = self.get_format_str() @@ -1489,10 +1489,10 @@ class MediaServer(models.Model): ''' ICONS = { - V(MediaServerType.PLEX): '', + Val(MediaServerType.PLEX): '', } HANDLERS = { - V(MediaServerType.PLEX): PlexMediaServer, + Val(MediaServerType.PLEX): PlexMediaServer, } server_type = models.CharField( diff --git a/tubesync/sync/signals.py b/tubesync/sync/signals.py index 157e97b1..ce847881 100644 --- a/tubesync/sync/signals.py +++ b/tubesync/sync/signals.py @@ -17,7 +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 V, YouTube_SourceType +from .choices import Val, YouTube_SourceType @receiver(pre_save, sender=Source) @@ -53,7 +53,7 @@ def source_post_save(sender, instance, created, **kwargs): priority=0, verbose_name=verbose_name.format(instance.name) ) - if instance.source_type != V(YouTube_SourceType.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 4d627082..071582a1 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -19,7 +19,7 @@ from .models import Source, Media from .tasks import cleanup_old_media from .filtering import filter_media from .utils import filter_response -from .choices import (V, Fallback, IndexSchedule, SourceResolution, +from .choices import (Val, Fallback, IndexSchedule, SourceResolution, YouTube_SourceType, youtube_long_source_types) @@ -228,7 +228,7 @@ class FrontEndTestCase(TestCase): expected_categories) # Update the source key data = { - 'source_type': V(YouTube_SourceType.CHANNEL), + 'source_type': Val(YouTube_SourceType.CHANNEL), 'key': 'updatedkey', # changed 'name': 'testname', 'directory': 'testdirectory', @@ -236,15 +236,15 @@ class FrontEndTestCase(TestCase): 'download_cap': 0, 'filter_text': '.*', 'filter_seconds_min': int(True), - 'index_schedule': V(IndexSchedule.EVERY_HOUR), + 'index_schedule': Val(IndexSchedule.EVERY_HOUR), 'delete_old_media': False, 'days_to_keep': 14, - 'source_resolution': V(SourceResolution.VIDEO_1080P), + 'source_resolution': Val(SourceResolution.VIDEO_1080P), 'source_vcodec': Source.SOURCE_VCODEC_VP9, 'source_acodec': Source.SOURCE_ACODEC_OPUS, 'prefer_60fps': False, 'prefer_hdr': False, - 'fallback': V(Fallback.FAIL), + 'fallback': Val(Fallback.FAIL), 'sponsorblock_categories': data_categories, 'sub_langs': 'en', } @@ -265,7 +265,7 @@ class FrontEndTestCase(TestCase): expected_categories) # Update the source index schedule which should recreate the scheduled task data = { - 'source_type': V(YouTube_SourceType.CHANNEL), + 'source_type': Val(YouTube_SourceType.CHANNEL), 'key': 'updatedkey', 'name': 'testname', 'directory': 'testdirectory', @@ -273,15 +273,15 @@ class FrontEndTestCase(TestCase): 'download_cap': 0, 'filter_text': '.*', 'filter_seconds_min': int(True), - 'index_schedule': V(IndexSchedule.EVERY_2_HOURS), # changed + 'index_schedule': Val(IndexSchedule.EVERY_2_HOURS), # changed 'delete_old_media': False, 'days_to_keep': 14, - 'source_resolution': V(SourceResolution.VIDEO_1080P), + 'source_resolution': Val(SourceResolution.VIDEO_1080P), 'source_vcodec': Source.SOURCE_VCODEC_VP9, 'source_acodec': Source.SOURCE_ACODEC_OPUS, 'prefer_60fps': False, 'prefer_hdr': False, - 'fallback': V(Fallback.FAIL), + 'fallback': Val(Fallback.FAIL), 'sponsorblock_categories': data_categories, 'sub_langs': 'en', } @@ -335,19 +335,19 @@ class FrontEndTestCase(TestCase): self.assertEqual(response.status_code, 200) # Add a test source test_source = Source.objects.create( - source_type=V(YouTube_SourceType.CHANNEL), + source_type=Val(YouTube_SourceType.CHANNEL), key='testkey', name='testname', directory='testdirectory', - index_schedule=V(IndexSchedule.EVERY_HOUR), + index_schedule=Val(IndexSchedule.EVERY_HOUR), delete_old_media=False, days_to_keep=14, - source_resolution=V(SourceResolution.VIDEO_1080P), + source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=V(Fallback.FAIL) + fallback=Val(Fallback.FAIL) ) # Add some media test_minimal_metadata = ''' @@ -516,7 +516,7 @@ class FilepathTestCase(TestCase): logging.disable(logging.CRITICAL) # Add a test source self.source = Source.objects.create( - source_type=V(YouTube_SourceType.CHANNEL), + source_type=Val(YouTube_SourceType.CHANNEL), key='testkey', name='testname', directory='testdirectory', @@ -524,12 +524,12 @@ class FilepathTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=V(SourceResolution.VIDEO_1080P), + source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=V(Fallback.FAIL) + fallback=Val(Fallback.FAIL) ) # Add some test media self.media = Media.objects.create( @@ -655,11 +655,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 = V(SourceResolution.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 = V(SourceResolution.VIDEO_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') @@ -676,7 +676,7 @@ class MediaTestCase(TestCase): logging.disable(logging.CRITICAL) # Add a test source self.source = Source.objects.create( - source_type=V(YouTube_SourceType.CHANNEL), + source_type=Val(YouTube_SourceType.CHANNEL), key='testkey', name='testname', directory='testdirectory', @@ -684,12 +684,12 @@ class MediaTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=V(SourceResolution.VIDEO_1080P), + source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=V(Fallback.FAIL) + fallback=Val(Fallback.FAIL) ) # Add some test media self.media = Media.objects.create( @@ -749,7 +749,7 @@ class MediaFilterTestCase(TestCase): # logging.disable(logging.CRITICAL) # Add a test source self.source = Source.objects.create( - source_type=V(YouTube_SourceType.CHANNEL), + source_type=Val(YouTube_SourceType.CHANNEL), key="testkey", name="testname", directory="testdirectory", @@ -757,12 +757,12 @@ class MediaFilterTestCase(TestCase): index_schedule=3600, delete_old_media=False, days_to_keep=14, - source_resolution=V(SourceResolution.VIDEO_1080P), + source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=V(Fallback.FAIL), + fallback=Val(Fallback.FAIL), ) # Add some test media self.media = Media.objects.create( @@ -919,19 +919,19 @@ class FormatMatchingTestCase(TestCase): logging.disable(logging.CRITICAL) # Add a test source self.source = Source.objects.create( - source_type=V(YouTube_SourceType.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=V(SourceResolution.VIDEO_1080P), + source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=V(Fallback.FAIL) + fallback=Val(Fallback.FAIL) ) # Add some media self.media = Media.objects.create( @@ -941,7 +941,7 @@ class FormatMatchingTestCase(TestCase): ) def test_combined_exact_format_matching(self): - self.source.fallback = V(Fallback.FAIL) + self.source.fallback = Val(Fallback.FAIL) self.media.metadata = all_test_metadata['boring'] self.media.save() expected_matches = { @@ -1071,7 +1071,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expected_match_type) def test_audio_exact_format_matching(self): - self.source.fallback = V(Fallback.FAIL) + self.source.fallback = Val(Fallback.FAIL) self.media.metadata = all_test_metadata['boring'] self.media.save() expected_matches = { @@ -1217,7 +1217,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expeceted_match_type) def test_video_exact_format_matching(self): - self.source.fallback = V(Fallback.FAIL) + self.source.fallback = Val(Fallback.FAIL) # Test no 60fps, no HDR metadata self.media.metadata = all_test_metadata['boring'] self.media.save() @@ -1427,7 +1427,7 @@ class FormatMatchingTestCase(TestCase): self.assertEqual(match_type, expeceted_match_type) def test_video_next_best_format_matching(self): - self.source.fallback = V(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() @@ -1737,19 +1737,19 @@ class ResponseFilteringTestCase(TestCase): logging.disable(logging.CRITICAL) # Add a test source self.source = Source.objects.create( - source_type=V(YouTube_SourceType.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=V(SourceResolution.VIDEO_1080P), + source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, source_acodec=Source.SOURCE_ACODEC_OPUS, prefer_60fps=False, prefer_hdr=False, - fallback=V(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 f7db753a..53c46f73 100644 --- a/tubesync/sync/views.py +++ b/tubesync/sync/views.py @@ -31,7 +31,7 @@ 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 (V, MediaServerType, SourceResolution, +from .choices import (Val, MediaServerType, SourceResolution, YouTube_SourceType, youtube_long_source_types, youtube_help, youtube_validation_urls) from . import signals @@ -51,7 +51,7 @@ class DashboardView(TemplateView): # Sources data['num_sources'] = Source.objects.all().count() data['num_video_sources'] = Source.objects.filter( - ~Q(source_resolution=V(SourceResolution.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() @@ -170,9 +170,9 @@ class ValidateSourceView(FormView): help_examples = youtube_help.get('examples') validation_urls = youtube_validation_urls prepopulate_fields = { - V(YouTube_SourceType.CHANNEL): ('source_type', 'key', 'name', 'directory'), - V(YouTube_SourceType.CHANNEL_ID): ('source_type', 'key'), - V(YouTube_SourceType.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): @@ -898,11 +898,11 @@ class AddMediaServerView(FormView): template_name = 'sync/mediaserver-add.html' server_types = { - 'plex': V(MediaServerType.PLEX), + 'plex': Val(MediaServerType.PLEX), } server_type_names = dict(MediaServerType.choices) forms = { - V(MediaServerType.PLEX): PlexMediaServerForm, + Val(MediaServerType.PLEX): PlexMediaServerForm, } def __init__(self, *args, **kwargs): @@ -1019,7 +1019,7 @@ class UpdateMediaServerView(FormView, SingleObjectMixin): template_name = 'sync/mediaserver-update.html' model = MediaServer forms = { - V(MediaServerType.PLEX): PlexMediaServerForm, + Val(MediaServerType.PLEX): PlexMediaServerForm, } def __init__(self, *args, **kwargs): From 0d6498805fe16a4343bd5abe2c4347a14e36e8e4 Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 13 Feb 2025 21:32:47 -0500 Subject: [PATCH 51/54] Remove `SOURCE_ACODEC_CHOICES` and `SOURCE_VCODEC_CHOICES` --- tubesync/sync/models.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index d7c1033c..6686dfa1 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -42,10 +42,7 @@ class Source(models.Model): ''' SOURCE_VCODEC_VP9 = Val(YouTube_VideoCodec.VP9) - SOURCE_VCODEC_CHOICES = list(reversed(YouTube_VideoCodec.choices[1:])) - SOURCE_ACODEC_OPUS = Val(YouTube_AudioCodec.OPUS) - SOURCE_ACODEC_CHOICES = list(reversed(YouTube_AudioCodec.choices)) FALLBACK_FAIL = Val(Fallback.FAIL) FALLBACK_NEXT_BEST = Val(Fallback.NEXT_BEST) @@ -247,7 +244,7 @@ class Source(models.Model): _('source video codec'), max_length=8, db_index=True, - choices=SOURCE_VCODEC_CHOICES, + 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)') ) @@ -255,7 +252,7 @@ class Source(models.Model): _('source audio codec'), max_length=8, db_index=True, - choices=SOURCE_ACODEC_CHOICES, + choices=list(reversed(YouTube_AudioCodec.choices)), default=YouTube_AudioCodec.OPUS, help_text=_('Source audio codec, desired audio encoding format to download') ) From bc6648b110a0d587658cf348aeee63e2f908a194 Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 13 Feb 2025 21:41:33 -0500 Subject: [PATCH 52/54] Remove `SOURCE_ACODEC_OPUS` --- tubesync/sync/models.py | 1 - tubesync/sync/tests.py | 17 +++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 6686dfa1..df733e9f 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -42,7 +42,6 @@ class Source(models.Model): ''' SOURCE_VCODEC_VP9 = Val(YouTube_VideoCodec.VP9) - SOURCE_ACODEC_OPUS = Val(YouTube_AudioCodec.OPUS) FALLBACK_FAIL = Val(Fallback.FAIL) FALLBACK_NEXT_BEST = Val(Fallback.NEXT_BEST) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index 071582a1..ace74c79 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -20,6 +20,7 @@ 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_SourceType, youtube_long_source_types) @@ -241,7 +242,7 @@ class FrontEndTestCase(TestCase): 'days_to_keep': 14, 'source_resolution': Val(SourceResolution.VIDEO_1080P), 'source_vcodec': Source.SOURCE_VCODEC_VP9, - 'source_acodec': Source.SOURCE_ACODEC_OPUS, + 'source_acodec': Val(YouTube_AudioCodec.OPUS), 'prefer_60fps': False, 'prefer_hdr': False, 'fallback': Val(Fallback.FAIL), @@ -278,7 +279,7 @@ class FrontEndTestCase(TestCase): 'days_to_keep': 14, 'source_resolution': Val(SourceResolution.VIDEO_1080P), 'source_vcodec': Source.SOURCE_VCODEC_VP9, - 'source_acodec': Source.SOURCE_ACODEC_OPUS, + 'source_acodec': Val(YouTube_AudioCodec.OPUS), 'prefer_60fps': False, 'prefer_hdr': False, 'fallback': Val(Fallback.FAIL), @@ -344,7 +345,7 @@ class FrontEndTestCase(TestCase): days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, - source_acodec=Source.SOURCE_ACODEC_OPUS, + source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, fallback=Val(Fallback.FAIL) @@ -526,7 +527,7 @@ class FilepathTestCase(TestCase): days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, - source_acodec=Source.SOURCE_ACODEC_OPUS, + source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, fallback=Val(Fallback.FAIL) @@ -686,7 +687,7 @@ class MediaTestCase(TestCase): days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, - source_acodec=Source.SOURCE_ACODEC_OPUS, + source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, fallback=Val(Fallback.FAIL) @@ -759,7 +760,7 @@ class MediaFilterTestCase(TestCase): days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, - source_acodec=Source.SOURCE_ACODEC_OPUS, + source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, fallback=Val(Fallback.FAIL), @@ -928,7 +929,7 @@ class FormatMatchingTestCase(TestCase): days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, - source_acodec=Source.SOURCE_ACODEC_OPUS, + source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, fallback=Val(Fallback.FAIL) @@ -1746,7 +1747,7 @@ class ResponseFilteringTestCase(TestCase): days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), source_vcodec=Source.SOURCE_VCODEC_VP9, - source_acodec=Source.SOURCE_ACODEC_OPUS, + source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, fallback=Val(Fallback.FAIL) From a9f9424dd53887bae5de6f55edf454ab1e3ff00c Mon Sep 17 00:00:00 2001 From: tcely Date: Thu, 13 Feb 2025 21:46:14 -0500 Subject: [PATCH 53/54] Remove `SOURCE_VCODEC_VP9` --- tubesync/sync/models.py | 2 -- tubesync/sync/tests.py | 18 +++++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index df733e9f..12b765db 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -41,8 +41,6 @@ class Source(models.Model): or a YouTube playlist. ''' - SOURCE_VCODEC_VP9 = Val(YouTube_VideoCodec.VP9) - FALLBACK_FAIL = Val(Fallback.FAIL) FALLBACK_NEXT_BEST = Val(Fallback.NEXT_BEST) FALLBACK_NEXT_BEST_HD = Val(Fallback.NEXT_BEST_HD) diff --git a/tubesync/sync/tests.py b/tubesync/sync/tests.py index ace74c79..1c99e82b 100644 --- a/tubesync/sync/tests.py +++ b/tubesync/sync/tests.py @@ -20,7 +20,7 @@ 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_AudioCodec, YouTube_VideoCodec, YouTube_SourceType, youtube_long_source_types) @@ -241,7 +241,7 @@ class FrontEndTestCase(TestCase): 'delete_old_media': False, 'days_to_keep': 14, 'source_resolution': Val(SourceResolution.VIDEO_1080P), - 'source_vcodec': Source.SOURCE_VCODEC_VP9, + 'source_vcodec': Val(YouTube_VideoCodec.VP9), 'source_acodec': Val(YouTube_AudioCodec.OPUS), 'prefer_60fps': False, 'prefer_hdr': False, @@ -278,7 +278,7 @@ class FrontEndTestCase(TestCase): 'delete_old_media': False, 'days_to_keep': 14, 'source_resolution': Val(SourceResolution.VIDEO_1080P), - 'source_vcodec': Source.SOURCE_VCODEC_VP9, + 'source_vcodec': Val(YouTube_VideoCodec.VP9), 'source_acodec': Val(YouTube_AudioCodec.OPUS), 'prefer_60fps': False, 'prefer_hdr': False, @@ -344,7 +344,7 @@ class FrontEndTestCase(TestCase): delete_old_media=False, days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), - source_vcodec=Source.SOURCE_VCODEC_VP9, + source_vcodec=Val(YouTube_VideoCodec.VP9), source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, @@ -526,7 +526,7 @@ class FilepathTestCase(TestCase): delete_old_media=False, days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), - source_vcodec=Source.SOURCE_VCODEC_VP9, + source_vcodec=Val(YouTube_VideoCodec.VP9), source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, @@ -686,7 +686,7 @@ class MediaTestCase(TestCase): delete_old_media=False, days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), - source_vcodec=Source.SOURCE_VCODEC_VP9, + source_vcodec=Val(YouTube_VideoCodec.VP9), source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, @@ -759,7 +759,7 @@ class MediaFilterTestCase(TestCase): delete_old_media=False, days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), - source_vcodec=Source.SOURCE_VCODEC_VP9, + source_vcodec=Val(YouTube_VideoCodec.VP9), source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, @@ -928,7 +928,7 @@ class FormatMatchingTestCase(TestCase): delete_old_media=False, days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), - source_vcodec=Source.SOURCE_VCODEC_VP9, + source_vcodec=Val(YouTube_VideoCodec.VP9), source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, @@ -1746,7 +1746,7 @@ class ResponseFilteringTestCase(TestCase): delete_old_media=False, days_to_keep=14, source_resolution=Val(SourceResolution.VIDEO_1080P), - source_vcodec=Source.SOURCE_VCODEC_VP9, + source_vcodec=Val(YouTube_VideoCodec.VP9), source_acodec=Val(YouTube_AudioCodec.OPUS), prefer_60fps=False, prefer_hdr=False, From 366e6fd864a54ae5370a4f48566f0d2ed40d521b Mon Sep 17 00:00:00 2001 From: tcely Date: Fri, 14 Feb 2025 22:23:13 -0500 Subject: [PATCH 54/54] Replace `FALLBACK_FAIL` Remove `FALLBACK_FAIL`, `FALLBACK_NEXT_BEST`, and `FALLBACK_NEXT_BEST_HD` --- tubesync/sync/models.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tubesync/sync/models.py b/tubesync/sync/models.py index 12b765db..d8a43649 100644 --- a/tubesync/sync/models.py +++ b/tubesync/sync/models.py @@ -41,10 +41,6 @@ class Source(models.Model): or a YouTube playlist. ''' - FALLBACK_FAIL = Val(Fallback.FAIL) - FALLBACK_NEXT_BEST = Val(Fallback.NEXT_BEST) - FALLBACK_NEXT_BEST_HD = Val(Fallback.NEXT_BEST_HD) - sponsorblock_categories = CommaSepChoiceField( _(''), max_length=128, @@ -452,7 +448,7 @@ class Source(models.Model): @property def can_fallback(self): - return self.fallback != self.FALLBACK_FAIL + return self.fallback != Val(Fallback.FAIL) @property def example_media_format_dict(self):