refactor: Improve comments section toggling logic

This commit is contained in:
Peifan Li
2025-11-23 21:00:06 -05:00
parent 733e577db4
commit 48504247dc
3 changed files with 83 additions and 71 deletions

View File

@@ -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();

View File

@@ -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
}

View File

@@ -89,6 +89,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [videoCollections, setVideoCollections] = useState<Collection[]>([]);
const [comments, setComments] = useState<Comment[]>([]);
const [loadingComments, setLoadingComments] = useState<boolean>(false);
const [showComments, setShowComments] = useState<boolean>(false);
const [commentsLoaded, setCommentsLoaded] = useState<boolean>(false);
const videoRef = useRef<HTMLVideoElement>(null);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
@@ -219,25 +221,28 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
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<VideoPlayerProps> = ({
{/* Comments Section */}
<Box sx={{ mt: 4 }}>
<Typography variant="h6" gutterBottom fontWeight="bold">
{t('latestComments')}
</Typography>
{loadingComments ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
<CircularProgress size={24} />
</Box>
) : comments.length > 0 ? (
<Stack spacing={2}>
{comments.map((comment) => (
<Box key={comment.id} sx={{ display: 'flex', gap: 2 }}>
<Avatar src={comment.avatar} alt={comment.author}>
{comment.author.charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="subtitle2" fontWeight="bold">
{comment.author}
</Typography>
<Typography variant="caption" color="text.secondary">
{comment.date}
</Typography>
</Box>
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
{comment.content}
</Typography>
</Box>
</Box>
))}
</Stack>
) : (
<Typography variant="body2" color="text.secondary">
{t('noComments')}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" fontWeight="bold">
{t('latestComments')}
</Typography>
<Button
variant="outlined"
onClick={handleToggleComments}
size="small"
>
{showComments ? "Hide Comments" : "Show Comments"}
</Button>
</Box>
{showComments && (
<>
{loadingComments ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 2 }}>
<CircularProgress size={24} />
</Box>
) : comments.length > 0 ? (
<Stack spacing={2}>
{comments.map((comment) => (
<Box key={comment.id} sx={{ display: 'flex', gap: 2 }}>
<Avatar src={comment.avatar} alt={comment.author}>
{comment.author.charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="subtitle2" fontWeight="bold">
{comment.author}
</Typography>
<Typography variant="caption" color="text.secondary">
{comment.date}
</Typography>
</Box>
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
{comment.content}
</Typography>
</Box>
</Box>
))}
</Stack>
) : (
<Typography variant="body2" color="text.secondary">
{t('noComments')}
</Typography>
)}
</>
)}
</Box>
</Box>