]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-playlist-element.ts
Lazy load avatars
[github/Chocobozzz/PeerTube.git] / server / models / video / video-playlist-element.ts
CommitLineData
418d092a
C
1import {
2 AllowNull,
3 BelongsTo,
4 Column,
5 CreatedAt,
6 DataType,
7 Default,
8 ForeignKey,
9 Is,
10 IsInt,
11 Min,
12 Model,
13 Table,
14 UpdatedAt
15} from 'sequelize-typescript'
bfbd9128 16import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
418d092a 17import { VideoPlaylistModel } from './video-playlist'
418d092a
C
18import { getSort, throwIfNotValid } from '../utils'
19import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
74dc3bca 20import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
418d092a 21import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
df0b219d 22import * as validator from 'validator'
bfbd9128
C
23import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize'
24import { UserModel } from '../account/user'
25import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
26import { AccountModel } from '../account/account'
27import { VideoPrivacy } from '../../../shared/models/videos'
418d092a
C
28
29@Table({
30 tableName: 'videoPlaylistElement',
31 indexes: [
32 {
33 fields: [ 'videoPlaylistId' ]
34 },
35 {
36 fields: [ 'videoId' ]
37 },
38 {
39 fields: [ 'videoPlaylistId', 'videoId' ],
40 unique: true
41 },
418d092a
C
42 {
43 fields: [ 'url' ],
44 unique: true
45 }
46 ]
47})
48export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> {
49 @CreatedAt
50 createdAt: Date
51
52 @UpdatedAt
53 updatedAt: Date
54
55 @AllowNull(false)
56 @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
57 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
58 url: string
59
60 @AllowNull(false)
61 @Default(1)
62 @IsInt
63 @Min(1)
64 @Column
65 position: number
66
67 @AllowNull(true)
68 @IsInt
69 @Min(0)
70 @Column
71 startTimestamp: number
72
73 @AllowNull(true)
74 @IsInt
75 @Min(0)
76 @Column
77 stopTimestamp: number
78
79 @ForeignKey(() => VideoPlaylistModel)
80 @Column
81 videoPlaylistId: number
82
83 @BelongsTo(() => VideoPlaylistModel, {
84 foreignKey: {
85 allowNull: false
86 },
87 onDelete: 'CASCADE'
88 })
89 VideoPlaylist: VideoPlaylistModel
90
91 @ForeignKey(() => VideoModel)
92 @Column
93 videoId: number
94
95 @BelongsTo(() => VideoModel, {
96 foreignKey: {
bfbd9128 97 allowNull: true
418d092a 98 },
bfbd9128 99 onDelete: 'set null'
418d092a
C
100 })
101 Video: VideoModel
102
1735c825 103 static deleteAllOf (videoPlaylistId: number, transaction?: Transaction) {
418d092a
C
104 const query = {
105 where: {
106 videoPlaylistId
107 },
108 transaction
109 }
110
111 return VideoPlaylistElementModel.destroy(query)
112 }
113
bfbd9128
C
114 static listForApi (options: {
115 start: number,
116 count: number,
117 videoPlaylistId: number,
118 serverAccount: AccountModel,
119 user?: UserModel
120 }) {
121 const accountIds = [ options.serverAccount.id ]
122 const videoScope: (ScopeOptions | string)[] = [
123 VideoScopeNames.WITH_BLACKLISTED
124 ]
125
126 if (options.user) {
127 accountIds.push(options.user.Account.id)
128 videoScope.push({ method: [ VideoScopeNames.WITH_USER_HISTORY, options.user.id ] })
129 }
130
131 const forApiOptions: ForAPIOptions = { withAccountBlockerIds: accountIds }
132 videoScope.push({
133 method: [
134 VideoScopeNames.FOR_API, forApiOptions
135 ]
136 })
137
138 const findQuery = {
139 offset: options.start,
140 limit: options.count,
141 order: getSort('position'),
142 where: {
143 videoPlaylistId: options.videoPlaylistId
144 },
145 include: [
146 {
147 model: VideoModel.scope(videoScope),
148 required: false
149 }
150 ]
151 }
152
153 const countQuery = {
154 where: {
155 videoPlaylistId: options.videoPlaylistId
156 }
157 }
158
159 return Promise.all([
160 VideoPlaylistElementModel.count(countQuery),
161 VideoPlaylistElementModel.findAll(findQuery)
162 ]).then(([ total, data ]) => ({ total, data }))
163 }
164
418d092a
C
165 static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) {
166 const query = {
167 where: {
168 videoPlaylistId,
169 videoId
170 }
171 }
172
173 return VideoPlaylistElementModel.findOne(query)
174 }
175
bfbd9128
C
176 static loadById (playlistElementId: number) {
177 return VideoPlaylistElementModel.findByPk(playlistElementId)
178 }
179
418d092a
C
180 static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) {
181 const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
182 const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId }
183
184 const query = {
185 include: [
186 {
187 attributes: [ 'privacy' ],
188 model: VideoPlaylistModel.unscoped(),
189 where: playlistWhere
190 },
191 {
192 attributes: [ 'url' ],
193 model: VideoModel.unscoped(),
194 where: videoWhere
195 }
196 ]
197 }
198
199 return VideoPlaylistElementModel.findOne(query)
200 }
201
1735c825 202 static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) {
418d092a
C
203 const query = {
204 attributes: [ 'url' ],
205 offset: start,
206 limit: count,
207 order: getSort('position'),
208 where: {
209 videoPlaylistId
df0b219d
C
210 },
211 transaction: t
418d092a
C
212 }
213
214 return VideoPlaylistElementModel
215 .findAndCountAll(query)
216 .then(({ rows, count }) => {
217 return { total: count, data: rows.map(e => e.url) }
218 })
219 }
220
65af03a2
C
221 static loadFirstElementWithVideoThumbnail (videoPlaylistId: number) {
222 const query = {
223 order: getSort('position'),
224 where: {
225 videoPlaylistId
226 },
227 include: [
228 {
229 model: VideoModel.scope(VideoScopeNames.WITH_THUMBNAILS),
230 required: true
231 }
232 ]
233 }
234
235 return VideoPlaylistElementModel
236 .findOne(query)
237 }
238
1735c825
C
239 static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) {
240 const query: AggregateOptions<number> = {
418d092a
C
241 where: {
242 videoPlaylistId
243 },
244 transaction
245 }
246
247 return VideoPlaylistElementModel.max('position', query)
248 .then(position => position ? position + 1 : 1)
249 }
250
251 static reassignPositionOf (
252 videoPlaylistId: number,
253 firstPosition: number,
254 endPosition: number,
255 newPosition: number,
1735c825 256 transaction?: Transaction
418d092a
C
257 ) {
258 const query = {
259 where: {
260 videoPlaylistId,
261 position: {
1735c825
C
262 [Op.gte]: firstPosition,
263 [Op.lte]: endPosition
418d092a
C
264 }
265 },
07b1a18a
C
266 transaction,
267 validate: false // We use a literal to update the position
418d092a
C
268 }
269
270 return VideoPlaylistElementModel.update({ position: Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) }, query)
271 }
272
273 static increasePositionOf (
274 videoPlaylistId: number,
275 fromPosition: number,
276 toPosition?: number,
277 by = 1,
1735c825 278 transaction?: Transaction
418d092a
C
279 ) {
280 const query = {
281 where: {
282 videoPlaylistId,
283 position: {
1735c825 284 [Op.gte]: fromPosition
418d092a
C
285 }
286 },
287 transaction
288 }
289
290 return VideoPlaylistElementModel.increment({ position: by }, query)
291 }
292
bfbd9128
C
293 getType (displayNSFW?: boolean, accountId?: number) {
294 const video = this.Video
295
296 if (!video) return VideoPlaylistElementType.DELETED
297
298 // Owned video, don't filter it
299 if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR
300
301 if (video.privacy === VideoPrivacy.PRIVATE) return VideoPlaylistElementType.PRIVATE
302
303 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
304 if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE
305
306 return VideoPlaylistElementType.REGULAR
307 }
308
309 getVideoElement (displayNSFW?: boolean, accountId?: number) {
310 if (!this.Video) return null
311 if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null
312
313 return this.Video.toFormattedJSON()
314 }
315
316 toFormattedJSON (options: { displayNSFW?: boolean, accountId?: number } = {}): VideoPlaylistElement {
317 return {
318 id: this.id,
319 position: this.position,
320 startTimestamp: this.startTimestamp,
321 stopTimestamp: this.stopTimestamp,
322
323 type: this.getType(options.displayNSFW, options.accountId),
324
325 video: this.getVideoElement(options.displayNSFW, options.accountId)
326 }
327 }
328
418d092a
C
329 toActivityPubObject (): PlaylistElementObject {
330 const base: PlaylistElementObject = {
331 id: this.url,
332 type: 'PlaylistElement',
333
334 url: this.Video.url,
335 position: this.position
336 }
337
338 if (this.startTimestamp) base.startTimestamp = this.startTimestamp
339 if (this.stopTimestamp) base.stopTimestamp = this.stopTimestamp
340
341 return base
342 }
343}