refactor: rename Source.prefer_audio_track to audio_track
Per review: 'prefer_audio_track' reads like the prefer_60fps/prefer_hdr booleans, but this is a pick-one choice. Renamed to audio_track and regenerated the migration (as 0038_source_audio_track) - it had drifted after the switch to single-letter values: default='original' and the long choices under max_length=1 would fail the AddField back-fill on Postgres. Also addresses the lint findings in the touched files: - matching.py: collapse nested prefer_60fps/prefer_hdr ifs (SIM102), fold matched_resolution and the video fallback branches (SIM114) - test_frontend.py: str.removeprefix, dict.values(), and a DTZ001 noqa on the deliberately naive datetime that make_aware() wraps
This commit is contained in:
@@ -19,7 +19,7 @@ SourceForm = forms.modelform_factory(
|
||||
'source_type', 'key', 'name', 'directory', 'filter_text', 'filter_text_invert', 'filter_seconds', 'filter_seconds_min',
|
||||
'media_format', 'target_schedule', 'index_schedule', 'index_videos', 'index_streams', 'download_media',
|
||||
'download_cap', 'delete_old_media', 'days_to_keep', 'source_resolution', 'source_vcodec', 'source_acodec',
|
||||
'prefer_60fps', 'prefer_hdr', 'prefer_audio_track', 'fallback', 'delete_removed_media', 'delete_files_on_disk', 'copy_channel_images',
|
||||
'prefer_60fps', 'prefer_hdr', 'audio_track', 'fallback', 'delete_removed_media', 'delete_files_on_disk', 'copy_channel_images',
|
||||
'copy_thumbnails', 'write_nfo', 'write_json', 'embed_metadata', 'embed_thumbnail',
|
||||
'enable_sponsorblock', 'sponsorblock_categories', 'write_subtitles', 'auto_subtitles', 'sub_langs',
|
||||
),
|
||||
|
||||
@@ -71,13 +71,11 @@ def get_best_combined_format(media):
|
||||
if media.source.source_acodec != fmt['acodec']:
|
||||
continue
|
||||
# if the source prefers 60fps, check for it
|
||||
if media.source.prefer_60fps:
|
||||
if not fmt['is_60fps']:
|
||||
continue
|
||||
if media.source.prefer_60fps and not fmt['is_60fps']:
|
||||
continue
|
||||
# If the source prefers HDR, check for it
|
||||
if media.source.prefer_hdr:
|
||||
if not fmt['is_hdr']:
|
||||
continue
|
||||
if media.source.prefer_hdr and not fmt['is_hdr']:
|
||||
continue
|
||||
# If we reach here, we have a combined match!
|
||||
matches.add(fmt['id'])
|
||||
by_fmt_id[fmt['id']] = fmt
|
||||
@@ -92,7 +90,7 @@ def get_best_combined_format(media):
|
||||
return False, False
|
||||
|
||||
# honour the source's audio-track preference
|
||||
prefer = Val(media.source.prefer_audio_track)
|
||||
prefer = Val(media.source.audio_track)
|
||||
preferred = by_fmt_id.get(f'~{prefer}~')
|
||||
if preferred and 'id' in preferred:
|
||||
return True, preferred['id']
|
||||
@@ -133,7 +131,7 @@ def get_best_audio_format(media):
|
||||
# Media has no audio formats at all
|
||||
return False, False
|
||||
source_acodec = media.source.source_acodec
|
||||
prefer = Val(media.source.prefer_audio_track)
|
||||
prefer = Val(media.source.audio_track)
|
||||
# Among formats with the requested codec, honour the audio-track preference.
|
||||
# A codec match still beats the track preference: if the source wants OPUS
|
||||
# and only the "original" track is MP4A, the OPUS "default" track wins.
|
||||
@@ -171,11 +169,10 @@ def get_best_video_format(media):
|
||||
media.source.fallback != Val(Fallback.REQUIRE_CODEC)
|
||||
)
|
||||
def matched_resolution(fmt):
|
||||
if fmt['format'] == source_resolution:
|
||||
return True
|
||||
elif fmt['height'] == source_resolution_height:
|
||||
return True
|
||||
return False
|
||||
return (
|
||||
fmt['format'] == source_resolution or
|
||||
fmt['height'] == source_resolution_height
|
||||
)
|
||||
# Filter video-only formats by resolution that matches the source
|
||||
video_formats = []
|
||||
sort_keys = [('height', False), ('vcodec', True), ('vbr', False)] # key, reverse
|
||||
@@ -514,13 +511,14 @@ 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 == Val(Fallback.REQUIRE_HD) and
|
||||
best_match['height'] >= fallback_hd_cutoff):
|
||||
return False, best_match['id']
|
||||
elif (media.source.fallback == Val(Fallback.REQUIRE_CODEC) and
|
||||
source_vcodec == best_match['vcodec']):
|
||||
return False, best_match['id']
|
||||
elif media.source.fallback == Val(Fallback.NEXT_BEST_RESOLUTION):
|
||||
accept_fallback = (
|
||||
(media.source.fallback == Val(Fallback.REQUIRE_HD) and
|
||||
best_match['height'] >= fallback_hd_cutoff) or
|
||||
(media.source.fallback == Val(Fallback.REQUIRE_CODEC) and
|
||||
source_vcodec == best_match['vcodec']) or
|
||||
(media.source.fallback == Val(Fallback.NEXT_BEST_RESOLUTION))
|
||||
)
|
||||
if accept_fallback:
|
||||
return False, best_match['id']
|
||||
# Nope, failed to find match
|
||||
return False, False
|
||||
|
||||
18
tubesync/sync/migrations/0038_source_audio_track.py
Normal file
18
tubesync/sync/migrations/0038_source_audio_track.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.8 on 2026-09-08 14:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sync', '0037_alter_source_fallback'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='source',
|
||||
name='audio_track',
|
||||
field=models.CharField(choices=[('o', 'Original audio (recorded language)'), ('d', "Publisher's default audio (often an auto-dub)")], db_index=True, default='o', help_text='Which audio track to download when the source publishes more than one. "Original audio" is the track in the language the video was actually recorded in. "Publisher\'s default audio" is whichever track the uploader set as the default for viewers, which for many channels is an automatic dubbed translation. This setting has no effect when a video only offers a single audio track.', max_length=1, verbose_name='audio track'),
|
||||
),
|
||||
]
|
||||
@@ -1,35 +0,0 @@
|
||||
# Generated by Django 5.2.11 on 2026-09-07 00:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sync', '0037_alter_source_fallback'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='source',
|
||||
name='prefer_audio_track',
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('original', 'Original audio (recorded language)'),
|
||||
('default', "Publisher's default audio (often an auto-dub)"),
|
||||
],
|
||||
db_index=True,
|
||||
default='original',
|
||||
help_text=(
|
||||
'Which audio track to download when the source publishes more than one. '
|
||||
'"Original audio" is the track in the language the video was actually '
|
||||
'recorded in. "Publisher\'s default audio" is whichever track the uploader '
|
||||
'set as the default for viewers, which for many channels is an automatic '
|
||||
'dubbed translation. This setting has no effect when a video only offers a '
|
||||
'single audio track.'
|
||||
),
|
||||
max_length=1,
|
||||
verbose_name='prefer audio track',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -252,8 +252,8 @@ class Source(db.models.Model):
|
||||
default=False,
|
||||
help_text=_('Where possible, prefer HDR media for this source'),
|
||||
)
|
||||
prefer_audio_track = db.models.CharField(
|
||||
_('prefer audio track'),
|
||||
audio_track = db.models.CharField(
|
||||
_('audio track'),
|
||||
max_length=1,
|
||||
db_index=True,
|
||||
choices=AudioTrack.choices,
|
||||
@@ -483,7 +483,7 @@ class Source(db.models.Model):
|
||||
ac = self.source_acodec
|
||||
f = ' 60FPS' if self.is_video and self.prefer_60fps else ''
|
||||
h = ' HDR' if self.is_video and self.prefer_hdr else ''
|
||||
a = '' if self.prefer_audio_track == Val(AudioTrack.ORIGINAL) else ' DEFAULT-AUDIO'
|
||||
a = '' if self.audio_track == Val(AudioTrack.ORIGINAL) else ' DEFAULT-AUDIO'
|
||||
return f'{self.source_resolution} (video:{vc}, audio:{ac}){f}{h}{a}'.strip()
|
||||
|
||||
@property
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
<td><span class="hide-on-med-and-up">Prefer HDR?<br></span><strong>{% if source.prefer_hdr %}<i class="fas fa-check"></i>{% else %}<i class="fas fa-times"></i>{% endif %}</strong></td>
|
||||
</tr>
|
||||
<tr title="Which audio track to download when a video has more than one">
|
||||
<td class="hide-on-small-only">Preferred audio track</td>
|
||||
<td><span class="hide-on-med-and-up">Preferred audio track<br></span><strong>{{ source.get_prefer_audio_track_display }}</strong></td>
|
||||
<td class="hide-on-small-only">Audio track</td>
|
||||
<td><span class="hide-on-med-and-up">Audio track<br></span><strong>{{ source.get_audio_track_display }}</strong></td>
|
||||
</tr>
|
||||
<tr title="Output file extension">
|
||||
<td class="hide-on-small-only">Output extension</td>
|
||||
|
||||
@@ -337,18 +337,18 @@ class FormatMatchingTestCase(TestCase):
|
||||
self.media.metadata = all_test_metadata['multi_audio']
|
||||
self.media.save()
|
||||
expected_matches = {
|
||||
# (acodec, prefer_audio_track): (match_type, code)
|
||||
# (acodec, audio_track): (match_type, code)
|
||||
(Val(YouTube_AudioCodec.OPUS), Val(AudioTrack.ORIGINAL)): (True, '251-en'),
|
||||
(Val(YouTube_AudioCodec.OPUS), Val(AudioTrack.DEFAULT)): (True, '251-es'),
|
||||
(Val(YouTube_AudioCodec.MP4A), Val(AudioTrack.ORIGINAL)): (True, '140-en'),
|
||||
(Val(YouTube_AudioCodec.MP4A), Val(AudioTrack.DEFAULT)): (True, '140-es'),
|
||||
}
|
||||
for params, expected in expected_matches.items():
|
||||
acodec, prefer_audio_track = params
|
||||
acodec, audio_track = params
|
||||
self.source.source_acodec = acodec
|
||||
self.source.prefer_audio_track = prefer_audio_track
|
||||
self.source.audio_track = audio_track
|
||||
self.assertEqual(self.media.get_best_audio_format(), expected,
|
||||
msg=f'{acodec} / {prefer_audio_track}')
|
||||
msg=f'{acodec} / {audio_track}')
|
||||
|
||||
def test_audio_track_preference_combined(self):
|
||||
self.source.fallback = Val(Fallback.FAIL)
|
||||
@@ -359,18 +359,18 @@ class FormatMatchingTestCase(TestCase):
|
||||
self.media.metadata = all_test_metadata['multi_audio']
|
||||
self.media.save()
|
||||
expected_matches = {
|
||||
# (resolution, prefer_audio_track): (match_type, code)
|
||||
# (resolution, audio_track): (match_type, code)
|
||||
('360p', Val(AudioTrack.ORIGINAL)): (True, '18-en'),
|
||||
('360p', Val(AudioTrack.DEFAULT)): (True, '18-es'),
|
||||
('720p', Val(AudioTrack.ORIGINAL)): (True, '22-en'),
|
||||
('720p', Val(AudioTrack.DEFAULT)): (True, '22-es'),
|
||||
}
|
||||
for params, expected in expected_matches.items():
|
||||
resolution, prefer_audio_track = params
|
||||
resolution, audio_track = params
|
||||
self.source.source_resolution = resolution
|
||||
self.source.prefer_audio_track = prefer_audio_track
|
||||
self.source.audio_track = audio_track
|
||||
self.assertEqual(self.media.get_best_combined_format(), expected,
|
||||
msg=f'{resolution} / {prefer_audio_track}')
|
||||
msg=f'{resolution} / {audio_track}')
|
||||
|
||||
def test_audio_track_preference_split_video_plus_audio(self):
|
||||
self.source.fallback = Val(Fallback.NEXT_BEST_RESOLUTION)
|
||||
@@ -379,9 +379,9 @@ class FormatMatchingTestCase(TestCase):
|
||||
self.source.source_acodec = Val(YouTube_AudioCodec.OPUS)
|
||||
self.media.metadata = all_test_metadata['multi_audio']
|
||||
self.media.save()
|
||||
self.source.prefer_audio_track = Val(AudioTrack.ORIGINAL)
|
||||
self.source.audio_track = Val(AudioTrack.ORIGINAL)
|
||||
self.assertEqual(self.media.get_format_str(), '248+251-en')
|
||||
self.source.prefer_audio_track = Val(AudioTrack.DEFAULT)
|
||||
self.source.audio_track = Val(AudioTrack.DEFAULT)
|
||||
self.assertEqual(self.media.get_format_str(), '248+251-es')
|
||||
|
||||
def test_audio_track_preference_no_marker_is_noop(self):
|
||||
@@ -391,7 +391,7 @@ class FormatMatchingTestCase(TestCase):
|
||||
self.media.metadata = all_test_metadata['boring']
|
||||
self.media.save()
|
||||
for prefer in (Val(AudioTrack.ORIGINAL), Val(AudioTrack.DEFAULT)):
|
||||
self.source.prefer_audio_track = prefer
|
||||
self.source.audio_track = prefer
|
||||
self.source.source_resolution = Val(SourceResolution.AUDIO)
|
||||
self.source.source_acodec = Val(YouTube_AudioCodec.OPUS)
|
||||
self.assertEqual(self.media.get_best_audio_format(), (True, '251'), msg=prefer)
|
||||
|
||||
@@ -118,7 +118,7 @@ class FrontEndTestCase(TestCase):
|
||||
c = Client()
|
||||
response = c.get('/source-validate')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
for (source_type, tests) in test_sources.items():
|
||||
for tests in test_sources.values():
|
||||
for test, urls in tests.items():
|
||||
for url in urls:
|
||||
data = {'source_url': url}
|
||||
@@ -185,7 +185,7 @@ class FrontEndTestCase(TestCase):
|
||||
'source_acodec': 'OPUS',
|
||||
'prefer_60fps': False,
|
||||
'prefer_hdr': False,
|
||||
'prefer_audio_track': 'o',
|
||||
'audio_track': 'o',
|
||||
'fallback': 'f',
|
||||
'sponsorblock_categories': data_categories,
|
||||
'sub_langs': 'en',
|
||||
@@ -194,8 +194,7 @@ class FrontEndTestCase(TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
url_parts = urlsplit(response.url)
|
||||
url_path = str(url_parts.path).strip()
|
||||
if url_path.startswith('/'):
|
||||
url_path = url_path[1:]
|
||||
url_path = url_path.removeprefix('/')
|
||||
path_parts = url_path.split('/')
|
||||
self.assertEqual(path_parts[0], 'source')
|
||||
source_uuid = path_parts[1]
|
||||
@@ -247,7 +246,7 @@ class FrontEndTestCase(TestCase):
|
||||
'source_acodec': Val(YouTube_AudioCodec.OPUS),
|
||||
'prefer_60fps': False,
|
||||
'prefer_hdr': False,
|
||||
'prefer_audio_track': 'o',
|
||||
'audio_track': 'o',
|
||||
'fallback': Val(Fallback.FAIL),
|
||||
'sponsorblock_categories': data_categories,
|
||||
'sub_langs': 'en',
|
||||
@@ -256,8 +255,7 @@ class FrontEndTestCase(TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
url_parts = urlsplit(response.url)
|
||||
url_path = str(url_parts.path).strip()
|
||||
if url_path.startswith('/'):
|
||||
url_path = url_path[1:]
|
||||
url_path = url_path.removeprefix('/')
|
||||
path_parts = url_path.split('/')
|
||||
self.assertEqual(path_parts[0], 'source')
|
||||
source_uuid = path_parts[1]
|
||||
@@ -285,7 +283,7 @@ class FrontEndTestCase(TestCase):
|
||||
'source_acodec': Val(YouTube_AudioCodec.OPUS),
|
||||
'prefer_60fps': False,
|
||||
'prefer_hdr': False,
|
||||
'prefer_audio_track': 'o',
|
||||
'audio_track': 'o',
|
||||
'fallback': Val(Fallback.FAIL),
|
||||
'sponsorblock_categories': data_categories,
|
||||
'sub_langs': 'en',
|
||||
@@ -294,8 +292,7 @@ class FrontEndTestCase(TestCase):
|
||||
self.assertEqual(response.status_code, 302)
|
||||
url_parts = urlsplit(response.url)
|
||||
url_path = str(url_parts.path).strip()
|
||||
if url_path.startswith('/'):
|
||||
url_path = url_path[1:]
|
||||
url_path = url_path.removeprefix('/')
|
||||
path_parts = url_path.split('/')
|
||||
self.assertEqual(path_parts[0], 'source')
|
||||
source_uuid = path_parts[1]
|
||||
@@ -352,7 +349,7 @@ class FrontEndTestCase(TestCase):
|
||||
# Add some media
|
||||
test_minimal_metadata = all_test_metadata['minimal']
|
||||
before_dt = timezone.now()
|
||||
past_date = timezone.make_aware(datetime(year=2000, month=1, day=1))
|
||||
past_date = timezone.make_aware(datetime(year=2000, month=1, day=1)) # noqa: DTZ001
|
||||
test_media1 = Media.objects.create(
|
||||
key='mediakey1',
|
||||
source=test_source,
|
||||
|
||||
Reference in New Issue
Block a user