Add download video and save playlist support.

This commit is contained in:
wei
2026-08-25 17:46:48 +08:00
parent eadc5c6267
commit 04e03c132d
19 changed files with 2514 additions and 1386 deletions
+91 -25
View File
@@ -1,15 +1,20 @@
import base64
import binascii
import hashlib
import hmac
import json
import platform
import shutil
import urllib
import winreg
try:
import winreg
except ImportError: # Windows-only standard library module
winreg = None
from pathlib import Path
from urllib.parse import unquote
import sys
import os
import logging
import requests
logger = logging.getLogger("PlaylistSaver.Utils")
logger.setLevel(logging.INFO)
@@ -20,6 +25,9 @@ def set_info(target_logger):
logger = target_logger
def key_exists(hive, sub_key):
if winreg is None:
return False
try:
# Attempt to open the key for reading
with winreg.OpenKey(hive, sub_key, 0, winreg.KEY_READ) as key:
@@ -42,9 +50,9 @@ def create_url_scheme(main_file_path, work_dir):
command_key = winreg.CreateKey(open_key, "command")
if getattr(sys, 'frozen', False):
command = f"\"{sys.executable}\" --new-work-dir \"{work_dir}\" \"%1\""
command = f"\"{sys.executable}\" --work-dir \"{work_dir}\" \"%1\""
else:
command = f"\"{sys.executable}\" \"{main_file_path}\" --new-work-dir \"{work_dir}\" \"%1\""
command = f"\"{sys.executable}\" \"{main_file_path}\" --work-dir \"{work_dir}\" \"%1\""
winreg.SetValueEx(command_key, None, 0, winreg.REG_SZ, command)
@@ -53,12 +61,15 @@ def create_url_scheme(main_file_path, work_dir):
except OSError as e:
logger.error("Unable to create URL scheme: %s", e)
def check_url_scheme(main_file_path, work_dir):
def check_url_scheme(executable_path, work_dir):
if winreg is None:
return
try:
if not key_exists(winreg.HKEY_LOCAL_MACHINE, "PlaylistSaver"):
create_url_scheme(main_file_path, work_dir)
if not key_exists(winreg.HKEY_CURRENT_USER, r"Software\Classes\PlaylistSaver"):
create_url_scheme(executable_path, work_dir)
except FileNotFoundError:
create_url_scheme(main_file_path, work_dir)
create_url_scheme(executable_path, work_dir)
def read_cookies(cookie_path):
try:
@@ -84,19 +95,54 @@ def save_base64_netscape_cookie(cookies_encoded, cookie_path):
logger.error("Unable to decode base64 cookie: %s", cookies_encoded)
return
logger.debug("==== COOKIE FILE ====")
for line in cookies.split("\n"):
print(line, "=>", len(line.split("\t")))
logger.debug("=====================")
save_cookies(cookies, cookie_path)
def parser_url_scheme(url, cookie_path: Path):
def _decode_urlsafe(value: str) -> bytes:
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
def decrypt_cookie_payload(payload: str, secret_key: str) -> str:
raw = _decode_urlsafe(payload)
if len(raw) < 49 or raw[0] != 1:
raise ValueError("Unsupported encrypted cookie payload.")
master_key = _decode_urlsafe(secret_key)
if len(master_key) != 32:
raise ValueError("Invalid PlaylistSaver secret key.")
signed_data, supplied_tag = raw[:-32], raw[-32:]
nonce, ciphertext = raw[1:17], raw[17:-32]
encryption_key = hmac.new(master_key, b"PlaylistSaver cookie encryption", hashlib.sha256).digest()
authentication_key = hmac.new(master_key, b"PlaylistSaver cookie authentication", hashlib.sha256).digest()
expected_tag = hmac.new(authentication_key, signed_data, hashlib.sha256).digest()
if not hmac.compare_digest(supplied_tag, expected_tag):
raise ValueError("Cookie payload authentication failed. Check the Helper secret key.")
plaintext = bytearray(len(ciphertext))
for offset in range(0, len(ciphertext), 32):
counter = (offset // 32).to_bytes(4, "big")
stream = hmac.new(encryption_key, nonce + counter, hashlib.sha256).digest()
block = ciphertext[offset:offset + 32]
plaintext[offset:offset + len(block)] = bytes(a ^ b for a, b in zip(block, stream))
return plaintext.decode("utf-8")
def parser_url_scheme(url, cookie_path: Path, secret_key: str | None = None):
logger.debug("Parsing URL scheme: %s", url)
url = urllib.parse.urlparse(unquote(url))
# parse_qs performs percent-decoding itself. Decoding the whole URL first
# would turn an encoded Base64 "+" into a query-string space.
url = urllib.parse.urlparse(url)
parameters = urllib.parse.parse_qs(url.query)
if "encryptedcookies" in parameters:
if not secret_key:
raise ValueError("PlaylistSaver secret key is unavailable.")
cookies = decrypt_cookie_payload(parameters["encryptedcookies"][0], secret_key)
save_cookies(cookies, cookie_path)
return
func_map = {
"base64cookies": {
"func": save_base64_netscape_cookie,
@@ -133,27 +179,33 @@ def find_mpv_path(program_dir: Path):
platform_name = platform.system().lower()
machine = platform.machine().lower()
if platform.system().lower() == "windows" and ("amd64" in machine or "x86_64" in machine):
if platform_name == "windows" and ("amd64" in machine or "x86_64" in machine):
local_candidates.append(Path(program_dir, "bin", "mpv", "nt", "x64", "mpv.exe"))
elif platform.system().lower() == "windows" and ("arm64" in machine or "aarch64" in machine):
elif platform_name == "windows" and ("arm64" in machine or "aarch64" in machine):
local_candidates.append(Path(program_dir, "bin", "mpv", "nt", "arm", "mpv.exe"))
elif platform.system().lower() == "windows" and machine in {"x86", "i386", "i686"}:
elif platform_name == "windows" and machine in {"x86", "i386", "i686"}:
local_candidates.append(Path(program_dir, "bin", "mpv", "nt", "x86", "mpv.exe"))
if platform.system().lower() == "darwin" and ("amd64" in machine or "x86_64" in machine):
if platform_name == "darwin" and ("amd64" in machine or "x86_64" in machine):
local_candidates.append(Path(program_dir, "bin", "mpv", "darwin", "x64", "mpv"))
elif platform.system() == "darwin" and ("arm64" in machine or "aarch64" in machine):
elif platform_name == "darwin" and ("arm64" in machine or "aarch64" in machine):
local_candidates.append(Path(program_dir, "bin", "mpv", "darwin", "arm", "mpv"))
if platform.system().lower() == "linux" and ("amd64" in machine or "x86_64" in machine):
if platform_name == "linux" and ("amd64" in machine or "x86_64" in machine):
local_candidates.append(Path(program_dir, "bin", "mpv", "linux", "x64", "mpv"))
elif platform.system().lower() == "linux" and ("arm64" in machine or "aarch64" in machine):
elif platform_name == "linux" and ("arm64" in machine or "aarch64" in machine):
local_candidates.append(Path(program_dir, "bin", "mpv", "linux", "arm", "mpv"))
elif platform.system().lower() == "linux" and machine in {"x86", "i386", "i686"}:
local_candidates.append(Path(program_dir, "bin", "mpv", "linux", "x86", "mpv.exe"))
elif platform_name == "linux" and machine in {"x86", "i386", "i686"}:
local_candidates.append(Path(program_dir, "bin", "mpv", "linux", "x86", "mpv"))
for candidate in local_candidates:
if candidate.exists() and candidate.is_file():
if os.name != "nt" and not os.access(candidate, os.X_OK):
try:
candidate.chmod(candidate.stat().st_mode | 0o111)
except OSError as exc:
logger.warning("Unable to make bundled mpv executable: %s", exc)
continue
return candidate
# failback to PATH mpv (if available)
@@ -201,4 +253,18 @@ def cookie_string_to_netscape(cookie_str):
value.strip()
]))
return "\n".join(lines)
return "\n".join(lines)
def download_file(url, dest: Path):
try:
dest.parent.mkdir(parents=True, exist_ok=True)
with requests.get(url, stream=True, timeout=30) as r:
r.raise_for_status()
with dest.open(mode="wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return True
except Exception as e:
logger.error("Unable to download file: %s", e)
return False