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'
15 ActivityHashTagObject,
16 ActivityMagnetUrlObject,
17 ActivityPlaylistSegmentHashesObject,
18 ActivityPlaylistUrlObject,
21 ActivityVideoUrlObject,
24 VideoStreamingPlaylistType
25 } from '@shared/models'
27 function 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
32 return minBy(validIcons, 'width')
35 function getPreviewFromIcons (videoObject: VideoObject) {
36 const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
38 return maxBy(validIcons, 'width')
41 function getTagsFromObject (videoObject: VideoObject) {
42 return videoObject.tag
43 .filter(isAPHashTagObject)
47 function videoFileActivityUrlToDBAttributes (
48 videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
49 urls: (ActivityTagObject | ActivityUrlObject)[]
51 const fileUrls = urls.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
53 if (fileUrls.length === 0) return []
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)
61 if (!magnet) throw new Error('Cannot find associated magnet uri for file ' + fileUrl.href)
63 const parsed = magnetUtil.decode(magnet.href)
64 if (!parsed || isVideoFileInfoHashValid(parsed.infoHash) === false) {
65 throw new Error('Cannot parse magnet URI ' + magnet.href)
68 const torrentUrl = Array.isArray(parsed.xs)
72 // Fetch associated metadata url, if any
73 const metadata = urls.filter(isAPVideoFileUrlMetadataObject)
75 return u.height === fileUrl.height &&
76 u.fps === fileUrl.fps &&
77 u.rel.includes(fileUrl.mediaType)
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
87 infoHash: parsed.infoHash,
90 fps: fileUrl.fps || -1,
91 metadataUrl: metadata?.href,
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,
98 // Use our own torrent name since we proxify torrent requests
99 torrentFilename: generateTorrentFileName(videoOrPlaylist, resolution),
101 // This is a video file owned by a video or by a streaming playlist
103 videoStreamingPlaylistId
106 attributes.push(attribute)
112 function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObject: VideoObject, videoFiles: MVideoFile[]) {
113 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
114 if (playlistUrls.length === 0) return []
116 const attributes: (FilteredModelAttributes<VideoStreamingPlaylistModel> & { tagAPObject?: ActivityTagObject[] })[] = []
117 for (const playlistUrlObject of playlistUrls) {
118 const segmentsSha256UrlObject = playlistUrlObject.tag.find(isAPPlaylistSegmentHashesUrlObject)
120 let files: unknown[] = playlistUrlObject.tag.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
122 // FIXME: backward compatibility introduced in v2.1.0
123 if (files.length === 0) files = videoFiles
125 if (!segmentsSha256UrlObject) {
126 logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
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,
137 tagAPObject: playlistUrlObject.tag
140 attributes.push(attribute)
146 function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoObject, to: string[] = []) {
147 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
148 ? VideoPrivacy.PUBLIC
149 : VideoPrivacy.UNLISTED
151 const duration = videoObject.duration.replace(/[^\d]+/, '')
152 const language = videoObject.language?.identifier
154 const category = videoObject.category
155 ? parseInt(videoObject.category.identifier, 10)
158 const licence = videoObject.licence
159 ? parseInt(videoObject.licence.identifier, 10)
162 const description = videoObject.content || null
163 const support = videoObject.support || null
166 name: videoObject.name,
167 uuid: videoObject.uuid,
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),
185 originallyPublishedAt: videoObject.originallyPublishedAt
186 ? new Date(videoObject.originallyPublishedAt)
189 updatedAt: new Date(videoObject.updated),
190 views: videoObject.views,
198 // ---------------------------------------------------------------------------
201 getThumbnailFromIcons,
206 videoActivityObjectToDBAttributes,
208 videoFileActivityUrlToDBAttributes,
209 streamingPlaylistActivityUrlToDBAttributes
212 // ---------------------------------------------------------------------------
214 function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
215 const urlMediaType = url.mediaType
217 return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/')
220 function isAPStreamingPlaylistUrlObject (url: any): url is ActivityPlaylistUrlObject {
221 return url && url.mediaType === 'application/x-mpegURL'
224 function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
225 return tag && tag.name === 'sha256' && tag.type === 'Link' && tag.mediaType === 'application/json'
228 function isAPMagnetUrlObject (url: any): url is ActivityMagnetUrlObject {
229 return url && url.mediaType === 'application/x-bittorrent;x-scheme-handler/magnet'
232 function isAPHashTagObject (url: any): url is ActivityHashTagObject {
233 return url && url.type === 'Hashtag'