diff options
Diffstat (limited to 'server/lib')
28 files changed, 592 insertions, 205 deletions
diff --git a/server/lib/activitypub/actors/image.ts b/server/lib/activitypub/actors/image.ts index 443ad0a63..d17c2ef1a 100644 --- a/server/lib/activitypub/actors/image.ts +++ b/server/lib/activitypub/actors/image.ts | |||
@@ -12,53 +12,52 @@ type ImageInfo = { | |||
12 | onDisk?: boolean | 12 | onDisk?: boolean |
13 | } | 13 | } |
14 | 14 | ||
15 | async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { | 15 | async function updateActorImages (actor: MActorImages, type: ActorImageType, imagesInfo: ImageInfo[], t: Transaction) { |
16 | const oldImageModel = type === ActorImageType.AVATAR | 16 | const avatarsOrBanners = type === ActorImageType.AVATAR |
17 | ? actor.Avatar | 17 | ? actor.Avatars |
18 | : actor.Banner | 18 | : actor.Banners |
19 | 19 | ||
20 | if (oldImageModel) { | 20 | if (imagesInfo.length === 0) { |
21 | // Don't update the avatar if the file URL did not change | 21 | await deleteActorImages(actor, type, t) |
22 | if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor | 22 | } |
23 | |||
24 | for (const imageInfo of imagesInfo) { | ||
25 | const oldImageModel = (avatarsOrBanners || []).find(i => i.width === imageInfo.width) | ||
23 | 26 | ||
24 | try { | 27 | if (oldImageModel) { |
25 | await oldImageModel.destroy({ transaction: t }) | 28 | // Don't update the avatar if the file URL did not change |
29 | if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) { | ||
30 | continue | ||
31 | } | ||
26 | 32 | ||
27 | setActorImage(actor, type, null) | 33 | await safeDeleteActorImage(actor, oldImageModel, type, t) |
28 | } catch (err) { | ||
29 | logger.error('Cannot remove old actor image of actor %s.', actor.url, { err }) | ||
30 | } | 34 | } |
31 | } | ||
32 | 35 | ||
33 | if (imageInfo) { | ||
34 | const imageModel = await ActorImageModel.create({ | 36 | const imageModel = await ActorImageModel.create({ |
35 | filename: imageInfo.name, | 37 | filename: imageInfo.name, |
36 | onDisk: imageInfo.onDisk ?? false, | 38 | onDisk: imageInfo.onDisk ?? false, |
37 | fileUrl: imageInfo.fileUrl, | 39 | fileUrl: imageInfo.fileUrl, |
38 | height: imageInfo.height, | 40 | height: imageInfo.height, |
39 | width: imageInfo.width, | 41 | width: imageInfo.width, |
40 | type | 42 | type, |
43 | actorId: actor.id | ||
41 | }, { transaction: t }) | 44 | }, { transaction: t }) |
42 | 45 | ||
43 | setActorImage(actor, type, imageModel) | 46 | addActorImage(actor, type, imageModel) |
44 | } | 47 | } |
45 | 48 | ||
46 | return actor | 49 | return actor |
47 | } | 50 | } |
48 | 51 | ||
49 | async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) { | 52 | async function deleteActorImages (actor: MActorImages, type: ActorImageType, t: Transaction) { |
50 | try { | 53 | try { |
51 | if (type === ActorImageType.AVATAR) { | 54 | const association = buildAssociationName(type) |
52 | await actor.Avatar.destroy({ transaction: t }) | ||
53 | |||
54 | actor.avatarId = null | ||
55 | actor.Avatar = null | ||
56 | } else { | ||
57 | await actor.Banner.destroy({ transaction: t }) | ||
58 | 55 | ||
59 | actor.bannerId = null | 56 | for (const image of actor[association]) { |
60 | actor.Banner = null | 57 | await image.destroy({ transaction: t }) |
61 | } | 58 | } |
59 | |||
60 | actor[association] = [] | ||
62 | } catch (err) { | 61 | } catch (err) { |
63 | logger.error('Cannot remove old image of actor %s.', actor.url, { err }) | 62 | logger.error('Cannot remove old image of actor %s.', actor.url, { err }) |
64 | } | 63 | } |
@@ -66,29 +65,37 @@ async function deleteActorImageInstance (actor: MActorImages, type: ActorImageTy | |||
66 | return actor | 65 | return actor |
67 | } | 66 | } |
68 | 67 | ||
68 | async function safeDeleteActorImage (actor: MActorImages, toDelete: MActorImage, type: ActorImageType, t: Transaction) { | ||
69 | try { | ||
70 | await toDelete.destroy({ transaction: t }) | ||
71 | |||
72 | const association = buildAssociationName(type) | ||
73 | actor[association] = actor[association].filter(image => image.id !== toDelete.id) | ||
74 | } catch (err) { | ||
75 | logger.error('Cannot remove old actor image of actor %s.', actor.url, { err }) | ||
76 | } | ||
77 | } | ||
78 | |||
69 | // --------------------------------------------------------------------------- | 79 | // --------------------------------------------------------------------------- |
70 | 80 | ||
71 | export { | 81 | export { |
72 | ImageInfo, | 82 | ImageInfo, |
73 | 83 | ||
74 | updateActorImageInstance, | 84 | updateActorImages, |
75 | deleteActorImageInstance | 85 | deleteActorImages |
76 | } | 86 | } |
77 | 87 | ||
78 | // --------------------------------------------------------------------------- | 88 | // --------------------------------------------------------------------------- |
79 | 89 | ||
80 | function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) { | 90 | function addActorImage (actor: MActorImages, type: ActorImageType, imageModel: MActorImage) { |
81 | const id = imageModel | 91 | const association = buildAssociationName(type) |
82 | ? imageModel.id | 92 | if (!actor[association]) actor[association] = [] |
83 | : null | 93 | |
84 | 94 | actor[association].push(imageModel) | |
85 | if (type === ActorImageType.AVATAR) { | 95 | } |
86 | actorModel.avatarId = id | ||
87 | actorModel.Avatar = imageModel | ||
88 | } else { | ||
89 | actorModel.bannerId = id | ||
90 | actorModel.Banner = imageModel | ||
91 | } | ||
92 | 96 | ||
93 | return actorModel | 97 | function buildAssociationName (type: ActorImageType) { |
98 | return type === ActorImageType.AVATAR | ||
99 | ? 'Avatars' | ||
100 | : 'Banners' | ||
94 | } | 101 | } |
diff --git a/server/lib/activitypub/actors/shared/creator.ts b/server/lib/activitypub/actors/shared/creator.ts index 999aed97d..500bc9912 100644 --- a/server/lib/activitypub/actors/shared/creator.ts +++ b/server/lib/activitypub/actors/shared/creator.ts | |||
@@ -6,8 +6,8 @@ import { ServerModel } from '@server/models/server/server' | |||
6 | import { VideoChannelModel } from '@server/models/video/video-channel' | 6 | import { VideoChannelModel } from '@server/models/video/video-channel' |
7 | import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models' | 7 | import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models' |
8 | import { ActivityPubActor, ActorImageType } from '@shared/models' | 8 | import { ActivityPubActor, ActorImageType } from '@shared/models' |
9 | import { updateActorImageInstance } from '../image' | 9 | import { updateActorImages } from '../image' |
10 | import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImageInfoFromObject } from './object-to-model-attributes' | 10 | import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImagesInfoFromObject } from './object-to-model-attributes' |
11 | import { fetchActorFollowsCount } from './url-to-object' | 11 | import { fetchActorFollowsCount } from './url-to-object' |
12 | 12 | ||
13 | export class APActorCreator { | 13 | export class APActorCreator { |
@@ -27,11 +27,11 @@ export class APActorCreator { | |||
27 | return sequelizeTypescript.transaction(async t => { | 27 | return sequelizeTypescript.transaction(async t => { |
28 | const server = await this.setServer(actorInstance, t) | 28 | const server = await this.setServer(actorInstance, t) |
29 | 29 | ||
30 | await this.setImageIfNeeded(actorInstance, ActorImageType.AVATAR, t) | ||
31 | await this.setImageIfNeeded(actorInstance, ActorImageType.BANNER, t) | ||
32 | |||
33 | const { actorCreated, created } = await this.saveActor(actorInstance, t) | 30 | const { actorCreated, created } = await this.saveActor(actorInstance, t) |
34 | 31 | ||
32 | await this.setImageIfNeeded(actorCreated, ActorImageType.AVATAR, t) | ||
33 | await this.setImageIfNeeded(actorCreated, ActorImageType.BANNER, t) | ||
34 | |||
35 | await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t) | 35 | await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t) |
36 | 36 | ||
37 | if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance | 37 | if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance |
@@ -71,10 +71,10 @@ export class APActorCreator { | |||
71 | } | 71 | } |
72 | 72 | ||
73 | private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) { | 73 | private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) { |
74 | const imageInfo = getImageInfoFromObject(this.actorObject, type) | 74 | const imagesInfo = getImagesInfoFromObject(this.actorObject, type) |
75 | if (!imageInfo) return | 75 | if (imagesInfo.length === 0) return |
76 | 76 | ||
77 | return updateActorImageInstance(actor as MActorImages, type, imageInfo, t) | 77 | return updateActorImages(actor as MActorImages, type, imagesInfo, t) |
78 | } | 78 | } |
79 | 79 | ||
80 | private async saveActor (actor: MActor, t: Transaction) { | 80 | private async saveActor (actor: MActor, t: Transaction) { |
diff --git a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts index 23bc972e5..f6a78c457 100644 --- a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts +++ b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts | |||
@@ -4,7 +4,7 @@ import { ActorModel } from '@server/models/actor/actor' | |||
4 | import { FilteredModelAttributes } from '@server/types' | 4 | import { FilteredModelAttributes } from '@server/types' |
5 | import { getLowercaseExtension } from '@shared/core-utils' | 5 | import { getLowercaseExtension } from '@shared/core-utils' |
6 | import { buildUUID } from '@shared/extra-utils' | 6 | import { buildUUID } from '@shared/extra-utils' |
7 | import { ActivityPubActor, ActorImageType } from '@shared/models' | 7 | import { ActivityIconObject, ActivityPubActor, ActorImageType } from '@shared/models' |
8 | 8 | ||
9 | function getActorAttributesFromObject ( | 9 | function getActorAttributesFromObject ( |
10 | actorObject: ActivityPubActor, | 10 | actorObject: ActivityPubActor, |
@@ -30,33 +30,36 @@ function getActorAttributesFromObject ( | |||
30 | } | 30 | } |
31 | } | 31 | } |
32 | 32 | ||
33 | function getImageInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) { | 33 | function getImagesInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) { |
34 | const mimetypes = MIMETYPES.IMAGE | 34 | const iconsOrImages = type === ActorImageType.AVATAR |
35 | const icon = type === ActorImageType.AVATAR | 35 | ? actorObject.icons || actorObject.icon |
36 | ? actorObject.icon | ||
37 | : actorObject.image | 36 | : actorObject.image |
38 | 37 | ||
39 | if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined | 38 | return normalizeIconOrImage(iconsOrImages).map(iconOrImage => { |
39 | const mimetypes = MIMETYPES.IMAGE | ||
40 | 40 | ||
41 | let extension: string | 41 | if (iconOrImage.type !== 'Image' || !isActivityPubUrlValid(iconOrImage.url)) return undefined |
42 | 42 | ||
43 | if (icon.mediaType) { | 43 | let extension: string |
44 | extension = mimetypes.MIMETYPE_EXT[icon.mediaType] | ||
45 | } else { | ||
46 | const tmp = getLowercaseExtension(icon.url) | ||
47 | 44 | ||
48 | if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp | 45 | if (iconOrImage.mediaType) { |
49 | } | 46 | extension = mimetypes.MIMETYPE_EXT[iconOrImage.mediaType] |
47 | } else { | ||
48 | const tmp = getLowercaseExtension(iconOrImage.url) | ||
50 | 49 | ||
51 | if (!extension) return undefined | 50 | if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp |
51 | } | ||
52 | 52 | ||
53 | return { | 53 | if (!extension) return undefined |
54 | name: buildUUID() + extension, | 54 | |
55 | fileUrl: icon.url, | 55 | return { |
56 | height: icon.height, | 56 | name: buildUUID() + extension, |
57 | width: icon.width, | 57 | fileUrl: iconOrImage.url, |
58 | type | 58 | height: iconOrImage.height, |
59 | } | 59 | width: iconOrImage.width, |
60 | type | ||
61 | } | ||
62 | }) | ||
60 | } | 63 | } |
61 | 64 | ||
62 | function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { | 65 | function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { |
@@ -65,6 +68,15 @@ function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { | |||
65 | 68 | ||
66 | export { | 69 | export { |
67 | getActorAttributesFromObject, | 70 | getActorAttributesFromObject, |
68 | getImageInfoFromObject, | 71 | getImagesInfoFromObject, |
69 | getActorDisplayNameFromObject | 72 | getActorDisplayNameFromObject |
70 | } | 73 | } |
74 | |||
75 | // --------------------------------------------------------------------------- | ||
76 | |||
77 | function normalizeIconOrImage (icon: ActivityIconObject | ActivityIconObject[]): ActivityIconObject[] { | ||
78 | if (Array.isArray(icon)) return icon | ||
79 | if (icon) return [ icon ] | ||
80 | |||
81 | return [] | ||
82 | } | ||
diff --git a/server/lib/activitypub/actors/updater.ts b/server/lib/activitypub/actors/updater.ts index 042438d9c..fe94af9f1 100644 --- a/server/lib/activitypub/actors/updater.ts +++ b/server/lib/activitypub/actors/updater.ts | |||
@@ -5,9 +5,9 @@ import { VideoChannelModel } from '@server/models/video/video-channel' | |||
5 | import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models' | 5 | import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models' |
6 | import { ActivityPubActor, ActorImageType } from '@shared/models' | 6 | import { ActivityPubActor, ActorImageType } from '@shared/models' |
7 | import { getOrCreateAPOwner } from './get' | 7 | import { getOrCreateAPOwner } from './get' |
8 | import { updateActorImageInstance } from './image' | 8 | import { updateActorImages } from './image' |
9 | import { fetchActorFollowsCount } from './shared' | 9 | import { fetchActorFollowsCount } from './shared' |
10 | import { getImageInfoFromObject } from './shared/object-to-model-attributes' | 10 | import { getImagesInfoFromObject } from './shared/object-to-model-attributes' |
11 | 11 | ||
12 | export class APActorUpdater { | 12 | export class APActorUpdater { |
13 | 13 | ||
@@ -29,8 +29,8 @@ export class APActorUpdater { | |||
29 | } | 29 | } |
30 | 30 | ||
31 | async update () { | 31 | async update () { |
32 | const avatarInfo = getImageInfoFromObject(this.actorObject, ActorImageType.AVATAR) | 32 | const avatarsInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.AVATAR) |
33 | const bannerInfo = getImageInfoFromObject(this.actorObject, ActorImageType.BANNER) | 33 | const bannersInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.BANNER) |
34 | 34 | ||
35 | try { | 35 | try { |
36 | await this.updateActorInstance(this.actor, this.actorObject) | 36 | await this.updateActorInstance(this.actor, this.actorObject) |
@@ -47,8 +47,8 @@ export class APActorUpdater { | |||
47 | } | 47 | } |
48 | 48 | ||
49 | await runInReadCommittedTransaction(async t => { | 49 | await runInReadCommittedTransaction(async t => { |
50 | await updateActorImageInstance(this.actor, ActorImageType.AVATAR, avatarInfo, t) | 50 | await updateActorImages(this.actor, ActorImageType.BANNER, bannersInfo, t) |
51 | await updateActorImageInstance(this.actor, ActorImageType.BANNER, bannerInfo, t) | 51 | await updateActorImages(this.actor, ActorImageType.AVATAR, avatarsInfo, t) |
52 | }) | 52 | }) |
53 | 53 | ||
54 | await runInReadCommittedTransaction(async t => { | 54 | await runInReadCommittedTransaction(async t => { |
diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts new file mode 100644 index 000000000..e9bd148f6 --- /dev/null +++ b/server/lib/actor-image.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import maxBy from 'lodash/maxBy' | ||
2 | |||
3 | function getBiggestActorImage <T extends { width: number }> (images: T[]) { | ||
4 | const image = maxBy(images, 'width') | ||
5 | |||
6 | // If width is null, maxBy won't return a value | ||
7 | if (!image) return images[0] | ||
8 | |||
9 | return image | ||
10 | } | ||
11 | |||
12 | export { | ||
13 | getBiggestActorImage | ||
14 | } | ||
diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts index 5d68f44e9..910fdeec1 100644 --- a/server/lib/auth/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts | |||
@@ -5,14 +5,14 @@ import { ActorModel } from '@server/models/actor/actor' | |||
5 | import { MOAuthClient } from '@server/types/models' | 5 | import { MOAuthClient } from '@server/types/models' |
6 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 6 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
7 | import { MUser } from '@server/types/models/user/user' | 7 | import { MUser } from '@server/types/models/user/user' |
8 | import { UserAdminFlag } from '@shared/models/users/user-flag.model' | 8 | import { pick } from '@shared/core-utils' |
9 | import { UserRole } from '@shared/models/users/user-role' | 9 | import { UserRole } from '@shared/models/users/user-role' |
10 | import { logger } from '../../helpers/logger' | 10 | import { logger } from '../../helpers/logger' |
11 | import { CONFIG } from '../../initializers/config' | 11 | import { CONFIG } from '../../initializers/config' |
12 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | 12 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
13 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' | 13 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' |
14 | import { UserModel } from '../../models/user/user' | 14 | import { UserModel } from '../../models/user/user' |
15 | import { createUserAccountAndChannelAndPlaylist } from '../user' | 15 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' |
16 | import { TokensCache } from './tokens-cache' | 16 | import { TokensCache } from './tokens-cache' |
17 | 17 | ||
18 | type TokenInfo = { | 18 | type TokenInfo = { |
@@ -229,19 +229,13 @@ async function createUserFromExternal (pluginAuth: string, options: { | |||
229 | const actor = await ActorModel.loadLocalByName(options.username) | 229 | const actor = await ActorModel.loadLocalByName(options.username) |
230 | if (actor) return null | 230 | if (actor) return null |
231 | 231 | ||
232 | const userToCreate = new UserModel({ | 232 | const userToCreate = buildUser({ |
233 | username: options.username, | 233 | ...pick(options, [ 'username', 'email', 'role' ]), |
234 | |||
235 | emailVerified: null, | ||
234 | password: null, | 236 | password: null, |
235 | email: options.email, | ||
236 | nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
237 | p2pEnabled: CONFIG.DEFAULTS.P2P.WEBAPP.ENABLED, | ||
238 | autoPlayVideo: true, | ||
239 | role: options.role, | ||
240 | videoQuota: CONFIG.USER.VIDEO_QUOTA, | ||
241 | videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY, | ||
242 | adminFlags: UserAdminFlag.NONE, | ||
243 | pluginAuth | 237 | pluginAuth |
244 | }) as MUser | 238 | }) |
245 | 239 | ||
246 | const { user } = await createUserAccountAndChannelAndPlaylist({ | 240 | const { user } = await createUserAccountAndChannelAndPlaylist({ |
247 | userToCreate, | 241 | userToCreate, |
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 19354ab70..945bc712f 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts | |||
@@ -1,8 +1,10 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { readFile } from 'fs-extra' | 2 | import { readFile } from 'fs-extra' |
3 | import memoizee from 'memoizee' | ||
3 | import { join } from 'path' | 4 | import { join } from 'path' |
4 | import validator from 'validator' | 5 | import validator from 'validator' |
5 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' | 6 | import { toCompleteUUID } from '@server/helpers/custom-validators/misc' |
7 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
6 | import { root } from '@shared/core-utils' | 8 | import { root } from '@shared/core-utils' |
7 | import { escapeHTML } from '@shared/core-utils/renderer' | 9 | import { escapeHTML } from '@shared/core-utils/renderer' |
8 | import { sha256 } from '@shared/extra-utils' | 10 | import { sha256 } from '@shared/extra-utils' |
@@ -16,10 +18,11 @@ import { mdToOneLinePlainText } from '../helpers/markdown' | |||
16 | import { CONFIG } from '../initializers/config' | 18 | import { CONFIG } from '../initializers/config' |
17 | import { | 19 | import { |
18 | ACCEPT_HEADERS, | 20 | ACCEPT_HEADERS, |
19 | ACTOR_IMAGES_SIZE, | ||
20 | CUSTOM_HTML_TAG_COMMENTS, | 21 | CUSTOM_HTML_TAG_COMMENTS, |
21 | EMBED_SIZE, | 22 | EMBED_SIZE, |
22 | FILES_CONTENT_HASH, | 23 | FILES_CONTENT_HASH, |
24 | MEMOIZE_LENGTH, | ||
25 | MEMOIZE_TTL, | ||
23 | PLUGIN_GLOBAL_CSS_PATH, | 26 | PLUGIN_GLOBAL_CSS_PATH, |
24 | WEBSERVER | 27 | WEBSERVER |
25 | } from '../initializers/constants' | 28 | } from '../initializers/constants' |
@@ -29,8 +32,14 @@ import { VideoModel } from '../models/video/video' | |||
29 | import { VideoChannelModel } from '../models/video/video-channel' | 32 | import { VideoChannelModel } from '../models/video/video-channel' |
30 | import { VideoPlaylistModel } from '../models/video/video-playlist' | 33 | import { VideoPlaylistModel } from '../models/video/video-playlist' |
31 | import { MAccountActor, MChannelActor } from '../types/models' | 34 | import { MAccountActor, MChannelActor } from '../types/models' |
35 | import { getBiggestActorImage } from './actor-image' | ||
32 | import { ServerConfigManager } from './server-config-manager' | 36 | import { ServerConfigManager } from './server-config-manager' |
33 | 37 | ||
38 | const getPlainTextDescriptionCached = memoizee(mdToOneLinePlainText, { | ||
39 | maxAge: MEMOIZE_TTL.MD_TO_PLAIN_TEXT_CLIENT_HTML, | ||
40 | max: MEMOIZE_LENGTH.MD_TO_PLAIN_TEXT_CLIENT_HTML | ||
41 | }) | ||
42 | |||
34 | type Tags = { | 43 | type Tags = { |
35 | ogType: string | 44 | ogType: string |
36 | twitterCard: 'player' | 'summary' | 'summary_large_image' | 45 | twitterCard: 'player' | 'summary' | 'summary_large_image' |
@@ -103,7 +112,7 @@ class ClientHtml { | |||
103 | res.status(HttpStatusCode.NOT_FOUND_404) | 112 | res.status(HttpStatusCode.NOT_FOUND_404) |
104 | return html | 113 | return html |
105 | } | 114 | } |
106 | const description = mdToOneLinePlainText(video.description) | 115 | const description = getPlainTextDescriptionCached(video.description) |
107 | 116 | ||
108 | let customHtml = ClientHtml.addTitleTag(html, video.name) | 117 | let customHtml = ClientHtml.addTitleTag(html, video.name) |
109 | customHtml = ClientHtml.addDescriptionTag(customHtml, description) | 118 | customHtml = ClientHtml.addDescriptionTag(customHtml, description) |
@@ -164,7 +173,7 @@ class ClientHtml { | |||
164 | return html | 173 | return html |
165 | } | 174 | } |
166 | 175 | ||
167 | const description = mdToOneLinePlainText(videoPlaylist.description) | 176 | const description = getPlainTextDescriptionCached(videoPlaylist.description) |
168 | 177 | ||
169 | let customHtml = ClientHtml.addTitleTag(html, videoPlaylist.name) | 178 | let customHtml = ClientHtml.addTitleTag(html, videoPlaylist.name) |
170 | customHtml = ClientHtml.addDescriptionTag(customHtml, description) | 179 | customHtml = ClientHtml.addDescriptionTag(customHtml, description) |
@@ -263,7 +272,7 @@ class ClientHtml { | |||
263 | return ClientHtml.getIndexHTML(req, res) | 272 | return ClientHtml.getIndexHTML(req, res) |
264 | } | 273 | } |
265 | 274 | ||
266 | const description = mdToOneLinePlainText(entity.description) | 275 | const description = getPlainTextDescriptionCached(entity.description) |
267 | 276 | ||
268 | let customHtml = ClientHtml.addTitleTag(html, entity.getDisplayName()) | 277 | let customHtml = ClientHtml.addTitleTag(html, entity.getDisplayName()) |
269 | customHtml = ClientHtml.addDescriptionTag(customHtml, description) | 278 | customHtml = ClientHtml.addDescriptionTag(customHtml, description) |
@@ -273,10 +282,11 @@ class ClientHtml { | |||
273 | const siteName = CONFIG.INSTANCE.NAME | 282 | const siteName = CONFIG.INSTANCE.NAME |
274 | const title = entity.getDisplayName() | 283 | const title = entity.getDisplayName() |
275 | 284 | ||
285 | const avatar = getBiggestActorImage(entity.Actor.Avatars) | ||
276 | const image = { | 286 | const image = { |
277 | url: entity.Actor.getAvatarUrl(), | 287 | url: ActorImageModel.getImageUrl(avatar), |
278 | width: ACTOR_IMAGES_SIZE.AVATARS.width, | 288 | width: avatar?.width, |
279 | height: ACTOR_IMAGES_SIZE.AVATARS.height | 289 | height: avatar?.height |
280 | } | 290 | } |
281 | 291 | ||
282 | const ogType = 'website' | 292 | const ogType = 'website' |
diff --git a/server/lib/hls.ts b/server/lib/hls.ts index 985f50587..43043315b 100644 --- a/server/lib/hls.ts +++ b/server/lib/hls.ts | |||
@@ -4,7 +4,7 @@ import { basename, dirname, join } from 'path' | |||
4 | import { MStreamingPlaylistFilesVideo, MVideo, MVideoUUID } from '@server/types/models' | 4 | import { MStreamingPlaylistFilesVideo, MVideo, MVideoUUID } from '@server/types/models' |
5 | import { sha256 } from '@shared/extra-utils' | 5 | import { sha256 } from '@shared/extra-utils' |
6 | import { VideoStorage } from '@shared/models' | 6 | import { VideoStorage } from '@shared/models' |
7 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils' | 7 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamDimensionsInfo } from '../helpers/ffmpeg' |
8 | import { logger } from '../helpers/logger' | 8 | import { logger } from '../helpers/logger' |
9 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' | 9 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' |
10 | import { generateRandomString } from '../helpers/utils' | 10 | import { generateRandomString } from '../helpers/utils' |
@@ -40,10 +40,10 @@ async function updateMasterHLSPlaylist (video: MVideo, playlist: MStreamingPlayl | |||
40 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) | 40 | const playlistFilename = getHlsResolutionPlaylistFilename(file.filename) |
41 | 41 | ||
42 | await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => { | 42 | await VideoPathManager.Instance.makeAvailableVideoFile(file.withVideoOrPlaylist(playlist), async videoFilePath => { |
43 | const size = await getVideoStreamSize(videoFilePath) | 43 | const size = await getVideoStreamDimensionsInfo(videoFilePath) |
44 | 44 | ||
45 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) | 45 | const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file) |
46 | const resolution = `RESOLUTION=${size.width}x${size.height}` | 46 | const resolution = `RESOLUTION=${size?.width || 0}x${size?.height || 0}` |
47 | 47 | ||
48 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` | 48 | let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}` |
49 | if (file.fps) line += ',FRAME-RATE=' + file.fps | 49 | if (file.fps) line += ',FRAME-RATE=' + file.fps |
diff --git a/server/lib/job-queue/handlers/video-edition.ts b/server/lib/job-queue/handlers/video-edition.ts new file mode 100644 index 000000000..c5ba0452f --- /dev/null +++ b/server/lib/job-queue/handlers/video-edition.ts | |||
@@ -0,0 +1,229 @@ | |||
1 | import { Job } from 'bull' | ||
2 | import { move, remove } from 'fs-extra' | ||
3 | import { join } from 'path' | ||
4 | import { addIntroOutro, addWatermark, cutVideo } from '@server/helpers/ffmpeg' | ||
5 | import { createTorrentAndSetInfoHashFromPath } from '@server/helpers/webtorrent' | ||
6 | import { CONFIG } from '@server/initializers/config' | ||
7 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | ||
8 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' | ||
9 | import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles' | ||
10 | import { isAbleToUploadVideo } from '@server/lib/user' | ||
11 | import { addMoveToObjectStorageJob, addOptimizeOrMergeAudioJob } from '@server/lib/video' | ||
12 | import { approximateIntroOutroAdditionalSize } from '@server/lib/video-editor' | ||
13 | import { VideoPathManager } from '@server/lib/video-path-manager' | ||
14 | import { buildNextVideoState } from '@server/lib/video-state' | ||
15 | import { UserModel } from '@server/models/user/user' | ||
16 | import { VideoModel } from '@server/models/video/video' | ||
17 | import { VideoFileModel } from '@server/models/video/video-file' | ||
18 | import { MVideo, MVideoFile, MVideoFullLight, MVideoId, MVideoWithAllFiles } from '@server/types/models' | ||
19 | import { getLowercaseExtension, pick } from '@shared/core-utils' | ||
20 | import { | ||
21 | buildFileMetadata, | ||
22 | buildUUID, | ||
23 | ffprobePromise, | ||
24 | getFileSize, | ||
25 | getVideoStreamDimensionsInfo, | ||
26 | getVideoStreamDuration, | ||
27 | getVideoStreamFPS | ||
28 | } from '@shared/extra-utils' | ||
29 | import { | ||
30 | VideoEditionPayload, | ||
31 | VideoEditionTaskPayload, | ||
32 | VideoEditorTask, | ||
33 | VideoEditorTaskCutPayload, | ||
34 | VideoEditorTaskIntroPayload, | ||
35 | VideoEditorTaskOutroPayload, | ||
36 | VideoEditorTaskWatermarkPayload, | ||
37 | VideoState | ||
38 | } from '@shared/models' | ||
39 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | ||
40 | |||
41 | const lTagsBase = loggerTagsFactory('video-edition') | ||
42 | |||
43 | async function processVideoEdition (job: Job) { | ||
44 | const payload = job.data as VideoEditionPayload | ||
45 | |||
46 | logger.info('Process video edition of %s in job %d.', payload.videoUUID, job.id) | ||
47 | |||
48 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID) | ||
49 | |||
50 | // No video, maybe deleted? | ||
51 | if (!video) { | ||
52 | logger.info('Can\'t process job %d, video does not exist.', job.id, lTagsBase(payload.videoUUID)) | ||
53 | return undefined | ||
54 | } | ||
55 | |||
56 | await checkUserQuotaOrThrow(video, payload) | ||
57 | |||
58 | const inputFile = video.getMaxQualityFile() | ||
59 | |||
60 | const editionResultPath = await VideoPathManager.Instance.makeAvailableVideoFile(inputFile, async originalFilePath => { | ||
61 | let tmpInputFilePath: string | ||
62 | let outputPath: string | ||
63 | |||
64 | for (const task of payload.tasks) { | ||
65 | const outputFilename = buildUUID() + inputFile.extname | ||
66 | outputPath = join(CONFIG.STORAGE.TMP_DIR, outputFilename) | ||
67 | |||
68 | await processTask({ | ||
69 | inputPath: tmpInputFilePath ?? originalFilePath, | ||
70 | video, | ||
71 | outputPath, | ||
72 | task | ||
73 | }) | ||
74 | |||
75 | if (tmpInputFilePath) await remove(tmpInputFilePath) | ||
76 | |||
77 | // For the next iteration | ||
78 | tmpInputFilePath = outputPath | ||
79 | } | ||
80 | |||
81 | return outputPath | ||
82 | }) | ||
83 | |||
84 | logger.info('Video edition ended for video %s.', video.uuid) | ||
85 | |||
86 | const newFile = await buildNewFile(video, editionResultPath) | ||
87 | |||
88 | const outputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, newFile) | ||
89 | await move(editionResultPath, outputPath) | ||
90 | |||
91 | await createTorrentAndSetInfoHashFromPath(video, newFile, outputPath) | ||
92 | |||
93 | await removeAllFiles(video, newFile) | ||
94 | |||
95 | await newFile.save() | ||
96 | |||
97 | video.state = buildNextVideoState() | ||
98 | video.duration = await getVideoStreamDuration(outputPath) | ||
99 | await video.save() | ||
100 | |||
101 | await federateVideoIfNeeded(video, false, undefined) | ||
102 | |||
103 | if (video.state === VideoState.TO_TRANSCODE) { | ||
104 | const user = await UserModel.loadByVideoId(video.id) | ||
105 | |||
106 | await addOptimizeOrMergeAudioJob(video, newFile, user, false) | ||
107 | } else if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) { | ||
108 | await addMoveToObjectStorageJob(video, false) | ||
109 | } | ||
110 | } | ||
111 | |||
112 | // --------------------------------------------------------------------------- | ||
113 | |||
114 | export { | ||
115 | processVideoEdition | ||
116 | } | ||
117 | |||
118 | // --------------------------------------------------------------------------- | ||
119 | |||
120 | type TaskProcessorOptions <T extends VideoEditionTaskPayload = VideoEditionTaskPayload> = { | ||
121 | inputPath: string | ||
122 | outputPath: string | ||
123 | video: MVideo | ||
124 | task: T | ||
125 | } | ||
126 | |||
127 | const taskProcessors: { [id in VideoEditorTask['name']]: (options: TaskProcessorOptions) => Promise<any> } = { | ||
128 | 'add-intro': processAddIntroOutro, | ||
129 | 'add-outro': processAddIntroOutro, | ||
130 | 'cut': processCut, | ||
131 | 'add-watermark': processAddWatermark | ||
132 | } | ||
133 | |||
134 | async function processTask (options: TaskProcessorOptions) { | ||
135 | const { video, task } = options | ||
136 | |||
137 | logger.info('Processing %s task for video %s.', task.name, video.uuid, { task }) | ||
138 | |||
139 | const processor = taskProcessors[options.task.name] | ||
140 | if (!process) throw new Error('Unknown task ' + task.name) | ||
141 | |||
142 | return processor(options) | ||
143 | } | ||
144 | |||
145 | function processAddIntroOutro (options: TaskProcessorOptions<VideoEditorTaskIntroPayload | VideoEditorTaskOutroPayload>) { | ||
146 | const { task } = options | ||
147 | |||
148 | return addIntroOutro({ | ||
149 | ...pick(options, [ 'inputPath', 'outputPath' ]), | ||
150 | |||
151 | introOutroPath: task.options.file, | ||
152 | type: task.name === 'add-intro' | ||
153 | ? 'intro' | ||
154 | : 'outro', | ||
155 | |||
156 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | ||
157 | profile: CONFIG.TRANSCODING.PROFILE | ||
158 | }) | ||
159 | } | ||
160 | |||
161 | function processCut (options: TaskProcessorOptions<VideoEditorTaskCutPayload>) { | ||
162 | const { task } = options | ||
163 | |||
164 | return cutVideo({ | ||
165 | ...pick(options, [ 'inputPath', 'outputPath' ]), | ||
166 | |||
167 | start: task.options.start, | ||
168 | end: task.options.end | ||
169 | }) | ||
170 | } | ||
171 | |||
172 | function processAddWatermark (options: TaskProcessorOptions<VideoEditorTaskWatermarkPayload>) { | ||
173 | const { task } = options | ||
174 | |||
175 | return addWatermark({ | ||
176 | ...pick(options, [ 'inputPath', 'outputPath' ]), | ||
177 | |||
178 | watermarkPath: task.options.file, | ||
179 | |||
180 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | ||
181 | profile: CONFIG.TRANSCODING.PROFILE | ||
182 | }) | ||
183 | } | ||
184 | |||
185 | async function buildNewFile (video: MVideoId, path: string) { | ||
186 | const videoFile = new VideoFileModel({ | ||
187 | extname: getLowercaseExtension(path), | ||
188 | size: await getFileSize(path), | ||
189 | metadata: await buildFileMetadata(path), | ||
190 | videoStreamingPlaylistId: null, | ||
191 | videoId: video.id | ||
192 | }) | ||
193 | |||
194 | const probe = await ffprobePromise(path) | ||
195 | |||
196 | videoFile.fps = await getVideoStreamFPS(path, probe) | ||
197 | videoFile.resolution = (await getVideoStreamDimensionsInfo(path, probe)).resolution | ||
198 | |||
199 | videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname) | ||
200 | |||
201 | return videoFile | ||
202 | } | ||
203 | |||
204 | async function removeAllFiles (video: MVideoWithAllFiles, webTorrentFileException: MVideoFile) { | ||
205 | const hls = video.getHLSPlaylist() | ||
206 | |||
207 | if (hls) { | ||
208 | await video.removeStreamingPlaylistFiles(hls) | ||
209 | await hls.destroy() | ||
210 | } | ||
211 | |||
212 | for (const file of video.VideoFiles) { | ||
213 | if (file.id === webTorrentFileException.id) continue | ||
214 | |||
215 | await video.removeWebTorrentFileAndTorrent(file) | ||
216 | await file.destroy() | ||
217 | } | ||
218 | } | ||
219 | |||
220 | async function checkUserQuotaOrThrow (video: MVideoFullLight, payload: VideoEditionPayload) { | ||
221 | const user = await UserModel.loadByVideoId(video.id) | ||
222 | |||
223 | const filePathFinder = (i: number) => (payload.tasks[i] as VideoEditorTaskIntroPayload | VideoEditorTaskOutroPayload).options.file | ||
224 | |||
225 | const additionalBytes = await approximateIntroOutroAdditionalSize(video, payload.tasks, filePathFinder) | ||
226 | if (await isAbleToUploadVideo(user.id, additionalBytes) === false) { | ||
227 | throw new Error('Quota exceeded for this user to edit the video') | ||
228 | } | ||
229 | } | ||
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 0d9e80cb8..6b2d60317 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -1,18 +1,18 @@ | |||
1 | import { Job } from 'bull' | 1 | import { Job } from 'bull' |
2 | import { copy, stat } from 'fs-extra' | 2 | import { copy, stat } from 'fs-extra' |
3 | import { getLowercaseExtension } from '@shared/core-utils' | ||
4 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 3 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
5 | import { CONFIG } from '@server/initializers/config' | 4 | import { CONFIG } from '@server/initializers/config' |
6 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' | 5 | import { federateVideoIfNeeded } from '@server/lib/activitypub/videos' |
7 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' | 6 | import { generateWebTorrentVideoFilename } from '@server/lib/paths' |
8 | import { addMoveToObjectStorageJob } from '@server/lib/video' | 7 | import { addMoveToObjectStorageJob } from '@server/lib/video' |
9 | import { VideoPathManager } from '@server/lib/video-path-manager' | 8 | import { VideoPathManager } from '@server/lib/video-path-manager' |
9 | import { VideoModel } from '@server/models/video/video' | ||
10 | import { VideoFileModel } from '@server/models/video/video-file' | ||
10 | import { MVideoFullLight } from '@server/types/models' | 11 | import { MVideoFullLight } from '@server/types/models' |
12 | import { getLowercaseExtension } from '@shared/core-utils' | ||
11 | import { VideoFileImportPayload, VideoStorage } from '@shared/models' | 13 | import { VideoFileImportPayload, VideoStorage } from '@shared/models' |
12 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' | 14 | import { getVideoStreamFPS, getVideoStreamDimensionsInfo } from '../../../helpers/ffmpeg' |
13 | import { logger } from '../../../helpers/logger' | 15 | import { logger } from '../../../helpers/logger' |
14 | import { VideoModel } from '../../../models/video/video' | ||
15 | import { VideoFileModel } from '../../../models/video/video-file' | ||
16 | 16 | ||
17 | async function processVideoFileImport (job: Job) { | 17 | async function processVideoFileImport (job: Job) { |
18 | const payload = job.data as VideoFileImportPayload | 18 | const payload = job.data as VideoFileImportPayload |
@@ -45,9 +45,9 @@ export { | |||
45 | // --------------------------------------------------------------------------- | 45 | // --------------------------------------------------------------------------- |
46 | 46 | ||
47 | async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { | 47 | async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { |
48 | const { resolution } = await getVideoFileResolution(inputFilePath) | 48 | const { resolution } = await getVideoStreamDimensionsInfo(inputFilePath) |
49 | const { size } = await stat(inputFilePath) | 49 | const { size } = await stat(inputFilePath) |
50 | const fps = await getVideoFileFPS(inputFilePath) | 50 | const fps = await getVideoStreamFPS(inputFilePath) |
51 | 51 | ||
52 | const fileExt = getLowercaseExtension(inputFilePath) | 52 | const fileExt = getLowercaseExtension(inputFilePath) |
53 | 53 | ||
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index b6e05d8f5..b3ca28c2f 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -25,7 +25,7 @@ import { | |||
25 | VideoResolution, | 25 | VideoResolution, |
26 | VideoState | 26 | VideoState |
27 | } from '@shared/models' | 27 | } from '@shared/models' |
28 | import { ffprobePromise, getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' | 28 | import { ffprobePromise, getVideoStreamDuration, getVideoStreamFPS, getVideoStreamDimensionsInfo } from '../../../helpers/ffmpeg' |
29 | import { logger } from '../../../helpers/logger' | 29 | import { logger } from '../../../helpers/logger' |
30 | import { getSecureTorrentName } from '../../../helpers/utils' | 30 | import { getSecureTorrentName } from '../../../helpers/utils' |
31 | import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' | 31 | import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' |
@@ -121,10 +121,10 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
121 | 121 | ||
122 | const { resolution } = await isAudioFile(tempVideoPath, probe) | 122 | const { resolution } = await isAudioFile(tempVideoPath, probe) |
123 | ? { resolution: VideoResolution.H_NOVIDEO } | 123 | ? { resolution: VideoResolution.H_NOVIDEO } |
124 | : await getVideoFileResolution(tempVideoPath) | 124 | : await getVideoStreamDimensionsInfo(tempVideoPath) |
125 | 125 | ||
126 | const fps = await getVideoFileFPS(tempVideoPath, probe) | 126 | const fps = await getVideoStreamFPS(tempVideoPath, probe) |
127 | const duration = await getDurationFromVideoFile(tempVideoPath, probe) | 127 | const duration = await getVideoStreamDuration(tempVideoPath, probe) |
128 | 128 | ||
129 | // Prepare video file object for creation in database | 129 | // Prepare video file object for creation in database |
130 | const fileExt = getLowercaseExtension(tempVideoPath) | 130 | const fileExt = getLowercaseExtension(tempVideoPath) |
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index a04cfa2c9..497f6612a 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { Job } from 'bull' | 1 | import { Job } from 'bull' |
2 | import { pathExists, readdir, remove } from 'fs-extra' | 2 | import { pathExists, readdir, remove } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { ffprobePromise, getAudioStream, getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | 4 | import { ffprobePromise, getAudioStream, getVideoStreamDuration, getVideoStreamDimensionsInfo } from '@server/helpers/ffmpeg' |
5 | import { VIDEO_LIVE } from '@server/initializers/constants' | 5 | import { VIDEO_LIVE } from '@server/initializers/constants' |
6 | import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server/lib/live' | 6 | import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server/lib/live' |
7 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveDirectory } from '@server/lib/paths' | 7 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveDirectory } from '@server/lib/paths' |
8 | import { generateVideoMiniature } from '@server/lib/thumbnail' | 8 | import { generateVideoMiniature } from '@server/lib/thumbnail' |
9 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding' | 9 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/transcoding' |
10 | import { VideoPathManager } from '@server/lib/video-path-manager' | 10 | import { VideoPathManager } from '@server/lib/video-path-manager' |
11 | import { moveToNextState } from '@server/lib/video-state' | 11 | import { moveToNextState } from '@server/lib/video-state' |
12 | import { VideoModel } from '@server/models/video/video' | 12 | import { VideoModel } from '@server/models/video/video' |
@@ -96,7 +96,7 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt | |||
96 | const probe = await ffprobePromise(concatenatedTsFilePath) | 96 | const probe = await ffprobePromise(concatenatedTsFilePath) |
97 | const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) | 97 | const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) |
98 | 98 | ||
99 | const { resolution, isPortraitMode } = await getVideoFileResolution(concatenatedTsFilePath, probe) | 99 | const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) |
100 | 100 | ||
101 | const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ | 101 | const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ |
102 | video: videoWithFiles, | 102 | video: videoWithFiles, |
@@ -107,7 +107,7 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt | |||
107 | }) | 107 | }) |
108 | 108 | ||
109 | if (!durationDone) { | 109 | if (!durationDone) { |
110 | videoWithFiles.duration = await getDurationFromVideoFile(outputPath) | 110 | videoWithFiles.duration = await getVideoStreamDuration(outputPath) |
111 | await videoWithFiles.save() | 111 | await videoWithFiles.save() |
112 | 112 | ||
113 | durationDone = true | 113 | durationDone = true |
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index 5540b791d..512979734 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Job } from 'bull' | 1 | import { Job } from 'bull' |
2 | import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils' | 2 | import { TranscodeVODOptionsType } from '@server/helpers/ffmpeg' |
3 | import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video' | 3 | import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video' |
4 | import { VideoPathManager } from '@server/lib/video-path-manager' | 4 | import { VideoPathManager } from '@server/lib/video-path-manager' |
5 | import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state' | 5 | import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state' |
@@ -16,7 +16,7 @@ import { | |||
16 | VideoTranscodingPayload | 16 | VideoTranscodingPayload |
17 | } from '@shared/models' | 17 | } from '@shared/models' |
18 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 18 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
19 | import { computeLowerResolutionsToTranscode } from '../../../helpers/ffprobe-utils' | 19 | import { computeLowerResolutionsToTranscode } from '../../../helpers/ffmpeg' |
20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
21 | import { CONFIG } from '../../../initializers/config' | 21 | import { CONFIG } from '../../../initializers/config' |
22 | import { VideoModel } from '../../../models/video/video' | 22 | import { VideoModel } from '../../../models/video/video' |
@@ -25,7 +25,7 @@ import { | |||
25 | mergeAudioVideofile, | 25 | mergeAudioVideofile, |
26 | optimizeOriginalVideofile, | 26 | optimizeOriginalVideofile, |
27 | transcodeNewWebTorrentResolution | 27 | transcodeNewWebTorrentResolution |
28 | } from '../../transcoding/video-transcoding' | 28 | } from '../../transcoding/transcoding' |
29 | 29 | ||
30 | type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<void> | 30 | type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<void> |
31 | 31 | ||
@@ -174,10 +174,10 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay | |||
174 | async function onVideoFirstWebTorrentTranscoding ( | 174 | async function onVideoFirstWebTorrentTranscoding ( |
175 | videoArg: MVideoWithFile, | 175 | videoArg: MVideoWithFile, |
176 | payload: OptimizeTranscodingPayload | MergeAudioTranscodingPayload, | 176 | payload: OptimizeTranscodingPayload | MergeAudioTranscodingPayload, |
177 | transcodeType: TranscodeOptionsType, | 177 | transcodeType: TranscodeVODOptionsType, |
178 | user: MUserId | 178 | user: MUserId |
179 | ) { | 179 | ) { |
180 | const { resolution, isPortraitMode, audioStream } = await videoArg.getMaxQualityFileInfo() | 180 | const { resolution, isPortraitMode, audioStream } = await videoArg.probeMaxQualityFile() |
181 | 181 | ||
182 | // Maybe the video changed in database, refresh it | 182 | // Maybe the video changed in database, refresh it |
183 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid) | 183 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid) |
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 22bd1f5d2..e10a3bab5 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -14,6 +14,7 @@ import { | |||
14 | JobType, | 14 | JobType, |
15 | MoveObjectStoragePayload, | 15 | MoveObjectStoragePayload, |
16 | RefreshPayload, | 16 | RefreshPayload, |
17 | VideoEditionPayload, | ||
17 | VideoFileImportPayload, | 18 | VideoFileImportPayload, |
18 | VideoImportPayload, | 19 | VideoImportPayload, |
19 | VideoLiveEndingPayload, | 20 | VideoLiveEndingPayload, |
@@ -31,6 +32,7 @@ import { refreshAPObject } from './handlers/activitypub-refresher' | |||
31 | import { processActorKeys } from './handlers/actor-keys' | 32 | import { processActorKeys } from './handlers/actor-keys' |
32 | import { processEmail } from './handlers/email' | 33 | import { processEmail } from './handlers/email' |
33 | import { processMoveToObjectStorage } from './handlers/move-to-object-storage' | 34 | import { processMoveToObjectStorage } from './handlers/move-to-object-storage' |
35 | import { processVideoEdition } from './handlers/video-edition' | ||
34 | import { processVideoFileImport } from './handlers/video-file-import' | 36 | import { processVideoFileImport } from './handlers/video-file-import' |
35 | import { processVideoImport } from './handlers/video-import' | 37 | import { processVideoImport } from './handlers/video-import' |
36 | import { processVideoLiveEnding } from './handlers/video-live-ending' | 38 | import { processVideoLiveEnding } from './handlers/video-live-ending' |
@@ -53,6 +55,7 @@ type CreateJobArgument = | |||
53 | { type: 'actor-keys', payload: ActorKeysPayload } | | 55 | { type: 'actor-keys', payload: ActorKeysPayload } | |
54 | { type: 'video-redundancy', payload: VideoRedundancyPayload } | | 56 | { type: 'video-redundancy', payload: VideoRedundancyPayload } | |
55 | { type: 'delete-resumable-upload-meta-file', payload: DeleteResumableUploadMetaFilePayload } | | 57 | { type: 'delete-resumable-upload-meta-file', payload: DeleteResumableUploadMetaFilePayload } | |
58 | { type: 'video-edition', payload: VideoEditionPayload } | | ||
56 | { type: 'move-to-object-storage', payload: MoveObjectStoragePayload } | 59 | { type: 'move-to-object-storage', payload: MoveObjectStoragePayload } |
57 | 60 | ||
58 | export type CreateJobOptions = { | 61 | export type CreateJobOptions = { |
@@ -75,7 +78,8 @@ const handlers: { [id in JobType]: (job: Job) => Promise<any> } = { | |||
75 | 'video-live-ending': processVideoLiveEnding, | 78 | 'video-live-ending': processVideoLiveEnding, |
76 | 'actor-keys': processActorKeys, | 79 | 'actor-keys': processActorKeys, |
77 | 'video-redundancy': processVideoRedundancy, | 80 | 'video-redundancy': processVideoRedundancy, |
78 | 'move-to-object-storage': processMoveToObjectStorage | 81 | 'move-to-object-storage': processMoveToObjectStorage, |
82 | 'video-edition': processVideoEdition | ||
79 | } | 83 | } |
80 | 84 | ||
81 | const jobTypes: JobType[] = [ | 85 | const jobTypes: JobType[] = [ |
@@ -93,7 +97,8 @@ const jobTypes: JobType[] = [ | |||
93 | 'video-redundancy', | 97 | 'video-redundancy', |
94 | 'actor-keys', | 98 | 'actor-keys', |
95 | 'video-live-ending', | 99 | 'video-live-ending', |
96 | 'move-to-object-storage' | 100 | 'move-to-object-storage', |
101 | 'video-edition' | ||
97 | ] | 102 | ] |
98 | 103 | ||
99 | class JobQueue { | 104 | class JobQueue { |
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index 33e49acc1..21c34a9a4 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts | |||
@@ -5,10 +5,10 @@ import { createServer as createServerTLS, Server as ServerTLS } from 'tls' | |||
5 | import { | 5 | import { |
6 | computeLowerResolutionsToTranscode, | 6 | computeLowerResolutionsToTranscode, |
7 | ffprobePromise, | 7 | ffprobePromise, |
8 | getVideoFileBitrate, | 8 | getVideoStreamBitrate, |
9 | getVideoFileFPS, | 9 | getVideoStreamFPS, |
10 | getVideoFileResolution | 10 | getVideoStreamDimensionsInfo |
11 | } from '@server/helpers/ffprobe-utils' | 11 | } from '@server/helpers/ffmpeg' |
12 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 12 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
13 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | 13 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' |
14 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE } from '@server/initializers/constants' | 14 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE } from '@server/initializers/constants' |
@@ -226,9 +226,9 @@ class LiveManager { | |||
226 | const probe = await ffprobePromise(inputUrl) | 226 | const probe = await ffprobePromise(inputUrl) |
227 | 227 | ||
228 | const [ { resolution, ratio }, fps, bitrate ] = await Promise.all([ | 228 | const [ { resolution, ratio }, fps, bitrate ] = await Promise.all([ |
229 | getVideoFileResolution(inputUrl, probe), | 229 | getVideoStreamDimensionsInfo(inputUrl, probe), |
230 | getVideoFileFPS(inputUrl, probe), | 230 | getVideoStreamFPS(inputUrl, probe), |
231 | getVideoFileBitrate(inputUrl, probe) | 231 | getVideoStreamBitrate(inputUrl, probe) |
232 | ]) | 232 | ]) |
233 | 233 | ||
234 | logger.info( | 234 | logger.info( |
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts index 22a47942a..f5f473039 100644 --- a/server/lib/live/shared/muxing-session.ts +++ b/server/lib/live/shared/muxing-session.ts | |||
@@ -5,14 +5,14 @@ import { FfmpegCommand } from 'fluent-ffmpeg' | |||
5 | import { appendFile, ensureDir, readFile, stat } from 'fs-extra' | 5 | import { appendFile, ensureDir, readFile, stat } from 'fs-extra' |
6 | import { basename, join } from 'path' | 6 | import { basename, join } from 'path' |
7 | import { EventEmitter } from 'stream' | 7 | import { EventEmitter } from 'stream' |
8 | import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg-utils' | 8 | import { getLiveMuxingCommand, getLiveTranscodingCommand } from '@server/helpers/ffmpeg' |
9 | import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger' | 9 | import { logger, loggerTagsFactory, LoggerTagsFn } from '@server/helpers/logger' |
10 | import { CONFIG } from '@server/initializers/config' | 10 | import { CONFIG } from '@server/initializers/config' |
11 | import { MEMOIZE_TTL, VIDEO_LIVE } from '@server/initializers/constants' | 11 | import { MEMOIZE_TTL, VIDEO_LIVE } from '@server/initializers/constants' |
12 | import { VideoFileModel } from '@server/models/video/video-file' | 12 | import { VideoFileModel } from '@server/models/video/video-file' |
13 | import { MStreamingPlaylistVideo, MUserId, MVideoLiveVideo } from '@server/types/models' | 13 | import { MStreamingPlaylistVideo, MUserId, MVideoLiveVideo } from '@server/types/models' |
14 | import { getLiveDirectory } from '../../paths' | 14 | import { getLiveDirectory } from '../../paths' |
15 | import { VideoTranscodingProfilesManager } from '../../transcoding/video-transcoding-profiles' | 15 | import { VideoTranscodingProfilesManager } from '../../transcoding/default-transcoding-profiles' |
16 | import { isAbleToUploadVideo } from '../../user' | 16 | import { isAbleToUploadVideo } from '../../user' |
17 | import { LiveQuotaStore } from '../live-quota-store' | 17 | import { LiveQuotaStore } from '../live-quota-store' |
18 | import { LiveSegmentShaStore } from '../live-segment-sha-store' | 18 | import { LiveSegmentShaStore } from '../live-segment-sha-store' |
diff --git a/server/lib/local-actor.ts b/server/lib/local-actor.ts index c6826759b..01046d017 100644 --- a/server/lib/local-actor.ts +++ b/server/lib/local-actor.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import 'multer' | ||
2 | import { queue } from 'async' | 1 | import { queue } from 'async' |
2 | import { remove } from 'fs-extra' | ||
3 | import LRUCache from 'lru-cache' | 3 | import LRUCache from 'lru-cache' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { ActorModel } from '@server/models/actor/actor' | 5 | import { ActorModel } from '@server/models/actor/actor' |
@@ -13,7 +13,7 @@ import { CONFIG } from '../initializers/config' | |||
13 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants' | 13 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY, WEBSERVER } from '../initializers/constants' |
14 | import { sequelizeTypescript } from '../initializers/database' | 14 | import { sequelizeTypescript } from '../initializers/database' |
15 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' | 15 | import { MAccountDefault, MActor, MChannelDefault } from '../types/models' |
16 | import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actors' | 16 | import { deleteActorImages, updateActorImages } from './activitypub/actors' |
17 | import { sendUpdateActor } from './activitypub/send' | 17 | import { sendUpdateActor } from './activitypub/send' |
18 | 18 | ||
19 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { | 19 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) { |
@@ -33,64 +33,69 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU | |||
33 | }) as MActor | 33 | }) as MActor |
34 | } | 34 | } |
35 | 35 | ||
36 | async function updateLocalActorImageFile ( | 36 | async function updateLocalActorImageFiles ( |
37 | accountOrChannel: MAccountDefault | MChannelDefault, | 37 | accountOrChannel: MAccountDefault | MChannelDefault, |
38 | imagePhysicalFile: Express.Multer.File, | 38 | imagePhysicalFile: Express.Multer.File, |
39 | type: ActorImageType | 39 | type: ActorImageType |
40 | ) { | 40 | ) { |
41 | const imageSize = type === ActorImageType.AVATAR | 41 | const processImageSize = async (imageSize: { width: number, height: number }) => { |
42 | ? ACTOR_IMAGES_SIZE.AVATARS | 42 | const extension = getLowercaseExtension(imagePhysicalFile.filename) |
43 | : ACTOR_IMAGES_SIZE.BANNERS | 43 | |
44 | 44 | const imageName = buildUUID() + extension | |
45 | const extension = getLowercaseExtension(imagePhysicalFile.filename) | 45 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) |
46 | 46 | await processImage(imagePhysicalFile.path, destination, imageSize, true) | |
47 | const imageName = buildUUID() + extension | 47 | |
48 | const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName) | 48 | return { |
49 | await processImage(imagePhysicalFile.path, destination, imageSize) | 49 | imageName, |
50 | 50 | imageSize | |
51 | return retryTransactionWrapper(() => { | 51 | } |
52 | return sequelizeTypescript.transaction(async t => { | 52 | } |
53 | const actorImageInfo = { | 53 | |
54 | name: imageName, | 54 | const processedImages = await Promise.all(ACTOR_IMAGES_SIZE[type].map(processImageSize)) |
55 | fileUrl: null, | 55 | await remove(imagePhysicalFile.path) |
56 | height: imageSize.height, | 56 | |
57 | width: imageSize.width, | 57 | return retryTransactionWrapper(() => sequelizeTypescript.transaction(async t => { |
58 | onDisk: true | 58 | const actorImagesInfo = processedImages.map(({ imageName, imageSize }) => ({ |
59 | } | 59 | name: imageName, |
60 | 60 | fileUrl: null, | |
61 | const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, type, actorImageInfo, t) | 61 | height: imageSize.height, |
62 | await updatedActor.save({ transaction: t }) | 62 | width: imageSize.width, |
63 | 63 | onDisk: true | |
64 | await sendUpdateActor(accountOrChannel, t) | 64 | })) |
65 | 65 | ||
66 | return type === ActorImageType.AVATAR | 66 | const updatedActor = await updateActorImages(accountOrChannel.Actor, type, actorImagesInfo, t) |
67 | ? updatedActor.Avatar | 67 | await updatedActor.save({ transaction: t }) |
68 | : updatedActor.Banner | 68 | |
69 | }) | 69 | await sendUpdateActor(accountOrChannel, t) |
70 | }) | 70 | |
71 | return type === ActorImageType.AVATAR | ||
72 | ? updatedActor.Avatars | ||
73 | : updatedActor.Banners | ||
74 | })) | ||
71 | } | 75 | } |
72 | 76 | ||
73 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { | 77 | async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) { |
74 | return retryTransactionWrapper(() => { | 78 | return retryTransactionWrapper(() => { |
75 | return sequelizeTypescript.transaction(async t => { | 79 | return sequelizeTypescript.transaction(async t => { |
76 | const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t) | 80 | const updatedActor = await deleteActorImages(accountOrChannel.Actor, type, t) |
77 | await updatedActor.save({ transaction: t }) | 81 | await updatedActor.save({ transaction: t }) |
78 | 82 | ||
79 | await sendUpdateActor(accountOrChannel, t) | 83 | await sendUpdateActor(accountOrChannel, t) |
80 | 84 | ||
81 | return updatedActor.Avatar | 85 | return updatedActor.Avatars |
82 | }) | 86 | }) |
83 | }) | 87 | }) |
84 | } | 88 | } |
85 | 89 | ||
86 | type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType } | 90 | type DownloadImageQueueTask = { |
91 | fileUrl: string | ||
92 | filename: string | ||
93 | type: ActorImageType | ||
94 | size: typeof ACTOR_IMAGES_SIZE[ActorImageType][0] | ||
95 | } | ||
87 | 96 | ||
88 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | 97 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { |
89 | const size = task.type === ActorImageType.AVATAR | 98 | downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, task.size) |
90 | ? ACTOR_IMAGES_SIZE.AVATARS | ||
91 | : ACTOR_IMAGES_SIZE.BANNERS | ||
92 | |||
93 | downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size) | ||
94 | .then(() => cb()) | 99 | .then(() => cb()) |
95 | .catch(err => cb(err)) | 100 | .catch(err => cb(err)) |
96 | }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) | 101 | }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) |
@@ -110,7 +115,7 @@ const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE. | |||
110 | 115 | ||
111 | export { | 116 | export { |
112 | actorImagePathUnsafeCache, | 117 | actorImagePathUnsafeCache, |
113 | updateLocalActorImageFile, | 118 | updateLocalActorImageFiles, |
114 | deleteLocalActorImageFile, | 119 | deleteLocalActorImageFile, |
115 | pushActorImageProcessInQueue, | 120 | pushActorImageProcessInQueue, |
116 | buildActorInstance | 121 | buildActorInstance |
diff --git a/server/lib/notifier/shared/comment/comment-mention.ts b/server/lib/notifier/shared/comment/comment-mention.ts index 765cbaad9..ecd1687b4 100644 --- a/server/lib/notifier/shared/comment/comment-mention.ts +++ b/server/lib/notifier/shared/comment/comment-mention.ts | |||
@@ -77,7 +77,7 @@ export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MU | |||
77 | userId: user.id, | 77 | userId: user.id, |
78 | commentId: this.payload.id | 78 | commentId: this.payload.id |
79 | }) | 79 | }) |
80 | notification.Comment = this.payload | 80 | notification.VideoComment = this.payload |
81 | 81 | ||
82 | return notification | 82 | return notification |
83 | } | 83 | } |
diff --git a/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts index b76fc15bf..757502703 100644 --- a/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts +++ b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts | |||
@@ -44,7 +44,7 @@ export class NewCommentForVideoOwner extends AbstractNotification <MCommentOwner | |||
44 | userId: user.id, | 44 | userId: user.id, |
45 | commentId: this.payload.id | 45 | commentId: this.payload.id |
46 | }) | 46 | }) |
47 | notification.Comment = this.payload | 47 | notification.VideoComment = this.payload |
48 | 48 | ||
49 | return notification | 49 | return notification |
50 | } | 50 | } |
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index 78e4a28ad..897271c0b 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { ffprobePromise } from '@server/helpers/ffprobe-utils' | 3 | import { ffprobePromise } from '@server/helpers/ffmpeg/ffprobe-utils' |
4 | import { buildLogger } from '@server/helpers/logger' | 4 | import { buildLogger } from '@server/helpers/logger' |
5 | import { CONFIG } from '@server/initializers/config' | 5 | import { CONFIG } from '@server/initializers/config' |
6 | import { WEBSERVER } from '@server/initializers/constants' | 6 | import { WEBSERVER } from '@server/initializers/constants' |
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts index d1756040a..f4d405676 100644 --- a/server/lib/plugins/register-helpers.ts +++ b/server/lib/plugins/register-helpers.ts | |||
@@ -21,7 +21,7 @@ import { | |||
21 | VideoPlaylistPrivacy, | 21 | VideoPlaylistPrivacy, |
22 | VideoPrivacy | 22 | VideoPrivacy |
23 | } from '@shared/models' | 23 | } from '@shared/models' |
24 | import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles' | 24 | import { VideoTranscodingProfilesManager } from '../transcoding/default-transcoding-profiles' |
25 | import { buildPluginHelpers } from './plugin-helpers-builder' | 25 | import { buildPluginHelpers } from './plugin-helpers-builder' |
26 | 26 | ||
27 | export class RegisterHelpers { | 27 | export class RegisterHelpers { |
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts index d97f21eb7..38512f384 100644 --- a/server/lib/server-config-manager.ts +++ b/server/lib/server-config-manager.ts | |||
@@ -8,7 +8,7 @@ import { HTMLServerConfig, RegisteredExternalAuthConfig, RegisteredIdAndPassAuth | |||
8 | import { Hooks } from './plugins/hooks' | 8 | import { Hooks } from './plugins/hooks' |
9 | import { PluginManager } from './plugins/plugin-manager' | 9 | import { PluginManager } from './plugins/plugin-manager' |
10 | import { getThemeOrDefault } from './plugins/theme-utils' | 10 | import { getThemeOrDefault } from './plugins/theme-utils' |
11 | import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles' | 11 | import { VideoTranscodingProfilesManager } from './transcoding/default-transcoding-profiles' |
12 | 12 | ||
13 | /** | 13 | /** |
14 | * | 14 | * |
@@ -151,6 +151,9 @@ class ServerConfigManager { | |||
151 | port: CONFIG.LIVE.RTMP.PORT | 151 | port: CONFIG.LIVE.RTMP.PORT |
152 | } | 152 | } |
153 | }, | 153 | }, |
154 | videoEditor: { | ||
155 | enabled: CONFIG.VIDEO_EDITOR.ENABLED | ||
156 | }, | ||
154 | import: { | 157 | import: { |
155 | videos: { | 158 | videos: { |
156 | http: { | 159 | http: { |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts index 36270e5c1..aa2d7a813 100644 --- a/server/lib/thumbnail.ts +++ b/server/lib/thumbnail.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' | 2 | import { ThumbnailType } from '@shared/models' |
3 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' | 3 | import { generateImageFilename, generateImageFromVideoFile, processImage } from '../helpers/image-utils' |
4 | import { generateImageFilename, processImage } from '../helpers/image-utils' | ||
5 | import { downloadImage } from '../helpers/requests' | 4 | import { downloadImage } from '../helpers/requests' |
6 | import { CONFIG } from '../initializers/config' | 5 | import { CONFIG } from '../initializers/config' |
7 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' | 6 | import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' |
diff --git a/server/lib/transcoding/video-transcoding-profiles.ts b/server/lib/transcoding/default-transcoding-profiles.ts index dcc8d4c5c..ba98a11ca 100644 --- a/server/lib/transcoding/video-transcoding-profiles.ts +++ b/server/lib/transcoding/default-transcoding-profiles.ts | |||
@@ -2,8 +2,14 @@ | |||
2 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { getAverageBitrate, getMinLimitBitrate } from '@shared/core-utils' | 3 | import { getAverageBitrate, getMinLimitBitrate } from '@shared/core-utils' |
4 | import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptionsBuilderParams, VideoResolution } from '../../../shared/models/videos' | 4 | import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptionsBuilderParams, VideoResolution } from '../../../shared/models/videos' |
5 | import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils' | 5 | import { |
6 | import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '../../helpers/ffprobe-utils' | 6 | buildStreamSuffix, |
7 | canDoQuickAudioTranscode, | ||
8 | ffprobePromise, | ||
9 | getAudioStream, | ||
10 | getMaxAudioBitrate, | ||
11 | resetSupportedEncoders | ||
12 | } from '../../helpers/ffmpeg' | ||
7 | 13 | ||
8 | /** | 14 | /** |
9 | * | 15 | * |
@@ -15,8 +21,14 @@ import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBi | |||
15 | * * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate | 21 | * * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate |
16 | */ | 22 | */ |
17 | 23 | ||
24 | // --------------------------------------------------------------------------- | ||
25 | // Default builders | ||
26 | // --------------------------------------------------------------------------- | ||
27 | |||
18 | const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => { | 28 | const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => { |
19 | const { fps, inputRatio, inputBitrate, resolution } = options | 29 | const { fps, inputRatio, inputBitrate, resolution } = options |
30 | |||
31 | // TODO: remove in 4.2, fps is not optional anymore | ||
20 | if (!fps) return { outputOptions: [ ] } | 32 | if (!fps) return { outputOptions: [ ] } |
21 | 33 | ||
22 | const targetBitrate = getTargetBitrate({ inputBitrate, ratio: inputRatio, fps, resolution }) | 34 | const targetBitrate = getTargetBitrate({ inputBitrate, ratio: inputRatio, fps, resolution }) |
@@ -45,10 +57,10 @@ const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOp | |||
45 | } | 57 | } |
46 | } | 58 | } |
47 | 59 | ||
48 | const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum }) => { | 60 | const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum, canCopyAudio }) => { |
49 | const probe = await ffprobePromise(input) | 61 | const probe = await ffprobePromise(input) |
50 | 62 | ||
51 | if (await canDoQuickAudioTranscode(input, probe)) { | 63 | if (canCopyAudio && await canDoQuickAudioTranscode(input, probe)) { |
52 | logger.debug('Copy audio stream %s by AAC encoder.', input) | 64 | logger.debug('Copy audio stream %s by AAC encoder.', input) |
53 | return { copy: true, outputOptions: [ ] } | 65 | return { copy: true, outputOptions: [ ] } |
54 | } | 66 | } |
@@ -75,7 +87,10 @@ const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) | |||
75 | return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] } | 87 | return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] } |
76 | } | 88 | } |
77 | 89 | ||
78 | // Used to get and update available encoders | 90 | // --------------------------------------------------------------------------- |
91 | // Profile manager to get and change default profiles | ||
92 | // --------------------------------------------------------------------------- | ||
93 | |||
79 | class VideoTranscodingProfilesManager { | 94 | class VideoTranscodingProfilesManager { |
80 | private static instance: VideoTranscodingProfilesManager | 95 | private static instance: VideoTranscodingProfilesManager |
81 | 96 | ||
diff --git a/server/lib/transcoding/video-transcoding.ts b/server/lib/transcoding/transcoding.ts index 9942a067b..d55364e25 100644 --- a/server/lib/transcoding/video-transcoding.ts +++ b/server/lib/transcoding/transcoding.ts | |||
@@ -6,8 +6,15 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | |||
6 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models' | 6 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models' |
7 | import { VideoResolution, VideoStorage } from '../../../shared/models/videos' | 7 | import { VideoResolution, VideoStorage } from '../../../shared/models/videos' |
8 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 8 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
9 | import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils' | 9 | import { |
10 | import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' | 10 | canDoQuickTranscode, |
11 | getVideoStreamDuration, | ||
12 | buildFileMetadata, | ||
13 | getVideoStreamFPS, | ||
14 | transcodeVOD, | ||
15 | TranscodeVODOptions, | ||
16 | TranscodeVODOptionsType | ||
17 | } from '../../helpers/ffmpeg' | ||
11 | import { CONFIG } from '../../initializers/config' | 18 | import { CONFIG } from '../../initializers/config' |
12 | import { P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants' | 19 | import { P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants' |
13 | import { VideoFileModel } from '../../models/video/video-file' | 20 | import { VideoFileModel } from '../../models/video/video-file' |
@@ -21,7 +28,7 @@ import { | |||
21 | getHlsResolutionPlaylistFilename | 28 | getHlsResolutionPlaylistFilename |
22 | } from '../paths' | 29 | } from '../paths' |
23 | import { VideoPathManager } from '../video-path-manager' | 30 | import { VideoPathManager } from '../video-path-manager' |
24 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' | 31 | import { VideoTranscodingProfilesManager } from './default-transcoding-profiles' |
25 | 32 | ||
26 | /** | 33 | /** |
27 | * | 34 | * |
@@ -38,13 +45,13 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid | |||
38 | return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => { | 45 | return VideoPathManager.Instance.makeAvailableVideoFile(inputVideoFile.withVideoOrPlaylist(video), async videoInputPath => { |
39 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) | 46 | const videoTranscodedPath = join(transcodeDirectory, video.id + '-transcoded' + newExtname) |
40 | 47 | ||
41 | const transcodeType: TranscodeOptionsType = await canDoQuickTranscode(videoInputPath) | 48 | const transcodeType: TranscodeVODOptionsType = await canDoQuickTranscode(videoInputPath) |
42 | ? 'quick-transcode' | 49 | ? 'quick-transcode' |
43 | : 'video' | 50 | : 'video' |
44 | 51 | ||
45 | const resolution = toEven(inputVideoFile.resolution) | 52 | const resolution = toEven(inputVideoFile.resolution) |
46 | 53 | ||
47 | const transcodeOptions: TranscodeOptions = { | 54 | const transcodeOptions: TranscodeVODOptions = { |
48 | type: transcodeType, | 55 | type: transcodeType, |
49 | 56 | ||
50 | inputPath: videoInputPath, | 57 | inputPath: videoInputPath, |
@@ -59,7 +66,7 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid | |||
59 | } | 66 | } |
60 | 67 | ||
61 | // Could be very long! | 68 | // Could be very long! |
62 | await transcode(transcodeOptions) | 69 | await transcodeVOD(transcodeOptions) |
63 | 70 | ||
64 | // Important to do this before getVideoFilename() to take in account the new filename | 71 | // Important to do this before getVideoFilename() to take in account the new filename |
65 | inputVideoFile.extname = newExtname | 72 | inputVideoFile.extname = newExtname |
@@ -121,7 +128,7 @@ function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: V | |||
121 | job | 128 | job |
122 | } | 129 | } |
123 | 130 | ||
124 | await transcode(transcodeOptions) | 131 | await transcodeVOD(transcodeOptions) |
125 | 132 | ||
126 | return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) | 133 | return onWebTorrentVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) |
127 | }) | 134 | }) |
@@ -158,7 +165,7 @@ function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolutio | |||
158 | } | 165 | } |
159 | 166 | ||
160 | try { | 167 | try { |
161 | await transcode(transcodeOptions) | 168 | await transcodeVOD(transcodeOptions) |
162 | 169 | ||
163 | await remove(audioInputPath) | 170 | await remove(audioInputPath) |
164 | await remove(tmpPreviewPath) | 171 | await remove(tmpPreviewPath) |
@@ -175,7 +182,7 @@ function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolutio | |||
175 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) | 182 | const videoOutputPath = VideoPathManager.Instance.getFSVideoFileOutputPath(video, inputVideoFile) |
176 | // ffmpeg generated a new video file, so update the video duration | 183 | // ffmpeg generated a new video file, so update the video duration |
177 | // See https://trac.ffmpeg.org/ticket/5456 | 184 | // See https://trac.ffmpeg.org/ticket/5456 |
178 | video.duration = await getDurationFromVideoFile(videoTranscodedPath) | 185 | video.duration = await getVideoStreamDuration(videoTranscodedPath) |
179 | await video.save() | 186 | await video.save() |
180 | 187 | ||
181 | return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) | 188 | return onWebTorrentVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) |
@@ -239,8 +246,8 @@ async function onWebTorrentVideoFileTranscoding ( | |||
239 | outputPath: string | 246 | outputPath: string |
240 | ) { | 247 | ) { |
241 | const stats = await stat(transcodingPath) | 248 | const stats = await stat(transcodingPath) |
242 | const fps = await getVideoFileFPS(transcodingPath) | 249 | const fps = await getVideoStreamFPS(transcodingPath) |
243 | const metadata = await getMetadataFromFile(transcodingPath) | 250 | const metadata = await buildFileMetadata(transcodingPath) |
244 | 251 | ||
245 | await move(transcodingPath, outputPath, { overwrite: true }) | 252 | await move(transcodingPath, outputPath, { overwrite: true }) |
246 | 253 | ||
@@ -299,7 +306,7 @@ async function generateHlsPlaylistCommon (options: { | |||
299 | job | 306 | job |
300 | } | 307 | } |
301 | 308 | ||
302 | await transcode(transcodeOptions) | 309 | await transcodeVOD(transcodeOptions) |
303 | 310 | ||
304 | // Create or update the playlist | 311 | // Create or update the playlist |
305 | const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video) | 312 | const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video) |
@@ -344,8 +351,8 @@ async function generateHlsPlaylistCommon (options: { | |||
344 | const stats = await stat(videoFilePath) | 351 | const stats = await stat(videoFilePath) |
345 | 352 | ||
346 | newVideoFile.size = stats.size | 353 | newVideoFile.size = stats.size |
347 | newVideoFile.fps = await getVideoFileFPS(videoFilePath) | 354 | newVideoFile.fps = await getVideoStreamFPS(videoFilePath) |
348 | newVideoFile.metadata = await getMetadataFromFile(videoFilePath) | 355 | newVideoFile.metadata = await buildFileMetadata(videoFilePath) |
349 | 356 | ||
350 | await createTorrentAndSetInfoHash(playlist, newVideoFile) | 357 | await createTorrentAndSetInfoHash(playlist, newVideoFile) |
351 | 358 | ||
diff --git a/server/lib/user.ts b/server/lib/user.ts index 0d292ac90..ea755f4be 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -1,9 +1,11 @@ | |||
1 | import { Transaction } from 'sequelize/types' | 1 | import { Transaction } from 'sequelize/types' |
2 | import { logger } from '@server/helpers/logger' | ||
3 | import { CONFIG } from '@server/initializers/config' | ||
2 | import { UserModel } from '@server/models/user/user' | 4 | import { UserModel } from '@server/models/user/user' |
3 | import { MActorDefault } from '@server/types/models/actor' | 5 | import { MActorDefault } from '@server/types/models/actor' |
4 | import { buildUUID } from '@shared/extra-utils' | 6 | import { buildUUID } from '@shared/extra-utils' |
5 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 7 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
6 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' | 8 | import { UserAdminFlag, UserNotificationSetting, UserNotificationSettingValue, UserRole } from '../../shared/models/users' |
7 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' | 9 | import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' |
8 | import { sequelizeTypescript } from '../initializers/database' | 10 | import { sequelizeTypescript } from '../initializers/database' |
9 | import { AccountModel } from '../models/account/account' | 11 | import { AccountModel } from '../models/account/account' |
@@ -22,6 +24,53 @@ import { createWatchLaterPlaylist } from './video-playlist' | |||
22 | 24 | ||
23 | type ChannelNames = { name: string, displayName: string } | 25 | type ChannelNames = { name: string, displayName: string } |
24 | 26 | ||
27 | function buildUser (options: { | ||
28 | username: string | ||
29 | password: string | ||
30 | email: string | ||
31 | |||
32 | role?: UserRole // Default to UserRole.User | ||
33 | adminFlags?: UserAdminFlag // Default to UserAdminFlag.NONE | ||
34 | |||
35 | emailVerified: boolean | null | ||
36 | |||
37 | videoQuota?: number // Default to CONFIG.USER.VIDEO_QUOTA | ||
38 | videoQuotaDaily?: number // Default to CONFIG.USER.VIDEO_QUOTA_DAILY | ||
39 | |||
40 | pluginAuth?: string | ||
41 | }): MUser { | ||
42 | const { | ||
43 | username, | ||
44 | password, | ||
45 | email, | ||
46 | role = UserRole.USER, | ||
47 | emailVerified, | ||
48 | videoQuota = CONFIG.USER.VIDEO_QUOTA, | ||
49 | videoQuotaDaily = CONFIG.USER.VIDEO_QUOTA_DAILY, | ||
50 | adminFlags = UserAdminFlag.NONE, | ||
51 | pluginAuth | ||
52 | } = options | ||
53 | |||
54 | return new UserModel({ | ||
55 | username, | ||
56 | password, | ||
57 | email, | ||
58 | |||
59 | nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
60 | p2pEnabled: CONFIG.DEFAULTS.P2P.WEBAPP.ENABLED, | ||
61 | autoPlayVideo: true, | ||
62 | |||
63 | role, | ||
64 | emailVerified, | ||
65 | adminFlags, | ||
66 | |||
67 | videoQuota: videoQuota, | ||
68 | videoQuotaDaily: videoQuotaDaily, | ||
69 | |||
70 | pluginAuth | ||
71 | }) | ||
72 | } | ||
73 | |||
25 | async function createUserAccountAndChannelAndPlaylist (parameters: { | 74 | async function createUserAccountAndChannelAndPlaylist (parameters: { |
26 | userToCreate: MUser | 75 | userToCreate: MUser |
27 | userDisplayName?: string | 76 | userDisplayName?: string |
@@ -117,7 +166,7 @@ async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) { | |||
117 | const email = isPendingEmail ? user.pendingEmail : user.email | 166 | const email = isPendingEmail ? user.pendingEmail : user.email |
118 | const username = user.username | 167 | const username = user.username |
119 | 168 | ||
120 | await Emailer.Instance.addVerifyEmailJob(username, email, url) | 169 | Emailer.Instance.addVerifyEmailJob(username, email, url) |
121 | } | 170 | } |
122 | 171 | ||
123 | async function getOriginalVideoFileTotalFromUser (user: MUserId) { | 172 | async function getOriginalVideoFileTotalFromUser (user: MUserId) { |
@@ -159,6 +208,11 @@ async function isAbleToUploadVideo (userId: number, newVideoSize: number) { | |||
159 | const uploadedTotal = newVideoSize + totalBytes | 208 | const uploadedTotal = newVideoSize + totalBytes |
160 | const uploadedDaily = newVideoSize + totalBytesDaily | 209 | const uploadedDaily = newVideoSize + totalBytesDaily |
161 | 210 | ||
211 | logger.debug( | ||
212 | 'Check user %d quota to upload another video.', userId, | ||
213 | { totalBytes, totalBytesDaily, videoQuota: user.videoQuota, videoQuotaDaily: user.videoQuotaDaily, newVideoSize } | ||
214 | ) | ||
215 | |||
162 | if (user.videoQuotaDaily === -1) return uploadedTotal < user.videoQuota | 216 | if (user.videoQuotaDaily === -1) return uploadedTotal < user.videoQuota |
163 | if (user.videoQuota === -1) return uploadedDaily < user.videoQuotaDaily | 217 | if (user.videoQuota === -1) return uploadedDaily < user.videoQuotaDaily |
164 | 218 | ||
@@ -174,7 +228,8 @@ export { | |||
174 | createUserAccountAndChannelAndPlaylist, | 228 | createUserAccountAndChannelAndPlaylist, |
175 | createLocalAccountWithoutKeys, | 229 | createLocalAccountWithoutKeys, |
176 | sendVerifyUserEmail, | 230 | sendVerifyUserEmail, |
177 | isAbleToUploadVideo | 231 | isAbleToUploadVideo, |
232 | buildUser | ||
178 | } | 233 | } |
179 | 234 | ||
180 | // --------------------------------------------------------------------------- | 235 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/video-editor.ts b/server/lib/video-editor.ts new file mode 100644 index 000000000..99b0bd949 --- /dev/null +++ b/server/lib/video-editor.ts | |||
@@ -0,0 +1,32 @@ | |||
1 | import { MVideoFullLight } from "@server/types/models" | ||
2 | import { getVideoStreamDuration } from "@shared/extra-utils" | ||
3 | import { VideoEditorTask } from "@shared/models" | ||
4 | |||
5 | function buildTaskFileFieldname (indice: number, fieldName = 'file') { | ||
6 | return `tasks[${indice}][options][${fieldName}]` | ||
7 | } | ||
8 | |||
9 | function getTaskFile (files: Express.Multer.File[], indice: number, fieldName = 'file') { | ||
10 | return files.find(f => f.fieldname === buildTaskFileFieldname(indice, fieldName)) | ||
11 | } | ||
12 | |||
13 | async function approximateIntroOutroAdditionalSize (video: MVideoFullLight, tasks: VideoEditorTask[], fileFinder: (i: number) => string) { | ||
14 | let additionalDuration = 0 | ||
15 | |||
16 | for (let i = 0; i < tasks.length; i++) { | ||
17 | const task = tasks[i] | ||
18 | |||
19 | if (task.name !== 'add-intro' && task.name !== 'add-outro') continue | ||
20 | |||
21 | const filePath = fileFinder(i) | ||
22 | additionalDuration += await getVideoStreamDuration(filePath) | ||
23 | } | ||
24 | |||
25 | return (video.getMaxQualityFile().size / video.duration) * additionalDuration | ||
26 | } | ||
27 | |||
28 | export { | ||
29 | approximateIntroOutroAdditionalSize, | ||
30 | buildTaskFileFieldname, | ||
31 | getTaskFile | ||
32 | } | ||
diff --git a/server/lib/video.ts b/server/lib/video.ts index 2690f953d..ec4256c1a 100644 --- a/server/lib/video.ts +++ b/server/lib/video.ts | |||
@@ -81,7 +81,7 @@ async function setVideoTags (options: { | |||
81 | video.Tags = tagInstances | 81 | video.Tags = tagInstances |
82 | } | 82 | } |
83 | 83 | ||
84 | async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoFile, user: MUserId) { | 84 | async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoFile, user: MUserId, isNewVideo = true) { |
85 | let dataInput: VideoTranscodingPayload | 85 | let dataInput: VideoTranscodingPayload |
86 | 86 | ||
87 | if (videoFile.isAudio()) { | 87 | if (videoFile.isAudio()) { |
@@ -90,13 +90,13 @@ async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoF | |||
90 | resolution: DEFAULT_AUDIO_RESOLUTION, | 90 | resolution: DEFAULT_AUDIO_RESOLUTION, |
91 | videoUUID: video.uuid, | 91 | videoUUID: video.uuid, |
92 | createHLSIfNeeded: true, | 92 | createHLSIfNeeded: true, |
93 | isNewVideo: true | 93 | isNewVideo |
94 | } | 94 | } |
95 | } else { | 95 | } else { |
96 | dataInput = { | 96 | dataInput = { |
97 | type: 'optimize-to-webtorrent', | 97 | type: 'optimize-to-webtorrent', |
98 | videoUUID: video.uuid, | 98 | videoUUID: video.uuid, |
99 | isNewVideo: true | 99 | isNewVideo |
100 | } | 100 | } |
101 | } | 101 | } |
102 | 102 | ||