You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
186 lines
5.4 KiB
186 lines
5.4 KiB
import { Response } from 'express'; |
|
import { AuthRequest } from '../types/index.js'; |
|
import { db, getSetting } from '../config/database.js'; |
|
import { formatDuration } from '../services/youtube.service.js'; |
|
import { refreshMultipleChannels, isRefreshInProgress, setRefreshInProgress } from '../services/cache.service.js'; |
|
|
|
export async function getAllVideos(req: AuthRequest, res: Response) { |
|
try { |
|
const { page = 1, limit = 12, channelId, search, sort = 'newest' } = req.query as any; |
|
|
|
const pageNum = page as number; |
|
const limitNum = limit as number; |
|
const offset = (pageNum - 1) * limitNum; |
|
|
|
// Build query |
|
let whereClause = 'v.duration_seconds >= 600'; |
|
const args: any[] = []; |
|
|
|
if (channelId) { |
|
whereClause += ' AND v.channel_id = ?'; |
|
args.push(channelId); |
|
} |
|
|
|
if (search) { |
|
whereClause += ' AND (v.title LIKE ? OR v.description LIKE ?)'; |
|
args.push(`%${search}%`, `%${search}%`); |
|
} |
|
|
|
// Sort clause |
|
let orderClause = 'v.published_at DESC'; |
|
if (sort === 'oldest') { |
|
orderClause = 'v.published_at ASC'; |
|
} else if (sort === 'popular') { |
|
orderClause = 'v.view_count DESC'; |
|
} |
|
|
|
// Get total count |
|
const countResult = await db.execute({ |
|
sql: `SELECT COUNT(*) as total FROM videos_cache v WHERE ${whereClause}`, |
|
args |
|
}); |
|
const total = countResult.rows[0].total as number; |
|
|
|
// Get videos |
|
const videosResult = await db.execute({ |
|
sql: ` |
|
SELECT |
|
v.*, |
|
c.name as channel_name, |
|
c.thumbnail_url as channel_thumbnail |
|
FROM videos_cache v |
|
JOIN channels c ON v.channel_id = c.id |
|
WHERE ${whereClause} |
|
ORDER BY ${orderClause} |
|
LIMIT ? OFFSET ? |
|
`, |
|
args: [...args, limitNum, offset] |
|
}); |
|
|
|
// Get oldest cache age |
|
const cacheAgeResult = await db.execute( |
|
'SELECT MIN(last_fetched) as oldest FROM cache_metadata' |
|
); |
|
|
|
let oldestCacheAge = 0; |
|
if (cacheAgeResult.rows.length > 0 && cacheAgeResult.rows[0].oldest) { |
|
const oldestFetch = new Date(cacheAgeResult.rows[0].oldest as string); |
|
oldestCacheAge = Math.floor((Date.now() - oldestFetch.getTime()) / 60000); |
|
} |
|
|
|
// Check cache age and trigger refresh if needed |
|
const cacheDuration = parseInt((await getSetting('cache_duration_minutes')) || '60'); |
|
const cacheExpired = oldestCacheAge > cacheDuration; |
|
const refreshInProgress = await isRefreshInProgress(); |
|
|
|
// Trigger async refresh if cache is expired and not already refreshing |
|
if (cacheExpired && !refreshInProgress) { |
|
// Fire and forget - don't await |
|
refreshAllChannelsAsync(); |
|
} |
|
|
|
const videos = videosResult.rows.map(row => ({ |
|
id: row.id, |
|
channelId: row.channel_id, |
|
channelName: row.channel_name, |
|
channelThumbnail: row.channel_thumbnail, |
|
title: row.title, |
|
description: row.description, |
|
thumbnailUrl: row.thumbnail_url, |
|
publishedAt: row.published_at, |
|
viewCount: row.view_count, |
|
likeCount: row.like_count, |
|
duration: row.duration, |
|
durationFormatted: formatDuration(row.duration as string) |
|
})); |
|
|
|
res.json({ |
|
success: true, |
|
data: { videos }, |
|
meta: { |
|
page: pageNum, |
|
limit: limitNum, |
|
total, |
|
totalPages: Math.ceil(total / limitNum), |
|
hasMore: offset + videos.length < total, |
|
oldestCacheAge, |
|
cacheStale: cacheExpired, |
|
refreshing: refreshInProgress |
|
} |
|
}); |
|
} catch (error: any) { |
|
console.error('Get videos error:', error); |
|
res.status(500).json({ |
|
success: false, |
|
error: { |
|
code: 'GET_VIDEOS_ERROR', |
|
message: 'Error fetching videos' |
|
} |
|
}); |
|
} |
|
} |
|
|
|
export async function refreshVideos(req: AuthRequest, res: Response) { |
|
try { |
|
let channelIds: string[] = req.body.channelIds || []; |
|
|
|
// If no specific channels, get all channels |
|
if (channelIds.length === 0) { |
|
const allChannels = await db.execute('SELECT id FROM channels'); |
|
channelIds = allChannels.rows.map(row => row.id as string); |
|
} |
|
|
|
if (channelIds.length === 0) { |
|
return res.json({ |
|
success: true, |
|
data: { |
|
channelsRefreshed: 0, |
|
videosAdded: 0, |
|
videosUpdated: 0, |
|
errors: [] |
|
} |
|
}); |
|
} |
|
|
|
// Refresh channels in parallel |
|
const result = await refreshMultipleChannels(channelIds, true); |
|
|
|
res.json({ |
|
success: true, |
|
data: { |
|
channelsRefreshed: result.success, |
|
videosAdded: result.videosAdded, |
|
videosUpdated: result.videosAdded, // Since we replace cache, all are "updated" |
|
errors: result.errors |
|
} |
|
}); |
|
} catch (error: any) { |
|
console.error('Refresh videos error:', error); |
|
res.status(500).json({ |
|
success: false, |
|
error: { |
|
code: 'REFRESH_VIDEOS_ERROR', |
|
message: 'Error refreshing videos' |
|
} |
|
}); |
|
} |
|
} |
|
|
|
async function refreshAllChannelsAsync() { |
|
try { |
|
await setRefreshInProgress(true); |
|
|
|
// Get all channel IDs |
|
const channels = await db.execute('SELECT id FROM channels'); |
|
const channelIds = channels.rows.map(row => row.id as string); |
|
|
|
if (channelIds.length > 0) { |
|
const result = await refreshMultipleChannels(channelIds, true); |
|
} |
|
} catch (error) { |
|
console.error('[AUTO-REFRESH] Error:', error); |
|
} finally { |
|
await setRefreshInProgress(false); |
|
} |
|
} |
|
|
|
|