]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/controllers/feeds.ts
Move to peertube feed fork
[github/Chocobozzz/PeerTube.git] / server / controllers / feeds.ts
1 import express from 'express'
2 import { Feed } from '@peertube/feed'
3 import { extname } from 'path'
4 import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown'
5 import { getServerActor } from '@server/models/application/application'
6 import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
7 import { VideoInclude } from '@shared/models'
8 import { buildNSFWFilter } from '../helpers/express-utils'
9 import { CONFIG } from '../initializers/config'
10 import { FEEDS, MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
11 import {
12 asyncMiddleware,
13 commonVideosFiltersValidator,
14 feedsFormatValidator,
15 setDefaultVideosSort,
16 setFeedFormatContentType,
17 videoCommentsFeedsValidator,
18 videoFeedsValidator,
19 videosSortValidator,
20 videoSubscriptionFeedsValidator
21 } from '../middlewares'
22 import { cacheRouteFactory } from '../middlewares/cache/cache'
23 import { VideoModel } from '../models/video/video'
24 import { VideoCommentModel } from '../models/video/video-comment'
25
26 const feedsRouter = express.Router()
27
28 const cacheRoute = cacheRouteFactory({
29 headerBlacklist: [ 'Content-Type' ]
30 })
31
32 feedsRouter.get('/feeds/video-comments.:format',
33 feedsFormatValidator,
34 setFeedFormatContentType,
35 cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
36 asyncMiddleware(videoFeedsValidator),
37 asyncMiddleware(videoCommentsFeedsValidator),
38 asyncMiddleware(generateVideoCommentsFeed)
39 )
40
41 feedsRouter.get('/feeds/videos.:format',
42 videosSortValidator,
43 setDefaultVideosSort,
44 feedsFormatValidator,
45 setFeedFormatContentType,
46 cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
47 commonVideosFiltersValidator,
48 asyncMiddleware(videoFeedsValidator),
49 asyncMiddleware(generateVideoFeed)
50 )
51
52 feedsRouter.get('/feeds/subscriptions.:format',
53 videosSortValidator,
54 setDefaultVideosSort,
55 feedsFormatValidator,
56 setFeedFormatContentType,
57 cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
58 commonVideosFiltersValidator,
59 asyncMiddleware(videoSubscriptionFeedsValidator),
60 asyncMiddleware(generateVideoFeedForSubscriptions)
61 )
62
63 // ---------------------------------------------------------------------------
64
65 export {
66 feedsRouter
67 }
68
69 // ---------------------------------------------------------------------------
70
71 async function generateVideoCommentsFeed (req: express.Request, res: express.Response) {
72 const start = 0
73 const video = res.locals.videoAll
74 const account = res.locals.account
75 const videoChannel = res.locals.videoChannel
76
77 const comments = await VideoCommentModel.listForFeed({
78 start,
79 count: FEEDS.COUNT,
80 videoId: video ? video.id : undefined,
81 accountId: account ? account.id : undefined,
82 videoChannelId: videoChannel ? videoChannel.id : undefined
83 })
84
85 let name: string
86 let description: string
87
88 if (videoChannel) {
89 name = videoChannel.getDisplayName()
90 description = videoChannel.description
91 } else if (account) {
92 name = account.getDisplayName()
93 description = account.description
94 } else {
95 name = video ? video.name : CONFIG.INSTANCE.NAME
96 description = video ? video.description : CONFIG.INSTANCE.DESCRIPTION
97 }
98 const feed = initFeed({
99 name,
100 description,
101 resourceType: 'video-comments',
102 queryString: new URL(WEBSERVER.URL + req.originalUrl).search
103 })
104
105 // Adding video items to the feed, one at a time
106 for (const comment of comments) {
107 const link = WEBSERVER.URL + comment.getCommentStaticPath()
108
109 let title = comment.Video.name
110 const author: { name: string, link: string }[] = []
111
112 if (comment.Account) {
113 title += ` - ${comment.Account.getDisplayName()}`
114 author.push({
115 name: comment.Account.getDisplayName(),
116 link: comment.Account.Actor.url
117 })
118 }
119
120 feed.addItem({
121 title,
122 id: comment.url,
123 link,
124 content: toSafeHtml(comment.text),
125 author,
126 date: comment.createdAt
127 })
128 }
129
130 // Now the feed generation is done, let's send it!
131 return sendFeed(feed, req, res)
132 }
133
134 async function generateVideoFeed (req: express.Request, res: express.Response) {
135 const start = 0
136 const account = res.locals.account
137 const videoChannel = res.locals.videoChannel
138 const nsfw = buildNSFWFilter(res, req.query.nsfw)
139
140 let name: string
141 let description: string
142
143 if (videoChannel) {
144 name = videoChannel.getDisplayName()
145 description = videoChannel.description
146 } else if (account) {
147 name = account.getDisplayName()
148 description = account.description
149 } else {
150 name = CONFIG.INSTANCE.NAME
151 description = CONFIG.INSTANCE.DESCRIPTION
152 }
153
154 const feed = initFeed({
155 name,
156 description,
157 resourceType: 'videos',
158 queryString: new URL(WEBSERVER.URL + req.url).search
159 })
160
161 const options = {
162 accountId: account ? account.id : null,
163 videoChannelId: videoChannel ? videoChannel.id : null
164 }
165
166 const server = await getServerActor()
167 const { data } = await VideoModel.listForApi({
168 start,
169 count: FEEDS.COUNT,
170 sort: req.query.sort,
171 displayOnlyForFollower: {
172 actorId: server.id,
173 orLocalVideos: true
174 },
175 nsfw,
176 isLocal: req.query.isLocal,
177 include: req.query.include | VideoInclude.FILES,
178 hasFiles: true,
179 countVideos: false,
180 ...options
181 })
182
183 addVideosToFeed(feed, data)
184
185 // Now the feed generation is done, let's send it!
186 return sendFeed(feed, req, res)
187 }
188
189 async function generateVideoFeedForSubscriptions (req: express.Request, res: express.Response) {
190 const start = 0
191 const account = res.locals.account
192 const nsfw = buildNSFWFilter(res, req.query.nsfw)
193 const name = account.getDisplayName()
194 const description = account.description
195
196 const feed = initFeed({
197 name,
198 description,
199 resourceType: 'videos',
200 queryString: new URL(WEBSERVER.URL + req.url).search
201 })
202
203 const { data } = await VideoModel.listForApi({
204 start,
205 count: FEEDS.COUNT,
206 sort: req.query.sort,
207 nsfw,
208
209 isLocal: req.query.isLocal,
210
211 hasFiles: true,
212 include: req.query.include | VideoInclude.FILES,
213
214 countVideos: false,
215
216 displayOnlyForFollower: {
217 actorId: res.locals.user.Account.Actor.id,
218 orLocalVideos: false
219 },
220 user: res.locals.user
221 })
222
223 addVideosToFeed(feed, data)
224
225 // Now the feed generation is done, let's send it!
226 return sendFeed(feed, req, res)
227 }
228
229 function initFeed (parameters: {
230 name: string
231 description: string
232 resourceType?: 'videos' | 'video-comments'
233 queryString?: string
234 }) {
235 const webserverUrl = WEBSERVER.URL
236 const { name, description, resourceType, queryString } = parameters
237
238 return new Feed({
239 title: name,
240 description: mdToOneLinePlainText(description),
241 // updated: TODO: somehowGetLatestUpdate, // optional, default = today
242 id: webserverUrl,
243 link: webserverUrl,
244 image: webserverUrl + '/client/assets/images/icons/icon-96x96.png',
245 favicon: webserverUrl + '/client/assets/images/favicon.png',
246 copyright: `All rights reserved, unless otherwise specified in the terms specified at ${webserverUrl}/about` +
247 ` and potential licenses granted by each content's rightholder.`,
248 generator: `Toraifōsu`, // ^.~
249 feedLinks: {
250 json: `${webserverUrl}/feeds/${resourceType}.json${queryString}`,
251 atom: `${webserverUrl}/feeds/${resourceType}.atom${queryString}`,
252 rss: `${webserverUrl}/feeds/${resourceType}.xml${queryString}`
253 },
254 author: {
255 name: 'Instance admin of ' + CONFIG.INSTANCE.NAME,
256 email: CONFIG.ADMIN.EMAIL,
257 link: `${webserverUrl}/about`
258 }
259 })
260 }
261
262 function addVideosToFeed (feed: Feed, videos: VideoModel[]) {
263 for (const video of videos) {
264 const formattedVideoFiles = video.getFormattedVideoFilesJSON(false)
265
266 const torrents = formattedVideoFiles.map(videoFile => ({
267 title: video.name,
268 url: videoFile.torrentUrl,
269 size_in_bytes: videoFile.size
270 }))
271
272 const videos = formattedVideoFiles.map(videoFile => {
273 const result = {
274 type: MIMETYPES.VIDEO.EXT_MIMETYPE[extname(videoFile.fileUrl)],
275 medium: 'video',
276 height: videoFile.resolution.id,
277 fileSize: videoFile.size,
278 url: videoFile.fileUrl,
279 framerate: videoFile.fps,
280 duration: video.duration
281 }
282
283 if (video.language) Object.assign(result, { lang: video.language })
284
285 return result
286 })
287
288 const categories: { value: number, label: string }[] = []
289 if (video.category) {
290 categories.push({
291 value: video.category,
292 label: getCategoryLabel(video.category)
293 })
294 }
295
296 feed.addItem({
297 title: video.name,
298 id: video.url,
299 link: WEBSERVER.URL + video.getWatchStaticPath(),
300 description: mdToOneLinePlainText(video.getTruncatedDescription()),
301 content: toSafeHtml(video.description),
302 author: [
303 {
304 name: video.VideoChannel.Account.getDisplayName(),
305 link: video.VideoChannel.Account.Actor.url
306 }
307 ],
308 date: video.publishedAt,
309 nsfw: video.nsfw,
310 torrents,
311
312 // Enclosure
313 video: {
314 url: videos[0].url,
315 length: videos[0].fileSize,
316 type: videos[0].type
317 },
318
319 // Media RSS
320 videos,
321
322 embed: {
323 url: video.getEmbedStaticPath(),
324 allowFullscreen: true
325 },
326 player: {
327 url: video.getWatchStaticPath()
328 },
329 categories,
330 community: {
331 statistics: {
332 views: video.views
333 }
334 },
335 thumbnails: [
336 {
337 url: WEBSERVER.URL + video.getPreviewStaticPath(),
338 height: PREVIEWS_SIZE.height,
339 width: PREVIEWS_SIZE.width
340 }
341 ]
342 })
343 }
344 }
345
346 function sendFeed (feed: Feed, req: express.Request, res: express.Response) {
347 const format = req.params.format
348
349 if (format === 'atom' || format === 'atom1') {
350 return res.send(feed.atom1()).end()
351 }
352
353 if (format === 'json' || format === 'json1') {
354 return res.send(feed.json1()).end()
355 }
356
357 if (format === 'rss' || format === 'rss2') {
358 return res.send(feed.rss2()).end()
359 }
360
361 // We're in the ambiguous '.xml' case and we look at the format query parameter
362 if (req.query.format === 'atom' || req.query.format === 'atom1') {
363 return res.send(feed.atom1()).end()
364 }
365
366 return res.send(feed.rss2()).end()
367 }