fbe0ee0a740844ae5094b538951f4645ed32be69
[oweals/peertube.git] / server / models / video / video-import.ts
1 import {
2   AfterUpdate,
3   AllowNull,
4   BelongsTo,
5   Column,
6   CreatedAt,
7   DataType,
8   Default,
9   DefaultScope,
10   ForeignKey,
11   Is,
12   Model,
13   Table,
14   UpdatedAt
15 } from 'sequelize-typescript'
16 import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
17 import { getSort, throwIfNotValid } from '../utils'
18 import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
19 import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports'
20 import { VideoImport, VideoImportState } from '../../../shared'
21 import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
22 import { UserModel } from '../account/user'
23 import * as Bluebird from 'bluebird'
24 import { MVideoImportDefault, MVideoImportFormattable } from '@server/typings/models/video/video-import'
25
26 @DefaultScope(() => ({
27   include: [
28     {
29       model: UserModel.unscoped(),
30       required: true
31     },
32     {
33       model: VideoModel.scope([
34         VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
35         VideoModelScopeNames.WITH_TAGS,
36         VideoModelScopeNames.WITH_THUMBNAILS
37       ]),
38       required: false
39     }
40   ]
41 }))
42
43 @Table({
44   tableName: 'videoImport',
45   indexes: [
46     {
47       fields: [ 'videoId' ],
48       unique: true
49     },
50     {
51       fields: [ 'userId' ]
52     }
53   ]
54 })
55 export class VideoImportModel extends Model<VideoImportModel> {
56   @CreatedAt
57   createdAt: Date
58
59   @UpdatedAt
60   updatedAt: Date
61
62   @AllowNull(true)
63   @Default(null)
64   @Is('VideoImportTargetUrl', value => throwIfNotValid(value, isVideoImportTargetUrlValid, 'targetUrl', true))
65   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max))
66   targetUrl: string
67
68   @AllowNull(true)
69   @Default(null)
70   @Is('VideoImportMagnetUri', value => throwIfNotValid(value, isVideoMagnetUriValid, 'magnetUri', true))
71   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max)) // Use the same constraints than URLs
72   magnetUri: string
73
74   @AllowNull(true)
75   @Default(null)
76   @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_NAME.max))
77   torrentName: string
78
79   @AllowNull(false)
80   @Default(null)
81   @Is('VideoImportState', value => throwIfNotValid(value, isVideoImportStateValid, 'state'))
82   @Column
83   state: VideoImportState
84
85   @AllowNull(true)
86   @Default(null)
87   @Column(DataType.TEXT)
88   error: string
89
90   @ForeignKey(() => UserModel)
91   @Column
92   userId: number
93
94   @BelongsTo(() => UserModel, {
95     foreignKey: {
96       allowNull: false
97     },
98     onDelete: 'cascade'
99   })
100   User: UserModel
101
102   @ForeignKey(() => VideoModel)
103   @Column
104   videoId: number
105
106   @BelongsTo(() => VideoModel, {
107     foreignKey: {
108       allowNull: true
109     },
110     onDelete: 'set null'
111   })
112   Video: VideoModel
113
114   @AfterUpdate
115   static deleteVideoIfFailed (instance: VideoImportModel, options) {
116     if (instance.state === VideoImportState.FAILED) {
117       return instance.Video.destroy({ transaction: options.transaction })
118     }
119
120     return undefined
121   }
122
123   static loadAndPopulateVideo (id: number): Bluebird<MVideoImportDefault> {
124     return VideoImportModel.findByPk(id)
125   }
126
127   static listUserVideoImportsForApi (userId: number, start: number, count: number, sort: string) {
128     const query = {
129       distinct: true,
130       include: [
131         {
132           attributes: [ 'id' ],
133           model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query
134           required: true
135         }
136       ],
137       offset: start,
138       limit: count,
139       order: getSort(sort),
140       where: {
141         userId
142       }
143     }
144
145     return VideoImportModel.findAndCountAll<MVideoImportDefault>(query)
146                            .then(({ rows, count }) => {
147                              return {
148                                data: rows,
149                                total: count
150                              }
151                            })
152   }
153
154   getTargetIdentifier () {
155     return this.targetUrl || this.magnetUri || this.torrentName
156   }
157
158   toFormattedJSON (this: MVideoImportFormattable): VideoImport {
159     const videoFormatOptions = {
160       completeDescription: true,
161       additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true }
162     }
163     const video = this.Video
164       ? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { tags: this.Video.Tags.map(t => t.name) })
165       : undefined
166
167     return {
168       id: this.id,
169
170       targetUrl: this.targetUrl,
171       magnetUri: this.magnetUri,
172       torrentName: this.torrentName,
173
174       state: {
175         id: this.state,
176         label: VideoImportModel.getStateLabel(this.state)
177       },
178       error: this.error,
179       updatedAt: this.updatedAt.toISOString(),
180       createdAt: this.createdAt.toISOString(),
181       video
182     }
183   }
184
185   private static getStateLabel (id: number) {
186     return VIDEO_IMPORT_STATES[id] || 'Unknown'
187   }
188 }