refactor(downloads): relax mkv enforcement for audio multistream if video does not include additional audio tracks
- Remove API-level validation that rejected `audio_multistream` unless the container was explicitly set to `mkv` in both app config and channel update endpoints. - Update yt-dlp option building to only force MKV output when multiple audio tracks are actually selected, instead of always forcing MKV whenever multistream is enabled. This keeps multistream behavior flexible and avoids unnecessary container restrictions for single-track selections.
This commit is contained in:
parent
776cc2bea1
commit
5bdc56c865
|
|
@ -182,26 +182,6 @@ class AppConfigApiView(ApiBaseView):
|
|||
serializer = AppConfigSerializer(data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
validated_data = serializer.validated_data
|
||||
current_config = AppConfig().config
|
||||
downloads_update = validated_data.get("downloads") or {}
|
||||
if downloads_update:
|
||||
audio_multistream = downloads_update.get(
|
||||
"audio_multistream",
|
||||
current_config["downloads"]["audio_multistream"],
|
||||
)
|
||||
container = downloads_update.get(
|
||||
"container",
|
||||
current_config["downloads"]["container"],
|
||||
)
|
||||
if audio_multistream and container != "mkv":
|
||||
error = ErrorResponseSerializer(
|
||||
{
|
||||
"error": (
|
||||
"audio_multistream requires mkv container"
|
||||
)
|
||||
}
|
||||
)
|
||||
return Response(error.data, status=400)
|
||||
updated_config = AppConfig().update_config(validated_data)
|
||||
updated_serializer = AppConfigSerializer(updated_config)
|
||||
return Response(updated_serializer.data)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ from channel.serializers import (
|
|||
ChannelUpdateSerializer,
|
||||
)
|
||||
from channel.src.index import YoutubeChannel, channel_overwrites
|
||||
from appsettings.src.config import AppConfig
|
||||
from channel.src.nav import ChannelNav
|
||||
from common.serializers import ErrorResponseSerializer
|
||||
from common.src.urlparser import Parser
|
||||
|
|
@ -145,29 +144,6 @@ class ChannelApiView(ApiBaseView):
|
|||
serializer.is_valid(raise_exception=True)
|
||||
validated_data = serializer.validated_data
|
||||
|
||||
current_overwrites = self.response.get("channel_overwrites", {})
|
||||
global_container = AppConfig().config["downloads"]["container"]
|
||||
if overwrites := validated_data.get("channel_overwrites"):
|
||||
audio_multistream = overwrites.get(
|
||||
"audio_multistream",
|
||||
current_overwrites.get("audio_multistream"),
|
||||
)
|
||||
container = overwrites.get(
|
||||
"download_container",
|
||||
current_overwrites.get("download_container"),
|
||||
)
|
||||
if container is None:
|
||||
container = global_container
|
||||
if audio_multistream and container != "mkv":
|
||||
error = ErrorResponseSerializer(
|
||||
{
|
||||
"error": (
|
||||
"audio_multistream requires mkv container"
|
||||
)
|
||||
}
|
||||
)
|
||||
return Response(error.data, status=400)
|
||||
|
||||
subscribed = validated_data.get("channel_subscribed")
|
||||
if subscribed is not None:
|
||||
YoutubeChannel(channel_id).change_subscribe(
|
||||
|
|
|
|||
|
|
@ -427,6 +427,7 @@ class VideoDownloader(DownloaderBase):
|
|||
|
||||
languages = self._get_audio_languages(channel_id)
|
||||
hls_formats: dict[str, str] = {} # lang -> format_id for HLS post-process
|
||||
selected_audio_count = 0
|
||||
|
||||
if not languages and self._is_audio_multistream_enabled(channel_id):
|
||||
# audio_multistream is on but no explicit language list – auto-discover
|
||||
|
|
@ -446,16 +447,20 @@ class VideoDownloader(DownloaderBase):
|
|||
dash_fmt, hls_formats = self._resolve_audio_formats(
|
||||
formats, languages
|
||||
)
|
||||
selected_audio_count = len(dash_fmt) + len(hls_formats)
|
||||
main_fmt = self._build_main_format(formats, dash_fmt)
|
||||
if main_fmt:
|
||||
obs["format"] = main_fmt
|
||||
if len(dash_fmt) > 1:
|
||||
obs["audio_multistreams"] = True
|
||||
obs["merge_output_format"] = "mkv"
|
||||
obs["outtmpl"] = self.CACHE_DIR + "/download/%(id)s.mkv"
|
||||
# Only force MKV when multiple audio tracks are actually selected.
|
||||
if selected_audio_count > 1:
|
||||
obs["merge_output_format"] = "mkv"
|
||||
obs["outtmpl"] = self.CACHE_DIR + "/download/%(id)s.mkv"
|
||||
print(f"{youtube_id}: main format: {main_fmt}")
|
||||
elif hls_formats:
|
||||
# all langs are HLS-only; keep default format for video+audio
|
||||
elif hls_formats and selected_audio_count > 1:
|
||||
# all selected langs are HLS-only and we have >1 tracks,
|
||||
# so force MKV for audio track merge.
|
||||
obs["merge_output_format"] = "mkv"
|
||||
obs["outtmpl"] = self.CACHE_DIR + "/download/%(id)s.mkv"
|
||||
|
||||
|
|
@ -465,7 +470,7 @@ class VideoDownloader(DownloaderBase):
|
|||
return False
|
||||
|
||||
# phase 2: merge HLS-only language audio tracks via ffmpeg
|
||||
if hls_formats:
|
||||
if hls_formats and selected_audio_count > 1:
|
||||
main_path = os.path.join(dl_cache, f"{youtube_id}.mkv")
|
||||
audio_files = []
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -328,6 +328,24 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
|
|||
def add_file_path(self):
|
||||
"""build media_url for where file will be located"""
|
||||
container = self._get_download_container()
|
||||
|
||||
# Prefer the actually downloaded cache file extension when available.
|
||||
# This keeps media_url aligned with effective runtime output when
|
||||
# multistream is enabled but only a single audio track is selected.
|
||||
cache_dir = os.path.join(EnvironmentSettings.CACHE_DIR, "download")
|
||||
preferred_order = [container, "mp4", "mkv"]
|
||||
seen: set[str] = set()
|
||||
for ext in preferred_order:
|
||||
if ext in seen:
|
||||
continue
|
||||
seen.add(ext)
|
||||
candidate = os.path.join(
|
||||
cache_dir, f"{self.json_data['youtube_id']}.{ext}"
|
||||
)
|
||||
if os.path.exists(candidate):
|
||||
container = ext
|
||||
break
|
||||
|
||||
self.json_data["media_url"] = os.path.join(
|
||||
self.json_data["channel"]["channel_id"],
|
||||
self.json_data["youtube_id"] + f".{container}",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import useIsAdmin from '../functions/useIsAdmin';
|
|||
import InputConfig from '../components/InputConfig';
|
||||
import ToggleConfig from '../components/ToggleConfig';
|
||||
import { useUserConfigStore } from '../stores/UserConfigStore';
|
||||
import { useAppSettingsStore } from '../stores/AppSettingsStore';
|
||||
import { ApiResponseType } from '../functions/APIClient';
|
||||
|
||||
export type ChannelBaseOutletContextType = {
|
||||
|
|
@ -36,7 +35,6 @@ type ChannelAboutParams = {
|
|||
const ChannelAbout = () => {
|
||||
const { channelId } = useParams() as ChannelAboutParams;
|
||||
const { userConfig } = useUserConfigStore();
|
||||
const { appSettingsConfig } = useAppSettingsStore();
|
||||
const { setStartNotification } = useOutletContext() as ChannelBaseOutletContextType;
|
||||
const navigate = useNavigate();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
|
@ -131,11 +129,6 @@ const ChannelAbout = () => {
|
|||
return 'Channel not found!';
|
||||
}
|
||||
|
||||
// The effective container is the channel override if set, otherwise the global setting.
|
||||
const globalContainer = appSettingsConfig.downloads.container ?? 'mp4';
|
||||
const effectiveContainer = downloadContainer ?? globalContainer;
|
||||
const isMkvActive = effectiveContainer === 'mkv';
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{`TA | Channel: About ${channel.channel_name}`}</title>
|
||||
|
|
@ -341,7 +334,7 @@ const ChannelAbout = () => {
|
|||
}
|
||||
|
||||
if (value !== 'mkv' && audioMultistream) {
|
||||
// Clear multistream FIRST — backend rejects container=mp4 while multistream=true
|
||||
// Clear multistream first when switching to single-audio container preference.
|
||||
await handleUpdateConfig('audio_multistream', null);
|
||||
setAudioMultistream(null);
|
||||
}
|
||||
|
|
@ -363,11 +356,10 @@ const ChannelAbout = () => {
|
|||
<ToggleConfig
|
||||
name="audio_multistream"
|
||||
value={audioMultistream ?? false}
|
||||
disabled={!isMkvActive}
|
||||
helperText={
|
||||
!isMkvActive
|
||||
? 'Choose mkv above (or use the global mkv setting) to enable multiple audio tracks.'
|
||||
: 'Downloads every available audio language for this channel.'
|
||||
audioMultistream
|
||||
? 'If multiple audio tracks are selected/found, Tube Archivist will automatically save as mkv. Single-audio downloads keep the selected/effective container.'
|
||||
: 'Enable to include multiple audio languages when available for this channel.'
|
||||
}
|
||||
updateCallback={handleUpdateConfig}
|
||||
resetCallback={() => {
|
||||
|
|
@ -379,7 +371,7 @@ const ChannelAbout = () => {
|
|||
<p className="settings-error">{audioMultistreamWarning}</p>
|
||||
)}
|
||||
</div>
|
||||
{isMkvActive && (
|
||||
{audioMultistream && (
|
||||
<div className="settings-box-wrapper">
|
||||
<div>
|
||||
<p>Audio Languages</p>
|
||||
|
|
|
|||
|
|
@ -535,13 +535,6 @@ const SettingsApplication = () => {
|
|||
value={downloadContainer}
|
||||
onChange={async (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = event.target.value as 'mp4' | 'mkv';
|
||||
|
||||
if (value !== 'mkv' && audioMultistream) {
|
||||
// Clear multistream FIRST — backend rejects container=mp4 while multistream=true
|
||||
await handleUpdateConfig('downloads.audio_multistream', false);
|
||||
setAudioMultistream(false);
|
||||
}
|
||||
|
||||
await handleUpdateConfig('downloads.container', value);
|
||||
setDownloadContainer(value);
|
||||
}}
|
||||
|
|
@ -568,11 +561,10 @@ const SettingsApplication = () => {
|
|||
<ToggleConfig
|
||||
name="downloads.audio_multistream"
|
||||
value={audioMultistream}
|
||||
disabled={downloadContainer !== 'mkv'}
|
||||
helperText={
|
||||
downloadContainer !== 'mkv'
|
||||
? 'Select the mkv container to enable multiple audio tracks.'
|
||||
: 'Downloads all available audio languages when supported.'
|
||||
audioMultistream
|
||||
? 'If multiple audio tracks are selected/found, Tube Archivist will automatically save as mkv. Single-audio downloads keep your selected container.'
|
||||
: 'Enable to include multiple audio languages when available.'
|
||||
}
|
||||
updateCallback={handleUpdateConfig}
|
||||
/>
|
||||
|
|
@ -580,7 +572,7 @@ const SettingsApplication = () => {
|
|||
<p className="settings-error">{audioMultistreamWarning}</p>
|
||||
)}
|
||||
</div>
|
||||
{audioMultistream && downloadContainer === 'mkv' && (
|
||||
{audioMultistream && (
|
||||
<div className="settings-box-wrapper">
|
||||
<div>
|
||||
<p>Audio languages</p>
|
||||
|
|
|
|||
Loading…
Reference in New Issue