aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account.ts3
-rw-r--r--server/models/account/user-video-history.ts55
-rw-r--r--server/models/account/user.ts23
-rw-r--r--server/models/activitypub/actor-follow.ts90
-rw-r--r--server/models/video/video-format-utils.ts9
-rw-r--r--server/models/video/video.ts111
6 files changed, 232 insertions, 59 deletions
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 27c75d886..5a237d733 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -248,7 +248,8 @@ export class AccountModel extends Model<AccountModel> {
248 displayName: this.getDisplayName(), 248 displayName: this.getDisplayName(),
249 description: this.description, 249 description: this.description,
250 createdAt: this.createdAt, 250 createdAt: this.createdAt,
251 updatedAt: this.updatedAt 251 updatedAt: this.updatedAt,
252 userId: this.userId ? this.userId : undefined
252 } 253 }
253 254
254 return Object.assign(actor, account) 255 return Object.assign(actor, account)
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts
new file mode 100644
index 000000000..0476cad9d
--- /dev/null
+++ b/server/models/account/user-video-history.ts
@@ -0,0 +1,55 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video'
3import { UserModel } from './user'
4
5@Table({
6 tableName: 'userVideoHistory',
7 indexes: [
8 {
9 fields: [ 'userId', 'videoId' ],
10 unique: true
11 },
12 {
13 fields: [ 'userId' ]
14 },
15 {
16 fields: [ 'videoId' ]
17 }
18 ]
19})
20export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
21 @CreatedAt
22 createdAt: Date
23
24 @UpdatedAt
25 updatedAt: Date
26
27 @AllowNull(false)
28 @IsInt
29 @Column
30 currentTime: number
31
32 @ForeignKey(() => VideoModel)
33 @Column
34 videoId: number
35
36 @BelongsTo(() => VideoModel, {
37 foreignKey: {
38 allowNull: false
39 },
40 onDelete: 'CASCADE'
41 })
42 Video: VideoModel
43
44 @ForeignKey(() => UserModel)
45 @Column
46 userId: number
47
48 @BelongsTo(() => UserModel, {
49 foreignKey: {
50 allowNull: false
51 },
52 onDelete: 'CASCADE'
53 })
54 User: UserModel
55}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index e56b0bf40..39654cfcf 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -181,7 +181,25 @@ export class UserModel extends Model<UserModel> {
181 return this.count() 181 return this.count()
182 } 182 }
183 183
184 static listForApi (start: number, count: number, sort: string) { 184 static listForApi (start: number, count: number, sort: string, search?: string) {
185 let where = undefined
186 if (search) {
187 where = {
188 [Sequelize.Op.or]: [
189 {
190 email: {
191 [Sequelize.Op.iLike]: '%' + search + '%'
192 }
193 },
194 {
195 username: {
196 [ Sequelize.Op.iLike ]: '%' + search + '%'
197 }
198 }
199 ]
200 }
201 }
202
185 const query = { 203 const query = {
186 attributes: { 204 attributes: {
187 include: [ 205 include: [
@@ -204,7 +222,8 @@ export class UserModel extends Model<UserModel> {
204 }, 222 },
205 offset: start, 223 offset: start,
206 limit: count, 224 limit: count,
207 order: getSort(sort) 225 order: getSort(sort),
226 where
208 } 227 }
209 228
210 return UserModel.findAndCountAll(query) 229 return UserModel.findAndCountAll(query)
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index 27bb43dae..3373355ef 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -280,7 +280,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
280 return ActorFollowModel.findAll(query) 280 return ActorFollowModel.findAll(query)
281 } 281 }
282 282
283 static listFollowingForApi (id: number, start: number, count: number, sort: string) { 283 static listFollowingForApi (id: number, start: number, count: number, sort: string, search?: string) {
284 const query = { 284 const query = {
285 distinct: true, 285 distinct: true,
286 offset: start, 286 offset: start,
@@ -299,7 +299,17 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
299 model: ActorModel, 299 model: ActorModel,
300 as: 'ActorFollowing', 300 as: 'ActorFollowing',
301 required: true, 301 required: true,
302 include: [ ServerModel ] 302 include: [
303 {
304 model: ServerModel,
305 required: true,
306 where: search ? {
307 host: {
308 [Sequelize.Op.iLike]: '%' + search + '%'
309 }
310 } : undefined
311 }
312 ]
303 } 313 }
304 ] 314 ]
305 } 315 }
@@ -313,6 +323,49 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
313 }) 323 })
314 } 324 }
315 325
326 static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) {
327 const query = {
328 distinct: true,
329 offset: start,
330 limit: count,
331 order: getSort(sort),
332 include: [
333 {
334 model: ActorModel,
335 required: true,
336 as: 'ActorFollower',
337 include: [
338 {
339 model: ServerModel,
340 required: true,
341 where: search ? {
342 host: {
343 [ Sequelize.Op.iLike ]: '%' + search + '%'
344 }
345 } : undefined
346 }
347 ]
348 },
349 {
350 model: ActorModel,
351 as: 'ActorFollowing',
352 required: true,
353 where: {
354 id
355 }
356 }
357 ]
358 }
359
360 return ActorFollowModel.findAndCountAll(query)
361 .then(({ rows, count }) => {
362 return {
363 data: rows,
364 total: count
365 }
366 })
367 }
368
316 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { 369 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
317 const query = { 370 const query = {
318 attributes: [], 371 attributes: [],
@@ -370,39 +423,6 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
370 }) 423 })
371 } 424 }
372 425
373 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
374 const query = {
375 distinct: true,
376 offset: start,
377 limit: count,
378 order: getSort(sort),
379 include: [
380 {
381 model: ActorModel,
382 required: true,
383 as: 'ActorFollower',
384 include: [ ServerModel ]
385 },
386 {
387 model: ActorModel,
388 as: 'ActorFollowing',
389 required: true,
390 where: {
391 id
392 }
393 }
394 ]
395 }
396
397 return ActorFollowModel.findAndCountAll(query)
398 .then(({ rows, count }) => {
399 return {
400 data: rows,
401 total: count
402 }
403 })
404 }
405
406 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { 426 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
407 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) 427 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
408 } 428 }
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index f23dde9b8..905e84449 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -10,6 +10,7 @@ import {
10 getVideoLikesActivityPubUrl, 10 getVideoLikesActivityPubUrl,
11 getVideoSharesActivityPubUrl 11 getVideoSharesActivityPubUrl
12} from '../../lib/activitypub' 12} from '../../lib/activitypub'
13import { isArray } from '../../helpers/custom-validators/misc'
13 14
14export type VideoFormattingJSONOptions = { 15export type VideoFormattingJSONOptions = {
15 completeDescription?: boolean 16 completeDescription?: boolean
@@ -24,6 +25,8 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
24 const formattedAccount = video.VideoChannel.Account.toFormattedJSON() 25 const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
25 const formattedVideoChannel = video.VideoChannel.toFormattedJSON() 26 const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
26 27
28 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
29
27 const videoObject: Video = { 30 const videoObject: Video = {
28 id: video.id, 31 id: video.id,
29 uuid: video.uuid, 32 uuid: video.uuid,
@@ -74,7 +77,11 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
74 url: formattedVideoChannel.url, 77 url: formattedVideoChannel.url,
75 host: formattedVideoChannel.host, 78 host: formattedVideoChannel.host,
76 avatar: formattedVideoChannel.avatar 79 avatar: formattedVideoChannel.avatar
77 } 80 },
81
82 userHistory: userHistory ? {
83 currentTime: userHistory.currentTime
84 } : undefined
78 } 85 }
79 86
80 if (options) { 87 if (options) {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 6c89c16bf..4f3f75613 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -92,6 +92,7 @@ import {
92 videoModelToFormattedJSON 92 videoModelToFormattedJSON
93} from './video-format-utils' 93} from './video-format-utils'
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history'
95 96
96// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 97// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
97const indexes: Sequelize.DefineIndexesOptions[] = [ 98const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -127,7 +128,8 @@ export enum ScopeNames {
127 WITH_TAGS = 'WITH_TAGS', 128 WITH_TAGS = 'WITH_TAGS',
128 WITH_FILES = 'WITH_FILES', 129 WITH_FILES = 'WITH_FILES',
129 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 130 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
130 WITH_BLACKLISTED = 'WITH_BLACKLISTED' 131 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
132 WITH_USER_HISTORY = 'WITH_USER_HISTORY'
131} 133}
132 134
133type ForAPIOptions = { 135type ForAPIOptions = {
@@ -233,7 +235,14 @@ type AvailableForListIDsOptions = {
233 ) 235 )
234 } 236 }
235 ] 237 ]
236 }, 238 }
239 },
240 include: []
241 }
242
243 // Only list public/published videos
244 if (!options.filter || options.filter !== 'all-local') {
245 const privacyWhere = {
237 // Always list public videos 246 // Always list public videos
238 privacy: VideoPrivacy.PUBLIC, 247 privacy: VideoPrivacy.PUBLIC,
239 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding 248 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
@@ -248,8 +257,9 @@ type AvailableForListIDsOptions = {
248 } 257 }
249 } 258 }
250 ] 259 ]
251 }, 260 }
252 include: [] 261
262 Object.assign(query.where, privacyWhere)
253 } 263 }
254 264
255 if (options.filter || options.accountId || options.videoChannelId) { 265 if (options.filter || options.accountId || options.videoChannelId) {
@@ -464,6 +474,8 @@ type AvailableForListIDsOptions = {
464 include: [ 474 include: [
465 { 475 {
466 model: () => VideoFileModel.unscoped(), 476 model: () => VideoFileModel.unscoped(),
477 // FIXME: typings
478 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
467 required: false, 479 required: false,
468 include: [ 480 include: [
469 { 481 {
@@ -482,6 +494,20 @@ type AvailableForListIDsOptions = {
482 required: false 494 required: false
483 } 495 }
484 ] 496 ]
497 },
498 [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => {
499 return {
500 include: [
501 {
502 attributes: [ 'currentTime' ],
503 model: UserVideoHistoryModel.unscoped(),
504 required: false,
505 where: {
506 userId
507 }
508 }
509 ]
510 }
485 } 511 }
486}) 512})
487@Table({ 513@Table({
@@ -672,11 +698,19 @@ export class VideoModel extends Model<VideoModel> {
672 name: 'videoId', 698 name: 'videoId',
673 allowNull: false 699 allowNull: false
674 }, 700 },
675 onDelete: 'cascade', 701 onDelete: 'cascade'
676 hooks: true
677 }) 702 })
678 VideoViews: VideoViewModel[] 703 VideoViews: VideoViewModel[]
679 704
705 @HasMany(() => UserVideoHistoryModel, {
706 foreignKey: {
707 name: 'videoId',
708 allowNull: false
709 },
710 onDelete: 'cascade'
711 })
712 UserVideoHistories: UserVideoHistoryModel[]
713
680 @HasOne(() => ScheduleVideoUpdateModel, { 714 @HasOne(() => ScheduleVideoUpdateModel, {
681 foreignKey: { 715 foreignKey: {
682 name: 'videoId', 716 name: 'videoId',
@@ -762,6 +796,16 @@ export class VideoModel extends Model<VideoModel> {
762 return VideoModel.scope(ScopeNames.WITH_FILES).findAll() 796 return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
763 } 797 }
764 798
799 static listLocal () {
800 const query = {
801 where: {
802 remote: false
803 }
804 }
805
806 return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query)
807 }
808
765 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 809 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
766 function getRawQuery (select: string) { 810 function getRawQuery (select: string) {
767 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + 811 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
@@ -930,8 +974,13 @@ export class VideoModel extends Model<VideoModel> {
930 accountId?: number, 974 accountId?: number,
931 videoChannelId?: number, 975 videoChannelId?: number,
932 actorId?: number 976 actorId?: number
933 trendingDays?: number 977 trendingDays?: number,
978 userId?: number
934 }, countVideos = true) { 979 }, countVideos = true) {
980 if (options.filter && options.filter === 'all-local' && !options.userId) {
981 throw new Error('Try to filter all-local but no userId is provided')
982 }
983
935 const query: IFindOptions<VideoModel> = { 984 const query: IFindOptions<VideoModel> = {
936 offset: options.start, 985 offset: options.start,
937 limit: options.count, 986 limit: options.count,
@@ -961,6 +1010,7 @@ export class VideoModel extends Model<VideoModel> {
961 accountId: options.accountId, 1010 accountId: options.accountId,
962 videoChannelId: options.videoChannelId, 1011 videoChannelId: options.videoChannelId,
963 includeLocalVideos: options.includeLocalVideos, 1012 includeLocalVideos: options.includeLocalVideos,
1013 userId: options.userId,
964 trendingDays 1014 trendingDays
965 } 1015 }
966 1016
@@ -983,6 +1033,8 @@ export class VideoModel extends Model<VideoModel> {
983 tagsAllOf?: string[] 1033 tagsAllOf?: string[]
984 durationMin?: number // seconds 1034 durationMin?: number // seconds
985 durationMax?: number // seconds 1035 durationMax?: number // seconds
1036 userId?: number,
1037 filter?: VideoFilter
986 }) { 1038 }) {
987 const whereAnd = [] 1039 const whereAnd = []
988 1040
@@ -1058,7 +1110,9 @@ export class VideoModel extends Model<VideoModel> {
1058 licenceOneOf: options.licenceOneOf, 1110 licenceOneOf: options.licenceOneOf,
1059 languageOneOf: options.languageOneOf, 1111 languageOneOf: options.languageOneOf,
1060 tagsOneOf: options.tagsOneOf, 1112 tagsOneOf: options.tagsOneOf,
1061 tagsAllOf: options.tagsAllOf 1113 tagsAllOf: options.tagsAllOf,
1114 userId: options.userId,
1115 filter: options.filter
1062 } 1116 }
1063 1117
1064 return VideoModel.getAvailableForApi(query, queryOptions) 1118 return VideoModel.getAvailableForApi(query, queryOptions)
@@ -1125,7 +1179,7 @@ export class VideoModel extends Model<VideoModel> {
1125 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1179 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
1126 } 1180 }
1127 1181
1128 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) { 1182 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
1129 const where = VideoModel.buildWhereIdOrUUID(id) 1183 const where = VideoModel.buildWhereIdOrUUID(id)
1130 1184
1131 const options = { 1185 const options = {
@@ -1134,14 +1188,20 @@ export class VideoModel extends Model<VideoModel> {
1134 transaction: t 1188 transaction: t
1135 } 1189 }
1136 1190
1191 const scopes = [
1192 ScopeNames.WITH_TAGS,
1193 ScopeNames.WITH_BLACKLISTED,
1194 ScopeNames.WITH_FILES,
1195 ScopeNames.WITH_ACCOUNT_DETAILS,
1196 ScopeNames.WITH_SCHEDULED_UPDATE
1197 ]
1198
1199 if (userId) {
1200 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
1201 }
1202
1137 return VideoModel 1203 return VideoModel
1138 .scope([ 1204 .scope(scopes)
1139 ScopeNames.WITH_TAGS,
1140 ScopeNames.WITH_BLACKLISTED,
1141 ScopeNames.WITH_FILES,
1142 ScopeNames.WITH_ACCOUNT_DETAILS,
1143 ScopeNames.WITH_SCHEDULED_UPDATE
1144 ])
1145 .findOne(options) 1205 .findOne(options)
1146 } 1206 }
1147 1207
@@ -1216,7 +1276,7 @@ export class VideoModel extends Model<VideoModel> {
1216 } 1276 }
1217 1277
1218 private static buildActorWhereWithFilter (filter?: VideoFilter) { 1278 private static buildActorWhereWithFilter (filter?: VideoFilter) {
1219 if (filter && filter === 'local') { 1279 if (filter && (filter === 'local' || filter === 'all-local')) {
1220 return { 1280 return {
1221 serverId: null 1281 serverId: null
1222 } 1282 }
@@ -1225,7 +1285,11 @@ export class VideoModel extends Model<VideoModel> {
1225 return {} 1285 return {}
1226 } 1286 }
1227 1287
1228 private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) { 1288 private static async getAvailableForApi (
1289 query: IFindOptions<VideoModel>,
1290 options: AvailableForListIDsOptions & { userId?: number},
1291 countVideos = true
1292 ) {
1229 const idsScope = { 1293 const idsScope = {
1230 method: [ 1294 method: [
1231 ScopeNames.AVAILABLE_FOR_LIST_IDS, options 1295 ScopeNames.AVAILABLE_FOR_LIST_IDS, options
@@ -1249,8 +1313,15 @@ export class VideoModel extends Model<VideoModel> {
1249 1313
1250 if (ids.length === 0) return { data: [], total: count } 1314 if (ids.length === 0) return { data: [], total: count }
1251 1315
1252 const apiScope = { 1316 // FIXME: typings
1253 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] 1317 const apiScope: any[] = [
1318 {
1319 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
1320 }
1321 ]
1322
1323 if (options.userId) {
1324 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] })
1254 } 1325 }
1255 1326
1256 const secondQuery = { 1327 const secondQuery = {