From 48504247dca89a90ce8fb9f48741d9a5bc1bfa44 Mon Sep 17 00:00:00 2001 From: Peifan Li Date: Sun, 23 Nov 2025 21:00:06 -0500 Subject: [PATCH] refactor: Improve comments section toggling logic --- backend/src/services/downloadService.ts | 17 +++- backend/src/utils/helpers.ts | 17 +--- frontend/src/pages/VideoPlayer.tsx | 120 ++++++++++++++---------- 3 files changed, 83 insertions(+), 71 deletions(-) diff --git a/backend/src/services/downloadService.ts b/backend/src/services/downloadService.ts index f79d678..b8e8dbd 100644 --- a/backend/src/services/downloadService.ts +++ b/backend/src/services/downloadService.ts @@ -8,8 +8,8 @@ import * as cheerio from "cheerio"; import puppeteer from "puppeteer"; import { IMAGES_DIR, VIDEOS_DIR } from "../config/paths"; import { - extractBilibiliVideoId, - sanitizeFilename + extractBilibiliVideoId, + sanitizeFilename } from "../utils/helpers"; import * as storageService from "./storageService"; import { Collection, Video } from "./storageService"; @@ -805,10 +805,19 @@ export async function downloadYouTubeVideo(videoUrl: string, downloadId?: string } // Use exec to capture stdout for progress + // Format selection prioritizes Safari-compatible codecs (H.264/AAC) + // avc1 is the H.264 variant that Safari supports best + // Use Android client to avoid SABR streaming issues and JS runtime requirements const subprocess = youtubedl.exec(videoUrl, { output: newVideoPath, - format: "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best", - }); + format: "bestvideo[ext=mp4][vcodec^=avc1]+bestaudio[ext=m4a][acodec=aac]/bestvideo[ext=mp4][vcodec=h264]+bestaudio[ext=m4a]/best[ext=mp4]/best", + mergeOutputFormat: "mp4", + 'extractor-args': "youtube:player_client=android", + addHeader: [ + 'Referer:https://www.youtube.com/', + 'User-Agent:Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36' + ] + } as any); subprocess.stdout?.on('data', (data: Buffer) => { const output = data.toString(); diff --git a/backend/src/utils/helpers.ts b/backend/src/utils/helpers.ts index d405341..e8fe7b4 100644 --- a/backend/src/utils/helpers.ts +++ b/backend/src/utils/helpers.ts @@ -106,24 +106,9 @@ export function sanitizeFilename(filename: string): string { // Remove hashtags (e.g. #tag) const withoutHashtags = filename.replace(/#\S+/g, "").trim(); - // Replace full-width punctuation with standard equivalents or underscores - const sanitized = withoutHashtags - .replace(/?/g, "_") - .replace(/!/g, "_") - .replace(/:/g, "_") - .replace(/“/g, "_") - .replace(/”/g, "_") - .replace(/,/g, "_") - .replace(/。/g, "_") - .replace(/、/g, "_") - .replace(/【/g, "_") - .replace(/】/g, "_") - .replace(/《/g, "_") - .replace(/》/g, "_"); - // Replace only unsafe characters for filesystems // This preserves non-Latin characters like Chinese, Japanese, Korean, etc. - return sanitized + return withoutHashtags .replace(/[\/\\:*?"<>|]/g, "_") // Replace unsafe filesystem characters .replace(/\s+/g, "_"); // Replace spaces with underscores } diff --git a/frontend/src/pages/VideoPlayer.tsx b/frontend/src/pages/VideoPlayer.tsx index 306138a..66b5df9 100644 --- a/frontend/src/pages/VideoPlayer.tsx +++ b/frontend/src/pages/VideoPlayer.tsx @@ -89,6 +89,8 @@ const VideoPlayer: React.FC = ({ const [videoCollections, setVideoCollections] = useState([]); const [comments, setComments] = useState([]); const [loadingComments, setLoadingComments] = useState(false); + const [showComments, setShowComments] = useState(false); + const [commentsLoaded, setCommentsLoaded] = useState(false); const videoRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); @@ -219,25 +221,28 @@ const VideoPlayer: React.FC = ({ fetchSettings(); }, [id]); // Re-run when video changes - // Fetch comments - useEffect(() => { - const fetchComments = async () => { - if (!id) return; + const fetchComments = async () => { + if (!id) return; - setLoadingComments(true); - try { - const response = await axios.get(`${API_URL}/videos/${id}/comments`); - setComments(response.data); - } catch (err) { - console.error('Error fetching comments:', err); - // We don't set a global error here as comments are secondary - } finally { - setLoadingComments(false); - } - }; + setLoadingComments(true); + try { + const response = await axios.get(`${API_URL}/videos/${id}/comments`); + setComments(response.data); + setCommentsLoaded(true); + } catch (err) { + console.error('Error fetching comments:', err); + // We don't set a global error here as comments are secondary + } finally { + setLoadingComments(false); + } + }; - fetchComments(); - }, [id]); + const handleToggleComments = () => { + if (!showComments && !commentsLoaded) { + fetchComments(); + } + setShowComments(!showComments); + }; // Find collections that contain this video useEffect(() => { @@ -614,41 +619,54 @@ const VideoPlayer: React.FC = ({ {/* Comments Section */} - - {t('latestComments')} - - - {loadingComments ? ( - - - - ) : comments.length > 0 ? ( - - {comments.map((comment) => ( - - - {comment.author.charAt(0).toUpperCase()} - - - - - {comment.author} - - - {comment.date} - - - - {comment.content} - - - - ))} - - ) : ( - - {t('noComments')} + + + {t('latestComments')} + + + + {showComments && ( + <> + {loadingComments ? ( + + + + ) : comments.length > 0 ? ( + + {comments.map((comment) => ( + + + {comment.author.charAt(0).toUpperCase()} + + + + + {comment.author} + + + {comment.date} + + + + {comment.content} + + + + ))} + + ) : ( + + {t('noComments')} + + )} + )}