aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/abuse/abuse-query-builder.ts5
-rw-r--r--server/models/abuse/abuse.ts2
-rw-r--r--server/models/actor/actor.ts4
-rw-r--r--server/models/server/plugin.ts13
-rw-r--r--server/models/user/user-notification.ts3
-rw-r--r--server/models/user/user.ts30
-rw-r--r--server/models/utils.ts5
-rw-r--r--server/models/video/formatter/video-format-utils.ts26
-rw-r--r--server/models/video/sql/video/shared/abstract-video-query-builder.ts2
-rw-r--r--server/models/video/sql/video/videos-id-list-query-builder.ts5
-rw-r--r--server/models/video/video-channel.ts4
-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-element.ts21
-rw-r--r--server/models/video/video-playlist.ts6
-rw-r--r--server/models/video/video-share.ts5
-rw-r--r--server/models/video/video-streaming-playlist.ts62
-rw-r--r--server/models/video/video.ts71
18 files changed, 250 insertions, 100 deletions
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/abuse-query-builder.ts
index cfc924ba4..74f4542e5 100644
--- a/server/models/abuse/abuse-query-builder.ts
+++ b/server/models/abuse/abuse-query-builder.ts
@@ -1,5 +1,6 @@
1 1
2import { exists } from '@server/helpers/custom-validators/misc' 2import { exists } from '@server/helpers/custom-validators/misc'
3import { forceNumber } from '@shared/core-utils'
3import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' 4import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models'
4import { buildBlockedAccountSQL, buildDirectionAndField } from '../utils' 5import { buildBlockedAccountSQL, buildDirectionAndField } from '../utils'
5 6
@@ -135,12 +136,12 @@ function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' |
135 } 136 }
136 137
137 if (exists(options.count)) { 138 if (exists(options.count)) {
138 const count = parseInt(options.count + '', 10) 139 const count = forceNumber(options.count)
139 suffix += `LIMIT ${count} ` 140 suffix += `LIMIT ${count} `
140 } 141 }
141 142
142 if (exists(options.start)) { 143 if (exists(options.start)) {
143 const start = parseInt(options.start + '', 10) 144 const start = forceNumber(options.start)
144 suffix += `OFFSET ${start} ` 145 suffix += `OFFSET ${start} `
145 } 146 }
146 } 147 }
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts
index f85f48e86..4c6a96a86 100644
--- a/server/models/abuse/abuse.ts
+++ b/server/models/abuse/abuse.ts
@@ -436,7 +436,7 @@ export class AbuseModel extends Model<Partial<AttributesOnly<AbuseModel>>> {
436 436
437 buildBaseVideoCommentAbuse (this: MAbuseUserFormattable) { 437 buildBaseVideoCommentAbuse (this: MAbuseUserFormattable) {
438 // Associated video comment could have been destroyed if the video has been deleted 438 // Associated video comment could have been destroyed if the video has been deleted
439 if (!this.VideoCommentAbuse || !this.VideoCommentAbuse.VideoComment) return null 439 if (!this.VideoCommentAbuse?.VideoComment) return null
440 440
441 const entity = this.VideoCommentAbuse.VideoComment 441 const entity = this.VideoCommentAbuse.VideoComment
442 442
diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts
index 88db241dc..d7afa727d 100644
--- a/server/models/actor/actor.ts
+++ b/server/models/actor/actor.ts
@@ -18,7 +18,7 @@ import {
18import { activityPubContextify } from '@server/lib/activitypub/context' 18import { activityPubContextify } from '@server/lib/activitypub/context'
19import { getBiggestActorImage } from '@server/lib/actor-image' 19import { getBiggestActorImage } from '@server/lib/actor-image'
20import { ModelCache } from '@server/models/model-cache' 20import { ModelCache } from '@server/models/model-cache'
21import { getLowercaseExtension } from '@shared/core-utils' 21import { forceNumber, getLowercaseExtension } from '@shared/core-utils'
22import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' 22import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models'
23import { AttributesOnly } from '@shared/typescript-utils' 23import { AttributesOnly } from '@shared/typescript-utils'
24import { 24import {
@@ -446,7 +446,7 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
446 } 446 }
447 447
448 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) { 448 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
449 const sanitizedOfId = parseInt(ofId + '', 10) 449 const sanitizedOfId = forceNumber(ofId)
450 const where = { id: sanitizedOfId } 450 const where = { id: sanitizedOfId }
451 451
452 let columnToUpdate: string 452 let columnToUpdate: string
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts
index fa5b4cc4b..71c205ffa 100644
--- a/server/models/server/plugin.ts
+++ b/server/models/server/plugin.ts
@@ -7,8 +7,9 @@ import {
7 isPluginDescriptionValid, 7 isPluginDescriptionValid,
8 isPluginHomepage, 8 isPluginHomepage,
9 isPluginNameValid, 9 isPluginNameValid,
10 isPluginTypeValid, 10 isPluginStableOrUnstableVersionValid,
11 isPluginVersionValid 11 isPluginStableVersionValid,
12 isPluginTypeValid
12} from '../../helpers/custom-validators/plugins' 13} from '../../helpers/custom-validators/plugins'
13import { getSort, throwIfNotValid } from '../utils' 14import { getSort, throwIfNotValid } from '../utils'
14 15
@@ -40,12 +41,12 @@ export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> {
40 type: number 41 type: number
41 42
42 @AllowNull(false) 43 @AllowNull(false)
43 @Is('PluginVersion', value => throwIfNotValid(value, isPluginVersionValid, 'version')) 44 @Is('PluginVersion', value => throwIfNotValid(value, isPluginStableOrUnstableVersionValid, 'version'))
44 @Column 45 @Column
45 version: string 46 version: string
46 47
47 @AllowNull(true) 48 @AllowNull(true)
48 @Is('PluginLatestVersion', value => throwIfNotValid(value, isPluginVersionValid, 'version')) 49 @Is('PluginLatestVersion', value => throwIfNotValid(value, isPluginStableVersionValid, 'version'))
49 @Column 50 @Column
50 latestVersion: string 51 latestVersion: string
51 52
@@ -121,7 +122,7 @@ export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> {
121 122
122 return PluginModel.findOne(query) 123 return PluginModel.findOne(query)
123 .then(p => { 124 .then(p => {
124 if (!p || !p.settings || p.settings === undefined) { 125 if (!p?.settings || p.settings === undefined) {
125 const registered = registeredSettings.find(s => s.name === settingName) 126 const registered = registeredSettings.find(s => s.name === settingName)
126 if (!registered || registered.default === undefined) return undefined 127 if (!registered || registered.default === undefined) return undefined
127 128
@@ -151,7 +152,7 @@ export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> {
151 const result: SettingEntries = {} 152 const result: SettingEntries = {}
152 153
153 for (const name of settingNames) { 154 for (const name of settingNames) {
154 if (!p || !p.settings || p.settings[name] === undefined) { 155 if (!p?.settings || p.settings[name] === undefined) {
155 const registered = registeredSettings.find(s => s.name === name) 156 const registered = registeredSettings.find(s => s.name === name)
156 157
157 if (registered?.default !== undefined) { 158 if (registered?.default !== undefined) {
diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts
index 6209cb4bf..d37fa5dc7 100644
--- a/server/models/user/user-notification.ts
+++ b/server/models/user/user-notification.ts
@@ -2,6 +2,7 @@ import { ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { getBiggestActorImage } from '@server/lib/actor-image' 3import { getBiggestActorImage } from '@server/lib/actor-image'
4import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user' 4import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
5import { forceNumber } from '@shared/core-utils'
5import { uuidToShort } from '@shared/extra-utils' 6import { uuidToShort } from '@shared/extra-utils'
6import { UserNotification, UserNotificationType } from '@shared/models' 7import { UserNotification, UserNotificationType } from '@shared/models'
7import { AttributesOnly } from '@shared/typescript-utils' 8import { AttributesOnly } from '@shared/typescript-utils'
@@ -284,7 +285,7 @@ export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNoti
284 } 285 }
285 286
286 static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) { 287 static removeNotificationsOf (options: { id: number, type: 'account' | 'server', forUserId?: number }) {
287 const id = parseInt(options.id + '', 10) 288 const id = forceNumber(options.id)
288 289
289 function buildAccountWhereQuery (base: string) { 290 function buildAccountWhereQuery (base: string) {
290 const whereSuffix = options.forUserId 291 const whereSuffix = options.forUserId
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
index 1a7c84390..672728a2a 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -70,6 +70,7 @@ import { VideoImportModel } from '../video/video-import'
70import { VideoLiveModel } from '../video/video-live' 70import { VideoLiveModel } from '../video/video-live'
71import { VideoPlaylistModel } from '../video/video-playlist' 71import { VideoPlaylistModel } from '../video/video-playlist'
72import { UserNotificationSettingModel } from './user-notification-setting' 72import { UserNotificationSettingModel } from './user-notification-setting'
73import { forceNumber } from '@shared/core-utils'
73 74
74enum ScopeNames { 75enum ScopeNames {
75 FOR_ME_API = 'FOR_ME_API', 76 FOR_ME_API = 'FOR_ME_API',
@@ -403,6 +404,11 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
403 @Column 404 @Column
404 lastLoginDate: Date 405 lastLoginDate: Date
405 406
407 @AllowNull(true)
408 @Default(null)
409 @Column
410 otpSecret: string
411
406 @CreatedAt 412 @CreatedAt
407 createdAt: Date 413 createdAt: Date
408 414
@@ -886,34 +892,36 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
886 autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist, 892 autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist,
887 videoLanguages: this.videoLanguages, 893 videoLanguages: this.videoLanguages,
888 894
889 role: this.role, 895 role: {
890 roleLabel: USER_ROLE_LABELS[this.role], 896 id: this.role,
897 label: USER_ROLE_LABELS[this.role]
898 },
891 899
892 videoQuota: this.videoQuota, 900 videoQuota: this.videoQuota,
893 videoQuotaDaily: this.videoQuotaDaily, 901 videoQuotaDaily: this.videoQuotaDaily,
894 902
895 videoQuotaUsed: videoQuotaUsed !== undefined 903 videoQuotaUsed: videoQuotaUsed !== undefined
896 ? parseInt(videoQuotaUsed + '', 10) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id) 904 ? forceNumber(videoQuotaUsed) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id)
897 : undefined, 905 : undefined,
898 906
899 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined 907 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
900 ? parseInt(videoQuotaUsedDaily + '', 10) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id) 908 ? forceNumber(videoQuotaUsedDaily) + LiveQuotaStore.Instance.getLiveQuotaOf(this.id)
901 : undefined, 909 : undefined,
902 910
903 videosCount: videosCount !== undefined 911 videosCount: videosCount !== undefined
904 ? parseInt(videosCount + '', 10) 912 ? forceNumber(videosCount)
905 : undefined, 913 : undefined,
906 abusesCount: abusesCount 914 abusesCount: abusesCount
907 ? parseInt(abusesCount, 10) 915 ? forceNumber(abusesCount)
908 : undefined, 916 : undefined,
909 abusesAcceptedCount: abusesAcceptedCount 917 abusesAcceptedCount: abusesAcceptedCount
910 ? parseInt(abusesAcceptedCount, 10) 918 ? forceNumber(abusesAcceptedCount)
911 : undefined, 919 : undefined,
912 abusesCreatedCount: abusesCreatedCount !== undefined 920 abusesCreatedCount: abusesCreatedCount !== undefined
913 ? parseInt(abusesCreatedCount + '', 10) 921 ? forceNumber(abusesCreatedCount)
914 : undefined, 922 : undefined,
915 videoCommentsCount: videoCommentsCount !== undefined 923 videoCommentsCount: videoCommentsCount !== undefined
916 ? parseInt(videoCommentsCount + '', 10) 924 ? forceNumber(videoCommentsCount)
917 : undefined, 925 : undefined,
918 926
919 noInstanceConfigWarningModal: this.noInstanceConfigWarningModal, 927 noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
@@ -935,7 +943,9 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
935 943
936 pluginAuth: this.pluginAuth, 944 pluginAuth: this.pluginAuth,
937 945
938 lastLoginDate: this.lastLoginDate 946 lastLoginDate: this.lastLoginDate,
947
948 twoFactorEnabled: !!this.otpSecret
939 } 949 }
940 950
941 if (parameters.withAdminFlags) { 951 if (parameters.withAdminFlags) {
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 1e168d419..3476799ce 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -1,5 +1,6 @@
1import { literal, Op, OrderItem, Sequelize } from 'sequelize' 1import { literal, Op, OrderItem, Sequelize } from 'sequelize'
2import validator from 'validator' 2import validator from 'validator'
3import { forceNumber } from '@shared/core-utils'
3 4
4type SortType = { sortModel: string, sortValue: string } 5type SortType = { sortModel: string, sortValue: string }
5 6
@@ -202,7 +203,7 @@ function buildBlockedAccountSQLOptimized (columnNameJoin: string, blockerIds: nu
202} 203}
203 204
204function buildServerIdsFollowedBy (actorId: any) { 205function buildServerIdsFollowedBy (actorId: any) {
205 const actorIdNumber = parseInt(actorId + '', 10) 206 const actorIdNumber = forceNumber(actorId)
206 207
207 return '(' + 208 return '(' +
208 'SELECT "actor"."serverId" FROM "actorFollow" ' + 209 'SELECT "actor"."serverId" FROM "actorFollow" ' +
@@ -218,7 +219,7 @@ function buildWhereIdOrUUID (id: number | string) {
218function parseAggregateResult (result: any) { 219function parseAggregateResult (result: any) {
219 if (!result) return 0 220 if (!result) return 0
220 221
221 const total = parseInt(result + '', 10) 222 const total = forceNumber(result)
222 if (isNaN(total)) return 0 223 if (isNaN(total)) return 0
223 224
224 return total 225 return total
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts
index e1b0eb610..f285db477 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,
@@ -57,7 +58,7 @@ export type VideoFormattingJSONOptions = {
57} 58}
58 59
59function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions { 60function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSanitize): VideoFormattingJSONOptions {
60 if (!query || !query.include) return {} 61 if (!query?.include) return {}
61 62
62 return { 63 return {
63 additionalAttributes: { 64 additionalAttributes: {
@@ -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/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts
index 3c74b0ea6..f0ce69501 100644
--- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts
+++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts
@@ -302,7 +302,7 @@ export class AbstractVideoQueryBuilder extends AbstractRunQuery {
302 } 302 }
303 303
304 protected buildAttributesObject (prefixKey: string, attributeKeys: string[]) { 304 protected buildAttributesObject (prefixKey: string, attributeKeys: string[]) {
305 const result: { [id: string]: string} = {} 305 const result: { [id: string]: string } = {}
306 306
307 const prefixValue = prefixKey.replace(/->/g, '.') 307 const prefixValue = prefixKey.replace(/->/g, '.')
308 308
diff --git a/server/models/video/sql/video/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts
index 14f903851..7c864bf27 100644
--- a/server/models/video/sql/video/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/video/videos-id-list-query-builder.ts
@@ -6,6 +6,7 @@ import { buildDirectionAndField, createSafeIn, parseRowCountResult } from '@serv
6import { MUserAccountId, MUserId } from '@server/types/models' 6import { MUserAccountId, MUserId } from '@server/types/models'
7import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' 7import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models'
8import { AbstractRunQuery } from '../../../shared/abstract-run-query' 8import { AbstractRunQuery } from '../../../shared/abstract-run-query'
9import { forceNumber } from '@shared/core-utils'
9 10
10/** 11/**
11 * 12 *
@@ -689,12 +690,12 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
689 } 690 }
690 691
691 private setLimit (countArg: number) { 692 private setLimit (countArg: number) {
692 const count = parseInt(countArg + '', 10) 693 const count = forceNumber(countArg)
693 this.limit = `LIMIT ${count}` 694 this.limit = `LIMIT ${count}`
694 } 695 }
695 696
696 private setOffset (startArg: number) { 697 private setOffset (startArg: number) {
697 const start = parseInt(startArg + '', 10) 698 const start = forceNumber(startArg)
698 this.offset = `OFFSET ${start}` 699 this.offset = `OFFSET ${start}`
699 } 700 }
700} 701}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 91dafbcf1..9e461b6ca 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -19,7 +19,7 @@ import {
19} from 'sequelize-typescript' 19} from 'sequelize-typescript'
20import { CONFIG } from '@server/initializers/config' 20import { CONFIG } from '@server/initializers/config'
21import { MAccountActor } from '@server/types/models' 21import { MAccountActor } from '@server/types/models'
22import { pick } from '@shared/core-utils' 22import { forceNumber, pick } from '@shared/core-utils'
23import { AttributesOnly } from '@shared/typescript-utils' 23import { AttributesOnly } from '@shared/typescript-utils'
24import { ActivityPubActor } from '../../../shared/models/activitypub' 24import { ActivityPubActor } from '../../../shared/models/activitypub'
25import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos' 25import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
@@ -280,7 +280,7 @@ export type SummaryOptions = {
280 ] 280 ]
281 }, 281 },
282 [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => { 282 [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => {
283 const daysPrior = parseInt(options.daysPrior + '', 10) 283 const daysPrior = forceNumber(options.daysPrior)
284 284
285 return { 285 return {
286 attributes: { 286 attributes: {
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-element.ts b/server/models/video/video-playlist-element.ts
index b45f15bd6..7181b5599 100644
--- a/server/models/video/video-playlist-element.ts
+++ b/server/models/video/video-playlist-element.ts
@@ -23,6 +23,7 @@ import {
23 MVideoPlaylistElementVideoUrlPlaylistPrivacy, 23 MVideoPlaylistElementVideoUrlPlaylistPrivacy,
24 MVideoPlaylistVideoThumbnail 24 MVideoPlaylistVideoThumbnail
25} from '@server/types/models/video/video-playlist-element' 25} from '@server/types/models/video/video-playlist-element'
26import { forceNumber } from '@shared/core-utils'
26import { AttributesOnly } from '@shared/typescript-utils' 27import { AttributesOnly } from '@shared/typescript-utils'
27import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' 28import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
28import { VideoPrivacy } from '../../../shared/models/videos' 29import { VideoPrivacy } from '../../../shared/models/videos'
@@ -185,7 +186,9 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide
185 playlistId: number | string, 186 playlistId: number | string,
186 playlistElementId: number 187 playlistElementId: number
187 ): Promise<MVideoPlaylistElementVideoUrlPlaylistPrivacy> { 188 ): Promise<MVideoPlaylistElementVideoUrlPlaylistPrivacy> {
188 const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId } 189 const playlistWhere = validator.isUUID('' + playlistId)
190 ? { uuid: playlistId }
191 : { id: playlistId }
189 192
190 const query = { 193 const query = {
191 include: [ 194 include: [
@@ -262,13 +265,15 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide
262 .then(position => position ? position + 1 : 1) 265 .then(position => position ? position + 1 : 1)
263 } 266 }
264 267
265 static reassignPositionOf ( 268 static reassignPositionOf (options: {
266 videoPlaylistId: number, 269 videoPlaylistId: number
267 firstPosition: number, 270 firstPosition: number
268 endPosition: number, 271 endPosition: number
269 newPosition: number, 272 newPosition: number
270 transaction?: Transaction 273 transaction?: Transaction
271 ) { 274 }) {
275 const { videoPlaylistId, firstPosition, endPosition, newPosition, transaction } = options
276
272 const query = { 277 const query = {
273 where: { 278 where: {
274 videoPlaylistId, 279 videoPlaylistId,
@@ -281,7 +286,7 @@ export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<Vide
281 validate: false // We use a literal to update the position 286 validate: false // We use a literal to update the position
282 } 287 }
283 288
284 const positionQuery = Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) 289 const positionQuery = Sequelize.literal(`${forceNumber(newPosition)} + "position" - ${forceNumber(firstPosition)}`)
285 return VideoPlaylistElementModel.update({ position: positionQuery }, query) 290 return VideoPlaylistElementModel.update({ position: positionQuery }, query)
286 } 291 }
287 292
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-share.ts b/server/models/video/video-share.ts
index ca63bb2d9..f2190037e 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -1,5 +1,6 @@
1import { literal, Op, QueryTypes, Transaction } from 'sequelize' 1import { literal, Op, QueryTypes, Transaction } from 'sequelize'
2import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { forceNumber } from '@shared/core-utils'
3import { AttributesOnly } from '@shared/typescript-utils' 4import { AttributesOnly } from '@shared/typescript-utils'
4import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 5import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
5import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 6import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
@@ -123,7 +124,7 @@ export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareMode
123 } 124 }
124 125
125 static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Transaction): Promise<MActorDefault[]> { 126 static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Transaction): Promise<MActorDefault[]> {
126 const safeOwnerId = parseInt(actorOwnerId + '', 10) 127 const safeOwnerId = forceNumber(actorOwnerId)
127 128
128 // /!\ On actor model 129 // /!\ On actor model
129 const query = { 130 const query = {
@@ -148,7 +149,7 @@ export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareMode
148 } 149 }
149 150
150 static loadActorsByVideoChannel (videoChannelId: number, t: Transaction): Promise<MActorDefault[]> { 151 static loadActorsByVideoChannel (videoChannelId: number, t: Transaction): Promise<MActorDefault[]> {
151 const safeChannelId = parseInt(videoChannelId + '', 10) 152 const safeChannelId = forceNumber(videoChannelId)
152 153
153 // /!\ On actor model 154 // /!\ On actor model
154 const query = { 155 const query = {
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