X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fcontrollers%2Ffeeds.ts;h=29502a15458d8fd91aba60b893a5dd5e32f0bab4;hb=c55e3d7227fe1453869e309025996b9d75256d5d;hp=ce57e5c111862fa49a2f3cea98544b8a11007034;hpb=4dae00e68b71ee3725a94eef891f3c43e10040b0;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index ce57e5c11..29502a154 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts @@ -1,29 +1,63 @@ -import * as express from 'express' -import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' -import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeedsValidator, videosSortValidator } from '../middlewares' +import express from 'express' +import Feed from 'pfeed' +import { getServerActor } from '@server/models/application/application' +import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' +import { VideoInclude } from '@shared/models' +import { buildNSFWFilter } from '../helpers/express-utils' +import { CONFIG } from '../initializers/config' +import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' +import { + asyncMiddleware, + commonVideosFiltersValidator, + feedsFormatValidator, + setDefaultVideosSort, + setFeedFormatContentType, + videoCommentsFeedsValidator, + videoFeedsValidator, + videosSortValidator, + videoSubscriptionFeedsValidator +} from '../middlewares' +import { cacheRouteFactory } from '../middlewares/cache/cache' import { VideoModel } from '../models/video/video' -import * as Feed from 'pfeed' -import { AccountModel } from '../models/account/account' -import { cacheRoute } from '../middlewares/cache' -import { VideoChannelModel } from '../models/video/video-channel' import { VideoCommentModel } from '../models/video/video-comment' const feedsRouter = express.Router() +const cacheRoute = cacheRouteFactory({ + headerBlacklist: [ 'Content-Type' ] +}) + feedsRouter.get('/feeds/video-comments.:format', - asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), + feedsFormatValidator, + setFeedFormatContentType, + cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS), + asyncMiddleware(videoFeedsValidator), asyncMiddleware(videoCommentsFeedsValidator), asyncMiddleware(generateVideoCommentsFeed) ) feedsRouter.get('/feeds/videos.:format', videosSortValidator, - setDefaultSort, - asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)), + setDefaultVideosSort, + feedsFormatValidator, + setFeedFormatContentType, + cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS), + commonVideosFiltersValidator, asyncMiddleware(videoFeedsValidator), asyncMiddleware(generateVideoFeed) ) +feedsRouter.get('/feeds/subscriptions.:format', + videosSortValidator, + setDefaultVideosSort, + feedsFormatValidator, + setFeedFormatContentType, + cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS), + commonVideosFiltersValidator, + asyncMiddleware(videoSubscriptionFeedsValidator), + asyncMiddleware(generateVideoFeedForSubscriptions) +) + // --------------------------------------------------------------------------- export { @@ -32,47 +66,74 @@ export { // --------------------------------------------------------------------------- -async function generateVideoCommentsFeed (req: express.Request, res: express.Response, next: express.NextFunction) { +async function generateVideoCommentsFeed (req: express.Request, res: express.Response) { const start = 0 + const video = res.locals.videoAll + const account = res.locals.account + const videoChannel = res.locals.videoChannel - const video = res.locals.video as VideoModel - const videoId: number = video ? video.id : undefined + const comments = await VideoCommentModel.listForFeed({ + start, + count: FEEDS.COUNT, + videoId: video ? video.id : undefined, + accountId: account ? account.id : undefined, + videoChannelId: videoChannel ? videoChannel.id : undefined + }) - const comments = await VideoCommentModel.listForFeed(start, FEEDS.COUNT, videoId) + let name: string + let description: string - const name = video ? video.name : CONFIG.INSTANCE.NAME - const description = video ? video.description : CONFIG.INSTANCE.DESCRIPTION - const feed = initFeed(name, description) + if (videoChannel) { + name = videoChannel.getDisplayName() + description = videoChannel.description + } else if (account) { + name = account.getDisplayName() + description = account.description + } else { + name = video ? video.name : CONFIG.INSTANCE.NAME + description = video ? video.description : CONFIG.INSTANCE.DESCRIPTION + } + const feed = initFeed({ + name, + description, + resourceType: 'video-comments', + queryString: new URL(WEBSERVER.URL + req.originalUrl).search + }) // Adding video items to the feed, one at a time - comments.forEach(comment => { - const link = CONFIG.WEBSERVER.URL + '/videos/watch/' + comment.Video.uuid + ';threadId=' + comment.getThreadId() + for (const comment of comments) { + const link = WEBSERVER.URL + comment.getCommentStaticPath() + + let title = comment.Video.name + const author: { name: string, link: string }[] = [] + + if (comment.Account) { + title += ` - ${comment.Account.getDisplayName()}` + author.push({ + name: comment.Account.getDisplayName(), + link: comment.Account.Actor.url + }) + } feed.addItem({ - title: `${comment.Video.name} - ${comment.Account.getDisplayName()}`, + title, id: comment.url, link, content: comment.text, - author: [ - { - name: comment.Account.getDisplayName(), - link: comment.Account.Actor.url - } - ], + author, date: comment.createdAt }) - }) + } // Now the feed generation is done, let's send it! return sendFeed(feed, req, res) } -async function generateVideoFeed (req: express.Request, res: express.Response, next: express.NextFunction) { +async function generateVideoFeed (req: express.Request, res: express.Response) { const start = 0 - - const account: AccountModel = res.locals.account - const videoChannel: VideoChannelModel = res.locals.videoChannel - const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' + const account = res.locals.account + const videoChannel = res.locals.videoChannel + const nsfw = buildNSFWFilter(res, req.query.nsfw) let name: string let description: string @@ -88,53 +149,89 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n description = CONFIG.INSTANCE.DESCRIPTION } - const feed = initFeed(name, description) + const feed = initFeed({ + name, + description, + resourceType: 'videos', + queryString: new URL(WEBSERVER.URL + req.url).search + }) + + const options = { + accountId: account ? account.id : null, + videoChannelId: videoChannel ? videoChannel.id : null + } - const resultList = await VideoModel.listForApi({ + const server = await getServerActor() + const { data } = await VideoModel.listForApi({ start, count: FEEDS.COUNT, sort: req.query.sort, - hideNSFW, - filter: req.query.filter, - withFiles: true, - accountId: account ? account.id : null, - videoChannelId: videoChannel ? videoChannel.id : null + displayOnlyForFollower: { + actorId: server.id, + orLocalVideos: true + }, + nsfw, + isLocal: req.query.isLocal, + include: req.query.include | VideoInclude.FILES, + hasFiles: true, + countVideos: false, + ...options }) - // Adding video items to the feed, one at a time - resultList.data.forEach(video => { - const formattedVideoFiles = video.getFormattedVideoFilesJSON() - const torrents = formattedVideoFiles.map(videoFile => ({ - title: video.name, - url: videoFile.torrentUrl, - size_in_bytes: videoFile.size - })) + addVideosToFeed(feed, data) - feed.addItem({ - title: video.name, - id: video.url, - link: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid, - description: video.getTruncatedDescription(), - content: video.description, - author: [ - { - name: video.VideoChannel.Account.getDisplayName(), - link: video.VideoChannel.Account.Actor.url - } - ], - date: video.publishedAt, - language: video.language, - nsfw: video.nsfw, - torrent: torrents - }) + // Now the feed generation is done, let's send it! + return sendFeed(feed, req, res) +} + +async function generateVideoFeedForSubscriptions (req: express.Request, res: express.Response) { + const start = 0 + const account = res.locals.account + const nsfw = buildNSFWFilter(res, req.query.nsfw) + const name = account.getDisplayName() + const description = account.description + + const feed = initFeed({ + name, + description, + resourceType: 'videos', + queryString: new URL(WEBSERVER.URL + req.url).search + }) + + const { data } = await VideoModel.listForApi({ + start, + count: FEEDS.COUNT, + sort: req.query.sort, + nsfw, + + isLocal: req.query.isLocal, + + hasFiles: true, + include: req.query.include | VideoInclude.FILES, + + countVideos: false, + + displayOnlyForFollower: { + actorId: res.locals.user.Account.Actor.id, + orLocalVideos: false + }, + user: res.locals.user }) + addVideosToFeed(feed, data) + // Now the feed generation is done, let's send it! return sendFeed(feed, req, res) } -function initFeed (name: string, description: string) { - const webserverUrl = CONFIG.WEBSERVER.URL +function initFeed (parameters: { + name: string + description: string + resourceType?: 'videos' | 'video-comments' + queryString?: string +}) { + const webserverUrl = WEBSERVER.URL + const { name, description, resourceType, queryString } = parameters return new Feed({ title: name, @@ -148,9 +245,9 @@ function initFeed (name: string, description: string) { ` and potential licenses granted by each content's rightholder.`, generator: `Toraifōsu`, // ^.~ feedLinks: { - json: `${webserverUrl}/feeds/videos.json`, - atom: `${webserverUrl}/feeds/videos.atom`, - rss: `${webserverUrl}/feeds/videos.xml` + json: `${webserverUrl}/feeds/${resourceType}.json${queryString}`, + atom: `${webserverUrl}/feeds/${resourceType}.atom${queryString}`, + rss: `${webserverUrl}/feeds/${resourceType}.xml${queryString}` }, author: { name: 'Instance admin of ' + CONFIG.INSTANCE.NAME, @@ -160,30 +257,102 @@ function initFeed (name: string, description: string) { }) } +function addVideosToFeed (feed, videos: VideoModel[]) { + /** + * Adding video items to the feed object, one at a time + */ + for (const video of videos) { + const formattedVideoFiles = video.getFormattedVideoFilesJSON(false) + + const torrents = formattedVideoFiles.map(videoFile => ({ + title: video.name, + url: videoFile.torrentUrl, + size_in_bytes: videoFile.size + })) + + const videos = formattedVideoFiles.map(videoFile => { + const result = { + type: 'video/mp4', + medium: 'video', + height: videoFile.resolution.label.replace('p', ''), + fileSize: videoFile.size, + url: videoFile.fileUrl, + framerate: videoFile.fps, + duration: video.duration + } + + if (video.language) Object.assign(result, { lang: video.language }) + + return result + }) + + const categories: { value: number, label: string }[] = [] + if (video.category) { + categories.push({ + value: video.category, + label: getCategoryLabel(video.category) + }) + } + + feed.addItem({ + title: video.name, + id: video.url, + link: WEBSERVER.URL + video.getWatchStaticPath(), + description: video.getTruncatedDescription(), + content: video.description, + author: [ + { + name: video.VideoChannel.Account.getDisplayName(), + link: video.VideoChannel.Account.Actor.url + } + ], + date: video.publishedAt, + nsfw: video.nsfw, + torrent: torrents, + videos, + embed: { + url: video.getEmbedStaticPath(), + allowFullscreen: true + }, + player: { + url: video.getWatchStaticPath() + }, + categories, + community: { + statistics: { + views: video.views + } + }, + thumbnail: [ + { + url: WEBSERVER.URL + video.getPreviewStaticPath(), + height: PREVIEWS_SIZE.height, + width: PREVIEWS_SIZE.width + } + ] + }) + } +} + function sendFeed (feed, req: express.Request, res: express.Response) { const format = req.params.format if (format === 'atom' || format === 'atom1') { - res.set('Content-Type', 'application/atom+xml') return res.send(feed.atom1()).end() } if (format === 'json' || format === 'json1') { - res.set('Content-Type', 'application/json') return res.send(feed.json1()).end() } if (format === 'rss' || format === 'rss2') { - res.set('Content-Type', 'application/rss+xml') return res.send(feed.rss2()).end() } // We're in the ambiguous '.xml' case and we look at the format query parameter if (req.query.format === 'atom' || req.query.format === 'atom1') { - res.set('Content-Type', 'application/atom+xml') return res.send(feed.atom1()).end() } - res.set('Content-Type', 'application/rss+xml') return res.send(feed.rss2()).end() }