aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/thumbnail.ts4
-rw-r--r--server/models/video/video-caption.ts6
-rw-r--r--server/models/video/video-file.ts181
-rw-r--r--server/models/video/video-format-utils.ts64
-rw-r--r--server/models/video/video-query-builder.ts8
-rw-r--r--server/models/video/video-streaming-playlist.ts58
-rw-r--r--server/models/video/video.ts60
7 files changed, 225 insertions, 156 deletions
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
index 4185ec5f2..9533c8d19 100644
--- a/server/models/video/thumbnail.ts
+++ b/server/models/video/thumbnail.ts
@@ -17,7 +17,7 @@ import {
17} from 'sequelize-typescript' 17} from 'sequelize-typescript'
18import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' 18import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
19import { afterCommitIfTransaction } from '@server/helpers/database-utils' 19import { afterCommitIfTransaction } from '@server/helpers/database-utils'
20import { MThumbnail, MThumbnailVideo, MVideoAccountLight } from '@server/types/models' 20import { MThumbnail, MThumbnailVideo, MVideoWithHost } from '@server/types/models'
21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
22import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
23import { CONFIG } from '../../initializers/config' 23import { CONFIG } from '../../initializers/config'
@@ -164,7 +164,7 @@ export class ThumbnailModel extends Model {
164 return join(directory, filename) 164 return join(directory, filename)
165 } 165 }
166 166
167 getFileUrl (video: MVideoAccountLight) { 167 getFileUrl (video: MVideoWithHost) {
168 const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename 168 const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
169 169
170 if (video.isOwned()) return WEBSERVER.URL + staticPath 170 if (video.isOwned()) return WEBSERVER.URL + staticPath
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index a1553ea15..71b067335 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -15,8 +15,9 @@ import {
15 Table, 15 Table,
16 UpdatedAt 16 UpdatedAt
17} from 'sequelize-typescript' 17} from 'sequelize-typescript'
18import { v4 as uuidv4 } from 'uuid'
18import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' 19import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
19import { MVideoAccountLight, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models' 20import { MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo, MVideoWithHost } from '@server/types/models'
20import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 21import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
21import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 22import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
22import { logger } from '../../helpers/logger' 23import { logger } from '../../helpers/logger'
@@ -24,7 +25,6 @@ import { CONFIG } from '../../initializers/config'
24import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants' 25import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
25import { buildWhereIdOrUUID, throwIfNotValid } from '../utils' 26import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
26import { VideoModel } from './video' 27import { VideoModel } from './video'
27import { v4 as uuidv4 } from 'uuid'
28 28
29export enum ScopeNames { 29export enum ScopeNames {
30 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE' 30 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
@@ -204,7 +204,7 @@ export class VideoCaptionModel extends Model {
204 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename) 204 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.filename)
205 } 205 }
206 206
207 getFileUrl (video: MVideoAccountLight) { 207 getFileUrl (video: MVideoWithHost) {
208 if (!this.Video) this.Video = video as VideoModel 208 if (!this.Video) this.Video = video as VideoModel
209 209
210 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() 210 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 48b337c68..57807cbfd 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -1,3 +1,7 @@
1import { remove } from 'fs-extra'
2import * as memoizee from 'memoizee'
3import { join } from 'path'
4import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
1import { 5import {
2 AllowNull, 6 AllowNull,
3 BelongsTo, 7 BelongsTo,
@@ -5,15 +9,22 @@ import {
5 CreatedAt, 9 CreatedAt,
6 DataType, 10 DataType,
7 Default, 11 Default,
12 DefaultScope,
8 ForeignKey, 13 ForeignKey,
9 HasMany, 14 HasMany,
10 Is, 15 Is,
11 Model, 16 Model,
12 Table,
13 UpdatedAt,
14 Scopes, 17 Scopes,
15 DefaultScope 18 Table,
19 UpdatedAt
16} from 'sequelize-typescript' 20} from 'sequelize-typescript'
21import { Where } from 'sequelize/types/lib/utils'
22import validator from 'validator'
23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
24import { logger } from '@server/helpers/logger'
25import { extractVideo } from '@server/helpers/video'
26import { getTorrentFilePath } from '@server/lib/video-paths'
27import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models'
17import { 28import {
18 isVideoFileExtnameValid, 29 isVideoFileExtnameValid,
19 isVideoFileInfoHashValid, 30 isVideoFileInfoHashValid,
@@ -21,20 +32,25 @@ import {
21 isVideoFileSizeValid, 32 isVideoFileSizeValid,
22 isVideoFPSResolutionValid 33 isVideoFPSResolutionValid
23} from '../../helpers/custom-validators/videos' 34} from '../../helpers/custom-validators/videos'
35import {
36 LAZY_STATIC_PATHS,
37 MEMOIZE_LENGTH,
38 MEMOIZE_TTL,
39 MIMETYPES,
40 STATIC_DOWNLOAD_PATHS,
41 STATIC_PATHS,
42 WEBSERVER
43} from '../../initializers/constants'
44import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
45import { VideoRedundancyModel } from '../redundancy/video-redundancy'
24import { parseAggregateResult, throwIfNotValid } from '../utils' 46import { parseAggregateResult, throwIfNotValid } from '../utils'
25import { VideoModel } from './video' 47import { VideoModel } from './video'
26import { VideoRedundancyModel } from '../redundancy/video-redundancy'
27import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 48import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
28import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
29import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants'
30import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
31import { MStreamingPlaylistVideo, MVideo } from '@server/types/models'
32import * as memoizee from 'memoizee'
33import validator from 'validator'
34 49
35export enum ScopeNames { 50export enum ScopeNames {
36 WITH_VIDEO = 'WITH_VIDEO', 51 WITH_VIDEO = 'WITH_VIDEO',
37 WITH_METADATA = 'WITH_METADATA' 52 WITH_METADATA = 'WITH_METADATA',
53 WITH_VIDEO_OR_PLAYLIST = 'WITH_VIDEO_OR_PLAYLIST'
38} 54}
39 55
40@DefaultScope(() => ({ 56@DefaultScope(() => ({
@@ -51,6 +67,28 @@ export enum ScopeNames {
51 } 67 }
52 ] 68 ]
53 }, 69 },
70 [ScopeNames.WITH_VIDEO_OR_PLAYLIST]: (options: { whereVideo?: Where } = {}) => {
71 return {
72 include: [
73 {
74 model: VideoModel.unscoped(),
75 required: false,
76 where: options.whereVideo
77 },
78 {
79 model: VideoStreamingPlaylistModel.unscoped(),
80 required: false,
81 include: [
82 {
83 model: VideoModel.unscoped(),
84 required: true,
85 where: options.whereVideo
86 }
87 ]
88 }
89 ]
90 }
91 },
54 [ScopeNames.WITH_METADATA]: { 92 [ScopeNames.WITH_METADATA]: {
55 attributes: { 93 attributes: {
56 include: [ 'metadata' ] 94 include: [ 'metadata' ]
@@ -82,6 +120,16 @@ export enum ScopeNames {
82 }, 120 },
83 121
84 { 122 {
123 fields: [ 'torrentFilename' ],
124 unique: true
125 },
126
127 {
128 fields: [ 'filename' ],
129 unique: true
130 },
131
132 {
85 fields: [ 'videoId', 'resolution', 'fps' ], 133 fields: [ 'videoId', 'resolution', 'fps' ],
86 unique: true, 134 unique: true,
87 where: { 135 where: {
@@ -142,6 +190,24 @@ export class VideoFileModel extends Model {
142 @Column 190 @Column
143 metadataUrl: string 191 metadataUrl: string
144 192
193 @AllowNull(true)
194 @Column
195 fileUrl: string
196
197 // Could be null for live files
198 @AllowNull(true)
199 @Column
200 filename: string
201
202 @AllowNull(true)
203 @Column
204 torrentUrl: string
205
206 // Could be null for live files
207 @AllowNull(true)
208 @Column
209 torrentFilename: string
210
145 @ForeignKey(() => VideoModel) 211 @ForeignKey(() => VideoModel)
146 @Column 212 @Column
147 videoId: number 213 videoId: number
@@ -199,6 +265,16 @@ export class VideoFileModel extends Model {
199 return !!videoFile 265 return !!videoFile
200 } 266 }
201 267
268 static loadWithVideoOrPlaylistByTorrentFilename (filename: string) {
269 const query = {
270 where: {
271 torrentFilename: filename
272 }
273 }
274
275 return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query)
276 }
277
202 static loadWithMetadata (id: number) { 278 static loadWithMetadata (id: number) {
203 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id) 279 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
204 } 280 }
@@ -215,28 +291,11 @@ export class VideoFileModel extends Model {
215 const options = { 291 const options = {
216 where: { 292 where: {
217 id 293 id
218 }, 294 }
219 include: [
220 {
221 model: VideoModel.unscoped(),
222 required: false,
223 where: whereVideo
224 },
225 {
226 model: VideoStreamingPlaylistModel.unscoped(),
227 required: false,
228 include: [
229 {
230 model: VideoModel.unscoped(),
231 required: true,
232 where: whereVideo
233 }
234 ]
235 }
236 ]
237 } 295 }
238 296
239 return VideoFileModel.findOne(options) 297 return VideoFileModel.scope({ method: [ ScopeNames.WITH_VIDEO_OR_PLAYLIST, whereVideo ] })
298 .findOne(options)
240 .then(file => { 299 .then(file => {
241 // We used `required: false` so check we have at least a video or a streaming playlist 300 // We used `required: false` so check we have at least a video or a streaming playlist
242 if (!file.Video && !file.VideoStreamingPlaylist) return null 301 if (!file.Video && !file.VideoStreamingPlaylist) return null
@@ -348,6 +407,10 @@ export class VideoFileModel extends Model {
348 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist 407 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
349 } 408 }
350 409
410 getVideo (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo {
411 return extractVideo(this.getVideoOrStreamingPlaylist())
412 }
413
351 isAudio () { 414 isAudio () {
352 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] 415 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
353 } 416 }
@@ -360,6 +423,62 @@ export class VideoFileModel extends Model {
360 return !!this.videoStreamingPlaylistId 423 return !!this.videoStreamingPlaylistId
361 } 424 }
362 425
426 getFileUrl (video: MVideoWithHost) {
427 if (!this.Video) this.Video = video as VideoModel
428
429 if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video)
430 if (this.fileUrl) return this.fileUrl
431
432 // Fallback if we don't have a file URL
433 return buildRemoteVideoBaseUrl(video, this.getFileStaticPath(video))
434 }
435
436 getFileStaticPath (video: MVideo) {
437 if (this.isHLS()) return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.filename)
438
439 return join(STATIC_PATHS.WEBSEED, this.filename)
440 }
441
442 getFileDownloadUrl (video: MVideoWithHost) {
443 const basePath = this.isHLS()
444 ? STATIC_DOWNLOAD_PATHS.HLS_VIDEOS
445 : STATIC_DOWNLOAD_PATHS.VIDEOS
446 const path = join(basePath, this.filename)
447
448 if (video.isOwned()) return WEBSERVER.URL + path
449
450 // FIXME: don't guess remote URL
451 return buildRemoteVideoBaseUrl(video, path)
452 }
453
454 getRemoteTorrentUrl (video: MVideoWithHost) {
455 if (video.isOwned()) throw new Error(`Video ${video.url} is not a remote video`)
456
457 if (this.torrentUrl) return this.torrentUrl
458
459 // Fallback if we don't have a torrent URL
460 return buildRemoteVideoBaseUrl(video, this.getTorrentStaticPath())
461 }
462
463 // We proxify torrent requests so use a local URL
464 getTorrentUrl () {
465 return WEBSERVER.URL + this.getTorrentStaticPath()
466 }
467
468 getTorrentStaticPath () {
469 return join(LAZY_STATIC_PATHS.TORRENTS, this.torrentFilename)
470 }
471
472 getTorrentDownloadUrl () {
473 return WEBSERVER.URL + join(STATIC_DOWNLOAD_PATHS.TORRENTS, this.torrentFilename)
474 }
475
476 removeTorrent () {
477 const torrentPath = getTorrentFilePath(this)
478 return remove(torrentPath)
479 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
480 }
481
363 hasSameUniqueKeysThan (other: MVideoFile) { 482 hasSameUniqueKeysThan (other: MVideoFile) {
364 return this.fps === other.fps && 483 return this.fps === other.fps &&
365 this.resolution === other.resolution && 484 this.resolution === other.resolution &&
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 77b8bcfe3..adf460734 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -1,16 +1,17 @@
1import { Video, VideoDetails } from '../../../shared/models/videos' 1import { generateMagnetUri } from '@server/helpers/webtorrent'
2import { VideoModel } from './video' 2import { getLocalVideoFileMetadataUrl } from '@server/lib/video-paths'
3import { VideoFile } from '@shared/models/videos/video-file.model'
3import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' 4import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
5import { Video, VideoDetails } from '../../../shared/models/videos'
6import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
7import { isArray } from '../../helpers/custom-validators/misc'
4import { MIMETYPES, WEBSERVER } from '../../initializers/constants' 8import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
5import { VideoCaptionModel } from './video-caption'
6import { 9import {
7 getLocalVideoCommentsActivityPubUrl, 10 getLocalVideoCommentsActivityPubUrl,
8 getLocalVideoDislikesActivityPubUrl, 11 getLocalVideoDislikesActivityPubUrl,
9 getLocalVideoLikesActivityPubUrl, 12 getLocalVideoLikesActivityPubUrl,
10 getLocalVideoSharesActivityPubUrl 13 getLocalVideoSharesActivityPubUrl
11} from '../../lib/activitypub/url' 14} from '../../lib/activitypub/url'
12import { isArray } from '../../helpers/custom-validators/misc'
13import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
14import { 15import {
15 MStreamingPlaylistRedundanciesOpt, 16 MStreamingPlaylistRedundanciesOpt,
16 MStreamingPlaylistVideo, 17 MStreamingPlaylistVideo,
@@ -18,12 +19,12 @@ import {
18 MVideoAP, 19 MVideoAP,
19 MVideoFile, 20 MVideoFile,
20 MVideoFormattable, 21 MVideoFormattable,
21 MVideoFormattableDetails 22 MVideoFormattableDetails,
23 MVideoWithHost
22} from '../../types/models' 24} from '../../types/models'
23import { MVideoFileRedundanciesOpt } from '../../types/models/video/video-file' 25import { MVideoFileRedundanciesOpt } from '../../types/models/video/video-file'
24import { VideoFile } from '@shared/models/videos/video-file.model' 26import { VideoModel } from './video'
25import { generateMagnetUri } from '@server/helpers/webtorrent' 27import { VideoCaptionModel } from './video-caption'
26import { extractVideo } from '@server/helpers/video'
27 28
28export type VideoFormattingJSONOptions = { 29export type VideoFormattingJSONOptions = {
29 completeDescription?: boolean 30 completeDescription?: boolean
@@ -153,12 +154,15 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
153 } 154 }
154 155
155 // Format and sort video files 156 // Format and sort video files
156 detailsJson.files = videoFilesModelToFormattedJSON(video, baseUrlHttp, baseUrlWs, video.VideoFiles) 157 detailsJson.files = videoFilesModelToFormattedJSON(video, video, baseUrlHttp, baseUrlWs, video.VideoFiles)
157 158
158 return Object.assign(formattedJson, detailsJson) 159 return Object.assign(formattedJson, detailsJson)
159} 160}
160 161
161function streamingPlaylistsModelToFormattedJSON (video: MVideo, playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] { 162function streamingPlaylistsModelToFormattedJSON (
163 video: MVideoFormattableDetails,
164 playlists: MStreamingPlaylistRedundanciesOpt[]
165): VideoStreamingPlaylist[] {
162 if (isArray(playlists) === false) return [] 166 if (isArray(playlists) === false) return []
163 167
164 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 168 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
@@ -171,7 +175,7 @@ function streamingPlaylistsModelToFormattedJSON (video: MVideo, playlists: MStre
171 ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) 175 ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
172 : [] 176 : []
173 177
174 const files = videoFilesModelToFormattedJSON(playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles) 178 const files = videoFilesModelToFormattedJSON(playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles)
175 179
176 return { 180 return {
177 id: playlist.id, 181 id: playlist.id,
@@ -190,14 +194,14 @@ function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) {
190 return -1 194 return -1
191} 195}
192 196
197// FIXME: refactor/merge model and video arguments
193function videoFilesModelToFormattedJSON ( 198function videoFilesModelToFormattedJSON (
194 model: MVideo | MStreamingPlaylistVideo, 199 model: MVideo | MStreamingPlaylistVideo,
200 video: MVideoFormattableDetails,
195 baseUrlHttp: string, 201 baseUrlHttp: string,
196 baseUrlWs: string, 202 baseUrlWs: string,
197 videoFiles: MVideoFileRedundanciesOpt[] 203 videoFiles: MVideoFileRedundanciesOpt[]
198): VideoFile[] { 204): VideoFile[] {
199 const video = extractVideo(model)
200
201 return [ ...videoFiles ] 205 return [ ...videoFiles ]
202 .filter(f => !f.isLive()) 206 .filter(f => !f.isLive())
203 .sort(sortByResolutionDesc) 207 .sort(sortByResolutionDesc)
@@ -207,21 +211,29 @@ function videoFilesModelToFormattedJSON (
207 id: videoFile.resolution, 211 id: videoFile.resolution,
208 label: videoFile.resolution + 'p' 212 label: videoFile.resolution + 'p'
209 }, 213 },
210 magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs), 214
215 // FIXME: deprecated in 3.2
216 magnetUri: generateMagnetUri(model, video, videoFile, baseUrlHttp, baseUrlWs),
217
211 size: videoFile.size, 218 size: videoFile.size,
212 fps: videoFile.fps, 219 fps: videoFile.fps,
213 torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp), 220
214 torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp), 221 torrentUrl: videoFile.getTorrentUrl(),
215 fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp), 222 torrentDownloadUrl: videoFile.getTorrentDownloadUrl(),
216 fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp), 223
217 metadataUrl: video.getVideoFileMetadataUrl(videoFile, baseUrlHttp) 224 fileUrl: videoFile.getFileUrl(video),
225 fileDownloadUrl: videoFile.getFileDownloadUrl(video),
226
227 metadataUrl: videoFile.metadataUrl ?? getLocalVideoFileMetadataUrl(video, videoFile)
218 } as VideoFile 228 } as VideoFile
219 }) 229 })
220} 230}
221 231
232// FIXME: refactor/merge model and video arguments
222function addVideoFilesInAPAcc ( 233function addVideoFilesInAPAcc (
223 acc: ActivityUrlObject[] | ActivityTagObject[], 234 acc: ActivityUrlObject[] | ActivityTagObject[],
224 model: MVideoAP | MStreamingPlaylistVideo, 235 model: MVideoAP | MStreamingPlaylistVideo,
236 video: MVideoWithHost,
225 baseUrlHttp: string, 237 baseUrlHttp: string,
226 baseUrlWs: string, 238 baseUrlWs: string,
227 files: MVideoFile[] 239 files: MVideoFile[]
@@ -234,7 +246,7 @@ function addVideoFilesInAPAcc (
234 acc.push({ 246 acc.push({
235 type: 'Link', 247 type: 'Link',
236 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any, 248 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any,
237 href: model.getVideoFileUrl(file, baseUrlHttp), 249 href: file.getFileUrl(video),
238 height: file.resolution, 250 height: file.resolution,
239 size: file.size, 251 size: file.size,
240 fps: file.fps 252 fps: file.fps
@@ -244,7 +256,7 @@ function addVideoFilesInAPAcc (
244 type: 'Link', 256 type: 'Link',
245 rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ], 257 rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ],
246 mediaType: 'application/json' as 'application/json', 258 mediaType: 'application/json' as 'application/json',
247 href: extractVideo(model).getVideoFileMetadataUrl(file, baseUrlHttp), 259 href: getLocalVideoFileMetadataUrl(video, file),
248 height: file.resolution, 260 height: file.resolution,
249 fps: file.fps 261 fps: file.fps
250 }) 262 })
@@ -252,14 +264,14 @@ function addVideoFilesInAPAcc (
252 acc.push({ 264 acc.push({
253 type: 'Link', 265 type: 'Link',
254 mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', 266 mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
255 href: model.getTorrentUrl(file, baseUrlHttp), 267 href: file.getTorrentUrl(),
256 height: file.resolution 268 height: file.resolution
257 }) 269 })
258 270
259 acc.push({ 271 acc.push({
260 type: 'Link', 272 type: 'Link',
261 mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', 273 mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
262 href: generateMagnetUri(model, file, baseUrlHttp, baseUrlWs), 274 href: generateMagnetUri(model, video, file, baseUrlHttp, baseUrlWs),
263 height: file.resolution 275 height: file.resolution
264 }) 276 })
265 } 277 }
@@ -307,7 +319,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
307 } 319 }
308 ] 320 ]
309 321
310 addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || []) 322 addVideoFilesInAPAcc(url, video, video, baseUrlHttp, baseUrlWs, video.VideoFiles || [])
311 323
312 for (const playlist of (video.VideoStreamingPlaylists || [])) { 324 for (const playlist of (video.VideoStreamingPlaylists || [])) {
313 const tag = playlist.p2pMediaLoaderInfohashes 325 const tag = playlist.p2pMediaLoaderInfohashes
@@ -320,7 +332,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
320 }) 332 })
321 333
322 const playlistWithVideo = Object.assign(playlist, { Video: video }) 334 const playlistWithVideo = Object.assign(playlist, { Video: video })
323 addVideoFilesInAPAcc(tag, playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles || []) 335 addVideoFilesInAPAcc(tag, playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles || [])
324 336
325 url.push({ 337 url.push({
326 type: 'Link', 338 type: 'Link',
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
index 822d0c89b..af1878e7a 100644
--- a/server/models/video/video-query-builder.ts
+++ b/server/models/video/video-query-builder.ts
@@ -516,6 +516,10 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
516 '"VideoFiles"."resolution"': '"VideoFiles.resolution"', 516 '"VideoFiles"."resolution"': '"VideoFiles.resolution"',
517 '"VideoFiles"."size"': '"VideoFiles.size"', 517 '"VideoFiles"."size"': '"VideoFiles.size"',
518 '"VideoFiles"."extname"': '"VideoFiles.extname"', 518 '"VideoFiles"."extname"': '"VideoFiles.extname"',
519 '"VideoFiles"."filename"': '"VideoFiles.filename"',
520 '"VideoFiles"."fileUrl"': '"VideoFiles.fileUrl"',
521 '"VideoFiles"."torrentFilename"': '"VideoFiles.torrentFilename"',
522 '"VideoFiles"."torrentUrl"': '"VideoFiles.torrentUrl"',
519 '"VideoFiles"."infoHash"': '"VideoFiles.infoHash"', 523 '"VideoFiles"."infoHash"': '"VideoFiles.infoHash"',
520 '"VideoFiles"."fps"': '"VideoFiles.fps"', 524 '"VideoFiles"."fps"': '"VideoFiles.fps"',
521 '"VideoFiles"."videoId"': '"VideoFiles.videoId"', 525 '"VideoFiles"."videoId"': '"VideoFiles.videoId"',
@@ -529,6 +533,10 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
529 '"VideoStreamingPlaylists->VideoFiles"."resolution"': '"VideoStreamingPlaylists.VideoFiles.resolution"', 533 '"VideoStreamingPlaylists->VideoFiles"."resolution"': '"VideoStreamingPlaylists.VideoFiles.resolution"',
530 '"VideoStreamingPlaylists->VideoFiles"."size"': '"VideoStreamingPlaylists.VideoFiles.size"', 534 '"VideoStreamingPlaylists->VideoFiles"."size"': '"VideoStreamingPlaylists.VideoFiles.size"',
531 '"VideoStreamingPlaylists->VideoFiles"."extname"': '"VideoStreamingPlaylists.VideoFiles.extname"', 535 '"VideoStreamingPlaylists->VideoFiles"."extname"': '"VideoStreamingPlaylists.VideoFiles.extname"',
536 '"VideoStreamingPlaylists->VideoFiles"."filename"': '"VideoStreamingPlaylists.VideoFiles.filename"',
537 '"VideoStreamingPlaylists->VideoFiles"."fileUrl"': '"VideoStreamingPlaylists.VideoFiles.fileUrl"',
538 '"VideoStreamingPlaylists->VideoFiles"."torrentFilename"': '"VideoStreamingPlaylists.VideoFiles.torrentFilename"',
539 '"VideoStreamingPlaylists->VideoFiles"."torrentUrl"': '"VideoStreamingPlaylists.VideoFiles.torrentUrl"',
532 '"VideoStreamingPlaylists->VideoFiles"."infoHash"': '"VideoStreamingPlaylists.VideoFiles.infoHash"', 540 '"VideoStreamingPlaylists->VideoFiles"."infoHash"': '"VideoStreamingPlaylists.VideoFiles.infoHash"',
533 '"VideoStreamingPlaylists->VideoFiles"."fps"': '"VideoStreamingPlaylists.VideoFiles.fps"', 541 '"VideoStreamingPlaylists->VideoFiles"."fps"': '"VideoStreamingPlaylists.VideoFiles.fps"',
534 '"VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId"': '"VideoStreamingPlaylists.VideoFiles.videoStreamingPlaylistId"', 542 '"VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId"': '"VideoStreamingPlaylists.VideoFiles.videoStreamingPlaylistId"',
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index 148768c21..c9375b433 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -1,28 +1,18 @@
1import * as memoizee from 'memoizee'
2import { join } from 'path'
3import { Op, QueryTypes } from 'sequelize'
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 5import { VideoFileModel } from '@server/models/video/video-file'
3import { throwIfNotValid } from '../utils' 6import { MStreamingPlaylist } from '@server/types/models'
4import { VideoModel } from './video'
5import { VideoRedundancyModel } from '../redundancy/video-redundancy'
6import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 7import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
8import {
9 CONSTRAINTS_FIELDS,
10 MEMOIZE_LENGTH,
11 MEMOIZE_TTL,
12 P2P_MEDIA_LOADER_PEER_VERSION,
13 STATIC_DOWNLOAD_PATHS,
14 STATIC_PATHS
15} from '../../initializers/constants'
16import { join } from 'path'
17import { sha1 } from '../../helpers/core-utils' 8import { sha1 } from '../../helpers/core-utils'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { isArrayOf } from '../../helpers/custom-validators/misc' 10import { isArrayOf } from '../../helpers/custom-validators/misc'
19import { Op, QueryTypes } from 'sequelize' 11import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
20import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideoFile } from '@server/types/models' 12import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants'
21import { VideoFileModel } from '@server/models/video/video-file' 13import { VideoRedundancyModel } from '../redundancy/video-redundancy'
22import { getTorrentFileName, getTorrentFilePath, getVideoFilename } from '@server/lib/video-paths' 14import { throwIfNotValid } from '../utils'
23import * as memoizee from 'memoizee' 15import { VideoModel } from './video'
24import { remove } from 'fs-extra'
25import { logger } from '@server/helpers/logger'
26 16
27@Table({ 17@Table({
28 tableName: 'videoStreamingPlaylist', 18 tableName: 'videoStreamingPlaylist',
@@ -196,26 +186,6 @@ export class VideoStreamingPlaylistModel extends Model {
196 return 'unknown' 186 return 'unknown'
197 } 187 }
198 188
199 getVideoRedundancyUrl (baseUrlHttp: string) {
200 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
201 }
202
203 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
204 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
205 }
206
207 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
208 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + getVideoFilename(this, videoFile)
209 }
210
211 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
212 return baseUrlHttp + join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, this.Video.uuid, getVideoFilename(this, videoFile))
213 }
214
215 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
216 return baseUrlHttp + join(STATIC_PATHS.TORRENTS, getTorrentFileName(this, videoFile))
217 }
218
219 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) { 189 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
220 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 190 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
221 } 191 }
@@ -224,10 +194,4 @@ export class VideoStreamingPlaylistModel extends Model {
224 return this.type === other.type && 194 return this.type === other.type &&
225 this.videoId === other.videoId 195 this.videoId === other.videoId
226 } 196 }
227
228 removeTorrent (this: MStreamingPlaylistVideo, videoFile: MVideoFile) {
229 const torrentPath = getTorrentFilePath(this, videoFile)
230 return remove(torrentPath)
231 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
232 }
233} 197}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 3321deed3..2e6b6aeec 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -24,10 +24,11 @@ import {
24 Table, 24 Table,
25 UpdatedAt 25 UpdatedAt
26} from 'sequelize-typescript' 26} from 'sequelize-typescript'
27import { v4 as uuidv4 } from 'uuid'
27import { buildNSFWFilter } from '@server/helpers/express-utils' 28import { buildNSFWFilter } from '@server/helpers/express-utils'
28import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
29import { LiveManager } from '@server/lib/live-manager' 30import { LiveManager } from '@server/lib/live-manager'
30import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 31import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths'
31import { getServerActor } from '@server/models/application/application' 32import { getServerActor } from '@server/models/application/application'
32import { ModelCache } from '@server/models/model-cache' 33import { ModelCache } from '@server/models/model-cache'
33import { VideoFile } from '@shared/models/videos/video-file.model' 34import { VideoFile } from '@shared/models/videos/video-file.model'
@@ -60,7 +61,6 @@ import {
60 CONSTRAINTS_FIELDS, 61 CONSTRAINTS_FIELDS,
61 LAZY_STATIC_PATHS, 62 LAZY_STATIC_PATHS,
62 REMOTE_SCHEME, 63 REMOTE_SCHEME,
63 STATIC_DOWNLOAD_PATHS,
64 STATIC_PATHS, 64 STATIC_PATHS,
65 VIDEO_CATEGORIES, 65 VIDEO_CATEGORIES,
66 VIDEO_LANGUAGES, 66 VIDEO_LANGUAGES,
@@ -78,6 +78,7 @@ import {
78 MStreamingPlaylistFilesVideo, 78 MStreamingPlaylistFilesVideo,
79 MUserAccountId, 79 MUserAccountId,
80 MUserId, 80 MUserId,
81 MVideo,
81 MVideoAccountLight, 82 MVideoAccountLight,
82 MVideoAccountLightBlacklistAllFiles, 83 MVideoAccountLightBlacklistAllFiles,
83 MVideoAP, 84 MVideoAP,
@@ -130,7 +131,6 @@ import { VideoShareModel } from './video-share'
130import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 131import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
131import { VideoTagModel } from './video-tag' 132import { VideoTagModel } from './video-tag'
132import { VideoViewModel } from './video-view' 133import { VideoViewModel } from './video-view'
133import { v4 as uuidv4 } from 'uuid'
134 134
135export enum ScopeNames { 135export enum ScopeNames {
136 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 136 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -790,7 +790,7 @@ export class VideoModel extends Model {
790 // Remove physical files and torrents 790 // Remove physical files and torrents
791 instance.VideoFiles.forEach(file => { 791 instance.VideoFiles.forEach(file => {
792 tasks.push(instance.removeFile(file)) 792 tasks.push(instance.removeFile(file))
793 tasks.push(instance.removeTorrent(file)) 793 tasks.push(file.removeTorrent())
794 }) 794 })
795 795
796 // Remove playlists file 796 // Remove playlists file
@@ -853,18 +853,14 @@ export class VideoModel extends Model {
853 return undefined 853 return undefined
854 } 854 }
855 855
856 static listLocal (): Promise<MVideoWithAllFiles[]> { 856 static listLocal (): Promise<MVideo[]> {
857 const query = { 857 const query = {
858 where: { 858 where: {
859 remote: false 859 remote: false
860 } 860 }
861 } 861 }
862 862
863 return VideoModel.scope([ 863 return VideoModel.findAll(query)
864 ScopeNames.WITH_WEBTORRENT_FILES,
865 ScopeNames.WITH_STREAMING_PLAYLISTS,
866 ScopeNames.WITH_THUMBNAILS
867 ]).findAll(query)
868 } 864 }
869 865
870 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 866 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
@@ -1623,6 +1619,10 @@ export class VideoModel extends Model {
1623 'resolution', 1619 'resolution',
1624 'size', 1620 'size',
1625 'extname', 1621 'extname',
1622 'filename',
1623 'fileUrl',
1624 'torrentFilename',
1625 'torrentUrl',
1626 'infoHash', 1626 'infoHash',
1627 'fps', 1627 'fps',
1628 'videoId', 1628 'videoId',
@@ -1891,14 +1891,14 @@ export class VideoModel extends Model {
1891 let files: VideoFile[] = [] 1891 let files: VideoFile[] = []
1892 1892
1893 if (Array.isArray(this.VideoFiles)) { 1893 if (Array.isArray(this.VideoFiles)) {
1894 const result = videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles) 1894 const result = videoFilesModelToFormattedJSON(this, this, baseUrlHttp, baseUrlWs, this.VideoFiles)
1895 files = files.concat(result) 1895 files = files.concat(result)
1896 } 1896 }
1897 1897
1898 for (const p of (this.VideoStreamingPlaylists || [])) { 1898 for (const p of (this.VideoStreamingPlaylists || [])) {
1899 p.Video = this 1899 p.Video = this
1900 1900
1901 const result = videoFilesModelToFormattedJSON(p, baseUrlHttp, baseUrlWs, p.VideoFiles) 1901 const result = videoFilesModelToFormattedJSON(p, this, baseUrlHttp, baseUrlWs, p.VideoFiles)
1902 files = files.concat(result) 1902 files = files.concat(result)
1903 } 1903 }
1904 1904
@@ -1956,12 +1956,6 @@ export class VideoModel extends Model {
1956 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) 1956 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
1957 } 1957 }
1958 1958
1959 removeTorrent (videoFile: MVideoFile) {
1960 const torrentPath = getTorrentFilePath(this, videoFile)
1961 return remove(torrentPath)
1962 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
1963 }
1964
1965 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { 1959 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
1966 const directoryPath = getHLSDirectory(this, isRedundancy) 1960 const directoryPath = getHLSDirectory(this, isRedundancy)
1967 1961
@@ -1977,7 +1971,7 @@ export class VideoModel extends Model {
1977 1971
1978 // Remove physical files and torrents 1972 // Remove physical files and torrents
1979 await Promise.all( 1973 await Promise.all(
1980 streamingPlaylistWithFiles.VideoFiles.map(file => streamingPlaylistWithFiles.removeTorrent(file)) 1974 streamingPlaylistWithFiles.VideoFiles.map(file => file.removeTorrent())
1981 ) 1975 )
1982 } 1976 }
1983 } 1977 }
@@ -2054,34 +2048,6 @@ export class VideoModel extends Model {
2054 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 2048 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
2055 } 2049 }
2056 2050
2057 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2058 return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
2059 }
2060
2061 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2062 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
2063 }
2064
2065 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2066 return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
2067 }
2068
2069 getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2070 const path = '/api/v1/videos/'
2071
2072 return this.isOwned()
2073 ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
2074 : videoFile.metadataUrl
2075 }
2076
2077 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2078 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
2079 }
2080
2081 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2082 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
2083 }
2084
2085 getBandwidthBits (videoFile: MVideoFile) { 2051 getBandwidthBits (videoFile: MVideoFile) {
2086 return Math.ceil((videoFile.size * 8) / this.duration) 2052 return Math.ceil((videoFile.size * 8) / this.duration)
2087 } 2053 }