]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/lib/activitypub/playlist.ts
Stronger actor association typing in AP functions
[github/Chocobozzz/PeerTube.git] / server / lib / activitypub / playlist.ts
index c9b428c92c11083dffd0b6b1519d6340d68e3195..c2e2a3283fe5e9fc16d0f73b3b616bec5a881ea4 100644 (file)
@@ -1,12 +1,12 @@
 import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
 import { crawlCollectionPage } from './crawl'
-import { ACTIVITY_PUB, CONFIG, CRAWL_REQUEST_CONCURRENCY, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers'
+import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
 import { AccountModel } from '../../models/account/account'
 import { isArray } from '../../helpers/custom-validators/misc'
 import { getOrCreateActorAndServerAndModel } from './actor'
 import { logger } from '../../helpers/logger'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
-import { doRequest, downloadImage } from '../../helpers/requests'
+import { doRequest } from '../../helpers/requests'
 import { checkUrlsSameHost } from '../../helpers/activitypub'
 import * as Bluebird from 'bluebird'
 import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
@@ -14,11 +14,13 @@ import { getOrCreateVideoAndAccountAndChannel } from './videos'
 import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
 import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
 import { VideoModel } from '../../models/video/video'
-import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
 import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
-import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
+import { sequelizeTypescript } from '../../initializers/database'
+import { createPlaylistMiniatureFromUrl } from '../thumbnail'
+import { FilteredModelAttributes } from '../../typings/sequelize'
+import { AccountModelId } from '../../typings/models'
 
-function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) {
+function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModelId, to: string[]) {
   const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED
 
   return {
@@ -28,7 +30,9 @@ function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount
     url: playlistObject.id,
     uuid: playlistObject.uuid,
     ownerAccountId: byAccount.id,
-    videoChannelId: null
+    videoChannelId: null,
+    createdAt: new Date(playlistObject.published),
+    updatedAt: new Date(playlistObject.updated)
   }
 }
 
@@ -71,7 +75,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: AccountM
   }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
 }
 
-async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) {
+async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: AccountModelId, to: string[]) {
   const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to)
 
   if (isArray(playlistObject.attributedTo) && playlistObject.attributedTo.length === 1) {
@@ -93,16 +97,52 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
     return Promise.resolve()
   })
 
-  // Empty playlists generally do not have a miniature, so skip it
-  if (accItems.length !== 0) {
+  const refreshedPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(playlist.id, null)
+
+  if (playlistObject.icon) {
     try {
-      await generateThumbnailFromUrl(playlist, playlistObject.icon)
+      const thumbnailModel = await createPlaylistMiniatureFromUrl(playlistObject.icon.url, refreshedPlaylist)
+      await refreshedPlaylist.setAndSaveThumbnail(thumbnailModel, undefined)
     } catch (err) {
       logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err })
     }
+  } else if (refreshedPlaylist.hasThumbnail()) {
+    await refreshedPlaylist.Thumbnail.destroy()
+    refreshedPlaylist.Thumbnail = null
   }
 
-  return resetVideoPlaylistElements(accItems, playlist)
+  return resetVideoPlaylistElements(accItems, refreshedPlaylist)
+}
+
+async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> {
+  if (!videoPlaylist.isOutdated()) return videoPlaylist
+
+  try {
+    const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
+    if (statusCode === 404) {
+      logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
+
+      await videoPlaylist.destroy()
+      return undefined
+    }
+
+    if (playlistObject === undefined) {
+      logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url)
+
+      await videoPlaylist.setAsRefreshed()
+      return videoPlaylist
+    }
+
+    const byAccount = videoPlaylist.OwnerAccount
+    await createOrUpdateVideoPlaylist(playlistObject, byAccount, playlistObject.to)
+
+    return videoPlaylist
+  } catch (err) {
+    logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err })
+
+    await videoPlaylist.setAsRefreshed()
+    return videoPlaylist
+  }
 }
 
 // ---------------------------------------------------------------------------
@@ -111,7 +151,8 @@ export {
   createAccountPlaylists,
   playlistObjectToDBAttributes,
   playlistElementObjectToDBAttributes,
-  createOrUpdateVideoPlaylist
+  createOrUpdateVideoPlaylist,
+  refreshVideoPlaylistIfNeeded
 }
 
 // ---------------------------------------------------------------------------
@@ -155,8 +196,22 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: Vide
   return undefined
 }
 
-function generateThumbnailFromUrl (playlist: VideoPlaylistModel, icon: ActivityIconObject) {
-  const thumbnailName = playlist.getThumbnailName()
+async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
+  const options = {
+    uri: playlistUrl,
+    method: 'GET',
+    json: true,
+    activityPub: true
+  }
+
+  logger.info('Fetching remote playlist %s.', playlistUrl)
+
+  const { response, body } = await doRequest(options)
+
+  if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
+    logger.debug('Remote video playlist JSON is not valid.', { body })
+    return { statusCode: response.statusCode, playlistObject: undefined }
+  }
 
-  return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE)
+  return { statusCode: response.statusCode, playlistObject: body }
 }