]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-import.ts
Bumped to version v5.2.1
[github/Chocobozzz/PeerTube.git] / server / models / video / video-import.ts
1 import { IncludeOptions, Op, WhereOptions } from 'sequelize'
2 import {
3 AfterUpdate,
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 Default,
10 DefaultScope,
11 ForeignKey,
12 Is,
13 Model,
14 Table,
15 UpdatedAt
16 } from 'sequelize-typescript'
17 import { afterCommitIfTransaction } from '@server/helpers/database-utils'
18 import { MVideoImportDefault, MVideoImportFormattable } from '@server/types/models/video/video-import'
19 import { VideoImport, VideoImportState } from '@shared/models'
20 import { AttributesOnly } from '@shared/typescript-utils'
21 import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports'
22 import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
23 import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
24 import { UserModel } from '../user/user'
25 import { getSort, searchAttribute, throwIfNotValid } from '../shared'
26 import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
27 import { VideoChannelSyncModel } from './video-channel-sync'
28
29 const defaultVideoScope = () => {
30 return VideoModel.scope([
31 VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
32 VideoModelScopeNames.WITH_TAGS,
33 VideoModelScopeNames.WITH_THUMBNAILS
34 ])
35 }
36
37 @DefaultScope(() => ({
38 include: [
39 {
40 model: UserModel.unscoped(),
41 required: true
42 },
43 {
44 model: defaultVideoScope(),
45 required: false
46 },
47 {
48 model: VideoChannelSyncModel.unscoped(),
49 required: false
50 }
51 ]
52 }))
53
54 @Table({
55 tableName: 'videoImport',
56 indexes: [
57 {
58 fields: [ 'videoId' ],
59 unique: true
60 },
61 {
62 fields: [ 'userId' ]
63 }
64 ]
65 })
66 export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportModel>>> {
67 @CreatedAt
68 createdAt: Date
69
70 @UpdatedAt
71 updatedAt: Date
72
73 @AllowNull(true)
74 @Default(null)
75 @Is('VideoImportTargetUrl', value => throwIfNotValid(value, isVideoImportTargetUrlValid, 'targetUrl', true))
76 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max))
77 targetUrl: string
78
79 @AllowNull(true)
80 @Default(null)
81 @Is('VideoImportMagnetUri', value => throwIfNotValid(value, isVideoMagnetUriValid, 'magnetUri', true))
82 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max)) // Use the same constraints than URLs
83 magnetUri: string
84
85 @AllowNull(true)
86 @Default(null)
87 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_NAME.max))
88 torrentName: string
89
90 @AllowNull(false)
91 @Default(null)
92 @Is('VideoImportState', value => throwIfNotValid(value, isVideoImportStateValid, 'state'))
93 @Column
94 state: VideoImportState
95
96 @AllowNull(true)
97 @Default(null)
98 @Column(DataType.TEXT)
99 error: string
100
101 @ForeignKey(() => UserModel)
102 @Column
103 userId: number
104
105 @BelongsTo(() => UserModel, {
106 foreignKey: {
107 allowNull: false
108 },
109 onDelete: 'cascade'
110 })
111 User: UserModel
112
113 @ForeignKey(() => VideoModel)
114 @Column
115 videoId: number
116
117 @BelongsTo(() => VideoModel, {
118 foreignKey: {
119 allowNull: true
120 },
121 onDelete: 'set null'
122 })
123 Video: VideoModel
124
125 @ForeignKey(() => VideoChannelSyncModel)
126 @Column
127 videoChannelSyncId: number
128
129 @BelongsTo(() => VideoChannelSyncModel, {
130 foreignKey: {
131 allowNull: true
132 },
133 onDelete: 'set null'
134 })
135 VideoChannelSync: VideoChannelSyncModel
136
137 @AfterUpdate
138 static deleteVideoIfFailed (instance: VideoImportModel, options) {
139 if (instance.state === VideoImportState.FAILED) {
140 return afterCommitIfTransaction(options.transaction, () => instance.Video.destroy())
141 }
142
143 return undefined
144 }
145
146 static loadAndPopulateVideo (id: number): Promise<MVideoImportDefault> {
147 return VideoImportModel.findByPk(id)
148 }
149
150 static listUserVideoImportsForApi (options: {
151 userId: number
152 start: number
153 count: number
154 sort: string
155
156 search?: string
157 targetUrl?: string
158 videoChannelSyncId?: number
159 }) {
160 const { userId, start, count, sort, targetUrl, videoChannelSyncId, search } = options
161
162 const where: WhereOptions = { userId }
163 const include: IncludeOptions[] = [
164 {
165 attributes: [ 'id' ],
166 model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query
167 required: true
168 },
169 {
170 model: VideoChannelSyncModel.unscoped(),
171 required: false
172 }
173 ]
174
175 if (targetUrl) where['targetUrl'] = targetUrl
176 if (videoChannelSyncId) where['videoChannelSyncId'] = videoChannelSyncId
177
178 if (search) {
179 include.push({
180 model: defaultVideoScope(),
181 required: true,
182 where: searchAttribute(search, 'name')
183 })
184 } else {
185 include.push({
186 model: defaultVideoScope(),
187 required: false
188 })
189 }
190
191 const query = {
192 distinct: true,
193 include,
194 offset: start,
195 limit: count,
196 order: getSort(sort),
197 where
198 }
199
200 return Promise.all([
201 VideoImportModel.unscoped().count(query),
202 VideoImportModel.findAll<MVideoImportDefault>(query)
203 ]).then(([ total, data ]) => ({ total, data }))
204 }
205
206 static async urlAlreadyImported (channelId: number, targetUrl: string): Promise<boolean> {
207 const element = await VideoImportModel.unscoped().findOne({
208 where: {
209 targetUrl,
210 state: {
211 [Op.in]: [ VideoImportState.PENDING, VideoImportState.PROCESSING, VideoImportState.SUCCESS ]
212 }
213 },
214 include: [
215 {
216 model: VideoModel,
217 required: true,
218 where: {
219 channelId
220 }
221 }
222 ]
223 })
224
225 return !!element
226 }
227
228 getTargetIdentifier () {
229 return this.targetUrl || this.magnetUri || this.torrentName
230 }
231
232 toFormattedJSON (this: MVideoImportFormattable): VideoImport {
233 const videoFormatOptions = {
234 completeDescription: true,
235 additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true }
236 }
237 const video = this.Video
238 ? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { tags: this.Video.Tags.map(t => t.name) })
239 : undefined
240
241 const videoChannelSync = this.VideoChannelSync
242 ? { id: this.VideoChannelSync.id, externalChannelUrl: this.VideoChannelSync.externalChannelUrl }
243 : undefined
244
245 return {
246 id: this.id,
247
248 targetUrl: this.targetUrl,
249 magnetUri: this.magnetUri,
250 torrentName: this.torrentName,
251
252 state: {
253 id: this.state,
254 label: VideoImportModel.getStateLabel(this.state)
255 },
256 error: this.error,
257 updatedAt: this.updatedAt.toISOString(),
258 createdAt: this.createdAt.toISOString(),
259 video,
260 videoChannelSync
261 }
262 }
263
264 private static getStateLabel (id: number) {
265 return VIDEO_IMPORT_STATES[id] || 'Unknown'
266 }
267 }