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 { VideoCommentModel } from '../models/video/video-comment' ; const feedsRouter = express . Router ( ) ; const cacheRoute = cacheRouteFactory ( { headerBlacklist : [ 'Content-Type' ] } ) ; feedsRouter . get ( '/feeds/video-comments.:format' , feedsFormatValidator , setFeedFormatContentType , cacheRoute ( ROUTE_CACHE_LIFETIME . FEEDS ) , asyncMiddleware ( videoFeedsValidator ) , asyncMiddleware ( videoCommentsFeedsValidator ) , asyncMiddleware ( generateVideoCommentsFeed ) ) ; feedsRouter . get ( '/feeds/videos.:format' , videosSortValidator , 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 { feedsRouter } ; async function generateVideoCommentsFeed ( req , res ) { const start = 0 ; const video = res . locals . videoAll ; const account = res . locals . account ; const videoChannel = res . locals . videoChannel ; const comments = await VideoCommentModel . listForFeed ( { start , count : FEEDS . COUNT , videoId : video ? video . id : undefined , accountId : account ? account . id : undefined , videoChannelId : videoChannel ? videoChannel . id : undefined } ) ; let name ; let 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 } ) ; for ( const comment of comments ) { const link = WEBSERVER . URL + comment . getCommentStaticPath ( ) ; let title = comment . Video . name ; const author = [ ] ; if ( comment . Account ) { title += ` - ${ comment . Account . getDisplayName ( ) }` ; author . push ( { name : comment . Account . getDisplayName ( ) , link : comment . Account . Actor . url } ) ; } feed . addItem ( { title , id : comment . url , link , content : comment . text , author , date : comment . createdAt } ) ; } return sendFeed ( feed , req , res ) ; } async function generateVideoFeed ( req , res ) { const start = 0 ; const account = res . locals . account ; const videoChannel = res . locals . videoChannel ; const nsfw = buildNSFWFilter ( res , req . query . nsfw ) ; let name ; let description ; if ( videoChannel ) { name = videoChannel . getDisplayName ( ) ; description = videoChannel . description ; } else if ( account ) { name = account . getDisplayName ( ) ; description = account . description ; } else { name = CONFIG . INSTANCE . NAME ; description = CONFIG . INSTANCE . 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 server = await getServerActor ( ) ; const { data } = await VideoModel . listForApi ( { start , count : FEEDS . COUNT , sort : req . query . sort , displayOnlyForFollower : { actorId : server . id , orLocalVideos : true } , nsfw , isLocal : req . query . isLocal , include : req . query . include | VideoInclude . FILES , hasFiles : true , countVideos : false , ... options } ) ; addVideosToFeed ( feed , data ) ; return sendFeed ( feed , req , res ) ; } async function generateVideoFeedForSubscriptions ( req , res ) { 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 ) ; return sendFeed ( feed , req , res ) ; } function initFeed ( parameters ) { const webserverUrl = WEBSERVER . URL ; const { name , description , resourceType , queryString } = parameters ; return new Feed ( { title : name , description , id : webserverUrl , link : webserverUrl , image : webserverUrl + '/client/assets/images/icons/icon-96x96.png' , favicon : webserverUrl + '/client/assets/images/favicon.png' , copyright : `All rights reserved, unless otherwise specified in the terms specified at ${ webserverUrl }/about` + ` and potential licenses granted by each content's rightholder.` , generator : `Toraifōsu` , feedLinks : { 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 , email : CONFIG . ADMIN . EMAIL , link : `${ webserverUrl }/about` } } ) ; } function addVideosToFeed ( feed , videos ) { /**
     * 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 = [ ] ; 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 , res ) { const format = req . params . format ; if ( format === 'atom' || format === 'atom1' ) { return res . send ( feed . atom1 ( ) ) . end ( ) ; } if ( format === 'json' || format === 'json1' ) { return res . send ( feed . json1 ( ) ) . end ( ) ; } if ( format === 'rss' || format === 'rss2' ) { return res . send ( feed . rss2 ( ) ) . end ( ) ; } if ( req . query . format === 'atom' || req . query . format === 'atom1' ) { return res . send ( feed . atom1 ( ) ) . end ( ) ; } return res . send ( feed . rss2 ( ) ) . end ( ) ; }