mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2026-09-03 20:53:09 -05:00
Merge branch 'release/v8.1.x' into develop
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
* Classic installation: `cd /var/www/peertube/peertube-latest && sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production node dist/scripts/migrations/peertube-8.1.js`
|
||||
* Docker installation: `cd /var/www/peertube-docker && docker compose exec -u peertube peertube node dist/scripts/migrations/peertube-8.1.js`
|
||||
* Running [regenerate-thumbnails](https://docs.joinpeertube.org/maintain/tools#regenerate-video-thumbnails) and [prune-storage](https://docs.joinpeertube.org/maintain/tools#prune-filesystem-object-storage) scripts after the upgrade and migration script is highly recommended
|
||||
* If you run PostgreSQL or Redis with TLS connection and self signed certificates, you must explicitly set `reject_unauthorized` to `true` or fill `ca`, `cert` and `key` settings in your [production.yaml](https://github.com/Chocobozzz/PeerTube/blob/develop/config/production.yaml.example#L84)
|
||||
|
||||
### Maintenance
|
||||
|
||||
|
||||
+9
-9
@@ -84,9 +84,9 @@ database:
|
||||
ssl: false
|
||||
ssl_settings:
|
||||
reject_unauthorized: false
|
||||
ca: '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: '/absolute/path/to/client-certificates/postgresql.crt'
|
||||
key: '/absolute/path/to/client-key/postgresql.key'
|
||||
ca: null # '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: null # '/absolute/path/to/client-certificates/postgresql.crt'
|
||||
key: null # '/absolute/path/to/client-key/postgresql.key'
|
||||
suffix: '_dev'
|
||||
username: 'peertube'
|
||||
password: 'peertube'
|
||||
@@ -104,18 +104,18 @@ redis:
|
||||
enable_tls: false
|
||||
tls_settings:
|
||||
reject_unauthorized: false
|
||||
ca: '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: '/absolute/path/to/client-certificates/postgresql.crt'
|
||||
key: '/absolute/path/to/client-key/postgresql.key'
|
||||
ca: null # '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: null # '/absolute/path/to/client-certificates/redis.crt'
|
||||
key: null # '/absolute/path/to/client-key/redis.key'
|
||||
|
||||
sentinel:
|
||||
enabled: false
|
||||
enable_tls: false
|
||||
tls_settings:
|
||||
reject_unauthorized: false
|
||||
ca: '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: '/absolute/path/to/client-certificates/postgresql.crt'
|
||||
key: '/absolute/path/to/client-key/postgresql.key'
|
||||
ca: null # '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: null # '/absolute/path/to/client-certificates/redis.crt'
|
||||
key: null # '/absolute/path/to/client-key/redis.key'
|
||||
master_name: ''
|
||||
password: ''
|
||||
sentinels:
|
||||
|
||||
@@ -82,9 +82,9 @@ database:
|
||||
ssl: false
|
||||
ssl_settings:
|
||||
reject_unauthorized: false
|
||||
ca: '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: '/absolute/path/to/client-certificates/postgresql.crt'
|
||||
key: '/absolute/path/to/client-key/postgresql.key'
|
||||
ca: null # '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: null # '/absolute/path/to/client-certificates/postgresql.crt'
|
||||
key: null # '/absolute/path/to/client-key/postgresql.key'
|
||||
suffix: '_prod'
|
||||
username: 'peertube'
|
||||
password: 'peertube'
|
||||
@@ -102,18 +102,18 @@ redis:
|
||||
enable_tls: false
|
||||
tls_settings:
|
||||
reject_unauthorized: false
|
||||
ca: '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: '/absolute/path/to/client-certificates/postgresql.crt'
|
||||
key: '/absolute/path/to/client-key/postgresql.key'
|
||||
ca: null # '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: null # '/absolute/path/to/client-certificates/redis.crt'
|
||||
key: null # '/absolute/path/to/client-key/redis.key'
|
||||
|
||||
sentinel:
|
||||
enabled: false
|
||||
enable_tls: false
|
||||
tls_settings:
|
||||
reject_unauthorized: false
|
||||
ca: '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: '/absolute/path/to/client-certificates/postgresql.crt'
|
||||
key: '/absolute/path/to/client-key/postgresql.key'
|
||||
ca: null # '/absolute/path/to/server-certificates/root.crt'
|
||||
cert: null # '/absolute/path/to/client-certificates/redis.crt'
|
||||
key: null # '/absolute/path/to/client-key/redis.key'
|
||||
master_name: ''
|
||||
password: ''
|
||||
sentinels:
|
||||
|
||||
@@ -149,8 +149,13 @@ describe('Test videos API validator', function () {
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with too many uuids', async function () {
|
||||
const customQuery = { ...query, uuids: new Array(101).fill('dfd70b83-639f-4980-94af-304a56ab4b35') }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with valid uuids', async function () {
|
||||
const customQuery = { ...query, uuids: [ 'dfd70b83-639f-4980-94af-304a56ab4b35' ] }
|
||||
const customQuery = { ...query, uuids: new Array(90).fill('dfd70b83-639f-4980-94af-304a56ab4b35') }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.OK_200 })
|
||||
})
|
||||
})
|
||||
@@ -184,8 +189,18 @@ describe('Test videos API validator', function () {
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with too many uuids', async function () {
|
||||
const customQuery = { ...query, uuids: new Array(101).fill('dfd70b83-639f-4980-94af-304a56ab4b35') }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: { ...query, uuids: new Array(90).fill('dfd70b83-639f-4980-94af-304a56ab4b35') },
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -217,8 +232,18 @@ describe('Test videos API validator', function () {
|
||||
await makeGetRequest({ url: server.url, path, query: { ...query, handles: [ '' ] }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with too many handles', async function () {
|
||||
const customQuery = { ...query, handles: new Array(101).fill('handle') }
|
||||
await makeGetRequest({ url: server.url, path, query: customQuery, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should succeed with the correct parameters', async function () {
|
||||
await makeGetRequest({ url: server.url, path, query, expectedStatus: HttpStatusCode.OK_200 })
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
query: { ...query, handles: new Array(90).fill('handle') },
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -808,6 +808,30 @@ describe('Test video playlists API validator', function () {
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with many handles', async function () {
|
||||
const handles = Array.from({ length: 90 }, (_, i) => i + 1)
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
query: { videoIds: handles },
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with too many handles', async function () {
|
||||
const handles = Array.from({ length: 101 }, (_, i) => i + 1)
|
||||
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
token: server.accessToken,
|
||||
path,
|
||||
query: { videoIds: handles },
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with the correct params', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
|
||||
@@ -479,6 +479,20 @@ describe('Test multiple servers', function () {
|
||||
let remoteVideosServer2 = []
|
||||
let remoteVideosServer3 = []
|
||||
|
||||
let fileUrls: string[] = []
|
||||
|
||||
async function grabFileUrls (videoUUID: string) {
|
||||
const video = await servers[0].videos.get({ id: videoUUID })
|
||||
const { storyboards } = await servers[0].storyboard.list({ id: videoUUID })
|
||||
|
||||
return [
|
||||
...video.files.map(f => f.fileUrl),
|
||||
...video.files.map(f => f.torrentUrl),
|
||||
...video.thumbnails.map(f => f.fileUrl),
|
||||
...storyboards.map(s => s.fileUrl)
|
||||
]
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
{
|
||||
const { data } = await servers[0].videos.list()
|
||||
@@ -495,6 +509,8 @@ describe('Test multiple servers', function () {
|
||||
localVideosServer3 = data.filter(video => video.isLocal === true).map(video => video.uuid)
|
||||
remoteVideosServer3 = data.filter(video => video.isLocal === false).map(video => video.uuid)
|
||||
}
|
||||
|
||||
fileUrls = await grabFileUrls(localVideosServer3[0])
|
||||
})
|
||||
|
||||
it('Should view multiple videos on owned servers', async function () {
|
||||
@@ -609,6 +625,12 @@ describe('Test multiple servers', function () {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not have updated torrent files/storyboards/thumbnails', async function () {
|
||||
const newFileUrls = await grabFileUrls(localVideosServer3[0])
|
||||
|
||||
expect(newFileUrls).to.have.members(fileUrls)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Should manipulate these videos', function () {
|
||||
|
||||
@@ -6,11 +6,10 @@ import { readFile } from 'fs/promises'
|
||||
import type { Instance as MagnetUriInstance } from 'magnet-uri'
|
||||
import type { ParseTorrent } from 'parse-torrent'
|
||||
import { basename, join } from 'path'
|
||||
import type { Torrent } from 'webtorrent'
|
||||
import WebTorrent from 'webtorrent'
|
||||
import type { Torrent, Instance, WebTorrent } from 'webtorrent'
|
||||
|
||||
export async function checkWebTorrentWorks (magnetUri: string, pathMatch?: RegExp) {
|
||||
let res: { webtorrent: WebTorrent.Instance, torrent: WebTorrent.Torrent }
|
||||
let res: { webtorrent: Instance, torrent: Torrent }
|
||||
|
||||
try {
|
||||
res = await webtorrentAdd(magnetUri)
|
||||
@@ -56,13 +55,14 @@ export async function magnetUriEncode (data: MagnetUriInstance) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function webtorrentAdd (torrentId: string) {
|
||||
const WebTorrent = (await import('webtorrent')).default
|
||||
const WebTorrent: WebTorrent = (await import('webtorrent')).default
|
||||
|
||||
const webtorrent = new WebTorrent({
|
||||
natUpnp: false,
|
||||
natPmp: false,
|
||||
utp: false,
|
||||
lsd: false
|
||||
lsd: false,
|
||||
dht: false
|
||||
} as any)
|
||||
|
||||
webtorrent.on('error', err => console.error('Error in webtorrent', err))
|
||||
|
||||
+3
-1
@@ -38,9 +38,11 @@ sed -i 's/"version": "\([^"]\+\)"/"version": "\1-'"$nightly_version"'"/' ./packa
|
||||
"$directory_name/LICENSE" "$directory_name/README.md" \
|
||||
"$directory_name/packages/core-utils/dist/" "$directory_name/packages/core-utils/package.json" \
|
||||
"$directory_name/packages/ffmpeg/dist/" "$directory_name/packages/ffmpeg/package.json" \
|
||||
"$directory_name/packages/node-utils/dist/" "$directory_name/packages/node-utils/package.json" \
|
||||
"$directory_name/packages/models/dist/" "$directory_name/packages/models/package.json" \
|
||||
"$directory_name/packages/node-utils/dist/" "$directory_name/packages/node-utils/package.json" \
|
||||
"$directory_name/packages/server-commands/dist/" "$directory_name/packages/server-commands/package.json" \
|
||||
"$directory_name/packages/transcription/dist/" "$directory_name/packages/transcription/package.json" \
|
||||
"$directory_name/packages/typescript-utils/dist/" "$directory_name/packages/typescript-utils/package.json" \
|
||||
"$directory_name/client/dist/" \
|
||||
"$directory_name/client/package.json" "$directory_name/config" \
|
||||
"$directory_name/dist" "$directory_name/package.json" \
|
||||
|
||||
+3
-1
@@ -85,9 +85,11 @@ find dist/ packages/core-utils/dist/ \
|
||||
"$directory_name/LICENSE" "$directory_name/README.md" \
|
||||
"$directory_name/packages/core-utils/dist/" "$directory_name/packages/core-utils/package.json" \
|
||||
"$directory_name/packages/ffmpeg/dist/" "$directory_name/packages/ffmpeg/package.json" \
|
||||
"$directory_name/packages/node-utils/dist/" "$directory_name/packages/node-utils/package.json" \
|
||||
"$directory_name/packages/models/dist/" "$directory_name/packages/models/package.json" \
|
||||
"$directory_name/packages/node-utils/dist/" "$directory_name/packages/node-utils/package.json" \
|
||||
"$directory_name/packages/server-commands/dist/" "$directory_name/packages/server-commands/package.json" \
|
||||
"$directory_name/packages/transcription/dist/" "$directory_name/packages/transcription/package.json" \
|
||||
"$directory_name/packages/typescript-utils/dist/" "$directory_name/packages/typescript-utils/package.json" \
|
||||
"$directory_name/client/dist/" \
|
||||
"$directory_name/client/package.json" "$directory_name/config" \
|
||||
"$directory_name/dist" "$directory_name/package.json" \
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { VideoPlaylistForAccountListQuery } from '@peertube/peertube-models'
|
||||
import { pickCommonVideoQuery } from '@server/helpers/query.js'
|
||||
import { scheduleActorRefreshIfNeeded } from '@server/lib/activitypub/actors/refresh.js'
|
||||
import { ActorFollowModel } from '@server/models/actor/actor-follow.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
|
||||
import express from 'express'
|
||||
import { buildNSFWFilters, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils.js'
|
||||
import { getFormattedObjects } from '../../helpers/utils.js'
|
||||
import { JobQueue } from '../../lib/job-queue/index.js'
|
||||
import { Hooks } from '../../lib/plugins/hooks.js'
|
||||
import {
|
||||
apiRateLimiter,
|
||||
@@ -146,9 +146,7 @@ export {
|
||||
function getAccount (req: express.Request, res: express.Response) {
|
||||
const account = res.locals.account
|
||||
|
||||
if (account.isOutdated()) {
|
||||
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'actor', url: account.Actor.url } })
|
||||
}
|
||||
scheduleActorRefreshIfNeeded(account.Actor)
|
||||
|
||||
return res.json(account.toFormattedJSON())
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ import { VideoPlaylistModel } from '../../../models/video/video-playlist.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import { channelCollaborators } from './video-channel-collaborators.js'
|
||||
import { videoChannelLogosRouter } from './video-channel-logos.js'
|
||||
import { scheduleActorRefreshIfNeeded } from '@server/lib/activitypub/actors/refresh.js'
|
||||
|
||||
const auditLogger = auditLoggerFactory('channels')
|
||||
|
||||
@@ -307,9 +308,7 @@ async function getVideoChannel (req: express.Request, res: express.Response) {
|
||||
const id = res.locals.videoChannel.id
|
||||
const videoChannel = await Hooks.wrapObject(res.locals.videoChannel, 'filter:api.video-channel.get.result', { id })
|
||||
|
||||
if (videoChannel.isOutdated()) {
|
||||
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } })
|
||||
}
|
||||
scheduleActorRefreshIfNeeded(videoChannel.Actor)
|
||||
|
||||
return res.json(videoChannel.toFormattedJSON())
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
VideoPlaylistUpdate
|
||||
} from '@peertube/peertube-models'
|
||||
import { uuidToShort } from '@peertube/peertube-node-utils'
|
||||
import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists/index.js'
|
||||
import { schedulePlaylistRefreshIfNeeded } from '@server/lib/activitypub/playlists/index.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import {
|
||||
generateThumbnailForPlaylist,
|
||||
@@ -170,7 +170,7 @@ async function listVideoPlaylists (req: express.Request, res: express.Response)
|
||||
function getVideoPlaylist (req: express.Request, res: express.Response) {
|
||||
const videoPlaylist = res.locals.videoPlaylistSummary
|
||||
|
||||
scheduleRefreshIfNeeded(videoPlaylist)
|
||||
schedulePlaylistRefreshIfNeeded(videoPlaylist)
|
||||
|
||||
return res.json(videoPlaylist.toFormattedJSON())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { HttpStatusCode, VideoChannelActivityAction } from '@peertube/peertube-models'
|
||||
import { pickCommonVideoQuery } from '@server/helpers/query.js'
|
||||
import { scheduleVideoRefreshIfNeeded } from '@server/lib/activitypub/videos/index.js'
|
||||
import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
@@ -10,7 +11,6 @@ import { logger } from '../../../helpers/logger.js'
|
||||
import { getFormattedObjects } from '../../../helpers/utils.js'
|
||||
import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants.js'
|
||||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
import { JobQueue } from '../../../lib/job-queue/index.js'
|
||||
import { Hooks } from '../../../lib/plugins/hooks.js'
|
||||
import {
|
||||
apiRateLimiter,
|
||||
@@ -142,9 +142,7 @@ async function getVideo (req: express.Request, res: express.Response) {
|
||||
// Filter may return null/undefined value to forbid video access
|
||||
if (!video) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
if (video.isOutdated()) {
|
||||
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
|
||||
}
|
||||
scheduleVideoRefreshIfNeeded(video)
|
||||
|
||||
return res.json(video.toFormattedDetailsJSON())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, VideoChannelActivityAction, VideoState } from '@peertube/peertube-models'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue/index.js'
|
||||
import { CreateJobTypeAndPayload, CreateJobOptions, JobQueue } from '@server/lib/job-queue/index.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { regenerateLocalVideoThumbnailsFromVideoIfNeeded } from '@server/lib/thumbnail.js'
|
||||
import { setupUploadResumableRoutes } from '@server/lib/uploadx.js'
|
||||
@@ -178,7 +178,7 @@ async function replaceVideoSourceResumable (req: express.Request, res: express.R
|
||||
}
|
||||
|
||||
async function addVideoJobsAfterUpload (video: MVideoFullLight, videoFile: MVideoFile) {
|
||||
const jobs: (CreateJobArgument & CreateJobOptions)[] = [
|
||||
const jobs: (CreateJobTypeAndPayload & CreateJobOptions)[] = [
|
||||
{
|
||||
type: 'manage-video-torrent' as const,
|
||||
payload: {
|
||||
|
||||
@@ -46,6 +46,9 @@ export async function getVideosForFeeds (options: {
|
||||
export function getCommonVideoFeedAttributes (video: VideoModel) {
|
||||
const localLink = WEBSERVER.URL + video.getWatchStaticPath()
|
||||
|
||||
let thumbnails = video.filterThumbnails('1:1')
|
||||
if (thumbnails.length === 0) thumbnails = video.filterThumbnails('16:9')
|
||||
|
||||
return {
|
||||
title: video.name,
|
||||
link: localLink,
|
||||
@@ -59,7 +62,7 @@ export function getCommonVideoFeedAttributes (video: VideoModel) {
|
||||
? [ { name: getCategoryLabel(video.category) } ]
|
||||
: undefined,
|
||||
|
||||
thumbnails: video.filterThumbnails('1:1').map(t => ({
|
||||
thumbnails: thumbnails.map(t => ({
|
||||
url: WEBSERVER.URL + t.getFileStaticPath(),
|
||||
width: t.width,
|
||||
height: t.height
|
||||
|
||||
@@ -3,6 +3,7 @@ import { UploadFilesForCheck } from 'express'
|
||||
import 'multer'
|
||||
import { sep } from 'path'
|
||||
import validator from 'validator'
|
||||
import { logger } from '../logger.js'
|
||||
|
||||
export function exists (value: any) {
|
||||
return value !== undefined && value !== null
|
||||
@@ -165,11 +166,24 @@ export function toValueOrNull (value: string) {
|
||||
return value
|
||||
}
|
||||
|
||||
export function toArray (value: any) {
|
||||
if (!value) return []
|
||||
if (isArray(value)) return value
|
||||
|
||||
if (value?.[0]) return Object.values(value)
|
||||
|
||||
return [ value ]
|
||||
}
|
||||
|
||||
export function toIntArray (value: any) {
|
||||
if (!value) return []
|
||||
if (isArray(value) === false) return [ validator.default.toInt(value) ]
|
||||
if (isArray(value)) return value.map(v => validator.default.toInt(v))
|
||||
|
||||
return value.map(v => validator.default.toInt(v))
|
||||
if (value?.[0]) return Object.values(value).map(v => validator.default.toInt(v + ''))
|
||||
|
||||
if (typeof value === 'string' || typeof value === 'number') return [ validator.default.toInt(value + '') ]
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,9 +2,10 @@ import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { CachePromiseFactory } from '@server/helpers/promise-cache.js'
|
||||
import { PeerTubeRequestError } from '@server/helpers/requests.js'
|
||||
import { JobQueue } from '@server/lib/job-queue/job-queue.js'
|
||||
import { ActorLoadByUrlType } from '@server/lib/model-loaders/index.js'
|
||||
import { ActorModel } from '@server/models/actor/actor.js'
|
||||
import { MActorFull, MActorOutdated } from '@server/types/models/index.js'
|
||||
import { MActorFull, MActorOutdated, MActorUrl } from '@server/types/models/index.js'
|
||||
import { fetchRemoteActor } from './shared/index.js'
|
||||
import { APActorUpdater } from './updater.js'
|
||||
import { getUrlFromWebfinger } from './webfinger.js'
|
||||
@@ -16,20 +17,28 @@ type RefreshOptions<T> = {
|
||||
fetchedType: Extract<ActorLoadByUrlType, 'all'> | 'partial'
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const promiseCache = new CachePromiseFactory(
|
||||
doRefresh,
|
||||
(options: RefreshOptions<MActorFull | MActorOutdated>) => options.actor.id + ''
|
||||
)
|
||||
|
||||
function refreshActorIfNeeded<T extends MActorFull | MActorOutdated> (options: RefreshOptions<T>): RefreshResult<T> {
|
||||
export function refreshActorIfNeeded<T extends MActorFull | MActorOutdated> (options: RefreshOptions<T>): RefreshResult<T> {
|
||||
const actorArg = options.actor
|
||||
if (!actorArg.isOutdated()) return Promise.resolve({ actor: actorArg, refreshed: false })
|
||||
|
||||
return promiseCache.run(options)
|
||||
}
|
||||
|
||||
export {
|
||||
refreshActorIfNeeded
|
||||
export function scheduleActorRefreshIfNeeded (actor: MActorOutdated & MActorUrl) {
|
||||
if (!actor.isOutdated()) return
|
||||
|
||||
JobQueue.Instance.createJobAsync({
|
||||
type: 'activitypub-refresher',
|
||||
payload: { type: 'actor', url: actor.url },
|
||||
deduplicationId: `refresh-actor-${actor.url}`
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,14 +2,14 @@ import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||
import { MVideoPlaylistFullSummary } from '@server/types/models/index.js'
|
||||
import { getAPId } from '../activity.js'
|
||||
import { createOrUpdateVideoPlaylist } from './create-update.js'
|
||||
import { scheduleRefreshIfNeeded } from './refresh.js'
|
||||
import { schedulePlaylistRefreshIfNeeded } from './refresh.js'
|
||||
import { fetchRemoteVideoPlaylist } from './shared/index.js'
|
||||
|
||||
export async function getOrCreateAPVideoPlaylist (playlistUrl: string): Promise<MVideoPlaylistFullSummary> {
|
||||
const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl)
|
||||
|
||||
if (playlistFromDatabase) {
|
||||
scheduleRefreshIfNeeded(playlistFromDatabase)
|
||||
schedulePlaylistRefreshIfNeeded(playlistFromDatabase)
|
||||
|
||||
return playlistFromDatabase
|
||||
}
|
||||
|
||||
@@ -6,10 +6,14 @@ import { MVideoPlaylist, MVideoPlaylistOwnerDefault } from '@server/types/models
|
||||
import { createOrUpdateVideoPlaylist } from './create-update.js'
|
||||
import { fetchRemoteVideoPlaylist } from './shared/index.js'
|
||||
|
||||
function scheduleRefreshIfNeeded (playlist: MVideoPlaylist) {
|
||||
function schedulePlaylistRefreshIfNeeded (playlist: MVideoPlaylist) {
|
||||
if (!playlist.isOutdated()) return
|
||||
|
||||
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: playlist.url } })
|
||||
JobQueue.Instance.createJobAsync({
|
||||
type: 'activitypub-refresher',
|
||||
payload: { type: 'video-playlist', url: playlist.url },
|
||||
deduplicationId: `refresh-video-playlist-${playlist.url}`
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwnerDefault): Promise<MVideoPlaylistOwnerDefault> {
|
||||
@@ -51,5 +55,5 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwnerD
|
||||
|
||||
export {
|
||||
refreshVideoPlaylistIfNeeded,
|
||||
scheduleRefreshIfNeeded
|
||||
schedulePlaylistRefreshIfNeeded
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { APObjectId } from '@peertube/peertube-models'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { JobQueue } from '@server/lib/job-queue/index.js'
|
||||
import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders/index.js'
|
||||
import { loadVideoByUrl } from '@server/lib/model-loaders/index.js'
|
||||
import {
|
||||
MVideoAccountLightBlacklistAllFiles,
|
||||
MVideoImmutable,
|
||||
@@ -10,10 +9,10 @@ import {
|
||||
MVideoThumbnailBlacklist
|
||||
} from '@server/types/models/index.js'
|
||||
import { getAPId } from '../activity.js'
|
||||
import { refreshVideoIfNeeded } from './refresh.js'
|
||||
import { refreshVideoIfNeeded, scheduleVideoRefreshIfNeeded } from './refresh.js'
|
||||
import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared/index.js'
|
||||
|
||||
type GetVideoResult <T> = Promise<{
|
||||
type GetVideoResult<T> = Promise<{
|
||||
video: T
|
||||
created: boolean
|
||||
autoBlacklisted?: boolean
|
||||
@@ -55,12 +54,22 @@ export async function getOrCreateAPVideo (
|
||||
|
||||
// Get video url
|
||||
const videoUrl = getAPId(options.videoObject)
|
||||
let videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType)
|
||||
const videoFromDatabase = await loadVideoByUrl(videoUrl, fetchType)
|
||||
|
||||
if (videoFromDatabase) {
|
||||
if (allowRefresh === true) {
|
||||
// Typings ensure allowRefresh === false in unsafe-only-immutable-attributes fetch type
|
||||
videoFromDatabase = await scheduleRefresh(videoFromDatabase as MVideoThumbnail, fetchType, syncParam)
|
||||
// We know that allowRefresh === false on `unsafe-only-immutable-attributes` fetch type because of type definitions
|
||||
let video = videoFromDatabase as MVideoThumbnail
|
||||
|
||||
if (allowRefresh === true && video.isOutdated()) {
|
||||
if (syncParam.refreshVideo === true) {
|
||||
video = await refreshVideoIfNeeded({
|
||||
video,
|
||||
fetchedType: fetchType,
|
||||
syncParam
|
||||
})
|
||||
} else {
|
||||
scheduleVideoRefreshIfNeeded(video)
|
||||
}
|
||||
}
|
||||
|
||||
return { video: videoFromDatabase, created: false }
|
||||
@@ -107,28 +116,3 @@ export async function maybeGetOrCreateAPVideo (options: GetVideoParamAll | GetVi
|
||||
return { video: undefined, created: false }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function scheduleRefresh (video: MVideoThumbnail, fetchType: VideoLoadByUrlType, syncParam: SyncParam) {
|
||||
if (!video.isOutdated()) return video
|
||||
|
||||
const refreshOptions = {
|
||||
video,
|
||||
fetchedType: fetchType,
|
||||
syncParam
|
||||
}
|
||||
|
||||
if (syncParam.refreshVideo === true) {
|
||||
return refreshVideoIfNeeded(refreshOptions)
|
||||
}
|
||||
|
||||
await JobQueue.Instance.createJob({
|
||||
type: 'activitypub-refresher',
|
||||
payload: { type: 'video', url: video.url }
|
||||
})
|
||||
|
||||
return video
|
||||
}
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { PeerTubeRequestError } from '@server/helpers/requests.js'
|
||||
import { JobQueue } from '@server/lib/job-queue/job-queue.js'
|
||||
import { VideoLoadByUrlType } from '@server/lib/model-loaders/index.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MVideoAccountLightBlacklistAllFiles, MVideoThumbnail } from '@server/types/models/index.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { MVideo, MVideoFullLight, MVideoThumbnail } from '@server/types/models/index.js'
|
||||
import { ActorFollowHealthCache } from '../../actor-follow-health-cache.js'
|
||||
import { fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared/index.js'
|
||||
import { APVideoUpdater } from './updater.js'
|
||||
|
||||
async function refreshVideoIfNeeded (options: {
|
||||
export function scheduleVideoRefreshIfNeeded (video: MVideo) {
|
||||
if (!video.isOutdated()) return
|
||||
|
||||
JobQueue.Instance.createJobAsync({
|
||||
type: 'activitypub-refresher',
|
||||
deduplicationId: `video-refresh-${video.url}`,
|
||||
payload: { type: 'video', url: video.url }
|
||||
})
|
||||
}
|
||||
|
||||
export async function refreshVideoIfNeeded (options: {
|
||||
video: MVideoThumbnail
|
||||
fetchedType: VideoLoadByUrlType
|
||||
syncParam: SyncParam
|
||||
@@ -17,7 +28,7 @@ async function refreshVideoIfNeeded (options: {
|
||||
|
||||
// We need more attributes if the argument video was fetched with not enough joints
|
||||
const video = options.fetchedType === 'all'
|
||||
? options.video as MVideoAccountLightBlacklistAllFiles
|
||||
? options.video as MVideoFullLight
|
||||
: await VideoModel.loadByUrlAndPopulateAccountAndFiles(options.video.url)
|
||||
|
||||
const lTags = loggerTagsFactory('ap', 'video', 'refresh', video.uuid, video.url)
|
||||
@@ -62,9 +73,3 @@ async function refreshVideoIfNeeded (options: {
|
||||
return video
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
refreshVideoIfNeeded
|
||||
}
|
||||
|
||||
@@ -110,13 +110,16 @@ export abstract class APVideoAbstractBuilder {
|
||||
}
|
||||
|
||||
protected async insertOrReplaceStoryboard (video: MVideoFullLight, t: Transaction) {
|
||||
const storyboardAttributes = getStoryboardAttributeFromObject(video, this.videoObject)
|
||||
|
||||
const existingStoryboard = await StoryboardModel.loadByVideo(video.id, t)
|
||||
if (existingStoryboard?.fileUrl === storyboardAttributes?.fileUrl) return
|
||||
|
||||
if (existingStoryboard) await existingStoryboard.destroy({ transaction: t })
|
||||
|
||||
const storyboardAttributes = getStoryboardAttributeFromObject(video, this.videoObject)
|
||||
if (!storyboardAttributes) return
|
||||
|
||||
return StoryboardModel.create(storyboardAttributes, { transaction: t })
|
||||
if (storyboardAttributes) {
|
||||
await StoryboardModel.create(storyboardAttributes, { transaction: t })
|
||||
}
|
||||
}
|
||||
|
||||
protected async insertOrReplaceLive (video: MVideoFullLight, transaction: Transaction) {
|
||||
@@ -134,11 +137,12 @@ export abstract class APVideoAbstractBuilder {
|
||||
}
|
||||
|
||||
protected async setWebVideoFiles (video: MVideoFullLight, t: Transaction) {
|
||||
const videoFileAttributes = getFileAttributesFromUrl(video, this.videoObject.url)
|
||||
const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
|
||||
const oldFiles = video.VideoFiles || []
|
||||
|
||||
const newVideoFiles = getFileAttributesFromUrl(video, this.videoObject.url, oldFiles).map(a => new VideoFileModel(a))
|
||||
|
||||
// Remove video files that do not exist anymore
|
||||
await deleteAllModels(filterNonExistingModels(video.VideoFiles || [], newVideoFiles), t)
|
||||
await deleteAllModels(filterNonExistingModels(oldFiles, newVideoFiles), t)
|
||||
|
||||
// Update or add other one
|
||||
const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
|
||||
@@ -217,7 +221,11 @@ export abstract class APVideoAbstractBuilder {
|
||||
) {
|
||||
const oldStreamingPlaylistFiles = this.getStreamingPlaylistFiles(oldPlaylists || [], playlistModel.type)
|
||||
|
||||
const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a))
|
||||
const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(
|
||||
playlistModel,
|
||||
tagObjects,
|
||||
oldStreamingPlaylistFiles
|
||||
).map(a => new VideoFileModel(a))
|
||||
|
||||
await deleteAllModels(filterNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles), t)
|
||||
|
||||
|
||||
@@ -54,54 +54,57 @@ export function getTagsFromObject (videoObject: VideoObject) {
|
||||
|
||||
export function getFileAttributesFromUrl (
|
||||
videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
|
||||
urls: (ActivityTagObject | ActivityUrlObject)[]
|
||||
urls: (ActivityTagObject | ActivityUrlObject)[],
|
||||
oldFiles: MVideoFile[]
|
||||
) {
|
||||
const fileUrls = urls.filter(u => isAPVideoUrlObject(u))
|
||||
if (fileUrls.length === 0) return []
|
||||
const fileUrlObjects = urls.filter(u => isAPVideoUrlObject(u))
|
||||
if (fileUrlObjects.length === 0) return []
|
||||
|
||||
const attributes: FilteredModelAttributes<VideoFileModel>[] = []
|
||||
for (const fileUrl of fileUrls) {
|
||||
for (const fileUrlObject of fileUrlObjects) {
|
||||
// Fetch associated metadata url, if any
|
||||
const metadata = urls.filter(isAPVideoFileUrlMetadataObject)
|
||||
.find(u => {
|
||||
return u.height === fileUrl.height &&
|
||||
u.fps === fileUrl.fps &&
|
||||
u.rel.includes(fileUrl.mediaType)
|
||||
return u.height === fileUrlObject.height &&
|
||||
u.fps === fileUrlObject.fps &&
|
||||
u.rel.includes(fileUrlObject.mediaType)
|
||||
})
|
||||
|
||||
const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType)
|
||||
const resolution = fileUrl.height
|
||||
const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrlObject.mediaType)
|
||||
const resolution = fileUrlObject.height
|
||||
|
||||
const [ videoId, videoStreamingPlaylistId ] = isStreamingPlaylist(videoOrPlaylist)
|
||||
? [ null, videoOrPlaylist.id ]
|
||||
: [ videoOrPlaylist.id, null ]
|
||||
|
||||
const { torrentFilename, infoHash, torrentUrl } = getTorrentRelatedInfo({ videoOrPlaylist, urls, fileUrl })
|
||||
const fileUrl = fileUrlObject.href
|
||||
const existingFile = oldFiles.find(f => f.fileUrl === fileUrl)
|
||||
const { torrentFilename, infoHash, torrentUrl } = getTorrentRelatedInfo({ videoOrPlaylist, urls, fileUrlObject, existingFile })
|
||||
|
||||
const attribute: Partial<AttributesOnly<MVideoFile>> = {
|
||||
extname,
|
||||
resolution,
|
||||
|
||||
size: fileUrl.size,
|
||||
fps: exists(fileUrl.fps) && fileUrl.fps >= 0
|
||||
? fileUrl.fps
|
||||
size: fileUrlObject.size,
|
||||
fps: exists(fileUrlObject.fps) && fileUrlObject.fps >= 0
|
||||
? fileUrlObject.fps
|
||||
: -1,
|
||||
|
||||
metadataUrl: metadata?.href,
|
||||
|
||||
width: fileUrl.width,
|
||||
height: fileUrl.height,
|
||||
width: fileUrlObject.width,
|
||||
height: fileUrlObject.height,
|
||||
|
||||
// Use the name of the remote file because we don't proxify video file requests
|
||||
filename: basename(fileUrl.href),
|
||||
fileUrl: fileUrl.href,
|
||||
filename: basename(fileUrl),
|
||||
fileUrl,
|
||||
|
||||
infoHash,
|
||||
torrentFilename,
|
||||
torrentUrl,
|
||||
|
||||
formatFlags: buildFileFormatFlags(fileUrl, isStreamingPlaylist(videoOrPlaylist)),
|
||||
streams: buildFileStreams(fileUrl, resolution),
|
||||
formatFlags: buildFileFormatFlags(fileUrlObject, isStreamingPlaylist(videoOrPlaylist)),
|
||||
streams: buildFileStreams(fileUrlObject, resolution),
|
||||
|
||||
// This is a video file owned by a video or by a streaming playlist
|
||||
videoId,
|
||||
@@ -349,13 +352,14 @@ function isAPSensitiveTagObject (tag: any): tag is ActivitySensitiveTagObject {
|
||||
function getTorrentRelatedInfo (options: {
|
||||
videoOrPlaylist: MVideo | MStreamingPlaylistVideo
|
||||
urls: (ActivityTagObject | ActivityUrlObject)[]
|
||||
fileUrl: ActivityVideoUrlObject
|
||||
fileUrlObject: ActivityVideoUrlObject
|
||||
existingFile?: MVideoFile
|
||||
}) {
|
||||
const { urls, fileUrl, videoOrPlaylist } = options
|
||||
const { urls, fileUrlObject, videoOrPlaylist, existingFile } = options
|
||||
|
||||
// Fetch associated magnet uri
|
||||
const magnet = urls.filter(isAPMagnetUrlObject)
|
||||
.find(u => u.height === fileUrl.height)
|
||||
.find(u => u.height === fileUrlObject.height)
|
||||
|
||||
if (!magnet) {
|
||||
return {
|
||||
@@ -378,7 +382,7 @@ function getTorrentRelatedInfo (options: {
|
||||
torrentUrl,
|
||||
|
||||
// Use our own torrent name since we proxify torrent requests
|
||||
torrentFilename: generateTorrentFileName(videoOrPlaylist, fileUrl.height),
|
||||
torrentFilename: existingFile?.torrentFilename ?? generateTorrentFileName(videoOrPlaylist, fileUrlObject.height),
|
||||
|
||||
infoHash: magnetParsed.infoHash
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MVideoCaption } from '@server/types/models/index.js'
|
||||
import { JobQueue } from '../job-queue/job-queue.js'
|
||||
import { scheduleVideoRefreshIfNeeded } from '../activitypub/videos/refresh.js'
|
||||
import { AbstractFileCache } from './shared/abstract-file-cache.js'
|
||||
|
||||
const lTags = loggerTagsFactory('lazy-load', 'video-captions')
|
||||
@@ -24,7 +24,7 @@ export class VideoCaptionsFileCache extends AbstractFileCache<MVideoCaption> {
|
||||
try {
|
||||
const video = await VideoModel.load(model.videoId)
|
||||
|
||||
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
|
||||
scheduleVideoRefreshIfNeeded(video)
|
||||
} catch (err) {
|
||||
logger.error('Error while refreshing video for lazy fetch', { ...lTags(), err })
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { StoryboardModel } from '@server/models/video/storyboard.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MStoryboard } from '@server/types/models/index.js'
|
||||
import { JobQueue } from '../job-queue/job-queue.js'
|
||||
import { scheduleVideoRefreshIfNeeded } from '../activitypub/videos/index.js'
|
||||
import { AbstractImageFileCache } from './shared/abstract-image-file-cache.js'
|
||||
|
||||
const lTags = loggerTagsFactory('lazy-load', 'video-storyboards')
|
||||
@@ -24,7 +24,7 @@ export class VideoStoryboardsImageFileCache extends AbstractImageFileCache<MStor
|
||||
try {
|
||||
const video = await VideoModel.load(model.videoId)
|
||||
|
||||
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
|
||||
scheduleVideoRefreshIfNeeded(video)
|
||||
} catch (err) {
|
||||
logger.error('Error while refreshing video for lazy fetch', { ...lTags(), err })
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { ThumbnailModel } from '@server/models/video/thumbnail.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MThumbnail } from '@server/types/models/index.js'
|
||||
import { JobQueue } from '../job-queue/job-queue.js'
|
||||
import { scheduleVideoRefreshIfNeeded } from '../activitypub/videos/refresh.js'
|
||||
import { AbstractImageFileCache } from './shared/abstract-image-file-cache.js'
|
||||
|
||||
const lTags = loggerTagsFactory('lazy-load', 'video-thumbnails')
|
||||
@@ -24,7 +24,7 @@ export class VideoThumbnailsImageFileCache extends AbstractImageFileCache<MThumb
|
||||
try {
|
||||
const video = await VideoModel.load(model.videoId)
|
||||
|
||||
JobQueue.Instance.createJobAsync({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
|
||||
scheduleVideoRefreshIfNeeded(video)
|
||||
} catch (err) {
|
||||
logger.error('Error while refreshing video for lazy fetch', { ...lTags(), err })
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ import { processVideoTranscoding } from './handlers/video-transcoding.js'
|
||||
import { processVideoTranscription } from './handlers/video-transcription.js'
|
||||
import { processVideosStats } from './handlers/video-stats.js'
|
||||
|
||||
export type CreateJobArgument =
|
||||
export type CreateJobTypeAndPayload =
|
||||
| { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload }
|
||||
| { type: 'activitypub-http-broadcast-parallel', payload: ActivitypubHttpBroadcastPayload }
|
||||
| { type: 'activitypub-http-unicast', payload: ActivitypubHttpUnicastPayload }
|
||||
@@ -110,6 +110,7 @@ export type CreateJobOptions = {
|
||||
delay?: number
|
||||
priority?: number
|
||||
failParentOnFailure?: boolean
|
||||
deduplicationId?: string
|
||||
}
|
||||
|
||||
const handlers: { [id in JobType]: (job: Job) => Promise<any> } = {
|
||||
@@ -356,12 +357,12 @@ class JobQueue {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
createJobAsync (options: CreateJobArgument & CreateJobOptions): void {
|
||||
createJobAsync (options: CreateJobTypeAndPayload & CreateJobOptions): void {
|
||||
this.createJob(options)
|
||||
.catch(err => logger.error('Cannot create job.', { err, options }))
|
||||
}
|
||||
|
||||
createJob (options: CreateJobArgument & CreateJobOptions | undefined) {
|
||||
createJob (options: CreateJobTypeAndPayload & CreateJobOptions | undefined) {
|
||||
if (!options) return
|
||||
|
||||
const queue: Queue = this.queues[options.type]
|
||||
@@ -375,7 +376,7 @@ class JobQueue {
|
||||
return queue.add('job', options.payload, jobOptions)
|
||||
}
|
||||
|
||||
createSequentialJobFlow (...jobs: ((CreateJobArgument & CreateJobOptions) | undefined)[]) {
|
||||
createSequentialJobFlow (...jobs: ((CreateJobTypeAndPayload & CreateJobOptions) | undefined)[]) {
|
||||
let lastJob: FlowJob
|
||||
|
||||
logger.debug('Creating jobs in local job queue', { jobs })
|
||||
@@ -395,7 +396,7 @@ class JobQueue {
|
||||
return this.flowProducer.add(lastJob)
|
||||
}
|
||||
|
||||
createJobWithChildren (parent: CreateJobArgument & CreateJobOptions, children: (CreateJobArgument & CreateJobOptions)[]) {
|
||||
createJobWithChildren (parent: CreateJobTypeAndPayload & CreateJobOptions, children: (CreateJobTypeAndPayload & CreateJobOptions)[]) {
|
||||
return this.flowProducer.add({
|
||||
...this.buildJobFlowOption(parent),
|
||||
|
||||
@@ -403,7 +404,7 @@ class JobQueue {
|
||||
})
|
||||
}
|
||||
|
||||
private buildJobFlowOption (job: CreateJobArgument & CreateJobOptions): FlowJob {
|
||||
private buildJobFlowOption (job: CreateJobTypeAndPayload & CreateJobOptions): FlowJob {
|
||||
return {
|
||||
name: 'job',
|
||||
data: job.payload,
|
||||
@@ -423,6 +424,12 @@ class JobQueue {
|
||||
priority: options.priority,
|
||||
delay: options.delay,
|
||||
|
||||
deduplication: options.deduplicationId
|
||||
? {
|
||||
id: options.deduplicationId
|
||||
}
|
||||
: undefined,
|
||||
|
||||
...this.buildJobRemovalOptions(type)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { buildYoutubeDLImport } from '@server/lib/video-pre-import.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import { VideoImportModel } from '@server/models/video/video-import.js'
|
||||
import { MChannelAccountDefault, MChannelSync } from '@server/types/models/index.js'
|
||||
import { CreateJobArgument, JobQueue } from './job-queue/index.js'
|
||||
import { CreateJobTypeAndPayload, JobQueue } from './job-queue/index.js'
|
||||
import { ServerConfigManager } from './server-config-manager.js'
|
||||
import { buildRetryImportJob } from './video-post-import.js'
|
||||
import { getLeastPrivatePrivacy } from './video.js'
|
||||
@@ -49,7 +49,7 @@ export async function synchronizeChannel (options: {
|
||||
{ targetUrls, ...lTags() }
|
||||
)
|
||||
|
||||
const children: CreateJobArgument[] = []
|
||||
const children: CreateJobTypeAndPayload[] = []
|
||||
|
||||
let buildJobErrors = 0
|
||||
|
||||
@@ -108,7 +108,7 @@ export async function synchronizeChannel (options: {
|
||||
}
|
||||
|
||||
// Will update the channel sync status
|
||||
const parent: CreateJobArgument = {
|
||||
const parent: CreateJobTypeAndPayload = {
|
||||
type: 'after-video-channel-import',
|
||||
payload: {
|
||||
channelSyncId: channelSync?.id,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
VideoFileStreamType,
|
||||
VideoTranscodingPayload
|
||||
} from '@peertube/peertube-models'
|
||||
import { CreateJobArgument, JobQueue } from '@server/lib/job-queue/index.js'
|
||||
import { CreateJobTypeAndPayload, JobQueue } from '@server/lib/job-queue/index.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { MUserId, MVideo } from '@server/types/models/index.js'
|
||||
import { getTranscodingJobPriority } from '../../transcoding-priority.js'
|
||||
@@ -47,7 +47,7 @@ export class TranscodingJobQueueBuilder extends AbstractJobBuilder<FullPayload>
|
||||
})
|
||||
})
|
||||
|
||||
const transcodingJobBuilderJob: CreateJobArgument = {
|
||||
const transcodingJobBuilderJob: CreateJobTypeAndPayload = {
|
||||
type: 'transcoding-job-builder',
|
||||
payload: {
|
||||
videoUUID: video.uuid,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MVideo, MVideoFile, MVideoFullLight, MVideoUUID } from '@server/types/models/index.js'
|
||||
import { CreateJobArgument, CreateJobOptions, JobQueue } from './job-queue/job-queue.js'
|
||||
import { CreateJobTypeAndPayload, CreateJobOptions, JobQueue } from './job-queue/job-queue.js'
|
||||
import { VideoStoryboardJobHandler } from './runners/index.js'
|
||||
import { createTranscriptionTaskIfNeeded } from './video-captions.js'
|
||||
import { moveFilesIfPrivacyChanged } from './video-privacy.js'
|
||||
@@ -106,7 +106,7 @@ export async function addVideoJobsAfterCreation (options: {
|
||||
}) {
|
||||
const { video, videoFile, generateTranscription } = options
|
||||
|
||||
const jobs: (CreateJobArgument & CreateJobOptions)[] = [
|
||||
const jobs: (CreateJobTypeAndPayload & CreateJobOptions)[] = [
|
||||
{
|
||||
type: 'manage-video-torrent' as 'manage-video-torrent',
|
||||
payload: {
|
||||
@@ -178,7 +178,7 @@ export async function addVideoJobsAfterUpdate (options: {
|
||||
oldPrivacy: VideoPrivacyType
|
||||
}) {
|
||||
const { video, nameChanged, oldPrivacy, isNewVideoForFederation } = options
|
||||
const jobs: CreateJobArgument[] = []
|
||||
const jobs: CreateJobTypeAndPayload[] = []
|
||||
|
||||
const filePathChanged = await moveFilesIfPrivacyChanged(video, oldPrivacy)
|
||||
const hls = video.getHLSPlaylist()
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { HttpStatusCode, ServerFollowCreate } from '@peertube/peertube-models'
|
||||
import { isProdInstance } from '@peertube/peertube-node-utils'
|
||||
import { isEachUniqueHandleValid, isFollowStateValid, isRemoteHandleValid } from '@server/helpers/custom-validators/follows.js'
|
||||
import { hasArrayLength, toArray } from '@server/helpers/custom-validators/misc.js'
|
||||
import { loadActorUrlOrGetFromWebfinger } from '@server/lib/activitypub/actors/index.js'
|
||||
import { getRemoteNameAndHost } from '@server/lib/activitypub/follow.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { MActorFollowActorsDefault } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor.js'
|
||||
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers.js'
|
||||
import { logger } from '../../helpers/logger.js'
|
||||
@@ -32,11 +33,13 @@ const listFollowsValidator = [
|
||||
|
||||
const followValidator = [
|
||||
body('hosts')
|
||||
.toArray()
|
||||
.customSanitizer(toArray)
|
||||
.custom(v => hasArrayLength(v, { max: 100 }))
|
||||
.custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
|
||||
|
||||
body('handles')
|
||||
.toArray()
|
||||
.customSanitizer(toArray)
|
||||
.custom(v => hasArrayLength(v, { max: 100 }))
|
||||
.custom(isEachUniqueHandleValid).withMessage('Should have an array of handles'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
@@ -147,10 +150,10 @@ const rejectFollowerValidator = [
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
followValidator,
|
||||
removeFollowingValidator,
|
||||
getFollowerValidator,
|
||||
acceptFollowerValidator,
|
||||
followValidator,
|
||||
getFollowerValidator,
|
||||
listFollowsValidator,
|
||||
rejectFollowerValidator,
|
||||
listFollowsValidator
|
||||
removeFollowingValidator
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { isSearchTargetValid } from '@server/helpers/custom-validators/search.js'
|
||||
import { isHostValid } from '@server/helpers/custom-validators/servers.js'
|
||||
import { areUUIDsValid, isDateValid, isNotEmptyStringArray, toCompleteUUIDs } from '../../helpers/custom-validators/misc.js'
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import {
|
||||
areUUIDsValid,
|
||||
hasArrayLength,
|
||||
isDateValid,
|
||||
isNotEmptyStringArray,
|
||||
toArray,
|
||||
toCompleteUUIDs
|
||||
} from '../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const videosSearchValidator = [
|
||||
@@ -33,7 +40,8 @@ const videosSearchValidator = [
|
||||
|
||||
query('uuids')
|
||||
.optional()
|
||||
.toArray()
|
||||
.customSanitizer(toArray)
|
||||
.custom(v => hasArrayLength(v, { max: 100 }))
|
||||
.customSanitizer(toCompleteUUIDs)
|
||||
.custom(areUUIDsValid).withMessage('Should have valid array of uuid'),
|
||||
|
||||
@@ -63,7 +71,8 @@ const videoChannelsListSearchValidator = [
|
||||
|
||||
query('handles')
|
||||
.optional()
|
||||
.toArray()
|
||||
.customSanitizer(toArray)
|
||||
.custom(v => hasArrayLength(v, { max: 100 }))
|
||||
.custom(isNotEmptyStringArray).withMessage('Should have valid array of handles'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
@@ -88,7 +97,8 @@ const videoPlaylistsListSearchValidator = [
|
||||
|
||||
query('uuids')
|
||||
.optional()
|
||||
.toArray()
|
||||
.customSanitizer(toArray)
|
||||
.custom(v => hasArrayLength(v, { max: 100 }))
|
||||
.customSanitizer(toCompleteUUIDs)
|
||||
.custom(areUUIDsValid).withMessage('Should have valid array of uuid'),
|
||||
|
||||
@@ -102,7 +112,7 @@ const videoPlaylistsListSearchValidator = [
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videosSearchValidator,
|
||||
videoChannelsListSearchValidator,
|
||||
videoPlaylistsListSearchValidator
|
||||
videoPlaylistsListSearchValidator,
|
||||
videosSearchValidator
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { MUserAccountId } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { body, param, query, ValidationChain } from 'express-validator'
|
||||
import {
|
||||
hasArrayLength,
|
||||
isArrayOf,
|
||||
isIdOrUUIDValid,
|
||||
isIdValid,
|
||||
@@ -465,6 +466,7 @@ export const commonVideoPlaylistFiltersValidator = [
|
||||
export const doVideosInPlaylistExistValidator = [
|
||||
query('videoIds')
|
||||
.customSanitizer(toIntArray)
|
||||
.custom(v => hasArrayLength(v, { max: 100 }))
|
||||
.custom(v => isArrayOf(v, isIdValid)).withMessage('Should have a valid video ids array'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
|
||||
Reference in New Issue
Block a user