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