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