feat: add home page with resume functionality and recently added VOD
This commit is contained in:
@@ -288,5 +288,34 @@ router.post('/hide/all', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get recent movies or series
|
||||
router.get('/recent', async (req, res) => {
|
||||
try {
|
||||
const { type, limit = 12 } = req.query;
|
||||
if (!type || (type !== 'movie' && type !== 'series')) {
|
||||
return res.status(400).json({ error: 'Valid type (movie or series) is required' });
|
||||
}
|
||||
|
||||
const db = getDb();
|
||||
const recentItems = db.prepare(`
|
||||
SELECT * FROM playlist_items
|
||||
WHERE type = ? AND is_hidden = 0
|
||||
ORDER BY added_at DESC
|
||||
LIMIT ?
|
||||
`).all(type, parseInt(limit));
|
||||
|
||||
// Parse JSON data for each item
|
||||
const formatted = recentItems.map(item => ({
|
||||
...item,
|
||||
data: JSON.parse(item.data)
|
||||
}));
|
||||
|
||||
res.json(formatted);
|
||||
} catch (err) {
|
||||
console.error(`Error getting recent ${req.query.type}:`, err);
|
||||
res.status(500).json({ error: 'Failed to get recent items' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getDb } = require('../db/sqlite');
|
||||
const { requireAuth } = require('../auth');
|
||||
|
||||
// Middleware to ensure authentication
|
||||
router.use(requireAuth);
|
||||
|
||||
/**
|
||||
* GET /api/history
|
||||
* Returns the watch history for the authenticated user
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const userId = req.user.id;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT * FROM watch_history
|
||||
WHERE user_id = ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT ?
|
||||
`).all(userId, limit);
|
||||
|
||||
const history = rows.map(row => ({
|
||||
...row,
|
||||
data: JSON.parse(row.data || '{}')
|
||||
}));
|
||||
|
||||
res.json(history);
|
||||
} catch (err) {
|
||||
console.error('[History] Error fetching history:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch history' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/history
|
||||
* Saves/updates watch progress for an item
|
||||
*/
|
||||
router.post('/', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const userId = req.user.id;
|
||||
const { id, type, parentId, progress, duration, data } = req.body;
|
||||
|
||||
if (!id || !type) {
|
||||
return res.status(400).json({ error: 'Missing required fields (id, type)' });
|
||||
}
|
||||
|
||||
const compositeId = `${userId}:${id}`;
|
||||
const timestamp = Date.now();
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO watch_history (id, user_id, item_type, item_id, parent_id, progress, duration, updated_at, data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
progress = excluded.progress,
|
||||
duration = excluded.duration,
|
||||
updated_at = excluded.updated_at,
|
||||
data = excluded.data
|
||||
`);
|
||||
|
||||
stmt.run(
|
||||
compositeId,
|
||||
userId,
|
||||
type,
|
||||
id.toString(),
|
||||
parentId ? parentId.toString() : null,
|
||||
progress || 0,
|
||||
duration || 0,
|
||||
timestamp,
|
||||
JSON.stringify(data || {})
|
||||
);
|
||||
|
||||
res.json({ success: true, timestamp });
|
||||
} catch (err) {
|
||||
console.error('[History] Error saving progress:', err);
|
||||
res.status(500).json({ error: 'Failed to save progress' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/history/:itemId
|
||||
* Removes an item from the user's watch history
|
||||
*/
|
||||
router.delete('/:itemId', (req, res) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const userId = req.user.id;
|
||||
const itemId = req.params.itemId;
|
||||
|
||||
const compositeId = `${userId}:${itemId}`;
|
||||
|
||||
const stmt = db.prepare('DELETE FROM watch_history WHERE id = ? AND user_id = ?');
|
||||
const result = stmt.run(compositeId, userId);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ error: 'Item not found in history' });
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[History] Error deleting history item:', err);
|
||||
res.status(500).json({ error: 'Failed to delete history item' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -28,11 +28,11 @@ const BROWSER_AUDIO_CODECS = ['aac', 'mp3', 'opus', 'vorbis'];
|
||||
/**
|
||||
* Probe stream with ffprobe
|
||||
*/
|
||||
function probeStream(url, ffprobePath, timeout = 15000) {
|
||||
function probeStream(url, ffprobePath, userAgent = null, timeout = 15000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
'-v', 'error',
|
||||
'-user_agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
'-user_agent', userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
'-print_format', 'json',
|
||||
'-show_streams',
|
||||
'-show_format',
|
||||
@@ -138,12 +138,13 @@ function analyzeProbeResult(probeResult, url) {
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const { url } = req.query;
|
||||
const { url, ua } = req.query;
|
||||
if (!url) {
|
||||
return res.status(400).json({ error: 'URL parameter is required' });
|
||||
}
|
||||
|
||||
const ffprobePath = req.app.locals.ffprobePath;
|
||||
const cacheKey = `${url}${ua ? `|${ua}` : ''}`;
|
||||
|
||||
if (!ffprobePath) {
|
||||
// No ffprobe available - assume needs transcoding to be safe
|
||||
@@ -159,20 +160,20 @@ router.get('/', async (req, res) => {
|
||||
}
|
||||
|
||||
// Check cache
|
||||
const cached = probeCache.get(url);
|
||||
const cached = probeCache.get(cacheKey);
|
||||
if (cached && (Date.now() - cached.timestamp < CACHE_TTL)) {
|
||||
console.log(`[Probe] Cache hit for: ${url.substring(0, 50)}...`);
|
||||
return res.json(cached.result);
|
||||
}
|
||||
|
||||
console.log(`[Probe] Probing: ${url.substring(0, 80)}...`);
|
||||
console.log(`[Probe] Probing: ${url.substring(0, 80)}... ${ua ? `(UA: ${ua})` : ''}`);
|
||||
|
||||
try {
|
||||
const probeResult = await probeStream(url, ffprobePath);
|
||||
const probeResult = await probeStream(url, ffprobePath, ua);
|
||||
const analysis = analyzeProbeResult(probeResult, url);
|
||||
|
||||
// Cache result
|
||||
probeCache.set(url, { result: analysis, timestamp: Date.now() });
|
||||
probeCache.set(cacheKey, { result: analysis, timestamp: Date.now() });
|
||||
|
||||
console.log(`[Probe] Result: video=${analysis.video}, audio=${analysis.audio}, ` +
|
||||
`container=${analysis.container}, compatible=${analysis.compatible}, ` +
|
||||
|
||||
Reference in New Issue
Block a user