refactor: Improve comments section toggling logic
This commit is contained in:
@@ -8,8 +8,8 @@ import * as cheerio from "cheerio";
|
|||||||
import puppeteer from "puppeteer";
|
import puppeteer from "puppeteer";
|
||||||
import { IMAGES_DIR, VIDEOS_DIR } from "../config/paths";
|
import { IMAGES_DIR, VIDEOS_DIR } from "../config/paths";
|
||||||
import {
|
import {
|
||||||
extractBilibiliVideoId,
|
extractBilibiliVideoId,
|
||||||
sanitizeFilename
|
sanitizeFilename
|
||||||
} from "../utils/helpers";
|
} from "../utils/helpers";
|
||||||
import * as storageService from "./storageService";
|
import * as storageService from "./storageService";
|
||||||
import { Collection, Video } 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
|
// 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, {
|
const subprocess = youtubedl.exec(videoUrl, {
|
||||||
output: newVideoPath,
|
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) => {
|
subprocess.stdout?.on('data', (data: Buffer) => {
|
||||||
const output = data.toString();
|
const output = data.toString();
|
||||||
|
|||||||
@@ -106,24 +106,9 @@ export function sanitizeFilename(filename: string): string {
|
|||||||
// Remove hashtags (e.g. #tag)
|
// Remove hashtags (e.g. #tag)
|
||||||
const withoutHashtags = filename.replace(/#\S+/g, "").trim();
|
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
|
// Replace only unsafe characters for filesystems
|
||||||
// This preserves non-Latin characters like Chinese, Japanese, Korean, etc.
|
// This preserves non-Latin characters like Chinese, Japanese, Korean, etc.
|
||||||
return sanitized
|
return withoutHashtags
|
||||||
.replace(/[\/\\:*?"<>|]/g, "_") // Replace unsafe filesystem characters
|
.replace(/[\/\\:*?"<>|]/g, "_") // Replace unsafe filesystem characters
|
||||||
.replace(/\s+/g, "_"); // Replace spaces with underscores
|
.replace(/\s+/g, "_"); // Replace spaces with underscores
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
const [videoCollections, setVideoCollections] = useState<Collection[]>([]);
|
const [videoCollections, setVideoCollections] = useState<Collection[]>([]);
|
||||||
const [comments, setComments] = useState<Comment[]>([]);
|
const [comments, setComments] = useState<Comment[]>([]);
|
||||||
const [loadingComments, setLoadingComments] = useState<boolean>(false);
|
const [loadingComments, setLoadingComments] = useState<boolean>(false);
|
||||||
|
const [showComments, setShowComments] = useState<boolean>(false);
|
||||||
|
const [commentsLoaded, setCommentsLoaded] = useState<boolean>(false);
|
||||||
|
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
@@ -219,25 +221,28 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
fetchSettings();
|
fetchSettings();
|
||||||
}, [id]); // Re-run when video changes
|
}, [id]); // Re-run when video changes
|
||||||
|
|
||||||
// Fetch comments
|
const fetchComments = async () => {
|
||||||
useEffect(() => {
|
if (!id) return;
|
||||||
const fetchComments = async () => {
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
setLoadingComments(true);
|
setLoadingComments(true);
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${API_URL}/videos/${id}/comments`);
|
const response = await axios.get(`${API_URL}/videos/${id}/comments`);
|
||||||
setComments(response.data);
|
setComments(response.data);
|
||||||
} catch (err) {
|
setCommentsLoaded(true);
|
||||||
console.error('Error fetching comments:', err);
|
} catch (err) {
|
||||||
// We don't set a global error here as comments are secondary
|
console.error('Error fetching comments:', err);
|
||||||
} finally {
|
// We don't set a global error here as comments are secondary
|
||||||
setLoadingComments(false);
|
} finally {
|
||||||
}
|
setLoadingComments(false);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fetchComments();
|
const handleToggleComments = () => {
|
||||||
}, [id]);
|
if (!showComments && !commentsLoaded) {
|
||||||
|
fetchComments();
|
||||||
|
}
|
||||||
|
setShowComments(!showComments);
|
||||||
|
};
|
||||||
|
|
||||||
// Find collections that contain this video
|
// Find collections that contain this video
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -614,41 +619,54 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
|
|
||||||
{/* Comments Section */}
|
{/* Comments Section */}
|
||||||
<Box sx={{ mt: 4 }}>
|
<Box sx={{ mt: 4 }}>
|
||||||
<Typography variant="h6" gutterBottom fontWeight="bold">
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
{t('latestComments')}
|
<Typography variant="h6" fontWeight="bold">
|
||||||
</Typography>
|
{t('latestComments')}
|
||||||
|
|
||||||
{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>
|
</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>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user