aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-08-23 17:58:39 +0200
committerChocobozzz <me@florianbigard.com>2018-08-27 09:41:54 +0200
commitf37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9 (patch)
tree2050443febcdb2a3eec68b7bbf9687e26dcb24dc /server/models/video
parent240085d0056fd97ac3c7fa8fa4ce9bc32afc4d6e (diff)
downloadPeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.tar.gz
PeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.tar.zst
PeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.zip
Add ability to search video channels
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/video-channel.ts163
1 files changed, 136 insertions, 27 deletions
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 9f80e0b8d..7d717fc68 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -12,6 +12,7 @@ import {
12 Is, 12 Is,
13 Model, 13 Model,
14 Scopes, 14 Scopes,
15 Sequelize,
15 Table, 16 Table,
16 UpdatedAt 17 UpdatedAt
17} from 'sequelize-typescript' 18} from 'sequelize-typescript'
@@ -24,19 +25,36 @@ import {
24} from '../../helpers/custom-validators/video-channels' 25} from '../../helpers/custom-validators/video-channels'
25import { sendDeleteActor } from '../../lib/activitypub/send' 26import { sendDeleteActor } from '../../lib/activitypub/send'
26import { AccountModel } from '../account/account' 27import { AccountModel } from '../account/account'
27import { ActorModel } from '../activitypub/actor' 28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
28import { getSort, throwIfNotValid } from '../utils' 29import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
29import { VideoModel } from './video' 30import { VideoModel } from './video'
30import { CONSTRAINTS_FIELDS } from '../../initializers' 31import { CONSTRAINTS_FIELDS } from '../../initializers'
31import { AvatarModel } from '../avatar/avatar'
32import { ServerModel } from '../server/server' 32import { ServerModel } from '../server/server'
33import { DefineIndexesOptions } from 'sequelize'
34
35// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
36const indexes: DefineIndexesOptions[] = [
37 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
38
39 {
40 fields: [ 'accountId' ]
41 },
42 {
43 fields: [ 'actorId' ]
44 }
45]
33 46
34enum ScopeNames { 47enum ScopeNames {
48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
35 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
36 WITH_ACTOR = 'WITH_ACTOR', 50 WITH_ACTOR = 'WITH_ACTOR',
37 WITH_VIDEOS = 'WITH_VIDEOS' 51 WITH_VIDEOS = 'WITH_VIDEOS'
38} 52}
39 53
54type AvailableForListOptions = {
55 actorId: number
56}
57
40@DefaultScope({ 58@DefaultScope({
41 include: [ 59 include: [
42 { 60 {
@@ -46,23 +64,57 @@ enum ScopeNames {
46 ] 64 ]
47}) 65})
48@Scopes({ 66@Scopes({
49 [ScopeNames.WITH_ACCOUNT]: { 67 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
50 include: [ 68 const actorIdNumber = parseInt(options.actorId + '', 10)
51 { 69
52 model: () => AccountModel.unscoped(), 70 // Only list local channels OR channels that are on an instance followed by actorId
53 required: true, 71 const inQueryInstanceFollow = '(' +
54 include: [ 72 'SELECT "actor"."serverId" FROM "actor" ' +
55 { 73 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = actor.id ' +
56 model: () => ActorModel.unscoped(), 74 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
57 required: true, 75 ')'
58 include: [ 76
77 return {
78 include: [
79 {
80 attributes: {
81 exclude: unusedActorAttributesForAPI
82 },
83 model: ActorModel,
84 where: {
85 [Sequelize.Op.or]: [
86 {
87 serverId: null
88 },
59 { 89 {
60 model: () => AvatarModel.unscoped(), 90 serverId: {
61 required: false 91 [ Sequelize.Op.in ]: Sequelize.literal(inQueryInstanceFollow)
92 }
62 } 93 }
63 ] 94 ]
64 } 95 }
65 ] 96 },
97 {
98 model: AccountModel,
99 required: true,
100 include: [
101 {
102 attributes: {
103 exclude: unusedActorAttributesForAPI
104 },
105 model: ActorModel, // Default scope includes avatar and server
106 required: true
107 }
108 ]
109 }
110 ]
111 }
112 },
113 [ScopeNames.WITH_ACCOUNT]: {
114 include: [
115 {
116 model: () => AccountModel,
117 required: true
66 } 118 }
67 ] 119 ]
68 }, 120 },
@@ -79,14 +131,7 @@ enum ScopeNames {
79}) 131})
80@Table({ 132@Table({
81 tableName: 'videoChannel', 133 tableName: 'videoChannel',
82 indexes: [ 134 indexes
83 {
84 fields: [ 'accountId' ]
85 },
86 {
87 fields: [ 'actorId' ]
88 }
89 ]
90}) 135})
91export class VideoChannelModel extends Model<VideoChannelModel> { 136export class VideoChannelModel extends Model<VideoChannelModel> {
92 137
@@ -170,15 +215,61 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
170 return VideoChannelModel.count(query) 215 return VideoChannelModel.count(query)
171 } 216 }
172 217
173 static listForApi (start: number, count: number, sort: string) { 218 static listForApi (actorId: number, start: number, count: number, sort: string) {
174 const query = { 219 const query = {
175 offset: start, 220 offset: start,
176 limit: count, 221 limit: count,
177 order: getSort(sort) 222 order: getSort(sort)
178 } 223 }
179 224
225 const scopes = {
226 method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId } as AvailableForListOptions ]
227 }
180 return VideoChannelModel 228 return VideoChannelModel
181 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 229 .scope(scopes)
230 .findAndCountAll(query)
231 .then(({ rows, count }) => {
232 return { total: count, data: rows }
233 })
234 }
235
236 static searchForApi (options: {
237 actorId: number
238 search: string
239 start: number
240 count: number
241 sort: string
242 }) {
243 const attributesInclude = []
244 const escapedSearch = VideoModel.sequelize.escape(options.search)
245 const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%')
246 attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search))
247
248 const query = {
249 attributes: {
250 include: attributesInclude
251 },
252 offset: options.start,
253 limit: options.count,
254 order: getSort(options.sort),
255 where: {
256 id: {
257 [ Sequelize.Op.in ]: Sequelize.literal(
258 '(' +
259 'SELECT id FROM "videoChannel" WHERE ' +
260 'lower(immutable_unaccent("name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
261 'lower(immutable_unaccent("name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
262 ')'
263 )
264 }
265 }
266 }
267
268 const scopes = {
269 method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId: options.actorId } as AvailableForListOptions ]
270 }
271 return VideoChannelModel
272 .scope(scopes)
182 .findAndCountAll(query) 273 .findAndCountAll(query)
183 .then(({ rows, count }) => { 274 .then(({ rows, count }) => {
184 return { total: count, data: rows } 275 return { total: count, data: rows }
@@ -239,7 +330,25 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
239 } 330 }
240 331
241 return VideoChannelModel 332 return VideoChannelModel
242 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 333 .scope([ ScopeNames.WITH_ACCOUNT ])
334 .findOne(query)
335 }
336
337 static loadByUrlAndPopulateAccount (url: string) {
338 const query = {
339 include: [
340 {
341 model: ActorModel,
342 required: true,
343 where: {
344 url
345 }
346 }
347 ]
348 }
349
350 return VideoChannelModel
351 .scope([ ScopeNames.WITH_ACCOUNT ])
243 .findOne(query) 352 .findOne(query)
244 } 353 }
245 354