]>
Commit | Line | Data |
---|---|---|
2ccaeeb3 C |
1 | import * as Bluebird from 'bluebird' |
2 | import * as magnetUtil from 'magnet-uri' | |
892211e8 C |
3 | import { join } from 'path' |
4 | import * as request from 'request' | |
5 | import { ActivityIconObject } from '../../../shared/index' | |
2ccaeeb3 C |
6 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
7 | import { VideoPrivacy } from '../../../shared/models/videos' | |
8 | import { isVideoTorrentObjectValid } from '../../helpers/custom-validators/activitypub/videos' | |
9 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | |
10 | import { retryTransactionWrapper } from '../../helpers/database-utils' | |
11 | import { logger } from '../../helpers/logger' | |
da854ddd | 12 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' |
2ccaeeb3 C |
13 | import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, STATIC_PATHS, VIDEO_MIMETYPE_EXT } from '../../initializers' |
14 | import { ActorModel } from '../../models/activitypub/actor' | |
15 | import { TagModel } from '../../models/video/tag' | |
3fd3ab2d | 16 | import { VideoModel } from '../../models/video/video' |
2ccaeeb3 C |
17 | import { VideoChannelModel } from '../../models/video/video-channel' |
18 | import { VideoFileModel } from '../../models/video/video-file' | |
19 | import { VideoShareModel } from '../../models/video/video-share' | |
20 | import { getOrCreateActorAndServerAndModel } from './actor' | |
892211e8 | 21 | |
d50acfab | 22 | function fetchRemoteVideoPreview (video: VideoModel, reject: Function) { |
892211e8 | 23 | // FIXME: use url |
50d6de9c | 24 | const host = video.VideoChannel.Account.Actor.Server.host |
892211e8 C |
25 | const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) |
26 | ||
f40bbe31 C |
27 | return request.get(REMOTE_SCHEME.HTTP + '://' + host + path, err => { |
28 | if (err) reject(err) | |
29 | }) | |
892211e8 C |
30 | } |
31 | ||
3fd3ab2d | 32 | async function fetchRemoteVideoDescription (video: VideoModel) { |
892211e8 | 33 | // FIXME: use url |
50d6de9c | 34 | const host = video.VideoChannel.Account.Actor.Server.host |
892211e8 C |
35 | const path = video.getDescriptionPath() |
36 | const options = { | |
37 | uri: REMOTE_SCHEME.HTTP + '://' + host + path, | |
38 | json: true | |
39 | } | |
40 | ||
41 | const { body } = await doRequest(options) | |
42 | return body.description ? body.description : '' | |
43 | } | |
44 | ||
3fd3ab2d | 45 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { |
892211e8 C |
46 | const thumbnailName = video.getThumbnailName() |
47 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) | |
48 | ||
49 | const options = { | |
50 | method: 'GET', | |
51 | uri: icon.url | |
52 | } | |
53 | return doRequestAndSaveToFile(options, thumbnailPath) | |
54 | } | |
55 | ||
2ccaeeb3 C |
56 | async function videoActivityObjectToDBAttributes (videoChannel: VideoChannelModel, |
57 | videoObject: VideoTorrentObject, | |
58 | to: string[] = [], | |
59 | cc: string[] = []) { | |
60 | let privacy = VideoPrivacy.PRIVATE | |
61 | if (to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.PUBLIC | |
62 | else if (cc.indexOf(ACTIVITY_PUB.PUBLIC) !== -1) privacy = VideoPrivacy.UNLISTED | |
63 | ||
64 | const duration = videoObject.duration.replace(/[^\d]+/, '') | |
65 | let language = null | |
66 | if (videoObject.language) { | |
67 | language = parseInt(videoObject.language.identifier, 10) | |
68 | } | |
69 | ||
70 | let category = null | |
71 | if (videoObject.category) { | |
72 | category = parseInt(videoObject.category.identifier, 10) | |
73 | } | |
74 | ||
75 | let licence = null | |
76 | if (videoObject.licence) { | |
77 | licence = parseInt(videoObject.licence.identifier, 10) | |
78 | } | |
79 | ||
80 | let description = null | |
81 | if (videoObject.content) { | |
82 | description = videoObject.content | |
83 | } | |
84 | ||
85 | return { | |
86 | name: videoObject.name, | |
87 | uuid: videoObject.uuid, | |
88 | url: videoObject.id, | |
89 | category, | |
90 | licence, | |
91 | language, | |
92 | description, | |
93 | nsfw: videoObject.nsfw, | |
94 | commentsEnabled: videoObject.commentsEnabled, | |
95 | channelId: videoChannel.id, | |
96 | duration: parseInt(duration, 10), | |
97 | createdAt: new Date(videoObject.published), | |
98 | // FIXME: updatedAt does not seems to be considered by Sequelize | |
99 | updatedAt: new Date(videoObject.updated), | |
100 | views: videoObject.views, | |
101 | likes: 0, | |
102 | dislikes: 0, | |
103 | remote: true, | |
104 | privacy | |
105 | } | |
106 | } | |
107 | ||
108 | function videoFileActivityUrlToDBAttributes (videoCreated: VideoModel, videoObject: VideoTorrentObject) { | |
109 | const mimeTypes = Object.keys(VIDEO_MIMETYPE_EXT) | |
110 | const fileUrls = videoObject.url.filter(u => { | |
111 | return mimeTypes.indexOf(u.mimeType) !== -1 && u.mimeType.startsWith('video/') | |
112 | }) | |
113 | ||
114 | if (fileUrls.length === 0) { | |
115 | throw new Error('Cannot find video files for ' + videoCreated.url) | |
116 | } | |
117 | ||
118 | const attributes = [] | |
119 | for (const fileUrl of fileUrls) { | |
120 | // Fetch associated magnet uri | |
121 | const magnet = videoObject.url.find(u => { | |
122 | return u.mimeType === 'application/x-bittorrent;x-scheme-handler/magnet' && u.width === fileUrl.width | |
123 | }) | |
124 | ||
125 | if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.url) | |
126 | ||
127 | const parsed = magnetUtil.decode(magnet.url) | |
128 | if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) throw new Error('Cannot parse magnet URI ' + magnet.url) | |
129 | ||
130 | const attribute = { | |
131 | extname: VIDEO_MIMETYPE_EXT[ fileUrl.mimeType ], | |
132 | infoHash: parsed.infoHash, | |
133 | resolution: fileUrl.width, | |
134 | size: fileUrl.size, | |
135 | videoId: videoCreated.id | |
136 | } | |
137 | attributes.push(attribute) | |
138 | } | |
139 | ||
140 | return attributes | |
141 | } | |
142 | ||
143 | async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel) { | |
144 | logger.debug('Adding remote video %s.', videoObject.id) | |
145 | ||
146 | return sequelizeTypescript.transaction(async t => { | |
147 | const sequelizeOptions = { | |
148 | transaction: t | |
149 | } | |
150 | const videoFromDatabase = await VideoModel.loadByUUIDOrURLAndPopulateAccount(videoObject.uuid, videoObject.id, t) | |
151 | if (videoFromDatabase) return videoFromDatabase | |
152 | ||
153 | const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to, videoObject.cc) | |
154 | const video = VideoModel.build(videoData) | |
155 | ||
156 | // Don't block on request | |
157 | generateThumbnailFromUrl(video, videoObject.icon) | |
158 | .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, err)) | |
159 | ||
160 | const videoCreated = await video.save(sequelizeOptions) | |
161 | ||
162 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) | |
163 | if (videoFileAttributes.length === 0) { | |
164 | throw new Error('Cannot find valid files for video %s ' + videoObject.url) | |
165 | } | |
166 | ||
167 | const tasks: Bluebird<any>[] = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) | |
168 | await Promise.all(tasks) | |
169 | ||
170 | const tags = videoObject.tag.map(t => t.name) | |
171 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | |
172 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) | |
173 | ||
174 | logger.info('Remote video with uuid %s inserted.', videoObject.uuid) | |
175 | ||
176 | videoCreated.VideoChannel = channelActor.VideoChannel | |
177 | return videoCreated | |
178 | }) | |
0032ebe9 C |
179 | } |
180 | ||
2ccaeeb3 C |
181 | async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) { |
182 | if (typeof videoObject === 'string') { | |
183 | const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoObject) | |
184 | if (videoFromDatabase) { | |
185 | return { | |
186 | video: videoFromDatabase, | |
187 | actor: videoFromDatabase.VideoChannel.Account.Actor, | |
188 | channelActor: videoFromDatabase.VideoChannel.Actor | |
189 | } | |
190 | } | |
191 | ||
192 | videoObject = await fetchRemoteVideo(videoObject) | |
193 | if (!videoObject) throw new Error('Cannot fetch remote video') | |
194 | } | |
195 | ||
196 | if (!actor) { | |
197 | const actorObj = videoObject.attributedTo.find(a => a.type === 'Person') | |
198 | if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url) | |
199 | ||
200 | actor = await getOrCreateActorAndServerAndModel(actorObj.id) | |
201 | } | |
202 | ||
203 | const channel = videoObject.attributedTo.find(a => a.type === 'Group') | |
204 | if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) | |
205 | ||
206 | const channelActor = await getOrCreateActorAndServerAndModel(channel.id) | |
207 | ||
208 | const options = { | |
209 | arguments: [ videoObject, channelActor ], | |
210 | errorMessage: 'Cannot insert the remote video with many retries.' | |
211 | } | |
212 | ||
213 | const video = await retryTransactionWrapper(getOrCreateVideo, options) | |
214 | ||
215 | return { actor, channelActor, video } | |
216 | } | |
217 | ||
218 | async function addVideoShares (instance: VideoModel, shareUrls: string[]) { | |
219 | for (const shareUrl of shareUrls) { | |
220 | // Fetch url | |
221 | const { body } = await doRequest({ | |
222 | uri: shareUrl, | |
223 | json: true, | |
224 | activityPub: true | |
225 | }) | |
226 | const actorUrl = body.actor | |
227 | if (!actorUrl) continue | |
228 | ||
229 | const actor = await getOrCreateActorAndServerAndModel(actorUrl) | |
230 | ||
231 | const entry = { | |
232 | actorId: actor.id, | |
233 | videoId: instance.id | |
234 | } | |
235 | ||
236 | await VideoShareModel.findOrCreate({ | |
237 | where: entry, | |
238 | defaults: entry | |
239 | }) | |
240 | } | |
0032ebe9 C |
241 | } |
242 | ||
892211e8 | 243 | export { |
2ccaeeb3 | 244 | getOrCreateAccountAndVideoAndChannel, |
892211e8 C |
245 | fetchRemoteVideoPreview, |
246 | fetchRemoteVideoDescription, | |
0032ebe9 | 247 | generateThumbnailFromUrl, |
2ccaeeb3 C |
248 | videoActivityObjectToDBAttributes, |
249 | videoFileActivityUrlToDBAttributes, | |
250 | getOrCreateVideo, | |
251 | addVideoShares} | |
252 | ||
253 | // --------------------------------------------------------------------------- | |
254 | ||
255 | async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> { | |
256 | const options = { | |
257 | uri: videoUrl, | |
258 | method: 'GET', | |
259 | json: true, | |
260 | activityPub: true | |
261 | } | |
262 | ||
263 | logger.info('Fetching remote video %s.', videoUrl) | |
264 | ||
265 | const { body } = await doRequest(options) | |
266 | ||
267 | if (isVideoTorrentObjectValid(body) === false) { | |
268 | logger.debug('Remote video JSON is not valid.', { body }) | |
269 | return undefined | |
270 | } | |
271 | ||
272 | return body | |
892211e8 | 273 | } |