diff options
author | Chocobozzz <me@florianbigard.com> | 2021-06-17 16:02:38 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-06-25 14:44:01 +0200 |
commit | 37a44fc915eef2140e22ceb96aba6b6eb2509007 (patch) | |
tree | dd4a370ecc96cf38c99b940261aadc27065da7ae /server/models/video | |
parent | 33eb19e5199cc9fa4d73c6675c97508e3e072ef9 (diff) | |
download | PeerTube-37a44fc915eef2140e22ceb96aba6b6eb2509007.tar.gz PeerTube-37a44fc915eef2140e22ceb96aba6b6eb2509007.tar.zst PeerTube-37a44fc915eef2140e22ceb96aba6b6eb2509007.zip |
Add ability to search playlists
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/sql/shared/abstract-videos-query-builder.ts | 2 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 4 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 84 |
3 files changed, 80 insertions, 10 deletions
diff --git a/server/models/video/sql/shared/abstract-videos-query-builder.ts b/server/models/video/sql/shared/abstract-videos-query-builder.ts index 3a1ee5b1f..09776bcb0 100644 --- a/server/models/video/sql/shared/abstract-videos-query-builder.ts +++ b/server/models/video/sql/shared/abstract-videos-query-builder.ts | |||
@@ -18,7 +18,7 @@ export class AbstractVideosQueryBuilder { | |||
18 | logging: options.logging, | 18 | logging: options.logging, |
19 | replacements: this.replacements, | 19 | replacements: this.replacements, |
20 | type: QueryTypes.SELECT as QueryTypes.SELECT, | 20 | type: QueryTypes.SELECT as QueryTypes.SELECT, |
21 | next: false | 21 | nest: false |
22 | } | 22 | } |
23 | 23 | ||
24 | return this.sequelize.query<any>(this.query, queryOptions) | 24 | return this.sequelize.query<any>(this.query, queryOptions) |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 33749ea70..f84b85290 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -434,8 +434,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"` | |||
434 | sort: string | 434 | sort: string |
435 | }) { | 435 | }) { |
436 | const attributesInclude = [] | 436 | const attributesInclude = [] |
437 | const escapedSearch = VideoModel.sequelize.escape(options.search) | 437 | const escapedSearch = VideoChannelModel.sequelize.escape(options.search) |
438 | const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%') | 438 | const escapedLikeSearch = VideoChannelModel.sequelize.escape('%' + options.search + '%') |
439 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) | 439 | attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search)) |
440 | 440 | ||
441 | const query = { | 441 | const query = { |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 1a05f8d42..7aa6b6c6e 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize' | 2 | import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
3 | import { | 3 | import { |
4 | AllowNull, | 4 | AllowNull, |
5 | BelongsTo, | 5 | BelongsTo, |
@@ -53,7 +53,15 @@ import { | |||
53 | } from '../../types/models/video/video-playlist' | 53 | } from '../../types/models/video/video-playlist' |
54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | 54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
55 | import { ActorModel } from '../actor/actor' | 55 | import { ActorModel } from '../actor/actor' |
56 | import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils' | 56 | import { |
57 | buildServerIdsFollowedBy, | ||
58 | buildTrigramSearchIndex, | ||
59 | buildWhereIdOrUUID, | ||
60 | createSimilarityAttribute, | ||
61 | getPlaylistSort, | ||
62 | isOutdated, | ||
63 | throwIfNotValid | ||
64 | } from '../utils' | ||
57 | import { ThumbnailModel } from './thumbnail' | 65 | import { ThumbnailModel } from './thumbnail' |
58 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 66 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
59 | import { VideoPlaylistElementModel } from './video-playlist-element' | 67 | import { VideoPlaylistElementModel } from './video-playlist-element' |
@@ -74,6 +82,11 @@ type AvailableForListOptions = { | |||
74 | videoChannelId?: number | 82 | videoChannelId?: number |
75 | listMyPlaylists?: boolean | 83 | listMyPlaylists?: boolean |
76 | search?: string | 84 | search?: string |
85 | withVideos?: boolean | ||
86 | } | ||
87 | |||
88 | function getVideoLengthSelect () { | ||
89 | return 'SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id"' | ||
77 | } | 90 | } |
78 | 91 | ||
79 | @Scopes(() => ({ | 92 | @Scopes(() => ({ |
@@ -89,7 +102,7 @@ type AvailableForListOptions = { | |||
89 | attributes: { | 102 | attributes: { |
90 | include: [ | 103 | include: [ |
91 | [ | 104 | [ |
92 | literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'), | 105 | literal(`(${getVideoLengthSelect()})`), |
93 | 'videosLength' | 106 | 'videosLength' |
94 | ] | 107 | ] |
95 | ] | 108 | ] |
@@ -178,11 +191,28 @@ type AvailableForListOptions = { | |||
178 | }) | 191 | }) |
179 | } | 192 | } |
180 | 193 | ||
194 | if (options.withVideos === true) { | ||
195 | whereAnd.push( | ||
196 | literal(`(${getVideoLengthSelect()}) != 0`) | ||
197 | ) | ||
198 | } | ||
199 | |||
200 | const attributesInclude = [] | ||
201 | |||
181 | if (options.search) { | 202 | if (options.search) { |
203 | const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search) | ||
204 | const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%') | ||
205 | attributesInclude.push(createSimilarityAttribute('VideoPlaylistModel.name', options.search)) | ||
206 | |||
182 | whereAnd.push({ | 207 | whereAnd.push({ |
183 | name: { | 208 | [Op.or]: [ |
184 | [Op.iLike]: '%' + options.search + '%' | 209 | Sequelize.literal( |
185 | } | 210 | 'lower(immutable_unaccent("VideoPlaylistModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' |
211 | ), | ||
212 | Sequelize.literal( | ||
213 | 'lower(immutable_unaccent("VideoPlaylistModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' | ||
214 | ) | ||
215 | ] | ||
186 | }) | 216 | }) |
187 | } | 217 | } |
188 | 218 | ||
@@ -191,6 +221,9 @@ type AvailableForListOptions = { | |||
191 | } | 221 | } |
192 | 222 | ||
193 | return { | 223 | return { |
224 | attributes: { | ||
225 | include: attributesInclude | ||
226 | }, | ||
194 | where, | 227 | where, |
195 | include: [ | 228 | include: [ |
196 | { | 229 | { |
@@ -211,6 +244,8 @@ type AvailableForListOptions = { | |||
211 | @Table({ | 244 | @Table({ |
212 | tableName: 'videoPlaylist', | 245 | tableName: 'videoPlaylist', |
213 | indexes: [ | 246 | indexes: [ |
247 | buildTrigramSearchIndex('video_playlist_name_trigram', 'name'), | ||
248 | |||
214 | { | 249 | { |
215 | fields: [ 'ownerAccountId' ] | 250 | fields: [ 'ownerAccountId' ] |
216 | }, | 251 | }, |
@@ -314,6 +349,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
314 | videoChannelId?: number | 349 | videoChannelId?: number |
315 | listMyPlaylists?: boolean | 350 | listMyPlaylists?: boolean |
316 | search?: string | 351 | search?: string |
352 | withVideos?: boolean // false by default | ||
317 | }) { | 353 | }) { |
318 | const query = { | 354 | const query = { |
319 | offset: options.start, | 355 | offset: options.start, |
@@ -331,7 +367,8 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
331 | accountId: options.accountId, | 367 | accountId: options.accountId, |
332 | videoChannelId: options.videoChannelId, | 368 | videoChannelId: options.videoChannelId, |
333 | listMyPlaylists: options.listMyPlaylists, | 369 | listMyPlaylists: options.listMyPlaylists, |
334 | search: options.search | 370 | search: options.search, |
371 | withVideos: options.withVideos || false | ||
335 | } as AvailableForListOptions | 372 | } as AvailableForListOptions |
336 | ] | 373 | ] |
337 | }, | 374 | }, |
@@ -347,6 +384,21 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
347 | }) | 384 | }) |
348 | } | 385 | } |
349 | 386 | ||
387 | static searchForApi (options: { | ||
388 | followerActorId: number | ||
389 | start: number | ||
390 | count: number | ||
391 | sort: string | ||
392 | search?: string | ||
393 | }) { | ||
394 | return VideoPlaylistModel.listForApi({ | ||
395 | ...options, | ||
396 | type: VideoPlaylistType.REGULAR, | ||
397 | listMyPlaylists: false, | ||
398 | withVideos: true | ||
399 | }) | ||
400 | } | ||
401 | |||
350 | static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { | 402 | static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) { |
351 | const where = { | 403 | const where = { |
352 | privacy: VideoPlaylistPrivacy.PUBLIC | 404 | privacy: VideoPlaylistPrivacy.PUBLIC |
@@ -445,6 +497,18 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
445 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) | 497 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) |
446 | } | 498 | } |
447 | 499 | ||
500 | static loadByUrlWithAccountAndChannelSummary (url: string): Promise<MVideoPlaylistFullSummary> { | ||
501 | const query = { | ||
502 | where: { | ||
503 | url | ||
504 | } | ||
505 | } | ||
506 | |||
507 | return VideoPlaylistModel | ||
508 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) | ||
509 | .findOne(query) | ||
510 | } | ||
511 | |||
448 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { | 512 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { |
449 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' | 513 | return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown' |
450 | } | 514 | } |
@@ -535,6 +599,10 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
535 | return setAsUpdated('videoPlaylist', this.id) | 599 | return setAsUpdated('videoPlaylist', this.id) |
536 | } | 600 | } |
537 | 601 | ||
602 | setVideosLength (videosLength: number) { | ||
603 | this.set('videosLength' as any, videosLength, { raw: true }) | ||
604 | } | ||
605 | |||
538 | isOwned () { | 606 | isOwned () { |
539 | return this.OwnerAccount.isOwned() | 607 | return this.OwnerAccount.isOwned() |
540 | } | 608 | } |
@@ -551,6 +619,8 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
551 | uuid: this.uuid, | 619 | uuid: this.uuid, |
552 | isLocal: this.isOwned(), | 620 | isLocal: this.isOwned(), |
553 | 621 | ||
622 | url: this.url, | ||
623 | |||
554 | displayName: this.name, | 624 | displayName: this.name, |
555 | description: this.description, | 625 | description: this.description, |
556 | privacy: { | 626 | privacy: { |