]>
Commit | Line | Data |
---|---|---|
418d092a C |
1 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
2 | import { crawlCollectionPage } from './crawl' | |
3 | import { ACTIVITY_PUB, CONFIG, CRAWL_REQUEST_CONCURRENCY, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers' | |
4 | import { AccountModel } from '../../models/account/account' | |
5 | import { isArray } from '../../helpers/custom-validators/misc' | |
6 | import { getOrCreateActorAndServerAndModel } from './actor' | |
7 | import { logger } from '../../helpers/logger' | |
8 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | |
9 | import { doRequest, downloadImage } from '../../helpers/requests' | |
10 | import { checkUrlsSameHost } from '../../helpers/activitypub' | |
11 | import * as Bluebird from 'bluebird' | |
12 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | |
13 | import { getOrCreateVideoAndAccountAndChannel } from './videos' | |
14 | import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' | |
15 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | |
16 | import { VideoModel } from '../../models/video/video' | |
17 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | |
18 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | |
19 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | |
20 | ||
21 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) { | |
22 | const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED | |
23 | ||
24 | return { | |
25 | name: playlistObject.name, | |
26 | description: playlistObject.content, | |
27 | privacy, | |
28 | url: playlistObject.id, | |
29 | uuid: playlistObject.uuid, | |
30 | ownerAccountId: byAccount.id, | |
df0b219d C |
31 | videoChannelId: null, |
32 | createdAt: new Date(playlistObject.published), | |
33 | updatedAt: new Date(playlistObject.updated) | |
418d092a C |
34 | } |
35 | } | |
36 | ||
37 | function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: VideoPlaylistModel, video: VideoModel) { | |
38 | return { | |
39 | position: elementObject.position, | |
40 | url: elementObject.id, | |
41 | startTimestamp: elementObject.startTimestamp || null, | |
42 | stopTimestamp: elementObject.stopTimestamp || null, | |
43 | videoPlaylistId: videoPlaylist.id, | |
44 | videoId: video.id | |
45 | } | |
46 | } | |
47 | ||
48 | async function createAccountPlaylists (playlistUrls: string[], account: AccountModel) { | |
49 | await Bluebird.map(playlistUrls, async playlistUrl => { | |
50 | try { | |
51 | const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl) | |
52 | if (exists === true) return | |
53 | ||
54 | // Fetch url | |
55 | const { body } = await doRequest<PlaylistObject>({ | |
56 | uri: playlistUrl, | |
57 | json: true, | |
58 | activityPub: true | |
59 | }) | |
60 | ||
61 | if (!isPlaylistObjectValid(body)) { | |
62 | throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) | |
63 | } | |
64 | ||
65 | if (!isArray(body.to)) { | |
66 | throw new Error('Playlist does not have an audience.') | |
67 | } | |
68 | ||
69 | return createOrUpdateVideoPlaylist(body, account, body.to) | |
70 | } catch (err) { | |
71 | logger.warn('Cannot add playlist element %s.', playlistUrl, { err }) | |
72 | } | |
73 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | |
74 | } | |
75 | ||
76 | async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) { | |
77 | const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to) | |
78 | ||
79 | if (isArray(playlistObject.attributedTo) && playlistObject.attributedTo.length === 1) { | |
80 | const actor = await getOrCreateActorAndServerAndModel(playlistObject.attributedTo[0]) | |
81 | ||
82 | if (actor.VideoChannel) { | |
83 | playlistAttributes.videoChannelId = actor.VideoChannel.id | |
84 | } else { | |
85 | logger.warn('Attributed to of video playlist %s is not a video channel.', playlistObject.id, { playlistObject }) | |
86 | } | |
87 | } | |
88 | ||
89 | const [ playlist ] = await VideoPlaylistModel.upsert<VideoPlaylistModel>(playlistAttributes, { returning: true }) | |
90 | ||
91 | let accItems: string[] = [] | |
92 | await crawlCollectionPage<string>(playlistObject.id, items => { | |
93 | accItems = accItems.concat(items) | |
94 | ||
95 | return Promise.resolve() | |
96 | }) | |
97 | ||
98 | // Empty playlists generally do not have a miniature, so skip it | |
99 | if (accItems.length !== 0) { | |
100 | try { | |
101 | await generateThumbnailFromUrl(playlist, playlistObject.icon) | |
102 | } catch (err) { | |
103 | logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) | |
104 | } | |
105 | } | |
106 | ||
107 | return resetVideoPlaylistElements(accItems, playlist) | |
108 | } | |
109 | ||
110 | // --------------------------------------------------------------------------- | |
111 | ||
112 | export { | |
113 | createAccountPlaylists, | |
114 | playlistObjectToDBAttributes, | |
115 | playlistElementObjectToDBAttributes, | |
116 | createOrUpdateVideoPlaylist | |
117 | } | |
118 | ||
119 | // --------------------------------------------------------------------------- | |
120 | ||
121 | async function resetVideoPlaylistElements (elementUrls: string[], playlist: VideoPlaylistModel) { | |
122 | const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = [] | |
123 | ||
124 | await Bluebird.map(elementUrls, async elementUrl => { | |
125 | try { | |
126 | // Fetch url | |
127 | const { body } = await doRequest<PlaylistElementObject>({ | |
128 | uri: elementUrl, | |
129 | json: true, | |
130 | activityPub: true | |
131 | }) | |
132 | ||
133 | if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) | |
134 | ||
135 | if (checkUrlsSameHost(body.id, elementUrl) !== true) { | |
136 | throw new Error(`Playlist element url ${elementUrl} host is different from the AP object id ${body.id}`) | |
137 | } | |
138 | ||
139 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: { id: body.url }, fetchType: 'only-video' }) | |
140 | ||
141 | elementsToCreate.push(playlistElementObjectToDBAttributes(body, playlist, video)) | |
142 | } catch (err) { | |
143 | logger.warn('Cannot add playlist element %s.', elementUrl, { err }) | |
144 | } | |
145 | }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) | |
146 | ||
147 | await sequelizeTypescript.transaction(async t => { | |
148 | await VideoPlaylistElementModel.deleteAllOf(playlist.id, t) | |
149 | ||
150 | for (const element of elementsToCreate) { | |
151 | await VideoPlaylistElementModel.create(element, { transaction: t }) | |
152 | } | |
153 | }) | |
154 | ||
155 | logger.info('Reset playlist %s with %s elements.', playlist.url, elementsToCreate.length) | |
156 | ||
157 | return undefined | |
158 | } | |
159 | ||
160 | function generateThumbnailFromUrl (playlist: VideoPlaylistModel, icon: ActivityIconObject) { | |
161 | const thumbnailName = playlist.getThumbnailName() | |
162 | ||
163 | return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) | |
164 | } |