aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/user/user.ts15
-rw-r--r--server/models/video/formatter/video-format-utils.ts24
-rw-r--r--server/models/video/video-file.ts80
-rw-r--r--server/models/video/video-job-info.ts6
-rw-r--r--server/models/video/video-playlist.ts6
-rw-r--r--server/models/video/video-streaming-playlist.ts62
-rw-r--r--server/models/video/video.ts71
7 files changed, 201 insertions, 63 deletions
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
index 1a7c84390..f70feed73 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -403,6 +403,11 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
403 @Column 403 @Column
404 lastLoginDate: Date 404 lastLoginDate: Date
405 405
406 @AllowNull(true)
407 @Default(null)
408 @Column
409 otpSecret: string
410
406 @CreatedAt 411 @CreatedAt
407 createdAt: Date 412 createdAt: Date
408 413
@@ -886,8 +891,10 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
886 autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist, 891 autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist,
887 videoLanguages: this.videoLanguages, 892 videoLanguages: this.videoLanguages,
888 893
889 role: this.role, 894 role: {
890 roleLabel: USER_ROLE_LABELS[this.role], 895 id: this.role,
896 label: USER_ROLE_LABELS[this.role]
897 },
891 898
892 videoQuota: this.videoQuota, 899 videoQuota: this.videoQuota,
893 videoQuotaDaily: this.videoQuotaDaily, 900 videoQuotaDaily: this.videoQuotaDaily,
@@ -935,7 +942,9 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
935 942
936 pluginAuth: this.pluginAuth, 943 pluginAuth: this.pluginAuth,
937 944
938 lastLoginDate: this.lastLoginDate 945 lastLoginDate: this.lastLoginDate,
946
947 twoFactorEnabled: !!this.otpSecret
939 } 948 }
940 949
941 if (parameters.withAdminFlags) { 950 if (parameters.withAdminFlags) {
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts
index e1b0eb610..240619f69 100644
--- a/server/models/video/formatter/video-format-utils.ts
+++ b/server/models/video/formatter/video-format-utils.ts
@@ -34,6 +34,7 @@ import {
34import { 34import {
35 MServer, 35 MServer,
36 MStreamingPlaylistRedundanciesOpt, 36 MStreamingPlaylistRedundanciesOpt,
37 MUserId,
37 MVideo, 38 MVideo,
38 MVideoAP, 39 MVideoAP,
39 MVideoFile, 40 MVideoFile,
@@ -102,6 +103,7 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoForm
102 }, 103 },
103 nsfw: video.nsfw, 104 nsfw: video.nsfw,
104 105
106 truncatedDescription: video.getTruncatedDescription(),
105 description: options && options.completeDescription === true 107 description: options && options.completeDescription === true
106 ? video.description 108 ? video.description
107 : video.getTruncatedDescription(), 109 : video.getTruncatedDescription(),
@@ -180,6 +182,7 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
180 const span = tracer.startSpan('peertube.VideoModel.toFormattedDetailsJSON') 182 const span = tracer.startSpan('peertube.VideoModel.toFormattedDetailsJSON')
181 183
182 const videoJSON = video.toFormattedJSON({ 184 const videoJSON = video.toFormattedJSON({
185 completeDescription: true,
183 additionalAttributes: { 186 additionalAttributes: {
184 scheduledUpdate: true, 187 scheduledUpdate: true,
185 blacklistInfo: true, 188 blacklistInfo: true,
@@ -245,8 +248,12 @@ function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) {
245function videoFilesModelToFormattedJSON ( 248function videoFilesModelToFormattedJSON (
246 video: MVideoFormattable, 249 video: MVideoFormattable,
247 videoFiles: MVideoFileRedundanciesOpt[], 250 videoFiles: MVideoFileRedundanciesOpt[],
248 includeMagnet = true 251 options: {
252 includeMagnet?: boolean // default true
253 } = {}
249): VideoFile[] { 254): VideoFile[] {
255 const { includeMagnet = true } = options
256
250 const trackerUrls = includeMagnet 257 const trackerUrls = includeMagnet
251 ? video.getTrackerUrls() 258 ? video.getTrackerUrls()
252 : [] 259 : []
@@ -281,11 +288,14 @@ function videoFilesModelToFormattedJSON (
281 }) 288 })
282} 289}
283 290
284function addVideoFilesInAPAcc ( 291function addVideoFilesInAPAcc (options: {
285 acc: ActivityUrlObject[] | ActivityTagObject[], 292 acc: ActivityUrlObject[] | ActivityTagObject[]
286 video: MVideo, 293 video: MVideo
287 files: MVideoFile[] 294 files: MVideoFile[]
288) { 295 user?: MUserId
296}) {
297 const { acc, video, files } = options
298
289 const trackerUrls = video.getTrackerUrls() 299 const trackerUrls = video.getTrackerUrls()
290 300
291 const sortedFiles = (files || []) 301 const sortedFiles = (files || [])
@@ -370,7 +380,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
370 } 380 }
371 ] 381 ]
372 382
373 addVideoFilesInAPAcc(url, video, video.VideoFiles || []) 383 addVideoFilesInAPAcc({ acc: url, video, files: video.VideoFiles || [] })
374 384
375 for (const playlist of (video.VideoStreamingPlaylists || [])) { 385 for (const playlist of (video.VideoStreamingPlaylists || [])) {
376 const tag = playlist.p2pMediaLoaderInfohashes 386 const tag = playlist.p2pMediaLoaderInfohashes
@@ -382,7 +392,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
382 href: playlist.getSha256SegmentsUrl(video) 392 href: playlist.getSha256SegmentsUrl(video)
383 }) 393 })
384 394
385 addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || []) 395 addVideoFilesInAPAcc({ acc: tag, video, files: playlist.VideoFiles || [] })
386 396
387 url.push({ 397 url.push({
388 type: 'Link', 398 type: 'Link',
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index d4f07f85f..9c4e6d078 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -22,8 +22,14 @@ import validator from 'validator'
22import { logger } from '@server/helpers/logger' 22import { logger } from '@server/helpers/logger'
23import { extractVideo } from '@server/helpers/video' 23import { extractVideo } from '@server/helpers/video'
24import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' 24import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url'
25import { getHLSPublicFileUrl, getWebTorrentPublicFileUrl } from '@server/lib/object-storage' 25import {
26 getHLSPrivateFileUrl,
27 getHLSPublicFileUrl,
28 getWebTorrentPrivateFileUrl,
29 getWebTorrentPublicFileUrl
30} from '@server/lib/object-storage'
26import { getFSTorrentFilePath } from '@server/lib/paths' 31import { getFSTorrentFilePath } from '@server/lib/paths'
32import { isVideoInPrivateDirectory } from '@server/lib/video-privacy'
27import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' 33import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models'
28import { VideoResolution, VideoStorage } from '@shared/models' 34import { VideoResolution, VideoStorage } from '@shared/models'
29import { AttributesOnly } from '@shared/typescript-utils' 35import { AttributesOnly } from '@shared/typescript-utils'
@@ -48,6 +54,7 @@ import { doesExist } from '../shared'
48import { parseAggregateResult, throwIfNotValid } from '../utils' 54import { parseAggregateResult, throwIfNotValid } from '../utils'
49import { VideoModel } from './video' 55import { VideoModel } from './video'
50import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 56import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
57import { CONFIG } from '@server/initializers/config'
51 58
52export enum ScopeNames { 59export enum ScopeNames {
53 WITH_VIDEO = 'WITH_VIDEO', 60 WITH_VIDEO = 'WITH_VIDEO',
@@ -295,6 +302,16 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
295 return VideoFileModel.findOne(query) 302 return VideoFileModel.findOne(query)
296 } 303 }
297 304
305 static loadWithVideoByFilename (filename: string): Promise<MVideoFileVideo | MVideoFileStreamingPlaylistVideo> {
306 const query = {
307 where: {
308 filename
309 }
310 }
311
312 return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query)
313 }
314
298 static loadWithVideoOrPlaylistByTorrentFilename (filename: string) { 315 static loadWithVideoOrPlaylistByTorrentFilename (filename: string) {
299 const query = { 316 const query = {
300 where: { 317 where: {
@@ -305,6 +322,10 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
305 return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query) 322 return VideoFileModel.scope(ScopeNames.WITH_VIDEO_OR_PLAYLIST).findOne(query)
306 } 323 }
307 324
325 static load (id: number): Promise<MVideoFile> {
326 return VideoFileModel.findByPk(id)
327 }
328
308 static loadWithMetadata (id: number) { 329 static loadWithMetadata (id: number) {
309 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id) 330 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
310 } 331 }
@@ -467,7 +488,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
467 } 488 }
468 489
469 getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo { 490 getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
470 if (this.videoId) return (this as MVideoFileVideo).Video 491 if (this.videoId || (this as MVideoFileVideo).Video) return (this as MVideoFileVideo).Video
471 492
472 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist 493 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
473 } 494 }
@@ -488,7 +509,25 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
488 return !!this.videoStreamingPlaylistId 509 return !!this.videoStreamingPlaylistId
489 } 510 }
490 511
491 getObjectStorageUrl () { 512 // ---------------------------------------------------------------------------
513
514 getObjectStorageUrl (video: MVideo) {
515 if (video.hasPrivateStaticPath() && CONFIG.OBJECT_STORAGE.PROXY.PROXIFY_PRIVATE_FILES === true) {
516 return this.getPrivateObjectStorageUrl(video)
517 }
518
519 return this.getPublicObjectStorageUrl()
520 }
521
522 private getPrivateObjectStorageUrl (video: MVideo) {
523 if (this.isHLS()) {
524 return getHLSPrivateFileUrl(video, this.filename)
525 }
526
527 return getWebTorrentPrivateFileUrl(this.filename)
528 }
529
530 private getPublicObjectStorageUrl () {
492 if (this.isHLS()) { 531 if (this.isHLS()) {
493 return getHLSPublicFileUrl(this.fileUrl) 532 return getHLSPublicFileUrl(this.fileUrl)
494 } 533 }
@@ -496,23 +535,46 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
496 return getWebTorrentPublicFileUrl(this.fileUrl) 535 return getWebTorrentPublicFileUrl(this.fileUrl)
497 } 536 }
498 537
538 // ---------------------------------------------------------------------------
539
499 getFileUrl (video: MVideo) { 540 getFileUrl (video: MVideo) {
500 if (this.storage === VideoStorage.OBJECT_STORAGE) { 541 if (video.isOwned()) {
501 return this.getObjectStorageUrl() 542 if (this.storage === VideoStorage.OBJECT_STORAGE) {
502 } 543 return this.getObjectStorageUrl(video)
544 }
503 545
504 if (!this.Video) this.Video = video as VideoModel 546 return WEBSERVER.URL + this.getFileStaticPath(video)
505 if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video) 547 }
506 548
507 return this.fileUrl 549 return this.fileUrl
508 } 550 }
509 551
552 // ---------------------------------------------------------------------------
553
510 getFileStaticPath (video: MVideo) { 554 getFileStaticPath (video: MVideo) {
511 if (this.isHLS()) return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.filename) 555 if (this.isHLS()) return this.getHLSFileStaticPath(video)
556
557 return this.getWebTorrentFileStaticPath(video)
558 }
559
560 private getWebTorrentFileStaticPath (video: MVideo) {
561 if (isVideoInPrivateDirectory(video.privacy)) {
562 return join(STATIC_PATHS.PRIVATE_WEBSEED, this.filename)
563 }
512 564
513 return join(STATIC_PATHS.WEBSEED, this.filename) 565 return join(STATIC_PATHS.WEBSEED, this.filename)
514 } 566 }
515 567
568 private getHLSFileStaticPath (video: MVideo) {
569 if (isVideoInPrivateDirectory(video.privacy)) {
570 return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.filename)
571 }
572
573 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.filename)
574 }
575
576 // ---------------------------------------------------------------------------
577
516 getFileDownloadUrl (video: MVideoWithHost) { 578 getFileDownloadUrl (video: MVideoWithHost) {
517 const path = this.isHLS() 579 const path = this.isHLS()
518 ? join(STATIC_DOWNLOAD_PATHS.HLS_VIDEOS, `${video.uuid}-${this.resolution}-fragmented${this.extname}`) 580 ? join(STATIC_DOWNLOAD_PATHS.HLS_VIDEOS, `${video.uuid}-${this.resolution}-fragmented${this.extname}`)
diff --git a/server/models/video/video-job-info.ts b/server/models/video/video-job-info.ts
index 7497addf1..740f6b5c6 100644
--- a/server/models/video/video-job-info.ts
+++ b/server/models/video/video-job-info.ts
@@ -84,7 +84,7 @@ export class VideoJobInfoModel extends Model<Partial<AttributesOnly<VideoJobInfo
84 static async decrease (videoUUID: string, column: VideoJobInfoColumnType): Promise<number> { 84 static async decrease (videoUUID: string, column: VideoJobInfoColumnType): Promise<number> {
85 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind: { videoUUID } } 85 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind: { videoUUID } }
86 86
87 const [ { pendingMove } ] = await VideoJobInfoModel.sequelize.query<{ pendingMove: number }>(` 87 const result = await VideoJobInfoModel.sequelize.query<{ pendingMove: number }>(`
88 UPDATE 88 UPDATE
89 "videoJobInfo" 89 "videoJobInfo"
90 SET 90 SET
@@ -97,7 +97,9 @@ export class VideoJobInfoModel extends Model<Partial<AttributesOnly<VideoJobInfo
97 "${column}"; 97 "${column}";
98 `, options) 98 `, options)
99 99
100 return pendingMove 100 if (result.length === 0) return undefined
101
102 return result[0].pendingMove
101 } 103 }
102 104
103 static async abortAllTasks (videoUUID: string, column: VideoJobInfoColumnType): Promise<void> { 105 static async abortAllTasks (videoUUID: string, column: VideoJobInfoColumnType): Promise<void> {
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index 81ce3dc9e..8bbe54c49 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -49,7 +49,7 @@ import {
49 MVideoPlaylistFormattable, 49 MVideoPlaylistFormattable,
50 MVideoPlaylistFull, 50 MVideoPlaylistFull,
51 MVideoPlaylistFullSummary, 51 MVideoPlaylistFullSummary,
52 MVideoPlaylistIdWithElements 52 MVideoPlaylistSummaryWithElements
53} from '../../types/models/video/video-playlist' 53} from '../../types/models/video/video-playlist'
54import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' 54import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
55import { ActorModel } from '../actor/actor' 55import { ActorModel } from '../actor/actor'
@@ -470,9 +470,9 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli
470 })) 470 }))
471 } 471 }
472 472
473 static listPlaylistIdsOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistIdWithElements[]> { 473 static listPlaylistSummariesOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistSummaryWithElements[]> {
474 const query = { 474 const query = {
475 attributes: [ 'id' ], 475 attributes: [ 'id', 'name', 'uuid' ],
476 where: { 476 where: {
477 ownerAccountId: accountId 477 ownerAccountId: accountId
478 }, 478 },
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index f587989dc..0386edf28 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -15,8 +15,10 @@ import {
15 Table, 15 Table,
16 UpdatedAt 16 UpdatedAt
17} from 'sequelize-typescript' 17} from 'sequelize-typescript'
18import { getHLSPublicFileUrl } from '@server/lib/object-storage' 18import { CONFIG } from '@server/initializers/config'
19import { getHLSPrivateFileUrl, getHLSPublicFileUrl } from '@server/lib/object-storage'
19import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths' 20import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths'
21import { isVideoInPrivateDirectory } from '@server/lib/video-privacy'
20import { VideoFileModel } from '@server/models/video/video-file' 22import { VideoFileModel } from '@server/models/video/video-file'
21import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models' 23import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models'
22import { sha1 } from '@shared/extra-utils' 24import { sha1 } from '@shared/extra-utils'
@@ -244,26 +246,52 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
244 this.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(masterPlaylistUrl, files) 246 this.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(masterPlaylistUrl, files)
245 } 247 }
246 248
249 // ---------------------------------------------------------------------------
250
247 getMasterPlaylistUrl (video: MVideo) { 251 getMasterPlaylistUrl (video: MVideo) {
248 if (this.storage === VideoStorage.OBJECT_STORAGE) { 252 if (video.isOwned()) {
249 return getHLSPublicFileUrl(this.playlistUrl) 253 if (this.storage === VideoStorage.OBJECT_STORAGE) {
250 } 254 return this.getMasterPlaylistObjectStorageUrl(video)
255 }
251 256
252 if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid) 257 return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video)
258 }
253 259
254 return this.playlistUrl 260 return this.playlistUrl
255 } 261 }
256 262
257 getSha256SegmentsUrl (video: MVideo) { 263 private getMasterPlaylistObjectStorageUrl (video: MVideo) {
258 if (this.storage === VideoStorage.OBJECT_STORAGE) { 264 if (video.hasPrivateStaticPath() && CONFIG.OBJECT_STORAGE.PROXY.PROXIFY_PRIVATE_FILES === true) {
259 return getHLSPublicFileUrl(this.segmentsSha256Url) 265 return getHLSPrivateFileUrl(video, this.playlistFilename)
260 } 266 }
261 267
262 if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive) 268 return getHLSPublicFileUrl(this.playlistUrl)
269 }
270
271 // ---------------------------------------------------------------------------
272
273 getSha256SegmentsUrl (video: MVideo) {
274 if (video.isOwned()) {
275 if (this.storage === VideoStorage.OBJECT_STORAGE) {
276 return this.getSha256SegmentsObjectStorageUrl(video)
277 }
278
279 return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video)
280 }
263 281
264 return this.segmentsSha256Url 282 return this.segmentsSha256Url
265 } 283 }
266 284
285 private getSha256SegmentsObjectStorageUrl (video: MVideo) {
286 if (video.hasPrivateStaticPath() && CONFIG.OBJECT_STORAGE.PROXY.PROXIFY_PRIVATE_FILES === true) {
287 return getHLSPrivateFileUrl(video, this.segmentsSha256Filename)
288 }
289
290 return getHLSPublicFileUrl(this.segmentsSha256Url)
291 }
292
293 // ---------------------------------------------------------------------------
294
267 getStringType () { 295 getStringType () {
268 if (this.type === VideoStreamingPlaylistType.HLS) return 'hls' 296 if (this.type === VideoStreamingPlaylistType.HLS) return 'hls'
269 297
@@ -283,13 +311,19 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
283 return Object.assign(this, { Video: video }) 311 return Object.assign(this, { Video: video })
284 } 312 }
285 313
286 private getMasterPlaylistStaticPath (videoUUID: string) { 314 private getMasterPlaylistStaticPath (video: MVideo) {
287 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename) 315 if (isVideoInPrivateDirectory(video.privacy)) {
316 return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.playlistFilename)
317 }
318
319 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.playlistFilename)
288 } 320 }
289 321
290 private getSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) { 322 private getSha256SegmentsStaticPath (video: MVideo) {
291 if (isLive) return join('/live', 'segments-sha256', videoUUID) 323 if (isVideoInPrivateDirectory(video.privacy)) {
324 return join(STATIC_PATHS.STREAMING_PLAYLISTS.PRIVATE_HLS, video.uuid, this.segmentsSha256Filename)
325 }
292 326
293 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.segmentsSha256Filename) 327 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, video.uuid, this.segmentsSha256Filename)
294 } 328 }
295} 329}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 468117504..56cc45cfe 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -26,14 +26,15 @@ import {
26} from 'sequelize-typescript' 26} from 'sequelize-typescript'
27import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 27import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
28import { LiveManager } from '@server/lib/live/live-manager' 28import { LiveManager } from '@server/lib/live/live-manager'
29import { removeHLSFileObjectStorage, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' 29import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage'
30import { tracer } from '@server/lib/opentelemetry/tracing' 30import { tracer } from '@server/lib/opentelemetry/tracing'
31import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' 31import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths'
32import { VideoPathManager } from '@server/lib/video-path-manager' 32import { VideoPathManager } from '@server/lib/video-path-manager'
33import { isVideoInPrivateDirectory } from '@server/lib/video-privacy'
33import { getServerActor } from '@server/models/application/application' 34import { getServerActor } from '@server/models/application/application'
34import { ModelCache } from '@server/models/model-cache' 35import { ModelCache } from '@server/models/model-cache'
35import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' 36import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils'
36import { ffprobePromise, getAudioStream, uuidToShort } from '@shared/extra-utils' 37import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils'
37import { 38import {
38 ResultList, 39 ResultList,
39 ThumbnailType, 40 ThumbnailType,
@@ -52,7 +53,7 @@ import {
52import { AttributesOnly } from '@shared/typescript-utils' 53import { AttributesOnly } from '@shared/typescript-utils'
53import { peertubeTruncate } from '../../helpers/core-utils' 54import { peertubeTruncate } from '../../helpers/core-utils'
54import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 55import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
55import { exists, isBooleanValid } from '../../helpers/custom-validators/misc' 56import { exists, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc'
56import { 57import {
57 isVideoDescriptionValid, 58 isVideoDescriptionValid,
58 isVideoDurationValid, 59 isVideoDurationValid,
@@ -784,9 +785,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
784 785
785 // Do not wait video deletion because we could be in a transaction 786 // Do not wait video deletion because we could be in a transaction
786 Promise.all(tasks) 787 Promise.all(tasks)
787 .catch(err => { 788 .then(() => logger.info('Removed files of video %s.', instance.url))
788 logger.error('Some errors when removing files of video %s in before destroy hook.', instance.uuid, { err }) 789 .catch(err => logger.error('Some errors when removing files of video %s in before destroy hook.', instance.uuid, { err }))
789 })
790 790
791 return undefined 791 return undefined
792 } 792 }
@@ -1458,6 +1458,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1458 const query = 'SELECT 1 FROM "videoShare" ' + 1458 const query = 'SELECT 1 FROM "videoShare" ' +
1459 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 1459 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
1460 'WHERE "actorFollow"."actorId" = $followerActorId AND "actorFollow"."state" = \'accepted\' AND "videoShare"."videoId" = $videoId ' + 1460 'WHERE "actorFollow"."actorId" = $followerActorId AND "actorFollow"."state" = \'accepted\' AND "videoShare"."videoId" = $videoId ' +
1461 'UNION ' +
1462 'SELECT 1 FROM "video" ' +
1463 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
1464 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
1465 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "account"."actorId" ' +
1466 'WHERE "actorFollow"."actorId" = $followerActorId AND "actorFollow"."state" = \'accepted\' AND "video"."id" = $videoId ' +
1461 'LIMIT 1' 1467 'LIMIT 1'
1462 1468
1463 const options = { 1469 const options = {
@@ -1696,12 +1702,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1696 let files: VideoFile[] = [] 1702 let files: VideoFile[] = []
1697 1703
1698 if (Array.isArray(this.VideoFiles)) { 1704 if (Array.isArray(this.VideoFiles)) {
1699 const result = videoFilesModelToFormattedJSON(this, this.VideoFiles, includeMagnet) 1705 const result = videoFilesModelToFormattedJSON(this, this.VideoFiles, { includeMagnet })
1700 files = files.concat(result) 1706 files = files.concat(result)
1701 } 1707 }
1702 1708
1703 for (const p of (this.VideoStreamingPlaylists || [])) { 1709 for (const p of (this.VideoStreamingPlaylists || [])) {
1704 const result = videoFilesModelToFormattedJSON(this, p.VideoFiles, includeMagnet) 1710 const result = videoFilesModelToFormattedJSON(this, p.VideoFiles, { includeMagnet })
1705 files = files.concat(result) 1711 files = files.concat(result)
1706 } 1712 }
1707 1713
@@ -1745,9 +1751,11 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1745 const probe = await ffprobePromise(originalFilePath) 1751 const probe = await ffprobePromise(originalFilePath)
1746 1752
1747 const { audioStream } = await getAudioStream(originalFilePath, probe) 1753 const { audioStream } = await getAudioStream(originalFilePath, probe)
1754 const hasAudio = await hasAudioStream(originalFilePath, probe)
1748 1755
1749 return { 1756 return {
1750 audioStream, 1757 audioStream,
1758 hasAudio,
1751 1759
1752 ...await getVideoStreamDimensionsInfo(originalFilePath, probe) 1760 ...await getVideoStreamDimensionsInfo(originalFilePath, probe)
1753 } 1761 }
@@ -1764,9 +1772,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1764 const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) 1772 const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
1765 if (!playlist) return undefined 1773 if (!playlist) return undefined
1766 1774
1767 playlist.Video = this 1775 return playlist.withVideo(this)
1768
1769 return playlist
1770 } 1776 }
1771 1777
1772 setHLSPlaylist (playlist: MStreamingPlaylist) { 1778 setHLSPlaylist (playlist: MStreamingPlaylist) {
@@ -1832,8 +1838,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1832 await remove(VideoPathManager.Instance.getFSHLSOutputPath(this, resolutionFilename)) 1838 await remove(VideoPathManager.Instance.getFSHLSOutputPath(this, resolutionFilename))
1833 1839
1834 if (videoFile.storage === VideoStorage.OBJECT_STORAGE) { 1840 if (videoFile.storage === VideoStorage.OBJECT_STORAGE) {
1835 await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), videoFile.filename) 1841 await removeHLSFileObjectStorageByFilename(streamingPlaylist.withVideo(this), videoFile.filename)
1836 await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), resolutionFilename) 1842 await removeHLSFileObjectStorageByFilename(streamingPlaylist.withVideo(this), resolutionFilename)
1837 } 1843 }
1838 } 1844 }
1839 1845
@@ -1842,7 +1848,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1842 await remove(filePath) 1848 await remove(filePath)
1843 1849
1844 if (streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) { 1850 if (streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) {
1845 await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), filename) 1851 await removeHLSFileObjectStorageByFilename(streamingPlaylist.withVideo(this), filename)
1846 } 1852 }
1847 } 1853 }
1848 1854
@@ -1868,24 +1874,39 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1868 return setAsUpdated('video', this.id, transaction) 1874 return setAsUpdated('video', this.id, transaction)
1869 } 1875 }
1870 1876
1871 requiresAuth () { 1877 // ---------------------------------------------------------------------------
1872 return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist
1873 }
1874 1878
1875 setPrivacy (newPrivacy: VideoPrivacy) { 1879 requiresAuth (options: {
1876 if (this.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) { 1880 urlParamId: string
1877 this.publishedAt = new Date() 1881 checkBlacklist: boolean
1882 }) {
1883 const { urlParamId, checkBlacklist } = options
1884
1885 if (this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL) {
1886 return true
1887 }
1888
1889 if (this.privacy === VideoPrivacy.UNLISTED) {
1890 if (urlParamId && !isUUIDValid(urlParamId)) return true
1891
1892 return false
1893 }
1894
1895 if (checkBlacklist && this.VideoBlacklist) return true
1896
1897 if (this.privacy !== VideoPrivacy.PUBLIC) {
1898 throw new Error(`Unknown video privacy ${this.privacy} to know if the video requires auth`)
1878 } 1899 }
1879 1900
1880 this.privacy = newPrivacy 1901 return false
1881 } 1902 }
1882 1903
1883 isConfidential () { 1904 hasPrivateStaticPath () {
1884 return this.privacy === VideoPrivacy.PRIVATE || 1905 return isVideoInPrivateDirectory(this.privacy)
1885 this.privacy === VideoPrivacy.UNLISTED ||
1886 this.privacy === VideoPrivacy.INTERNAL
1887 } 1906 }
1888 1907
1908 // ---------------------------------------------------------------------------
1909
1889 async setNewState (newState: VideoState, isNewVideo: boolean, transaction: Transaction) { 1910 async setNewState (newState: VideoState, isNewVideo: boolean, transaction: Transaction) {
1890 if (this.state === newState) throw new Error('Cannot use same state ' + newState) 1911 if (this.state === newState) throw new Error('Cannot use same state ' + newState)
1891 1912