]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/activitypub/videos/shared/object-to-model-attributes.ts
Refactor AP video create/update
[github/Chocobozzz/PeerTube.git] / server / lib / activitypub / videos / shared / object-to-model-attributes.ts
1 import { maxBy, minBy } from 'lodash'
2 import * as magnetUtil from 'magnet-uri'
3 import { basename } from 'path'
4 import { isAPVideoFileUrlMetadataObject } from '@server/helpers/custom-validators/activitypub/videos'
5 import { isVideoFileInfoHashValid } from '@server/helpers/custom-validators/videos'
6 import { logger } from '@server/helpers/logger'
7 import { getExtFromMimetype } from '@server/helpers/video'
8 import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants'
9 import { generateTorrentFileName } from '@server/lib/video-paths'
10 import { VideoFileModel } from '@server/models/video/video-file'
11 import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
12 import { FilteredModelAttributes } from '@server/types'
13 import { MChannelId, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models'
14 import {
15 ActivityHashTagObject,
16 ActivityMagnetUrlObject,
17 ActivityPlaylistSegmentHashesObject,
18 ActivityPlaylistUrlObject,
19 ActivityTagObject,
20 ActivityUrlObject,
21 ActivityVideoUrlObject,
22 VideoObject,
23 VideoPrivacy,
24 VideoStreamingPlaylistType
25 } from '@shared/models'
26 import { VideoCaptionModel } from '@server/models/video/video-caption'
27
28 function getThumbnailFromIcons (videoObject: VideoObject) {
29 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
30 // Fallback if there are not valid icons
31 if (validIcons.length === 0) validIcons = videoObject.icon
32
33 return minBy(validIcons, 'width')
34 }
35
36 function getPreviewFromIcons (videoObject: VideoObject) {
37 const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
38
39 return maxBy(validIcons, 'width')
40 }
41
42 function getTagsFromObject (videoObject: VideoObject) {
43 return videoObject.tag
44 .filter(isAPHashTagObject)
45 .map(t => t.name)
46 }
47
48 function getFileAttributesFromUrl (
49 videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
50 urls: (ActivityTagObject | ActivityUrlObject)[]
51 ) {
52 const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
53
54 if (fileUrls.length === 0) return []
55
56 const attributes: FilteredModelAttributes<VideoFileModel>[] = []
57 for (const fileUrl of fileUrls) {
58 // Fetch associated magnet uri
59 const magnet = urls.filter(isAPMagnetUrlObject)
60 .find(u => u.height === fileUrl.height)
61
62 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
63
64 const parsed = magnetUtil.decode(magnet.href)
65 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) {
66 throw new Error('Cannot parse magnet URI ' + magnet.href)
67 }
68
69 const torrentUrl = Array.isArray(parsed.xs)
70 ? parsed.xs[0]
71 : parsed.xs
72
73 // Fetch associated metadata url, if any
74 const metadata = urls.filter(isAPVideoFileUrlMetadataObject)
75 .find(u => {
76 return u.height === fileUrl.height &&
77 u.fps === fileUrl.fps &&
78 u.rel.includes(fileUrl.mediaType)
79 })
80
81 const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType)
82 const resolution = fileUrl.height
83 const videoId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id
84 const videoStreamingPlaylistId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null
85
86 const attribute = {
87 extname,
88 infoHash: parsed.infoHash,
89 resolution,
90 size: fileUrl.size,
91 fps: fileUrl.fps || -1,
92 metadataUrl: metadata?.href,
93
94 // Use the name of the remote file because we don't proxify video file requests
95 filename: basename(fileUrl.href),
96 fileUrl: fileUrl.href,
97
98 torrentUrl,
99 // Use our own torrent name since we proxify torrent requests
100 torrentFilename: generateTorrentFileName(videoOrPlaylist, resolution),
101
102 // This is a video file owned by a video or by a streaming playlist
103 videoId,
104 videoStreamingPlaylistId
105 }
106
107 attributes.push(attribute)
108 }
109
110 return attributes
111 }
112
113 function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject: VideoObject, videoFiles: MVideoFile[]) {
114 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
115 if (playlistUrls.length === 0) return []
116
117 const attributes: (FilteredModelAttributes<VideoStreamingPlaylistModel> & { tagAPObject?: ActivityTagObject[] })[] = []
118 for (const playlistUrlObject of playlistUrls) {
119 const segmentsSha256UrlObject = playlistUrlObject.tag.find(isAPPlaylistSegmentHashesUrlObject)
120
121 let files: unknown[] = playlistUrlObject.tag.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
122
123 // FIXME: backward compatibility introduced in v2.1.0
124 if (files.length === 0) files = videoFiles
125
126 if (!segmentsSha256UrlObject) {
127 logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
128 continue
129 }
130
131 const attribute = {
132 type: VideoStreamingPlaylistType.HLS,
133 playlistUrl: playlistUrlObject.href,
134 segmentsSha256Url: segmentsSha256UrlObject.href,
135 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
136 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
137 videoId: video.id,
138
139 tagAPObject: playlistUrlObject.tag
140 }
141
142 attributes.push(attribute)
143 }
144
145 return attributes
146 }
147
148 function getLiveAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
149 return {
150 saveReplay: videoObject.liveSaveReplay,
151 permanentLive: videoObject.permanentLive,
152 videoId: video.id
153 }
154 }
155
156 function getCaptionAttributesFromObject (video: MVideoId, videoObject: VideoObject) {
157 return videoObject.subtitleLanguage.map(c => ({
158 videoId: video.id,
159 filename: VideoCaptionModel.generateCaptionName(c.identifier),
160 language: c.identifier,
161 fileUrl: c.url
162 }))
163 }
164
165 function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
166 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
167 ? VideoPrivacy.PUBLIC
168 : VideoPrivacy.UNLISTED
169
170 const duration = videoObject.duration.replace(/[^\d]+/, '')
171 const language = videoObject.language?.identifier
172
173 const category = videoObject.category
174 ? parseInt(videoObject.category.identifier, 10)
175 : undefined
176
177 const licence = videoObject.licence
178 ? parseInt(videoObject.licence.identifier, 10)
179 : undefined
180
181 const description = videoObject.content || null
182 const support = videoObject.support || null
183
184 return {
185 name: videoObject.name,
186 uuid: videoObject.uuid,
187 url: videoObject.id,
188 category,
189 licence,
190 language,
191 description,
192 support,
193 nsfw: videoObject.sensitive,
194 commentsEnabled: videoObject.commentsEnabled,
195 downloadEnabled: videoObject.downloadEnabled,
196 waitTranscoding: videoObject.waitTranscoding,
197 isLive: videoObject.isLiveBroadcast,
198 state: videoObject.state,
199 channelId: videoChannel.id,
200 duration: parseInt(duration, 10),
201 createdAt: new Date(videoObject.published),
202 publishedAt: new Date(videoObject.published),
203
204 originallyPublishedAt: videoObject.originallyPublishedAt
205 ? new Date(videoObject.originallyPublishedAt)
206 : null,
207
208 updatedAt: new Date(videoObject.updated),
209 views: videoObject.views,
210 likes: 0,
211 dislikes: 0,
212 remote: true,
213 privacy
214 }
215 }
216
217 // ---------------------------------------------------------------------------
218
219 export {
220 getThumbnailFromIcons,
221 getPreviewFromIcons,
222
223 getTagsFromObject,
224
225 getFileAttributesFromUrl,
226 getStreamingPlaylistAttributesFromObject,
227
228 getLiveAttributesFromObject,
229 getCaptionAttributesFromObject,
230
231 getVideoAttributesFromObject
232 }
233
234 // ---------------------------------------------------------------------------
235
236 function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
237 const urlMediaType = url.mediaType
238
239 return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/')
240 }
241
242 function isAPStreamingPlaylistUrlObject (url: any): url is ActivityPlaylistUrlObject {
243 return url && url.mediaType === 'application/x-mpegURL'
244 }
245
246 function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
247 return tag && tag.name === 'sha256' && tag.type === 'Link' && tag.mediaType === 'application/json'
248 }
249
250 function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject {
251 return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet'
252 }
253
254 function isAPHashTagObject (url: any): url is ActivityHashTagObject {
255 return url && url.type === 'Hashtag'
256 }