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