Optimize playlist thumbnail loading speed.

This commit is contained in:
2026-06-15 00:16:44 +08:00
parent cb335d64df
commit d92f1b0bb8
+139 -27
View File
@@ -1,5 +1,7 @@
import asyncio
import base64 import base64
import binascii import binascii
import datetime
import json import json
import logging import logging
import os import os
@@ -8,10 +10,12 @@ import subprocess
import sys import sys
import threading import threading
import time import time
from pathlib import Path
from tkinter import messagebox, simpledialog from tkinter import messagebox, simpledialog
from typing import Callable from typing import Callable, List
from yt_dlp.networking.common import Request, _REQUEST_HANDLERS, _RH_PREFERENCES
from PIL import Image, ImageTk from PIL import Image
from bs4 import BeautifulSoup
import customtkinter as ctk import customtkinter as ctk
import urllib import urllib
from urllib.parse import unquote from urllib.parse import unquote
@@ -332,15 +336,30 @@ def fetch_playlist_videos(playlist: dict):
opts = get_ydl_opts() opts = get_ydl_opts()
opts.update({"extract_flat": True,}) opts.update({"extract_flat": True,
"skip_download": True,
"quiet": True,})
with fetch_playlist_lock: with fetch_playlist_lock:
with YoutubeDL(get_ydl_opts()) as ydl: with YoutubeDL(opts) as ydl:
data = ydl.extract_info(playlist_url, download=False) data = ydl.extract_info(playlist_url, download=False)
save_playlist_videos(playlist_id, data) save_playlist_videos(playlist_id, data)
return data return data
def fetch_video_details(video_url: str):
opts = get_ydl_opts()
opts.update({"extract_flat": True,})
try:
with YoutubeDL(opts) as ydl:
info = ydl.extract_info(video_url, download=False)
except Exception as e:
logger.error(f"Unable to fetch video details: {e}\nURL:{video_url}")
return None
return info
def resolve_video_page_url(video: dict): def resolve_video_page_url(video: dict):
return ( return (
video.get("webpage_url") video.get("webpage_url")
@@ -426,18 +445,23 @@ def background(url_schema, cookie_file: str, callback: Callable):
# messagebox.showerror("Error", "No playlists found.") # messagebox.showerror("Error", "No playlists found.")
with fetch_playlist_lock: with fetch_playlist_lock:
opts = get_ydl_opts()
opts.update({
'extract_flat': True,
'skip_download': True,
'quiet': True,
})
try: try:
with YoutubeDL(get_ydl_opts()) as ydl: with YoutubeDL(opts) as ydl:
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) print("An error occurred:", e)
pause() pause()
sys.exit(-1) sys.exit(-1)
# print("=== YOUTUBE DATA ===")
# print(json.dumps(data, indent=4))
# print("=== YOUTUBE DATA ===")
save_json(data, get_profile_playlists_cache_path()) save_json(data, get_profile_playlists_cache_path())
callback(data) callback(data)
@@ -729,9 +753,38 @@ def main(url_schema, cookie_file: str):
daemon=True, daemon=True,
).start() ).start()
class VideoObject:
def __init__(self, vid_id, url, title, duration, index, master):
self.id = vid_id
self.url = url
self.title = title
self.duration = duration
self.frame = ctk.CTkFrame(master, border_width=1, border_color="#666666")
self.frame.pack(fill="x", padx=5, pady=5)
self.title_label = ctk.CTkLabel(
self.frame,
text=f"{index}. {self.title}",
wraplength=600,
justify="left",
).pack(anchor="w", padx=10, pady=(10, 5))
self.duration_label = ctk.CTkLabel(
self.frame,
text=duration,
text_color="#aaaaaa",
).pack(anchor="w", padx=10, pady=(0, 10))
self.thumbnail_label = None
def apply_thumbnail(self, thumbnail):
self.thumbnail_label = ctk.CTkLabel(self.frame, image=thumbnail, text="")
self.thumbnail_label.pack(anchor="e", padx=10, pady=(0, 10))
self.thumbnail_label.update()
def open_playlist_window(playlist: dict): def open_playlist_window(playlist: dict):
playlist_title = playlist.get("title", "Playlist not found") playlist_title = playlist.get("title", "Playlist not found")
# playlist_id = playlist.get("id", "unknown")
playlist_window = ctk.CTkToplevel(root) playlist_window = ctk.CTkToplevel(root)
playlist_window.title(playlist_title) playlist_window.title(playlist_title)
@@ -759,28 +812,87 @@ def main(url_schema, cookie_file: str):
entries = [entry for entry in entries if entry is not None] entries = [entry for entry in entries if entry is not None]
videos = []
for index, video in enumerate(entries, start=1): for index, video in enumerate(entries, start=1):
video_title = video.get("title", "Video not found") video_title = video.get("title", "Video not found")
duration = video.get("duration_string") or "Unknown duration"
video_item = ctk.CTkFrame(video_frame, border_width=1, border_color="#666666") if video.get("duration"):
video_item.pack(fill="x", padx=5, pady=5) duration = str(datetime.timedelta(seconds=video.get("duration")))
else:
duration = "Missing duration data"
ctk.CTkLabel( video_url = resolve_video_page_url(video)
video_item,
text=f"{index}. {video_title}",
wraplength=600,
justify="left",
).pack(anchor="w", padx=10, pady=(10, 5))
ctk.CTkLabel( video_obj = VideoObject(
video_item, video.get("id"),
text=duration, video_url,
text_color="#aaaaaa", video_title,
).pack(anchor="w", padx=10, pady=(0, 10)) duration,
index,
video_frame
)
videos.append(video_obj)
# Open video player (mpv) when video_frame is clicked # Open video player (mpv) when video_frame is clicked
bind_click_recursive(video_item, lambda _event, current_video=video: open_video_player(current_video)) bind_click_recursive(video_obj.frame, lambda _event, current_video=video: open_video_player(current_video))
threading.Thread(target=video_info_updater, args=(videos,), daemon=True).start()
sem = asyncio.Semaphore(5)
# Update video detail to frame after playlist is loaded (make playlist load faster)
def video_info_updater(videos: list[VideoObject]):
sem = asyncio.Semaphore(5)
async def inner(video):
async with sem:
info = await asyncio.to_thread(fetch_video_details, video.url)
if info is None:
root.after(0, lambda: video.duration.configure(text="No data"))
return
thumbnails = info.get("thumbnails", [])
if not thumbnails:
return
thumbnail_url = thumbnails[0].get("url")
thumbnail_resolution = thumbnails[0].get("resolution", "unknown")
if not thumbnail_url:
return
thumbnail_path = Path(
ROOT_DIR,
"temp",
"thumbnails",
f"thumbnail_{video.id}_{thumbnail_resolution}.jpg"
)
if not thumbnail_path.exists():
await asyncio.to_thread(
download_file,
thumbnail_url,
thumbnail_path.as_posix()
)
if thumbnail_path.exists():
def apply():
image = Image.open(thumbnail_path.as_posix())
thumbnail = ctk.CTkImage(image, size=(60, 30))
video.apply_thumbnail(thumbnail)
root.after(0, apply)
async def tasker():
await asyncio.gather(
*(inner(video) for video in videos)
)
asyncio.run(tasker())
def worker(): def worker():
try: try:
@@ -831,7 +943,7 @@ def main(url_schema, cookie_file: str):
# Don't apply thumbnail if not available # Don't apply thumbnail if not available
if thumbnail: if thumbnail:
thumbnail_label = ctk.CTkLabel(playlist_item, image=thumbnail) thumbnail_label = ctk.CTkLabel(playlist_item, image=thumbnail, text="")
thumbnail_label.pack(anchor="e", padx=5, pady=5) thumbnail_label.pack(anchor="e", padx=5, pady=5)
bind_click_recursive(playlist_item, lambda _event, current_playlist=playlist: open_playlist_window(current_playlist)) bind_click_recursive(playlist_item, lambda _event, current_playlist=playlist: open_playlist_window(current_playlist))