Cleanup invalid rates/comments/shares
[oweals/peertube.git] / server / models / video / video-playlist-element.ts
1 import {
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'
16 import { VideoModel } from './video'
17 import { VideoPlaylistModel } from './video-playlist'
18 import * as Sequelize from 'sequelize'
19 import { getSort, throwIfNotValid } from '../utils'
20 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
21 import { CONSTRAINTS_FIELDS } from '../../initializers'
22 import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
23 import * as validator from 'validator'
24
25 @Table({
26   tableName: 'videoPlaylistElement',
27   indexes: [
28     {
29       fields: [ 'videoPlaylistId' ]
30     },
31     {
32       fields: [ 'videoId' ]
33     },
34     {
35       fields: [ 'videoPlaylistId', 'videoId' ],
36       unique: true
37     },
38     {
39       fields: [ 'url' ],
40       unique: true
41     }
42   ]
43 })
44 export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> {
45   @CreatedAt
46   createdAt: Date
47
48   @UpdatedAt
49   updatedAt: Date
50
51   @AllowNull(false)
52   @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
53   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
54   url: string
55
56   @AllowNull(false)
57   @Default(1)
58   @IsInt
59   @Min(1)
60   @Column
61   position: number
62
63   @AllowNull(true)
64   @IsInt
65   @Min(0)
66   @Column
67   startTimestamp: number
68
69   @AllowNull(true)
70   @IsInt
71   @Min(0)
72   @Column
73   stopTimestamp: number
74
75   @ForeignKey(() => VideoPlaylistModel)
76   @Column
77   videoPlaylistId: number
78
79   @BelongsTo(() => VideoPlaylistModel, {
80     foreignKey: {
81       allowNull: false
82     },
83     onDelete: 'CASCADE'
84   })
85   VideoPlaylist: VideoPlaylistModel
86
87   @ForeignKey(() => VideoModel)
88   @Column
89   videoId: number
90
91   @BelongsTo(() => VideoModel, {
92     foreignKey: {
93       allowNull: false
94     },
95     onDelete: 'CASCADE'
96   })
97   Video: VideoModel
98
99   static deleteAllOf (videoPlaylistId: number, transaction?: Sequelize.Transaction) {
100     const query = {
101       where: {
102         videoPlaylistId
103       },
104       transaction
105     }
106
107     return VideoPlaylistElementModel.destroy(query)
108   }
109
110   static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) {
111     const query = {
112       where: {
113         videoPlaylistId,
114         videoId
115       }
116     }
117
118     return VideoPlaylistElementModel.findOne(query)
119   }
120
121   static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) {
122     const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
123     const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId }
124
125     const query = {
126       include: [
127         {
128           attributes: [ 'privacy' ],
129           model: VideoPlaylistModel.unscoped(),
130           where: playlistWhere
131         },
132         {
133           attributes: [ 'url' ],
134           model: VideoModel.unscoped(),
135           where: videoWhere
136         }
137       ]
138     }
139
140     return VideoPlaylistElementModel.findOne(query)
141   }
142
143   static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Sequelize.Transaction) {
144     const query = {
145       attributes: [ 'url' ],
146       offset: start,
147       limit: count,
148       order: getSort('position'),
149       where: {
150         videoPlaylistId
151       },
152       transaction: t
153     }
154
155     return VideoPlaylistElementModel
156       .findAndCountAll(query)
157       .then(({ rows, count }) => {
158         return { total: count, data: rows.map(e => e.url) }
159       })
160   }
161
162   static getNextPositionOf (videoPlaylistId: number, transaction?: Sequelize.Transaction) {
163     const query = {
164       where: {
165         videoPlaylistId
166       },
167       transaction
168     }
169
170     return VideoPlaylistElementModel.max('position', query)
171       .then(position => position ? position + 1 : 1)
172   }
173
174   static reassignPositionOf (
175     videoPlaylistId: number,
176     firstPosition: number,
177     endPosition: number,
178     newPosition: number,
179     transaction?: Sequelize.Transaction
180   ) {
181     const query = {
182       where: {
183         videoPlaylistId,
184         position: {
185           [Sequelize.Op.gte]: firstPosition,
186           [Sequelize.Op.lte]: endPosition
187         }
188       },
189       transaction,
190       validate: false // We use a literal to update the position
191     }
192
193     return VideoPlaylistElementModel.update({ position: Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) }, query)
194   }
195
196   static increasePositionOf (
197     videoPlaylistId: number,
198     fromPosition: number,
199     toPosition?: number,
200     by = 1,
201     transaction?: Sequelize.Transaction
202   ) {
203     const query = {
204       where: {
205         videoPlaylistId,
206         position: {
207           [Sequelize.Op.gte]: fromPosition
208         }
209       },
210       transaction
211     }
212
213     return VideoPlaylistElementModel.increment({ position: by }, query)
214   }
215
216   toActivityPubObject (): PlaylistElementObject {
217     const base: PlaylistElementObject = {
218       id: this.url,
219       type: 'PlaylistElement',
220
221       url: this.Video.url,
222       position: this.position
223     }
224
225     if (this.startTimestamp) base.startTimestamp = this.startTimestamp
226     if (this.stopTimestamp) base.stopTimestamp = this.stopTimestamp
227
228     return base
229   }
230 }