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 binascii
import datetime
import json
import logging
import os
@@ -8,10 +10,12 @@ import subprocess
import sys
import threading
import time
from pathlib import Path
from tkinter import messagebox, simpledialog
from typing import Callable
from PIL import Image, ImageTk
from typing import Callable, List
from yt_dlp.networking.common import Request, _REQUEST_HANDLERS, _RH_PREFERENCES
from PIL import Image
from bs4 import BeautifulSoup
import customtkinter as ctk
import urllib
from urllib.parse import unquote
@@ -332,15 +336,30 @@ def fetch_playlist_videos(playlist: dict):
opts = get_ydl_opts()
opts.update({"extract_flat": True,})
opts.update({"extract_flat": True,
"skip_download": True,
"quiet": True,})
with fetch_playlist_lock:
with YoutubeDL(get_ydl_opts()) as ydl:
with YoutubeDL(opts) as ydl:
data = ydl.extract_info(playlist_url, download=False)
save_playlist_videos(playlist_id, 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):
return (
video.get("webpage_url")
@@ -426,18 +445,23 @@ def background(url_schema, cookie_file: str, callback: Callable):
# messagebox.showerror("Error", "No playlists found.")
with fetch_playlist_lock:
opts = get_ydl_opts()
opts.update({
'extract_flat': True,
'skip_download': True,
'quiet': True,
})
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)
except Exception as e:
print("An error occurred:", e)
pause()
sys.exit(-1)
# print("=== YOUTUBE DATA ===")
# print(json.dumps(data, indent=4))
# print("=== YOUTUBE DATA ===")
save_json(data, get_profile_playlists_cache_path())
callback(data)
@@ -729,9 +753,38 @@ def main(url_schema, cookie_file: str):
daemon=True,
).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):
playlist_title = playlist.get("title", "Playlist not found")
# playlist_id = playlist.get("id", "unknown")
playlist_window = ctk.CTkToplevel(root)
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]
videos = []
for index, video in enumerate(entries, start=1):
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")
video_item.pack(fill="x", padx=5, pady=5)
if video.get("duration"):
duration = str(datetime.timedelta(seconds=video.get("duration")))
else:
duration = "Missing duration data"
ctk.CTkLabel(
video_item,
text=f"{index}. {video_title}",
wraplength=600,
justify="left",
).pack(anchor="w", padx=10, pady=(10, 5))
video_url = resolve_video_page_url(video)
ctk.CTkLabel(
video_item,
text=duration,
text_color="#aaaaaa",
).pack(anchor="w", padx=10, pady=(0, 10))
video_obj = VideoObject(
video.get("id"),
video_url,
video_title,
duration,
index,
video_frame
)
videos.append(video_obj)
# 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():
try:
@@ -831,7 +943,7 @@ def main(url_schema, cookie_file: str):
# Don't apply thumbnail if not available
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)
bind_click_recursive(playlist_item, lambda _event, current_playlist=playlist: open_playlist_window(current_playlist))