diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/account/account.ts | 4 | ||||
-rw-r--r-- | server/models/account/user.ts | 9 | ||||
-rw-r--r-- | server/models/activitypub/actor.ts | 35 | ||||
-rw-r--r-- | server/models/oauth/oauth-token.ts | 47 | ||||
-rw-r--r-- | server/models/redundancy/video-redundancy.ts | 189 | ||||
-rw-r--r-- | server/models/video/tag.ts | 5 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 296 | ||||
-rw-r--r-- | server/models/video/video.ts | 555 |
8 files changed, 627 insertions, 513 deletions
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 6bbfc6f4e..580d920ce 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -134,8 +134,8 @@ export class AccountModel extends Model<AccountModel> { | |||
134 | return undefined | 134 | return undefined |
135 | } | 135 | } |
136 | 136 | ||
137 | static load (id: number) { | 137 | static load (id: number, transaction?: Sequelize.Transaction) { |
138 | return AccountModel.findById(id) | 138 | return AccountModel.findById(id, { transaction }) |
139 | } | 139 | } |
140 | 140 | ||
141 | static loadByUUID (uuid: string) { | 141 | static loadByUUID (uuid: string) { |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 680b1d52d..e56b0bf40 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { | 2 | import { |
3 | AfterDelete, | ||
4 | AfterUpdate, | ||
3 | AllowNull, | 5 | AllowNull, |
4 | BeforeCreate, | 6 | BeforeCreate, |
5 | BeforeUpdate, | 7 | BeforeUpdate, |
@@ -39,6 +41,7 @@ import { AccountModel } from './account' | |||
39 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' | 41 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' |
40 | import { values } from 'lodash' | 42 | import { values } from 'lodash' |
41 | import { NSFW_POLICY_TYPES } from '../../initializers' | 43 | import { NSFW_POLICY_TYPES } from '../../initializers' |
44 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
42 | 45 | ||
43 | enum ScopeNames { | 46 | enum ScopeNames { |
44 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' | 47 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' |
@@ -168,6 +171,12 @@ export class UserModel extends Model<UserModel> { | |||
168 | } | 171 | } |
169 | } | 172 | } |
170 | 173 | ||
174 | @AfterUpdate | ||
175 | @AfterDelete | ||
176 | static removeTokenCache (instance: UserModel) { | ||
177 | return clearCacheByUserId(instance.id) | ||
178 | } | ||
179 | |||
171 | static countTotal () { | 180 | static countTotal () { |
172 | return this.count() | 181 | return this.count() |
173 | } | 182 | } |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index ef8dd9f7c..f8bb59323 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -266,6 +266,18 @@ export class ActorModel extends Model<ActorModel> { | |||
266 | return ActorModel.unscoped().findById(id) | 266 | return ActorModel.unscoped().findById(id) |
267 | } | 267 | } |
268 | 268 | ||
269 | static isActorUrlExist (url: string) { | ||
270 | const query = { | ||
271 | raw: true, | ||
272 | where: { | ||
273 | url | ||
274 | } | ||
275 | } | ||
276 | |||
277 | return ActorModel.unscoped().findOne(query) | ||
278 | .then(a => !!a) | ||
279 | } | ||
280 | |||
269 | static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { | 281 | static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { |
270 | const query = { | 282 | const query = { |
271 | where: { | 283 | where: { |
@@ -315,6 +327,29 @@ export class ActorModel extends Model<ActorModel> { | |||
315 | where: { | 327 | where: { |
316 | url | 328 | url |
317 | }, | 329 | }, |
330 | transaction, | ||
331 | include: [ | ||
332 | { | ||
333 | attributes: [ 'id' ], | ||
334 | model: AccountModel.unscoped(), | ||
335 | required: false | ||
336 | }, | ||
337 | { | ||
338 | attributes: [ 'id' ], | ||
339 | model: VideoChannelModel.unscoped(), | ||
340 | required: false | ||
341 | } | ||
342 | ] | ||
343 | } | ||
344 | |||
345 | return ActorModel.unscoped().findOne(query) | ||
346 | } | ||
347 | |||
348 | static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction) { | ||
349 | const query = { | ||
350 | where: { | ||
351 | url | ||
352 | }, | ||
318 | transaction | 353 | transaction |
319 | } | 354 | } |
320 | 355 | ||
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index 4c53848dc..ef9592c04 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts | |||
@@ -1,9 +1,23 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { |
2 | AfterDelete, | ||
3 | AfterUpdate, | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | ForeignKey, | ||
9 | Model, | ||
10 | Scopes, | ||
11 | Table, | ||
12 | UpdatedAt | ||
13 | } from 'sequelize-typescript' | ||
2 | import { logger } from '../../helpers/logger' | 14 | import { logger } from '../../helpers/logger' |
3 | import { AccountModel } from '../account/account' | ||
4 | import { UserModel } from '../account/user' | 15 | import { UserModel } from '../account/user' |
5 | import { OAuthClientModel } from './oauth-client' | 16 | import { OAuthClientModel } from './oauth-client' |
6 | import { Transaction } from 'sequelize' | 17 | import { Transaction } from 'sequelize' |
18 | import { AccountModel } from '../account/account' | ||
19 | import { ActorModel } from '../activitypub/actor' | ||
20 | import { clearCacheByToken } from '../../lib/oauth-model' | ||
7 | 21 | ||
8 | export type OAuthTokenInfo = { | 22 | export type OAuthTokenInfo = { |
9 | refreshToken: string | 23 | refreshToken: string |
@@ -17,18 +31,27 @@ export type OAuthTokenInfo = { | |||
17 | } | 31 | } |
18 | 32 | ||
19 | enum ScopeNames { | 33 | enum ScopeNames { |
20 | WITH_ACCOUNT = 'WITH_ACCOUNT' | 34 | WITH_USER = 'WITH_USER' |
21 | } | 35 | } |
22 | 36 | ||
23 | @Scopes({ | 37 | @Scopes({ |
24 | [ScopeNames.WITH_ACCOUNT]: { | 38 | [ScopeNames.WITH_USER]: { |
25 | include: [ | 39 | include: [ |
26 | { | 40 | { |
27 | model: () => UserModel, | 41 | model: () => UserModel.unscoped(), |
42 | required: true, | ||
28 | include: [ | 43 | include: [ |
29 | { | 44 | { |
30 | model: () => AccountModel, | 45 | attributes: [ 'id' ], |
31 | required: true | 46 | model: () => AccountModel.unscoped(), |
47 | required: true, | ||
48 | include: [ | ||
49 | { | ||
50 | attributes: [ 'id' ], | ||
51 | model: () => ActorModel.unscoped(), | ||
52 | required: true | ||
53 | } | ||
54 | ] | ||
32 | } | 55 | } |
33 | ] | 56 | ] |
34 | } | 57 | } |
@@ -102,6 +125,12 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> { | |||
102 | }) | 125 | }) |
103 | OAuthClients: OAuthClientModel[] | 126 | OAuthClients: OAuthClientModel[] |
104 | 127 | ||
128 | @AfterUpdate | ||
129 | @AfterDelete | ||
130 | static removeTokenCache (token: OAuthTokenModel) { | ||
131 | return clearCacheByToken(token.accessToken) | ||
132 | } | ||
133 | |||
105 | static getByRefreshTokenAndPopulateClient (refreshToken: string) { | 134 | static getByRefreshTokenAndPopulateClient (refreshToken: string) { |
106 | const query = { | 135 | const query = { |
107 | where: { | 136 | where: { |
@@ -138,7 +167,7 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> { | |||
138 | } | 167 | } |
139 | } | 168 | } |
140 | 169 | ||
141 | return OAuthTokenModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query).then(token => { | 170 | return OAuthTokenModel.scope(ScopeNames.WITH_USER).findOne(query).then(token => { |
142 | if (token) token['user'] = token.User | 171 | if (token) token['user'] = token.User |
143 | 172 | ||
144 | return token | 173 | return token |
@@ -152,7 +181,7 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> { | |||
152 | } | 181 | } |
153 | } | 182 | } |
154 | 183 | ||
155 | return OAuthTokenModel.scope(ScopeNames.WITH_ACCOUNT) | 184 | return OAuthTokenModel.scope(ScopeNames.WITH_USER) |
156 | .findOne(query) | 185 | .findOne(query) |
157 | .then(token => { | 186 | .then(token => { |
158 | if (token) { | 187 | if (token) { |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 48ec77206..fb07287a8 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -14,11 +14,10 @@ import { | |||
14 | UpdatedAt | 14 | UpdatedAt |
15 | } from 'sequelize-typescript' | 15 | } from 'sequelize-typescript' |
16 | import { ActorModel } from '../activitypub/actor' | 16 | import { ActorModel } from '../activitypub/actor' |
17 | import { throwIfNotValid } from '../utils' | 17 | import { getVideoSort, throwIfNotValid } from '../utils' |
18 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 18 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
19 | import { CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' | 19 | import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' |
20 | import { VideoFileModel } from '../video/video-file' | 20 | import { VideoFileModel } from '../video/video-file' |
21 | import { isDateValid } from '../../helpers/custom-validators/misc' | ||
22 | import { getServerActor } from '../../helpers/utils' | 21 | import { getServerActor } from '../../helpers/utils' |
23 | import { VideoModel } from '../video/video' | 22 | import { VideoModel } from '../video/video' |
24 | import { VideoRedundancyStrategy } from '../../../shared/models/redundancy' | 23 | import { VideoRedundancyStrategy } from '../../../shared/models/redundancy' |
@@ -28,6 +27,7 @@ import { VideoChannelModel } from '../video/video-channel' | |||
28 | import { ServerModel } from '../server/server' | 27 | import { ServerModel } from '../server/server' |
29 | import { sample } from 'lodash' | 28 | import { sample } from 'lodash' |
30 | import { isTestInstance } from '../../helpers/core-utils' | 29 | import { isTestInstance } from '../../helpers/core-utils' |
30 | import * as Bluebird from 'bluebird' | ||
31 | 31 | ||
32 | export enum ScopeNames { | 32 | export enum ScopeNames { |
33 | WITH_VIDEO = 'WITH_VIDEO' | 33 | WITH_VIDEO = 'WITH_VIDEO' |
@@ -145,65 +145,90 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
145 | return VideoRedundancyModel.findOne(query) | 145 | return VideoRedundancyModel.findOne(query) |
146 | } | 146 | } |
147 | 147 | ||
148 | static async getVideoSample (p: Bluebird<VideoModel[]>) { | ||
149 | const rows = await p | ||
150 | const ids = rows.map(r => r.id) | ||
151 | const id = sample(ids) | ||
152 | |||
153 | return VideoModel.loadWithFile(id, undefined, !isTestInstance()) | ||
154 | } | ||
155 | |||
148 | static async findMostViewToDuplicate (randomizedFactor: number) { | 156 | static async findMostViewToDuplicate (randomizedFactor: number) { |
149 | // On VideoModel! | 157 | // On VideoModel! |
150 | const query = { | 158 | const query = { |
159 | attributes: [ 'id', 'views' ], | ||
151 | logging: !isTestInstance(), | 160 | logging: !isTestInstance(), |
152 | limit: randomizedFactor, | 161 | limit: randomizedFactor, |
153 | order: [ [ 'views', 'DESC' ] ], | 162 | order: getVideoSort('-views'), |
154 | include: [ | 163 | include: [ |
155 | { | 164 | await VideoRedundancyModel.buildVideoFileForDuplication(), |
156 | model: VideoFileModel.unscoped(), | 165 | VideoRedundancyModel.buildServerRedundancyInclude() |
157 | required: true, | ||
158 | where: { | ||
159 | id: { | ||
160 | [ Sequelize.Op.notIn ]: await VideoRedundancyModel.buildExcludeIn() | ||
161 | } | ||
162 | } | ||
163 | }, | ||
164 | { | ||
165 | attributes: [], | ||
166 | model: VideoChannelModel.unscoped(), | ||
167 | required: true, | ||
168 | include: [ | ||
169 | { | ||
170 | attributes: [], | ||
171 | model: ActorModel.unscoped(), | ||
172 | required: true, | ||
173 | include: [ | ||
174 | { | ||
175 | attributes: [], | ||
176 | model: ServerModel.unscoped(), | ||
177 | required: true, | ||
178 | where: { | ||
179 | redundancyAllowed: true | ||
180 | } | ||
181 | } | ||
182 | ] | ||
183 | } | ||
184 | ] | ||
185 | } | ||
186 | ] | 166 | ] |
187 | } | 167 | } |
188 | 168 | ||
189 | const rows = await VideoModel.unscoped().findAll(query) | 169 | return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query)) |
170 | } | ||
190 | 171 | ||
191 | return sample(rows) | 172 | static async findTrendingToDuplicate (randomizedFactor: number) { |
173 | // On VideoModel! | ||
174 | const query = { | ||
175 | attributes: [ 'id', 'views' ], | ||
176 | subQuery: false, | ||
177 | logging: !isTestInstance(), | ||
178 | group: 'VideoModel.id', | ||
179 | limit: randomizedFactor, | ||
180 | order: getVideoSort('-trending'), | ||
181 | include: [ | ||
182 | await VideoRedundancyModel.buildVideoFileForDuplication(), | ||
183 | VideoRedundancyModel.buildServerRedundancyInclude(), | ||
184 | |||
185 | VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS) | ||
186 | ] | ||
187 | } | ||
188 | |||
189 | return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query)) | ||
192 | } | 190 | } |
193 | 191 | ||
194 | static async getVideoFiles (strategy: VideoRedundancyStrategy) { | 192 | static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) { |
193 | // On VideoModel! | ||
194 | const query = { | ||
195 | attributes: [ 'id', 'publishedAt' ], | ||
196 | logging: !isTestInstance(), | ||
197 | limit: randomizedFactor, | ||
198 | order: getVideoSort('-publishedAt'), | ||
199 | where: { | ||
200 | views: { | ||
201 | [ Sequelize.Op.gte ]: minViews | ||
202 | } | ||
203 | }, | ||
204 | include: [ | ||
205 | await VideoRedundancyModel.buildVideoFileForDuplication(), | ||
206 | VideoRedundancyModel.buildServerRedundancyInclude() | ||
207 | ] | ||
208 | } | ||
209 | |||
210 | return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query)) | ||
211 | } | ||
212 | |||
213 | static async getTotalDuplicated (strategy: VideoRedundancyStrategy) { | ||
195 | const actor = await getServerActor() | 214 | const actor = await getServerActor() |
196 | 215 | ||
197 | const queryVideoFiles = { | 216 | const options = { |
198 | logging: !isTestInstance(), | 217 | logging: !isTestInstance(), |
199 | where: { | 218 | include: [ |
200 | actorId: actor.id, | 219 | { |
201 | strategy | 220 | attributes: [], |
202 | } | 221 | model: VideoRedundancyModel, |
222 | required: true, | ||
223 | where: { | ||
224 | actorId: actor.id, | ||
225 | strategy | ||
226 | } | ||
227 | } | ||
228 | ] | ||
203 | } | 229 | } |
204 | 230 | ||
205 | return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO) | 231 | return VideoFileModel.sum('size', options) |
206 | .findAll(queryVideoFiles) | ||
207 | } | 232 | } |
208 | 233 | ||
209 | static listAllExpired () { | 234 | static listAllExpired () { |
@@ -211,7 +236,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
211 | logging: !isTestInstance(), | 236 | logging: !isTestInstance(), |
212 | where: { | 237 | where: { |
213 | expiresOn: { | 238 | expiresOn: { |
214 | [Sequelize.Op.lt]: new Date() | 239 | [ Sequelize.Op.lt ]: new Date() |
215 | } | 240 | } |
216 | } | 241 | } |
217 | } | 242 | } |
@@ -220,6 +245,37 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
220 | .findAll(query) | 245 | .findAll(query) |
221 | } | 246 | } |
222 | 247 | ||
248 | static async getStats (strategy: VideoRedundancyStrategy) { | ||
249 | const actor = await getServerActor() | ||
250 | |||
251 | const query = { | ||
252 | raw: true, | ||
253 | attributes: [ | ||
254 | [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ], | ||
255 | [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', 'videoId')), 'totalVideos' ], | ||
256 | [ Sequelize.fn('COUNT', 'videoFileId'), 'totalVideoFiles' ] | ||
257 | ], | ||
258 | where: { | ||
259 | strategy, | ||
260 | actorId: actor.id | ||
261 | }, | ||
262 | include: [ | ||
263 | { | ||
264 | attributes: [], | ||
265 | model: VideoFileModel, | ||
266 | required: true | ||
267 | } | ||
268 | ] | ||
269 | } | ||
270 | |||
271 | return VideoRedundancyModel.find(query as any) // FIXME: typings | ||
272 | .then((r: any) => ({ | ||
273 | totalUsed: parseInt(r.totalUsed.toString(), 10), | ||
274 | totalVideos: r.totalVideos, | ||
275 | totalVideoFiles: r.totalVideoFiles | ||
276 | })) | ||
277 | } | ||
278 | |||
223 | toActivityPubObject (): CacheFileObject { | 279 | toActivityPubObject (): CacheFileObject { |
224 | return { | 280 | return { |
225 | id: this.url, | 281 | id: this.url, |
@@ -237,13 +293,50 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
237 | } | 293 | } |
238 | } | 294 | } |
239 | 295 | ||
240 | private static async buildExcludeIn () { | 296 | // Don't include video files we already duplicated |
297 | private static async buildVideoFileForDuplication () { | ||
241 | const actor = await getServerActor() | 298 | const actor = await getServerActor() |
242 | 299 | ||
243 | return Sequelize.literal( | 300 | const notIn = Sequelize.literal( |
244 | '(' + | 301 | '(' + |
245 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "expiresOn" >= NOW()` + | 302 | `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "expiresOn" >= NOW()` + |
246 | ')' | 303 | ')' |
247 | ) | 304 | ) |
305 | |||
306 | return { | ||
307 | attributes: [], | ||
308 | model: VideoFileModel.unscoped(), | ||
309 | required: true, | ||
310 | where: { | ||
311 | id: { | ||
312 | [ Sequelize.Op.notIn ]: notIn | ||
313 | } | ||
314 | } | ||
315 | } | ||
316 | } | ||
317 | |||
318 | private static buildServerRedundancyInclude () { | ||
319 | return { | ||
320 | attributes: [], | ||
321 | model: VideoChannelModel.unscoped(), | ||
322 | required: true, | ||
323 | include: [ | ||
324 | { | ||
325 | attributes: [], | ||
326 | model: ActorModel.unscoped(), | ||
327 | required: true, | ||
328 | include: [ | ||
329 | { | ||
330 | attributes: [], | ||
331 | model: ServerModel.unscoped(), | ||
332 | required: true, | ||
333 | where: { | ||
334 | redundancyAllowed: true | ||
335 | } | ||
336 | } | ||
337 | ] | ||
338 | } | ||
339 | ] | ||
340 | } | ||
248 | } | 341 | } |
249 | } | 342 | } |
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts index e39a418cd..b39621eaf 100644 --- a/server/models/video/tag.ts +++ b/server/models/video/tag.ts | |||
@@ -48,11 +48,10 @@ export class TagModel extends Model<TagModel> { | |||
48 | }, | 48 | }, |
49 | defaults: { | 49 | defaults: { |
50 | name: tag | 50 | name: tag |
51 | } | 51 | }, |
52 | transaction | ||
52 | } | 53 | } |
53 | 54 | ||
54 | if (transaction) query['transaction'] = transaction | ||
55 | |||
56 | const promise = TagModel.findOrCreate(query) | 55 | const promise = TagModel.findOrCreate(query) |
57 | .then(([ tagInstance ]) => tagInstance) | 56 | .then(([ tagInstance ]) => tagInstance) |
58 | tasks.push(promise) | 57 | tasks.push(promise) |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts new file mode 100644 index 000000000..a9a58624d --- /dev/null +++ b/server/models/video/video-format-utils.ts | |||
@@ -0,0 +1,296 @@ | |||
1 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | ||
2 | import { VideoModel } from './video' | ||
3 | import { VideoFileModel } from './video-file' | ||
4 | import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' | ||
5 | import { CONFIG, THUMBNAILS_SIZE, VIDEO_EXT_MIMETYPE } from '../../initializers' | ||
6 | import { VideoCaptionModel } from './video-caption' | ||
7 | import { | ||
8 | getVideoCommentsActivityPubUrl, | ||
9 | getVideoDislikesActivityPubUrl, | ||
10 | getVideoLikesActivityPubUrl, | ||
11 | getVideoSharesActivityPubUrl | ||
12 | } from '../../lib/activitypub' | ||
13 | |||
14 | export type VideoFormattingJSONOptions = { | ||
15 | additionalAttributes: { | ||
16 | state?: boolean, | ||
17 | waitTranscoding?: boolean, | ||
18 | scheduledUpdate?: boolean, | ||
19 | blacklistInfo?: boolean | ||
20 | } | ||
21 | } | ||
22 | function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video { | ||
23 | const formattedAccount = video.VideoChannel.Account.toFormattedJSON() | ||
24 | const formattedVideoChannel = video.VideoChannel.toFormattedJSON() | ||
25 | |||
26 | const videoObject: Video = { | ||
27 | id: video.id, | ||
28 | uuid: video.uuid, | ||
29 | name: video.name, | ||
30 | category: { | ||
31 | id: video.category, | ||
32 | label: VideoModel.getCategoryLabel(video.category) | ||
33 | }, | ||
34 | licence: { | ||
35 | id: video.licence, | ||
36 | label: VideoModel.getLicenceLabel(video.licence) | ||
37 | }, | ||
38 | language: { | ||
39 | id: video.language, | ||
40 | label: VideoModel.getLanguageLabel(video.language) | ||
41 | }, | ||
42 | privacy: { | ||
43 | id: video.privacy, | ||
44 | label: VideoModel.getPrivacyLabel(video.privacy) | ||
45 | }, | ||
46 | nsfw: video.nsfw, | ||
47 | description: video.getTruncatedDescription(), | ||
48 | isLocal: video.isOwned(), | ||
49 | duration: video.duration, | ||
50 | views: video.views, | ||
51 | likes: video.likes, | ||
52 | dislikes: video.dislikes, | ||
53 | thumbnailPath: video.getThumbnailStaticPath(), | ||
54 | previewPath: video.getPreviewStaticPath(), | ||
55 | embedPath: video.getEmbedStaticPath(), | ||
56 | createdAt: video.createdAt, | ||
57 | updatedAt: video.updatedAt, | ||
58 | publishedAt: video.publishedAt, | ||
59 | account: { | ||
60 | id: formattedAccount.id, | ||
61 | uuid: formattedAccount.uuid, | ||
62 | name: formattedAccount.name, | ||
63 | displayName: formattedAccount.displayName, | ||
64 | url: formattedAccount.url, | ||
65 | host: formattedAccount.host, | ||
66 | avatar: formattedAccount.avatar | ||
67 | }, | ||
68 | channel: { | ||
69 | id: formattedVideoChannel.id, | ||
70 | uuid: formattedVideoChannel.uuid, | ||
71 | name: formattedVideoChannel.name, | ||
72 | displayName: formattedVideoChannel.displayName, | ||
73 | url: formattedVideoChannel.url, | ||
74 | host: formattedVideoChannel.host, | ||
75 | avatar: formattedVideoChannel.avatar | ||
76 | } | ||
77 | } | ||
78 | |||
79 | if (options) { | ||
80 | if (options.additionalAttributes.state === true) { | ||
81 | videoObject.state = { | ||
82 | id: video.state, | ||
83 | label: VideoModel.getStateLabel(video.state) | ||
84 | } | ||
85 | } | ||
86 | |||
87 | if (options.additionalAttributes.waitTranscoding === true) { | ||
88 | videoObject.waitTranscoding = video.waitTranscoding | ||
89 | } | ||
90 | |||
91 | if (options.additionalAttributes.scheduledUpdate === true && video.ScheduleVideoUpdate) { | ||
92 | videoObject.scheduledUpdate = { | ||
93 | updateAt: video.ScheduleVideoUpdate.updateAt, | ||
94 | privacy: video.ScheduleVideoUpdate.privacy || undefined | ||
95 | } | ||
96 | } | ||
97 | |||
98 | if (options.additionalAttributes.blacklistInfo === true) { | ||
99 | videoObject.blacklisted = !!video.VideoBlacklist | ||
100 | videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null | ||
101 | } | ||
102 | } | ||
103 | |||
104 | return videoObject | ||
105 | } | ||
106 | |||
107 | function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { | ||
108 | const formattedJson = video.toFormattedJSON({ | ||
109 | additionalAttributes: { | ||
110 | scheduledUpdate: true, | ||
111 | blacklistInfo: true | ||
112 | } | ||
113 | }) | ||
114 | |||
115 | const tags = video.Tags ? video.Tags.map(t => t.name) : [] | ||
116 | const detailsJson = { | ||
117 | support: video.support, | ||
118 | descriptionPath: video.getDescriptionAPIPath(), | ||
119 | channel: video.VideoChannel.toFormattedJSON(), | ||
120 | account: video.VideoChannel.Account.toFormattedJSON(), | ||
121 | tags, | ||
122 | commentsEnabled: video.commentsEnabled, | ||
123 | waitTranscoding: video.waitTranscoding, | ||
124 | state: { | ||
125 | id: video.state, | ||
126 | label: VideoModel.getStateLabel(video.state) | ||
127 | }, | ||
128 | files: [] | ||
129 | } | ||
130 | |||
131 | // Format and sort video files | ||
132 | detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) | ||
133 | |||
134 | return Object.assign(formattedJson, detailsJson) | ||
135 | } | ||
136 | |||
137 | function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { | ||
138 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | ||
139 | |||
140 | return videoFiles | ||
141 | .map(videoFile => { | ||
142 | let resolutionLabel = videoFile.resolution + 'p' | ||
143 | |||
144 | return { | ||
145 | resolution: { | ||
146 | id: videoFile.resolution, | ||
147 | label: resolutionLabel | ||
148 | }, | ||
149 | magnetUri: video.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), | ||
150 | size: videoFile.size, | ||
151 | fps: videoFile.fps, | ||
152 | torrentUrl: video.getTorrentUrl(videoFile, baseUrlHttp), | ||
153 | torrentDownloadUrl: video.getTorrentDownloadUrl(videoFile, baseUrlHttp), | ||
154 | fileUrl: video.getVideoFileUrl(videoFile, baseUrlHttp), | ||
155 | fileDownloadUrl: video.getVideoFileDownloadUrl(videoFile, baseUrlHttp) | ||
156 | } as VideoFile | ||
157 | }) | ||
158 | .sort((a, b) => { | ||
159 | if (a.resolution.id < b.resolution.id) return 1 | ||
160 | if (a.resolution.id === b.resolution.id) return 0 | ||
161 | return -1 | ||
162 | }) | ||
163 | } | ||
164 | |||
165 | function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | ||
166 | const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() | ||
167 | if (!video.Tags) video.Tags = [] | ||
168 | |||
169 | const tag = video.Tags.map(t => ({ | ||
170 | type: 'Hashtag' as 'Hashtag', | ||
171 | name: t.name | ||
172 | })) | ||
173 | |||
174 | let language | ||
175 | if (video.language) { | ||
176 | language = { | ||
177 | identifier: video.language, | ||
178 | name: VideoModel.getLanguageLabel(video.language) | ||
179 | } | ||
180 | } | ||
181 | |||
182 | let category | ||
183 | if (video.category) { | ||
184 | category = { | ||
185 | identifier: video.category + '', | ||
186 | name: VideoModel.getCategoryLabel(video.category) | ||
187 | } | ||
188 | } | ||
189 | |||
190 | let licence | ||
191 | if (video.licence) { | ||
192 | licence = { | ||
193 | identifier: video.licence + '', | ||
194 | name: VideoModel.getLicenceLabel(video.licence) | ||
195 | } | ||
196 | } | ||
197 | |||
198 | const url: ActivityUrlObject[] = [] | ||
199 | for (const file of video.VideoFiles) { | ||
200 | url.push({ | ||
201 | type: 'Link', | ||
202 | mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | ||
203 | href: video.getVideoFileUrl(file, baseUrlHttp), | ||
204 | height: file.resolution, | ||
205 | size: file.size, | ||
206 | fps: file.fps | ||
207 | }) | ||
208 | |||
209 | url.push({ | ||
210 | type: 'Link', | ||
211 | mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
212 | href: video.getTorrentUrl(file, baseUrlHttp), | ||
213 | height: file.resolution | ||
214 | }) | ||
215 | |||
216 | url.push({ | ||
217 | type: 'Link', | ||
218 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
219 | href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs), | ||
220 | height: file.resolution | ||
221 | }) | ||
222 | } | ||
223 | |||
224 | // Add video url too | ||
225 | url.push({ | ||
226 | type: 'Link', | ||
227 | mimeType: 'text/html', | ||
228 | href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid | ||
229 | }) | ||
230 | |||
231 | const subtitleLanguage = [] | ||
232 | for (const caption of video.VideoCaptions) { | ||
233 | subtitleLanguage.push({ | ||
234 | identifier: caption.language, | ||
235 | name: VideoCaptionModel.getLanguageLabel(caption.language) | ||
236 | }) | ||
237 | } | ||
238 | |||
239 | return { | ||
240 | type: 'Video' as 'Video', | ||
241 | id: video.url, | ||
242 | name: video.name, | ||
243 | duration: getActivityStreamDuration(video.duration), | ||
244 | uuid: video.uuid, | ||
245 | tag, | ||
246 | category, | ||
247 | licence, | ||
248 | language, | ||
249 | views: video.views, | ||
250 | sensitive: video.nsfw, | ||
251 | waitTranscoding: video.waitTranscoding, | ||
252 | state: video.state, | ||
253 | commentsEnabled: video.commentsEnabled, | ||
254 | published: video.publishedAt.toISOString(), | ||
255 | updated: video.updatedAt.toISOString(), | ||
256 | mediaType: 'text/markdown', | ||
257 | content: video.getTruncatedDescription(), | ||
258 | support: video.support, | ||
259 | subtitleLanguage, | ||
260 | icon: { | ||
261 | type: 'Image', | ||
262 | url: video.getThumbnailUrl(baseUrlHttp), | ||
263 | mediaType: 'image/jpeg', | ||
264 | width: THUMBNAILS_SIZE.width, | ||
265 | height: THUMBNAILS_SIZE.height | ||
266 | }, | ||
267 | url, | ||
268 | likes: getVideoLikesActivityPubUrl(video), | ||
269 | dislikes: getVideoDislikesActivityPubUrl(video), | ||
270 | shares: getVideoSharesActivityPubUrl(video), | ||
271 | comments: getVideoCommentsActivityPubUrl(video), | ||
272 | attributedTo: [ | ||
273 | { | ||
274 | type: 'Person', | ||
275 | id: video.VideoChannel.Account.Actor.url | ||
276 | }, | ||
277 | { | ||
278 | type: 'Group', | ||
279 | id: video.VideoChannel.Actor.url | ||
280 | } | ||
281 | ] | ||
282 | } | ||
283 | } | ||
284 | |||
285 | function getActivityStreamDuration (duration: number) { | ||
286 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | ||
287 | return 'PT' + duration + 'S' | ||
288 | } | ||
289 | |||
290 | export { | ||
291 | videoModelToFormattedJSON, | ||
292 | videoModelToFormattedDetailsJSON, | ||
293 | videoFilesModelToFormattedJSON, | ||
294 | videoModelToActivityPubObject, | ||
295 | getActivityStreamDuration | ||
296 | } | ||
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 27c631dcd..6c89c16bf 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { map, maxBy } from 'lodash' | 2 | import { maxBy } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import * as parseTorrent from 'parse-torrent' | 4 | import * as parseTorrent from 'parse-torrent' |
5 | import { extname, join } from 'path' | 5 | import { join } from 'path' |
6 | import * as Sequelize from 'sequelize' | 6 | import * as Sequelize from 'sequelize' |
7 | import { | 7 | import { |
8 | AllowNull, | 8 | AllowNull, |
@@ -27,7 +27,7 @@ import { | |||
27 | Table, | 27 | Table, |
28 | UpdatedAt | 28 | UpdatedAt |
29 | } from 'sequelize-typescript' | 29 | } from 'sequelize-typescript' |
30 | import { ActivityUrlObject, VideoPrivacy, VideoResolution, VideoState } from '../../../shared' | 30 | import { VideoPrivacy, VideoState } from '../../../shared' |
31 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 31 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
32 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 32 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' |
33 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' | 33 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' |
@@ -45,7 +45,7 @@ import { | |||
45 | isVideoStateValid, | 45 | isVideoStateValid, |
46 | isVideoSupportValid | 46 | isVideoSupportValid |
47 | } from '../../helpers/custom-validators/videos' | 47 | } from '../../helpers/custom-validators/videos' |
48 | import { generateImageFromVideoFile, getVideoFileFPS, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils' | 48 | import { generateImageFromVideoFile, getVideoFileResolution } from '../../helpers/ffmpeg-utils' |
49 | import { logger } from '../../helpers/logger' | 49 | import { logger } from '../../helpers/logger' |
50 | import { getServerActor } from '../../helpers/utils' | 50 | import { getServerActor } from '../../helpers/utils' |
51 | import { | 51 | import { |
@@ -59,18 +59,11 @@ import { | |||
59 | STATIC_PATHS, | 59 | STATIC_PATHS, |
60 | THUMBNAILS_SIZE, | 60 | THUMBNAILS_SIZE, |
61 | VIDEO_CATEGORIES, | 61 | VIDEO_CATEGORIES, |
62 | VIDEO_EXT_MIMETYPE, | ||
63 | VIDEO_LANGUAGES, | 62 | VIDEO_LANGUAGES, |
64 | VIDEO_LICENCES, | 63 | VIDEO_LICENCES, |
65 | VIDEO_PRIVACIES, | 64 | VIDEO_PRIVACIES, |
66 | VIDEO_STATES | 65 | VIDEO_STATES |
67 | } from '../../initializers' | 66 | } from '../../initializers' |
68 | import { | ||
69 | getVideoCommentsActivityPubUrl, | ||
70 | getVideoDislikesActivityPubUrl, | ||
71 | getVideoLikesActivityPubUrl, | ||
72 | getVideoSharesActivityPubUrl | ||
73 | } from '../../lib/activitypub' | ||
74 | import { sendDeleteVideo } from '../../lib/activitypub/send' | 67 | import { sendDeleteVideo } from '../../lib/activitypub/send' |
75 | import { AccountModel } from '../account/account' | 68 | import { AccountModel } from '../account/account' |
76 | import { AccountVideoRateModel } from '../account/account-video-rate' | 69 | import { AccountVideoRateModel } from '../account/account-video-rate' |
@@ -88,9 +81,17 @@ import { VideoTagModel } from './video-tag' | |||
88 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 81 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
89 | import { VideoCaptionModel } from './video-caption' | 82 | import { VideoCaptionModel } from './video-caption' |
90 | import { VideoBlacklistModel } from './video-blacklist' | 83 | import { VideoBlacklistModel } from './video-blacklist' |
91 | import { copy, remove, rename, stat, writeFile } from 'fs-extra' | 84 | import { remove, writeFile } from 'fs-extra' |
92 | import { VideoViewModel } from './video-views' | 85 | import { VideoViewModel } from './video-views' |
93 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 86 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
87 | import { | ||
88 | videoFilesModelToFormattedJSON, | ||
89 | VideoFormattingJSONOptions, | ||
90 | videoModelToActivityPubObject, | ||
91 | videoModelToFormattedDetailsJSON, | ||
92 | videoModelToFormattedJSON | ||
93 | } from './video-format-utils' | ||
94 | import * as validator from 'validator' | ||
94 | 95 | ||
95 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 96 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
96 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 97 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -221,6 +222,7 @@ type AvailableForListIDsOptions = { | |||
221 | }, | 222 | }, |
222 | [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { | 223 | [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { |
223 | const query: IFindOptions<VideoModel> = { | 224 | const query: IFindOptions<VideoModel> = { |
225 | raw: true, | ||
224 | attributes: [ 'id' ], | 226 | attributes: [ 'id' ], |
225 | where: { | 227 | where: { |
226 | id: { | 228 | id: { |
@@ -387,16 +389,7 @@ type AvailableForListIDsOptions = { | |||
387 | } | 389 | } |
388 | 390 | ||
389 | if (options.trendingDays) { | 391 | if (options.trendingDays) { |
390 | query.include.push({ | 392 | query.include.push(VideoModel.buildTrendingQuery(options.trendingDays)) |
391 | attributes: [], | ||
392 | model: VideoViewModel, | ||
393 | required: false, | ||
394 | where: { | ||
395 | startDate: { | ||
396 | [ Sequelize.Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays) | ||
397 | } | ||
398 | } | ||
399 | }) | ||
400 | 393 | ||
401 | query.subQuery = false | 394 | query.subQuery = false |
402 | } | 395 | } |
@@ -474,6 +467,7 @@ type AvailableForListIDsOptions = { | |||
474 | required: false, | 467 | required: false, |
475 | include: [ | 468 | include: [ |
476 | { | 469 | { |
470 | attributes: [ 'fileUrl' ], | ||
477 | model: () => VideoRedundancyModel.unscoped(), | 471 | model: () => VideoRedundancyModel.unscoped(), |
478 | required: false | 472 | required: false |
479 | } | 473 | } |
@@ -937,7 +931,7 @@ export class VideoModel extends Model<VideoModel> { | |||
937 | videoChannelId?: number, | 931 | videoChannelId?: number, |
938 | actorId?: number | 932 | actorId?: number |
939 | trendingDays?: number | 933 | trendingDays?: number |
940 | }) { | 934 | }, countVideos = true) { |
941 | const query: IFindOptions<VideoModel> = { | 935 | const query: IFindOptions<VideoModel> = { |
942 | offset: options.start, | 936 | offset: options.start, |
943 | limit: options.count, | 937 | limit: options.count, |
@@ -970,7 +964,7 @@ export class VideoModel extends Model<VideoModel> { | |||
970 | trendingDays | 964 | trendingDays |
971 | } | 965 | } |
972 | 966 | ||
973 | return VideoModel.getAvailableForApi(query, queryOptions) | 967 | return VideoModel.getAvailableForApi(query, queryOptions, countVideos) |
974 | } | 968 | } |
975 | 969 | ||
976 | static async searchAndPopulateAccountAndServer (options: { | 970 | static async searchAndPopulateAccountAndServer (options: { |
@@ -1070,41 +1064,34 @@ export class VideoModel extends Model<VideoModel> { | |||
1070 | return VideoModel.getAvailableForApi(query, queryOptions) | 1064 | return VideoModel.getAvailableForApi(query, queryOptions) |
1071 | } | 1065 | } |
1072 | 1066 | ||
1073 | static load (id: number, t?: Sequelize.Transaction) { | 1067 | static load (id: number | string, t?: Sequelize.Transaction) { |
1074 | const options = t ? { transaction: t } : undefined | 1068 | const where = VideoModel.buildWhereIdOrUUID(id) |
1075 | 1069 | const options = { | |
1076 | return VideoModel.findById(id, options) | 1070 | where, |
1077 | } | 1071 | transaction: t |
1078 | |||
1079 | static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { | ||
1080 | const query: IFindOptions<VideoModel> = { | ||
1081 | where: { | ||
1082 | url | ||
1083 | } | ||
1084 | } | 1072 | } |
1085 | 1073 | ||
1086 | if (t !== undefined) query.transaction = t | 1074 | return VideoModel.findOne(options) |
1087 | |||
1088 | return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) | ||
1089 | } | 1075 | } |
1090 | 1076 | ||
1091 | static loadAndPopulateAccountAndServerAndTags (id: number) { | 1077 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { |
1078 | const where = VideoModel.buildWhereIdOrUUID(id) | ||
1079 | |||
1092 | const options = { | 1080 | const options = { |
1093 | order: [ [ 'Tags', 'name', 'ASC' ] ] | 1081 | attributes: [ 'id' ], |
1082 | where, | ||
1083 | transaction: t | ||
1094 | } | 1084 | } |
1095 | 1085 | ||
1096 | return VideoModel | 1086 | return VideoModel.findOne(options) |
1097 | .scope([ | ||
1098 | ScopeNames.WITH_TAGS, | ||
1099 | ScopeNames.WITH_BLACKLISTED, | ||
1100 | ScopeNames.WITH_FILES, | ||
1101 | ScopeNames.WITH_ACCOUNT_DETAILS, | ||
1102 | ScopeNames.WITH_SCHEDULED_UPDATE | ||
1103 | ]) | ||
1104 | .findById(id, options) | ||
1105 | } | 1087 | } |
1106 | 1088 | ||
1107 | static loadByUUID (uuid: string) { | 1089 | static loadWithFile (id: number, t?: Sequelize.Transaction, logging?: boolean) { |
1090 | return VideoModel.scope(ScopeNames.WITH_FILES) | ||
1091 | .findById(id, { transaction: t, logging }) | ||
1092 | } | ||
1093 | |||
1094 | static loadByUUIDWithFile (uuid: string) { | ||
1108 | const options = { | 1095 | const options = { |
1109 | where: { | 1096 | where: { |
1110 | uuid | 1097 | uuid |
@@ -1116,12 +1103,34 @@ export class VideoModel extends Model<VideoModel> { | |||
1116 | .findOne(options) | 1103 | .findOne(options) |
1117 | } | 1104 | } |
1118 | 1105 | ||
1119 | static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string, t?: Sequelize.Transaction) { | 1106 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { |
1120 | const options = { | 1107 | const query: IFindOptions<VideoModel> = { |
1121 | order: [ [ 'Tags', 'name', 'ASC' ] ], | ||
1122 | where: { | 1108 | where: { |
1123 | uuid | 1109 | url |
1110 | }, | ||
1111 | transaction | ||
1112 | } | ||
1113 | |||
1114 | return VideoModel.findOne(query) | ||
1115 | } | ||
1116 | |||
1117 | static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) { | ||
1118 | const query: IFindOptions<VideoModel> = { | ||
1119 | where: { | ||
1120 | url | ||
1124 | }, | 1121 | }, |
1122 | transaction | ||
1123 | } | ||
1124 | |||
1125 | return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) | ||
1126 | } | ||
1127 | |||
1128 | static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) { | ||
1129 | const where = VideoModel.buildWhereIdOrUUID(id) | ||
1130 | |||
1131 | const options = { | ||
1132 | order: [ [ 'Tags', 'name', 'ASC' ] ], | ||
1133 | where, | ||
1125 | transaction: t | 1134 | transaction: t |
1126 | } | 1135 | } |
1127 | 1136 | ||
@@ -1169,7 +1178,14 @@ export class VideoModel extends Model<VideoModel> { | |||
1169 | } | 1178 | } |
1170 | 1179 | ||
1171 | // threshold corresponds to how many video the field should have to be returned | 1180 | // threshold corresponds to how many video the field should have to be returned |
1172 | static getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { | 1181 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { |
1182 | const actorId = (await getServerActor()).id | ||
1183 | |||
1184 | const scopeOptions = { | ||
1185 | actorId, | ||
1186 | includeLocalVideos: true | ||
1187 | } | ||
1188 | |||
1173 | const query: IFindOptions<VideoModel> = { | 1189 | const query: IFindOptions<VideoModel> = { |
1174 | attributes: [ field ], | 1190 | attributes: [ field ], |
1175 | limit: count, | 1191 | limit: count, |
@@ -1177,20 +1193,28 @@ export class VideoModel extends Model<VideoModel> { | |||
1177 | having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.col(field)), { | 1193 | having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.col(field)), { |
1178 | [ Sequelize.Op.gte ]: threshold | 1194 | [ Sequelize.Op.gte ]: threshold |
1179 | }) as any, // FIXME: typings | 1195 | }) as any, // FIXME: typings |
1180 | where: { | ||
1181 | [ field ]: { | ||
1182 | [ Sequelize.Op.not ]: null | ||
1183 | }, | ||
1184 | privacy: VideoPrivacy.PUBLIC, | ||
1185 | state: VideoState.PUBLISHED | ||
1186 | }, | ||
1187 | order: [ this.sequelize.random() ] | 1196 | order: [ this.sequelize.random() ] |
1188 | } | 1197 | } |
1189 | 1198 | ||
1190 | return VideoModel.findAll(query) | 1199 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] }) |
1200 | .findAll(query) | ||
1191 | .then(rows => rows.map(r => r[ field ])) | 1201 | .then(rows => rows.map(r => r[ field ])) |
1192 | } | 1202 | } |
1193 | 1203 | ||
1204 | static buildTrendingQuery (trendingDays: number) { | ||
1205 | return { | ||
1206 | attributes: [], | ||
1207 | subQuery: false, | ||
1208 | model: VideoViewModel, | ||
1209 | required: false, | ||
1210 | where: { | ||
1211 | startDate: { | ||
1212 | [ Sequelize.Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) | ||
1213 | } | ||
1214 | } | ||
1215 | } | ||
1216 | } | ||
1217 | |||
1194 | private static buildActorWhereWithFilter (filter?: VideoFilter) { | 1218 | private static buildActorWhereWithFilter (filter?: VideoFilter) { |
1195 | if (filter && filter === 'local') { | 1219 | if (filter && filter === 'local') { |
1196 | return { | 1220 | return { |
@@ -1201,7 +1225,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1201 | return {} | 1225 | return {} |
1202 | } | 1226 | } |
1203 | 1227 | ||
1204 | private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions) { | 1228 | private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) { |
1205 | const idsScope = { | 1229 | const idsScope = { |
1206 | method: [ | 1230 | method: [ |
1207 | ScopeNames.AVAILABLE_FOR_LIST_IDS, options | 1231 | ScopeNames.AVAILABLE_FOR_LIST_IDS, options |
@@ -1218,7 +1242,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1218 | } | 1242 | } |
1219 | 1243 | ||
1220 | const [ count, rowsId ] = await Promise.all([ | 1244 | const [ count, rowsId ] = await Promise.all([ |
1221 | VideoModel.scope(countScope).count(countQuery), | 1245 | countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined), |
1222 | VideoModel.scope(idsScope).findAll(query) | 1246 | VideoModel.scope(idsScope).findAll(query) |
1223 | ]) | 1247 | ]) |
1224 | const ids = rowsId.map(r => r.id) | 1248 | const ids = rowsId.map(r => r.id) |
@@ -1247,26 +1271,30 @@ export class VideoModel extends Model<VideoModel> { | |||
1247 | } | 1271 | } |
1248 | } | 1272 | } |
1249 | 1273 | ||
1250 | private static getCategoryLabel (id: number) { | 1274 | static getCategoryLabel (id: number) { |
1251 | return VIDEO_CATEGORIES[ id ] || 'Misc' | 1275 | return VIDEO_CATEGORIES[ id ] || 'Misc' |
1252 | } | 1276 | } |
1253 | 1277 | ||
1254 | private static getLicenceLabel (id: number) { | 1278 | static getLicenceLabel (id: number) { |
1255 | return VIDEO_LICENCES[ id ] || 'Unknown' | 1279 | return VIDEO_LICENCES[ id ] || 'Unknown' |
1256 | } | 1280 | } |
1257 | 1281 | ||
1258 | private static getLanguageLabel (id: string) { | 1282 | static getLanguageLabel (id: string) { |
1259 | return VIDEO_LANGUAGES[ id ] || 'Unknown' | 1283 | return VIDEO_LANGUAGES[ id ] || 'Unknown' |
1260 | } | 1284 | } |
1261 | 1285 | ||
1262 | private static getPrivacyLabel (id: number) { | 1286 | static getPrivacyLabel (id: number) { |
1263 | return VIDEO_PRIVACIES[ id ] || 'Unknown' | 1287 | return VIDEO_PRIVACIES[ id ] || 'Unknown' |
1264 | } | 1288 | } |
1265 | 1289 | ||
1266 | private static getStateLabel (id: number) { | 1290 | static getStateLabel (id: number) { |
1267 | return VIDEO_STATES[ id ] || 'Unknown' | 1291 | return VIDEO_STATES[ id ] || 'Unknown' |
1268 | } | 1292 | } |
1269 | 1293 | ||
1294 | static buildWhereIdOrUUID (id: number | string) { | ||
1295 | return validator.isInt('' + id) ? { id } : { uuid: id } | ||
1296 | } | ||
1297 | |||
1270 | getOriginalFile () { | 1298 | getOriginalFile () { |
1271 | if (Array.isArray(this.VideoFiles) === false) return undefined | 1299 | if (Array.isArray(this.VideoFiles) === false) return undefined |
1272 | 1300 | ||
@@ -1359,273 +1387,20 @@ export class VideoModel extends Model<VideoModel> { | |||
1359 | return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) | 1387 | return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) |
1360 | } | 1388 | } |
1361 | 1389 | ||
1362 | toFormattedJSON (options?: { | 1390 | toFormattedJSON (options?: VideoFormattingJSONOptions): Video { |
1363 | additionalAttributes: { | 1391 | return videoModelToFormattedJSON(this, options) |
1364 | state?: boolean, | ||
1365 | waitTranscoding?: boolean, | ||
1366 | scheduledUpdate?: boolean, | ||
1367 | blacklistInfo?: boolean | ||
1368 | } | ||
1369 | }): Video { | ||
1370 | const formattedAccount = this.VideoChannel.Account.toFormattedJSON() | ||
1371 | const formattedVideoChannel = this.VideoChannel.toFormattedJSON() | ||
1372 | |||
1373 | const videoObject: Video = { | ||
1374 | id: this.id, | ||
1375 | uuid: this.uuid, | ||
1376 | name: this.name, | ||
1377 | category: { | ||
1378 | id: this.category, | ||
1379 | label: VideoModel.getCategoryLabel(this.category) | ||
1380 | }, | ||
1381 | licence: { | ||
1382 | id: this.licence, | ||
1383 | label: VideoModel.getLicenceLabel(this.licence) | ||
1384 | }, | ||
1385 | language: { | ||
1386 | id: this.language, | ||
1387 | label: VideoModel.getLanguageLabel(this.language) | ||
1388 | }, | ||
1389 | privacy: { | ||
1390 | id: this.privacy, | ||
1391 | label: VideoModel.getPrivacyLabel(this.privacy) | ||
1392 | }, | ||
1393 | nsfw: this.nsfw, | ||
1394 | description: this.getTruncatedDescription(), | ||
1395 | isLocal: this.isOwned(), | ||
1396 | duration: this.duration, | ||
1397 | views: this.views, | ||
1398 | likes: this.likes, | ||
1399 | dislikes: this.dislikes, | ||
1400 | thumbnailPath: this.getThumbnailStaticPath(), | ||
1401 | previewPath: this.getPreviewStaticPath(), | ||
1402 | embedPath: this.getEmbedStaticPath(), | ||
1403 | createdAt: this.createdAt, | ||
1404 | updatedAt: this.updatedAt, | ||
1405 | publishedAt: this.publishedAt, | ||
1406 | account: { | ||
1407 | id: formattedAccount.id, | ||
1408 | uuid: formattedAccount.uuid, | ||
1409 | name: formattedAccount.name, | ||
1410 | displayName: formattedAccount.displayName, | ||
1411 | url: formattedAccount.url, | ||
1412 | host: formattedAccount.host, | ||
1413 | avatar: formattedAccount.avatar | ||
1414 | }, | ||
1415 | channel: { | ||
1416 | id: formattedVideoChannel.id, | ||
1417 | uuid: formattedVideoChannel.uuid, | ||
1418 | name: formattedVideoChannel.name, | ||
1419 | displayName: formattedVideoChannel.displayName, | ||
1420 | url: formattedVideoChannel.url, | ||
1421 | host: formattedVideoChannel.host, | ||
1422 | avatar: formattedVideoChannel.avatar | ||
1423 | } | ||
1424 | } | ||
1425 | |||
1426 | if (options) { | ||
1427 | if (options.additionalAttributes.state === true) { | ||
1428 | videoObject.state = { | ||
1429 | id: this.state, | ||
1430 | label: VideoModel.getStateLabel(this.state) | ||
1431 | } | ||
1432 | } | ||
1433 | |||
1434 | if (options.additionalAttributes.waitTranscoding === true) { | ||
1435 | videoObject.waitTranscoding = this.waitTranscoding | ||
1436 | } | ||
1437 | |||
1438 | if (options.additionalAttributes.scheduledUpdate === true && this.ScheduleVideoUpdate) { | ||
1439 | videoObject.scheduledUpdate = { | ||
1440 | updateAt: this.ScheduleVideoUpdate.updateAt, | ||
1441 | privacy: this.ScheduleVideoUpdate.privacy || undefined | ||
1442 | } | ||
1443 | } | ||
1444 | |||
1445 | if (options.additionalAttributes.blacklistInfo === true) { | ||
1446 | videoObject.blacklisted = !!this.VideoBlacklist | ||
1447 | videoObject.blacklistedReason = this.VideoBlacklist ? this.VideoBlacklist.reason : null | ||
1448 | } | ||
1449 | } | ||
1450 | |||
1451 | return videoObject | ||
1452 | } | 1392 | } |
1453 | 1393 | ||
1454 | toFormattedDetailsJSON (): VideoDetails { | 1394 | toFormattedDetailsJSON (): VideoDetails { |
1455 | const formattedJson = this.toFormattedJSON({ | 1395 | return videoModelToFormattedDetailsJSON(this) |
1456 | additionalAttributes: { | ||
1457 | scheduledUpdate: true, | ||
1458 | blacklistInfo: true | ||
1459 | } | ||
1460 | }) | ||
1461 | |||
1462 | const detailsJson = { | ||
1463 | support: this.support, | ||
1464 | descriptionPath: this.getDescriptionPath(), | ||
1465 | channel: this.VideoChannel.toFormattedJSON(), | ||
1466 | account: this.VideoChannel.Account.toFormattedJSON(), | ||
1467 | tags: map(this.Tags, 'name'), | ||
1468 | commentsEnabled: this.commentsEnabled, | ||
1469 | waitTranscoding: this.waitTranscoding, | ||
1470 | state: { | ||
1471 | id: this.state, | ||
1472 | label: VideoModel.getStateLabel(this.state) | ||
1473 | }, | ||
1474 | files: [] | ||
1475 | } | ||
1476 | |||
1477 | // Format and sort video files | ||
1478 | detailsJson.files = this.getFormattedVideoFilesJSON() | ||
1479 | |||
1480 | return Object.assign(formattedJson, detailsJson) | ||
1481 | } | 1396 | } |
1482 | 1397 | ||
1483 | getFormattedVideoFilesJSON (): VideoFile[] { | 1398 | getFormattedVideoFilesJSON (): VideoFile[] { |
1484 | const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() | 1399 | return videoFilesModelToFormattedJSON(this, this.VideoFiles) |
1485 | |||
1486 | return this.VideoFiles | ||
1487 | .map(videoFile => { | ||
1488 | let resolutionLabel = videoFile.resolution + 'p' | ||
1489 | |||
1490 | return { | ||
1491 | resolution: { | ||
1492 | id: videoFile.resolution, | ||
1493 | label: resolutionLabel | ||
1494 | }, | ||
1495 | magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), | ||
1496 | size: videoFile.size, | ||
1497 | fps: videoFile.fps, | ||
1498 | torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), | ||
1499 | torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp), | ||
1500 | fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp), | ||
1501 | fileDownloadUrl: this.getVideoFileDownloadUrl(videoFile, baseUrlHttp) | ||
1502 | } as VideoFile | ||
1503 | }) | ||
1504 | .sort((a, b) => { | ||
1505 | if (a.resolution.id < b.resolution.id) return 1 | ||
1506 | if (a.resolution.id === b.resolution.id) return 0 | ||
1507 | return -1 | ||
1508 | }) | ||
1509 | } | 1400 | } |
1510 | 1401 | ||
1511 | toActivityPubObject (): VideoTorrentObject { | 1402 | toActivityPubObject (): VideoTorrentObject { |
1512 | const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() | 1403 | return videoModelToActivityPubObject(this) |
1513 | if (!this.Tags) this.Tags = [] | ||
1514 | |||
1515 | const tag = this.Tags.map(t => ({ | ||
1516 | type: 'Hashtag' as 'Hashtag', | ||
1517 | name: t.name | ||
1518 | })) | ||
1519 | |||
1520 | let language | ||
1521 | if (this.language) { | ||
1522 | language = { | ||
1523 | identifier: this.language, | ||
1524 | name: VideoModel.getLanguageLabel(this.language) | ||
1525 | } | ||
1526 | } | ||
1527 | |||
1528 | let category | ||
1529 | if (this.category) { | ||
1530 | category = { | ||
1531 | identifier: this.category + '', | ||
1532 | name: VideoModel.getCategoryLabel(this.category) | ||
1533 | } | ||
1534 | } | ||
1535 | |||
1536 | let licence | ||
1537 | if (this.licence) { | ||
1538 | licence = { | ||
1539 | identifier: this.licence + '', | ||
1540 | name: VideoModel.getLicenceLabel(this.licence) | ||
1541 | } | ||
1542 | } | ||
1543 | |||
1544 | const url: ActivityUrlObject[] = [] | ||
1545 | for (const file of this.VideoFiles) { | ||
1546 | url.push({ | ||
1547 | type: 'Link', | ||
1548 | mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any, | ||
1549 | href: this.getVideoFileUrl(file, baseUrlHttp), | ||
1550 | height: file.resolution, | ||
1551 | size: file.size, | ||
1552 | fps: file.fps | ||
1553 | }) | ||
1554 | |||
1555 | url.push({ | ||
1556 | type: 'Link', | ||
1557 | mimeType: 'application/x-bittorrent' as 'application/x-bittorrent', | ||
1558 | href: this.getTorrentUrl(file, baseUrlHttp), | ||
1559 | height: file.resolution | ||
1560 | }) | ||
1561 | |||
1562 | url.push({ | ||
1563 | type: 'Link', | ||
1564 | mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', | ||
1565 | href: this.generateMagnetUri(file, baseUrlHttp, baseUrlWs), | ||
1566 | height: file.resolution | ||
1567 | }) | ||
1568 | } | ||
1569 | |||
1570 | // Add video url too | ||
1571 | url.push({ | ||
1572 | type: 'Link', | ||
1573 | mimeType: 'text/html', | ||
1574 | href: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid | ||
1575 | }) | ||
1576 | |||
1577 | const subtitleLanguage = [] | ||
1578 | for (const caption of this.VideoCaptions) { | ||
1579 | subtitleLanguage.push({ | ||
1580 | identifier: caption.language, | ||
1581 | name: VideoCaptionModel.getLanguageLabel(caption.language) | ||
1582 | }) | ||
1583 | } | ||
1584 | |||
1585 | return { | ||
1586 | type: 'Video' as 'Video', | ||
1587 | id: this.url, | ||
1588 | name: this.name, | ||
1589 | duration: this.getActivityStreamDuration(), | ||
1590 | uuid: this.uuid, | ||
1591 | tag, | ||
1592 | category, | ||
1593 | licence, | ||
1594 | language, | ||
1595 | views: this.views, | ||
1596 | sensitive: this.nsfw, | ||
1597 | waitTranscoding: this.waitTranscoding, | ||
1598 | state: this.state, | ||
1599 | commentsEnabled: this.commentsEnabled, | ||
1600 | published: this.publishedAt.toISOString(), | ||
1601 | updated: this.updatedAt.toISOString(), | ||
1602 | mediaType: 'text/markdown', | ||
1603 | content: this.getTruncatedDescription(), | ||
1604 | support: this.support, | ||
1605 | subtitleLanguage, | ||
1606 | icon: { | ||
1607 | type: 'Image', | ||
1608 | url: this.getThumbnailUrl(baseUrlHttp), | ||
1609 | mediaType: 'image/jpeg', | ||
1610 | width: THUMBNAILS_SIZE.width, | ||
1611 | height: THUMBNAILS_SIZE.height | ||
1612 | }, | ||
1613 | url, | ||
1614 | likes: getVideoLikesActivityPubUrl(this), | ||
1615 | dislikes: getVideoDislikesActivityPubUrl(this), | ||
1616 | shares: getVideoSharesActivityPubUrl(this), | ||
1617 | comments: getVideoCommentsActivityPubUrl(this), | ||
1618 | attributedTo: [ | ||
1619 | { | ||
1620 | type: 'Person', | ||
1621 | id: this.VideoChannel.Account.Actor.url | ||
1622 | }, | ||
1623 | { | ||
1624 | type: 'Group', | ||
1625 | id: this.VideoChannel.Actor.url | ||
1626 | } | ||
1627 | ] | ||
1628 | } | ||
1629 | } | 1404 | } |
1630 | 1405 | ||
1631 | getTruncatedDescription () { | 1406 | getTruncatedDescription () { |
@@ -1635,130 +1410,13 @@ export class VideoModel extends Model<VideoModel> { | |||
1635 | return peertubeTruncate(this.description, maxLength) | 1410 | return peertubeTruncate(this.description, maxLength) |
1636 | } | 1411 | } |
1637 | 1412 | ||
1638 | async optimizeOriginalVideofile () { | ||
1639 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
1640 | const newExtname = '.mp4' | ||
1641 | const inputVideoFile = this.getOriginalFile() | ||
1642 | const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) | ||
1643 | const videoTranscodedPath = join(videosDirectory, this.id + '-transcoded' + newExtname) | ||
1644 | |||
1645 | const transcodeOptions = { | ||
1646 | inputPath: videoInputPath, | ||
1647 | outputPath: videoTranscodedPath | ||
1648 | } | ||
1649 | |||
1650 | // Could be very long! | ||
1651 | await transcode(transcodeOptions) | ||
1652 | |||
1653 | try { | ||
1654 | await remove(videoInputPath) | ||
1655 | |||
1656 | // Important to do this before getVideoFilename() to take in account the new file extension | ||
1657 | inputVideoFile.set('extname', newExtname) | ||
1658 | |||
1659 | const videoOutputPath = this.getVideoFilePath(inputVideoFile) | ||
1660 | await rename(videoTranscodedPath, videoOutputPath) | ||
1661 | const stats = await stat(videoOutputPath) | ||
1662 | const fps = await getVideoFileFPS(videoOutputPath) | ||
1663 | |||
1664 | inputVideoFile.set('size', stats.size) | ||
1665 | inputVideoFile.set('fps', fps) | ||
1666 | |||
1667 | await this.createTorrentAndSetInfoHash(inputVideoFile) | ||
1668 | await inputVideoFile.save() | ||
1669 | |||
1670 | } catch (err) { | ||
1671 | // Auto destruction... | ||
1672 | this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err })) | ||
1673 | |||
1674 | throw err | ||
1675 | } | ||
1676 | } | ||
1677 | |||
1678 | async transcodeOriginalVideofile (resolution: VideoResolution, isPortraitMode: boolean) { | ||
1679 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | ||
1680 | const extname = '.mp4' | ||
1681 | |||
1682 | // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed | ||
1683 | const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile())) | ||
1684 | |||
1685 | const newVideoFile = new VideoFileModel({ | ||
1686 | resolution, | ||
1687 | extname, | ||
1688 | size: 0, | ||
1689 | videoId: this.id | ||
1690 | }) | ||
1691 | const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) | ||
1692 | |||
1693 | const transcodeOptions = { | ||
1694 | inputPath: videoInputPath, | ||
1695 | outputPath: videoOutputPath, | ||
1696 | resolution, | ||
1697 | isPortraitMode | ||
1698 | } | ||
1699 | |||
1700 | await transcode(transcodeOptions) | ||
1701 | |||
1702 | const stats = await stat(videoOutputPath) | ||
1703 | const fps = await getVideoFileFPS(videoOutputPath) | ||
1704 | |||
1705 | newVideoFile.set('size', stats.size) | ||
1706 | newVideoFile.set('fps', fps) | ||
1707 | |||
1708 | await this.createTorrentAndSetInfoHash(newVideoFile) | ||
1709 | |||
1710 | await newVideoFile.save() | ||
1711 | |||
1712 | this.VideoFiles.push(newVideoFile) | ||
1713 | } | ||
1714 | |||
1715 | async importVideoFile (inputFilePath: string) { | ||
1716 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) | ||
1717 | const { size } = await stat(inputFilePath) | ||
1718 | const fps = await getVideoFileFPS(inputFilePath) | ||
1719 | |||
1720 | let updatedVideoFile = new VideoFileModel({ | ||
1721 | resolution: videoFileResolution, | ||
1722 | extname: extname(inputFilePath), | ||
1723 | size, | ||
1724 | fps, | ||
1725 | videoId: this.id | ||
1726 | }) | ||
1727 | |||
1728 | const currentVideoFile = this.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution) | ||
1729 | |||
1730 | if (currentVideoFile) { | ||
1731 | // Remove old file and old torrent | ||
1732 | await this.removeFile(currentVideoFile) | ||
1733 | await this.removeTorrent(currentVideoFile) | ||
1734 | // Remove the old video file from the array | ||
1735 | this.VideoFiles = this.VideoFiles.filter(f => f !== currentVideoFile) | ||
1736 | |||
1737 | // Update the database | ||
1738 | currentVideoFile.set('extname', updatedVideoFile.extname) | ||
1739 | currentVideoFile.set('size', updatedVideoFile.size) | ||
1740 | currentVideoFile.set('fps', updatedVideoFile.fps) | ||
1741 | |||
1742 | updatedVideoFile = currentVideoFile | ||
1743 | } | ||
1744 | |||
1745 | const outputPath = this.getVideoFilePath(updatedVideoFile) | ||
1746 | await copy(inputFilePath, outputPath) | ||
1747 | |||
1748 | await this.createTorrentAndSetInfoHash(updatedVideoFile) | ||
1749 | |||
1750 | await updatedVideoFile.save() | ||
1751 | |||
1752 | this.VideoFiles.push(updatedVideoFile) | ||
1753 | } | ||
1754 | |||
1755 | getOriginalFileResolution () { | 1413 | getOriginalFileResolution () { |
1756 | const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) | 1414 | const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) |
1757 | 1415 | ||
1758 | return getVideoFileResolution(originalFilePath) | 1416 | return getVideoFileResolution(originalFilePath) |
1759 | } | 1417 | } |
1760 | 1418 | ||
1761 | getDescriptionPath () { | 1419 | getDescriptionAPIPath () { |
1762 | return `/api/${API_VERSION}/videos/${this.uuid}/description` | 1420 | return `/api/${API_VERSION}/videos/${this.uuid}/description` |
1763 | } | 1421 | } |
1764 | 1422 | ||
@@ -1786,11 +1444,6 @@ export class VideoModel extends Model<VideoModel> { | |||
1786 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) | 1444 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) |
1787 | } | 1445 | } |
1788 | 1446 | ||
1789 | getActivityStreamDuration () { | ||
1790 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | ||
1791 | return 'PT' + this.duration + 'S' | ||
1792 | } | ||
1793 | |||
1794 | isOutdated () { | 1447 | isOutdated () { |
1795 | if (this.isOwned()) return false | 1448 | if (this.isOwned()) return false |
1796 | 1449 | ||