diff --git a/.vscode/settings.json b/.vscode/settings.json index d3f90560..ef14a3c0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,6 +30,7 @@ "edgechromium", "engineio", "euuo", + "faststart", "finaldir", "flac", "forcejson", @@ -52,6 +53,7 @@ "Microformat", "microformats", "mkvtoolsnix", + "movflags", "mpegts", "msvideo", "muxdelay", @@ -73,6 +75,7 @@ "qtpy", "quicktime", "rejecttitle", + "remux", "rtime", "smhd", "socketio", diff --git a/app/library/Presets.py b/app/library/Presets.py index 6ef5e2c6..ee6c149d 100644 --- a/app/library/Presets.py +++ b/app/library/Presets.py @@ -20,34 +20,39 @@ DEFAULT_PRESETS: list[dict[int, dict[str, str | bool]]] = [ "id": "3e163c6c-64eb-4448-924f-814b629b3810", "name": "default", "default": True, + "description": "Default preset for yt-dlp. It will download whatever yt-dlp decides is the best quality for the video and audio.", }, { - "id": "5bf9c42b-8852-468a-99f5-915622dfba25", - "name": "Best video and audio", - "cli": "--format 'bv+ba/b'", + "id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48d", + "name": "Mobile", + "cli": '-t mp4 --merge-output-format mp4 --add-chapters --remux-video mp4 \n--embed-metadata --embed-thumbnail \n--postprocessor-args "-movflags +faststart"', "default": True, + "description": "This preset is designed for mobile devices. It will download the best quality video and audio in mp4 format, merge them, and add chapters, metadata, and thumbnail.", }, { "id": "441675ed-b739-40f0-a0b0-1ecfcb9dc48b", - "name": "1080p H264/m4a or best available", + "name": "1080p", "cli": "-S vcodec:h264 --format 'bv[height<=1080][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'", "default": True, + "description": "Download the best quality video and audio in mp4 format for 1080p resolution.", }, { "id": "9719fcc3-4cf2-4d88-b1e4-74dff3dba00e", - "name": "720p h264/m4a or best available", + "name": "720p", "cli": "-S vcodec:h264 --format 'bv[height<=720][ext=mp4]+ba[ext=m4a]/b[ext=mp4]/b[ext=webm]'", "default": True, + "description": "Download the best quality video and audio in mp4 format for 720p resolution.", }, { "id": "a6fd4b25-2b3e-458d-bb57-b75e41cc4330", - "name": "Audio only", + "name": "Audio Only", "cli": "--extract-audio --add-chapters --embed-metadata --embed-thumbnail --format 'bestaudio/best'", "default": True, + "description": "This preset is designed to download only the audio of the video. It will extract the audio, add chapters, metadata, and thumbnail.", }, { "id": "2ade2c28-cad4-4a06-b7eb-2439fdf46f60", - "name": "yt-dlp info reader plugin", + "name": "Info Reader Plugin", "description": 'This preset generate specific filename format and metadata to work with yt-dlp info reader plugins for jellyfin/emby and plex and to make play state sync work for WatchState.\n\nThere is one more step you need to do via Other > Terminal if you have it enabled or directly from container shell\n\nyt-dlp -I0 --write-info-json --write-thumbnail --convert-thumbnails jpg --paths /downloads/youtube -o "%(channel|Unknown_title)s [%(channel_id|Unknown_id)s]/%(title).180B [%(channel_id|Unknown_id)s].%(ext)s" -- https://www.youtube.com/channel/UCClfFsWcT3N2I7VTXXyt84A\n\nChange the url to the channel you want to download.\n\nFor more information please visit \nhttps://github.com/arabcoders/watchstate/blob/master/FAQ.md#how-to-get-watchstate-working-with-youtube-contentlibrary', "folder": "youtube", "template": "%(channel)s %(channel_id|Unknown_id)s/Season %(release_date>%Y,upload_date>%Y|Unknown)s/%(release_date>%Y%m%d,upload_date>%Y%m%d)s - %(title).180B [%(extractor)s-%(id)s].%(ext)s", @@ -104,14 +109,16 @@ class Presets(metaclass=Singleton): _instance = None """The instance of the class.""" + _config: Config = None + _default: list[Preset] = [] def __init__(self, file: str | Path | None = None, config: Config | None = None): Presets._instance = self - config = config or Config.get_instance() + self._config = config or Config.get_instance() - self._file: Path = Path(file) if file else Path(config.config_path).joinpath("presets.json") + self._file: Path = Path(file) if file else Path(self._config.config_path).joinpath("presets.json") if self._file.exists() and "600" != self._file.stat().st_mode: try: @@ -163,6 +170,10 @@ class Presets(metaclass=Singleton): """ self.load() + if not self.get(self._config.default_preset): + LOG.error(f"Default preset '{self._config.default_preset}' not found, using 'default' preset.") + self._config.default_preset = "default" + def get_all(self) -> list[Preset]: """Return the items.""" return self._default + self._items diff --git a/app/routes/api/yt_dlp.py b/app/routes/api/yt_dlp.py index a735b3a4..eb6bfeb5 100644 --- a/app/routes/api/yt_dlp.py +++ b/app/routes/api/yt_dlp.py @@ -77,13 +77,14 @@ async def convert(request: Request) -> Response: @route("GET", "api/yt-dlp/url/info/", "get_info") -async def get_info(request: Request, cache: Cache) -> Response: +async def get_info(request: Request, cache: Cache, config: Config) -> Response: """ Get the video info. Args: request (Request): The request object. cache (Cache): The cache instance. + config (Config): The config instance. Returns: Response: The response object @@ -91,25 +92,27 @@ async def get_info(request: Request, cache: Cache) -> Response: """ url: str | None = request.query.get("url") if not url: - return web.json_response(data={"error": "URL is required."}, status=web.HTTPBadRequest.status_code) + return web.json_response( + data={"status": False, "message": "URL is required.", "error": "URL is required."}, + status=web.HTTPBadRequest.status_code, + ) try: validate_url(url) except ValueError as e: - return web.json_response(data={"error": str(e)}, status=web.HTTPBadRequest.status_code) + return web.json_response( + data={"status": False, "message": str(e), "error": str(e)}, + status=web.HTTPBadRequest.status_code, + ) - config = Config.get_instance() - - preset = request.query.get("preset") - if preset: - exists: Preset | None = Presets.get_instance().get(preset) - if not exists: - return web.json_response( - data={"status": False, "message": f"Preset '{preset}' does not exist."}, - status=web.HTTPBadRequest.status_code, - ) - else: - preset: str = config.default_preset + preset: str = request.query.get("preset", config.default_preset) + exists: Preset | None = Presets.get_instance().get(preset) + if not exists: + msg: str = f"Preset '{preset}' does not exist." + return web.json_response( + data={"status": False, "message": msg, "error": msg}, + status=web.HTTPBadRequest.status_code, + ) try: key: str = cache.hash(f"{preset}:{url}") @@ -118,6 +121,7 @@ async def get_info(request: Request, cache: Cache) -> Response: data: Any | None = cache.get(key) data["_cached"] = { "status": "hit", + "preset": preset, "key": key, "ttl": data.get("_cached", {}).get("ttl", 300), "ttl_left": data.get("_cached", {}).get("expires", time.time() + 300) - time.time(), @@ -130,7 +134,7 @@ async def get_info(request: Request, cache: Cache) -> Response: if ytdlp_proxy := config.get_ytdlp_args().get("proxy", None): opts["proxy"] = ytdlp_proxy - ytdlp_opts = YTDLPOpts.get_instance().preset(name=preset).add(opts).get_all() + ytdlp_opts: dict = YTDLPOpts.get_instance().preset(name=preset).add(opts).get_all() data = extract_info( config=ytdlp_opts, @@ -153,6 +157,7 @@ async def get_info(request: Request, cache: Cache) -> Response: data["_cached"] = { "status": "miss", + "preset": preset, "key": key, "ttl": 300, "ttl_left": 300, diff --git a/ui/components/Dropdown.vue b/ui/components/Dropdown.vue index 9add139f..6702393e 100644 --- a/ui/components/Dropdown.vue +++ b/ui/components/Dropdown.vue @@ -1,7 +1,7 @@ - + yt-dlp Information @@ -357,7 +357,8 @@ - + yt-dlp Information diff --git a/ui/components/NewDownload.vue b/ui/components/NewDownload.vue index d330f1da..7d725f7d 100644 --- a/ui/components/NewDownload.vue +++ b/ui/components/NewDownload.vue @@ -76,16 +76,16 @@ Opts - -
-
-
- {{ get_preset(form.preset)?.description }} +
+
+
+ {{ get_preset(form.preset)?.description }} +
-
+
-
-
+
+ + + + yt-dlp Information + + + + Remove from archive + + + + + + Reset local settings + + +
+
+
-
+
@@ -363,6 +382,18 @@ const convertOptions = async args => { return null; } +onUpdated(async () => { + await nextTick() + + if ('' === form.value?.preset) { + form.value.preset = config.app.default_preset + } + + if (form.value?.preset && !config.presets.some(p => p.name === form.value.preset)) { + form.value.preset = config.app.default_preset + } +}) + onMounted(async () => { await nextTick() @@ -370,6 +401,10 @@ onMounted(async () => { form.value.preset = config.app.default_preset } + if (form.value?.preset && !config.presets.some(p => p.name === form.value.preset)) { + form.value.preset = config.app.default_preset + } + if (props?.item) { Object.keys(props.item).forEach(key => { if (key in form.value) { @@ -400,7 +435,6 @@ const filter_presets = (flag = true) => config.presets.filter(item => item.defau const get_preset = name => config.presets.find(item => item.name === name) const expand_description = e => toggleClass(e.target, ['is-ellipsis', 'is-pre-wrap']) - const removeFromArchive = async url => { try { const req = await request(`/api/archive/0`, { diff --git a/ui/components/Queue.vue b/ui/components/Queue.vue index 25f86c0b..1e4c3684 100644 --- a/ui/components/Queue.vue +++ b/ui/components/Queue.vue @@ -120,18 +120,19 @@ Cancel Item - + @@ -227,10 +228,11 @@ Play video - + - + yt-dlp Information diff --git a/ui/pages/index.vue b/ui/pages/index.vue index a2c6af3c..b1b8c27b 100644 --- a/ui/pages/index.vue +++ b/ui/pages/index.vue @@ -60,14 +60,16 @@
- - + - - +
@@ -83,8 +85,11 @@ const bg_opacity = useStorage('random_bg_opacity', 0.85) const display_style = useStorage('display_style', 'cards') const show_thumbnail = useStorage('show_thumbnail', true) -const get_info = ref('') -const get_info_use_url = ref(false) +const info_view = ref({ + url: '', + preset: '', + useUrl: false, +}) as Ref<{ url: string, preset: string, useUrl: boolean }> const item_form = ref({}) const query = ref() const toggleFilter = ref(false) @@ -126,16 +131,18 @@ const pauseDownload = () => { } const close_info = () => { - get_info.value = '' - get_info_use_url.value = false + info_view.value.url = '' + info_view.value.preset = '' + info_view.value.useUrl = false } -const view_info = (url: string, useUrl: boolean = false) => { - get_info.value = url - get_info_use_url.value = useUrl +const view_info = (url: string, useUrl: boolean = false, preset: string = '') => { + info_view.value.url = url + info_view.value.useUrl = useUrl + info_view.value.preset = preset } -watch(get_info, v => { +watch(() => info_view.value.url, v => { if (!bg_enable.value) { return }