aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/user-video-history.ts55
-rw-r--r--server/models/video/video-format-utils.ts9
-rw-r--r--server/models/video/video.ts80
3 files changed, 127 insertions, 17 deletions
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/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index f23dde9b8..78972b199 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 'util'
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..0a2d7e6de 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -92,6 +92,8 @@ 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'
96
95 97
96// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 98// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
97const indexes: Sequelize.DefineIndexesOptions[] = [ 99const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -127,7 +129,8 @@ export enum ScopeNames {
127 WITH_TAGS = 'WITH_TAGS', 129 WITH_TAGS = 'WITH_TAGS',
128 WITH_FILES = 'WITH_FILES', 130 WITH_FILES = 'WITH_FILES',
129 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 131 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
130 WITH_BLACKLISTED = 'WITH_BLACKLISTED' 132 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
133 WITH_USER_HISTORY = 'WITH_USER_HISTORY'
131} 134}
132 135
133type ForAPIOptions = { 136type ForAPIOptions = {
@@ -464,6 +467,8 @@ type AvailableForListIDsOptions = {
464 include: [ 467 include: [
465 { 468 {
466 model: () => VideoFileModel.unscoped(), 469 model: () => VideoFileModel.unscoped(),
470 // FIXME: typings
471 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
467 required: false, 472 required: false,
468 include: [ 473 include: [
469 { 474 {
@@ -482,6 +487,20 @@ type AvailableForListIDsOptions = {
482 required: false 487 required: false
483 } 488 }
484 ] 489 ]
490 },
491 [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => {
492 return {
493 include: [
494 {
495 attributes: [ 'currentTime' ],
496 model: UserVideoHistoryModel.unscoped(),
497 required: false,
498 where: {
499 userId
500 }
501 }
502 ]
503 }
485 } 504 }
486}) 505})
487@Table({ 506@Table({
@@ -672,11 +691,19 @@ export class VideoModel extends Model<VideoModel> {
672 name: 'videoId', 691 name: 'videoId',
673 allowNull: false 692 allowNull: false
674 }, 693 },
675 onDelete: 'cascade', 694 onDelete: 'cascade'
676 hooks: true
677 }) 695 })
678 VideoViews: VideoViewModel[] 696 VideoViews: VideoViewModel[]
679 697
698 @HasMany(() => UserVideoHistoryModel, {
699 foreignKey: {
700 name: 'videoId',
701 allowNull: false
702 },
703 onDelete: 'cascade'
704 })
705 UserVideoHistories: UserVideoHistoryModel[]
706
680 @HasOne(() => ScheduleVideoUpdateModel, { 707 @HasOne(() => ScheduleVideoUpdateModel, {
681 foreignKey: { 708 foreignKey: {
682 name: 'videoId', 709 name: 'videoId',
@@ -930,7 +957,8 @@ export class VideoModel extends Model<VideoModel> {
930 accountId?: number, 957 accountId?: number,
931 videoChannelId?: number, 958 videoChannelId?: number,
932 actorId?: number 959 actorId?: number
933 trendingDays?: number 960 trendingDays?: number,
961 userId?: number
934 }, countVideos = true) { 962 }, countVideos = true) {
935 const query: IFindOptions<VideoModel> = { 963 const query: IFindOptions<VideoModel> = {
936 offset: options.start, 964 offset: options.start,
@@ -961,6 +989,7 @@ export class VideoModel extends Model<VideoModel> {
961 accountId: options.accountId, 989 accountId: options.accountId,
962 videoChannelId: options.videoChannelId, 990 videoChannelId: options.videoChannelId,
963 includeLocalVideos: options.includeLocalVideos, 991 includeLocalVideos: options.includeLocalVideos,
992 userId: options.userId,
964 trendingDays 993 trendingDays
965 } 994 }
966 995
@@ -983,6 +1012,7 @@ export class VideoModel extends Model<VideoModel> {
983 tagsAllOf?: string[] 1012 tagsAllOf?: string[]
984 durationMin?: number // seconds 1013 durationMin?: number // seconds
985 durationMax?: number // seconds 1014 durationMax?: number // seconds
1015 userId?: number
986 }) { 1016 }) {
987 const whereAnd = [] 1017 const whereAnd = []
988 1018
@@ -1058,7 +1088,8 @@ export class VideoModel extends Model<VideoModel> {
1058 licenceOneOf: options.licenceOneOf, 1088 licenceOneOf: options.licenceOneOf,
1059 languageOneOf: options.languageOneOf, 1089 languageOneOf: options.languageOneOf,
1060 tagsOneOf: options.tagsOneOf, 1090 tagsOneOf: options.tagsOneOf,
1061 tagsAllOf: options.tagsAllOf 1091 tagsAllOf: options.tagsAllOf,
1092 userId: options.userId
1062 } 1093 }
1063 1094
1064 return VideoModel.getAvailableForApi(query, queryOptions) 1095 return VideoModel.getAvailableForApi(query, queryOptions)
@@ -1125,7 +1156,7 @@ export class VideoModel extends Model<VideoModel> {
1125 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1156 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
1126 } 1157 }
1127 1158
1128 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) { 1159 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
1129 const where = VideoModel.buildWhereIdOrUUID(id) 1160 const where = VideoModel.buildWhereIdOrUUID(id)
1130 1161
1131 const options = { 1162 const options = {
@@ -1134,14 +1165,20 @@ export class VideoModel extends Model<VideoModel> {
1134 transaction: t 1165 transaction: t
1135 } 1166 }
1136 1167
1168 const scopes = [
1169 ScopeNames.WITH_TAGS,
1170 ScopeNames.WITH_BLACKLISTED,
1171 ScopeNames.WITH_FILES,
1172 ScopeNames.WITH_ACCOUNT_DETAILS,
1173 ScopeNames.WITH_SCHEDULED_UPDATE
1174 ]
1175
1176 if (userId) {
1177 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
1178 }
1179
1137 return VideoModel 1180 return VideoModel
1138 .scope([ 1181 .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) 1182 .findOne(options)
1146 } 1183 }
1147 1184
@@ -1225,7 +1262,11 @@ export class VideoModel extends Model<VideoModel> {
1225 return {} 1262 return {}
1226 } 1263 }
1227 1264
1228 private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) { 1265 private static async getAvailableForApi (
1266 query: IFindOptions<VideoModel>,
1267 options: AvailableForListIDsOptions & { userId?: number},
1268 countVideos = true
1269 ) {
1229 const idsScope = { 1270 const idsScope = {
1230 method: [ 1271 method: [
1231 ScopeNames.AVAILABLE_FOR_LIST_IDS, options 1272 ScopeNames.AVAILABLE_FOR_LIST_IDS, options
@@ -1249,8 +1290,15 @@ export class VideoModel extends Model<VideoModel> {
1249 1290
1250 if (ids.length === 0) return { data: [], total: count } 1291 if (ids.length === 0) return { data: [], total: count }
1251 1292
1252 const apiScope = { 1293 // FIXME: typings
1253 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ] 1294 const apiScope: any[] = [
1295 {
1296 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
1297 }
1298 ]
1299
1300 if (options.userId) {
1301 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] })
1254 } 1302 }
1255 1303
1256 const secondQuery = { 1304 const secondQuery = {