feeds.ts 7.0 KB

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