diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/user/user.ts | 15 | ||||
-rw-r--r-- | server/models/video/formatter/video-format-utils.ts | 24 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 80 | ||||
-rw-r--r-- | server/models/video/video-job-info.ts | 6 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 6 | ||||
-rw-r--r-- | server/models/video/video-streaming-playlist.ts | 62 | ||||
-rw-r--r-- | server/models/video/video.ts | 71 |
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 { | |||
34 | import { | 34 | import { |
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) { | |||
245 | function videoFilesModelToFormattedJSON ( | 248 | function 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 | ||
284 | function addVideoFilesInAPAcc ( | 291 | function 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' | |||
22 | import { logger } from '@server/helpers/logger' | 22 | import { logger } from '@server/helpers/logger' |
23 | import { extractVideo } from '@server/helpers/video' | 23 | import { extractVideo } from '@server/helpers/video' |
24 | import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' | 24 | import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' |
25 | import { getHLSPublicFileUrl, getWebTorrentPublicFileUrl } from '@server/lib/object-storage' | 25 | import { |
26 | getHLSPrivateFileUrl, | ||
27 | getHLSPublicFileUrl, | ||
28 | getWebTorrentPrivateFileUrl, | ||
29 | getWebTorrentPublicFileUrl | ||
30 | } from '@server/lib/object-storage' | ||
26 | import { getFSTorrentFilePath } from '@server/lib/paths' | 31 | import { getFSTorrentFilePath } from '@server/lib/paths' |
32 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | ||
27 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' | 33 | import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' |
28 | import { VideoResolution, VideoStorage } from '@shared/models' | 34 | import { VideoResolution, VideoStorage } from '@shared/models' |
29 | import { AttributesOnly } from '@shared/typescript-utils' | 35 | import { AttributesOnly } from '@shared/typescript-utils' |
@@ -48,6 +54,7 @@ import { doesExist } from '../shared' | |||
48 | import { parseAggregateResult, throwIfNotValid } from '../utils' | 54 | import { parseAggregateResult, throwIfNotValid } from '../utils' |
49 | import { VideoModel } from './video' | 55 | import { VideoModel } from './video' |
50 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 56 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
57 | import { CONFIG } from '@server/initializers/config' | ||
51 | 58 | ||
52 | export enum ScopeNames { | 59 | export 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' |
54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | 54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
55 | import { ActorModel } from '../actor/actor' | 55 | import { 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' |
18 | import { getHLSPublicFileUrl } from '@server/lib/object-storage' | 18 | import { CONFIG } from '@server/initializers/config' |
19 | import { getHLSPrivateFileUrl, getHLSPublicFileUrl } from '@server/lib/object-storage' | ||
19 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths' | 20 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths' |
21 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | ||
20 | import { VideoFileModel } from '@server/models/video/video-file' | 22 | import { VideoFileModel } from '@server/models/video/video-file' |
21 | import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models' | 23 | import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models' |
22 | import { sha1 } from '@shared/extra-utils' | 24 | import { 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' |
27 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 27 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
28 | import { LiveManager } from '@server/lib/live/live-manager' | 28 | import { LiveManager } from '@server/lib/live/live-manager' |
29 | import { removeHLSFileObjectStorage, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' | 29 | import { removeHLSFileObjectStorageByFilename, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' |
30 | import { tracer } from '@server/lib/opentelemetry/tracing' | 30 | import { tracer } from '@server/lib/opentelemetry/tracing' |
31 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' | 31 | import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFilename } from '@server/lib/paths' |
32 | import { VideoPathManager } from '@server/lib/video-path-manager' | 32 | import { VideoPathManager } from '@server/lib/video-path-manager' |
33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | ||
33 | import { getServerActor } from '@server/models/application/application' | 34 | import { getServerActor } from '@server/models/application/application' |
34 | import { ModelCache } from '@server/models/model-cache' | 35 | import { ModelCache } from '@server/models/model-cache' |
35 | import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' | 36 | import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' |
36 | import { ffprobePromise, getAudioStream, uuidToShort } from '@shared/extra-utils' | 37 | import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils' |
37 | import { | 38 | import { |
38 | ResultList, | 39 | ResultList, |
39 | ThumbnailType, | 40 | ThumbnailType, |
@@ -52,7 +53,7 @@ import { | |||
52 | import { AttributesOnly } from '@shared/typescript-utils' | 53 | import { AttributesOnly } from '@shared/typescript-utils' |
53 | import { peertubeTruncate } from '../../helpers/core-utils' | 54 | import { peertubeTruncate } from '../../helpers/core-utils' |
54 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 55 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
55 | import { exists, isBooleanValid } from '../../helpers/custom-validators/misc' | 56 | import { exists, isBooleanValid, isUUIDValid } from '../../helpers/custom-validators/misc' |
56 | import { | 57 | import { |
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 | ||