feeds.ts 9.5 KB

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