Added new feature to parse yt-dlp CLI arguments in yt-dlp JSON config box
This commit is contained in:
parent
a02233a8dd
commit
cb735cbb97
7 changed files with 505 additions and 355 deletions
54
Pipfile.lock
generated
54
Pipfile.lock
generated
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
685
ui/yarn.lock
685
ui/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue