1 import { IncludeOptions, Op, WhereOptions } from 'sequelize'
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'
29 const defaultVideoScope = () => {
30 return VideoModel.scope([
31 VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
32 VideoModelScopeNames.WITH_TAGS,
33 VideoModelScopeNames.WITH_THUMBNAILS
37 @DefaultScope(() => ({
40 model: UserModel.unscoped(),
44 model: defaultVideoScope(),
48 model: VideoChannelSyncModel.unscoped(),
55 tableName: 'videoImport',
58 fields: [ 'videoId' ],
66 export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportModel>>> {
75 @Is('VideoImportTargetUrl', value => throwIfNotValid(value, isVideoImportTargetUrlValid, 'targetUrl', true))
76 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max))
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
87 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_NAME.max))
92 @Is('VideoImportState', value => throwIfNotValid(value, isVideoImportStateValid, 'state'))
94 state: VideoImportState
98 @Column(DataType.TEXT)
101 @ForeignKey(() => UserModel)
105 @BelongsTo(() => UserModel, {
113 @ForeignKey(() => VideoModel)
117 @BelongsTo(() => VideoModel, {
125 @ForeignKey(() => VideoChannelSyncModel)
127 videoChannelSyncId: number
129 @BelongsTo(() => VideoChannelSyncModel, {
135 VideoChannelSync: VideoChannelSyncModel
138 static deleteVideoIfFailed (instance: VideoImportModel, options) {
139 if (instance.state === VideoImportState.FAILED) {
140 return afterCommitIfTransaction(options.transaction, () => instance.Video.destroy())
146 static loadAndPopulateVideo (id: number): Promise<MVideoImportDefault> {
147 return VideoImportModel.findByPk(id)
150 static listUserVideoImportsForApi (options: {
158 videoChannelSyncId?: number
160 const { userId, start, count, sort, targetUrl, videoChannelSyncId, search } = options
162 const where: WhereOptions = { userId }
163 const include: IncludeOptions[] = [
165 attributes: [ 'id' ],
166 model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query
170 model: VideoChannelSyncModel.unscoped(),
175 if (targetUrl) where['targetUrl'] = targetUrl
176 if (videoChannelSyncId) where['videoChannelSyncId'] = videoChannelSyncId
180 model: defaultVideoScope(),
182 where: searchAttribute(search, 'name')
186 model: defaultVideoScope(),
196 order: getSort(sort),
201 VideoImportModel.unscoped().count(query),
202 VideoImportModel.findAll<MVideoImportDefault>(query)
203 ]).then(([ total, data ]) => ({ total, data }))
206 static async urlAlreadyImported (channelId: number, targetUrl: string): Promise<boolean> {
207 const element = await VideoImportModel.unscoped().findOne({
211 [Op.in]: [ VideoImportState.PENDING, VideoImportState.PROCESSING, VideoImportState.SUCCESS ]
228 getTargetIdentifier () {
229 return this.targetUrl || this.magnetUri || this.torrentName
232 toFormattedJSON (this: MVideoImportFormattable): VideoImport {
233 const videoFormatOptions = {
234 completeDescription: true,
235 additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true }
237 const video = this.Video
238 ? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { tags: this.Video.Tags.map(t => t.name) })
241 const videoChannelSync = this.VideoChannelSync
242 ? { id: this.VideoChannelSync.id, externalChannelUrl: this.VideoChannelSync.externalChannelUrl }
248 targetUrl: this.targetUrl,
249 magnetUri: this.magnetUri,
250 torrentName: this.torrentName,
254 label: VideoImportModel.getStateLabel(this.state)
257 updatedAt: this.updatedAt.toISOString(),
258 createdAt: this.createdAt.toISOString(),
264 private static getStateLabel (id: number) {
265 return VIDEO_IMPORT_STATES[id] || 'Unknown'