Yep just added many things.
This commit is contained in:
@@ -753,25 +753,23 @@ def main(url_schema, cookie_file: str):
|
|||||||
daemon=True,
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
class VideoObject:
|
class VideoObject(ctk.CTkFrame):
|
||||||
def __init__(self, vid_id, url, title, duration, index, master):
|
def __init__(self, vid_id, url, title, duration, index, master, border_width=1, border_color="#666666"):
|
||||||
|
super().__init__(master)
|
||||||
self.id = vid_id
|
self.id = vid_id
|
||||||
self.url = url
|
self.url = url
|
||||||
self.title = title
|
self.title = title
|
||||||
self.duration = duration
|
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.title_label = ctk.CTkLabel(
|
||||||
self.frame,
|
self,
|
||||||
text=f"{index}. {self.title}",
|
text=f"{index}. {self.title}",
|
||||||
wraplength=600,
|
wraplength=600,
|
||||||
justify="left",
|
justify="left",
|
||||||
).pack(anchor="w", padx=10, pady=(10, 5))
|
).pack(anchor="w", padx=10, pady=(10, 5))
|
||||||
|
|
||||||
self.duration_label = ctk.CTkLabel(
|
self.duration_label = ctk.CTkLabel(
|
||||||
self.frame,
|
self,
|
||||||
text=duration,
|
text=duration,
|
||||||
text_color="#aaaaaa",
|
text_color="#aaaaaa",
|
||||||
).pack(anchor="w", padx=10, pady=(0, 10))
|
).pack(anchor="w", padx=10, pady=(0, 10))
|
||||||
@@ -779,35 +777,178 @@ def main(url_schema, cookie_file: str):
|
|||||||
self.thumbnail_label = None
|
self.thumbnail_label = None
|
||||||
|
|
||||||
def apply_thumbnail(self, thumbnail):
|
def apply_thumbnail(self, thumbnail):
|
||||||
self.thumbnail_label = ctk.CTkLabel(self.frame, image=thumbnail, text="")
|
self.thumbnail_label = ctk.CTkLabel(self, image=thumbnail, text="")
|
||||||
self.thumbnail_label.pack(anchor="e", padx=10, pady=(0, 10))
|
self.thumbnail_label.pack(anchor="e", padx=10, pady=(0, 10))
|
||||||
self.thumbnail_label.update()
|
self.thumbnail_label.update()
|
||||||
|
|
||||||
|
max_item_in_one_page = 15
|
||||||
|
|
||||||
|
class PlaylistWindow(ctk.CTkToplevel):
|
||||||
|
def __init__(self, title, master):
|
||||||
|
ctk.CTkToplevel.__init__(self, master)
|
||||||
|
self.title = title
|
||||||
|
self.geometry("700x700")
|
||||||
|
|
||||||
|
self.header_label = ctk.CTkLabel(
|
||||||
|
self,
|
||||||
|
text=title,
|
||||||
|
font=("Arial", 20, "bold")
|
||||||
|
)
|
||||||
|
self.header_label.pack(anchor="w", padx=15, pady=(15, 5))
|
||||||
|
|
||||||
|
self.info_label = ctk.CTkLabel(
|
||||||
|
self,
|
||||||
|
text="Loading playlist videos..."
|
||||||
|
)
|
||||||
|
self.info_label.pack(anchor="w", padx=15, pady=(0, 10))
|
||||||
|
|
||||||
|
# Main Area
|
||||||
|
self.content_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
|
self.content_frame.pack(fill="both", expand=True, padx=15, pady=(0, 15))
|
||||||
|
|
||||||
|
# Video List
|
||||||
|
self.video_frame = ctk.CTkScrollableFrame(self.content_frame)
|
||||||
|
self.video_frame.pack(
|
||||||
|
side="left",
|
||||||
|
fill="both",
|
||||||
|
expand=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Control Panel
|
||||||
|
self.operate_frame = ctk.CTkFrame(
|
||||||
|
self.content_frame,
|
||||||
|
width=180
|
||||||
|
)
|
||||||
|
self.operate_frame.pack(
|
||||||
|
side="right",
|
||||||
|
fill="y",
|
||||||
|
padx=(10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.operate_frame.pack_propagate(False)
|
||||||
|
|
||||||
|
self.current_page_label = ctk.CTkLabel(
|
||||||
|
self.operate_frame,
|
||||||
|
text="Page Unloaded"
|
||||||
|
)
|
||||||
|
self.current_page_label.pack(pady=(15, 5))
|
||||||
|
|
||||||
|
self.current_length_label = ctk.CTkLabel(
|
||||||
|
self.operate_frame,
|
||||||
|
text="Counting..."
|
||||||
|
)
|
||||||
|
self.current_length_label.pack(pady=(0, 15))
|
||||||
|
|
||||||
|
self.go_page_btn_frame = ctk.CTkFrame(
|
||||||
|
self.operate_frame,
|
||||||
|
fg_color="transparent"
|
||||||
|
)
|
||||||
|
self.go_page_btn_frame.pack(side="bottom", pady=15)
|
||||||
|
|
||||||
|
self.previous_page_btn = ctk.CTkButton(
|
||||||
|
self.go_page_btn_frame,
|
||||||
|
text="◀",
|
||||||
|
width=50,
|
||||||
|
height=40,
|
||||||
|
command=self.previous_page
|
||||||
|
)
|
||||||
|
self.previous_page_btn.configure(state="disabled")
|
||||||
|
|
||||||
|
self.next_page_btn = ctk.CTkButton(
|
||||||
|
self.go_page_btn_frame,
|
||||||
|
text="▶",
|
||||||
|
width=50,
|
||||||
|
height=40,
|
||||||
|
command=self.next_page
|
||||||
|
)
|
||||||
|
|
||||||
|
self.previous_page_btn.grid(row=0, column=0, padx=5)
|
||||||
|
self.next_page_btn.grid(row=0, column=1, padx=5)
|
||||||
|
|
||||||
|
self.pages = []
|
||||||
|
self.inited = False
|
||||||
|
self.current_page = 0
|
||||||
|
|
||||||
|
self.video_updater = None
|
||||||
|
|
||||||
|
self.thread_pool = []
|
||||||
|
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self.on_close)
|
||||||
|
|
||||||
|
def load_page(self, page):
|
||||||
|
if len(self.pages) < page+1 or (self.current_page == page and self.inited):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.inited:
|
||||||
|
self.inited = True
|
||||||
|
|
||||||
|
videos: List[VideoObject] = self.pages[page]
|
||||||
|
|
||||||
|
# Remove old items
|
||||||
|
for item in self.video_frame.winfo_children():
|
||||||
|
item.pack_forget()
|
||||||
|
|
||||||
|
for video in videos:
|
||||||
|
video.pack(fill="x", padx=5, pady=5)
|
||||||
|
|
||||||
|
self.current_length_label.configure(text=f"Total: {len(videos)}")
|
||||||
|
self.current_page_label.configure(text=f"Page {self.current_page+1}")
|
||||||
|
|
||||||
|
if self.video_updater:
|
||||||
|
thread = threading.Thread(target=self.video_updater, args=(videos,))
|
||||||
|
self.thread_pool.append(thread)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def handle_page_switcher(self):
|
||||||
|
if self.current_page == 0:
|
||||||
|
self.previous_page_btn.configure(state="disabled")
|
||||||
|
else:
|
||||||
|
self.previous_page_btn.configure(state="normal")
|
||||||
|
|
||||||
|
if self.current_page == len(self.pages)-1:
|
||||||
|
self.next_page_btn.configure(state="disabled")
|
||||||
|
else:
|
||||||
|
self.next_page_btn.configure(state="normal")
|
||||||
|
|
||||||
|
def previous_page(self):
|
||||||
|
if self.current_page-1 > len(self.pages)-1 or self.current_page-1 < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.load_page(self.current_page-1)
|
||||||
|
self.current_page -= 1
|
||||||
|
self.handle_page_switcher()
|
||||||
|
|
||||||
|
def next_page(self):
|
||||||
|
if self.current_page+1 > len(self.pages)-1 or self.current_page+1 < 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.load_page(self.current_page+1)
|
||||||
|
self.current_page += 1
|
||||||
|
self.handle_page_switcher()
|
||||||
|
|
||||||
|
def on_close(self):
|
||||||
|
for thread in self.thread_pool:
|
||||||
|
if thread.is_alive():
|
||||||
|
thread.stop()
|
||||||
|
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
|
||||||
def open_playlist_window(playlist: dict):
|
def open_playlist_window(playlist: dict):
|
||||||
playlist_title = playlist.get("title", "Playlist not found")
|
window = PlaylistWindow(
|
||||||
|
playlist["title"],
|
||||||
playlist_window = ctk.CTkToplevel(root)
|
root
|
||||||
playlist_window.title(playlist_title)
|
)
|
||||||
playlist_window.geometry("700x600")
|
|
||||||
|
|
||||||
header_label = ctk.CTkLabel(playlist_window, text=playlist_title)
|
|
||||||
header_label.pack(anchor="w", padx=15, pady=(15, 10))
|
|
||||||
|
|
||||||
info_label = ctk.CTkLabel(playlist_window, text="Loading playlist videos...")
|
|
||||||
info_label.pack(anchor="w", padx=15, pady=(0, 10))
|
|
||||||
|
|
||||||
video_frame = ctk.CTkScrollableFrame(playlist_window)
|
|
||||||
video_frame.pack(fill="both", expand=True, padx=15, pady=(0, 15))
|
|
||||||
|
|
||||||
def render_videos(playlist_data):
|
def render_videos(playlist_data):
|
||||||
for widget in video_frame.winfo_children():
|
for widget in window.video_frame.winfo_children():
|
||||||
widget.destroy()
|
widget.destroy()
|
||||||
|
|
||||||
entries = playlist_data.get("entries") or []
|
entries = playlist_data.get("entries") or []
|
||||||
info_label.configure(text=f"{len(entries)} videos")
|
window.info_label.configure(text=f"{len(entries)} videos")
|
||||||
|
|
||||||
if not entries:
|
if not entries:
|
||||||
ctk.CTkLabel(video_frame, text="No videos found.").pack(anchor="w", padx=10, pady=10)
|
ctk.CTkLabel(window.video_frame, text="No videos found.").pack(anchor="w", padx=10, pady=10)
|
||||||
return
|
return
|
||||||
|
|
||||||
entries = [entry for entry in entries if entry is not None]
|
entries = [entry for entry in entries if entry is not None]
|
||||||
@@ -830,17 +971,17 @@ def main(url_schema, cookie_file: str):
|
|||||||
video_title,
|
video_title,
|
||||||
duration,
|
duration,
|
||||||
index,
|
index,
|
||||||
video_frame
|
window.video_frame
|
||||||
)
|
)
|
||||||
|
|
||||||
videos.append(video_obj)
|
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_obj.frame, lambda _event, current_video=video: open_video_player(current_video))
|
bind_click_recursive(video_obj, lambda _event, current_video=video: open_video_player(current_video))
|
||||||
|
|
||||||
threading.Thread(target=video_info_updater, args=(videos,), daemon=True).start()
|
window.pages = [videos[i:i + max_item_in_one_page] for i in range(0, len(videos), max_item_in_one_page)]
|
||||||
|
window.load_page(0)
|
||||||
|
|
||||||
sem = asyncio.Semaphore(5)
|
|
||||||
|
|
||||||
# Update video detail to frame after playlist is loaded (make playlist load faster)
|
# Update video detail to frame after playlist is loaded (make playlist load faster)
|
||||||
def video_info_updater(videos: list[VideoObject]):
|
def video_info_updater(videos: list[VideoObject]):
|
||||||
@@ -851,7 +992,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.configure(text="No data"))
|
root.after(0, lambda: video.duration_label.configure(text="No data"))
|
||||||
return
|
return
|
||||||
|
|
||||||
thumbnails = info.get("thumbnails", [])
|
thumbnails = info.get("thumbnails", [])
|
||||||
@@ -882,7 +1023,7 @@ def main(url_schema, cookie_file: str):
|
|||||||
if thumbnail_path.exists():
|
if thumbnail_path.exists():
|
||||||
def apply():
|
def apply():
|
||||||
image = Image.open(thumbnail_path.as_posix())
|
image = Image.open(thumbnail_path.as_posix())
|
||||||
thumbnail = ctk.CTkImage(image, size=(60, 30))
|
thumbnail = ctk.CTkImage(image, size=(150, 85))
|
||||||
video.apply_thumbnail(thumbnail)
|
video.apply_thumbnail(thumbnail)
|
||||||
|
|
||||||
root.after(0, apply)
|
root.after(0, apply)
|
||||||
@@ -899,12 +1040,13 @@ 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)
|
||||||
playlist_window.after(0, lambda: info_label.configure(text=f"Unable to load playlist: {e}"))
|
window.after(0, lambda: window.info_label.configure(text=f"Unable to load playlist: {e}"))
|
||||||
return
|
return
|
||||||
|
|
||||||
playlist_window.after(0, lambda: render_videos(playlist_data))
|
window.after(0, lambda: render_videos(playlist_data))
|
||||||
|
|
||||||
threading.Thread(target=worker, daemon=True).start()
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
|
window.video_updater = video_info_updater
|
||||||
|
|
||||||
# Load playlist when background thread finished playlists data fetch process
|
# Load playlist when background thread finished playlists data fetch process
|
||||||
def build_ui(playlists_data):
|
def build_ui(playlists_data):
|
||||||
|
|||||||
Reference in New Issue
Block a user