create-update.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. import Bluebird from 'bluebird'
  2. import { isArray } from '@server/helpers/custom-validators/misc.js'
  3. import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
  4. import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
  5. import { CRAWL_REQUEST_CONCURRENCY } from '@server/initializers/constants.js'
  6. import { sequelizeTypescript } from '@server/initializers/database.js'
  7. import { updateRemotePlaylistMiniatureFromUrl } from '@server/lib/thumbnail.js'
  8. import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element.js'
  9. import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
  10. import { FilteredModelAttributes } from '@server/types/index.js'
  11. import { MThumbnail, MVideoPlaylist, MVideoPlaylistFull, MVideoPlaylistVideosLength } from '@server/types/models/index.js'
  12. import { PlaylistObject } from '@peertube/peertube-models'
  13. import { AttributesOnly } from '@peertube/peertube-typescript-utils'
  14. import { getAPId } from '../activity.js'
  15. import { getOrCreateAPActor } from '../actors/index.js'
  16. import { crawlCollectionPage } from '../crawl.js'
  17. import { getOrCreateAPVideo } from '../videos/index.js'
  18. import {
  19. fetchRemotePlaylistElement,
  20. fetchRemoteVideoPlaylist,
  21. playlistElementObjectToDBAttributes,
  22. playlistObjectToDBAttributes
  23. } from './shared/index.js'
  24. const lTags = loggerTagsFactory('ap', 'video-playlist')
  25. async function createAccountPlaylists (playlistUrls: string[]) {
  26. await Bluebird.map(playlistUrls, async playlistUrl => {
  27. try {
  28. const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl)
  29. if (exists === true) return
  30. const { playlistObject } = await fetchRemoteVideoPlaylist(playlistUrl)
  31. if (playlistObject === undefined) {
  32. throw new Error(`Cannot refresh remote playlist ${playlistUrl}: invalid body.`)
  33. }
  34. return createOrUpdateVideoPlaylist(playlistObject)
  35. } catch (err) {
  36. logger.warn('Cannot add playlist element %s.', playlistUrl, { err, ...lTags(playlistUrl) })
  37. }
  38. }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
  39. }
  40. async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, to?: string[]) {
  41. const playlistAttributes = playlistObjectToDBAttributes(playlistObject, to || playlistObject.to)
  42. await setVideoChannel(playlistObject, playlistAttributes)
  43. const [ upsertPlaylist ] = await VideoPlaylistModel.upsert<MVideoPlaylistVideosLength>(playlistAttributes, { returning: true })
  44. const playlistElementUrls = await fetchElementUrls(playlistObject)
  45. // Refetch playlist from DB since elements fetching could be long in time
  46. const playlist = await VideoPlaylistModel.loadWithAccountAndChannel(upsertPlaylist.id, null)
  47. await updatePlaylistThumbnail(playlistObject, playlist)
  48. const elementsLength = await rebuildVideoPlaylistElements(playlistElementUrls, playlist)
  49. playlist.setVideosLength(elementsLength)
  50. return playlist
  51. }
  52. // ---------------------------------------------------------------------------
  53. export {
  54. createAccountPlaylists,
  55. createOrUpdateVideoPlaylist
  56. }
  57. // ---------------------------------------------------------------------------
  58. async function setVideoChannel (playlistObject: PlaylistObject, playlistAttributes: AttributesOnly<VideoPlaylistModel>) {
  59. if (!isArray(playlistObject.attributedTo) || playlistObject.attributedTo.length !== 1) {
  60. throw new Error('Not attributed to for playlist object ' + getAPId(playlistObject))
  61. }
  62. const actor = await getOrCreateAPActor(getAPId(playlistObject.attributedTo[0]), 'all')
  63. if (!actor.VideoChannel) {
  64. logger.warn('Playlist "attributedTo" %s is not a video channel.', playlistObject.id, { playlistObject, ...lTags(playlistObject.id) })
  65. return
  66. }
  67. playlistAttributes.videoChannelId = actor.VideoChannel.id
  68. playlistAttributes.ownerAccountId = actor.VideoChannel.Account.id
  69. }
  70. async function fetchElementUrls (playlistObject: PlaylistObject) {
  71. let accItems: string[] = []
  72. await crawlCollectionPage<string>(playlistObject.id, items => {
  73. accItems = accItems.concat(items)
  74. return Promise.resolve()
  75. })
  76. return accItems
  77. }
  78. async function updatePlaylistThumbnail (playlistObject: PlaylistObject, playlist: MVideoPlaylistFull) {
  79. if (playlistObject.icon) {
  80. let thumbnailModel: MThumbnail
  81. try {
  82. thumbnailModel = await updateRemotePlaylistMiniatureFromUrl({ downloadUrl: playlistObject.icon.url, playlist })
  83. await playlist.setAndSaveThumbnail(thumbnailModel, undefined)
  84. } catch (err) {
  85. logger.warn('Cannot set thumbnail of %s.', playlistObject.id, { err, ...lTags(playlistObject.id, playlist.uuid, playlist.url) })
  86. if (thumbnailModel) await thumbnailModel.removeThumbnail()
  87. }
  88. return
  89. }
  90. // Playlist does not have an icon, destroy existing one
  91. if (playlist.hasThumbnail()) {
  92. await playlist.Thumbnail.destroy()
  93. playlist.Thumbnail = null
  94. }
  95. }
  96. async function rebuildVideoPlaylistElements (elementUrls: string[], playlist: MVideoPlaylist) {
  97. const elementsToCreate = await buildElementsDBAttributes(elementUrls, playlist)
  98. await retryTransactionWrapper(() => sequelizeTypescript.transaction(async t => {
  99. await VideoPlaylistElementModel.deleteAllOf(playlist.id, t)
  100. for (const element of elementsToCreate) {
  101. await VideoPlaylistElementModel.create(element, { transaction: t })
  102. }
  103. }))
  104. logger.info('Rebuilt playlist %s with %s elements.', playlist.url, elementsToCreate.length, lTags(playlist.uuid, playlist.url))
  105. return elementsToCreate.length
  106. }
  107. async function buildElementsDBAttributes (elementUrls: string[], playlist: MVideoPlaylist) {
  108. const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = []
  109. await Bluebird.map(elementUrls, async elementUrl => {
  110. try {
  111. const { elementObject } = await fetchRemotePlaylistElement(elementUrl)
  112. const { video } = await getOrCreateAPVideo({ videoObject: { id: elementObject.url }, fetchType: 'only-video-and-blacklist' })
  113. elementsToCreate.push(playlistElementObjectToDBAttributes(elementObject, playlist, video))
  114. } catch (err) {
  115. logger.warn('Cannot add playlist element %s.', elementUrl, { err, ...lTags(playlist.uuid, playlist.url) })
  116. }
  117. }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
  118. return elementsToCreate
  119. }