Channel sync (#5135)

* Add external channel URL for channel update / creation (#754)

* Disallow synchronisation if user has no video quota (#754)

* More constraints serverside (#754)

* Disable sync if server configuration does not allow HTTP import (#754)

* Working version synchronizing videos with a job (#754)

TODO: refactoring, too much code duplication

* More logs and try/catch (#754)

* Fix eslint error (#754)

* WIP: support synchronization time change (#754)

* New frontend #754

* WIP: Create sync front (#754)

* Enhance UI, sync creation form (#754)

* Warning message when HTTP upload is disallowed

* More consistent names (#754)

* Binding Front with API (#754)

* Add a /me API (#754)

* Improve list UI (#754)

* Implement creation and deletion routes (#754)

* Lint (#754)

* Lint again (#754)

* WIP: UI for triggering import existing videos (#754)

* Implement jobs for syncing and importing channels

* Don't sync videos before sync creation + avoid concurrency issue (#754)

* Cleanup (#754)

* Cleanup: OpenAPI + API rework (#754)

* Remove dead code (#754)

* Eslint (#754)

* Revert the mess with whitespaces in constants.ts (#754)

* Some fixes after rebase (#754)

* Several fixes after PR remarks (#754)

* Front + API: Rename video-channels-sync to video-channel-syncs (#754)

* Allow enabling channel sync through UI (#754)

* getChannelInfo (#754)

* Minor fixes: openapi + model + sql (#754)

* Simplified API validators (#754)

* Rename MChannelSync to MChannelSyncChannel (#754)

* Add command for VideoChannelSync (#754)

* Use synchronization.enabled config (#754)

* Check parameters test + some fixes (#754)

* Fix conflict mistake (#754)

* Restrict access to video channel sync list API (#754)

* Start adding unit test for synchronization (#754)

* Continue testing (#754)

* Tests finished + convertion of job to scheduler (#754)

* Add lastSyncAt field (#754)

* Fix externalRemoteUrl sort + creation date not well formatted (#754)

* Small fix (#754)

* Factorize addYoutubeDLImport and buildVideo (#754)

* Check duplicates on channel not on users (#754)

* factorize thumbnail generation (#754)

* Fetch error should return status 400 (#754)

* Separate video-channel-import and video-channel-sync-latest (#754)

* Bump DB migration version after rebase (#754)

* Prettier states in UI table (#754)

* Add DefaultScope in VideoChannelSyncModel (#754)

* Fix audit logs (#754)

* Ensure user can upload when importing channel + minor fixes (#754)

* Mark synchronization as failed on exception + typos (#754)

* Change REST API for importing videos into channel (#754)

* Add option for fully synchronize a chnanel (#754)

* Return a whole sync object on creation to avoid tricks in Front (#754)

* Various remarks (#754)

* Single quotes by default (#754)

* Rename synchronization to video_channel_synchronization

* Add check.latest_videos_count and max_per_user options (#754)

* Better channel rendering in list #754

* Allow sorting with channel name and state (#754)

* Add missing tests for channel imports (#754)

* Prefer using a parent job for channel sync

* Styling

* Client styling

Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
Florent
2022-08-10 09:53:39 +02:00
committed by GitHub
parent 06ac128958
commit 2a491182e4
90 changed files with 2944 additions and 359 deletions

View File

@@ -1,7 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import 'mocha'
import { omit } from 'lodash'
import { merge, omit } from 'lodash'
import { CustomConfig, HttpStatusCode } from '@shared/models'
import {
cleanupTests,
createSingleServer,
@@ -11,7 +12,6 @@ import {
PeerTubeServer,
setAccessTokensToServers
} from '@shared/server-commands'
import { CustomConfig, HttpStatusCode } from '@shared/models'
describe('Test config API validators', function () {
const path = '/api/v1/config/custom'
@@ -162,6 +162,10 @@ describe('Test config API validators', function () {
torrent: {
enabled: false
}
},
videoChannelSynchronization: {
enabled: false,
maxPerUser: 10
}
},
trending: {
@@ -346,7 +350,26 @@ describe('Test config API validators', function () {
})
})
it('Should success with the correct parameters', async function () {
it('Should fail with a disabled http upload & enabled sync', async function () {
const newUpdateParams: CustomConfig = merge({}, updateParams, {
import: {
videos: {
http: { enabled: false }
},
videoChannelSynchronization: { enabled: true }
}
})
await makePutBodyRequest({
url: server.url,
path,
fields: newUpdateParams,
token: server.accessToken,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
it('Should succeed with the correct parameters', async function () {
await makePutBodyRequest({
url: server.url,
path,

View File

@@ -27,6 +27,7 @@ import './video-channels'
import './video-comments'
import './video-files'
import './video-imports'
import './video-channel-syncs'
import './video-playlists'
import './video-source'
import './video-studio'

View File

@@ -70,7 +70,7 @@ describe('Test upload quota', function () {
})
it('Should fail to import with HTTP/Torrent/magnet', async function () {
this.timeout(120000)
this.timeout(120_000)
const baseAttributes = {
channelId: server.store.channel.id,

View File

@@ -0,0 +1,318 @@
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination, FIXTURE_URLS } from '@server/tests/shared'
import { HttpStatusCode, VideoChannelSyncCreate } from '@shared/models'
import {
ChannelSyncsCommand,
createSingleServer,
makePostBodyRequest,
PeerTubeServer,
setAccessTokensToServers,
setDefaultVideoChannel
} from '@shared/server-commands'
describe('Test video channel sync API validator', () => {
const path = '/api/v1/video-channel-syncs'
let server: PeerTubeServer
let command: ChannelSyncsCommand
let rootChannelId: number
let rootChannelSyncId: number
const userInfo = {
accessToken: '',
username: 'user1',
id: -1,
channelId: -1,
syncId: -1
}
async function withChannelSyncDisabled<T> (callback: () => Promise<T>): Promise<void> {
try {
await server.config.disableChannelSync()
await callback()
} finally {
await server.config.enableChannelSync()
}
}
async function withMaxSyncsPerUser<T> (maxSync: number, callback: () => Promise<T>): Promise<void> {
const origConfig = await server.config.getCustomConfig()
await server.config.updateExistingSubConfig({
newConfig: {
import: {
videoChannelSynchronization: {
maxPerUser: maxSync
}
}
}
})
try {
await callback()
} finally {
await server.config.updateCustomConfig({ newCustomConfig: origConfig })
}
}
before(async function () {
this.timeout(30_000)
server = await createSingleServer(1)
await setAccessTokensToServers([ server ])
await setDefaultVideoChannel([ server ])
command = server.channelSyncs
rootChannelId = server.store.channel.id
{
userInfo.accessToken = await server.users.generateUserAndToken(userInfo.username)
const { videoChannels, id: userId } = await server.users.getMyInfo({ token: userInfo.accessToken })
userInfo.id = userId
userInfo.channelId = videoChannels[0].id
}
await server.config.enableChannelSync()
})
describe('When creating a sync', function () {
let baseCorrectParams: VideoChannelSyncCreate
before(function () {
baseCorrectParams = {
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
videoChannelId: rootChannelId
}
})
it('Should fail when sync is disabled', async function () {
await withChannelSyncDisabled(async () => {
await command.create({
token: server.accessToken,
attributes: baseCorrectParams,
expectedStatus: HttpStatusCode.FORBIDDEN_403
})
})
})
it('Should fail with nothing', async function () {
const fields = {}
await makePostBodyRequest({
url: server.url,
path,
token: server.accessToken,
fields,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
it('Should fail with no authentication', async function () {
await command.create({
token: null,
attributes: baseCorrectParams,
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
})
})
it('Should fail without a target url', async function () {
const attributes: VideoChannelSyncCreate = {
...baseCorrectParams,
externalChannelUrl: null
}
await command.create({
token: server.accessToken,
attributes,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
it('Should fail without a channelId', async function () {
const attributes: VideoChannelSyncCreate = {
...baseCorrectParams,
videoChannelId: null
}
await command.create({
token: server.accessToken,
attributes,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
it('Should fail with a channelId refering nothing', async function () {
const attributes: VideoChannelSyncCreate = {
...baseCorrectParams,
videoChannelId: 42
}
await command.create({
token: server.accessToken,
attributes,
expectedStatus: HttpStatusCode.NOT_FOUND_404
})
})
it('Should fail to create a sync when the user does not own the channel', async function () {
await command.create({
token: userInfo.accessToken,
attributes: baseCorrectParams,
expectedStatus: HttpStatusCode.FORBIDDEN_403
})
})
it('Should succeed to create a sync with root and for another user\'s channel', async function () {
const { videoChannelSync } = await command.create({
token: server.accessToken,
attributes: {
...baseCorrectParams,
videoChannelId: userInfo.channelId
},
expectedStatus: HttpStatusCode.OK_200
})
userInfo.syncId = videoChannelSync.id
})
it('Should succeed with the correct parameters', async function () {
const { videoChannelSync } = await command.create({
token: server.accessToken,
attributes: baseCorrectParams,
expectedStatus: HttpStatusCode.OK_200
})
rootChannelSyncId = videoChannelSync.id
})
it('Should fail when the user exceeds allowed number of synchronizations', async function () {
await withMaxSyncsPerUser(1, async () => {
await command.create({
token: server.accessToken,
attributes: {
...baseCorrectParams,
videoChannelId: userInfo.channelId
},
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
})
})
describe('When listing my channel syncs', function () {
const myPath = '/api/v1/accounts/root/video-channel-syncs'
it('Should fail with a bad start pagination', async function () {
await checkBadStartPagination(server.url, myPath, server.accessToken)
})
it('Should fail with a bad count pagination', async function () {
await checkBadCountPagination(server.url, myPath, server.accessToken)
})
it('Should fail with an incorrect sort', async function () {
await checkBadSortPagination(server.url, myPath, server.accessToken)
})
it('Should succeed with the correct parameters', async function () {
await command.listByAccount({
accountName: 'root',
token: server.accessToken,
expectedStatus: HttpStatusCode.OK_200
})
})
it('Should fail with no authentication', async function () {
await command.listByAccount({
accountName: 'root',
token: null,
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
})
})
it('Should fail when a simple user lists another user\'s synchronizations', async function () {
await command.listByAccount({
accountName: 'root',
token: userInfo.accessToken,
expectedStatus: HttpStatusCode.FORBIDDEN_403
})
})
it('Should succeed when root lists another user\'s synchronizations', async function () {
await command.listByAccount({
accountName: userInfo.username,
token: server.accessToken,
expectedStatus: HttpStatusCode.OK_200
})
})
it('Should succeed even with synchronization disabled', async function () {
await withChannelSyncDisabled(async function () {
await command.listByAccount({
accountName: 'root',
token: server.accessToken,
expectedStatus: HttpStatusCode.OK_200
})
})
})
})
describe('When triggering deletion', function () {
it('should fail with no authentication', async function () {
await command.delete({
channelSyncId: userInfo.syncId,
token: null,
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
})
})
it('Should fail when channelSyncId does not refer to any sync', async function () {
await command.delete({
channelSyncId: 42,
token: server.accessToken,
expectedStatus: HttpStatusCode.NOT_FOUND_404
})
})
it('Should fail when sync is not owned by the user', async function () {
await command.delete({
channelSyncId: rootChannelSyncId,
token: userInfo.accessToken,
expectedStatus: HttpStatusCode.FORBIDDEN_403
})
})
it('Should succeed when root delete a sync they do not own', async function () {
await command.delete({
channelSyncId: userInfo.syncId,
token: server.accessToken,
expectedStatus: HttpStatusCode.NO_CONTENT_204
})
})
it('should succeed when user delete a sync they own', async function () {
const { videoChannelSync } = await command.create({
attributes: {
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
videoChannelId: userInfo.channelId
},
token: server.accessToken,
expectedStatus: HttpStatusCode.OK_200
})
await command.delete({
channelSyncId: videoChannelSync.id,
token: server.accessToken,
expectedStatus: HttpStatusCode.NO_CONTENT_204
})
})
it('Should succeed even when synchronization is disabled', async function () {
await withChannelSyncDisabled(async function () {
await command.delete({
channelSyncId: rootChannelSyncId,
token: server.accessToken,
expectedStatus: HttpStatusCode.NO_CONTENT_204
})
})
})
})
after(async function () {
await server?.kill()
})
})

View File

@@ -3,8 +3,8 @@
import 'mocha'
import * as chai from 'chai'
import { omit } from 'lodash'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
import { buildAbsoluteFixturePath } from '@shared/core-utils'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination, FIXTURE_URLS } from '@server/tests/shared'
import { areHttpImportTestsDisabled, buildAbsoluteFixturePath } from '@shared/core-utils'
import { HttpStatusCode, VideoChannelUpdate } from '@shared/models'
import {
ChannelsCommand,
@@ -23,7 +23,13 @@ const expect = chai.expect
describe('Test video channels API validator', function () {
const videoChannelPath = '/api/v1/video-channels'
let server: PeerTubeServer
let accessTokenUser: string
const userInfo = {
accessToken: '',
channelName: 'fake_channel',
id: -1,
videoQuota: -1,
videoQuotaDaily: -1
}
let command: ChannelsCommand
// ---------------------------------------------------------------
@@ -35,14 +41,15 @@ describe('Test video channels API validator', function () {
await setAccessTokensToServers([ server ])
const user = {
const userCreds = {
username: 'fake',
password: 'fake_password'
}
{
await server.users.create({ username: user.username, password: user.password })
accessTokenUser = await server.login.getAccessToken(user)
const user = await server.users.create({ username: userCreds.username, password: userCreds.password })
userInfo.id = user.id
userInfo.accessToken = await server.login.getAccessToken(userCreds)
}
command = server.channels
@@ -191,7 +198,7 @@ describe('Test video channels API validator', function () {
await makePutBodyRequest({
url: server.url,
path,
token: accessTokenUser,
token: userInfo.accessToken,
fields: baseCorrectParams,
expectedStatus: HttpStatusCode.FORBIDDEN_403
})
@@ -339,7 +346,7 @@ describe('Test video channels API validator', function () {
})
it('Should fail with a another user', async function () {
await makeGetRequest({ url: server.url, path, token: accessTokenUser, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
await makeGetRequest({ url: server.url, path, token: userInfo.accessToken, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
})
it('Should succeed with the correct params', async function () {
@@ -347,13 +354,122 @@ describe('Test video channels API validator', function () {
})
})
describe('When triggering full synchronization', function () {
it('Should fail when HTTP upload is disabled', async function () {
await server.config.disableImports()
await command.importVideos({
channelName: 'super_channel',
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
token: server.accessToken,
expectedStatus: HttpStatusCode.FORBIDDEN_403
})
await server.config.enableImports()
})
it('Should fail when externalChannelUrl is not provided', async function () {
await command.importVideos({
channelName: 'super_channel',
externalChannelUrl: null,
token: server.accessToken,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
it('Should fail when externalChannelUrl is malformed', async function () {
await command.importVideos({
channelName: 'super_channel',
externalChannelUrl: 'not-a-url',
token: server.accessToken,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
it('Should fail with no authentication', async function () {
await command.importVideos({
channelName: 'super_channel',
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
token: null,
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
})
})
it('Should fail when sync is not owned by the user', async function () {
await command.importVideos({
channelName: 'super_channel',
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
token: userInfo.accessToken,
expectedStatus: HttpStatusCode.FORBIDDEN_403
})
})
it('Should fail when the user has no quota', async function () {
await server.users.update({
userId: userInfo.id,
videoQuota: 0
})
await command.importVideos({
channelName: 'fake_channel',
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
token: userInfo.accessToken,
expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
})
await server.users.update({
userId: userInfo.id,
videoQuota: userInfo.videoQuota
})
})
it('Should fail when the user has no daily quota', async function () {
await server.users.update({
userId: userInfo.id,
videoQuotaDaily: 0
})
await command.importVideos({
channelName: 'fake_channel',
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
token: userInfo.accessToken,
expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
})
await server.users.update({
userId: userInfo.id,
videoQuotaDaily: userInfo.videoQuotaDaily
})
})
it('Should succeed when sync is run by its owner', async function () {
if (!areHttpImportTestsDisabled()) return
await command.importVideos({
channelName: 'fake_channel',
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
token: userInfo.accessToken
})
})
it('Should succeed when sync is run with root and for another user\'s channel', async function () {
if (!areHttpImportTestsDisabled()) return
await command.importVideos({
channelName: 'fake_channel',
externalChannelUrl: FIXTURE_URLS.youtubeChannel
})
})
})
describe('When deleting a video channel', function () {
it('Should fail with a non authenticated user', async function () {
await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
})
it('Should fail with another authenticated user', async function () {
await command.delete({ token: accessTokenUser, channelName: 'super_channel', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
await command.delete({ token: userInfo.accessToken, channelName: 'super_channel', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
})
it('Should fail with an unknown video channel id', async function () {

View File

@@ -88,7 +88,13 @@ describe('Test video imports API validator', function () {
it('Should fail with nothing', async function () {
const fields = {}
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
await makePostBodyRequest({
url: server.url,
path,
token: server.accessToken,
fields,
expectedStatus: HttpStatusCode.BAD_REQUEST_400
})
})
it('Should fail without a target url', async function () {

View File

@@ -368,6 +368,10 @@ const newCustomConfig: CustomConfig = {
torrent: {
enabled: false
}
},
videoChannelSynchronization: {
enabled: false,
maxPerUser: 10
}
},
trending: {

View File

@@ -0,0 +1,50 @@
import { expect } from 'chai'
import { FIXTURE_URLS } from '@server/tests/shared'
import { areHttpImportTestsDisabled } from '@shared/core-utils'
import {
createSingleServer,
getServerImportConfig,
PeerTubeServer,
setAccessTokensToServers,
setDefaultVideoChannel,
waitJobs
} from '@shared/server-commands'
describe('Test videos import in a channel', function () {
if (areHttpImportTestsDisabled()) return
function runSuite (mode: 'youtube-dl' | 'yt-dlp') {
describe('Import using ' + mode, function () {
let server: PeerTubeServer
before(async function () {
this.timeout(120_000)
server = await createSingleServer(1, getServerImportConfig(mode))
await setAccessTokensToServers([ server ])
await setDefaultVideoChannel([ server ])
await server.config.enableChannelSync()
})
it('Should import a whole channel', async function () {
this.timeout(240_000)
await server.channels.importVideos({ channelName: server.store.channel.name, externalChannelUrl: FIXTURE_URLS.youtubeChannel })
await waitJobs(server)
const videos = await server.videos.listByChannel({ handle: server.store.channel.name })
expect(videos.total).to.equal(2)
})
after(async function () {
await server?.kill()
})
})
}
runSuite('yt-dlp')
runSuite('youtube-dl')
})

View File

@@ -4,6 +4,8 @@ import './single-server'
import './video-captions'
import './video-change-ownership'
import './video-channels'
import './channel-import-videos'
import './video-channel-syncs'
import './video-comments'
import './video-description'
import './video-files'

View File

@@ -0,0 +1,226 @@
import 'mocha'
import { expect } from 'chai'
import { FIXTURE_URLS } from '@server/tests/shared'
import { areHttpImportTestsDisabled } from '@shared/core-utils'
import { HttpStatusCode, VideoChannelSyncState, VideoInclude, VideoPrivacy } from '@shared/models'
import {
ChannelSyncsCommand,
createSingleServer,
getServerImportConfig,
PeerTubeServer,
setAccessTokensToServers,
setDefaultAccountAvatar,
setDefaultChannelAvatar,
setDefaultVideoChannel,
waitJobs
} from '@shared/server-commands'
describe('Test channel synchronizations', function () {
if (areHttpImportTestsDisabled()) return
function runSuite (mode: 'youtube-dl' | 'yt-dlp') {
describe('Sync using ' + mode, function () {
let server: PeerTubeServer
let command: ChannelSyncsCommand
let startTestDate: Date
const userInfo = {
accessToken: '',
username: 'user1',
channelName: 'user1_channel',
channelId: -1,
syncId: -1
}
async function changeDateForSync (channelSyncId: number, newDate: string) {
await server.sql.updateQuery(
`UPDATE "videoChannelSync" ` +
`SET "createdAt"='${newDate}', "lastSyncAt"='${newDate}' ` +
`WHERE id=${channelSyncId}`
)
}
before(async function () {
this.timeout(120_000)
startTestDate = new Date()
server = await createSingleServer(1, getServerImportConfig(mode))
await setAccessTokensToServers([ server ])
await setDefaultVideoChannel([ server ])
await setDefaultChannelAvatar([ server ])
await setDefaultAccountAvatar([ server ])
await server.config.enableChannelSync()
command = server.channelSyncs
{
userInfo.accessToken = await server.users.generateUserAndToken(userInfo.username)
const { videoChannels } = await server.users.getMyInfo({ token: userInfo.accessToken })
userInfo.channelId = videoChannels[0].id
}
})
it('Should fetch the latest channel videos of a remote channel', async function () {
this.timeout(120_000)
{
const { video } = await server.imports.importVideo({
attributes: {
channelId: server.store.channel.id,
privacy: VideoPrivacy.PUBLIC,
targetUrl: FIXTURE_URLS.youtube
}
})
expect(video.name).to.equal('small video - youtube')
const { total } = await server.videos.listByChannel({ handle: 'root_channel', include: VideoInclude.NOT_PUBLISHED_STATE })
expect(total).to.equal(1)
}
const { videoChannelSync } = await command.create({
attributes: {
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
videoChannelId: server.store.channel.id
},
token: server.accessToken,
expectedStatus: HttpStatusCode.OK_200
})
// Ensure any missing video not already fetched will be considered as new
await changeDateForSync(videoChannelSync.id, '1970-01-01')
await server.debug.sendCommand({
body: {
command: 'process-video-channel-sync-latest'
}
})
{
await waitJobs(server)
const { total, data } = await server.videos.listByChannel({ handle: 'root_channel', include: VideoInclude.NOT_PUBLISHED_STATE })
expect(total).to.equal(2)
expect(data[0].name).to.equal('test')
}
})
it('Should add another synchronization', async function () {
const externalChannelUrl = FIXTURE_URLS.youtubeChannel + '?foo=bar'
const { videoChannelSync } = await command.create({
attributes: {
externalChannelUrl,
videoChannelId: server.store.channel.id
},
token: server.accessToken,
expectedStatus: HttpStatusCode.OK_200
})
expect(videoChannelSync.externalChannelUrl).to.equal(externalChannelUrl)
expect(videoChannelSync.channel).to.include({
id: server.store.channel.id,
name: 'root_channel'
})
expect(videoChannelSync.state.id).to.equal(VideoChannelSyncState.WAITING_FIRST_RUN)
expect(new Date(videoChannelSync.createdAt)).to.be.above(startTestDate).and.to.be.at.most(new Date())
})
it('Should add a synchronization for another user', async function () {
const { videoChannelSync } = await command.create({
attributes: {
externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?baz=qux',
videoChannelId: userInfo.channelId
},
token: userInfo.accessToken
})
userInfo.syncId = videoChannelSync.id
})
it('Should not import a channel if not asked', async function () {
await waitJobs(server)
const { data } = await command.listByAccount({ accountName: userInfo.username })
expect(data[0].state).to.contain({
id: VideoChannelSyncState.WAITING_FIRST_RUN,
label: 'Waiting first run'
})
})
it('Should only fetch the videos newer than the creation date', async function () {
this.timeout(120_000)
await changeDateForSync(userInfo.syncId, '2019-03-01')
await server.debug.sendCommand({
body: {
command: 'process-video-channel-sync-latest'
}
})
await waitJobs(server)
const { data, total } = await server.videos.listByChannel({
handle: userInfo.channelName,
include: VideoInclude.NOT_PUBLISHED_STATE
})
expect(total).to.equal(1)
expect(data[0].name).to.equal('test')
})
it('Should list channel synchronizations', async function () {
// Root
{
const { total, data } = await command.listByAccount({ accountName: 'root' })
expect(total).to.equal(2)
expect(data[0]).to.deep.contain({
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
state: {
id: VideoChannelSyncState.SYNCED,
label: 'Synchronized'
}
})
expect(new Date(data[0].lastSyncAt)).to.be.greaterThan(startTestDate)
expect(data[0].channel).to.contain({ id: server.store.channel.id })
expect(data[1]).to.contain({ externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?foo=bar' })
}
// User
{
const { total, data } = await command.listByAccount({ accountName: userInfo.username })
expect(total).to.equal(1)
expect(data[0]).to.deep.contain({
externalChannelUrl: FIXTURE_URLS.youtubeChannel + '?baz=qux',
state: {
id: VideoChannelSyncState.SYNCED,
label: 'Synchronized'
}
})
}
})
it('Should remove user\'s channel synchronizations', async function () {
await command.delete({ channelSyncId: userInfo.syncId })
const { total } = await command.listByAccount({ accountName: userInfo.username })
expect(total).to.equal(0)
})
after(async function () {
await server?.kill()
})
})
}
runSuite('youtube-dl')
runSuite('yt-dlp')
})

View File

@@ -12,6 +12,7 @@ import {
createMultipleServers,
createSingleServer,
doubleFollow,
getServerImportConfig,
PeerTubeServer,
setAccessTokensToServers,
setDefaultVideoChannel,
@@ -84,24 +85,9 @@ describe('Test video imports', function () {
let servers: PeerTubeServer[] = []
before(async function () {
this.timeout(30_000)
this.timeout(60_000)
// Run servers
servers = await createMultipleServers(2, {
import: {
videos: {
http: {
youtube_dl_release: {
url: mode === 'youtube-dl'
? 'https://yt-dl.org/downloads/latest/youtube-dl'
: 'https://api.github.com/repos/yt-dlp/yt-dlp/releases',
name: mode
}
}
}
}
})
servers = await createMultipleServers(2, getServerImportConfig(mode))
await setAccessTokensToServers(servers)
await setDefaultVideoChannel(servers)