Added new feature to parse yt-dlp CLI arguments in yt-dlp JSON config box

This commit is contained in:
ArabCoders 2025-01-23 17:35:03 +03:00
parent a02233a8dd
commit cb735cbb97
7 changed files with 505 additions and 355 deletions

54
Pipfile.lock generated
View file

@ -446,36 +446,36 @@
},
"debugpy": {
"hashes": [
"sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920",
"sha256:116bf8342062246ca749013df4f6ea106f23bc159305843491f64672a55af2e5",
"sha256:189058d03a40103a57144752652b3ab08ff02b7595d0ce1f651b9acc3a3a35a0",
"sha256:23dc34c5e03b0212fa3c49a874df2b8b1b8fda95160bd79c01eb3ab51ea8d851",
"sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b",
"sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd",
"sha256:32db46ba45849daed7ccf3f2e26f7a386867b077f39b2a974bb5c4c2c3b0a280",
"sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9",
"sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1",
"sha256:52c3cf9ecda273a19cc092961ee34eb9ba8687d67ba34cc7b79a521c1c64c4c0",
"sha256:52d8a3166c9f2815bfae05f386114b0b2d274456980d41f320299a8d9a5615a7",
"sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f",
"sha256:654130ca6ad5de73d978057eaf9e582244ff72d4574b3e106fb8d3d2a0d32458",
"sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57",
"sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e",
"sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308",
"sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296",
"sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3",
"sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1",
"sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1",
"sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e",
"sha256:ad7efe588c8f5cf940f40c3de0cd683cc5b76819446abaa50dc0829a30c094db",
"sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28",
"sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737",
"sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768",
"sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1"
"sha256:086b32e233e89a2740c1615c2f775c34ae951508b28b308681dbbb87bba97d06",
"sha256:22a11c493c70413a01ed03f01c3c3a2fc4478fc6ee186e340487b2edcd6f4180",
"sha256:274b6a2040349b5c9864e475284bce5bb062e63dce368a394b8cc865ae3b00c6",
"sha256:2ae5df899732a6051b49ea2632a9ea67f929604fd2b036613a9f12bc3163b92d",
"sha256:36f4829839ef0afdfdd208bb54f4c3d0eea86106d719811681a8627ae2e53dd5",
"sha256:39dfbb6fa09f12fae32639e3286112fc35ae976114f1f3d37375f3130a820969",
"sha256:4703575b78dd697b294f8c65588dc86874ed787b7348c65da70cfc885efdf1e1",
"sha256:4ad9a94d8f5c9b954e0e3b137cc64ef3f579d0df3c3698fe9c3734ee397e4abb",
"sha256:557cc55b51ab2f3371e238804ffc8510b6ef087673303890f57a24195d096e61",
"sha256:5cc45235fefac57f52680902b7d197fb2f3650112379a6fa9aa1b1c1d3ed3f02",
"sha256:646530b04f45c830ceae8e491ca1c9320a2d2f0efea3141487c82130aba70dce",
"sha256:696d8ae4dff4cbd06bf6b10d671e088b66669f110c7c4e18a44c43cf75ce966f",
"sha256:7e94b643b19e8feb5215fa508aee531387494bf668b2eca27fa769ea11d9f498",
"sha256:88a77f422f31f170c4b7e9ca58eae2a6c8e04da54121900651dfa8e66c29901a",
"sha256:898fba72b81a654e74412a67c7e0a81e89723cfe2a3ea6fcd3feaa3395138ca9",
"sha256:9649eced17a98ce816756ce50433b2dd85dfa7bc92ceb60579d68c053f98dff9",
"sha256:9af40506a59450f1315168d47a970db1a65aaab5df3833ac389d2899a5d63b3f",
"sha256:a28ed481d530e3138553be60991d2d61103ce6da254e51547b79549675f539b7",
"sha256:a2ba7ffe58efeae5b8fad1165357edfe01464f9aef25e814e891ec690e7dd82a",
"sha256:a4042edef80364239f5b7b5764e55fd3ffd40c32cf6753da9bda4ff0ac466018",
"sha256:b0232cd42506d0c94f9328aaf0d1d0785f90f87ae72d9759df7e5051be039738",
"sha256:b202f591204023b3ce62ff9a47baa555dc00bb092219abf5caf0e3718ac20e7c",
"sha256:b5c6c967d02fee30e157ab5227706f965d5c37679c687b1e7bbc5d9e7128bd41",
"sha256:cbbd4149c4fc5e7d508ece083e78c17442ee13b0e69bfa6bd63003e486770f45",
"sha256:f30b03b0f27608a0b26c75f0bb8a880c752c0e0b01090551b9d87c7d783e2069",
"sha256:fdb3c6d342825ea10b90e43d7f20f01535a72b3a1997850c0c3cefa5c27a4a2c"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==1.8.11"
"version": "==1.8.12"
},
"frozenlist": {
"hashes": [

View file

@ -12,7 +12,7 @@ YTPTube started as a fork of [meTube](https://github.com/alexta69/metube), Since
* Completely redesigned the frontend UI.
* Switched out of binary file storage in favor of SQLite.
* Handle live streams.
* Support per link, `yt-dlp config` and `cookies`. and `output format`.
* Support per link `yt-dlp JSON config or cli options`, `cookies` & `output format`.
* Tasks Runner. It allow you to queue channels for downloading using simple `json` file.
* Webhook sender. It allow you to add webhook endpoints that receive events related to downloads using simple `json` file.
* Multi-downloads support.

View file

@ -24,7 +24,7 @@ from .M3u8 import M3u8
from .Playlist import Playlist
from .Segments import Segments
from .Subtitle import Subtitle
from .Utils import StreamingError, calcDownloadPath, getVideoInfo, validate_url
from .Utils import StreamingError, arg_converter, calcDownloadPath, getVideoInfo, validate_url
LOG = logging.getLogger("http_api")
MIME = magic.Magic(mime=True)
@ -217,6 +217,24 @@ class HttpAPI(common):
await self.queue.test()
return web.json_response(data={"status": "pong"}, status=web.HTTPOk.status_code)
@route("POST", "api/yt-dlp/convert")
async def yt_dlp_convert(self, request: Request) -> Response:
post = await request.json()
args: str | None = post.get("args")
if not args:
return web.json_response(data={"error": "args is required."}, status=web.HTTPBadRequest.status_code)
try:
return web.json_response(data=arg_converter(args), status=web.HTTPOk.status_code)
except Exception as e:
err = str(e).strip()
err = err.split("\n")[-1] if "\n" in err else err
LOG.error(f"Failed to convert args. '{err}'.")
return web.json_response(
data={"error": f"Failed to convert args. '{err}'."}, status=web.HTTPBadRequest.status_code
)
@route("POST", "api/add")
async def add_url(self, request: Request) -> Response:
post = await request.json()
@ -637,6 +655,10 @@ class HttpAPI(common):
async def add_cors(self, _: Request) -> Response:
return web.json_response(data={"status": "ok"}, status=web.HTTPOk.status_code)
@route("OPTIONS", "api/yt-dlp/convert")
async def cors_ytdlp_convert(self, _: Request) -> Response:
return web.json_response(data={"status": "ok"}, status=web.HTTPOk.status_code)
@route("OPTIONS", "api/delete")
async def delete_cors(self, _: Request) -> Response:
return web.json_response(data={"status": "ok"}, status=web.HTTPOk.status_code)

View file

@ -17,7 +17,7 @@ from .config import Config
from .DownloadQueue import DownloadQueue
from .Emitter import Emitter
from .encoder import Encoder
from .Utils import isDownloaded
from .Utils import isDownloaded, arg_converter
LOG = logging.getLogger("socket_api")
@ -276,3 +276,24 @@ class HttpSocket(common):
async def resume(self, sid: str, _=None):
self.queue.resume()
await self.emitter.emit("paused", {"paused": False, "at": time.time()})
@ws_event
async def ytdlp_convert(self, sid: str, data: dict):
if not isinstance(data, dict) or "args" not in data:
await self.emitter.warning("Invalid request or no options were given.", to=sid)
return
args: str | None = data.get("args")
if not args:
await self.emitter.warning("no options were given.", to=sid)
return
try:
await self.emitter.emit("ytdlp_convert", arg_converter(args), to=sid)
return
except Exception as e:
err = str(e).strip()
err = err.split("\n")[-1] if "\n" in err else err
LOG.error(f"Failed to convert args. '{err}'.")
await self.emitter.error(f"Failed to convert options. '{e}'.", to=sid)

View file

@ -5,6 +5,7 @@ import logging
import os
import pathlib
import re
import shlex
import socket
import uuid
from datetime import datetime, timezone
@ -109,7 +110,7 @@ def getVideoInfo(url: str, ytdlp_opts: dict = None, no_archive: bool = True) ->
return yt_dlp.YoutubeDL().extract_info(url, download=False)
def calcDownloadPath(basePath: str, folder: str|None = None, createPath: bool = True) -> str:
def calcDownloadPath(basePath: str, folder: str | None = None, createPath: bool = True) -> str:
"""Calculates download path and prevents folder traversal.
Returns:
@ -501,3 +502,36 @@ def validate_url(url: str) -> bool:
raise ValueError("Access to internal urls or private networks is not allowed.")
return True
def arg_converter(args: str) -> dict:
"""
Convert yt-dlp options to a dictionary.
Args:
opts (str): yt-dlp options string.
Returns:
dict: yt-dlp options dictionary.
"""
import yt_dlp.options
create_parser = yt_dlp.options.create_parser
def defaultOpts(args: str):
patched_parser = create_parser()
try:
yt_dlp.options.create_parser = lambda: patched_parser
return yt_dlp.parse_options(args)
finally:
yt_dlp.options.create_parser = create_parser
default_opts = defaultOpts([]).ydl_opts
opts = yt_dlp.parse_options(shlex.split(args)).ydl_opts
diff = {k: v for k, v in opts.items() if default_opts[k] != v}
if "postprocessors" in diff:
diff["postprocessors"] = [pp for pp in diff["postprocessors"] if pp not in default_opts["postprocessors"]]
return json.loads(json.dumps(diff))

View file

@ -76,11 +76,11 @@
<div class="field">
<label class="label is-inline" for="ytdlpConfig"
v-tooltip="'Extends current global yt-dlp config. (JSON)'">
JSON yt-dlp config
JSON yt-dlp config or CLI options.
</label>
<div class="control">
<textarea class="textarea" id="ytdlpConfig" v-model="ytdlpConfig"
:disabled="!socket.isConnected"></textarea>
<textarea class="textarea" id="ytdlpConfig" v-model="ytdlpConfig" :disabled="!socket.isConnected"
placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
</div>
<span class="subtitle is-6">
Some config fields are ignored like cookiefile, path, and output_format etc.
@ -152,7 +152,35 @@ const url = useStorage('downloadUrl', null)
const showAdvanced = useStorage('show_advanced', false)
const addInProgress = ref(false)
const addDownload = () => {
const addDownload = async () => {
// -- send request to convert cli options to JSON
if (ytdlpConfig.value && ytdlpConfig.value.length > 2 && !ytdlpConfig.value.trim().startsWith('{')) {
const response = await fetch(config.app.url_host + config.app.url_prefix + 'api/yt-dlp/convert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ args: ytdlpConfig.value }),
});
const data = await response.json()
if (200 !== response.status) {
toast.error(`Error: (${response.status}): ${data.error}`)
return
}
ytdlpConfig.value = JSON.stringify(data, null, 2)
}
if (ytdlpConfig.value && ytdlpConfig.value.length > 2) {
try {
JSON.parse(ytdlpConfig.value)
} catch (e) {
toast.error(`Invalid JSON yt-dlp config. ${e.message}`)
return
}
}
addInProgress.value = true
url.value.split(',').forEach(url => {
if (!url.trim()) {

File diff suppressed because it is too large Load diff