]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-import.ts
Add ability to list imports of a channel sync
[github/Chocobozzz/PeerTube.git] / server / models / video / video-import.ts
CommitLineData
a3b472a1 1import { IncludeOptions, Op, WhereOptions } from 'sequelize'
fbad87b0 2import {
ed31c059 3 AfterUpdate,
fbad87b0
C
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'
7d9ba5c0 17import { afterCommitIfTransaction } from '@server/helpers/database-utils'
b49f22d8 18import { MVideoImportDefault, MVideoImportFormattable } from '@server/types/models/video/video-import'
d17c7b4e 19import { VideoImport, VideoImportState } from '@shared/models'
6b5f72be 20import { AttributesOnly } from '@shared/typescript-utils'
b49f22d8 21import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports'
ce33919c 22import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
b49f22d8 23import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
7d9ba5c0 24import { UserModel } from '../user/user'
a3b472a1 25import { getSort, searchAttribute, throwIfNotValid } from '../utils'
b49f22d8 26import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
a3b472a1
C
27import { VideoChannelSyncModel } from './video-channel-sync'
28
29const defaultVideoScope = () => {
30 return VideoModel.scope([
31 VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
32 VideoModelScopeNames.WITH_TAGS,
33 VideoModelScopeNames.WITH_THUMBNAILS
34 ])
35}
fbad87b0 36
3acc5084 37@DefaultScope(() => ({
fbad87b0
C
38 include: [
39 {
3acc5084 40 model: UserModel.unscoped(),
a84b8fa5
C
41 required: true
42 },
43 {
a3b472a1
C
44 model: defaultVideoScope(),
45 required: false
46 },
47 {
48 model: VideoChannelSyncModel.unscoped(),
a84b8fa5 49 required: false
fbad87b0
C
50 }
51 ]
3acc5084 52}))
fbad87b0
C
53
54@Table({
55 tableName: 'videoImport',
56 indexes: [
57 {
58 fields: [ 'videoId' ],
59 unique: true
a84b8fa5
C
60 },
61 {
62 fields: [ 'userId' ]
fbad87b0
C
63 }
64 ]
65})
16c016e8 66export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportModel>>> {
fbad87b0
C
67 @CreatedAt
68 createdAt: Date
69
70 @UpdatedAt
71 updatedAt: Date
72
ce33919c
C
73 @AllowNull(true)
74 @Default(null)
1735c825 75 @Is('VideoImportTargetUrl', value => throwIfNotValid(value, isVideoImportTargetUrlValid, 'targetUrl', true))
fbad87b0
C
76 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max))
77 targetUrl: string
78
ce33919c
C
79 @AllowNull(true)
80 @Default(null)
1735c825 81 @Is('VideoImportMagnetUri', value => throwIfNotValid(value, isVideoMagnetUriValid, 'magnetUri', true))
ce33919c
C
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
fbad87b0
C
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
a84b8fa5
C
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
fbad87b0
C
113 @ForeignKey(() => VideoModel)
114 @Column
115 videoId: number
116
117 @BelongsTo(() => VideoModel, {
118 foreignKey: {
ed31c059 119 allowNull: true
fbad87b0 120 },
ed31c059 121 onDelete: 'set null'
fbad87b0
C
122 })
123 Video: VideoModel
124
a3b472a1
C
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
ed31c059
C
137 @AfterUpdate
138 static deleteVideoIfFailed (instance: VideoImportModel, options) {
139 if (instance.state === VideoImportState.FAILED) {
44d1f7f2 140 return afterCommitIfTransaction(options.transaction, () => instance.Video.destroy())
ed31c059
C
141 }
142
143 return undefined
144 }
145
b49f22d8 146 static loadAndPopulateVideo (id: number): Promise<MVideoImportDefault> {
9b39106d 147 return VideoImportModel.findByPk(id)
fbad87b0
C
148 }
149
d511df28
C
150 static listUserVideoImportsForApi (options: {
151 userId: number
152 start: number
153 count: number
154 sort: string
155
a3b472a1 156 search?: string
d511df28 157 targetUrl?: string
a3b472a1 158 videoChannelSyncId?: number
d511df28 159 }) {
a3b472a1 160 const { userId, start, count, sort, targetUrl, videoChannelSyncId, search } = options
d511df28
C
161
162 const where: WhereOptions = { userId }
a3b472a1
C
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 ]
d511df28
C
174
175 if (targetUrl) where['targetUrl'] = targetUrl
a3b472a1
C
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 }
d511df28 190
ed31c059 191 const query = {
590fb506 192 distinct: true,
a3b472a1 193 include,
a84b8fa5
C
194 offset: start,
195 limit: count,
196 order: getSort(sort),
d511df28 197 where
ed31c059
C
198 }
199
d0800f76 200 return Promise.all([
201 VideoImportModel.unscoped().count(query),
202 VideoImportModel.findAll<MVideoImportDefault>(query)
203 ]).then(([ total, data ]) => ({ total, data }))
ed31c059
C
204 }
205
2a491182
F
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
dc133480
C
228 getTargetIdentifier () {
229 return this.targetUrl || this.magnetUri || this.torrentName
230 }
231
1ca9f7c3 232 toFormattedJSON (this: MVideoImportFormattable): VideoImport {
fbad87b0 233 const videoFormatOptions = {
c39e86b8 234 completeDescription: true,
fbad87b0
C
235 additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true }
236 }
ed31c059 237 const video = this.Video
c39e86b8 238 ? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { tags: this.Video.Tags.map(t => t.name) })
ed31c059 239 : undefined
fbad87b0 240
a3b472a1
C
241 const videoChannelSync = this.VideoChannelSync
242 ? { id: this.VideoChannelSync.id, externalChannelUrl: this.VideoChannelSync.externalChannelUrl }
243 : undefined
244
fbad87b0 245 return {
d7f83948 246 id: this.id,
990b6a0b 247
fbad87b0 248 targetUrl: this.targetUrl,
990b6a0b
C
249 magnetUri: this.magnetUri,
250 torrentName: this.torrentName,
251
ed31c059
C
252 state: {
253 id: this.state,
254 label: VideoImportModel.getStateLabel(this.state)
255 },
d7f83948 256 error: this.error,
ed31c059
C
257 updatedAt: this.updatedAt.toISOString(),
258 createdAt: this.createdAt.toISOString(),
a3b472a1
C
259 video,
260 videoChannelSync
fbad87b0
C
261 }
262 }
268eebed 263
ed31c059
C
264 private static getStateLabel (id: number) {
265 return VIDEO_IMPORT_STATES[id] || 'Unknown'
266 }
fbad87b0 267}