mirror of
https://github.com/TheBinaryNinja/tvapp2.git
synced 2026-06-12 09:25:41 -04:00
114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
import { Router } from 'express';
|
|
import { Playlist } from '../models/Playlist.js';
|
|
import { PlaylistChannel } from '../models/PlaylistChannel.js';
|
|
import { Channel } from '../models/Channel.js';
|
|
import { SourceChannel, type SourceChannelDoc } from '../models/SourceChannel.js';
|
|
import { toUiChannel } from '../sources/translate.js';
|
|
|
|
export const playlistsRouter = Router();
|
|
|
|
// Channel count comes from SourceChannel for (Default) source playlists, else the legacy join table.
|
|
async function channelCountFor(doc: { id: string; source?: string | null }): Promise<number> {
|
|
if (doc.source) return SourceChannel.countDocuments({ source: doc.source });
|
|
return PlaylistChannel.countDocuments({ playlistId: doc.id });
|
|
}
|
|
|
|
playlistsRouter.get('/', async (_req, res, next) => {
|
|
try {
|
|
const docs = await Playlist.find({}, { _id: 0 }).lean();
|
|
const [legacyCounts, sourceCounts] = await Promise.all([
|
|
PlaylistChannel.aggregate<{ _id: string; count: number }>([
|
|
{ $group: { _id: '$playlistId', count: { $sum: 1 } } },
|
|
]),
|
|
SourceChannel.aggregate<{ _id: string; count: number }>([
|
|
{ $group: { _id: '$source', count: { $sum: 1 } } },
|
|
]),
|
|
]);
|
|
const legacyById = new Map(legacyCounts.map((c) => [c._id, c.count]));
|
|
const sourceBySource = new Map(sourceCounts.map((c) => [c._id, c.count]));
|
|
res.json(
|
|
docs.map((d) => ({
|
|
...d,
|
|
channels: d.source ? sourceBySource.get(d.source) ?? 0 : legacyById.get(d.id) ?? 0,
|
|
})),
|
|
);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
playlistsRouter.get('/:id', async (req, res, next) => {
|
|
try {
|
|
const doc = await Playlist.findOne({ id: req.params.id }, { _id: 0 }).lean();
|
|
if (!doc) return res.status(404).json({ error: 'not_found' });
|
|
res.json({ ...doc, channels: await channelCountFor(doc) });
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
// List channels in a playlist. (Default) source playlists project the canonical SourceChannel docs
|
|
// through the translation layer (UI shape, nulls for unmapped fields); legacy playlists use the join.
|
|
playlistsRouter.get('/:id/channels', async (req, res, next) => {
|
|
try {
|
|
const playlist = await Playlist.findOne({ id: req.params.id }).lean();
|
|
if (!playlist) return res.status(404).json({ error: 'not_found' });
|
|
|
|
if (playlist.source) {
|
|
const docs = await SourceChannel.find({ source: playlist.source })
|
|
.sort({ groupKey: 1, name: 1 })
|
|
.lean<SourceChannelDoc[]>();
|
|
return res.json(docs.map((d, order) => ({ ...toUiChannel(d), order })));
|
|
}
|
|
|
|
const memberships = await PlaylistChannel.find({ playlistId: req.params.id }, { _id: 0 })
|
|
.sort({ order: 1 })
|
|
.lean();
|
|
const channelIds = memberships.map((m) => m.channelId);
|
|
const channels = await Channel.find({ id: { $in: channelIds } }, { _id: 0 }).lean();
|
|
const byId = new Map(channels.map((c) => [c.id, c]));
|
|
res.json(
|
|
memberships
|
|
.map((m) => {
|
|
const ch = byId.get(m.channelId);
|
|
return ch ? { ...ch, order: m.order } : null;
|
|
})
|
|
.filter(Boolean),
|
|
);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
// Add a channel to a (legacy) playlist (idempotent on the unique pair).
|
|
playlistsRouter.post('/:id/channels', async (req, res, next) => {
|
|
try {
|
|
const { channelId, order } = req.body ?? {};
|
|
if (typeof channelId !== 'string' || typeof order !== 'number') {
|
|
return res.status(400).json({ error: 'channelId (string) and order (number) required' });
|
|
}
|
|
const doc = await PlaylistChannel.findOneAndUpdate(
|
|
{ playlistId: req.params.id, channelId },
|
|
{ $set: { order } },
|
|
{ upsert: true, new: true, projection: { _id: 0 } },
|
|
).lean();
|
|
res.status(201).json(doc);
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
// Remove a channel from a (legacy) playlist.
|
|
playlistsRouter.delete('/:id/channels/:channelId', async (req, res, next) => {
|
|
try {
|
|
const result = await PlaylistChannel.deleteOne({
|
|
playlistId: req.params.id,
|
|
channelId: req.params.channelId,
|
|
});
|
|
if (result.deletedCount === 0) return res.status(404).json({ error: 'not_found' });
|
|
res.status(204).end();
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|