]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/controllers/feeds.ts
Fix live constraints tests
[github/Chocobozzz/PeerTube.git] / server / controllers / feeds.ts
CommitLineData
41fb13c3 1import express from 'express'
4393b255
C
2import { Feed } from '@peertube/feed'
3import { extname } from 'path'
c68e2b2d 4import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown'
2760b454 5import { getServerActor } from '@server/models/application/application'
7c3a6636 6import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
3c10840f 7import { VideoInclude } from '@shared/models'
8054669f
C
8import { buildNSFWFilter } from '../helpers/express-utils'
9import { CONFIG } from '../initializers/config'
4393b255 10import { FEEDS, MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
1cd3facc
C
11import {
12 asyncMiddleware,
13 commonVideosFiltersValidator,
8054669f
C
14 feedsFormatValidator,
15 setDefaultVideosSort,
16 setFeedFormatContentType,
1cd3facc
C
17 videoCommentsFeedsValidator,
18 videoFeedsValidator,
afff310e 19 videosSortValidator,
18490b07 20 videoSubscriptionFeedsValidator
1cd3facc 21} from '../middlewares'
20bafcb6 22import { cacheRouteFactory } from '../middlewares/cache/cache'
8054669f 23import { VideoModel } from '../models/video/video'
fe3a55b0 24import { VideoCommentModel } from '../models/video/video-comment'
244e76a5
RK
25
26const feedsRouter = express.Router()
27
20bafcb6
C
28const cacheRoute = cacheRouteFactory({
29 headerBlacklist: [ 'Content-Type' ]
30})
31
fe3a55b0 32feedsRouter.get('/feeds/video-comments.:format',
f2f0eda5
RK
33 feedsFormatValidator,
34 setFeedFormatContentType,
20bafcb6 35 cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
00494d6e 36 asyncMiddleware(videoFeedsValidator),
fe3a55b0
C
37 asyncMiddleware(videoCommentsFeedsValidator),
38 asyncMiddleware(generateVideoCommentsFeed)
39)
40
244e76a5 41feedsRouter.get('/feeds/videos.:format',
7b87d2d5 42 videosSortValidator,
8054669f 43 setDefaultVideosSort,
f2f0eda5
RK
44 feedsFormatValidator,
45 setFeedFormatContentType,
20bafcb6 46 cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
1cd3facc 47 commonVideosFiltersValidator,
fe3a55b0
C
48 asyncMiddleware(videoFeedsValidator),
49 asyncMiddleware(generateVideoFeed)
244e76a5
RK
50)
51
5beb89f2
RK
52feedsRouter.get('/feeds/subscriptions.:format',
53 videosSortValidator,
54 setDefaultVideosSort,
55 feedsFormatValidator,
56 setFeedFormatContentType,
20bafcb6 57 cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
5beb89f2 58 commonVideosFiltersValidator,
18490b07 59 asyncMiddleware(videoSubscriptionFeedsValidator),
5beb89f2
RK
60 asyncMiddleware(generateVideoFeedForSubscriptions)
61)
62
244e76a5
RK
63// ---------------------------------------------------------------------------
64
65export {
66 feedsRouter
67}
68
69// ---------------------------------------------------------------------------
70
dae86118 71async function generateVideoCommentsFeed (req: express.Request, res: express.Response) {
fe3a55b0 72 const start = 0
453e83ea 73 const video = res.locals.videoAll
00494d6e
RK
74 const account = res.locals.account
75 const videoChannel = res.locals.videoChannel
fe3a55b0 76
00494d6e
RK
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 })
fe3a55b0 84
00494d6e
RK
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 })
749c7247 104
fe3a55b0 105 // Adding video items to the feed, one at a time
68b6fd21 106 for (const comment of comments) {
bdb54e6f 107 const localLink = WEBSERVER.URL + comment.getCommentStaticPath()
4dae00e6 108
65d2ae2a 109 let title = comment.Video.name
c3340977
C
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 }
65d2ae2a 119
fe3a55b0 120 feed.addItem({
65d2ae2a 121 title,
bdb54e6f
C
122 id: localLink,
123 link: localLink,
228d8e8e 124 content: toSafeHtml(comment.text),
c3340977 125 author,
fe3a55b0
C
126 date: comment.createdAt
127 })
68b6fd21 128 }
fe3a55b0
C
129
130 // Now the feed generation is done, let's send it!
131 return sendFeed(feed, req, res)
132}
133
dae86118 134async function generateVideoFeed (req: express.Request, res: express.Response) {
4195cd2b 135 const start = 0
dae86118
C
136 const account = res.locals.account
137 const videoChannel = res.locals.videoChannel
d525fc39 138 const nsfw = buildNSFWFilter(res, req.query.nsfw)
244e76a5 139
749c7247
C
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
00494d6e
RK
154 const feed = initFeed({
155 name,
156 description,
157 resourceType: 'videos',
158 queryString: new URL(WEBSERVER.URL + req.url).search
159 })
749c7247 160
5beb89f2
RK
161 const options = {
162 accountId: account ? account.id : null,
163 videoChannelId: videoChannel ? videoChannel.id : null
164 }
afff310e 165
2760b454 166 const server = await getServerActor()
649e8129 167 const { data } = await VideoModel.listForApi({
0626e7af 168 start,
48dce1c9
C
169 count: FEEDS.COUNT,
170 sort: req.query.sort,
2760b454
C
171 displayOnlyForFollower: {
172 actorId: server.id,
173 orLocalVideos: true
174 },
d525fc39 175 nsfw,
2760b454 176 isLocal: req.query.isLocal,
3c10840f
C
177 include: req.query.include | VideoInclude.FILES,
178 hasFiles: true,
649e8129 179 countVideos: false,
afff310e 180 ...options
48dce1c9 181 })
244e76a5 182
649e8129 183 addVideosToFeed(feed, data)
5beb89f2
RK
184
185 // Now the feed generation is done, let's send it!
186 return sendFeed(feed, req, res)
187}
188
189async 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
649e8129 203 const { data } = await VideoModel.listForApi({
5beb89f2
RK
204 start,
205 count: FEEDS.COUNT,
206 sort: req.query.sort,
5beb89f2 207 nsfw,
2760b454
C
208
209 isLocal: req.query.isLocal,
649e8129 210
3c10840f
C
211 hasFiles: true,
212 include: req.query.include | VideoInclude.FILES,
213
649e8129 214 countVideos: false,
18490b07 215
2760b454
C
216 displayOnlyForFollower: {
217 actorId: res.locals.user.Account.Actor.id,
218 orLocalVideos: false
219 },
18490b07 220 user: res.locals.user
5beb89f2
RK
221 })
222
649e8129 223 addVideosToFeed(feed, data)
5beb89f2
RK
224
225 // Now the feed generation is done, let's send it!
226 return sendFeed(feed, req, res)
227}
228
229function 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,
c68e2b2d 240 description: mdToOneLinePlainText(description),
5beb89f2
RK
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
4393b255 262function addVideosToFeed (feed: Feed, videos: VideoModel[]) {
5beb89f2 263 for (const video of videos) {
f66db4d5 264 const formattedVideoFiles = video.getFormattedVideoFilesJSON(false)
c4b4ab71 265
244e76a5
RK
266 const torrents = formattedVideoFiles.map(videoFile => ({
267 title: video.name,
268 url: videoFile.torrentUrl,
269 size_in_bytes: videoFile.size
270 }))
c4b4ab71 271
bdb54e6f 272 const videoFiles = formattedVideoFiles.map(videoFile => {
c4b4ab71 273 const result = {
4393b255 274 type: MIMETYPES.VIDEO.EXT_MIMETYPE[extname(videoFile.fileUrl)],
c4b4ab71 275 medium: 'video',
4393b255 276 height: videoFile.resolution.id,
c4b4ab71
C
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,
7c3a6636 292 label: getCategoryLabel(video.category)
c4b4ab71
C
293 })
294 }
244e76a5 295
bdb54e6f
C
296 const localLink = WEBSERVER.URL + video.getWatchStaticPath()
297
244e76a5
RK
298 feed.addItem({
299 title: video.name,
bdb54e6f
C
300 id: localLink,
301 link: localLink,
c68e2b2d 302 description: mdToOneLinePlainText(video.getTruncatedDescription()),
228d8e8e 303 content: toSafeHtml(video.description),
244e76a5
RK
304 author: [
305 {
306 name: video.VideoChannel.Account.getDisplayName(),
307 link: video.VideoChannel.Account.Actor.url
308 }
309 ],
310 date: video.publishedAt,
244e76a5 311 nsfw: video.nsfw,
4393b255
C
312 torrents,
313
314 // Enclosure
315 video: {
bdb54e6f
C
316 url: videoFiles[0].url,
317 length: videoFiles[0].fileSize,
318 type: videoFiles[0].type
4393b255
C
319 },
320
321 // Media RSS
bdb54e6f 322 videos: videoFiles,
4393b255 323
16d9224a 324 embed: {
bdb54e6f 325 url: WEBSERVER.URL + video.getEmbedStaticPath(),
16d9224a
RK
326 allowFullscreen: true
327 },
328 player: {
bdb54e6f 329 url: WEBSERVER.URL + video.getWatchStaticPath()
16d9224a 330 },
c4b4ab71 331 categories,
16d9224a
RK
332 community: {
333 statistics: {
334 views: video.views
335 }
336 },
4393b255 337 thumbnails: [
b81eb8fd 338 {
5a2c0f0c
C
339 url: WEBSERVER.URL + video.getPreviewStaticPath(),
340 height: PREVIEWS_SIZE.height,
341 width: PREVIEWS_SIZE.width
b81eb8fd
RK
342 }
343 ]
244e76a5 344 })
5beb89f2 345 }
244e76a5
RK
346}
347
4393b255 348function sendFeed (feed: Feed, req: express.Request, res: express.Response) {
244e76a5
RK
349 const format = req.params.format
350
351 if (format === 'atom' || format === 'atom1') {
244e76a5
RK
352 return res.send(feed.atom1()).end()
353 }
354
355 if (format === 'json' || format === 'json1') {
244e76a5
RK
356 return res.send(feed.json1()).end()
357 }
358
359 if (format === 'rss' || format === 'rss2') {
244e76a5
RK
360 return res.send(feed.rss2()).end()
361 }
362
363 // We're in the ambiguous '.xml' case and we look at the format query parameter
364 if (req.query.format === 'atom' || req.query.format === 'atom1') {
244e76a5
RK
365 return res.send(feed.atom1()).end()
366 }
367
244e76a5
RK
368 return res.send(feed.rss2()).end()
369}