aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-06-17 16:02:38 +0200
committerChocobozzz <chocobozzz@cpy.re>2021-06-25 14:44:01 +0200
commit37a44fc915eef2140e22ceb96aba6b6eb2509007 (patch)
treedd4a370ecc96cf38c99b940261aadc27065da7ae /server/models/video
parent33eb19e5199cc9fa4d73c6675c97508e3e072ef9 (diff)
downloadPeerTube-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.ts2
-rw-r--r--server/models/video/video-channel.ts4
-rw-r--r--server/models/video/video-playlist.ts84
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 @@
1import { join } from 'path' 1import { join } from 'path'
2import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize' 2import { FindOptions, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
3import { 3import {
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'
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'
56import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils' 56import {
57 buildServerIdsFollowedBy,
58 buildTrigramSearchIndex,
59 buildWhereIdOrUUID,
60 createSimilarityAttribute,
61 getPlaylistSort,
62 isOutdated,
63 throwIfNotValid
64} from '../utils'
57import { ThumbnailModel } from './thumbnail' 65import { ThumbnailModel } from './thumbnail'
58import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' 66import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
59import { VideoPlaylistElementModel } from './video-playlist-element' 67import { 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
88function 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: {