Merge branch 'master' into develop
[oweals/peertube.git] / server / controllers / feeds.ts
1 import * as express from 'express'
2 import { FEEDS, ROUTE_CACHE_LIFETIME, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants'
3 import {
4   asyncMiddleware,
5   commonVideosFiltersValidator,
6   setDefaultSort,
7   videoCommentsFeedsValidator,
8   videoFeedsValidator,
9   videosSortValidator,
10   feedsFormatValidator,
11   setFeedFormatContentType
12 } from '../middlewares'
13 import { VideoModel } from '../models/video/video'
14 import * as Feed from 'pfeed'
15 import { cacheRoute } from '../middlewares/cache'
16 import { VideoCommentModel } from '../models/video/video-comment'
17 import { buildNSFWFilter } from '../helpers/express-utils'
18 import { CONFIG } from '../initializers/config'
19
20 const feedsRouter = express.Router()
21
22 feedsRouter.get('/feeds/video-comments.:format',
23   feedsFormatValidator,
24   setFeedFormatContentType,
25   asyncMiddleware(cacheRoute({
26     headerBlacklist: [
27       'Content-Type'
28     ]
29   })(ROUTE_CACHE_LIFETIME.FEEDS)),
30   asyncMiddleware(videoCommentsFeedsValidator),
31   asyncMiddleware(generateVideoCommentsFeed)
32 )
33
34 feedsRouter.get('/feeds/videos.:format',
35   videosSortValidator,
36   setDefaultSort,
37   feedsFormatValidator,
38   setFeedFormatContentType,
39   asyncMiddleware(cacheRoute({
40     headerBlacklist: [
41       'Content-Type'
42     ]
43   })(ROUTE_CACHE_LIFETIME.FEEDS)),
44   commonVideosFiltersValidator,
45   asyncMiddleware(videoFeedsValidator),
46   asyncMiddleware(generateVideoFeed)
47 )
48
49 // ---------------------------------------------------------------------------
50
51 export {
52   feedsRouter
53 }
54
55 // ---------------------------------------------------------------------------
56
57 async function generateVideoCommentsFeed (req: express.Request, res: express.Response) {
58   const start = 0
59
60   const video = res.locals.videoAll
61   const videoId: number = video ? video.id : undefined
62
63   const comments = await VideoCommentModel.listForFeed(start, FEEDS.COUNT, videoId)
64
65   const name = video ? video.name : CONFIG.INSTANCE.NAME
66   const description = video ? video.description : CONFIG.INSTANCE.DESCRIPTION
67   const feed = initFeed(name, description)
68
69   // Adding video items to the feed, one at a time
70   for (const comment of comments) {
71     const link = WEBSERVER.URL + comment.getCommentStaticPath()
72
73     let title = comment.Video.name
74     const author: { name: string, link: string }[] = []
75
76     if (comment.Account) {
77       title += ` - ${comment.Account.getDisplayName()}`
78       author.push({
79         name: comment.Account.getDisplayName(),
80         link: comment.Account.Actor.url
81       })
82     }
83
84     feed.addItem({
85       title,
86       id: comment.url,
87       link,
88       content: comment.text,
89       author,
90       date: comment.createdAt
91     })
92   }
93
94   // Now the feed generation is done, let's send it!
95   return sendFeed(feed, req, res)
96 }
97
98 async function generateVideoFeed (req: express.Request, res: express.Response) {
99   const start = 0
100
101   const account = res.locals.account
102   const videoChannel = res.locals.videoChannel
103   const nsfw = buildNSFWFilter(res, req.query.nsfw)
104
105   let name: string
106   let description: string
107
108   if (videoChannel) {
109     name = videoChannel.getDisplayName()
110     description = videoChannel.description
111   } else if (account) {
112     name = account.getDisplayName()
113     description = account.description
114   } else {
115     name = CONFIG.INSTANCE.NAME
116     description = CONFIG.INSTANCE.DESCRIPTION
117   }
118
119   const feed = initFeed(name, description)
120
121   const resultList = await VideoModel.listForApi({
122     start,
123     count: FEEDS.COUNT,
124     sort: req.query.sort,
125     includeLocalVideos: true,
126     nsfw,
127     filter: req.query.filter,
128     withFiles: true,
129     accountId: account ? account.id : null,
130     videoChannelId: videoChannel ? videoChannel.id : null
131   })
132
133   // Adding video items to the feed, one at a time
134   resultList.data.forEach(video => {
135     const formattedVideoFiles = video.getFormattedVideoFilesJSON()
136
137     const torrents = formattedVideoFiles.map(videoFile => ({
138       title: video.name,
139       url: videoFile.torrentUrl,
140       size_in_bytes: videoFile.size
141     }))
142
143     const videos = formattedVideoFiles.map(videoFile => {
144       const result = {
145         type: 'video/mp4',
146         medium: 'video',
147         height: videoFile.resolution.label.replace('p', ''),
148         fileSize: videoFile.size,
149         url: videoFile.fileUrl,
150         framerate: videoFile.fps,
151         duration: video.duration
152       }
153
154       if (video.language) Object.assign(result, { lang: video.language })
155
156       return result
157     })
158
159     const categories: { value: number, label: string }[] = []
160     if (video.category) {
161       categories.push({
162         value: video.category,
163         label: VideoModel.getCategoryLabel(video.category)
164       })
165     }
166
167     feed.addItem({
168       title: video.name,
169       id: video.url,
170       link: WEBSERVER.URL + '/videos/watch/' + video.uuid,
171       description: video.getTruncatedDescription(),
172       content: video.description,
173       author: [
174         {
175           name: video.VideoChannel.Account.getDisplayName(),
176           link: video.VideoChannel.Account.Actor.url
177         }
178       ],
179       date: video.publishedAt,
180       nsfw: video.nsfw,
181       torrent: torrents,
182       videos,
183       embed: {
184         url: video.getEmbedStaticPath(),
185         allowFullscreen: true
186       },
187       player: {
188         url: video.getWatchStaticPath()
189       },
190       categories,
191       community: {
192         statistics: {
193           views: video.views
194         }
195       },
196       thumbnail: [
197         {
198           url: WEBSERVER.URL + video.getMiniatureStaticPath(),
199           height: THUMBNAILS_SIZE.height,
200           width: THUMBNAILS_SIZE.width
201         }
202       ]
203     })
204   })
205
206   // Now the feed generation is done, let's send it!
207   return sendFeed(feed, req, res)
208 }
209
210 function initFeed (name: string, description: string) {
211   const webserverUrl = WEBSERVER.URL
212
213   return new Feed({
214     title: name,
215     description,
216     // updated: TODO: somehowGetLatestUpdate, // optional, default = today
217     id: webserverUrl,
218     link: webserverUrl,
219     image: webserverUrl + '/client/assets/images/icons/icon-96x96.png',
220     favicon: webserverUrl + '/client/assets/images/favicon.png',
221     copyright: `All rights reserved, unless otherwise specified in the terms specified at ${webserverUrl}/about` +
222     ` and potential licenses granted by each content's rightholder.`,
223     generator: `Toraifōsu`, // ^.~
224     feedLinks: {
225       json: `${webserverUrl}/feeds/videos.json`,
226       atom: `${webserverUrl}/feeds/videos.atom`,
227       rss: `${webserverUrl}/feeds/videos.xml`
228     },
229     author: {
230       name: 'Instance admin of ' + CONFIG.INSTANCE.NAME,
231       email: CONFIG.ADMIN.EMAIL,
232       link: `${webserverUrl}/about`
233     }
234   })
235 }
236
237 function sendFeed (feed, req: express.Request, res: express.Response) {
238   const format = req.params.format
239
240   if (format === 'atom' || format === 'atom1') {
241     return res.send(feed.atom1()).end()
242   }
243
244   if (format === 'json' || format === 'json1') {
245     return res.send(feed.json1()).end()
246   }
247
248   if (format === 'rss' || format === 'rss2') {
249     return res.send(feed.rss2()).end()
250   }
251
252   // We're in the ambiguous '.xml' case and we look at the format query parameter
253   if (req.query.format === 'atom' || req.query.format === 'atom1') {
254     return res.send(feed.atom1()).end()
255   }
256
257   return res.send(feed.rss2()).end()
258 }