Gui update & Services

This commit is contained in:
VineFeeder
2025-11-11 15:19:35 +00:00
parent c71c631dc5
commit 4e9300197d
5 changed files with 385 additions and 30 deletions
+190
View File
@@ -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)
+153
View File
@@ -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
@@ -87,7 +87,7 @@ class TPTV(Service):
self.profile = ctx.parent.params.get("profile") self.profile = ctx.parent.params.get("profile")
if not self.profile: if not self.profile:
self.profile = "default" self.profile = "default"
self.session = requests.session()
self.session.headers.update(self.config["headers"]) self.session.headers.update(self.config["headers"])
def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None: def authenticate(self, cookies: Optional[MozillaCookieJar] = None, credential: Optional[Credential] = None) -> None:
@@ -147,7 +147,7 @@ class TPTV(Service):
tokens = res tokens = res
self.log.info(" + Acquired tokens...") self.log.info(" + Acquired tokens...")
cache.set(tokens) # cache.set(tokens)
self.authorization = tokens self.authorization = tokens
+38 -27
View File
@@ -1,5 +1,5 @@
from PyQt6.QtWidgets import (QApplication,QWidget,QVBoxLayout,QLabel,QLineEdit,QPushButton,QCheckBox,QFrame,) 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.QtGui import QPalette, QColor
from PyQt6.QtCore import QTimer from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QHBoxLayout, QSlider 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: def derive_loader_class_name(service_modname: str) -> str:
""" """
Derive a class name from a service module name. 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 # Set button text to white in dark mode, remove red border
for i in range(self.highlighted_layout.count()): for i in range(self.highlighted_layout.count()):
button = self.highlighted_layout.itemAt(i).widget() b = self.highlighted_layout.itemAt(i).widget()
if isinstance(button, QPushButton): if isinstance(b, QPushButton) and b.objectName() != "enviedButton": # <-- skip Envied
button.setStyleSheet(""" b.setStyleSheet("""
color: white; color: white;
background-color:#1E1E2E; background-color:#1E1E2E;
border: none; border: none;
padding: 5px; padding: 5px;
""") """)
button.repaint() # Force update of the button's appearance b.repaint()
else: else:
self.setPalette(QApplication.palette()) self.setPalette(QApplication.palette())
self.search_url_label.setStyleSheet("color: black;") self.search_url_label.setStyleSheet("color: black;")
@@ -336,15 +318,15 @@ class VineFeeder(QWidget):
self.batch_label.setStyleSheet("color: black;") self.batch_label.setStyleSheet("color: black;")
for i in range(self.highlighted_layout.count()): for i in range(self.highlighted_layout.count()):
button = self.highlighted_layout.itemAt(i).widget() b = self.highlighted_layout.itemAt(i).widget()
if isinstance(button, QPushButton): if isinstance(b, QPushButton) and b.objectName() != "enviedButton": # <-- skip Envied
button.setStyleSheet(""" b.setStyleSheet("""
color: black; color: black;
background-color: #aeaeae; background-color: #aeaeae;
border: none; border: none;
padding: 5px; padding: 5px;
""") """)
button.repaint() # Force update of the button's appearance b.repaint()
# Update batch_label based on dark mode and batch mode state # Update batch_label based on dark mode and batch mode state
if self.batch_slider.value() == 1: if self.batch_slider.value() == 1:
self.batch_label.setStyleSheet("color: lightgreen; padding-left: 5px; border: none;") self.batch_label.setStyleSheet("color: lightgreen; padding-left: 5px; border: none;")
@@ -477,7 +459,36 @@ class VineFeeder(QWidget):
button.clicked.connect( button.clicked.connect(
lambda: self.run_script("./gui.py")) lambda: self.run_script("./gui.py"))
self.highlighted_layout.addWidget(button) 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): def run_script(self, path: str):
""" """
@@ -1,2 +1,3 @@
BATCH_DOWNLOAD: false BATCH_DOWNLOAD: false
TERMINAL_RESET: true TERMINAL_RESET: false
TERMINAL: gnome-terminal