feat: Initialize status.json for tracking download status
This commit is contained in:
5
backend/data/status.json
Normal file
5
backend/data/status.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"isDownloading": false,
|
||||
"title": "",
|
||||
"timestamp": 1741832171094
|
||||
}
|
||||
@@ -35,6 +35,31 @@ fs.ensureDirSync(dataDir);
|
||||
|
||||
// Define path for videos.json in the data directory for persistence
|
||||
const videosDataPath = path.join(dataDir, "videos.json");
|
||||
// Define path for status.json to track download status
|
||||
const statusDataPath = path.join(dataDir, "status.json");
|
||||
|
||||
// Initialize status.json if it doesn't exist
|
||||
if (!fs.existsSync(statusDataPath)) {
|
||||
fs.writeFileSync(
|
||||
statusDataPath,
|
||||
JSON.stringify({ isDownloading: false, title: "" }, null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to update download status
|
||||
function updateDownloadStatus(isDownloading, title = "") {
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
statusDataPath,
|
||||
JSON.stringify({ isDownloading, title, timestamp: Date.now() }, null, 2)
|
||||
);
|
||||
console.log(
|
||||
`Download status updated: isDownloading=${isDownloading}, title=${title}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error updating download status:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Serve static files from the uploads directory
|
||||
app.use("/videos", express.static(videosDir));
|
||||
@@ -361,6 +386,18 @@ app.post("/api/download", async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Set download status to true with initial title
|
||||
let initialTitle = "Downloading video...";
|
||||
if (videoUrl.includes("youtube.com") || videoUrl.includes("youtu.be")) {
|
||||
initialTitle = "Downloading YouTube video...";
|
||||
} else if (
|
||||
videoUrl.includes("bilibili.com") ||
|
||||
videoUrl.includes("b23.tv")
|
||||
) {
|
||||
initialTitle = "Downloading Bilibili video...";
|
||||
}
|
||||
updateDownloadStatus(true, initialTitle);
|
||||
|
||||
// Resolve shortened URLs (like b23.tv)
|
||||
if (videoUrl.includes("b23.tv")) {
|
||||
videoUrl = await resolveShortUrl(videoUrl);
|
||||
@@ -392,6 +429,7 @@ app.post("/api/download", async (req, res) => {
|
||||
// Check if it's a Bilibili URL
|
||||
if (isBilibiliUrl(videoUrl)) {
|
||||
console.log("Detected Bilibili URL");
|
||||
updateDownloadStatus(true, "Downloading Bilibili video...");
|
||||
|
||||
try {
|
||||
// Download Bilibili video
|
||||
@@ -415,6 +453,9 @@ app.post("/api/download", async (req, res) => {
|
||||
thumbnailUrl = bilibiliInfo.thumbnailUrl;
|
||||
thumbnailSaved = bilibiliInfo.thumbnailSaved;
|
||||
|
||||
// Update download status with actual title
|
||||
updateDownloadStatus(true, `Downloading: ${videoTitle}`);
|
||||
|
||||
// Update the safe base filename with the actual title
|
||||
const newSafeBaseFilename = `${sanitizeFilename(
|
||||
videoTitle
|
||||
@@ -441,6 +482,8 @@ app.post("/api/download", async (req, res) => {
|
||||
}
|
||||
} catch (bilibiliError) {
|
||||
console.error("Error in Bilibili download process:", bilibiliError);
|
||||
// Set download status to false on error
|
||||
updateDownloadStatus(false);
|
||||
return res.status(500).json({
|
||||
error: "Failed to download Bilibili video",
|
||||
details: bilibiliError.message,
|
||||
@@ -448,6 +491,7 @@ app.post("/api/download", async (req, res) => {
|
||||
}
|
||||
} else {
|
||||
console.log("Detected YouTube URL");
|
||||
updateDownloadStatus(true, "Downloading YouTube video...");
|
||||
|
||||
try {
|
||||
// Get YouTube video info first
|
||||
@@ -472,6 +516,9 @@ app.post("/api/download", async (req, res) => {
|
||||
new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
||||
thumbnailUrl = info.thumbnail;
|
||||
|
||||
// Update download status with actual title
|
||||
updateDownloadStatus(true, `Downloading: ${videoTitle}`);
|
||||
|
||||
// Update the safe base filename with the actual title
|
||||
const newSafeBaseFilename = `${sanitizeFilename(
|
||||
videoTitle
|
||||
@@ -530,6 +577,8 @@ app.post("/api/download", async (req, res) => {
|
||||
}
|
||||
} catch (youtubeError) {
|
||||
console.error("Error in YouTube download process:", youtubeError);
|
||||
// Set download status to false on error
|
||||
updateDownloadStatus(false);
|
||||
return res.status(500).json({
|
||||
error: "Failed to download YouTube video",
|
||||
details: youtubeError.message,
|
||||
@@ -572,9 +621,14 @@ app.post("/api/download", async (req, res) => {
|
||||
|
||||
console.log("Video added to database");
|
||||
|
||||
// Set download status to false when complete
|
||||
updateDownloadStatus(false);
|
||||
|
||||
res.status(200).json({ success: true, video: videoData });
|
||||
} catch (error) {
|
||||
console.error("Error downloading video:", error);
|
||||
// Set download status to false on error
|
||||
updateDownloadStatus(false);
|
||||
res
|
||||
.status(500)
|
||||
.json({ error: "Failed to download video", details: error.message });
|
||||
@@ -838,6 +892,31 @@ app.delete("/api/collections/:id", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// API endpoint to get download status
|
||||
app.get("/api/download-status", (req, res) => {
|
||||
try {
|
||||
if (!fs.existsSync(statusDataPath)) {
|
||||
updateDownloadStatus(false);
|
||||
return res.status(200).json({ isDownloading: false, title: "" });
|
||||
}
|
||||
|
||||
const status = JSON.parse(fs.readFileSync(statusDataPath, "utf8"));
|
||||
|
||||
// Check if the status is stale (older than 5 minutes)
|
||||
const now = Date.now();
|
||||
if (status.timestamp && now - status.timestamp > 5 * 60 * 1000) {
|
||||
console.log("Download status is stale, resetting to false");
|
||||
updateDownloadStatus(false);
|
||||
return res.status(200).json({ isDownloading: false, title: "" });
|
||||
}
|
||||
|
||||
res.status(200).json(status);
|
||||
} catch (error) {
|
||||
console.error("Error fetching download status:", error);
|
||||
res.status(500).json({ error: "Failed to fetch download status" });
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
|
||||
@@ -66,6 +66,48 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// Add a function to check download status from the backend
|
||||
const checkBackendDownloadStatus = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/download-status`);
|
||||
|
||||
if (response.data.isDownloading) {
|
||||
// If backend is downloading, update the local status
|
||||
setDownloadingTitle(response.data.title || 'Downloading...');
|
||||
|
||||
// Save to localStorage for persistence
|
||||
const statusData = {
|
||||
title: response.data.title || 'Downloading...',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
localStorage.setItem(DOWNLOAD_STATUS_KEY, JSON.stringify(statusData));
|
||||
} else {
|
||||
// If backend says download is not in progress, clear the status
|
||||
// This ensures the header returns to normal after download completes
|
||||
if (downloadingTitle) {
|
||||
console.log('Backend says download is complete, clearing status');
|
||||
localStorage.removeItem(DOWNLOAD_STATUS_KEY);
|
||||
setDownloadingTitle('');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking backend download status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Check backend download status periodically
|
||||
useEffect(() => {
|
||||
// Check immediately on mount
|
||||
checkBackendDownloadStatus();
|
||||
|
||||
// Then check every 5 seconds
|
||||
const statusCheckInterval = setInterval(checkBackendDownloadStatus, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(statusCheckInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch collections on component mount
|
||||
useEffect(() => {
|
||||
fetchCollections();
|
||||
@@ -74,30 +116,8 @@ function App() {
|
||||
// Fetch videos on component mount
|
||||
useEffect(() => {
|
||||
fetchVideos();
|
||||
|
||||
// Also check for stale download status
|
||||
if (downloadingTitle) {
|
||||
const checkDownloadStatus = async () => {
|
||||
try {
|
||||
// Make a simple API call to check if the server is still processing the download
|
||||
await axios.get(`${API_URL}/videos`);
|
||||
|
||||
// If we've been downloading for more than 3 minutes, assume it's done or failed
|
||||
const status = getStoredDownloadStatus();
|
||||
if (status && status.timestamp && Date.now() - status.timestamp > 3 * 60 * 1000) {
|
||||
console.log('Download has been running too long, clearing status');
|
||||
localStorage.removeItem(DOWNLOAD_STATUS_KEY);
|
||||
setDownloadingTitle('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking download status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
checkDownloadStatus();
|
||||
}
|
||||
}, [downloadingTitle]);
|
||||
|
||||
}, []);
|
||||
|
||||
// Set up localStorage and event listeners
|
||||
useEffect(() => {
|
||||
console.log('Setting up localStorage and event listeners');
|
||||
@@ -192,10 +212,19 @@ function App() {
|
||||
const response = await axios.post(`${API_URL}/download`, { youtubeUrl: videoUrl });
|
||||
setVideos(prevVideos => [response.data.video, ...prevVideos]);
|
||||
setIsSearchMode(false);
|
||||
|
||||
// Explicitly clear download status after successful download
|
||||
setDownloadingTitle('');
|
||||
localStorage.removeItem(DOWNLOAD_STATUS_KEY);
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error downloading video:', err);
|
||||
|
||||
// Always clear download status on error
|
||||
setDownloadingTitle('');
|
||||
localStorage.removeItem(DOWNLOAD_STATUS_KEY);
|
||||
|
||||
// Check if the error is because the input is a search term
|
||||
if (err.response?.data?.isSearchTerm) {
|
||||
// Handle as search term
|
||||
@@ -208,7 +237,6 @@ function App() {
|
||||
};
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setDownloadingTitle('');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -329,18 +357,35 @@ function App() {
|
||||
};
|
||||
|
||||
const handleDownloadFromSearch = async (videoUrl, title) => {
|
||||
// Abort any ongoing search request
|
||||
if (searchAbortController.current) {
|
||||
searchAbortController.current.abort();
|
||||
searchAbortController.current = null;
|
||||
try {
|
||||
// Abort any ongoing search request
|
||||
if (searchAbortController.current) {
|
||||
searchAbortController.current.abort();
|
||||
searchAbortController.current = null;
|
||||
}
|
||||
|
||||
setIsSearchMode(false);
|
||||
// If title is provided, use it for the downloading message
|
||||
if (title) {
|
||||
setDownloadingTitle(title);
|
||||
}
|
||||
|
||||
const result = await handleVideoSubmit(videoUrl);
|
||||
|
||||
// Ensure download status is cleared after download completes
|
||||
if (!result.success) {
|
||||
setDownloadingTitle('');
|
||||
localStorage.removeItem(DOWNLOAD_STATUS_KEY);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error in handleDownloadFromSearch:', error);
|
||||
// Always clear download status on error
|
||||
setDownloadingTitle('');
|
||||
localStorage.removeItem(DOWNLOAD_STATUS_KEY);
|
||||
return { success: false, error: 'Failed to download video' };
|
||||
}
|
||||
|
||||
setIsSearchMode(false);
|
||||
// If title is provided, use it for the downloading message
|
||||
if (title) {
|
||||
setDownloadingTitle(title);
|
||||
}
|
||||
return await handleVideoSubmit(videoUrl);
|
||||
};
|
||||
|
||||
// For debugging
|
||||
@@ -404,43 +449,41 @@ function App() {
|
||||
// This is handled on the server side now
|
||||
|
||||
// Add the video to the selected collection
|
||||
const response = await axios.put(`${API_URL}/collections/${collectionId}`, {
|
||||
videoId,
|
||||
action: 'add'
|
||||
});
|
||||
const response = await axios.put(`${API_URL}/collections/${collectionId}/videos/${videoId}`);
|
||||
|
||||
// Update the collections state with the updated collection
|
||||
setCollections(prevCollections =>
|
||||
prevCollections.map(collection =>
|
||||
collection.id === collectionId ? response.data : collection
|
||||
)
|
||||
);
|
||||
// Update the collections state with the new video
|
||||
setCollections(prevCollections => prevCollections.map(collection =>
|
||||
collection.id === collectionId ? { ...collection, videos: [...collection.videos, response.data] } : collection
|
||||
));
|
||||
|
||||
return true;
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error adding video to collection:', error);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Remove a video from its collection
|
||||
// Remove a video from a collection
|
||||
const handleRemoveFromCollection = async (videoId) => {
|
||||
try {
|
||||
// Find all collections that contain this video
|
||||
const collectionsWithVideo = collections.filter(collection =>
|
||||
// Get all collections
|
||||
const collectionsWithVideo = collections.filter(collection =>
|
||||
collection.videos.includes(videoId)
|
||||
);
|
||||
|
||||
// Remove the video from each collection
|
||||
// For each collection that contains the video, remove it
|
||||
for (const collection of collectionsWithVideo) {
|
||||
await axios.put(`${API_URL}/collections/${collection.id}`, {
|
||||
videoId,
|
||||
action: 'remove'
|
||||
action: "remove"
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh collections from the server
|
||||
fetchCollections();
|
||||
// Update the collections state
|
||||
setCollections(prevCollections => prevCollections.map(collection => ({
|
||||
...collection,
|
||||
videos: collection.videos.filter(v => v !== videoId)
|
||||
})));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -450,41 +493,19 @@ function App() {
|
||||
};
|
||||
|
||||
// Delete a collection
|
||||
const handleDeleteCollection = async (collectionId, deleteVideos = false) => {
|
||||
const handleDeleteCollection = async (collectionId) => {
|
||||
try {
|
||||
// Get the collection to be deleted
|
||||
const collectionToDelete = collections.find(c => c.id === collectionId);
|
||||
|
||||
if (!collectionToDelete) {
|
||||
console.error('Collection not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// If deleteVideos is true, delete all videos in the collection
|
||||
if (deleteVideos && collectionToDelete.videos.length > 0) {
|
||||
// Delete each video in the collection
|
||||
for (const videoId of collectionToDelete.videos) {
|
||||
try {
|
||||
await axios.delete(`${API_URL}/videos/${videoId}`);
|
||||
// Update the videos state
|
||||
setVideos(prevVideos => prevVideos.filter(video => video.id !== videoId));
|
||||
} catch (videoError) {
|
||||
console.error(`Error deleting video ${videoId}:`, videoError);
|
||||
// Continue with other videos even if one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the collection
|
||||
await axios.delete(`${API_URL}/collections/${collectionId}`);
|
||||
|
||||
// Refresh collections from the server
|
||||
fetchCollections();
|
||||
// Update the collections state
|
||||
setCollections(prevCollections =>
|
||||
prevCollections.filter(collection => collection.id !== collectionId)
|
||||
);
|
||||
|
||||
return true;
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error deleting collection:', error);
|
||||
return false;
|
||||
return { success: false, error: 'Failed to delete collection' };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
469
frontend/src/App.jsx.bak
Normal file
469
frontend/src/App.jsx.bak
Normal file
@@ -0,0 +1,469 @@
|
||||
import axios from 'axios';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Route, BrowserRouter as Router, Routes } from 'react-router-dom';
|
||||
import './App.css';
|
||||
import AuthorVideos from './pages/AuthorVideos';
|
||||
import CollectionPage from './pages/CollectionPage';
|
||||
import Home from './pages/Home';
|
||||
import SearchResults from './pages/SearchResults';
|
||||
import VideoPlayer from './pages/VideoPlayer';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
const DOWNLOAD_STATUS_KEY = 'mytube_download_status';
|
||||
const DOWNLOAD_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds
|
||||
const COLLECTIONS_KEY = 'mytube_collections';
|
||||
|
||||
// Helper function to get download status from localStorage
|
||||
const getStoredDownloadStatus = () => {
|
||||
try {
|
||||
const savedStatus = localStorage.getItem(DOWNLOAD_STATUS_KEY);
|
||||
if (!savedStatus) return null;
|
||||
|
||||
const parsedStatus = JSON.parse(savedStatus);
|
||||
|
||||
// Check if the saved status is too old (stale)
|
||||
if (parsedStatus.timestamp && Date.now() - parsedStatus.timestamp > DOWNLOAD_TIMEOUT) {
|
||||
localStorage.removeItem(DOWNLOAD_STATUS_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsedStatus;
|
||||
} catch (error) {
|
||||
console.error('Error parsing download status from localStorage:', error);
|
||||
localStorage.removeItem(DOWNLOAD_STATUS_KEY);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [videos, setVideos] = useState([]);
|
||||
const [searchResults, setSearchResults] = useState([]);
|
||||
const [localSearchResults, setLocalSearchResults] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [youtubeLoading, setYoutubeLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [isSearchMode, setIsSearchMode] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [collections, setCollections] = useState([]);
|
||||
|
||||
// Reference to the current search request's abort controller
|
||||
const searchAbortController = useRef(null);
|
||||
|
||||
// Get initial download status from localStorage
|
||||
const initialStatus = getStoredDownloadStatus();
|
||||
const [downloadingTitle, setDownloadingTitle] = useState(
|
||||
initialStatus ? initialStatus.title || '' : ''
|
||||
);
|
||||
|
||||
// Fetch collections from the server
|
||||
const fetchCollections = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/collections`);
|
||||
setCollections(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching collections:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a function to check download status from the backend
|
||||
const checkBackendDownloadStatus = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/download-status`);
|
||||
|
||||
if (response.data.isDownloading) {
|
||||
// If backend is downloading, update the local status
|
||||
setDownloadingTitle(response.data.title || 'Downloading...');
|
||||
|
||||
// Save to localStorage for persistence
|
||||
const statusData = {
|
||||
title: response.data.title || 'Downloading...',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
localStorage.setItem(DOWNLOAD_STATUS_KEY, JSON.stringify(statusData));
|
||||
} else if (downloadingTitle && !response.data.isDownloading) {
|
||||
// If we think we're downloading but backend says no, clear the status
|
||||
console.log('Backend says download is complete, clearing status');
|
||||
localStorage.removeItem(DOWNLOAD_STATUS_KEY);
|
||||
setDownloadingTitle('');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking backend download status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Check backend download status periodically
|
||||
useEffect(() => {
|
||||
// Check immediately on mount
|
||||
checkBackendDownloadStatus();
|
||||
|
||||
// Then check every 5 seconds
|
||||
const statusCheckInterval = setInterval(checkBackendDownloadStatus, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(statusCheckInterval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch collections on component mount
|
||||
useEffect(() => {
|
||||
fetchCollections();
|
||||
}, []);
|
||||
|
||||
// Fetch videos on component mount
|
||||
useEffect(() => {
|
||||
fetchVideos();
|
||||
}, []);
|
||||
|
||||
// Set up localStorage and event listeners
|
||||
useEffect(() => {
|
||||
console.log('Setting up localStorage and event listeners');
|
||||
|
||||
// Set up event listener for storage changes (for multi-tab support)
|
||||
const handleStorageChange = (e) => {
|
||||
if (e.key === DOWNLOAD_STATUS_KEY) {
|
||||
try {
|
||||
const newStatus = e.newValue ? JSON.parse(e.newValue) : { title: '' };
|
||||
console.log('Storage changed, new status:', newStatus);
|
||||
setDownloadingTitle(newStatus.title || '');
|
||||
} catch (error) {
|
||||
console.error('Error handling storage change:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
// Set up periodic check for stale download status
|
||||
const checkDownloadStatus = () => {
|
||||
const status = getStoredDownloadStatus();
|
||||
if (!status && downloadingTitle) {
|
||||
console.log('Clearing stale download status');
|
||||
setDownloadingTitle('');
|
||||
}
|
||||
};
|
||||
|
||||
// Check every minute
|
||||
const statusCheckInterval = setInterval(checkDownloadStatus, 60000);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
clearInterval(statusCheckInterval);
|
||||
};
|
||||
}, [downloadingTitle]);
|
||||
|
||||
// Update localStorage whenever downloadingTitle changes
|
||||
useEffect(() => {
|
||||
console.log('Download title changed:', downloadingTitle);
|
||||
|
||||
if (downloadingTitle) {
|
||||
const statusData = {
|
||||
title: downloadingTitle,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
console.log('Saving to localStorage:', statusData);
|
||||
localStorage.setItem(DOWNLOAD_STATUS_KEY, JSON.stringify(statusData));
|
||||
} else {
|
||||
console.log('Removing from localStorage');
|
||||
localStorage.removeItem(DOWNLOAD_STATUS_KEY);
|
||||
}
|
||||
}, [downloadingTitle]);
|
||||
|
||||
const fetchVideos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await axios.get(`${API_URL}/videos`);
|
||||
setVideos(response.data);
|
||||
setError(null);
|
||||
|
||||
// Check if we need to clear a stale download status
|
||||
if (downloadingTitle) {
|
||||
const status = getStoredDownloadStatus();
|
||||
if (!status) {
|
||||
console.log('Clearing download status after fetching videos');
|
||||
setDownloadingTitle('');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching videos:', err);
|
||||
setError('Failed to load videos. Please try again later.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVideoSubmit = async (videoUrl) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Extract title from URL for display during download
|
||||
let displayTitle = videoUrl;
|
||||
if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
|
||||
displayTitle = 'YouTube video';
|
||||
} else if (videoUrl.includes('bilibili.com') || videoUrl.includes('b23.tv')) {
|
||||
displayTitle = 'Bilibili video';
|
||||
}
|
||||
|
||||
// Set download status before making the API call
|
||||
setDownloadingTitle(displayTitle);
|
||||
|
||||
const response = await axios.post(`${API_URL}/download`, { youtubeUrl: videoUrl });
|
||||
setVideos(prevVideos => [response.data.video, ...prevVideos]);
|
||||
setIsSearchMode(false);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('Error downloading video:', err);
|
||||
|
||||
// Check if the error is because the input is a search term
|
||||
if (err.response?.data?.isSearchTerm) {
|
||||
// Handle as search term
|
||||
return await handleSearch(err.response.data.searchTerm);
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: err.response?.data?.error || 'Failed to download video. Please try again.'
|
||||
};
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setDownloadingTitle('');
|
||||
}
|
||||
};
|
||||
|
||||
const searchLocalVideos = (query) => {
|
||||
if (!query || !videos.length) return [];
|
||||
|
||||
const searchTermLower = query.toLowerCase();
|
||||
|
||||
return videos.filter(video =>
|
||||
video.title.toLowerCase().includes(searchTermLower) ||
|
||||
video.author.toLowerCase().includes(searchTermLower)
|
||||
);
|
||||
};
|
||||
|
||||
const handleSearch = async (query) => {
|
||||
// Don't enter search mode if the query is empty
|
||||
if (!query || query.trim() === '') {
|
||||
resetSearch();
|
||||
return { success: false, error: 'Please enter a search term' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Cancel any previous search request
|
||||
if (searchAbortController.current) {
|
||||
searchAbortController.current.abort();
|
||||
}
|
||||
|
||||
// Create a new abort controller for this request
|
||||
searchAbortController.current = new AbortController();
|
||||
const signal = searchAbortController.current.signal;
|
||||
|
||||
// Set search mode and term immediately
|
||||
setIsSearchMode(true);
|
||||
setSearchTerm(query);
|
||||
|
||||
// Search local videos first (synchronously)
|
||||
const localResults = searchLocalVideos(query);
|
||||
setLocalSearchResults(localResults);
|
||||
|
||||
// Set loading state only for YouTube results
|
||||
setYoutubeLoading(true);
|
||||
|
||||
// Then search YouTube asynchronously
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/search`, {
|
||||
params: { query },
|
||||
signal: signal // Pass the abort signal to axios
|
||||
});
|
||||
|
||||
// Only update results if the request wasn't aborted
|
||||
if (!signal.aborted) {
|
||||
setSearchResults(response.data.results);
|
||||
}
|
||||
} catch (youtubeErr) {
|
||||
// Don't handle if it's an abort error
|
||||
if (youtubeErr.name !== 'CanceledError' && youtubeErr.name !== 'AbortError') {
|
||||
console.error('Error searching YouTube:', youtubeErr);
|
||||
}
|
||||
// Don't set overall error if only YouTube search fails
|
||||
} finally {
|
||||
// Only update loading state if the request wasn't aborted
|
||||
if (!signal.aborted) {
|
||||
setYoutubeLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
// Don't handle if it's an abort error
|
||||
if (err.name !== 'CanceledError' && err.name !== 'AbortError') {
|
||||
console.error('Error in search process:', err);
|
||||
|
||||
// Even if there's an error in the overall process,
|
||||
// we still want to show local results if available
|
||||
const localResults = searchLocalVideos(query);
|
||||
if (localResults.length > 0) {
|
||||
setLocalSearchResults(localResults);
|
||||
setIsSearchMode(true);
|
||||
setSearchTerm(query);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to search. Please try again.'
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
// Only update loading state if the request wasn't aborted
|
||||
if (searchAbortController.current && !searchAbortController.current.signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Delete a video
|
||||
const handleDeleteVideo = async (id) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// First, remove the video from any collections
|
||||
await handleRemoveFromCollection(id);
|
||||
|
||||
// Then delete the video
|
||||
await axios.delete(`${API_URL}/videos/${id}`);
|
||||
|
||||
// Update the videos state
|
||||
setVideos(prevVideos => prevVideos.filter(video => video.id !== id));
|
||||
|
||||
setLoading(false);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error deleting video:', error);
|
||||
setError('Failed to delete video');
|
||||
setLoading(false);
|
||||
return { success: false, error: 'Failed to delete video' };
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadFromSearch = async (videoUrl, title) => {
|
||||
// Abort any ongoing search request
|
||||
if (searchAbortController.current) {
|
||||
searchAbortController.current.abort();
|
||||
searchAbortController.current = null;
|
||||
}
|
||||
|
||||
setIsSearchMode(false);
|
||||
// If title is provided, use it for the downloading message
|
||||
if (title) {
|
||||
setDownloadingTitle(title);
|
||||
}
|
||||
return await handleVideoSubmit(videoUrl);
|
||||
};
|
||||
|
||||
// For debugging
|
||||
useEffect(() => {
|
||||
console.log('Current download status:', {
|
||||
downloadingTitle,
|
||||
isDownloading: !!downloadingTitle,
|
||||
localStorage: localStorage.getItem(DOWNLOAD_STATUS_KEY)
|
||||
});
|
||||
}, [downloadingTitle]);
|
||||
|
||||
// Cleanup effect to abort any pending search requests when unmounting
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Abort any ongoing search request when component unmounts
|
||||
if (searchAbortController.current) {
|
||||
searchAbortController.current.abort();
|
||||
searchAbortController.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update the resetSearch function to abort any ongoing search
|
||||
const resetSearch = () => {
|
||||
// Abort any ongoing search request
|
||||
if (searchAbortController.current) {
|
||||
searchAbortController.current.abort();
|
||||
searchAbortController.current = null;
|
||||
}
|
||||
|
||||
// Reset search-related state
|
||||
setIsSearchMode(false);
|
||||
setSearchTerm('');
|
||||
setSearchResults([]);
|
||||
setLocalSearchResults([]);
|
||||
setYoutubeLoading(false);
|
||||
};
|
||||
|
||||
// Create a new collection
|
||||
const handleCreateCollection = async (name, videoId = null) => {
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/collections`, {
|
||||
name,
|
||||
videoId
|
||||
});
|
||||
|
||||
// Update the collections state with the new collection from the server
|
||||
setCollections(prevCollections => [...prevCollections, response.data]);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error creating collection:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Add a video to a collection
|
||||
const handleAddToCollection = async (collectionId, videoId) => {
|
||||
try {
|
||||
// If videoId is provided, remove it from any other collections first
|
||||
// This is handled on the server side now
|
||||
|
||||
// Add the video to the selected collection
|
||||
const response = await axios.put(`${API_URL}/collections/${collectionId}/videos/${videoId}`);
|
||||
|
||||
// Update the collections state with the new video
|
||||
setCollections(prevCollections => prevCollections.map(collection =>
|
||||
collection.id === collectionId ? { ...collection, videos: [...collection.videos, response.data] } : collection
|
||||
));
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error adding video to collection:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Remove a video from a collection
|
||||
const handleRemoveFromCollection = async (videoId) => {
|
||||
try {
|
||||
// Remove the video from all collections
|
||||
await axios.delete(`${API_URL}/collections/videos/${videoId}`);
|
||||
|
||||
// Update the collections state
|
||||
setCollections(prevCollections => prevCollections.map(collection => ({
|
||||
...collection,
|
||||
videos: collection.videos.filter(v => v.id !== videoId)
|
||||
})));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error removing video from collection:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/author-videos" element={<AuthorVideos />} />
|
||||
<Route path="/collection-page" element={<CollectionPage />} />
|
||||
<Route path="/search-results" element={<SearchResults />} />
|
||||
<Route path="/video-player" element={<VideoPlayer />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
Reference in New Issue
Block a user