Add download video and save playlist support.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user