aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/helpers/query.ts2
-rw-r--r--server/middlewares/validators/videos/videos.ts21
-rw-r--r--server/models/video/sql/videos-id-list-query-builder.ts45
-rw-r--r--server/models/video/video.ts44
-rw-r--r--server/tests/api/videos/videos-common-filters.ts80
5 files changed, 162 insertions, 30 deletions
diff --git a/server/helpers/query.ts b/server/helpers/query.ts
index 79cf076d1..97bbdfc65 100644
--- a/server/helpers/query.ts
+++ b/server/helpers/query.ts
@@ -21,6 +21,8 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
21 'isLocal', 21 'isLocal',
22 'include', 22 'include',
23 'skipCount', 23 'skipCount',
24 'hasHLSFiles',
25 'hasWebtorrentFiles',
24 'search' 26 'search'
25 ]) 27 ])
26} 28}
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 44233b653..5f1234379 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -496,6 +496,14 @@ const commonVideosFiltersValidator = [
496 .optional() 496 .optional()
497 .customSanitizer(toBooleanOrNull) 497 .customSanitizer(toBooleanOrNull)
498 .custom(isBooleanValid).withMessage('Should have a valid local boolean'), 498 .custom(isBooleanValid).withMessage('Should have a valid local boolean'),
499 query('hasHLSFiles')
500 .optional()
501 .customSanitizer(toBooleanOrNull)
502 .custom(isBooleanValid).withMessage('Should have a valid has hls boolean'),
503 query('hasWebtorrentFiles')
504 .optional()
505 .customSanitizer(toBooleanOrNull)
506 .custom(isBooleanValid).withMessage('Should have a valid has webtorrent boolean'),
499 query('skipCount') 507 query('skipCount')
500 .optional() 508 .optional()
501 .customSanitizer(toBooleanOrNull) 509 .customSanitizer(toBooleanOrNull)
@@ -525,12 +533,13 @@ const commonVideosFiltersValidator = [
525 533
526 const user = res.locals.oauth?.token.User 534 const user = res.locals.oauth?.token.User
527 535
528 if (req.query.include && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) { 536 if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
529 res.fail({ 537 if (req.query.include) {
530 status: HttpStatusCode.UNAUTHORIZED_401, 538 return res.fail({
531 message: 'You are not allowed to see all local videos.' 539 status: HttpStatusCode.UNAUTHORIZED_401,
532 }) 540 message: 'You are not allowed to see all videos.'
533 return 541 })
542 }
534 } 543 }
535 544
536 return next() 545 return next()
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts
index 5064afafe..4a882e790 100644
--- a/server/models/video/sql/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/videos-id-list-query-builder.ts
@@ -44,6 +44,8 @@ export type BuildVideosListQueryOptions = {
44 uuids?: string[] 44 uuids?: string[]
45 45
46 hasFiles?: boolean 46 hasFiles?: boolean
47 hasHLSFiles?: boolean
48 hasWebtorrentFiles?: boolean
47 49
48 accountId?: number 50 accountId?: number
49 videoChannelId?: number 51 videoChannelId?: number
@@ -169,6 +171,14 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
169 this.whereFileExists() 171 this.whereFileExists()
170 } 172 }
171 173
174 if (exists(options.hasWebtorrentFiles)) {
175 this.whereWebTorrentFileExists(options.hasWebtorrentFiles)
176 }
177
178 if (exists(options.hasHLSFiles)) {
179 this.whereHLSFileExists(options.hasHLSFiles)
180 }
181
172 if (options.tagsOneOf) { 182 if (options.tagsOneOf) {
173 this.whereTagsOneOf(options.tagsOneOf) 183 this.whereTagsOneOf(options.tagsOneOf)
174 } 184 }
@@ -371,16 +381,31 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
371 } 381 }
372 382
373 private whereFileExists () { 383 private whereFileExists () {
374 this.and.push( 384 this.and.push(`(${this.buildWebTorrentFileExistsQuery(true)} OR ${this.buildHLSFileExistsQuery(true)})`)
375 '(' + 385 }
376 ' EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id") ' + 386
377 ' OR EXISTS (' + 387 private whereWebTorrentFileExists (exists: boolean) {
378 ' SELECT 1 FROM "videoStreamingPlaylist" ' + 388 this.and.push(this.buildWebTorrentFileExistsQuery(exists))
379 ' INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ' + 389 }
380 ' WHERE "videoStreamingPlaylist"."videoId" = "video"."id"' + 390
381 ' )' + 391 private whereHLSFileExists (exists: boolean) {
382 ')' 392 this.and.push(this.buildHLSFileExistsQuery(exists))
383 ) 393 }
394
395 private buildWebTorrentFileExistsQuery (exists: boolean) {
396 const prefix = exists ? '' : 'NOT '
397
398 return prefix + 'EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")'
399 }
400
401 private buildHLSFileExistsQuery (exists: boolean) {
402 const prefix = exists ? '' : 'NOT '
403
404 return prefix + 'EXISTS (' +
405 ' SELECT 1 FROM "videoStreamingPlaylist" ' +
406 ' INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ' +
407 ' WHERE "videoStreamingPlaylist"."videoId" = "video"."id"' +
408 ')'
384 } 409 }
385 410
386 private whereTagsOneOf (tagsOneOf: string[]) { 411 private whereTagsOneOf (tagsOneOf: string[]) {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index f9618c102..aef4fd20a 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1030,6 +1030,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1030 include?: VideoInclude 1030 include?: VideoInclude
1031 1031
1032 hasFiles?: boolean // default false 1032 hasFiles?: boolean // default false
1033 hasWebtorrentFiles?: boolean
1034 hasHLSFiles?: boolean
1033 1035
1034 categoryOneOf?: number[] 1036 categoryOneOf?: number[]
1035 licenceOneOf?: number[] 1037 licenceOneOf?: number[]
@@ -1053,9 +1055,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1053 1055
1054 search?: string 1056 search?: string
1055 }) { 1057 }) {
1056 if (VideoModel.isPrivateInclude(options.include) && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1058 VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
1057 throw new Error('Try to filter all-local but no user has not the see all videos right')
1058 }
1059 1059
1060 const trendingDays = options.sort.endsWith('trending') 1060 const trendingDays = options.sort.endsWith('trending')
1061 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS 1061 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
@@ -1088,6 +1088,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1088 'videoPlaylistId', 1088 'videoPlaylistId',
1089 'user', 1089 'user',
1090 'historyOfUser', 1090 'historyOfUser',
1091 'hasHLSFiles',
1092 'hasWebtorrentFiles',
1091 'search' 1093 'search'
1092 ]), 1094 ]),
1093 1095
@@ -1103,27 +1105,39 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1103 start: number 1105 start: number
1104 count: number 1106 count: number
1105 sort: string 1107 sort: string
1106 search?: string 1108
1107 host?: string
1108 startDate?: string // ISO 8601
1109 endDate?: string // ISO 8601
1110 originallyPublishedStartDate?: string
1111 originallyPublishedEndDate?: string
1112 nsfw?: boolean 1109 nsfw?: boolean
1113 isLive?: boolean 1110 isLive?: boolean
1114 isLocal?: boolean 1111 isLocal?: boolean
1115 include?: VideoInclude 1112 include?: VideoInclude
1113
1116 categoryOneOf?: number[] 1114 categoryOneOf?: number[]
1117 licenceOneOf?: number[] 1115 licenceOneOf?: number[]
1118 languageOneOf?: string[] 1116 languageOneOf?: string[]
1119 tagsOneOf?: string[] 1117 tagsOneOf?: string[]
1120 tagsAllOf?: string[] 1118 tagsAllOf?: string[]
1119
1120 displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
1121
1122 user?: MUserAccountId
1123
1124 hasWebtorrentFiles?: boolean
1125 hasHLSFiles?: boolean
1126
1127 search?: string
1128
1129 host?: string
1130 startDate?: string // ISO 8601
1131 endDate?: string // ISO 8601
1132 originallyPublishedStartDate?: string
1133 originallyPublishedEndDate?: string
1134
1121 durationMin?: number // seconds 1135 durationMin?: number // seconds
1122 durationMax?: number // seconds 1136 durationMax?: number // seconds
1123 user?: MUserAccountId
1124 uuids?: string[] 1137 uuids?: string[]
1125 displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
1126 }) { 1138 }) {
1139 VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
1140
1127 const serverActor = await getServerActor() 1141 const serverActor = await getServerActor()
1128 1142
1129 const queryOptions = { 1143 const queryOptions = {
@@ -1148,6 +1162,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1148 'originallyPublishedEndDate', 1162 'originallyPublishedEndDate',
1149 'durationMin', 1163 'durationMin',
1150 'durationMax', 1164 'durationMax',
1165 'hasHLSFiles',
1166 'hasWebtorrentFiles',
1151 'uuids', 1167 'uuids',
1152 'search', 1168 'search',
1153 'displayOnlyForFollower' 1169 'displayOnlyForFollower'
@@ -1489,6 +1505,12 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1489 } 1505 }
1490 } 1506 }
1491 1507
1508 private static throwIfPrivateIncludeWithoutUser (include: VideoInclude, user: MUserAccountId) {
1509 if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1510 throw new Error('Try to filter all-local but no user has not the see all videos right')
1511 }
1512 }
1513
1492 private static isPrivateInclude (include: VideoInclude) { 1514 private static isPrivateInclude (include: VideoInclude) {
1493 return include & VideoInclude.BLACKLISTED || 1515 return include & VideoInclude.BLACKLISTED ||
1494 include & VideoInclude.BLOCKED_OWNER || 1516 include & VideoInclude.BLOCKED_OWNER ||
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts
index 03c5c3b3f..4f22d4ac3 100644
--- a/server/tests/api/videos/videos-common-filters.ts
+++ b/server/tests/api/videos/videos-common-filters.ts
@@ -135,6 +135,8 @@ describe('Test videos filter', function () {
135 server: PeerTubeServer 135 server: PeerTubeServer
136 path: string 136 path: string
137 isLocal?: boolean 137 isLocal?: boolean
138 hasWebtorrentFiles?: boolean
139 hasHLSFiles?: boolean
138 include?: VideoInclude 140 include?: VideoInclude
139 category?: number 141 category?: number
140 tagsAllOf?: string[] 142 tagsAllOf?: string[]
@@ -146,7 +148,7 @@ describe('Test videos filter', function () {
146 path: options.path, 148 path: options.path,
147 token: options.token ?? options.server.accessToken, 149 token: options.token ?? options.server.accessToken,
148 query: { 150 query: {
149 ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf' ]), 151 ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles' ]),
150 152
151 sort: 'createdAt' 153 sort: 'createdAt'
152 }, 154 },
@@ -397,11 +399,9 @@ describe('Test videos filter', function () {
397 399
398 for (const path of paths) { 400 for (const path of paths) {
399 { 401 {
400
401 const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag2' ] }) 402 const videos = await listVideos({ server: servers[0], path, tagsAllOf: [ 'tag1', 'tag2' ] })
402 expect(videos).to.have.lengthOf(1) 403 expect(videos).to.have.lengthOf(1)
403 expect(videos[0].name).to.equal('tag filter') 404 expect(videos[0].name).to.equal('tag filter')
404
405 } 405 }
406 406
407 { 407 {
@@ -421,6 +421,80 @@ describe('Test videos filter', function () {
421 } 421 }
422 } 422 }
423 }) 423 })
424
425 it('Should filter by HLS or WebTorrent files', async function () {
426 this.timeout(360000)
427
428 const finderFactory = (name: string) => (videos: Video[]) => videos.some(v => v.name === name)
429
430 await servers[0].config.enableTranscoding(true, false)
431 await servers[0].videos.upload({ attributes: { name: 'webtorrent video' } })
432 const hasWebtorrent = finderFactory('webtorrent video')
433
434 await waitJobs(servers)
435
436 await servers[0].config.enableTranscoding(false, true)
437 await servers[0].videos.upload({ attributes: { name: 'hls video' } })
438 const hasHLS = finderFactory('hls video')
439
440 await waitJobs(servers)
441
442 await servers[0].config.enableTranscoding(true, true)
443 await servers[0].videos.upload({ attributes: { name: 'hls and webtorrent video' } })
444 const hasBoth = finderFactory('hls and webtorrent video')
445
446 await waitJobs(servers)
447
448 for (const path of paths) {
449 {
450 const videos = await listVideos({ server: servers[0], path, hasWebtorrentFiles: true })
451
452 expect(hasWebtorrent(videos)).to.be.true
453 expect(hasHLS(videos)).to.be.false
454 expect(hasBoth(videos)).to.be.true
455 }
456
457 {
458 const videos = await listVideos({ server: servers[0], path, hasWebtorrentFiles: false })
459
460 expect(hasWebtorrent(videos)).to.be.false
461 expect(hasHLS(videos)).to.be.true
462 expect(hasBoth(videos)).to.be.false
463 }
464
465 {
466 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true })
467
468 expect(hasWebtorrent(videos)).to.be.false
469 expect(hasHLS(videos)).to.be.true
470 expect(hasBoth(videos)).to.be.true
471 }
472
473 {
474 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false })
475
476 expect(hasWebtorrent(videos)).to.be.true
477 expect(hasHLS(videos)).to.be.false
478 expect(hasBoth(videos)).to.be.false
479 }
480
481 {
482 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: false, hasWebtorrentFiles: false })
483
484 expect(hasWebtorrent(videos)).to.be.false
485 expect(hasHLS(videos)).to.be.false
486 expect(hasBoth(videos)).to.be.false
487 }
488
489 {
490 const videos = await listVideos({ server: servers[0], path, hasHLSFiles: true, hasWebtorrentFiles: true })
491
492 expect(hasWebtorrent(videos)).to.be.false
493 expect(hasHLS(videos)).to.be.false
494 expect(hasBoth(videos)).to.be.true
495 }
496 }
497 })
424 }) 498 })
425 499
426 after(async function () { 500 after(async function () {