Some preparing for Qt support...

This commit is contained in:
2026-06-15 22:08:02 +08:00
parent fba2ba1221
commit 76904a62e3
3 changed files with 1239 additions and 135 deletions
+19 -135
View File
@@ -1,6 +1,4 @@
import asyncio import asyncio
import base64
import binascii
import datetime import datetime
import json import json
import logging import logging
@@ -13,24 +11,21 @@ import time
from pathlib import Path from pathlib import Path
from tkinter import messagebox, simpledialog from tkinter import messagebox, simpledialog
from typing import Callable, List from typing import Callable, List
from yt_dlp.networking.common import Request, _REQUEST_HANDLERS, _RH_PREFERENCES
from PIL import Image from PIL import Image
from bs4 import BeautifulSoup
import customtkinter as ctk import customtkinter as ctk
import urllib
from urllib.parse import unquote
import click import click
import requests import requests
import winreg
from yt_dlp import YoutubeDL from yt_dlp import YoutubeDL
from utils import save_json, read_json, check_url_scheme, parser_url_scheme
VERSION = "Prototype" VERSION = "Alpha-2-QtTest"
ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = Path.cwd()
save_path = os.path.join(ROOT_DIR, "save") save_path = os.path.join(ROOT_DIR, "save")
temp_path = os.path.join(ROOT_DIR, "temp") temp_path = os.path.join(ROOT_DIR, "temp")
profiles_path = os.path.join(save_path, "profiles") profiles_path = os.path.join(save_path, "profiles")
profiles_meta_path = os.path.join(save_path, "profiles.json") profiles_meta_path = os.path.join(save_path, "profiles.json")
DEFAULT_PROFILE_NAME = "default" DEFAULT_PROFILE_NAME = "default"
active_profile_name = DEFAULT_PROFILE_NAME active_profile_name = DEFAULT_PROFILE_NAME
cookie_path = os.path.join(profiles_path, DEFAULT_PROFILE_NAME, "cookies.txt") cookie_path = os.path.join(profiles_path, DEFAULT_PROFILE_NAME, "cookies.txt")
@@ -39,59 +34,9 @@ logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
fetch_playlist_lock = threading.Lock() fetch_playlist_lock = threading.Lock()
mpv_processes = {} mpv_processes: dict[ctk.CTkToplevel, subprocess.Popen] = {}
download_playlists_lock = threading.Lock() download_playlists_lock = threading.Lock()
def key_exists(hive, sub_key):
try:
# Attempt to open the key for reading
with winreg.OpenKey(hive, sub_key, 0, winreg.KEY_READ) as key:
return True
except FileNotFoundError:
return False
def create_url_scheme():
try:
scheme = "PlaylistSaver"
base = r"Software\Classes\\" + scheme
key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, base)
winreg.SetValueEx(key, None, 0, winreg.REG_SZ, f"URL:{scheme} Protocol")
winreg.SetValueEx(key, "URL Protocol", 0, winreg.REG_SZ, "")
shell_key = winreg.CreateKey(key, "shell")
open_key = winreg.CreateKey(shell_key, "open")
command_key = winreg.CreateKey(open_key, "command")
if getattr(sys, 'frozen', False):
command = f"\"{sys.executable}\" \"%1\""
else:
command = f"\"{sys.executable}\" \"{os.path.abspath(__file__)}\" \"%1\""
winreg.SetValueEx(command_key, None, 0, winreg.REG_SZ, command)
logger.info("URL scheme registered.")
except OSError as e:
logger.error("Unable to create URL scheme:", e)
def check_url_scheme():
try:
if not key_exists(winreg.HKEY_LOCAL_MACHINE, "PlaylistSaver"):
create_url_scheme()
except FileNotFoundError:
create_url_scheme()
def read_cookies():
try:
with open(cookie_path, "r", encoding='utf-8') as f:
return f.read()
except FileNotFoundError:
logger.error("Cookies file not found.")
except Exception as e:
logger.error("An unexpected error occurred while reading cookies:", e)
def load_profiles_meta(): def load_profiles_meta():
meta = read_json(profiles_meta_path) meta = read_json(profiles_meta_path)
if not meta: if not meta:
@@ -175,14 +120,6 @@ def get_cookie_owner():
print("Cookie owner: ", json.dumps(data, indent=2)) print("Cookie owner: ", json.dumps(data, indent=2))
def save_cookies(cookies):
try:
os.makedirs(os.path.dirname(cookie_path), exist_ok=True)
with open(cookie_path, "w", encoding='utf-8') as f:
f.write(cookies)
except Exception as e:
logger.error("An unexpected error occurred while saving cookies:", e)
def cookie_string_to_netscape(cookie_str): def cookie_string_to_netscape(cookie_str):
lines = ["# Netscape HTTP Cookie File"] lines = ["# Netscape HTTP Cookie File"]
@@ -205,56 +142,6 @@ def cookie_string_to_netscape(cookie_str):
return "\n".join(lines) return "\n".join(lines)
def pause():
input("Press enter to continue...")
def save_base64_netscape_cookie(cookies_encoded):
try:
cookies = base64.b64decode(cookies_encoded).decode("utf-8")
except binascii.Error:
print("Unable to decode base64 cookie:", cookies_encoded)
return None
print("==== COOKIE FILE ====")
for line in cookies.split("\n"):
print(line, "=>", len(line.split("\t")))
print("=====================")
save_cookies(cookies)
def parser_url_scheme(url):
print("Parsing URL scheme:", url)
url = urllib.parse.urlparse(unquote(sys.argv[1]))
parameters = urllib.parse.parse_qs(url.query)
func_map = {
"base64cookies": save_base64_netscape_cookie,
}
for arg_name in parameters:
match_func = func_map.get(arg_name, None)
if match_func:
match_func(parameters[arg_name][0])
else:
print("Unknown parameter:", arg_name, " Value:", parameters[arg_name])
def save_json(data: dict, dest):
try:
os.makedirs(os.path.dirname(dest), exist_ok=True)
with open(dest, "w", encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
except Exception as e:
logger.error("Unable to save json:", e)
def read_json(path):
try:
with open(path, "r", encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error("Unable to read json:", e)
def load_playlists(): def load_playlists():
playlists_path = get_profile_playlists_cache_path() playlists_path = get_profile_playlists_cache_path()
@@ -385,7 +272,7 @@ def play_video_with_mpv(video_url: str, window: ctk.CTkToplevel, status_label: c
mpv_path = resolve_mpv_path() mpv_path = resolve_mpv_path()
if not mpv_path: if not mpv_path:
window.after(0, lambda: status_label.configure(text="mpv not found in bin/ or PATH.")) window.after(0, lambda label=status_label: label.configure(text="mpv not found in bin/ or PATH."))
return return
try: try:
@@ -397,11 +284,11 @@ def play_video_with_mpv(video_url: str, window: ctk.CTkToplevel, status_label: c
) )
except Exception as e: except Exception as e:
logger.error("Unable to start mpv: %s", e) logger.error("Unable to start mpv: %s", e)
window.after(0, lambda: status_label.configure(text=f"Unable to start mpv: {e}")) window.after(0, lambda label=status_label: label.configure(text=f"Unable to start mpv: {e}"))
return return
mpv_processes[window] = process mpv_processes[window] = process
window.after(0, lambda: status_label.configure(text="Playing in mpv...")) window.after(0, lambda label=status_label: label.configure(text="Playing in mpv..."))
process.wait() process.wait()
@@ -410,7 +297,7 @@ def play_video_with_mpv(video_url: str, window: ctk.CTkToplevel, status_label: c
if window.winfo_exists(): if window.winfo_exists():
status_label.configure(text="Playback finished.") status_label.configure(text="Playback finished.")
window.after(0, on_finish) window.after(0, lambda func=on_finish: func())
def download_file(url, dest): def download_file(url, dest):
try: try:
@@ -431,7 +318,7 @@ def background(url_schema, cookie_file: str, callback: Callable):
if url_schema is not None: if url_schema is not None:
try: try:
parser_url_scheme(url_schema) parser_url_scheme(url_schema, work_dir=ROOT_DIR)
except Exception as e: except Exception as e:
logger.error("Unable to parse URL scheme:", e) logger.error("Unable to parse URL scheme:", e)
input("Press enter to continue...") input("Press enter to continue...")
@@ -442,10 +329,7 @@ def background(url_schema, cookie_file: str, callback: Callable):
data = load_playlists() data = load_playlists()
if not data: if not data:
# messagebox.showerror("Error", "No playlists found.")
with fetch_playlist_lock: with fetch_playlist_lock:
opts = get_ydl_opts() opts = get_ydl_opts()
opts.update({ opts.update({
'extract_flat': True, 'extract_flat': True,
@@ -458,8 +342,8 @@ def background(url_schema, cookie_file: str, callback: Callable):
data = ydl.extract_info("https://www.youtube.com/feed/playlists", download=False) data = ydl.extract_info("https://www.youtube.com/feed/playlists", download=False)
except Exception as e: except Exception as e:
print("An error occurred:", e) logger.error("An error occurred:", e)
pause() messagebox.showerror("Error", f"Unable to fetch playlists: {e}")
sys.exit(-1) sys.exit(-1)
save_json(data, get_profile_playlists_cache_path()) save_json(data, get_profile_playlists_cache_path())
@@ -476,7 +360,7 @@ def main(url_schema, cookie_file: str):
def on_data_ready(data): def on_data_ready(data):
current_playlists_data["entries"] = data.get("entries", []) current_playlists_data["entries"] = data.get("entries", [])
root.after(0, lambda: build_ui(data)) root.after(0, lambda func=build_ui: func(data))
# CTk # CTk
root = ctk.CTk() root = ctk.CTk()
@@ -515,7 +399,7 @@ def main(url_schema, cookie_file: str):
profile_label.configure(text=f"Current Profile: {get_active_profile_name()}") profile_label.configure(text=f"Current Profile: {get_active_profile_name()}")
def update_download_status(message: str): def update_download_status(message: str):
root.after(0, lambda: download_status_label.configure(text=message)) root.after(0, lambda label=download_status_label: label.configure(text=message))
def reload_playlists(current_url_schema=None, current_cookie_file=None): def reload_playlists(current_url_schema=None, current_cookie_file=None):
for widget in playlist_frame.winfo_children(): for widget in playlist_frame.winfo_children():
@@ -693,7 +577,7 @@ def main(url_schema, cookie_file: str):
f"Failed: {failed_count}" f"Failed: {failed_count}"
) )
update_download_status(finished_message) update_download_status(finished_message)
root.after(0, lambda: batch_download_btn.configure(state="normal")) root.after(0, lambda btn=batch_download_btn: btn.configure(state="normal"))
threading.Thread(target=worker, daemon=True).start() threading.Thread(target=worker, daemon=True).start()
@@ -992,7 +876,7 @@ def main(url_schema, cookie_file: str):
info = await asyncio.to_thread(fetch_video_details, video.url) info = await asyncio.to_thread(fetch_video_details, video.url)
if info is None: if info is None:
root.after(0, lambda: video.duration_label.configure(text="No data")) root.after(0, lambda v=video: v.duration_label.configure(text="No data"))
return return
thumbnails = info.get("thumbnails", []) thumbnails = info.get("thumbnails", [])
@@ -1026,7 +910,7 @@ def main(url_schema, cookie_file: str):
thumbnail = ctk.CTkImage(image, size=(150, 85)) thumbnail = ctk.CTkImage(image, size=(150, 85))
video.apply_thumbnail(thumbnail) video.apply_thumbnail(thumbnail)
root.after(0, apply) root.after(0, lambda func=apply: func())
async def tasker(): async def tasker():
await asyncio.gather( await asyncio.gather(
@@ -1040,10 +924,10 @@ def main(url_schema, cookie_file: str):
playlist_data = fetch_playlist_videos(playlist) playlist_data = fetch_playlist_videos(playlist)
except Exception as e: except Exception as e:
logger.error("Unable to load playlist videos: %s", e) logger.error("Unable to load playlist videos: %s", e)
window.after(0, lambda: window.info_label.configure(text=f"Unable to load playlist: {e}")) window.after(0, lambda w=window: w.info_label.configure(text=f"Unable to load playlist: {e}"))
return return
window.after(0, lambda: render_videos(playlist_data)) window.after(0, lambda func=render_videos: func(playlist_data))
threading.Thread(target=worker, daemon=True).start() threading.Thread(target=worker, daemon=True).start()
window.video_updater = video_info_updater window.video_updater = video_info_updater
+1095
View File
File diff suppressed because it is too large Load Diff
+125
View File
@@ -0,0 +1,125 @@
import base64
import binascii
import json
import urllib
import winreg
from pathlib import Path
from urllib.parse import unquote
import sys
import os
import logging
logger = logging.getLogger("PlaylistSaver.Utils")
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler(sys.stdout))
def set_info(target_logger):
global logger
logger = target_logger
def key_exists(hive, sub_key):
try:
# Attempt to open the key for reading
with winreg.OpenKey(hive, sub_key, 0, winreg.KEY_READ) as key:
return True
except FileNotFoundError:
return False
def create_url_scheme():
try:
scheme = "PlaylistSaver"
base = r"Software\Classes\\" + scheme
key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, base)
winreg.SetValueEx(key, None, 0, winreg.REG_SZ, f"URL:{scheme} Protocol")
winreg.SetValueEx(key, "URL Protocol", 0, winreg.REG_SZ, "")
shell_key = winreg.CreateKey(key, "shell")
open_key = winreg.CreateKey(shell_key, "open")
command_key = winreg.CreateKey(open_key, "command")
if getattr(sys, 'frozen', False):
command = f"\"{sys.executable}\" \"%1\""
else:
command = f"\"{sys.executable}\" \"{os.path.abspath(__file__)}\" \"%1\""
winreg.SetValueEx(command_key, None, 0, winreg.REG_SZ, command)
logger.info("URL scheme registered.")
except OSError as e:
logger.error("Unable to create URL scheme:", e)
def check_url_scheme():
try:
if not key_exists(winreg.HKEY_LOCAL_MACHINE, "PlaylistSaver"):
create_url_scheme()
except FileNotFoundError:
create_url_scheme()
def read_cookies(cookie_path):
try:
with open(cookie_path, "r", encoding='utf-8') as f:
return f.read()
except FileNotFoundError:
logger.error("Cookies file not found.")
except Exception as e:
logger.error("An unexpected error occurred while reading cookies:", e)
def save_cookies(cookies, cookie_path):
try:
os.makedirs(os.path.dirname(cookie_path), exist_ok=True)
with open(cookie_path, "w", encoding='utf-8') as f:
f.write(cookies)
except Exception as e:
logger.error("An unexpected error occurred while saving cookies:", e)
def save_base64_netscape_cookie(cookies_encoded, cookie_path):
try:
cookies = base64.b64decode(cookies_encoded).decode("utf-8")
except binascii.Error:
logger.error("Unable to decode base64 cookie:", 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, work_dir: Path):
logger.debug("Parsing URL scheme:", url)
url = urllib.parse.urlparse(unquote(sys.argv[1]))
parameters = urllib.parse.parse_qs(url.query)
func_map = {
"base64cookies": {
"func": save_base64_netscape_cookie,
"args": [parameters["base64cookies"][0], work_dir]
},
}
for arg_name in parameters:
match_func = func_map.get(arg_name, {}).get("func")
if match_func:
match_func(*func_map[arg_name].get("args", []))
else:
logger.warning("Unknown parameter:", arg_name, " Value:", parameters[arg_name])
def save_json(data: dict, dest):
try:
os.makedirs(os.path.dirname(dest), exist_ok=True)
with open(dest, "w", encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=4)
except Exception as e:
logger.error("Unable to save json:", e)
def read_json(path):
try:
with open(path, "r", encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.error("Unable to read json:", e)