Merge pull request #207 from arabcoders/dev

Added advanced iOS shortcut
This commit is contained in:
Abdulmohsen 2025-03-11 02:23:11 +03:00 committed by GitHub
commit 10aeff4bfa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 97 additions and 60 deletions

View file

@ -129,6 +129,13 @@ This shortcut is powerful, as it's allow you to select your preset on the fly pu
Combined with the new and powerful presets system, you could add presets for specific websites that need cookies,
and use that preset to download directly from your iOS device.
#### Advanced iOS Shortcut
This shortcut [YTPTube To Media](https://www.icloud.com/shortcuts/6e3db0bd532843e3aec70e6ce211be08) is more advanced, as it's parses
the `yt-dlp` output and attempt to download the media directly to your iOS device. It doesn't always work, but it's a good
starting point for those who want to download media directly to their iOS device. We provide no support for this use case
other than the shortcut itself. this shortcut missing support for parsing the http_headers, it's only parse the cookies.
## Running behind a reverse proxy
It's advisable to run YTPTube behind a reverse proxy, if authentication and/or HTTPS support are required.

View file

@ -18,6 +18,7 @@ import httpx
import magic
from aiohttp import web
from aiohttp.web import Request, RequestHandler, Response
from yt_dlp.cookies import LenientSimpleCookie
from .cache import Cache
from .common import Common
@ -40,13 +41,14 @@ from .Utils import (
arg_converter,
decrypt_data,
encrypt_data,
extract_info,
get_file,
get_mime_type,
get_sidecar_subtitles,
get_video_info,
validate_url,
validate_uuid,
)
from .YTDLPOpts import YTDLPOpts
LOG = logging.getLogger("http_api")
MIME = magic.Magic(mime=True)
@ -471,33 +473,65 @@ class HttpAPI(Common):
except ValueError as e:
return web.json_response(data={"error": str(e)}, status=web.HTTPBadRequest.status_code)
try:
key = self.cache.hash(url)
preset = request.query.get("preset")
if preset:
exists = Presets.get_instance().get(name=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 = self.config.default_preset
if self.cache.has(key):
try:
key = self.cache.hash(url + str(preset))
if self.cache.has(key) and not request.query.get("force", False):
data = self.cache.get(key)
data["_cached"] = {
"status": "hit",
"key": key,
"ttl": data.get("_cached", {}).get("ttl", 300),
"ttl_left": data.get("_cached", {}).get("expires", time.time() + 300) - time.time(),
"expires": data.get("_cached", {}).get("expires", time.time() + 300),
}
return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
return web.Response(body=json.dumps(data, indent=4), status=web.HTTPOk.status_code)
opts = {
"proxy": self.config.ytdl_options.get("proxy", None),
}
opts = {}
if self.config.ytdl_options.get("proxy", None):
opts["proxy"] = self.config.ytdl_options.get("proxy", None)
ytdlp_opts = YTDLPOpts.get_instance().preset(name=preset, with_cookies=True).add(opts).get_all()
data = extract_info(
config=ytdlp_opts,
url=url,
debug=False,
no_archive=True,
follow_redirect=True,
)
if "formats" in data:
for index, item in enumerate(data["formats"]):
if "cookies" in item and len(item["cookies"]) > 0:
cookies = [f"{c.key}={c.value}" for c in LenientSimpleCookie(item["cookies"]).values()]
if len(cookies) > 0:
data["formats"][index]["h_cookies"] = "; ".join(cookies)
data["formats"][index]["h_cookies"] = data["formats"][index]["h_cookies"].strip()
data = get_video_info(url=url, ytdlp_opts=opts, no_archive=True)
self.cache.set(key=self.cache.hash(url), value=data, ttl=300)
data["_cached"] = {
"key": self.cache.hash(url),
"status": "miss",
"key": key,
"ttl": 300,
"ttl_left": 300,
"expires": time.time() + 300,
}
return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=self.encoder.encode)
self.cache.set(key=key, value=data, ttl=300)
return web.Response(body=json.dumps(data, indent=4), status=web.HTTPOk.status_code)
except Exception as e:
LOG.error(f"Error encountered while grabbing video info '{url}'. '{e}'.")
LOG.exception(e)
@ -505,6 +539,7 @@ class HttpAPI(Common):
data={
"error": "failed to get video info.",
"message": str(e),
"formats": [],
},
status=web.HTTPInternalServerError.status_code,
)
@ -536,17 +571,17 @@ class HttpAPI(Common):
tasks = []
response = []
def get_video_info_wrapper(id: str, url: str) -> tuple[str, dict]:
def info_wrapper(id: str, url: str) -> tuple[str, dict]:
try:
return (
id,
get_video_info(
url=url,
ytdlp_opts={
extract_info(
config={
"proxy": self.config.ytdl_options.get("proxy", None),
"simulate": True,
"dump_single_json": True,
},
url=url,
no_archive=True,
),
)
@ -575,9 +610,7 @@ class HttpAPI(Common):
continue
tasks.append(
asyncio.get_event_loop().run_in_executor(
None, lambda id=id, url=url: get_video_info_wrapper(id=id, url=url)
)
asyncio.get_event_loop().run_in_executor(None, lambda id=id, url=url: info_wrapper(id=id, url=url))
)
if len(tasks) > 0:

View file

@ -30,37 +30,6 @@ class StreamingError(Exception):
"""Raised when an error occurs during streaming."""
def get_video_info(url: str, ytdlp_opts: dict | None = None, no_archive: bool = True) -> Any | dict[str, Any] | None:
"""
Extracts video information from the given URL.
Args:
url (str): URL to extract information from.
ytdlp_opts (dict): Additional options to pass to yt-dlp.
no_archive (bool): Do not use download archive.
Returns:
dict: Video information.
"""
params: dict = {
"quiet": True,
"color": "no_color",
"extract_flat": True,
"ignoreerrors": True,
"skip_download": True,
"ignore_no_formats_error": True,
}
if ytdlp_opts:
params = {**params, **ytdlp_opts}
if no_archive and "download_archive" in params:
del params["download_archive"]
return yt_dlp.YoutubeDL(params=params).extract_info(url, download=False)
def calc_download_path(base_path: str, folder: str | None = None, create_path: bool = True) -> str:
"""
Calculates download path and prevents folder traversal.
@ -93,7 +62,13 @@ def calc_download_path(base_path: str, folder: str | None = None, create_path: b
return download_path
def extract_info(config: dict, url: str, debug: bool = False) -> dict:
def extract_info(
config: dict,
url: str,
debug: bool = False,
no_archive: bool = False,
follow_redirect: bool = False,
) -> dict:
"""
Extracts video information from the given URL.
@ -101,6 +76,8 @@ def extract_info(config: dict, url: str, debug: bool = False) -> dict:
config (dict): Configuration options.
url (str): URL to extract information from.
debug (bool): Enable debug logging.
no_archive (bool): Disable download archive.
follow_redirect (bool): Follow URL redirects.
Returns:
dict: Video information.
@ -144,7 +121,15 @@ def extract_info(config: dict, url: str, debug: bool = False) -> dict:
params["logger"] = log_wrapper
return yt_dlp.YoutubeDL(params=params).extract_info(url, download=False)
if no_archive and "download_archive" in params:
del params["download_archive"]
data = yt_dlp.YoutubeDL(params=params).extract_info(url, download=False)
if follow_redirect and "_type" in data and "url" == data["_type"]:
return extract_info(config, data["url"], debug=debug, no_archive=no_archive, follow_redirect=follow_redirect)
return data
def merge_dict(source: dict, destination: dict) -> dict:

View file

@ -17,33 +17,45 @@
<div class="navbar-end is-flex" style="flex-flow:wrap">
<div class="navbar-item is-hidden-tablet">
<NuxtLink class="button is-dark has-tooltip-bottom" to="/" v-tooltip.bottom="'Downloads'">
<NuxtLink class="button is-dark has-tooltip-bottom" to="/">
<span :class="socket.isConnected ? 'has-text-success' : 'has-text-danger'" class="icon">
<img src="/favicon.ico" /></span>
</NuxtLink>
</div>
<div class="navbar-item" v-if="!config.app.basic_mode">
<NuxtLink class="button is-dark has-tooltip-bottom" to="/presets" v-tooltip.bottom="'Presets'">
<span class="icon"><i class="fa-solid fa-sliders" /></span>
<NuxtLink class="button is-dark has-tooltip-bottom" to="/presets">
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-sliders" /></span>
<span class="is-hidden-mobile">Presets</span>
</span>
</NuxtLink>
</div>
<div class="navbar-item" v-if="!config.app.basic_mode && config.app.console_enabled">
<NuxtLink class="button is-dark has-tooltip-bottom" to="/console" v-tooltip.bottom="'Terminal'">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<NuxtLink class="button is-dark has-tooltip-bottom" to="/console">
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span class="is-hidden-mobile">Terminal</span>
</span>
</NuxtLink>
</div>
<div class="navbar-item" v-if="!config.app.basic_mode" v-tooltip.bottom="'Tasks'">
<div class="navbar-item" v-if="!config.app.basic_mode">
<NuxtLink class="button is-dark has-tooltip-bottom" to="/tasks">
<span class="icon"><i class="fa-solid fa-tasks" /></span>
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-tasks" /></span>
<span class="is-hidden-mobile">Tasks</span>
</span>
</NuxtLink>
</div>
<div class="navbar-item" v-if="!config.app.basic_mode" v-tooltip.bottom="'Notifications'">
<NuxtLink class="button is-dark has-tooltip-bottom" to="/notifications">
<span class="icon"><i class="fa-solid fa-paper-plane" /></span>
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-paper-plane" /></span>
<span class="is-hidden-mobile">Notifications</span>
</span>
</NuxtLink>
</div>