Fixed channel override not triggering multi-audio track MKV downloads.

Also adds proper `ValueError` handling in the channel overwrites
API view to return a structured 400 error response.
This commit is contained in:
Dakota Gravitt 2026-02-24 10:22:02 -06:00
parent 6fc2bf36b6
commit d5fc906641
7 changed files with 63 additions and 4 deletions

View File

@ -19,6 +19,7 @@ class ChannelOverwriteSerializer(
audio_multistream = serializers.BooleanField(
required=False, allow_null=True
)
audio_languages = serializers.CharField(required=False, allow_null=True)
autodelete_days = serializers.IntegerField(required=False, allow_null=True)
index_playlists = serializers.BooleanField(required=False, allow_null=True)
integrate_sponsorblock = serializers.BooleanField(

View File

@ -268,6 +268,7 @@ class YoutubeChannel(YouTubeItem):
"download_format",
"download_container",
"audio_multistream",
"audio_languages",
"autodelete_days",
"index_playlists",
"integrate_sponsorblock",

View File

@ -175,7 +175,11 @@ class ChannelApiView(ApiBaseView):
)
if overwrites := validated_data.get("channel_overwrites"):
channel_overwrites(channel_id, overwrites)
try:
channel_overwrites(channel_id, overwrites)
except ValueError as err:
error = ErrorResponseSerializer({"error": str(err)})
return Response(error.data, status=400)
if overwrites.get("index_playlists"):
index_channel_playlists.delay(channel_id)

View File

@ -391,6 +391,27 @@ class VideoDownloader(DownloaderBase):
if lang.strip()
]
def _is_audio_multistream_enabled(self, channel_id: str) -> bool:
"""return True if audio_multistream is enabled globally or via channel overwrite"""
overwrites = self.channel_overwrites.get(channel_id, {})
if "audio_multistream" in overwrites and overwrites["audio_multistream"] is not None:
return bool(overwrites["audio_multistream"])
return bool(self.config["downloads"].get("audio_multistream"))
@staticmethod
def _discover_audio_languages(formats: list[dict]) -> list[str]:
"""collect all unique language codes from available audio formats"""
seen: set[str] = set()
langs: list[str] = []
for f in formats:
if (f.get("acodec") or "none") == "none":
continue
lang = (f.get("language") or "").strip()
if lang and lang not in seen:
seen.add(lang)
langs.append(lang)
return langs
def _dl_single_vid(self, youtube_id: str, channel_id: str) -> bool:
"""download single video, with optional multi-language audio merging"""
obs = self.obs.copy()
@ -400,9 +421,20 @@ class VideoDownloader(DownloaderBase):
languages = self._get_audio_languages(channel_id)
hls_formats: dict[str, str] = {} # lang -> format_id for HLS post-process
if not languages and self._is_audio_multistream_enabled(channel_id):
# audio_multistream is on but no explicit language list auto-discover
print(f"{youtube_id}: audio_multistream enabled, auto-discovering languages")
formats = self._get_formats(youtube_id)
if formats:
languages = self._discover_audio_languages(formats)
print(f"{youtube_id}: discovered audio languages: {languages}")
else:
formats = None # will be fetched below if needed
if languages:
print(f"{youtube_id}: applying audio languages {languages}")
formats = self._get_formats(youtube_id)
if formats is None:
formats = self._get_formats(youtube_id)
if formats:
dash_fmt, hls_formats = self._resolve_audio_formats(
formats, languages

View File

@ -342,12 +342,17 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
if channel_overwrites.get("download_container"):
container = channel_overwrites.get("download_container")
# audio_languages forces mkv for multi-stream support
# audio_languages or audio_multistream forces mkv for multi-stream support
audio_languages = (
channel_overwrites.get("audio_languages")
or self.config["downloads"].get("audio_languages")
)
if audio_languages:
audio_multistream = (
channel_overwrites.get("audio_multistream")
if channel_overwrites.get("audio_multistream") is not None
else self.config["downloads"].get("audio_multistream")
)
if audio_languages or audio_multistream:
container = "mkv"
return container

View File

@ -50,6 +50,7 @@ const ChannelAbout = () => {
const [downloadContainer, setDownloadContainer] = useState<'mp4' | 'mkv' | null>(null);
const [audioMultistream, setAudioMultistream] = useState<boolean | null>(null);
const [audioMultistreamWarning, setAudioMultistreamWarning] = useState<string | null>(null);
const [audioLanguages, setAudioLanguages] = useState<string | null>(null);
const [autoDeleteAfter, setAutoDeleteAfter] = useState<number | null>(null);
const [indexPlaylists, setIndexPlaylists] = useState(false);
const [enableSponsorblock, setEnableSponsorblock] = useState<boolean | null>(null);
@ -76,6 +77,7 @@ const ChannelAbout = () => {
channelResponseData?.channel_overwrites?.audio_multistream ?? null,
);
setAudioMultistreamWarning(null);
setAudioLanguages(channelResponseData?.channel_overwrites?.audio_languages ?? null);
setAutoDeleteAfter(channelResponseData?.channel_overwrites?.autodelete_days ?? null);
setIndexPlaylists(channelResponseData?.channel_overwrites?.index_playlists ?? false);
setEnableSponsorblock(
@ -356,6 +358,19 @@ const ChannelAbout = () => {
<p className="settings-error">{audioMultistreamWarning}</p>
)}
</div>
<div className="settings-box-wrapper">
<div>
<p>Audio Languages</p>
</div>
<InputConfig
type="text"
name="audio_languages"
value={audioLanguages}
setValue={setAudioLanguages}
oldValue={channel.channel_overwrites?.audio_languages ?? null}
updateCallback={handleUpdateConfig}
/>
</div>
<div className="settings-box-wrapper">
<div>
<p>

View File

@ -22,6 +22,7 @@ type ChannelOverwritesType = {
download_format: string | null;
download_container: 'mp4' | 'mkv' | null;
audio_multistream: boolean | null;
audio_languages: string | null;
autodelete_days: number | null;
index_playlists: boolean | null;
integrate_sponsorblock: boolean | null;