diff --git a/envied_gui.py b/envied_gui.py new file mode 100644 index 0000000..becf92e --- /dev/null +++ b/envied_gui.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QFrame, QLabel +from PyQt6.QtCore import QProcess +from PyQt6.QtCore import Qt +import sys +import os +import subprocess +import shutil + + +CFG = os.path.abspath("./packages/envied/src/envied/envied.yaml") +class EnviedPanel(QFrame): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Envied Control ") + # Frame styling: 1px white border (no pink) + self.setObjectName("enviedFrame") + self.setStyleSheet("#enviedFrame { background-color: #2D2D2D; border: 1px solid pink; }") + # ("color: #f5c2e7; border: none; background-color:#1E1E2E;padding: 5px;") + + # Layout + layout = QVBoxLayout(self) + layout.setContentsMargins(5, 5, 5, 5) + layout.setSpacing(5) + + + # Helper to make consistently styled buttons + def make_btn(text): + b = QPushButton(text) + b.setStyleSheet( + "background-color: #1E1E2E; \ + color: #f5c2e7; \ + border-width: 2px;\ + border-color: pink;\ + font: 14px;\ + min-width: 10em;\ + padding: 5px;" + ) + b.setAutoFillBackground(True) + b.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + return b + + # Buttons + btn_run = make_btn("run envied") + btn_check = make_btn("env check") + btn_info = make_btn("env info") + btn_cfg = make_btn("config") + + layout.addWidget(btn_run) + layout.addWidget(btn_check) + layout.addWidget(btn_info) + layout.addWidget(btn_cfg) + + # --- Wire up clicks --- + # Option A: non-blocking (recommended): use QProcess so the GUI stays responsive + btn_run.clicked.connect(lambda: open_prefilled_terminal()) + btn_check.clicked.connect(lambda: self.run_cmd(["uv", "run", "envied", "env", "check"])) + btn_info.clicked.connect(lambda: self.run_cmd(["uv", "run", "envied", "env", "info"])) + '''btn_cfg.clicked.connect( + lambda: os.system('nano ./packages/envied/src/envied/envied.yaml') if os.name == 'posix' + else os.system('notepad.exe ./packages/envied/src/envied/envied.yaml') + )''' + btn_cfg.clicked.connect(open_config_detached) + + # ---- Helpers ---- + def run_cmd(self, argv): + """ + Non-blocking run using QProcess. + """ + proc = QProcess(self) + # Optional: forward output to your terminal + proc.setProgram(argv[0]) + proc.setArguments(argv[1:]) + # Show output in your launching terminal (detach these if not needed) + proc.readyReadStandardOutput.connect(lambda p=proc: print(p.readAllStandardOutput().data().decode(), end="")) + proc.readyReadStandardError.connect(lambda p=proc: print(p.readAllStandardError().data().decode(), end="")) + proc.start() + + def run_script(self, what): + """ + Placeholder hook for your 'config' action. + e.g., open a config dialog or run a script. + """ + print(f"[EnviedPanel] run_script({what}) not implemented yet.") + +def get_terminal(): + terminals = ["gnome-terminal", "xterm", "konsole", "lxterminal", "xfce4-terminal"] + for term in terminals: + if shutil.which(term): + return term + elif os.__name__ =="nt": + TERMINALS = ["WindowsTerminal.exe", "OpenConsole.exe","powershell.exe", "Terminal.exe", "cmd.exe"] + for TERMINAL in TERMINALS: + if shutil.which(TERMINAL): + return TERMINAL + raise EnvironmentError("No suitable terminal emulator found.") + +def open_prefilled_terminal(): + prefill = 'uv run envied dl --select-titles ' + term = get_terminal() + + if term == "gnome-terminal": + # -l starts login shell; -c runs the command, then we keep the shell open with 'exec bash' + cmd = [ + "gnome-terminal", + "--", + "bash", "-lc", + # read: -e enables readline, -i sets initial text + f'read -e -i "{prefill}" cmd; eval "$cmd"; exec bash' + ] + elif term == "konsole": + cmd = [ + "konsole", "-e", + "bash", "-lc", + f'read -e -i "{prefill}" cmd; eval "$cmd"; exec bash' + ] + elif term =="lxterminal": # xterm fallback + cmd = [ + "xterm", "-e", + "bash", "-lc", + f'read -e -i "{prefill}" cmd; eval "$cmd"; exec bash' + ] + + elif os.name == "nt": + # --- Windows: open PowerShell (or Windows Terminal -> PowerShell) + # and pre-fill the command line using PSReadLine. + # This requires PSReadLine (present by default on modern Windows). + ps_prefill = prefill.replace('"', r'`"') # escape quotes for PowerShell + + if shutil.which("wt.exe"): + # Windows Terminal present: open a new tab running PowerShell + cmd = [ + "wt.exe", + "powershell", + "-NoExit", + "-Command", + f"[Microsoft.PowerShell.PSConsoleReadLine]::Insert(\"{ps_prefill}\")" + ] + elif shutil.which("powershell.exe"): + # Fallback: plain PowerShell + cmd = [ + "powershell.exe", + "-NoExit", + "-Command", + f"[Microsoft.PowerShell.PSConsoleReadLine]::Insert(\"{ps_prefill}\")" + ] + else: + # Last resort: cmd.exe can't truly prefill. Keep the window open and hint user. + cmd = [ + "cmd.exe", "/K", + f'echo Please use PowerShell for prefill. Intended command: {prefill}' + ] + + + + subprocess.Popen(cmd) + + +def open_config_detached(): + if os.name == "nt": + QProcess.startDetached("notepad.exe", [CFG]) + return + + # POSIX + # Easiest: open in the default GUI editor + if shutil.which("xdg-open"): + QProcess.startDetached("xdg-open", [CFG]) + return + + # If you insist on nano, run it *inside* a terminal emulator + # (xterm is very common and works out of the box) + if shutil.which("xterm"): + QProcess.startDetached("xterm", ["-e", "nano", CFG]) + return + + # Last resort (may fail without a TTY) + QProcess.startDetached("nano", [CFG]) + +if __name__ == "__main__": + from PyQt6.QtWidgets import QApplication + app = QApplication(sys.argv) + envied_panel = EnviedPanel() + window = envied_panel + window.show() + sys.exit(app.exec()) + + + + + subprocess.Popen(cmd) diff --git a/packages/envied/src/envied/envied.yaml.save b/packages/envied/src/envied/envied.yaml.save new file mode 100644 index 0000000..84c65f4 --- /dev/null +++ b/packages/envied/src/envied/envied.yaml.save @@ -0,0 +1,153 @@ + # 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 + +# shakapackager or mp4decrypt +decryption: shakapackager + +scene_naming: false +series_year: false + +# Widevine pssh display; fold, crop or ellipsis +pssh_display: ellipsis + +# Login credentials for each Service +credentials: + ALL4: angela.slaney@gmail.com:Nood13sh + ROKU: moretonplumber@yandex.com:$WPJ~Mdh2:X@EtB + TVNZ: moretonplumber@yandex.com:penumbra + TPTV: moretonplumber@yandex.com:penumbra1234 + CBC: moretonplumber@yandex.com:penumbra +# Override default directories used across unshackle +directories: + cache: Cache + cookies: Cookies + dcsl: DCSL # Device Certificate Status List + downloads: /home/angela/Downloads/devine/ + 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 Service +cdm: + default: device + +# Use pywidevine Serve-compliant Remote CDMs +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: /home/angela/Programming/gits/devine-services/key_store.db + +- type: HTTPAPI + name: drmlab + host: http://api.drmlab.io/vault/ + password: gEX75q7I5YVkvgF5SUkcNd41IbGrDtTT +#- type: API +# name: "Online 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 + ALL4: n_m3u8dl_re + default: n_m3u8dl_re + +# 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 unshackle +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: gBM9ejjmY1J15jccH12VT7Qy + password: 1kmuCrRXnT55uAjiUTd8twv9 + server_map: + us: 6918 + uk: 2613 + nz: 100 diff --git a/packages/envied/src/envied/services/TPTV/__init__.py b/packages/envied/src/envied/services/TPTV/__init__.py index 0f1f5de..07f5d42 100644 --- a/packages/envied/src/envied/services/TPTV/__init__.py +++ b/packages/envied/src/envied/services/TPTV/__init__.py @@ -87,7 +87,7 @@ class TPTV(Service): self.profile = ctx.parent.params.get("profile") if not self.profile: self.profile = "default" - + self.session = requests.session() self.session.headers.update(self.config["headers"]) def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None: @@ -147,7 +147,7 @@ class TPTV(Service): tokens = res self.log.info(" + Acquired tokens...") - cache.set(tokens) + # cache.set(tokens) self.authorization = tokens diff --git a/packages/vinefeeder/src/vinefeeder/__main__.py b/packages/vinefeeder/src/vinefeeder/__main__.py index cb90844..fc15705 100644 --- a/packages/vinefeeder/src/vinefeeder/__main__.py +++ b/packages/vinefeeder/src/vinefeeder/__main__.py @@ -1,5 +1,5 @@ from PyQt6.QtWidgets import (QApplication,QWidget,QVBoxLayout,QLabel,QLineEdit,QPushButton,QCheckBox,QFrame,) -from PyQt6.QtCore import Qt +from PyQt6.QtCore import Qt, QProcess from PyQt6.QtGui import QPalette, QColor from PyQt6.QtCore import QTimer from PyQt6.QtWidgets import QHBoxLayout, QSlider @@ -61,23 +61,6 @@ _CFG_NAME = "config.yaml" -'''def load_config_with_fallback() -> tuple[dict, Path | None]: - """ - Returns (config_dict, user_path_if_used_or_None). - Prefers user config; falls back to package-bundled default. - """ - user_path = _user_config_path() - if user_path.exists(): - data = yaml.safe_load(user_path.read_text(encoding="utf-8")) or {} - return data, user_path - - # packaged default - with resources.files(_PKG_NAME).joinpath(_CFG_NAME).open("rb") as f: - data = yaml.safe_load(f) or {} - return data, None''' - - - def derive_loader_class_name(service_modname: str) -> str: """ Derive a class name from a service module name. @@ -319,16 +302,15 @@ class VineFeeder(QWidget): # Set button text to white in dark mode, remove red border for i in range(self.highlighted_layout.count()): - button = self.highlighted_layout.itemAt(i).widget() - if isinstance(button, QPushButton): - button.setStyleSheet(""" + b = self.highlighted_layout.itemAt(i).widget() + if isinstance(b, QPushButton) and b.objectName() != "enviedButton": # <-- skip Envied + b.setStyleSheet(""" color: white; background-color:#1E1E2E; border: none; padding: 5px; """) - button.repaint() # Force update of the button's appearance - + b.repaint() else: self.setPalette(QApplication.palette()) self.search_url_label.setStyleSheet("color: black;") @@ -336,15 +318,15 @@ class VineFeeder(QWidget): self.batch_label.setStyleSheet("color: black;") for i in range(self.highlighted_layout.count()): - button = self.highlighted_layout.itemAt(i).widget() - if isinstance(button, QPushButton): - button.setStyleSheet(""" + b = self.highlighted_layout.itemAt(i).widget() + if isinstance(b, QPushButton) and b.objectName() != "enviedButton": # <-- skip Envied + b.setStyleSheet(""" color: black; background-color: #aeaeae; border: none; padding: 5px; """) - button.repaint() # Force update of the button's appearance + b.repaint() # Update batch_label based on dark mode and batch mode state if self.batch_slider.value() == 1: self.batch_label.setStyleSheet("color: lightgreen; padding-left: 5px; border: none;") @@ -477,8 +459,37 @@ class VineFeeder(QWidget): button.clicked.connect( lambda: self.run_script("./gui.py")) self.highlighted_layout.addWidget(button) + if os.name == "posix": # linux + button = QPushButton("Envied") + button.setObjectName("enviedButton") # <-- add this + button.setStyleSheet("color: #f5c2e7; border: none; background-color:#1E1E2E;padding: 5px;") # your pink + button.clicked.connect(lambda: self.run_script("./envied_gui.py")) + + elif os.name == "nt": + button = QPushButton("Envied Config") + button.setObjectName("enviedButton") # <-- add this + button.setStyleSheet("color: #f5c2e7; border: none; background-color:#1E1E2E;padding: 5px;") # your pink + + + CFG = os.path.abspath("./packages/envied/src/envied/envied.yaml") + button.clicked.connect(lambda: QProcess.startDetached("notepad.exe", [CFG])) + self.highlighted_layout.addWidget(button) + + def run_cmd(self, argv): + """ + Non-blocking run using QProcess. + """ + proc = QProcess(self) + # Optional: forward output to your terminal + proc.setProgram(argv[0]) + proc.setArguments(argv[1:]) + # Show output in your launching terminal (detach these if not needed) + proc.readyReadStandardOutput.connect(lambda p=proc: print(p.readAllStandardOutput().data().decode(), end="")) + proc.readyReadStandardError.connect(lambda p=proc: print(p.readAllStandardError().data().decode(), end="")) + proc.start() + def run_script(self, path: str): """ Rules: diff --git a/packages/vinefeeder/src/vinefeeder/config.yaml b/packages/vinefeeder/src/vinefeeder/config.yaml index 0057b81..10f0c6a 100644 --- a/packages/vinefeeder/src/vinefeeder/config.yaml +++ b/packages/vinefeeder/src/vinefeeder/config.yaml @@ -1,2 +1,3 @@ BATCH_DOWNLOAD: false -TERMINAL_RESET: true +TERMINAL_RESET: false +TERMINAL: gnome-terminal