diff options
author | Wicklow <123956049+wickloww@users.noreply.github.com> | 2023-04-12 07:32:20 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-12 09:32:20 +0200 |
commit | 2a4c0d8bbe29178ae90e776bb9453f86e6d23bd9 (patch) | |
tree | dfe4b6e1e06f617f8968285ca394e73fedefe6b2 | |
parent | 0cda019c1d1f77e06e524362880c38e93b1f5c70 (diff) | |
download | PeerTube-2a4c0d8bbe29178ae90e776bb9453f86e6d23bd9.tar.gz PeerTube-2a4c0d8bbe29178ae90e776bb9453f86e6d23bd9.tar.zst PeerTube-2a4c0d8bbe29178ae90e776bb9453f86e6d23bd9.zip |
Feature/filter already watched videos (#5739)
* filter already watched videos
* Updated code based on review comments
-rw-r--r-- | client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts | 3 | ||||
-rw-r--r-- | client/src/app/shared/shared-search/advanced-search.model.ts | 12 | ||||
-rw-r--r-- | server/helpers/query.ts | 6 | ||||
-rw-r--r-- | server/middlewares/validators/videos/videos.ts | 11 | ||||
-rw-r--r-- | server/models/video/sql/video/videos-id-list-query-builder.ts | 22 | ||||
-rw-r--r-- | server/models/video/video.ts | 10 | ||||
-rw-r--r-- | server/tests/api/check-params/videos-common-filters.ts | 19 | ||||
-rw-r--r-- | server/tests/api/videos/videos-common-filters.ts | 32 | ||||
-rw-r--r-- | shared/models/search/videos-common-query.model.ts | 2 | ||||
-rw-r--r-- | support/doc/api/openapi.yaml | 11 |
10 files changed, 119 insertions, 9 deletions
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts index 4654da847..b0ae910ac 100644 --- a/client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts +++ b/client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts | |||
@@ -63,6 +63,9 @@ export class RecentVideosRecommendationService implements RecommendationService | |||
63 | searchTarget: 'local', | 63 | searchTarget: 'local', |
64 | nsfw: user.nsfwPolicy | 64 | nsfw: user.nsfwPolicy |
65 | ? this.videos.nsfwPolicyToParam(user.nsfwPolicy) | 65 | ? this.videos.nsfwPolicyToParam(user.nsfwPolicy) |
66 | : undefined, | ||
67 | excludeAlreadyWatched: user.id | ||
68 | ? true | ||
66 | : undefined | 69 | : undefined |
67 | }) | 70 | }) |
68 | } | 71 | } |
diff --git a/client/src/app/shared/shared-search/advanced-search.model.ts b/client/src/app/shared/shared-search/advanced-search.model.ts index e8bb00fd3..29fe3e8dc 100644 --- a/client/src/app/shared/shared-search/advanced-search.model.ts +++ b/client/src/app/shared/shared-search/advanced-search.model.ts | |||
@@ -40,6 +40,8 @@ export class AdvancedSearch { | |||
40 | searchTarget: SearchTargetType | 40 | searchTarget: SearchTargetType |
41 | resultType: AdvancedSearchResultType | 41 | resultType: AdvancedSearchResultType |
42 | 42 | ||
43 | excludeAlreadyWatched?: boolean | ||
44 | |||
43 | constructor (options?: { | 45 | constructor (options?: { |
44 | startDate?: string | 46 | startDate?: string |
45 | endDate?: string | 47 | endDate?: string |
@@ -62,6 +64,8 @@ export class AdvancedSearch { | |||
62 | sort?: string | 64 | sort?: string |
63 | searchTarget?: SearchTargetType | 65 | searchTarget?: SearchTargetType |
64 | resultType?: AdvancedSearchResultType | 66 | resultType?: AdvancedSearchResultType |
67 | |||
68 | excludeAlreadyWatched?: boolean | ||
65 | }) { | 69 | }) { |
66 | if (!options) return | 70 | if (!options) return |
67 | 71 | ||
@@ -87,6 +91,8 @@ export class AdvancedSearch { | |||
87 | 91 | ||
88 | this.resultType = options.resultType || undefined | 92 | this.resultType = options.resultType || undefined |
89 | 93 | ||
94 | this.excludeAlreadyWatched = options.excludeAlreadyWatched || undefined | ||
95 | |||
90 | if (!this.resultType && this.hasVideoFilter()) { | 96 | if (!this.resultType && this.hasVideoFilter()) { |
91 | this.resultType = 'videos' | 97 | this.resultType = 'videos' |
92 | } | 98 | } |
@@ -138,7 +144,8 @@ export class AdvancedSearch { | |||
138 | host: this.host, | 144 | host: this.host, |
139 | sort: this.sort, | 145 | sort: this.sort, |
140 | searchTarget: this.searchTarget, | 146 | searchTarget: this.searchTarget, |
141 | resultType: this.resultType | 147 | resultType: this.resultType, |
148 | excludeAlreadyWatched: this.excludeAlreadyWatched | ||
142 | } | 149 | } |
143 | } | 150 | } |
144 | 151 | ||
@@ -162,7 +169,8 @@ export class AdvancedSearch { | |||
162 | host: this.host, | 169 | host: this.host, |
163 | isLive, | 170 | isLive, |
164 | sort: this.sort, | 171 | sort: this.sort, |
165 | searchTarget: this.searchTarget | 172 | searchTarget: this.searchTarget, |
173 | excludeAlreadyWatched: this.excludeAlreadyWatched | ||
166 | } | 174 | } |
167 | } | 175 | } |
168 | 176 | ||
diff --git a/server/helpers/query.ts b/server/helpers/query.ts index 1142d02e4..10efae41c 100644 --- a/server/helpers/query.ts +++ b/server/helpers/query.ts | |||
@@ -24,7 +24,8 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) { | |||
24 | 'skipCount', | 24 | 'skipCount', |
25 | 'hasHLSFiles', | 25 | 'hasHLSFiles', |
26 | 'hasWebtorrentFiles', | 26 | 'hasWebtorrentFiles', |
27 | 'search' | 27 | 'search', |
28 | 'excludeAlreadyWatched' | ||
28 | ]) | 29 | ]) |
29 | } | 30 | } |
30 | 31 | ||
@@ -41,7 +42,8 @@ function pickSearchVideoQuery (query: VideosSearchQueryAfterSanitize) { | |||
41 | 'originallyPublishedEndDate', | 42 | 'originallyPublishedEndDate', |
42 | 'durationMin', | 43 | 'durationMin', |
43 | 'durationMax', | 44 | 'durationMax', |
44 | 'uuids' | 45 | 'uuids', |
46 | 'excludeAlreadyWatched' | ||
45 | ]) | 47 | ]) |
46 | } | 48 | } |
47 | } | 49 | } |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index e29eb4a32..ea6bd0721 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -489,6 +489,10 @@ const commonVideosFiltersValidator = [ | |||
489 | query('search') | 489 | query('search') |
490 | .optional() | 490 | .optional() |
491 | .custom(exists), | 491 | .custom(exists), |
492 | query('excludeAlreadyWatched') | ||
493 | .optional() | ||
494 | .customSanitizer(toBooleanOrNull) | ||
495 | .isBoolean().withMessage('Should be a valid excludeAlreadyWatched boolean'), | ||
492 | 496 | ||
493 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 497 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
494 | if (areValidationErrors(req, res)) return | 498 | if (areValidationErrors(req, res)) return |
@@ -520,6 +524,13 @@ const commonVideosFiltersValidator = [ | |||
520 | } | 524 | } |
521 | } | 525 | } |
522 | 526 | ||
527 | if (!user && exists(req.query.excludeAlreadyWatched)) { | ||
528 | res.fail({ | ||
529 | status: HttpStatusCode.BAD_REQUEST_400, | ||
530 | message: 'Cannot use excludeAlreadyWatched parameter when auth token is not provided' | ||
531 | }) | ||
532 | return false | ||
533 | } | ||
523 | return next() | 534 | return next() |
524 | } | 535 | } |
525 | ] | 536 | ] |
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 62f1855c7..cba77c1d1 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 | |||
@@ -78,6 +78,8 @@ export type BuildVideosListQueryOptions = { | |||
78 | 78 | ||
79 | transaction?: Transaction | 79 | transaction?: Transaction |
80 | logging?: boolean | 80 | logging?: boolean |
81 | |||
82 | excludeAlreadyWatched?: boolean | ||
81 | } | 83 | } |
82 | 84 | ||
83 | export class VideosIdListQueryBuilder extends AbstractRunQuery { | 85 | export class VideosIdListQueryBuilder extends AbstractRunQuery { |
@@ -260,6 +262,14 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
260 | this.whereDurationMax(options.durationMax) | 262 | this.whereDurationMax(options.durationMax) |
261 | } | 263 | } |
262 | 264 | ||
265 | if (options.excludeAlreadyWatched) { | ||
266 | if (exists(options.user.id)) { | ||
267 | this.whereExcludeAlreadyWatched(options.user.id) | ||
268 | } else { | ||
269 | throw new Error('Cannot use excludeAlreadyWatched parameter when auth token is not provided') | ||
270 | } | ||
271 | } | ||
272 | |||
263 | this.whereSearch(options.search) | 273 | this.whereSearch(options.search) |
264 | 274 | ||
265 | if (options.isCount === true) { | 275 | if (options.isCount === true) { |
@@ -598,6 +608,18 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
598 | this.replacements.durationMax = durationMax | 608 | this.replacements.durationMax = durationMax |
599 | } | 609 | } |
600 | 610 | ||
611 | private whereExcludeAlreadyWatched (userId: number) { | ||
612 | this.and.push( | ||
613 | 'NOT EXISTS (' + | ||
614 | ' SELECT 1' + | ||
615 | ' FROM "userVideoHistory"' + | ||
616 | ' WHERE "video"."id" = "userVideoHistory"."videoId"' + | ||
617 | ' AND "userVideoHistory"."userId" = :excludeAlreadyWatchedUserId' + | ||
618 | ')' | ||
619 | ) | ||
620 | this.replacements.excludeAlreadyWatchedUserId = userId | ||
621 | } | ||
622 | |||
601 | private groupForTrending (trendingDays: number) { | 623 | private groupForTrending (trendingDays: number) { |
602 | const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) | 624 | const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) |
603 | 625 | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 0c5ed64ec..f817c4a33 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1086,6 +1086,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1086 | countVideos?: boolean | 1086 | countVideos?: boolean |
1087 | 1087 | ||
1088 | search?: string | 1088 | search?: string |
1089 | |||
1090 | excludeAlreadyWatched?: boolean | ||
1089 | }) { | 1091 | }) { |
1090 | VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) | 1092 | VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) |
1091 | VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user) | 1093 | VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user) |
@@ -1124,7 +1126,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1124 | 'historyOfUser', | 1126 | 'historyOfUser', |
1125 | 'hasHLSFiles', | 1127 | 'hasHLSFiles', |
1126 | 'hasWebtorrentFiles', | 1128 | 'hasWebtorrentFiles', |
1127 | 'search' | 1129 | 'search', |
1130 | 'excludeAlreadyWatched' | ||
1128 | ]), | 1131 | ]), |
1129 | 1132 | ||
1130 | serverAccountIdForBlock: serverActor.Account.id, | 1133 | serverAccountIdForBlock: serverActor.Account.id, |
@@ -1170,6 +1173,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1170 | durationMin?: number // seconds | 1173 | durationMin?: number // seconds |
1171 | durationMax?: number // seconds | 1174 | durationMax?: number // seconds |
1172 | uuids?: string[] | 1175 | uuids?: string[] |
1176 | |||
1177 | excludeAlreadyWatched?: boolean | ||
1173 | }) { | 1178 | }) { |
1174 | VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) | 1179 | VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) |
1175 | VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user) | 1180 | VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user) |
@@ -1203,7 +1208,8 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1203 | 'hasWebtorrentFiles', | 1208 | 'hasWebtorrentFiles', |
1204 | 'uuids', | 1209 | 'uuids', |
1205 | 'search', | 1210 | 'search', |
1206 | 'displayOnlyForFollower' | 1211 | 'displayOnlyForFollower', |
1212 | 'excludeAlreadyWatched' | ||
1207 | ]), | 1213 | ]), |
1208 | serverAccountIdForBlock: serverActor.Account.id | 1214 | serverAccountIdForBlock: serverActor.Account.id |
1209 | } | 1215 | } |
diff --git a/server/tests/api/check-params/videos-common-filters.ts b/server/tests/api/check-params/videos-common-filters.ts index 11d9fd95b..3e44e2f67 100644 --- a/server/tests/api/check-params/videos-common-filters.ts +++ b/server/tests/api/check-params/videos-common-filters.ts | |||
@@ -122,6 +122,8 @@ describe('Test video filters validators', function () { | |||
122 | include?: VideoInclude | 122 | include?: VideoInclude |
123 | privacyOneOf?: VideoPrivacy[] | 123 | privacyOneOf?: VideoPrivacy[] |
124 | expectedStatus: HttpStatusCode | 124 | expectedStatus: HttpStatusCode |
125 | excludeAlreadyWatched?: boolean | ||
126 | unauthenticatedUser?: boolean | ||
125 | }) { | 127 | }) { |
126 | const paths = [ | 128 | const paths = [ |
127 | '/api/v1/video-channels/root_channel/videos', | 129 | '/api/v1/video-channels/root_channel/videos', |
@@ -131,14 +133,19 @@ describe('Test video filters validators', function () { | |||
131 | ] | 133 | ] |
132 | 134 | ||
133 | for (const path of paths) { | 135 | for (const path of paths) { |
136 | const token = options.unauthenticatedUser | ||
137 | ? undefined | ||
138 | : options.token || server.accessToken | ||
139 | |||
134 | await makeGetRequest({ | 140 | await makeGetRequest({ |
135 | url: server.url, | 141 | url: server.url, |
136 | path, | 142 | path, |
137 | token: options.token || server.accessToken, | 143 | token, |
138 | query: { | 144 | query: { |
139 | isLocal: options.isLocal, | 145 | isLocal: options.isLocal, |
140 | privacyOneOf: options.privacyOneOf, | 146 | privacyOneOf: options.privacyOneOf, |
141 | include: options.include | 147 | include: options.include, |
148 | excludeAlreadyWatched: options.excludeAlreadyWatched | ||
142 | }, | 149 | }, |
143 | expectedStatus: options.expectedStatus | 150 | expectedStatus: options.expectedStatus |
144 | }) | 151 | }) |
@@ -213,6 +220,14 @@ describe('Test video filters validators', function () { | |||
213 | } | 220 | } |
214 | }) | 221 | }) |
215 | }) | 222 | }) |
223 | |||
224 | it('Should fail when trying to exclude already watched videos for an unlogged user', async function () { | ||
225 | await testEndpoints({ excludeAlreadyWatched: true, unauthenticatedUser: true, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
226 | }) | ||
227 | |||
228 | it('Should succeed when trying to exclude already watched videos for a logged user', async function () { | ||
229 | await testEndpoints({ token: userAccessToken, excludeAlreadyWatched: true, expectedStatus: HttpStatusCode.OK_200 }) | ||
230 | }) | ||
216 | }) | 231 | }) |
217 | 232 | ||
218 | after(async function () { | 233 | after(async function () { |
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts index 1ab78ac49..30251706b 100644 --- a/server/tests/api/videos/videos-common-filters.ts +++ b/server/tests/api/videos/videos-common-filters.ts | |||
@@ -162,13 +162,23 @@ describe('Test videos filter', function () { | |||
162 | tagsAllOf?: string[] | 162 | tagsAllOf?: string[] |
163 | token?: string | 163 | token?: string |
164 | expectedStatus?: HttpStatusCode | 164 | expectedStatus?: HttpStatusCode |
165 | excludeAlreadyWatched?: boolean | ||
165 | }) { | 166 | }) { |
166 | const res = await makeGetRequest({ | 167 | const res = await makeGetRequest({ |
167 | url: options.server.url, | 168 | url: options.server.url, |
168 | path: options.path, | 169 | path: options.path, |
169 | token: options.token ?? options.server.accessToken, | 170 | token: options.token ?? options.server.accessToken, |
170 | query: { | 171 | query: { |
171 | ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles', 'privacyOneOf' ]), | 172 | ...pick(options, [ |
173 | 'isLocal', | ||
174 | 'include', | ||
175 | 'category', | ||
176 | 'tagsAllOf', | ||
177 | 'hasWebtorrentFiles', | ||
178 | 'hasHLSFiles', | ||
179 | 'privacyOneOf', | ||
180 | 'excludeAlreadyWatched' | ||
181 | ]), | ||
172 | 182 | ||
173 | sort: 'createdAt' | 183 | sort: 'createdAt' |
174 | }, | 184 | }, |
@@ -187,6 +197,7 @@ describe('Test videos filter', function () { | |||
187 | token?: string | 197 | token?: string |
188 | expectedStatus?: HttpStatusCode | 198 | expectedStatus?: HttpStatusCode |
189 | skipSubscription?: boolean | 199 | skipSubscription?: boolean |
200 | excludeAlreadyWatched?: boolean | ||
190 | } | 201 | } |
191 | ) { | 202 | ) { |
192 | const { skipSubscription = false } = options | 203 | const { skipSubscription = false } = options |
@@ -525,6 +536,25 @@ describe('Test videos filter', function () { | |||
525 | } | 536 | } |
526 | } | 537 | } |
527 | }) | 538 | }) |
539 | |||
540 | it('Should filter already watched videos by the user', async function () { | ||
541 | const { id } = await servers[0].videos.upload({ attributes: { name: 'video for history' } }) | ||
542 | |||
543 | for (const path of paths) { | ||
544 | const videos = await listVideos({ server: servers[0], path, isLocal: true, excludeAlreadyWatched: true }) | ||
545 | const foundVideo = videos.find(video => video.id === id) | ||
546 | |||
547 | expect(foundVideo).to.not.be.undefined | ||
548 | } | ||
549 | await servers[0].views.view({ id, token: servers[0].accessToken }) | ||
550 | |||
551 | for (const path of paths) { | ||
552 | const videos = await listVideos({ server: servers[0], path, excludeAlreadyWatched: true }) | ||
553 | const foundVideo = videos.find(video => video.id === id) | ||
554 | |||
555 | expect(foundVideo).to.be.undefined | ||
556 | } | ||
557 | }) | ||
528 | }) | 558 | }) |
529 | 559 | ||
530 | after(async function () { | 560 | after(async function () { |
diff --git a/shared/models/search/videos-common-query.model.ts b/shared/models/search/videos-common-query.model.ts index 2cbf7b014..da479c928 100644 --- a/shared/models/search/videos-common-query.model.ts +++ b/shared/models/search/videos-common-query.model.ts | |||
@@ -35,6 +35,8 @@ export interface VideosCommonQuery { | |||
35 | skipCount?: boolean | 35 | skipCount?: boolean |
36 | 36 | ||
37 | search?: string | 37 | search?: string |
38 | |||
39 | excludeAlreadyWatched?: boolean | ||
38 | } | 40 | } |
39 | 41 | ||
40 | export interface VideosCommonQueryAfterSanitize extends VideosCommonQuery { | 42 | export interface VideosCommonQueryAfterSanitize extends VideosCommonQuery { |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index 046eec544..a36ae0c7e 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -717,6 +717,7 @@ paths: | |||
717 | - $ref: '#/components/parameters/start' | 717 | - $ref: '#/components/parameters/start' |
718 | - $ref: '#/components/parameters/count' | 718 | - $ref: '#/components/parameters/count' |
719 | - $ref: '#/components/parameters/videosSort' | 719 | - $ref: '#/components/parameters/videosSort' |
720 | - $ref: '#/components/parameters/excludeAlreadyWatched' | ||
720 | responses: | 721 | responses: |
721 | '200': | 722 | '200': |
722 | description: successful operation | 723 | description: successful operation |
@@ -1835,6 +1836,7 @@ paths: | |||
1835 | - $ref: '#/components/parameters/start' | 1836 | - $ref: '#/components/parameters/start' |
1836 | - $ref: '#/components/parameters/count' | 1837 | - $ref: '#/components/parameters/count' |
1837 | - $ref: '#/components/parameters/videosSort' | 1838 | - $ref: '#/components/parameters/videosSort' |
1839 | - $ref: '#/components/parameters/excludeAlreadyWatched' | ||
1838 | responses: | 1840 | responses: |
1839 | '200': | 1841 | '200': |
1840 | description: successful operation | 1842 | description: successful operation |
@@ -2378,6 +2380,7 @@ paths: | |||
2378 | - $ref: '#/components/parameters/start' | 2380 | - $ref: '#/components/parameters/start' |
2379 | - $ref: '#/components/parameters/count' | 2381 | - $ref: '#/components/parameters/count' |
2380 | - $ref: '#/components/parameters/videosSort' | 2382 | - $ref: '#/components/parameters/videosSort' |
2383 | - $ref: '#/components/parameters/excludeAlreadyWatched' | ||
2381 | responses: | 2384 | responses: |
2382 | '200': | 2385 | '200': |
2383 | description: successful operation | 2386 | description: successful operation |
@@ -3799,6 +3802,7 @@ paths: | |||
3799 | - $ref: '#/components/parameters/start' | 3802 | - $ref: '#/components/parameters/start' |
3800 | - $ref: '#/components/parameters/count' | 3803 | - $ref: '#/components/parameters/count' |
3801 | - $ref: '#/components/parameters/videosSort' | 3804 | - $ref: '#/components/parameters/videosSort' |
3805 | - $ref: '#/components/parameters/excludeAlreadyWatched' | ||
3802 | responses: | 3806 | responses: |
3803 | '200': | 3807 | '200': |
3804 | description: successful operation | 3808 | description: successful operation |
@@ -4742,6 +4746,7 @@ paths: | |||
4742 | - $ref: '#/components/parameters/count' | 4746 | - $ref: '#/components/parameters/count' |
4743 | - $ref: '#/components/parameters/searchTarget' | 4747 | - $ref: '#/components/parameters/searchTarget' |
4744 | - $ref: '#/components/parameters/videosSearchSort' | 4748 | - $ref: '#/components/parameters/videosSearchSort' |
4749 | - $ref: '#/components/parameters/excludeAlreadyWatched' | ||
4745 | - name: startDate | 4750 | - name: startDate |
4746 | in: query | 4751 | in: query |
4747 | description: Get videos that are published after this date | 4752 | description: Get videos that are published after this date |
@@ -5872,6 +5877,12 @@ components: | |||
5872 | schema: | 5877 | schema: |
5873 | $ref: '#/components/schemas/VideoPrivacySet' | 5878 | $ref: '#/components/schemas/VideoPrivacySet' |
5874 | description: '**PeerTube >= 4.0** Display only videos in this specific privacy/privacies' | 5879 | description: '**PeerTube >= 4.0** Display only videos in this specific privacy/privacies' |
5880 | excludeAlreadyWatched: | ||
5881 | name: excludeAlreadyWatched | ||
5882 | in: query | ||
5883 | description: Whether or not to exclude videos that are in the user's video history | ||
5884 | schema: | ||
5885 | type: boolean | ||
5875 | uuids: | 5886 | uuids: |
5876 | name: uuids | 5887 | name: uuids |
5877 | in: query | 5888 | in: query |