feeds.ts 9.46 KB
Newer Older
1
import * as express from 'express'
2
import * as Feed from 'pfeed'
3
import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
Chocobozzz's avatar
Chocobozzz committed
4
import { VideoFilter } from '../../shared/models/videos/video-query.type'
5
6
import { buildNSFWFilter } from '../helpers/express-utils'
import { CONFIG } from '../initializers/config'
Chocobozzz's avatar
Chocobozzz committed
7
import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
8
9
10
import {
  asyncMiddleware,
  commonVideosFiltersValidator,
11
12
13
  feedsFormatValidator,
  setDefaultVideosSort,
  setFeedFormatContentType,
14
15
  videoCommentsFeedsValidator,
  videoFeedsValidator,
16
  videosSortValidator,
Chocobozzz's avatar
Chocobozzz committed
17
  videoSubscriptionFeedsValidator
18
} from '../middlewares'
Chocobozzz's avatar
Chocobozzz committed
19
import { cacheRouteFactory } from '../middlewares/cache/cache'
20
import { VideoModel } from '../models/video/video'
Chocobozzz's avatar
Chocobozzz committed
21
import { VideoCommentModel } from '../models/video/video-comment'
22
23
24

const feedsRouter = express.Router()

Chocobozzz's avatar
Chocobozzz committed
25
26
27
28
const cacheRoute = cacheRouteFactory({
  headerBlacklist: [ 'Content-Type' ]
})

Chocobozzz's avatar
Chocobozzz committed
29
feedsRouter.get('/feeds/video-comments.:format',
30
31
  feedsFormatValidator,
  setFeedFormatContentType,
Chocobozzz's avatar
Chocobozzz committed
32
  cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
33
  asyncMiddleware(videoFeedsValidator),
Chocobozzz's avatar
Chocobozzz committed
34
35
36
37
  asyncMiddleware(videoCommentsFeedsValidator),
  asyncMiddleware(generateVideoCommentsFeed)
)

38
feedsRouter.get('/feeds/videos.:format',
Chocobozzz's avatar
Chocobozzz committed
39
  videosSortValidator,
40
  setDefaultVideosSort,
41
42
  feedsFormatValidator,
  setFeedFormatContentType,
Chocobozzz's avatar
Chocobozzz committed
43
  cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
44
  commonVideosFiltersValidator,
Chocobozzz's avatar
Chocobozzz committed
45
46
  asyncMiddleware(videoFeedsValidator),
  asyncMiddleware(generateVideoFeed)
47
48
)

Rigel Kent's avatar
Rigel Kent committed
49
50
51
52
53
feedsRouter.get('/feeds/subscriptions.:format',
  videosSortValidator,
  setDefaultVideosSort,
  feedsFormatValidator,
  setFeedFormatContentType,
Chocobozzz's avatar
Chocobozzz committed
54
  cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS),
Rigel Kent's avatar
Rigel Kent committed
55
  commonVideosFiltersValidator,
Chocobozzz's avatar
Chocobozzz committed
56
  asyncMiddleware(videoSubscriptionFeedsValidator),
Rigel Kent's avatar
Rigel Kent committed
57
58
59
  asyncMiddleware(generateVideoFeedForSubscriptions)
)

60
61
62
63
64
65
66
67
// ---------------------------------------------------------------------------

export {
  feedsRouter
}

// ---------------------------------------------------------------------------

Chocobozzz's avatar
Chocobozzz committed
68
async function generateVideoCommentsFeed (req: express.Request, res: express.Response) {
Chocobozzz's avatar
Chocobozzz committed
69
  const start = 0
Chocobozzz's avatar
Chocobozzz committed
70
  const video = res.locals.videoAll
71
72
  const account = res.locals.account
  const videoChannel = res.locals.videoChannel
Chocobozzz's avatar
Chocobozzz committed
73

74
75
76
77
78
79
80
  const comments = await VideoCommentModel.listForFeed({
    start,
    count: FEEDS.COUNT,
    videoId: video ? video.id : undefined,
    accountId: account ? account.id : undefined,
    videoChannelId: videoChannel ? videoChannel.id : undefined
  })
Chocobozzz's avatar
Chocobozzz committed
81

82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
  let name: string
  let description: string

  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
  })
101

Chocobozzz's avatar
Chocobozzz committed
102
  // Adding video items to the feed, one at a time
103
  for (const comment of comments) {
Chocobozzz's avatar
Chocobozzz committed
104
    const link = WEBSERVER.URL + comment.getCommentStaticPath()
Chocobozzz's avatar
Chocobozzz committed
105

Chocobozzz's avatar
Chocobozzz committed
106
    let title = comment.Video.name
Chocobozzz's avatar
Chocobozzz committed
107
108
109
110
111
112
113
114
115
    const author: { name: string, link: string }[] = []

    if (comment.Account) {
      title += ` - ${comment.Account.getDisplayName()}`
      author.push({
        name: comment.Account.getDisplayName(),
        link: comment.Account.Actor.url
      })
    }
Chocobozzz's avatar
Chocobozzz committed
116

Chocobozzz's avatar
Chocobozzz committed
117
    feed.addItem({
Chocobozzz's avatar
Chocobozzz committed
118
      title,
Chocobozzz's avatar
Chocobozzz committed
119
      id: comment.url,
Chocobozzz's avatar
Chocobozzz committed
120
      link,
Chocobozzz's avatar
Chocobozzz committed
121
      content: comment.text,
Chocobozzz's avatar
Chocobozzz committed
122
      author,
Chocobozzz's avatar
Chocobozzz committed
123
124
      date: comment.createdAt
    })
125
  }
Chocobozzz's avatar
Chocobozzz committed
126
127
128
129
130

  // Now the feed generation is done, let's send it!
  return sendFeed(feed, req, res)
}

Chocobozzz's avatar
Chocobozzz committed
131
async function generateVideoFeed (req: express.Request, res: express.Response) {
Chocobozzz's avatar
Chocobozzz committed
132
  const start = 0
Chocobozzz's avatar
Chocobozzz committed
133
134
  const account = res.locals.account
  const videoChannel = res.locals.videoChannel
Chocobozzz's avatar
Chocobozzz committed
135
  const nsfw = buildNSFWFilter(res, req.query.nsfw)
136

137
138
139
140
141
142
143
144
145
146
147
148
149
150
  let name: string
  let description: string

  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
  }

151
152
153
154
155
156
  const feed = initFeed({
    name,
    description,
    resourceType: 'videos',
    queryString: new URL(WEBSERVER.URL + req.url).search
  })
157

Rigel Kent's avatar
Rigel Kent committed
158
159
160
161
  const options = {
    accountId: account ? account.id : null,
    videoChannelId: videoChannel ? videoChannel.id : null
  }
162

Chocobozzz's avatar
Chocobozzz committed
163
  const { data } = await VideoModel.listForApi({
Chocobozzz's avatar
Chocobozzz committed
164
    start,
Chocobozzz's avatar
Chocobozzz committed
165
166
    count: FEEDS.COUNT,
    sort: req.query.sort,
167
    includeLocalVideos: true,
Chocobozzz's avatar
Chocobozzz committed
168
    nsfw,
169
    filter: req.query.filter as VideoFilter,
Chocobozzz's avatar
Chocobozzz committed
170
    withFiles: true,
Chocobozzz's avatar
Chocobozzz committed
171
    countVideos: false,
172
    ...options
Chocobozzz's avatar
Chocobozzz committed
173
  })
174

Chocobozzz's avatar
Chocobozzz committed
175
  addVideosToFeed(feed, data)
Rigel Kent's avatar
Rigel Kent committed
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194

  // 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
  })

Chocobozzz's avatar
Chocobozzz committed
195
  const { data } = await VideoModel.listForApi({
Rigel Kent's avatar
Rigel Kent committed
196
197
198
    start,
    count: FEEDS.COUNT,
    sort: req.query.sort,
Chocobozzz's avatar
Chocobozzz committed
199
    includeLocalVideos: false,
Rigel Kent's avatar
Rigel Kent committed
200
201
    nsfw,
    filter: req.query.filter as VideoFilter,
Chocobozzz's avatar
Chocobozzz committed
202

Rigel Kent's avatar
Rigel Kent committed
203
    withFiles: true,
Chocobozzz's avatar
Chocobozzz committed
204
    countVideos: false,
Chocobozzz's avatar
Chocobozzz committed
205
206
207

    followerActorId: res.locals.user.Account.Actor.id,
    user: res.locals.user
Rigel Kent's avatar
Rigel Kent committed
208
209
  })

Chocobozzz's avatar
Chocobozzz committed
210
  addVideosToFeed(feed, data)
Rigel Kent's avatar
Rigel Kent committed
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249

  // Now the feed generation is done, let's send it!
  return sendFeed(feed, req, res)
}

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,
    description,
    // updated: TODO: somehowGetLatestUpdate, // optional, default = today
    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: VideoModel[]) {
250
251
252
  /**
   * Adding video items to the feed object, one at a time
   */
Rigel Kent's avatar
Rigel Kent committed
253
  for (const video of videos) {
Chocobozzz's avatar
Chocobozzz committed
254
    const formattedVideoFiles = video.getFormattedVideoFilesJSON(false)
Chocobozzz's avatar
Chocobozzz committed
255

256
257
258
259
260
    const torrents = formattedVideoFiles.map(videoFile => ({
      title: video.name,
      url: videoFile.torrentUrl,
      size_in_bytes: videoFile.size
    }))
Chocobozzz's avatar
Chocobozzz committed
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281

    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,
282
        label: getCategoryLabel(video.category)
Chocobozzz's avatar
Chocobozzz committed
283
284
      })
    }
285
286
287
288

    feed.addItem({
      title: video.name,
      id: video.url,
289
      link: WEBSERVER.URL + '/w/' + video.uuid,
290
291
292
293
294
295
296
297
298
299
      description: video.getTruncatedDescription(),
      content: video.description,
      author: [
        {
          name: video.VideoChannel.Account.getDisplayName(),
          link: video.VideoChannel.Account.Actor.url
        }
      ],
      date: video.publishedAt,
      nsfw: video.nsfw,
300
      torrent: torrents,
301
302
303
304
305
306
307
308
      videos,
      embed: {
        url: video.getEmbedStaticPath(),
        allowFullscreen: true
      },
      player: {
        url: video.getWatchStaticPath()
      },
Chocobozzz's avatar
Chocobozzz committed
309
      categories,
310
311
312
313
314
      community: {
        statistics: {
          views: video.views
        }
      },
315
316
      thumbnail: [
        {
Chocobozzz's avatar
Chocobozzz committed
317
318
319
          url: WEBSERVER.URL + video.getPreviewStaticPath(),
          height: PREVIEWS_SIZE.height,
          width: PREVIEWS_SIZE.width
320
321
        }
      ]
322
    })
Rigel Kent's avatar
Rigel Kent committed
323
  }
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
}

function sendFeed (feed, req: express.Request, res: express.Response) {
  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()
  }

  // We're in the ambiguous '.xml' case and we look at the format query parameter
  if (req.query.format === 'atom' || req.query.format === 'atom1') {
    return res.send(feed.atom1()).end()
  }

  return res.send(feed.rss2()).end()
}