metube/app/dl_formats.py
2025-05-24 08:55:08 -04:00

123 lines
4.2 KiB
Python

import copy
AUDIO_FORMATS = ("m4a", "mp3", "opus", "wav", "flac")
def get_format(format: str, quality: str) -> str:
"""
Returns format for download
Args:
format (str): format selected
quality (str): quality selected
Raises:
Exception: unknown quality, unknown format
Returns:
dl_format: Formatted download string
"""
format = format or "any"
if format.startswith("custom:"):
return format[7:]
if format == "thumbnail":
# Quality is irrelevant in this case since we skip the download
return "bestaudio/best"
if format in AUDIO_FORMATS:
# Audio quality needs to be set post-download, set in opts
return f"bestaudio[ext={format}]/bestaudio/best"
if format in ("mp4", "any"):
if quality == "audio":
return "bestaudio/best"
# video {res} {vfmt} + audio {afmt} {res} {vfmt}
vfmt, afmt = ("[ext=mp4]", "[ext=m4a]") if format == "mp4" else ("", "")
vres = f"[height<={quality}]" if quality not in ("best", "best_ios", "worst") else ""
vcombo = vres + vfmt
if quality == "best_ios":
# iOS has strict requirements for video files, requiring h264 or h265
# video codec and aac audio codec in MP4 container. This format string
# attempts to get the fully compatible formats first, then the h264/h265
# video codec with any M4A audio codec (because audio is faster to
# convert if needed), and falls back to getting the best available MP4
# file.
return f"bestvideo[vcodec~='^((he|a)vc|h26[45])']{vres}+bestaudio[acodec=aac]/bestvideo[vcodec~='^((he|a)vc|h26[45])']{vres}+bestaudio{afmt}/bestvideo{vcombo}+bestaudio{afmt}/best{vcombo}"
return f"bestvideo{vcombo}+bestaudio{afmt}/best{vcombo}"
raise Exception(f"Unkown format {format}")
def get_opts(format: str, quality: str, ytdl_opts: dict) -> dict:
"""
Returns extra download options
Mostly postprocessing options
Args:
format (str): format selected
quality (str): quality of format selected (needed for some formats)
ytdl_opts (dict): current options selected
Returns:
ytdl_opts: Extra options
"""
opts = copy.deepcopy(ytdl_opts)
postprocessors = []
if format in AUDIO_FORMATS:
postprocessors.append(
{
"key": "FFmpegExtractAudio",
"preferredcodec": format,
"preferredquality": 0 if quality == "best" else quality,
}
)
# Add voice mono settings for MP3
if format == "mp3" and quality == "32_mono":
opts["postprocessor_args"] = {
"ffmpeg": ["-ac", "1", "-ar", "22050", "-q:a", "8"]
}
# Add voice mono settings for OPUS
if format == "opus" and quality == "19_mono":
opts["postprocessor_args"] = {
"ffmpeg": [
"-ac", "1", # Force mono
"-c:a", "libopus", # Use OPUS codec
"-b:a", "19k", # Set bitrate to 19kbps
"-application", "voip", # Optimize for speech
"-frame_duration", "20", # Standard frame size
"-compression_level", "10" # Highest quality compression
]
}
# Audio formats without thumbnail
if format not in ("wav") and "writethumbnail" not in opts:
opts["writethumbnail"] = True
postprocessors.append(
{
"key": "FFmpegThumbnailsConvertor",
"format": "jpg",
"when": "before_dl",
}
)
postprocessors.append({"key": "FFmpegMetadata"})
postprocessors.append({"key": "EmbedThumbnail"})
if format == "thumbnail":
opts["skip_download"] = True
opts["writethumbnail"] = True
postprocessors.append(
{"key": "FFmpegThumbnailsConvertor", "format": "jpg", "when": "before_dl"}
)
opts["postprocessors"] = postprocessors + (
opts["postprocessors"] if "postprocessors" in opts else []
)
return opts