chore: update Angular TypeScript build info cache
This commit is contained in:
@@ -41,6 +41,16 @@ import {
|
||||
unlikeVideo,
|
||||
listLikedVideos,
|
||||
isVideoLiked,
|
||||
// Playlists
|
||||
createPlaylist,
|
||||
listPlaylists,
|
||||
getPlaylistRaw,
|
||||
updatePlaylist,
|
||||
deletePlaylist,
|
||||
listPlaylistItems,
|
||||
addPlaylistVideo,
|
||||
removePlaylistVideo,
|
||||
reorderPlaylistVideos,
|
||||
} from './db.mjs';
|
||||
|
||||
const app = express();
|
||||
@@ -1286,3 +1296,178 @@ try {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`[newtube-api] listening on http://localhost:${PORT}`);
|
||||
});
|
||||
|
||||
// --- Playlists ---
|
||||
// Create a new playlist
|
||||
r.post('/playlists', authMiddleware, (req, res) => {
|
||||
try {
|
||||
const { title, description, thumbnail, isPrivate } = req.body || {};
|
||||
if (!title || String(title).trim().length === 0) {
|
||||
return res.status(400).json({ error: 'title_required' });
|
||||
}
|
||||
const pl = createPlaylist({ userId: req.user.id, title: String(title).trim(), description, thumbnail, isPrivate: !!isPrivate });
|
||||
return res.status(201).json(pl);
|
||||
} catch (e) {
|
||||
const msg = String(e?.message || e);
|
||||
if (msg === 'title_required') return res.status(400).json({ error: msg });
|
||||
return res.status(500).json({ error: 'create_failed', details: msg });
|
||||
}
|
||||
});
|
||||
|
||||
// List current user's playlists (pagination + search)
|
||||
r.get('/playlists', authMiddleware, (req, res) => {
|
||||
try {
|
||||
const limit = Math.min(200, Math.max(1, Number(req.query.limit || 50)));
|
||||
const offset = Math.max(0, Number(req.query.offset || 0));
|
||||
const q = typeof req.query.q === 'string' ? req.query.q : undefined;
|
||||
const rows = listPlaylists({ userId: req.user.id, limit, offset, q });
|
||||
return res.json(rows);
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'list_failed', details: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// Get playlist details (owner only for now)
|
||||
r.get('/playlists/:id', authMiddleware, (req, res) => {
|
||||
try {
|
||||
const id = String(req.params.id || '');
|
||||
const pl = getPlaylistRaw(id);
|
||||
if (!pl) return res.status(404).json({ error: 'not_found' });
|
||||
if (pl.userId !== req.user.id) return res.status(404).json({ error: 'not_found' });
|
||||
const limit = Math.min(2000, Math.max(1, Number(req.query.limit || 500)));
|
||||
const offset = Math.max(0, Number(req.query.offset || 0));
|
||||
const items = listPlaylistItems({ playlistId: id, limit, offset });
|
||||
return res.json({ ...pl, items });
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'get_failed', details: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// Update a playlist (title/description/thumbnail/isPrivate)
|
||||
r.put('/playlists/:id', authMiddleware, (req, res) => {
|
||||
try {
|
||||
const id = String(req.params.id || '');
|
||||
const patch = req.body || {};
|
||||
const result = updatePlaylist({ userId: req.user.id, id, patch });
|
||||
if (result === 'forbidden') return res.status(403).json({ error: 'forbidden' });
|
||||
if (!result) return res.status(404).json({ error: 'not_found' });
|
||||
return res.json(result);
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'update_failed', details: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a playlist
|
||||
r.delete('/playlists/:id', authMiddleware, (req, res) => {
|
||||
try {
|
||||
const id = String(req.params.id || '');
|
||||
const result = deletePlaylist({ userId: req.user.id, id });
|
||||
if (result === 'forbidden') return res.status(403).json({ error: 'forbidden' });
|
||||
if (!result || !result.removed) return res.status(404).json({ error: 'not_found' });
|
||||
return res.status(204).end();
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'delete_failed', details: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// Add a video to a playlist (enrich title/thumbnail if missing)
|
||||
r.post('/playlists/:id/videos', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const playlistId = String(req.params.id || '');
|
||||
let { provider, videoId, title, thumbnail, sourceUrl, slug, instance } = req.body || {};
|
||||
provider = String(provider || '').trim();
|
||||
videoId = String(videoId || '').trim();
|
||||
if (!provider || !videoId) return res.status(400).json({ error: 'provider_and_videoId_required' });
|
||||
|
||||
// Optional enrichment like likes route
|
||||
try {
|
||||
const needTitle = !(typeof title === 'string' && title.trim().length > 0);
|
||||
const needThumb = !(typeof thumbnail === 'string' && thumbnail.trim().length > 0);
|
||||
if (needTitle || needThumb) {
|
||||
const url = providerUrlFrom(provider, videoId, { instance, slug, sourceUrl });
|
||||
try {
|
||||
const raw = await youtubedl(url, { dumpSingleJson: true, noWarnings: true, noCheckCertificates: true, skipDownload: true });
|
||||
const meta = (typeof raw === 'string') ? JSON.parse(raw || '{}') : (raw || {});
|
||||
if (needTitle) title = meta?.title || title || '';
|
||||
if (needThumb) thumbnail = meta?.thumbnail || (Array.isArray(meta?.thumbnails) && meta.thumbnails.length ? meta.thumbnails[0].url : thumbnail || '');
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const row = addPlaylistVideo({ userId: req.user.id, playlistId, provider, videoId, title, thumbnail });
|
||||
if (row === 'not_found') return res.status(404).json({ error: 'playlist_not_found' });
|
||||
if (row === 'forbidden') return res.status(403).json({ error: 'forbidden' });
|
||||
return res.status(201).json(row);
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'add_video_failed', details: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// Remove a video from a playlist (provider required via query)
|
||||
r.delete('/playlists/:id/videos/:videoId', authMiddleware, (req, res) => {
|
||||
try {
|
||||
const playlistId = String(req.params.id || '');
|
||||
const videoId = String(req.params.videoId || '');
|
||||
const provider = req.query.provider ? String(req.query.provider) : '';
|
||||
if (!provider || !videoId) return res.status(400).json({ error: 'provider_and_videoId_required' });
|
||||
const result = removePlaylistVideo({ userId: req.user.id, playlistId, provider, videoId });
|
||||
if (result === 'not_found') return res.status(404).json({ error: 'playlist_not_found' });
|
||||
if (result === 'forbidden') return res.status(403).json({ error: 'forbidden' });
|
||||
return res.json(result);
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'remove_video_failed', details: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// Reorder playlist items
|
||||
r.put('/playlists/:id/reorder', authMiddleware, (req, res) => {
|
||||
try {
|
||||
const playlistId = String(req.params.id || '');
|
||||
const order = Array.isArray(req.body?.order) ? req.body.order : [];
|
||||
if (!order.length) return res.status(400).json({ error: 'order_required' });
|
||||
const result = reorderPlaylistVideos({ userId: req.user.id, playlistId, order });
|
||||
if (result === 'not_found') return res.status(404).json({ error: 'playlist_not_found' });
|
||||
if (result === 'forbidden') return res.status(403).json({ error: 'forbidden' });
|
||||
return res.json(result);
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'reorder_failed', details: String(e?.message || e) });
|
||||
}
|
||||
});
|
||||
|
||||
// --- OpenAPI (minimal for playlists) ---
|
||||
r.get('/openapi.json', (_req, res) => {
|
||||
const doc = {
|
||||
openapi: '3.0.0',
|
||||
info: { title: 'NewTube API', version: '1.0.0' },
|
||||
paths: {
|
||||
'/playlists': {
|
||||
get: { summary: 'List user playlists', security: [{ bearerAuth: [] }], parameters: [
|
||||
{ name: 'limit', in: 'query', schema: { type: 'integer' } },
|
||||
{ name: 'offset', in: 'query', schema: { type: 'integer' } },
|
||||
{ name: 'q', in: 'query', schema: { type: 'string' } },
|
||||
] },
|
||||
post: { summary: 'Create playlist', security: [{ bearerAuth: [] }], requestBody: { required: true, content: { 'application/json': { schema: { type: 'object', properties: { title: { type: 'string' }, description: { type: 'string' }, thumbnail: { type: 'string' }, isPrivate: { type: 'boolean' } }, required: ['title'] } } } } }
|
||||
},
|
||||
'/playlists/{id}': {
|
||||
get: { summary: 'Get playlist details', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }] },
|
||||
put: { summary: 'Update playlist', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }] },
|
||||
delete: { summary: 'Delete playlist', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }] }
|
||||
},
|
||||
'/playlists/{id}/videos': {
|
||||
post: { summary: 'Add video to playlist', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }] }
|
||||
},
|
||||
'/playlists/{id}/videos/{videoId}': {
|
||||
delete: { summary: 'Remove video from playlist', security: [{ bearerAuth: [] }], parameters: [
|
||||
{ name: 'id', in: 'path', required: true, schema: { type: 'string' } },
|
||||
{ name: 'videoId', in: 'path', required: true, schema: { type: 'string' } },
|
||||
{ name: 'provider', in: 'query', required: true, schema: { type: 'string' } }
|
||||
] }
|
||||
},
|
||||
'/playlists/{id}/reorder': {
|
||||
put: { summary: 'Reorder playlist items', security: [{ bearerAuth: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }] }
|
||||
}
|
||||
},
|
||||
components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } } }
|
||||
};
|
||||
res.json(doc);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user