This commit is contained in:
2026-02-10 23:00:30 -08:00
parent c2449d0ba7
commit 5323b7c76f
6 changed files with 192 additions and 104 deletions

View File

@@ -22,7 +22,7 @@
"((1fichier\\.com|megadl\\.fr|alterupload\\.com|cjoint\\.net|desfichiers\\.com|dfichiers\\.com|mesfichiers\\.org|piecejointe\\.net|pjointe\\.com|tenvoi\\.com|dl4free\\.com)/\\?[a-zA-Z0-9]{5,30}(&pw=[^&]+)?)"
],
"regexp": "((1fichier\\.com|megadl\\.fr|alterupload\\.com|cjoint\\.net|desfichiers\\.com|dfichiers\\.com|mesfichiers\\.org|piecejointe\\.net|pjointe\\.com|tenvoi\\.com|dl4free\\.com)/\\?[a-zA-Z0-9]{5,30}(&pw=[^&]+)?)",
"status": false
"status": true
},
"rapidgator": {
"name": "rapidgator",
@@ -37,7 +37,7 @@
"(rapidgator\\.net/file/[0-9]{7,8})"
],
"regexp": "((rapidgator\\.net|rg\\.to|rapidgator\\.asia)/file/([0-9a-zA-Z]{32}))|((rapidgator\\.net/file/[0-9]{7,8}))",
"status": true
"status": false
},
"turbobit": {
"name": "turbobit",
@@ -439,7 +439,7 @@
"(hexupload\\.net|hexload\\.com)/([a-zA-Z0-9]{12})"
],
"regexp": "(hexupload\\.net|hexload\\.com)/([a-zA-Z0-9]{12})",
"status": false
"status": true
},
"hot4share": {
"name": "hot4share",
@@ -495,7 +495,7 @@
"(katfile\\.com/[0-9a-zA-Z]{12})"
],
"regexp": "(katfile\\.(cloud|online)/([0-9a-zA-Z]{12}))|((katfile\\.com/[0-9a-zA-Z]{12}))",
"status": false
"status": true
},
"mediafire": {
"name": "mediafire",
@@ -507,20 +507,6 @@
"mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})"
],
"regexp": "mediafire\\.com/(\\?|download/|file/|download\\.php\\?)([0-9a-z]{15})",
"status": true
},
"mexashare": {
"name": "mexashare",
"type": "premium",
"domains": [
"mexashare.com",
"mx-sh.net",
"mexa.sh"
],
"regexps": [
"((mexashare\\.com|mx-sh\\.net|mexa\\.sh)/[0-9a-zA-Z]{12})"
],
"regexp": "((mexashare\\.com|mx-sh\\.net|mexa\\.sh)/[0-9a-zA-Z]{12})",
"status": false
},
"mixdrop": {
@@ -622,7 +608,7 @@
"(simfileshare\\.net/download/[0-9]+/)"
],
"regexp": "(simfileshare\\.net/download/[0-9]+/)",
"status": false
"status": true
},
"streamtape": {
"name": "streamtape",
@@ -802,7 +788,7 @@
"(worldbytez\\.(net|com)/[a-zA-Z0-9]{12})"
],
"regexp": "(worldbytez\\.(net|com)/[a-zA-Z0-9]{12})",
"status": false
"status": true
}
},
"streams": {
@@ -17956,9 +17942,9 @@
"generic.tld"
],
"regexps": [
"((example.com|1fichier.com|4shared.com|vev.io|clipwatching.com|clicknupload.click|playvidto.com|uploadrar.com|simfileshare.net|usersdrive.com|fastbit.cc|dropgalaxy.in|uploadboy.com|file.al|filespace.com|uploader.link|9xupload.asia|hexupload.net|filefactory.com|filerio.in|drive.google.com|gigapeta.com|isra.cloud|katfile.com|mediafire.com|mega.co.nz|alldebrid.com|prefiles.com|rapidgator.net|alfafile.net|scribd.com|turbobit.net|hitfile.net|sendit.cloud|ddl.to|exload.com|uploadhaven.com|vidoza.net|mixdrop.co|dropapk.to|indishare.me|world-files.com|uploadbox.io|worldbytez.com|mp4upload.com|mexashare.com|upload42.com|uploading.vn|filedot.to|zofile.com|spicyfile.com|modsbase.com|sharemods.com|dl-file.com|dosya.co|loadstar.club|dailyuploads.net|file-upload.com|uploadbank.com|filezip.cc|hot4share.com|streamtape.com)/folders?/[^'\"<>;]+)"
"((example.com|1fichier.com|4shared.com|vev.io|clipwatching.com|clicknupload.click|playvidto.com|uploadrar.com|simfileshare.net|usersdrive.com|fastbit.cc|dropgalaxy.in|uploadboy.com|file.al|filespace.com|uploader.link|9xupload.asia|hexupload.net|filefactory.com|filerio.in|drive.google.com|gigapeta.com|isra.cloud|katfile.com|mediafire.com|mega.co.nz|alldebrid.com|prefiles.com|rapidgator.net|alfafile.net|scribd.com|turbobit.net|hitfile.net|sendit.cloud|ddl.to|exload.com|uploadhaven.com|vidoza.net|mixdrop.co|dropapk.to|indishare.me|world-files.com|uploadbox.io|worldbytez.com|mp4upload.com|upload42.com|uploading.vn|filedot.to|zofile.com|spicyfile.com|modsbase.com|sharemods.com|dl-file.com|dosya.co|loadstar.club|dailyuploads.net|file-upload.com|uploadbank.com|filezip.cc|hot4share.com|streamtape.com)/folders?/[^'\"<>;]+)"
],
"regexp": "((example.com|1fichier.com|4shared.com|vev.io|clipwatching.com|clicknupload.click|playvidto.com|uploadrar.com|simfileshare.net|usersdrive.com|fastbit.cc|dropgalaxy.in|uploadboy.com|file.al|filespace.com|uploader.link|9xupload.asia|hexupload.net|filefactory.com|filerio.in|drive.google.com|gigapeta.com|isra.cloud|katfile.com|mediafire.com|mega.co.nz|alldebrid.com|prefiles.com|rapidgator.net|alfafile.net|scribd.com|turbobit.net|hitfile.net|sendit.cloud|ddl.to|exload.com|uploadhaven.com|vidoza.net|mixdrop.co|dropapk.to|indishare.me|world-files.com|uploadbox.io|worldbytez.com|mp4upload.com|mexashare.com|upload42.com|uploading.vn|filedot.to|zofile.com|spicyfile.com|modsbase.com|sharemods.com|dl-file.com|dosya.co|loadstar.club|dailyuploads.net|file-upload.com|uploadbank.com|filezip.cc|hot4share.com|streamtape.com)/folders?/[^'\"<>;]+)"
"regexp": "((example.com|1fichier.com|4shared.com|vev.io|clipwatching.com|clicknupload.click|playvidto.com|uploadrar.com|simfileshare.net|usersdrive.com|fastbit.cc|dropgalaxy.in|uploadboy.com|file.al|filespace.com|uploader.link|9xupload.asia|hexupload.net|filefactory.com|filerio.in|drive.google.com|gigapeta.com|isra.cloud|katfile.com|mediafire.com|mega.co.nz|alldebrid.com|prefiles.com|rapidgator.net|alfafile.net|scribd.com|turbobit.net|hitfile.net|sendit.cloud|ddl.to|exload.com|uploadhaven.com|vidoza.net|mixdrop.co|dropapk.to|indishare.me|world-files.com|uploadbox.io|worldbytez.com|mp4upload.com|upload42.com|uploading.vn|filedot.to|zofile.com|spicyfile.com|modsbase.com|sharemods.com|dl-file.com|dosya.co|loadstar.club|dailyuploads.net|file-upload.com|uploadbank.com|filezip.cc|hot4share.com|streamtape.com)/folders?/[^'\"<>;]+)"
},
"google": {
"name": "google",

View File

@@ -145,7 +145,8 @@ class PipeObject:
title_text = cmdlet_name
# Color the title (requested: yellow instead of Rich's default blue-ish title).
debug_inspect(self, title=f"[yellow]{title_text}[/yellow]")
# We disable value=False to avoid duplicating the object repr which is redundant with the attribute listing
debug_inspect(self, title=f"[yellow]{title_text}[/yellow]", value=False)
def to_dict(self) -> Dict[str, Any]:
"""Serialize to dictionary, excluding None and empty values."""

View File

@@ -18,12 +18,13 @@ from contextlib import AbstractContextManager, nullcontext
from API.HTTP import _download_direct_file
from SYS.models import DownloadError, DownloadOptions, DownloadMediaResult
from SYS.logger import log, debug
from SYS.logger import log, debug, is_debug_enabled
from SYS.pipeline_progress import PipelineProgress
from SYS.result_table import Table
from SYS.rich_display import stderr_console as get_stderr_console
from SYS import pipeline as pipeline_context
from SYS.metadata import normalize_urls as normalize_url_list
from SYS.utils import sha256_file
from tool.ytdlp import (
YtDlpTool,
@@ -1495,6 +1496,41 @@ class Download_File(Cmdlet):
forced_single_applied = True
# Proactive fallback for single audio formats which might be unstable
if (
actual_format
and isinstance(actual_format, str)
and actual_format == "audio"
):
actual_format = "bestaudio/best"
# DEBUG: Render config panel for tracking pipeline state
if is_debug_enabled():
try:
from rich.table import Table as RichTable
from rich import box as RichBox
from tool.ytdlp import YtDlpDefaults
t = RichTable(title="Download Config", show_header=True, header_style="bold magenta", box=RichBox.ROUNDED)
t.add_column("Property", style="cyan")
t.add_column("Value", style="green")
t.add_row("URL", url)
t.add_row("Mode", mode)
t.add_row("Format", str(actual_format))
t.add_row("Playlist Items", str(actual_playlist_items))
# Browser/Cookie info from ytdlp tool
defaults = getattr(ytdlp_tool, "defaults", None)
if isinstance(defaults, YtDlpDefaults):
t.add_row("Cookie File", str(defaults.cookiefile or "None"))
t.add_row("Browser Cookies", str(defaults.cookies_from_browser or "None"))
t.add_row("User Agent", str(defaults.user_agent or "default"))
debug(t)
except Exception:
pass
# If no format explicitly chosen, we might want to check available formats
# and maybe show a table if multiple are available?
if (
actual_format
and isinstance(actual_format, str)

View File

@@ -17,7 +17,7 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Tuple
from urllib.parse import urlsplit, quote, urljoin, unquote
from SYS.logger import log, debug
from SYS.logger import log, debug, is_debug_enabled
from API.HTTP import HTTPClient
from SYS.pipeline_progress import PipelineProgress
from SYS.utils import ensure_directory, unique_path, unique_preserve_order
@@ -27,6 +27,7 @@ Cmdlet = sh.Cmdlet
CmdletArg = sh.CmdletArg
SharedArgs = sh.SharedArgs
create_pipe_object_result = sh.create_pipe_object_result
coerce_to_pipe_object = sh.coerce_to_pipe_object
normalize_result_input = sh.normalize_result_input
should_show_help = sh.should_show_help
get_field = sh.get_field
@@ -592,13 +593,33 @@ def _capture(
}
})
tool.debug_dump()
if is_debug_enabled():
try:
from rich.table import Table
from rich import box
t = Table(title="Screenshot Config", show_header=True, header_style="bold magenta", box=box.ROUNDED)
t.add_column("Property", style="cyan")
t.add_column("Value", style="green")
t.add_row("URL", options.url)
t.add_row("Format", _normalize_format(options.output_format))
# Browser details
defaults = getattr(tool, "defaults", None)
if defaults:
t.add_row("Browser", getattr(defaults, "browser", "unknown"))
t.add_row("Headless", str(getattr(defaults, "headless", "unknown")))
t.add_row("Viewport", f"{getattr(defaults, 'viewport_width', '?')}x{getattr(defaults, 'viewport_height', '?')}")
t.add_row("Timeout", f"{getattr(defaults, 'navigation_timeout_ms', '?')}ms")
t.add_row("Full Page", str(options.full_page))
t.add_row("Destination", str(destination))
debug(t)
except Exception:
pass
debug("Launching browser...")
format_name = _normalize_format(options.output_format)
headless = options.headless or format_name == "pdf"
debug(f"[_capture] Format: {format_name}, Headless: {headless}")
if format_name == "pdf" and not options.headless:
warnings.append(
"pdf output requires headless Chromium; overriding headless mode"
@@ -1129,6 +1150,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
is_temp=True,
parent_hash=hashlib.sha256(url.encode()).hexdigest(),
tag=merged_tags,
url=url, # Explicitly map url to top-level PipeObject field
source_url=url, # Map source_url as well
extra={
"source_url": url,
"archive_url": screenshot_result.archive_url,
@@ -1141,6 +1164,24 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
pipeline_context.emit(pipe_obj)
all_emitted.append(pipe_obj)
# Debug: show PipeObject preview if enabled
if is_debug_enabled():
try:
debug("[screen-shot] Output PipeObject preview")
po = coerce_to_pipe_object(pipe_obj)
from SYS.logger import _sanitize_pipe_object_for_debug as _sanitize # Or use helper if avail
# Add simple sanitize helper if not available
def _safe_table(obj):
try:
# Try calling debug_table on the object
if hasattr(obj, "debug_table"):
obj.debug_table()
except Exception:
pass
_safe_table(po)
except Exception:
pass
# If we created a local progress UI, advance it per completed item.
progress.on_emit(pipe_obj)

View File

@@ -253,76 +253,6 @@ class PlaywrightTool:
)
def config_schema() -> List[Dict[str, Any]]:
"""Return a schema describing editable Playwright tool defaults for the config UI.
Notes:
- `user_agent` is a dropdown with a `custom` option; put the real UA in
`user_agent_custom` when choosing `custom`.
- Viewport dimensions are offered as convenient choices.
- `ffmpeg_path` intentionally defaults to empty; Playwright will consult
a global `FFMPEG_PATH` environment variable (or fallback to bundled/system).
"""
_defaults = PlaywrightDefaults()
browser_choices = ["chromium", "firefox", "webkit"]
viewport_width_choices = [1920, 1366, 1280, 1024, 800]
viewport_height_choices = [1080, 900, 768, 720, 600]
return [
{
"key": "browser",
"label": "Playwright browser",
"default": _defaults.browser,
"choices": browser_choices,
},
{
"key": "headless",
"label": "Headless",
"default": str(_defaults.headless),
"choices": ["true", "false"],
},
{
"key": "user_agent",
"label": "User Agent",
"default": "default",
"choices": ["default", "native", "custom"],
},
{
"key": "user_agent_custom",
"label": "Custom User Agent (used when User Agent = custom)",
"default": "",
},
{
"key": "viewport_width",
"label": "Viewport width",
"default": _defaults.viewport_width,
"choices": viewport_width_choices,
},
{
"key": "viewport_height",
"label": "Viewport height",
"default": _defaults.viewport_height,
"choices": viewport_height_choices,
},
{
"key": "navigation_timeout_ms",
"label": "Navigation timeout (ms)",
"default": _defaults.navigation_timeout_ms,
},
{
"key": "ignore_https_errors",
"label": "Ignore HTTPS errors",
"default": str(_defaults.ignore_https_errors),
"choices": ["true", "false"],
},
{
"key": "ffmpeg_path",
"label": "FFmpeg path (leave empty to use global/bundled)",
"default": "",
},
]
def require(self) -> None:
"""Ensure Playwright is present; raise a helpful RuntimeError if not."""
try:
@@ -635,3 +565,74 @@ def config_schema() -> List[Dict[str, Any]]:
except Exception:
pass
return None
def config_schema() -> List[Dict[str, Any]]:
"""Return a schema describing editable Playwright tool defaults for the config UI.
Notes:
- `user_agent` is a dropdown with a `custom` option; put the real UA in
`user_agent_custom` when choosing `custom`.
- Viewport dimensions are offered as convenient choices.
- `ffmpeg_path` intentionally defaults to empty; Playwright will consult
a global `FFMPEG_PATH` environment variable (or fallback to bundled/system).
"""
_defaults = PlaywrightDefaults()
browser_choices = ["chromium", "firefox", "webkit"]
viewport_width_choices = [1920, 1366, 1280, 1024, 800]
viewport_height_choices = [1080, 900, 768, 720, 600]
return [
{
"key": "browser",
"label": "Playwright browser",
"default": _defaults.browser,
"choices": browser_choices,
},
{
"key": "headless",
"label": "Headless",
"default": str(_defaults.headless),
"choices": ["true", "false"],
},
{
"key": "user_agent",
"label": "User Agent",
"default": "default",
"choices": ["default", "native", "custom"],
},
{
"key": "user_agent_custom",
"label": "Custom User Agent (used when User Agent = custom)",
"default": "",
},
{
"key": "viewport_width",
"label": "Viewport width",
"default": _defaults.viewport_width,
"choices": viewport_width_choices,
},
{
"key": "viewport_height",
"label": "Viewport height",
"default": _defaults.viewport_height,
"choices": viewport_height_choices,
},
{
"key": "navigation_timeout_ms",
"label": "Navigation timeout (ms)",
"default": _defaults.navigation_timeout_ms,
},
{
"key": "ignore_https_errors",
"label": "Ignore HTTPS errors",
"default": str(_defaults.ignore_https_errors),
"choices": ["true", "false"],
},
{
"key": "ffmpeg_path",
"label": "FFmpeg path (leave empty to use global/bundled)",
"default": "",
},
]

View File

@@ -770,8 +770,8 @@ class YtDlpTool:
"""Resolve numeric heights (720, 1080p) to yt-dlp height selectors.
Examples:
"720" -> "bv*[height<=720]+ba"
"1080p" -> "bv*[height<=1080]+ba"
"720" -> "bestvideo[height<=720]+bestaudio/best[height<=720]"
"1080p" -> "bestvideo[height<=1080]+bestaudio/best[height<=1080]"
"""
if not format_str or not isinstance(format_str, str):
return None
@@ -783,10 +783,33 @@ class YtDlpTool:
# Strip trailing 'p' if present (e.g. 720p -> 720)
if s.endswith('p'):
s = s[:-1]
# Heuristic: 240/360/480/720/1080/1440/2160 are common height inputs
# But small IDs like 18, 22, 137 are format IDs.
# YouTube Format IDs are usually 2-3 digits.
# Heights are also 3-4 digits.
# "240" is ambiguous (Format 240 vs Height 240).
# We assume common video heights are intended as heights.
if s.isdigit():
height = int(s)
if height >= 144:
return f"bv*[height<={height}]+ba"
val = int(s)
# Common video heights that overlap with format IDs:
# - None currently overlap with common legacy itag IDs (17,18,22,34-38,43-46)
# or dash video IDs (133-137, 160, 242-248, 264, 271, 278, 298-315...).
# - 240, 360, 480, 720, 1080, 1440, 2160
#
# Format 240 is likely not a thing (242, 243, ... exist).
# Format 480 ... none in common lists.
# Format 720 ... none.
# So if it looks like a standard resolution, treat as height constraint.
if val in {144, 240, 360, 480, 540, 720, 1080, 1440, 2160, 2880, 4320}:
return f"bestvideo[height<={val}]+bestaudio/best[height<={val}]"
# If user types something like 500, we can also treat as height constraint if > 100
if val >= 100 and val not in {133, 134, 135, 136, 137, 160, 242, 243, 244, 247, 248, 278, 394, 395, 396, 397, 398, 399}:
return f"bestvideo[height<={val}]+bestaudio/best[height<={val}]"
return None
def _load_defaults(self) -> YtDlpDefaults: