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