diff options
author | Chocobozzz <me@florianbigard.com> | 2021-11-03 11:32:41 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2021-11-03 11:32:41 +0100 |
commit | d324756edb836672f12284cd18e642a658b273d8 (patch) | |
tree | 3b323682bd7380491ad904daaeaea10be606e0f9 /server | |
parent | d5d9c5b79edf613e97a752a3d59062fb42045275 (diff) | |
download | PeerTube-d324756edb836672f12284cd18e642a658b273d8.tar.gz PeerTube-d324756edb836672f12284cd18e642a658b273d8.tar.zst PeerTube-d324756edb836672f12284cd18e642a658b273d8.zip |
Add ability to filter by file type
Diffstat (limited to 'server')
-rw-r--r-- | server/helpers/query.ts | 2 | ||||
-rw-r--r-- | server/middlewares/validators/videos/videos.ts | 21 | ||||
-rw-r--r-- | server/models/video/sql/videos-id-list-query-builder.ts | 45 | ||||
-rw-r--r-- | server/models/video/video.ts | 44 | ||||
-rw-r--r-- | server/tests/api/videos/videos-common-filters.ts | 80 |
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 () { |