Merge pull request #207 from arabcoders/dev
Added advanced iOS shortcut
This commit is contained in:
commit
10aeff4bfa
4 changed files with 97 additions and 60 deletions
|
|
@ -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,
|
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.
|
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
|
## Running behind a reverse proxy
|
||||||
|
|
||||||
It's advisable to run YTPTube behind a reverse proxy, if authentication and/or HTTPS support are required.
|
It's advisable to run YTPTube behind a reverse proxy, if authentication and/or HTTPS support are required.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import httpx
|
||||||
import magic
|
import magic
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from aiohttp.web import Request, RequestHandler, Response
|
from aiohttp.web import Request, RequestHandler, Response
|
||||||
|
from yt_dlp.cookies import LenientSimpleCookie
|
||||||
|
|
||||||
from .cache import Cache
|
from .cache import Cache
|
||||||
from .common import Common
|
from .common import Common
|
||||||
|
|
@ -40,13 +41,14 @@ from .Utils import (
|
||||||
arg_converter,
|
arg_converter,
|
||||||
decrypt_data,
|
decrypt_data,
|
||||||
encrypt_data,
|
encrypt_data,
|
||||||
|
extract_info,
|
||||||
get_file,
|
get_file,
|
||||||
get_mime_type,
|
get_mime_type,
|
||||||
get_sidecar_subtitles,
|
get_sidecar_subtitles,
|
||||||
get_video_info,
|
|
||||||
validate_url,
|
validate_url,
|
||||||
validate_uuid,
|
validate_uuid,
|
||||||
)
|
)
|
||||||
|
from .YTDLPOpts import YTDLPOpts
|
||||||
|
|
||||||
LOG = logging.getLogger("http_api")
|
LOG = logging.getLogger("http_api")
|
||||||
MIME = magic.Magic(mime=True)
|
MIME = magic.Magic(mime=True)
|
||||||
|
|
@ -471,33 +473,65 @@ class HttpAPI(Common):
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return web.json_response(data={"error": str(e)}, status=web.HTTPBadRequest.status_code)
|
return web.json_response(data={"error": str(e)}, status=web.HTTPBadRequest.status_code)
|
||||||
|
|
||||||
try:
|
preset = request.query.get("preset")
|
||||||
key = self.cache.hash(url)
|
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 = self.cache.get(key)
|
||||||
data["_cached"] = {
|
data["_cached"] = {
|
||||||
|
"status": "hit",
|
||||||
"key": key,
|
"key": key,
|
||||||
"ttl": data.get("_cached", {}).get("ttl", 300),
|
"ttl": data.get("_cached", {}).get("ttl", 300),
|
||||||
"ttl_left": data.get("_cached", {}).get("expires", time.time() + 300) - time.time(),
|
"ttl_left": data.get("_cached", {}).get("expires", time.time() + 300) - time.time(),
|
||||||
"expires": data.get("_cached", {}).get("expires", time.time() + 300),
|
"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 = {
|
opts = {}
|
||||||
"proxy": self.config.ytdl_options.get("proxy", None),
|
|
||||||
}
|
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"] = {
|
data["_cached"] = {
|
||||||
"key": self.cache.hash(url),
|
"status": "miss",
|
||||||
|
"key": key,
|
||||||
"ttl": 300,
|
"ttl": 300,
|
||||||
"ttl_left": 300,
|
"ttl_left": 300,
|
||||||
"expires": time.time() + 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:
|
except Exception as e:
|
||||||
LOG.error(f"Error encountered while grabbing video info '{url}'. '{e}'.")
|
LOG.error(f"Error encountered while grabbing video info '{url}'. '{e}'.")
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
|
|
@ -505,6 +539,7 @@ class HttpAPI(Common):
|
||||||
data={
|
data={
|
||||||
"error": "failed to get video info.",
|
"error": "failed to get video info.",
|
||||||
"message": str(e),
|
"message": str(e),
|
||||||
|
"formats": [],
|
||||||
},
|
},
|
||||||
status=web.HTTPInternalServerError.status_code,
|
status=web.HTTPInternalServerError.status_code,
|
||||||
)
|
)
|
||||||
|
|
@ -536,17 +571,17 @@ class HttpAPI(Common):
|
||||||
tasks = []
|
tasks = []
|
||||||
response = []
|
response = []
|
||||||
|
|
||||||
def get_video_info_wrapper(id: str, url: str) -> tuple[str, dict]:
|
def info_wrapper(id: str, url: str) -> tuple[str, dict]:
|
||||||
try:
|
try:
|
||||||
return (
|
return (
|
||||||
id,
|
id,
|
||||||
get_video_info(
|
extract_info(
|
||||||
url=url,
|
config={
|
||||||
ytdlp_opts={
|
|
||||||
"proxy": self.config.ytdl_options.get("proxy", None),
|
"proxy": self.config.ytdl_options.get("proxy", None),
|
||||||
"simulate": True,
|
"simulate": True,
|
||||||
"dump_single_json": True,
|
"dump_single_json": True,
|
||||||
},
|
},
|
||||||
|
url=url,
|
||||||
no_archive=True,
|
no_archive=True,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -575,9 +610,7 @@ class HttpAPI(Common):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tasks.append(
|
tasks.append(
|
||||||
asyncio.get_event_loop().run_in_executor(
|
asyncio.get_event_loop().run_in_executor(None, lambda id=id, url=url: info_wrapper(id=id, url=url))
|
||||||
None, lambda id=id, url=url: get_video_info_wrapper(id=id, url=url)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(tasks) > 0:
|
if len(tasks) > 0:
|
||||||
|
|
|
||||||
|
|
@ -30,37 +30,6 @@ class StreamingError(Exception):
|
||||||
"""Raised when an error occurs during streaming."""
|
"""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:
|
def calc_download_path(base_path: str, folder: str | None = None, create_path: bool = True) -> str:
|
||||||
"""
|
"""
|
||||||
Calculates download path and prevents folder traversal.
|
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
|
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.
|
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.
|
config (dict): Configuration options.
|
||||||
url (str): URL to extract information from.
|
url (str): URL to extract information from.
|
||||||
debug (bool): Enable debug logging.
|
debug (bool): Enable debug logging.
|
||||||
|
no_archive (bool): Disable download archive.
|
||||||
|
follow_redirect (bool): Follow URL redirects.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Video information.
|
dict: Video information.
|
||||||
|
|
@ -144,7 +121,15 @@ def extract_info(config: dict, url: str, debug: bool = False) -> dict:
|
||||||
|
|
||||||
params["logger"] = log_wrapper
|
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:
|
def merge_dict(source: dict, destination: dict) -> dict:
|
||||||
|
|
|
||||||
|
|
@ -17,33 +17,45 @@
|
||||||
<div class="navbar-end is-flex" style="flex-flow:wrap">
|
<div class="navbar-end is-flex" style="flex-flow:wrap">
|
||||||
|
|
||||||
<div class="navbar-item is-hidden-tablet">
|
<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">
|
<span :class="socket.isConnected ? 'has-text-success' : 'has-text-danger'" class="icon">
|
||||||
<img src="/favicon.ico" /></span>
|
<img src="/favicon.ico" /></span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item" v-if="!config.app.basic_mode">
|
<div class="navbar-item" v-if="!config.app.basic_mode">
|
||||||
<NuxtLink class="button is-dark has-tooltip-bottom" to="/presets" v-tooltip.bottom="'Presets'">
|
<NuxtLink class="button is-dark has-tooltip-bottom" to="/presets">
|
||||||
<span class="icon"><i class="fa-solid fa-sliders" /></span>
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fa-solid fa-sliders" /></span>
|
||||||
|
<span class="is-hidden-mobile">Presets</span>
|
||||||
|
</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item" v-if="!config.app.basic_mode && config.app.console_enabled">
|
<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'">
|
<NuxtLink class="button is-dark has-tooltip-bottom" to="/console">
|
||||||
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fa-solid fa-terminal" /></span>
|
||||||
|
<span class="is-hidden-mobile">Terminal</span>
|
||||||
|
</span>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</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">
|
<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>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-item" v-if="!config.app.basic_mode" v-tooltip.bottom="'Notifications'">
|
<div class="navbar-item" v-if="!config.app.basic_mode" v-tooltip.bottom="'Notifications'">
|
||||||
<NuxtLink class="button is-dark has-tooltip-bottom" to="/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>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue