diff --git a/controller/latest.js b/controller/latest.js index 5ff748b..3924c77 100644 --- a/controller/latest.js +++ b/controller/latest.js @@ -1,24 +1,35 @@ const { PrismaClient } = require('@prisma/client') +const redis = require('../utils/redis.js') const prisma = new PrismaClient() exports.getLatest = async (req, res) => { - res.json(await prisma.videos.findMany({ - take: 30, - orderBy: [ - { - archived: 'desc' + let json + const cached = await redis.get('latest') + + if (cached) { + json = JSON.parse(cached) + } else { + json = await prisma.videos.findMany({ + take: 90, + orderBy: [ + { + archived: 'desc' + } + ], + select: { + id: true, + title: true, + thumbnail: true, + published: true, + archived: true, + channel: true, + channelId: true, + channelAvatar: true, + channelVerified: true } - ], - select: { - id: true, - title: true, - thumbnail: true, - published: true, - archived: true, - channel: true, - channelId: true, - channelAvatar: true, - channelVerified: true - } - })) + }) + await redis.set('latest', JSON.stringify(json), 'EX', 3600) + } + + res.json(json) } \ No newline at end of file diff --git a/controller/search.js b/controller/search.js index e6a7c9d..6b5e18a 100644 --- a/controller/search.js +++ b/controller/search.js @@ -1,8 +1,22 @@ +const crypto = require('node:crypto') const validate = require('../utils/validate.js') +const redis = require('../utils/redis.js') +const { RedisRateLimiter } = require('rolling-rate-limiter') const { PrismaClient } = require('@prisma/client') const prisma = new PrismaClient() +const limiter = new RedisRateLimiter({ + client: redis, + namespace: 'search:', + interval: 5 * 60 * 1000, + maxInInterval: 5 +}) + exports.searchVideo = async (req, res) => { + const ipHash = crypto.createHash('sha256').update(req.headers['x-userip'] || '0.0.0.0').digest('hex') + const isLimited = await limiter.limit(ipHash) + if (isLimited) return res.status(429).send('error-You have been ratelimited.') + const id = await validate.validateVideoInput(req.query.search) if (id.fail) { const videos = await prisma.videos.findMany({ @@ -24,7 +38,7 @@ exports.searchPlaylist = async (req, res) => { if (id.fail) { res.status(500).send(id.message) } else { - res.send(``) + res.redirect(`${process.env.FRONTEND}/playlist?list=${id}`) } } @@ -33,6 +47,6 @@ exports.searchChannel = async (req, res) => { if (id.fail) { res.status(500).send(id.message) } else { - res.send(``) + res.redirect(`${process.env.FRONTEND}/channel/${id}`) } } \ No newline at end of file diff --git a/controller/transparency.js b/controller/transparency.js index e014334..554f038 100644 --- a/controller/transparency.js +++ b/controller/transparency.js @@ -1,14 +1,25 @@ const { PrismaClient } = require('@prisma/client') +const redis = require('../utils/redis.js') const prisma = new PrismaClient() exports.getReports = async (req, res) => { - res.json((await prisma.reports.findMany()).map(r => { - return { - ...r, - details: (r.details).split('<').join('<').split('>').join('>'), - date: (r.date).toISOString().slice(0,10) - } - })) + let json + const cached = await redis.get('transparency') + + if (cached) { + json = JSON.parse(cached) + } else { + json = (await prisma.reports.findMany()).map(r => { + return { + ...r, + details: (r.details).split('<').join('<').split('>').join('>'), + date: (r.date).toISOString().slice(0,10) + } + }) + await redis.set('transparency', JSON.stringify(json), 'EX', 3600) + } + + res.json(json) } exports.getReport = async (req, res) => { diff --git a/controller/video.js b/controller/video.js index 808abdc..e735ffe 100644 --- a/controller/video.js +++ b/controller/video.js @@ -3,28 +3,37 @@ const prisma = new PrismaClient() const DOMPurify = require('isomorphic-dompurify') const metadata = require('../utils/metadata.js') +const redis = require('../utils/redis.js') exports.getVideo = async (req, res) => { - const info = await prisma.videos.findFirst({ - where: { - id: req.params.id - }, - select: { - title: true, - description: true, - thumbnail: true, - source: true, - published: true, - archived: true, - channel: true, - channelId: true, - channelAvatar: true, - channelVerified: true, - disabled: true - } - }) - - if (!info) return res.json({ error: '404' }) + let info + const cached = await redis.get(`video:${req.params.id}`) + + if (cached) { + info = JSON.parse(cached) + } else { + info = await prisma.videos.findFirst({ + where: { + id: req.params.id + }, + select: { + title: true, + description: true, + thumbnail: true, + source: true, + published: true, + archived: true, + channel: true, + channelId: true, + channelAvatar: true, + channelVerified: true, + disabled: true + } + }) + + if (!info) return res.json({ error: '404' }) + await redis.set(`video:${req.params.id}`, JSON.stringify(info), 'EX', 3600) + } res.json({ ...info, @@ -33,11 +42,17 @@ exports.getVideo = async (req, res) => { } exports.getChannel = async (req, res) => { - const videos = await metadata.getChannelVideos(req.params.id) - const channel = await metadata.getChannel(req.params.id) + const cached = await redis.get(`channel:${req.params.id}`) + if (cached) return res.json(JSON.parse(cached)) - if (!videos || !channel) return res.json({ error: '500' }) - if (videos.error) return res.json({ error: '404' }) + const [videos, channel] = await Promise.all([ + metadata.getChannelVideos(req.params.id), + metadata.getChannel(req.params.id) + ]) + + if (!videos || !channel || videos.error) { + return res.json({ error: '404' }); + } const archived = await prisma.videos.findMany({ where: { @@ -52,43 +67,66 @@ exports.getChannel = async (req, res) => { } }) - var allVideos = [] - allVideos = allVideos.concat((videos).map(video => { - return { - id: video.url.replace('/watch?v=', ''), - published: (new Date(video.uploaded)).toISOString().slice(0,10), - ...video - } - })) - - await Promise.all(archived.map(async (v) => { - const allVideo = allVideos.find(o => o.id == v.id) - if (allVideo) { - const index = allVideos.findIndex(o => o.id == v.id) - allVideos[index] = v + const processedVideos = videos.map(video => ({ + id: video.url.replace('/watch?v=', ''), + published: new Date(video.uploaded).toISOString().slice(0, 10), + ...video + })); + + archived.forEach(v => { + const existingVideoIndex = processedVideos.findIndex(video => video.id === v.id); + if (existingVideoIndex !== -1) { + processedVideos[existingVideoIndex] = v; } else { - allVideos.push({ - ...v, - deleted: undefined - }) + processedVideos.push({ ...v, deleted: undefined }); } - })) - - allVideos.sort((a, b) => new Date(b.published) - new Date(a.published)) - - res.json({ - name: channel.author, + }); + + processedVideos.sort((a, b) => new Date(b.published) - new Date(a.published)); + + const json = { + name: channel.author, avatar: channel.authorThumbnails[1].url, verified: channel.authorVerified, - videos: allVideos + videos: processedVideos + } + await redis.set(`channel:${req.params.id}`, JSON.stringify(json), 'EX', 3600) + res.json(json) +} + +exports.getOnlyChannelVideos = async (req, res) => { + const cached = await redis.get(`channelVideos:${req.params.id}`) + if (cached) return res.json(JSON.parse(cached)) + + const archived = await prisma.videos.findMany({ + where: { + channelId: req.params.id + }, + select: { + id: true, + title: true, + thumbnail: true, + published: true, + archived: true + }, + orderBy: { + published: 'desc' + } }) + + const json = { + videos: archived + } + await redis.set(`channelVideos:${req.params.id}`, JSON.stringify(json), 'EX', 3600) + res.json(json) } exports.getPlaylist = async (req, res) => { + const cached = await redis.get(`playlist:${req.params.id}`) + if (cached) return res.json(JSON.parse(cached)) + const playlist = await metadata.getPlaylistVideos(req.params.id) - - if (!playlist) return res.json({ error: '500' }) - if (playlist.error) return res.json({ error: '404' }) + if (!playlist || playlist.error) return res.json({ error: '404' }) const playlistArchived = await prisma.videos.findMany({ where: { @@ -103,59 +141,54 @@ exports.getPlaylist = async (req, res) => { } }) - var allVideos = [] - allVideos = allVideos.concat((playlist.relatedStreams).map(video => { - return { - id: video.url.replace('/watch?v=', ''), - published: (new Date(video.uploaded)).toISOString().slice(0,10), - ...video - } - })) + const allVideos = playlist.relatedStreams.map(video => ({ + id: video.url.replace('/watch?v=', ''), + published: new Date(video.uploaded).toISOString().slice(0, 10), + ...video + })); await Promise.all(playlistArchived.map(async (v) => { - const allVideo = allVideos.find(o => o.id == v.id) + const allVideo = allVideos.find(o => o.id == v.id); if (allVideo) { - const index = allVideos.findIndex(o => o.id == v.id) - allVideos[index] = v + const index = allVideos.findIndex(o => o.id == v.id); + allVideos[index] = v; } else { - const live = await metadata.getVideoMetadata(v.id) - + const live = await metadata.getVideoMetadata(v.id); allVideos.push({ - ...v, + ...v, deleted: live.error ? true : false - }) + }); } - })) - - await Promise.all(allVideos.map(async (v) => { - if (!v.archived) { - const video = await prisma.videos.findFirst({ - where: { - id: v.id - }, - select: { - id: true, - title: true, - thumbnail: true, - published: true, - archived: true - } - }) - - if (video) { - const index = allVideos.findIndex(o => o.id == v.id) - allVideos[index] = video + })); + + await Promise.all(allVideos.filter(v => !v.archived).map(async (v) => { + const video = await prisma.videos.findFirst({ + where: { + id: v.id + }, + select: { + id: true, + title: true, + thumbnail: true, + published: true, + archived: true } + }); + if (video) { + const index = allVideos.findIndex(o => o.id == v.id); + allVideos[index] = video; } - })) - - allVideos.sort((a, b) => new Date(a.published) - new Date(b.published)) - - res.json({ + })); + + allVideos.sort((a, b) => new Date(b.published) - new Date(a.published)); + + const json = { name: playlist.name, - channel: playlist.uploader, + channel: playlist.uploader, url: playlist.uploaderUrl, avatar: playlist.uploaderAvatar, - videos: allVideos - }) + videos: allVideos + } + await redis.set(`playlist:${req.params.id}`, JSON.stringify(json), 'EX', 3600) + res.json(json) } \ No newline at end of file diff --git a/index.js b/index.js index 809c958..e29fe1f 100644 --- a/index.js +++ b/index.js @@ -19,6 +19,7 @@ app.use(cors()) app.get('/latest', latestController.getLatest) app.get('/video/:id', videoController.getVideo) app.get('/channel/:id', videoController.getChannel) +app.get('/channel/:id/videos', videoController.getOnlyChannelVideos) app.get('/playlist/:id', videoController.getPlaylist) app.get('/search/video', searchController.searchVideo) diff --git a/utils/ytdlp.js b/utils/ytdlp.js index 17430bb..4852b5d 100644 --- a/utils/ytdlp.js +++ b/utils/ytdlp.js @@ -21,13 +21,13 @@ async function downloadVideo(url, ws) { }) child.on("close", async (code, signal) => { - if (code == 2) { + if (code == 0) { // https://itsfoss.com/linux-exit-codes/ reject({ - fail: true + fail: false }) } else { resolve({ - fail: false + fail: true }) } })