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": { "debugpy": {
"hashes": [ "hashes": [
"sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920", "sha256:086b32e233e89a2740c1615c2f775c34ae951508b28b308681dbbb87bba97d06",
"sha256:116bf8342062246ca749013df4f6ea106f23bc159305843491f64672a55af2e5", "sha256:22a11c493c70413a01ed03f01c3c3a2fc4478fc6ee186e340487b2edcd6f4180",
"sha256:189058d03a40103a57144752652b3ab08ff02b7595d0ce1f651b9acc3a3a35a0", "sha256:274b6a2040349b5c9864e475284bce5bb062e63dce368a394b8cc865ae3b00c6",
"sha256:23dc34c5e03b0212fa3c49a874df2b8b1b8fda95160bd79c01eb3ab51ea8d851", "sha256:2ae5df899732a6051b49ea2632a9ea67f929604fd2b036613a9f12bc3163b92d",
"sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b", "sha256:36f4829839ef0afdfdd208bb54f4c3d0eea86106d719811681a8627ae2e53dd5",
"sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd", "sha256:39dfbb6fa09f12fae32639e3286112fc35ae976114f1f3d37375f3130a820969",
"sha256:32db46ba45849daed7ccf3f2e26f7a386867b077f39b2a974bb5c4c2c3b0a280", "sha256:4703575b78dd697b294f8c65588dc86874ed787b7348c65da70cfc885efdf1e1",
"sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9", "sha256:4ad9a94d8f5c9b954e0e3b137cc64ef3f579d0df3c3698fe9c3734ee397e4abb",
"sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1", "sha256:557cc55b51ab2f3371e238804ffc8510b6ef087673303890f57a24195d096e61",
"sha256:52c3cf9ecda273a19cc092961ee34eb9ba8687d67ba34cc7b79a521c1c64c4c0", "sha256:5cc45235fefac57f52680902b7d197fb2f3650112379a6fa9aa1b1c1d3ed3f02",
"sha256:52d8a3166c9f2815bfae05f386114b0b2d274456980d41f320299a8d9a5615a7", "sha256:646530b04f45c830ceae8e491ca1c9320a2d2f0efea3141487c82130aba70dce",
"sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f", "sha256:696d8ae4dff4cbd06bf6b10d671e088b66669f110c7c4e18a44c43cf75ce966f",
"sha256:654130ca6ad5de73d978057eaf9e582244ff72d4574b3e106fb8d3d2a0d32458", "sha256:7e94b643b19e8feb5215fa508aee531387494bf668b2eca27fa769ea11d9f498",
"sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57", "sha256:88a77f422f31f170c4b7e9ca58eae2a6c8e04da54121900651dfa8e66c29901a",
"sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e", "sha256:898fba72b81a654e74412a67c7e0a81e89723cfe2a3ea6fcd3feaa3395138ca9",
"sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308", "sha256:9649eced17a98ce816756ce50433b2dd85dfa7bc92ceb60579d68c053f98dff9",
"sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296", "sha256:9af40506a59450f1315168d47a970db1a65aaab5df3833ac389d2899a5d63b3f",
"sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3", "sha256:a28ed481d530e3138553be60991d2d61103ce6da254e51547b79549675f539b7",
"sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1", "sha256:a2ba7ffe58efeae5b8fad1165357edfe01464f9aef25e814e891ec690e7dd82a",
"sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1", "sha256:a4042edef80364239f5b7b5764e55fd3ffd40c32cf6753da9bda4ff0ac466018",
"sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e", "sha256:b0232cd42506d0c94f9328aaf0d1d0785f90f87ae72d9759df7e5051be039738",
"sha256:ad7efe588c8f5cf940f40c3de0cd683cc5b76819446abaa50dc0829a30c094db", "sha256:b202f591204023b3ce62ff9a47baa555dc00bb092219abf5caf0e3718ac20e7c",
"sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28", "sha256:b5c6c967d02fee30e157ab5227706f965d5c37679c687b1e7bbc5d9e7128bd41",
"sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737", "sha256:cbbd4149c4fc5e7d508ece083e78c17442ee13b0e69bfa6bd63003e486770f45",
"sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768", "sha256:f30b03b0f27608a0b26c75f0bb8a880c752c0e0b01090551b9d87c7d783e2069",
"sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1" "sha256:fdb3c6d342825ea10b90e43d7f20f01535a72b3a1997850c0c3cefa5c27a4a2c"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==1.8.11" "version": "==1.8.12"
}, },
"frozenlist": { "frozenlist": {
"hashes": [ "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. * Completely redesigned the frontend UI.
* Switched out of binary file storage in favor of SQLite. * Switched out of binary file storage in favor of SQLite.
* Handle live streams. * 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. * 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. * Webhook sender. It allow you to add webhook endpoints that receive events related to downloads using simple `json` file.
* Multi-downloads support. * Multi-downloads support.

View file

@ -24,7 +24,7 @@ from .M3u8 import M3u8
from .Playlist import Playlist from .Playlist import Playlist
from .Segments import Segments from .Segments import Segments
from .Subtitle import Subtitle 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") LOG = logging.getLogger("http_api")
MIME = magic.Magic(mime=True) MIME = magic.Magic(mime=True)
@ -217,6 +217,24 @@ class HttpAPI(common):
await self.queue.test() await self.queue.test()
return web.json_response(data={"status": "pong"}, status=web.HTTPOk.status_code) 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") @route("POST", "api/add")
async def add_url(self, request: Request) -> Response: async def add_url(self, request: Request) -> Response:
post = await request.json() post = await request.json()
@ -637,6 +655,10 @@ class HttpAPI(common):
async def add_cors(self, _: Request) -> Response: async def add_cors(self, _: Request) -> Response:
return web.json_response(data={"status": "ok"}, status=web.HTTPOk.status_code) 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") @route("OPTIONS", "api/delete")
async def delete_cors(self, _: Request) -> Response: async def delete_cors(self, _: Request) -> Response:
return web.json_response(data={"status": "ok"}, status=web.HTTPOk.status_code) 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 .DownloadQueue import DownloadQueue
from .Emitter import Emitter from .Emitter import Emitter
from .encoder import Encoder from .encoder import Encoder
from .Utils import isDownloaded from .Utils import isDownloaded, arg_converter
LOG = logging.getLogger("socket_api") LOG = logging.getLogger("socket_api")
@ -276,3 +276,24 @@ class HttpSocket(common):
async def resume(self, sid: str, _=None): async def resume(self, sid: str, _=None):
self.queue.resume() self.queue.resume()
await self.emitter.emit("paused", {"paused": False, "at": time.time()}) 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 os
import pathlib import pathlib
import re import re
import shlex
import socket import socket
import uuid import uuid
from datetime import datetime, timezone 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) 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. """Calculates download path and prevents folder traversal.
Returns: Returns:
@ -501,3 +502,36 @@ def validate_url(url: str) -> bool:
raise ValueError("Access to internal urls or private networks is not allowed.") raise ValueError("Access to internal urls or private networks is not allowed.")
return True 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"> <div class="field">
<label class="label is-inline" for="ytdlpConfig" <label class="label is-inline" for="ytdlpConfig"
v-tooltip="'Extends current global yt-dlp config. (JSON)'"> v-tooltip="'Extends current global yt-dlp config. (JSON)'">
JSON yt-dlp config JSON yt-dlp config or CLI options.
</label> </label>
<div class="control"> <div class="control">
<textarea class="textarea" id="ytdlpConfig" v-model="ytdlpConfig" <textarea class="textarea" id="ytdlpConfig" v-model="ytdlpConfig" :disabled="!socket.isConnected"
:disabled="!socket.isConnected"></textarea> placeholder="--no-embed-metadata --no-embed-thumbnail"></textarea>
</div> </div>
<span class="subtitle is-6"> <span class="subtitle is-6">
Some config fields are ignored like cookiefile, path, and output_format etc. 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 showAdvanced = useStorage('show_advanced', false)
const addInProgress = ref(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 addInProgress.value = true
url.value.split(',').forEach(url => { url.value.split(',').forEach(url => {
if (!url.trim()) { if (!url.trim()) {

File diff suppressed because it is too large Load diff