diff --git a/.gitignore b/.gitignore index 5c3c8d3..85b665c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ Cache/ Logs/ Downloads packages/envied/src/envied/logs/ - +packages/envied/src/envied/envied.yaml +images/ # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] diff --git a/WVDs/device.wvd b/WVDs/device.wvd new file mode 100644 index 0000000..e55d373 Binary files /dev/null and b/WVDs/device.wvd differ diff --git a/packages/envied/src/envied/unshackle-example.yaml b/packages/envied/src/envied/envied-example.yaml similarity index 100% rename from packages/envied/src/envied/unshackle-example.yaml rename to packages/envied/src/envied/envied-example.yaml diff --git a/packages/envied/src/envied/envied-working-example.yaml b/packages/envied/src/envied/envied-working-example.yaml new file mode 100644 index 0000000..ec5f166 --- /dev/null +++ b/packages/envied/src/envied/envied-working-example.yaml @@ -0,0 +1,150 @@ +# Group or Username to postfix to the end of all download fienames following a dash +#tag: '' + +# Set terminal background color (custom option not in CONFIG.md) +set_terminal_bg: false + +# Muxing configuration +muxing: + set_title: false + +# +decryption: mp4decrypt + +scene_naming: false +series_year: false + +# Login credentials for each Service +credentials: + ALL4: email:password + ROKU: email:password + TVNZ: email:password + TPTV: email:password + CBC: email:password +# Override default directories used across envied +directories: + cache: Cache + cookies: Cookies + dcsl: DCSL # Device Certificate Status List + downloads: Downloads + logs: Logs + temp: Temp + wvds: WVDs + prds: PRDs + # Additional directories that can be configured: + # commands: Commands + #services: + # - ./envied/services/ + vaults: vaults/ + # fonts: Fonts + +# Pre-define which Widevine or PlayReady device to use for each Serviceuv run envied +cdm: + default: device + +# Use pywidevine Serve-compliant Remote CDMs +# Currently not working! +remote_cdm: + - name: "CDRM_Project_API" + device_type: ANDROID + system_id: 22590 + security_level: 3 + host: "https://cdrm-project.com/remotecdm/widevine" + secret: "CDRM" + device_name: "public" + + +# Key Vaults store your obtained Content Encryption Keys (CEKs) +key_vaults: +#- type: SQLite +# name: Local vault +# path: key_store.db + +- type: HTTPAPI + name: drmlab + host: http://api.drmlab.io/vault/ + password: gEX75q7I5YVkvgF5SUkcNd41IbGrDtTT +- type: API + name: CDRM Vault + uri: https://cdrm-project.com/api/cache + token: CDRM + + +# Choose what software to use to download data +downloader: n_m3u8dl_re +# Options: requests | aria2c | curl_impersonate | n_m3u8dl_re +# Can also be a mapping: +# downloader: +# NF: requests +# AMZN: n_m3u8dl_re +# DSNP: n_m3u8dl_re +# default: requests + +# aria2c downloader configuration +aria2c: + max_concurrent_downloads: 4 + max_connection_per_server: 3 + split: 5 + file_allocation: falloc # none | prealloc | falloc | trunc + +# N_m3u8DL-RE downloader configuration +n_m3u8dl_re: + thread_count: 16 + ad_keyword: "advertisement" + use_proxy: true + +# curl_impersonate downloader configuration +curl_impersonate: + browser: chrome120 + +# Pre-define default options and switches of the dl command +dl: + best: true + sub_format: srt + downloads: 4 + workers: 16 + +# Chapter Name to use when exporting a Chapter without a Name +chapter_fallback_name: "Chapter {j:02}" + +# Case-Insensitive dictionary of headers for all Services +headers: + Accept-Language: "en-US,en;q=0.8" + User-Agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36" + +# Override default filenames used across envied +filenames: + log: "envied_{name}_{time}.log" + config: "config.yaml" + root_config: "envied.yaml" + chapters: "Chapters_{title}_{random}.txt" + subtitle: "Subtitle_{id}_{language}.srt" + +# API key for The Movie Database (TMDB) +tmdb_api_key: "" + +# conversion_method: +# - auto (default): Smart routing - subby for WebVTT/SAMI, standard for others +# - subby: Always use subby with advanced processing +# - pycaption: Use only pycaption library (no SubtitleEdit, no subby) +# - subtitleedit: Prefer SubtitleEdit when available, fall back to pycaption +subtitle: + conversion_method: auto + sdh_method: auto + + +# Configuration data for each Service +services: + # Service-specific configuration goes here + # EXAMPLE: + # api_key: "service_specific_key" + +# Legacy NordVPN configuration (use proxy_providers instead) +#proxy_providers: +# nordvpn: +# username: username +# password: password +# server_map: +# us: 6918 +# uk: 2613 +# nz: 100 diff --git a/tools/fix_imports.py b/tools/fix_imports.py new file mode 100644 index 0000000..acac416 --- /dev/null +++ b/tools/fix_imports.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +""" +fix_imports.py — Make intra-package imports in a src/ layout relative. + +Usage: + python tools/fix_imports.py \ + --root packages/vinefeeder/src \ + --pkg-name vinefeeder \ + [--apply] + +By default it prints a plan (dry-run). Add --apply to rewrite files. +Backups (*.bak) are created when applying changes. +""" +from __future__ import annotations + +import argparse +import ast +import difflib +import shutil +from pathlib import Path +from typing import Iterable, List, Tuple + + +def find_package_modules(pkg_dir: Path) -> set[str]: + """ + Return a set of top-level module/package names inside pkg_dir. + Example: { 'pretty', 'parsing_utils', 'batchloader', 'services', ... } + """ + names: set[str] = set() + for p in pkg_dir.iterdir(): + if p.name.startswith("_"): + continue + if p.is_dir() and (p / "__init__.py").exists(): + names.add(p.name) + elif p.suffix == ".py": + names.add(p.stem) + return names + + +def rewrite_imports( + src: str, + file_path: Path, + pkg_name: str, + local_modules: set[str], +) -> Tuple[str, List[str]]: + """ + Return (new_source, notes). Keeps formatting line-by-line as much as possible. + We only rewrite simple import statements using AST nodes to decide the intent, + then patch lines textually. + """ + notes: List[str] = [] + try: + tree = ast.parse(src) + except SyntaxError as e: + notes.append(f"SKIP (syntax error): {e}") + return src, notes + + # Collect edits as (lineno-1, new_line) for lines we fully replace + edits: dict[int, str] = {} + + # We’ll need original lines to reconstruct edits + lines = src.splitlines(keepends=False) + + for node in tree.body: + # Handle: import X [, Y] [as Z] + if isinstance(node, ast.Import): + # If *all* top-level names refer to local modules, we can rewrite: + # import pretty -> from . import pretty + # import pretty as p -> from . import pretty as p + # import pretty, parsing_utils -> from . import pretty, parsing_utils + local_aliases = [] + non_local = False + for alias in node.names: + top = alias.name.split(".")[0] + if top in local_modules or top == pkg_name: + local_aliases.append(alias) + else: + non_local = True + break + + if non_local or not local_aliases: + continue # leave it + + # Build a single "from . import ..." line preserving aliases + parts = [] + for alias in node.names: + top = alias.name.split(".")[0] + # Only rewrite locals, leave non-locals untouched (rare mixed case) + if top in local_modules or top == pkg_name: + if alias.asname: + parts.append(f"{alias.name} as {alias.asname}") + else: + parts.append(alias.name) + else: + # If we ever hit mixed, bail out and don't rewrite + parts = [] + break + if not parts: + continue + + new_line = f"from . import {', '.join(parts)}" + # Replace the entire original line (best-effort: single-line import) + lineno = node.lineno - 1 + old = lines[lineno].strip() + edits[lineno] = new_line + notes.append(f"import→relative: `{old}` -> `{new_line}`") + + # Handle: from X import Y + elif isinstance(node, ast.ImportFrom): + # Already relative? + if node.level and node.level > 0: + continue + + if node.module is None: + continue + + top = node.module.split(".")[0] + + # Case A: from vinefeeder.something import thing + if top == pkg_name: + # Convert to from .something import thing (preserve the tail) + tail = node.module.split(".", 1)[1] if "." in node.module else "" + dot_path = f".{tail}" if tail else "." + names = [] + for alias in node.names: + if alias.asname: + names.append(f"{alias.name} as {alias.asname}") + else: + names.append(alias.name) + new_line = f"from {dot_path} import {', '.join(names)}" + lineno = node.lineno - 1 + old = lines[lineno].strip() + edits[lineno] = new_line + notes.append(f"abs→relative: `{old}` -> `{new_line}`") + continue + + # Case B: from sibling import thing (e.g., from pretty import foo) + if top in local_modules: + # Keep subpath if present (e.g., services.util) + tail = node.module + new_line = f"from .{tail} import " + ", ".join( + f"{n.name} as {n.asname}" if n.asname else n.name for n in node.names + ) + lineno = node.lineno - 1 + old = lines[lineno].strip() + edits[lineno] = new_line + notes.append(f"sibling→relative: `{old}` -> `{new_line}`") + + if not edits: + return src, notes + + # Apply edits (line-level replacement) + new_lines = [] + for i, line in enumerate(lines): + new_lines.append(edits.get(i, line)) + new_src = "\n".join(new_lines) + ("\n" if src.endswith("\n") else "") + + return new_src, notes + + +def iter_python_files(pkg_dir: Path) -> Iterable[Path]: + for p in pkg_dir.rglob("*.py"): + yield p + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--root", required=True, help="Path to the src/ root (e.g., packages/vinefeeder/src)") + ap.add_argument("--pkg-name", required=True, help="Top-level package name (e.g., vinefeeder)") + ap.add_argument("--apply", action="store_true", help="Rewrite files in place (creates .bak backups)") + args = ap.parse_args() + + src_root = Path(args.root).resolve() + pkg_dir = (src_root / args.pkg_name).resolve() + + if not pkg_dir.exists(): + print(f"[error] Package dir not found: {pkg_dir}") + return 2 + + local_modules = find_package_modules(pkg_dir) + print(f"[info] Package: {args.pkg_name}") + print(f"[info] Package dir: {pkg_dir}") + print(f"[info] Local modules found: {sorted(local_modules)}") + print() + + total_changes = 0 + + for py in iter_python_files(pkg_dir): + old = py.read_text(encoding="utf-8") + new, notes = rewrite_imports(old, py, args.pkg_name, local_modules) + if not notes: + continue + + total_changes += 1 + print(f"--- {py}") + for n in notes: + print(" -", n) + + if args.apply and new != old: + bak = py.with_suffix(py.suffix + ".bak") + shutil.copy2(py, bak) + py.write_text(new, encoding="utf-8") + + # Show a small unified diff for context + diff = difflib.unified_diff( + old.splitlines(), new.splitlines(), + fromfile=str(py), + tofile=str(py), + lineterm="" + ) + print("\n".join(diff)) + print() + + if total_changes == 0: + print("[info] No imports to rewrite.") + else: + print(f"[done] Files with changes: {total_changes} {'(dry-run)' if not args.apply else ''}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())