diff options
author | Chocobozzz <me@florianbigard.com> | 2018-10-05 11:15:06 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-10-05 11:22:38 +0200 |
commit | 6e46de095d7169355dd83030f6ce4a582304153a (patch) | |
tree | dfa78e2008d3d135a00b798b05350b4975145acc /server/models | |
parent | a585824160d016db7c9bff0e1cb1ffa3aaf73d74 (diff) | |
download | PeerTube-6e46de095d7169355dd83030f6ce4a582304153a.tar.gz PeerTube-6e46de095d7169355dd83030f6ce4a582304153a.tar.zst PeerTube-6e46de095d7169355dd83030f6ce4a582304153a.zip |
Add user history and resume videos
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/account/user-video-history.ts | 55 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 9 | ||||
-rw-r--r-- | server/models/video/video.ts | 80 |
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 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoModel } from '../video/video' | ||
3 | import { 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 | }) | ||
20 | export 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' |
13 | import { isArray } from 'util' | ||
13 | 14 | ||
14 | export type VideoFormattingJSONOptions = { | 15 | export 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' |
94 | import * as validator from 'validator' | 94 | import * as validator from 'validator' |
95 | import { 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 |
97 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 99 | const 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 | ||
133 | type ForAPIOptions = { | 136 | type 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 = { |