// src/App.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:4000';
function App() {
const [videos, setVideos] = useState([]);
const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(true);
const [channelsLoading, setChannelsLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedChannelId, setSelectedChannelId] = useState('');
const [expandedDescriptions, setExpandedDescriptions] = useState({});
const [showAddSubscription, setShowAddSubscription] = useState(false);
const [subscriptionUrl, setSubscriptionUrl] = useState('');
const [timeBetweenFetches, setTimeBetweenFetches] = useState(300); // Default 5 minutes
const [addingSubscription, setAddingSubscription] = useState(false);
const [subscriptionError, setSubscriptionError] = useState(null);
const [subscriptionSuccess, setSubscriptionSuccess] = useState(null);
const [editingSubscription, setEditingSubscription] = useState(null);
const [newTimeBetweenFetches, setNewTimeBetweenFetches] = useState(300);
const [updatingSubscription, setUpdatingSubscription] = useState(false);
const [deletingSubscription, setDeletingSubscription] = useState(false);
const fetchChannels = async () => {
try {
setChannelsLoading(true);
const response = await axios.get(`${API_BASE_URL}/subs-info`);
if (response.data && Array.isArray(response.data)) {
const formattedChannels = response.data.map(subscription => {
// Extract the ID and type from the _id field
const isPlaylist = subscription._id.startsWith('yt:playlist:');
const id = subscription._id.replace('yt:channel:', '').replace('yt:playlist:', '');
const type = isPlaylist ? 'playlist' : 'channel';
return {
id: id,
_id: subscription._id,
type: type,
title: subscription.title,
last_fetch: subscription.last_fetch,
last_video_update: subscription.last_video_update,
last_viewed: subscription.last_viewed,
new_vids: subscription.new_vids,
time_between_fetches: subscription.time_between_fetches,
videos: subscription.videos
};
});
setChannels(formattedChannels);
}
} catch (err) {
console.error('Error fetching channels:', err);
setError('Failed to fetch available subscriptions.');
} finally {
setChannelsLoading(false);
}
};
const setViewed = async (subscriptionId, viewedTime) => {
try {
const formData = new FormData();
formData.append('viewed_time', viewedTime);
await axios.patch(`${API_BASE_URL}/set-viewed/${subscriptionId}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
} catch (err) {
console.error('Error setting viewed time:', err);
// Don't show error to user as this is a background operation
}
};
const updateTimeBetweenFetches = async (subscriptionId, newTime) => {
try {
setUpdatingSubscription(true);
const formData = new FormData();
formData.append('time_between_fetches', newTime.toString());
await axios.patch(`${API_BASE_URL}/set-time-between-fetches/${subscriptionId}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
// Refresh the channels list to show updated data
fetchChannels();
setEditingSubscription(null);
setSubscriptionSuccess('Fetch interval updated successfully!');
} catch (err) {
console.error('Error updating fetch interval:', err);
setSubscriptionError('Failed to update fetch interval. Please try again.');
} finally {
setUpdatingSubscription(false);
}
};
const deleteSubscription = async (subscriptionId) => {
if (!window.confirm('Are you sure you want to delete this subscription? This action cannot be undone.')) {
return;
}
try {
setDeletingSubscription(true);
await axios.delete(`${API_BASE_URL}/delete-sub/${subscriptionId}`);
// If the deleted subscription was selected, clear the selection
if (selectedChannelId === subscriptionId.replace('yt:channel:', '').replace('yt:playlist:', '')) {
setSelectedChannelId('');
setVideos([]);
}
// Refresh the channels list
fetchChannels();
setSubscriptionSuccess('Subscription deleted successfully!');
} catch (err) {
console.error('Error deleting subscription:', err);
setSubscriptionError('Failed to delete subscription. Please try again.');
} finally {
setDeletingSubscription(false);
}
};
const fetchVideos = async (channelId) => {
try {
setLoading(true);
setError(null);
setExpandedDescriptions({}); // Reset expanded states when fetching new videos
// Find the full _id for the API call
const subscription = channels.find(ch => ch.id === channelId);
if (!subscription) return;
const apiUrl = `${API_BASE_URL}/vid-from-link/${subscription._id}`;
const response = await axios.get(apiUrl);
if (response.data && Array.isArray(response.data)) {
setVideos(response.data);
const currentTime = new Date().toISOString();
setViewed(subscription._id, currentTime);
} else {
throw new Error('Invalid response format');
}
} catch (err) {
setError('Failed to fetch videos. Please check the subscription and ensure the API is running.');
console.error('Error fetching videos:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchChannels();
}, []);
useEffect(() => {
if (selectedChannelId) {
fetchVideos(selectedChannelId);
}
}, [selectedChannelId]);
const handleChannelChange = (e) => {
setSelectedChannelId(e.target.value);
};
const handleRefreshChannels = () => {
fetchChannels();
};
const toggleDescription = (videoId) => {
setExpandedDescriptions(prev => ({
...prev,
[videoId]: !prev[videoId]
}));
};
const formatDate = (dateString) => {
try {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return dateString;
}
};
const formatRelativeTime = (dateString) => {
try {
const date = new Date(dateString);
const now = new Date();
const diffInHours = Math.floor((now - date) / (1000 * 60 * 60));
if (diffInHours < 1) {
return 'Just now';
} else if (diffInHours < 24) {
return `${diffInHours}h ago`;
} else {
const diffInDays = Math.floor(diffInHours / 24);
return `${diffInDays}d ago`;
}
} catch {
return dateString;
}
};
const formatDuration = (seconds) => {
if (seconds < 0) {
return '?:??';
}
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
} else {
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
};
const handleAddSubscription = async (e) => {
e.preventDefault();
if (!subscriptionUrl.trim()) {
setSubscriptionError('Please enter a YouTube channel/playlist URL');
return;
}
try {
setAddingSubscription(true);
setSubscriptionError(null);
setSubscriptionSuccess(null);
const formData = new FormData();
formData.append('url', subscriptionUrl.trim());
formData.append('time_between_fetches', timeBetweenFetches.toString());
const response = await axios.post(`${API_BASE_URL}/add-sub/`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
if (response.data) {
setSubscriptionSuccess('Subscription added successfully!');
setSubscriptionUrl('');
setTimeBetweenFetches(300);
setShowAddSubscription(false);
// Refresh the subs list to include the new subscription
fetchChannels();
} else {
throw new Error(response.data.error || 'Failed to add subscription');
}
} catch (err) {
console.error('Error adding subscription:', err);
setSubscriptionError(
err.response?.data?.error ||
err.message ||
'Failed to add subscription. Please check the URL and try again.'
);
} finally {
setAddingSubscription(false);
}
};
const resetSubscriptionForm = () => {
setSubscriptionUrl('');
setTimeBetweenFetches(300);
setSubscriptionError(null);
setSubscriptionSuccess(null);
};
const startEditingSubscription = (subscription) => {
setEditingSubscription(subscription);
setNewTimeBetweenFetches(subscription.time_between_fetches);
};
const cancelEditing = () => {
setEditingSubscription(null);
setNewTimeBetweenFetches(300);
};
return (
{channelsLoading && Loading subscriptions...
}
{error && (
{error}
selectedChannelId && fetchVideos(selectedChannelId)} className="retry-button">
Try Again
)}
{!channelsLoading && channels.length === 0 && !error && (
No subscriptions available
No YouTube channels or playlists found in the subscription list.
Refresh Subscriptions
)}
{loading && selectedChannelId && (
Loading videos for {channels.find(ch => ch.id === selectedChannelId)?.title}...
)}
{!loading && !error && selectedChannelId && (
Latest Videos from {channels.find(ch => ch.id === selectedChannelId)?.title}
- {selectedChannelId}
{videos.length > 0 && ` (${videos.length})`}
{videos.sort((a, b) => new Date(b.published) - new Date(a.published)).map((video) => (
{
e.target.src = 'https://via.placeholder.com/300x180/333/fff?text=No+Thumbnail';
}}
/>
{formatDuration(video.duration)}
{channels.find(ch => ch.id === selectedChannelId)?.last_viewed &&
new Date(video.published) > new Date(channels.find(ch => ch.id === selectedChannelId).last_viewed) && (
NEW
)}
▶
{video.title}
By:
{video.author}
Published: {formatDate(video.published)}
{video.updated && (
Updated: {formatDate(video.updated)}
)}
{video.summary && (
{video.summary}
{video.summary.length > 100 && (
toggleDescription(video.id)}
>
{expandedDescriptions[video.id] ? 'Show Less' : 'Show More'}
)}
)}
))}
)}
{!loading && !error && selectedChannelId && videos.length === 0 && (
No videos found for this subscription
The channel or playlist might not have any videos or there was an issue loading them.
)}
{!channelsLoading && !selectedChannelId && channels.length > 0 && (
Please select a subscription to view videos
Choose a channel or playlist from the dropdown above to see its latest videos.
)}
);
}
export default App;