Add ability to list imports of a channel sync
This commit is contained in:
parent
0567049a98
commit
a3b472a12e
|
@ -30,12 +30,13 @@
|
||||||
|
|
||||||
<ng-template pTemplate="header">
|
<ng-template pTemplate="header">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 10%"><my-global-icon iconName="columns"></my-global-icon></th>
|
<th style="width: 10%"></th>
|
||||||
<th style="width: 25%" i18n pSortableColumn="externalChannelUrl">External Channel <p-sortIcon field="externalChannelUrl"></p-sortIcon></th>
|
<th style="width: 25%" i18n pSortableColumn="externalChannelUrl">External Channel <p-sortIcon field="externalChannelUrl"></p-sortIcon></th>
|
||||||
<th style="width: 25%" i18n pSortableColumn="videoChannel">Channel <p-sortIcon field="videoChannel"></p-sortIcon></th>
|
<th style="width: 25%" i18n pSortableColumn="videoChannel">Channel <p-sortIcon field="videoChannel"></p-sortIcon></th>
|
||||||
<th style="width: 10%" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
<th style="width: 10%" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||||
<th style="width: 10%" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
<th style="width: 10%" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
<th style="width: 10%" i18n pSortableColumn="lastSyncAt">Last synchronization at <p-sortIcon field="lastSyncAt"></p-sortIcon></th>
|
<th style="width: 10%" i18n pSortableColumn="lastSyncAt">Last synchronization at <p-sortIcon field="lastSyncAt"></p-sortIcon></th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
@ -78,6 +79,12 @@
|
||||||
|
|
||||||
<td>{{ videoChannelSync.createdAt | date: 'short' }}</td>
|
<td>{{ videoChannelSync.createdAt | date: 'short' }}</td>
|
||||||
<td>{{ videoChannelSync.lastSyncAt | date: 'short' }}</td>
|
<td>{{ videoChannelSync.lastSyncAt | date: 'short' }}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<a i18n routerLink="/my-library/video-imports" [queryParams]="{ search: 'videoChannelSyncId:' + videoChannelSync.id }" class="peertube-button-link grey-button">
|
||||||
|
List imports
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</p-table>
|
</p-table>
|
||||||
|
|
|
@ -100,7 +100,7 @@ export class MyVideoChannelSyncsComponent extends RestTable implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
fullySynchronize (videoChannelSync: VideoChannelSync) {
|
fullySynchronize (videoChannelSync: VideoChannelSync) {
|
||||||
this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl)
|
this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl, videoChannelSync.id)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.notifier.success($localize`Full synchronization requested successfully for ${videoChannelSync.channel.displayName}.`)
|
this.notifier.success($localize`Full synchronization requested successfully for ${videoChannelSync.channel.displayName}.`)
|
||||||
|
|
|
@ -59,7 +59,7 @@ export class VideoChannelSyncEditComponent extends FormReactive implements OnIni
|
||||||
this.videoChannelSyncService.createSync(videoChannelSyncCreate)
|
this.videoChannelSyncService.createSync(videoChannelSyncCreate)
|
||||||
.pipe(mergeMap(({ videoChannelSync }) => {
|
.pipe(mergeMap(({ videoChannelSync }) => {
|
||||||
return importExistingVideos
|
return importExistingVideos
|
||||||
? this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl)
|
? this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl, videoChannelSync.id)
|
||||||
: Promise.resolve(null)
|
: Promise.resolve(null)
|
||||||
}))
|
}))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
|
|
|
@ -3,9 +3,18 @@
|
||||||
<ng-container i18n>My imports</ng-container>
|
<ng-container i18n>My imports</ng-container>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<div class="mb-4 d-flex justify-content-between">
|
||||||
|
<my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
|
||||||
|
|
||||||
|
<a routerLink="/my-library/video-channel-syncs" class="button-link">
|
||||||
|
<my-global-icon iconName="repeat" aria-hidden="true"></my-global-icon>
|
||||||
|
<ng-container i18n>My synchronizations</ng-container>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p-table
|
<p-table
|
||||||
[value]="videoImports" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
[value]="videoImports" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
|
||||||
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
|
[rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" dataKey="id"
|
||||||
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} imports"
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} imports"
|
||||||
[expandedRowKeys]="expandedRows"
|
[expandedRowKeys]="expandedRows"
|
||||||
|
|
|
@ -8,3 +8,9 @@ pre {
|
||||||
.video-import-error {
|
.video-import-error {
|
||||||
color: #ff0000;
|
color: #ff0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button-link {
|
||||||
|
@include peertube-button-link;
|
||||||
|
@include grey-button;
|
||||||
|
@include button-with-icon(18px, 3px, -1px);
|
||||||
|
}
|
||||||
|
|
|
@ -33,12 +33,16 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case VideoImportState.FAILED:
|
case VideoImportState.FAILED:
|
||||||
return 'badge-red'
|
return 'badge-red'
|
||||||
|
|
||||||
case VideoImportState.REJECTED:
|
case VideoImportState.REJECTED:
|
||||||
return 'badge-banned'
|
return 'badge-banned'
|
||||||
|
|
||||||
case VideoImportState.PENDING:
|
case VideoImportState.PENDING:
|
||||||
return 'badge-yellow'
|
return 'badge-yellow'
|
||||||
|
|
||||||
case VideoImportState.PROCESSING:
|
case VideoImportState.PROCESSING:
|
||||||
return 'badge-blue'
|
return 'badge-blue'
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return 'badge-green'
|
return 'badge-green'
|
||||||
}
|
}
|
||||||
|
@ -87,7 +91,7 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected reloadData () {
|
protected reloadData () {
|
||||||
this.videoImportService.getMyVideoImports(this.pagination, this.sort)
|
this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: resultList => {
|
next: resultList => {
|
||||||
this.videoImports = resultList.data
|
this.videoImports = resultList.data
|
||||||
|
|
|
@ -3,7 +3,14 @@ import { catchError, map, tap } from 'rxjs/operators'
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
|
import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
|
||||||
import { ActorImage, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models'
|
import {
|
||||||
|
ActorImage,
|
||||||
|
ResultList,
|
||||||
|
VideoChannel as VideoChannelServer,
|
||||||
|
VideoChannelCreate,
|
||||||
|
VideoChannelUpdate,
|
||||||
|
VideosImportInChannelCreate
|
||||||
|
} from '@shared/models'
|
||||||
import { environment } from '../../../../environments/environment'
|
import { environment } from '../../../../environments/environment'
|
||||||
import { Account } from '../account'
|
import { Account } from '../account'
|
||||||
import { AccountService } from '../account/account.service'
|
import { AccountService } from '../account/account.service'
|
||||||
|
@ -96,9 +103,15 @@ export class VideoChannelService {
|
||||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
importVideos (videoChannelName: string, externalChannelUrl: string) {
|
importVideos (videoChannelName: string, externalChannelUrl: string, syncId?: number) {
|
||||||
const path = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/import-videos'
|
const path = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/import-videos'
|
||||||
return this.authHttp.post(path, { externalChannelUrl })
|
|
||||||
|
const body: VideosImportInChannelCreate = {
|
||||||
|
externalChannelUrl,
|
||||||
|
videoChannelSyncId: syncId
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.authHttp.post(path, body)
|
||||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,10 +43,23 @@ export class VideoImportService {
|
||||||
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
.pipe(catchError(res => this.restExtractor.handleError(res)))
|
||||||
}
|
}
|
||||||
|
|
||||||
getMyVideoImports (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoImport>> {
|
getMyVideoImports (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<VideoImport>> {
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
const filters = this.restService.parseQueryStringFilter(search, {
|
||||||
|
videoChannelSyncId: {
|
||||||
|
prefix: 'videoChannelSyncId:'
|
||||||
|
},
|
||||||
|
targetUrl: {
|
||||||
|
prefix: 'targetUrl:'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
params = this.restService.addObjectParams(params, filters)
|
||||||
|
}
|
||||||
|
|
||||||
return this.authHttp
|
return this.authHttp
|
||||||
.get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params })
|
.get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -25,7 +25,13 @@ import {
|
||||||
usersUpdateMeValidator,
|
usersUpdateMeValidator,
|
||||||
usersVideoRatingValidator
|
usersVideoRatingValidator
|
||||||
} from '../../../middlewares'
|
} from '../../../middlewares'
|
||||||
import { deleteMeValidator, usersVideosValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
|
import {
|
||||||
|
deleteMeValidator,
|
||||||
|
getMyVideoImportsValidator,
|
||||||
|
usersVideosValidator,
|
||||||
|
videoImportsSortValidator,
|
||||||
|
videosSortValidator
|
||||||
|
} from '../../../middlewares/validators'
|
||||||
import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
|
import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
|
||||||
import { AccountModel } from '../../../models/account/account'
|
import { AccountModel } from '../../../models/account/account'
|
||||||
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
|
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
|
||||||
|
@ -60,6 +66,7 @@ meRouter.get('/me/videos/imports',
|
||||||
videoImportsSortValidator,
|
videoImportsSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
|
getMyVideoImportsValidator,
|
||||||
asyncMiddleware(getUserVideoImports)
|
asyncMiddleware(getUserVideoImports)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -138,7 +145,7 @@ async function getUserVideoImports (req: express.Request, res: express.Response)
|
||||||
const resultList = await VideoImportModel.listUserVideoImportsForApi({
|
const resultList = await VideoImportModel.listUserVideoImportsForApi({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
|
||||||
...pick(req.query, [ 'targetUrl', 'start', 'count', 'sort' ])
|
...pick(req.query, [ 'targetUrl', 'start', 'count', 'sort', 'search', 'videoChannelSyncId' ])
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { ActorFollowModel } from '@server/models/actor/actor-follow'
|
||||||
import { getServerActor } from '@server/models/application/application'
|
import { getServerActor } from '@server/models/application/application'
|
||||||
import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
|
import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
|
||||||
import { MChannelBannerAccountDefault } from '@server/types/models'
|
import { MChannelBannerAccountDefault } from '@server/types/models'
|
||||||
import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate } from '@shared/models'
|
import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate, VideosImportInChannelCreate } from '@shared/models'
|
||||||
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
|
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
|
||||||
import { resetSequelizeInstance } from '../../helpers/database-utils'
|
import { resetSequelizeInstance } from '../../helpers/database-utils'
|
||||||
import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
|
import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
|
||||||
|
@ -166,7 +166,7 @@ videoChannelRouter.get('/:nameWithHost/followers',
|
||||||
videoChannelRouter.post('/:nameWithHost/import-videos',
|
videoChannelRouter.post('/:nameWithHost/import-videos',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoChannelsNameWithHostValidator),
|
asyncMiddleware(videoChannelsNameWithHostValidator),
|
||||||
videoChannelImportVideosValidator,
|
asyncMiddleware(videoChannelImportVideosValidator),
|
||||||
ensureIsLocalChannel,
|
ensureIsLocalChannel,
|
||||||
ensureCanManageChannel,
|
ensureCanManageChannel,
|
||||||
asyncMiddleware(ensureChannelOwnerCanUpload),
|
asyncMiddleware(ensureChannelOwnerCanUpload),
|
||||||
|
@ -418,13 +418,14 @@ async function listVideoChannelFollowers (req: express.Request, res: express.Res
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importVideosInChannel (req: express.Request, res: express.Response) {
|
async function importVideosInChannel (req: express.Request, res: express.Response) {
|
||||||
const { externalChannelUrl } = req.body
|
const { externalChannelUrl } = req.body as VideosImportInChannelCreate
|
||||||
|
|
||||||
await JobQueue.Instance.createJob({
|
await JobQueue.Instance.createJob({
|
||||||
type: 'video-channel-import',
|
type: 'video-channel-import',
|
||||||
payload: {
|
payload: {
|
||||||
externalChannelUrl,
|
externalChannelUrl,
|
||||||
videoChannelId: res.locals.videoChannel.id
|
videoChannelId: res.locals.videoChannel.id,
|
||||||
|
partOfChannelSyncId: res.locals.videoChannelSync?.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 730
|
const LAST_MIGRATION_VERSION = 735
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
db: any
|
||||||
|
}): Promise<void> {
|
||||||
|
await utils.queryInterface.addColumn('videoImport', 'videoChannelSyncId', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
defaultValue: null,
|
||||||
|
allowNull: true,
|
||||||
|
references: {
|
||||||
|
model: 'videoChannelSync',
|
||||||
|
key: 'id'
|
||||||
|
},
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
onDelete: 'SET NULL'
|
||||||
|
}, { transaction: utils.transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function down (utils: {
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
}) {
|
||||||
|
await utils.queryInterface.dropTable('videoChannelSync', { transaction: utils.transaction })
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ import { logger } from '@server/helpers/logger'
|
||||||
import { CONFIG } from '@server/initializers/config'
|
import { CONFIG } from '@server/initializers/config'
|
||||||
import { synchronizeChannel } from '@server/lib/sync-channel'
|
import { synchronizeChannel } from '@server/lib/sync-channel'
|
||||||
import { VideoChannelModel } from '@server/models/video/video-channel'
|
import { VideoChannelModel } from '@server/models/video/video-channel'
|
||||||
|
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
|
||||||
|
import { MChannelSync } from '@server/types/models'
|
||||||
import { VideoChannelImportPayload } from '@shared/models'
|
import { VideoChannelImportPayload } from '@shared/models'
|
||||||
|
|
||||||
export async function processVideoChannelImport (job: Job) {
|
export async function processVideoChannelImport (job: Job) {
|
||||||
|
@ -12,13 +14,20 @@ export async function processVideoChannelImport (job: Job) {
|
||||||
|
|
||||||
// Channel import requires only http upload to be allowed
|
// Channel import requires only http upload to be allowed
|
||||||
if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
|
if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
|
||||||
logger.error('Cannot import channel as the HTTP upload is disabled')
|
throw new Error('Cannot import channel as the HTTP upload is disabled')
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
|
if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
|
||||||
logger.error('Cannot import channel as the synchronization is disabled')
|
throw new Error('Cannot import channel as the synchronization is disabled')
|
||||||
return
|
}
|
||||||
|
|
||||||
|
let channelSync: MChannelSync
|
||||||
|
if (payload.partOfChannelSyncId) {
|
||||||
|
channelSync = await VideoChannelSyncModel.loadWithChannel(payload.partOfChannelSyncId)
|
||||||
|
|
||||||
|
if (!channelSync) {
|
||||||
|
throw new Error('Unlnown channel sync specified in videos channel import')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(payload.videoChannelId)
|
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(payload.videoChannelId)
|
||||||
|
@ -28,7 +37,8 @@ export async function processVideoChannelImport (job: Job) {
|
||||||
|
|
||||||
await synchronizeChannel({
|
await synchronizeChannel({
|
||||||
channel: videoChannel,
|
channel: videoChannel,
|
||||||
externalChannelUrl: payload.externalChannelUrl
|
externalChannelUrl: payload.externalChannelUrl,
|
||||||
|
channelSync
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`Failed to import channel ${videoChannel.name}`, { err })
|
logger.error(`Failed to import channel ${videoChannel.name}`, { err })
|
||||||
|
|
|
@ -36,10 +36,6 @@ export class VideoChannelSyncLatestScheduler extends AbstractScheduler {
|
||||||
|
|
||||||
const onlyAfter = sync.lastSyncAt || sync.createdAt
|
const onlyAfter = sync.lastSyncAt || sync.createdAt
|
||||||
|
|
||||||
sync.state = VideoChannelSyncState.PROCESSING
|
|
||||||
sync.lastSyncAt = new Date()
|
|
||||||
await sync.save()
|
|
||||||
|
|
||||||
await synchronizeChannel({
|
await synchronizeChannel({
|
||||||
channel,
|
channel,
|
||||||
externalChannelUrl: sync.externalChannelUrl,
|
externalChannelUrl: sync.externalChannelUrl,
|
||||||
|
|
|
@ -18,6 +18,12 @@ export async function synchronizeChannel (options: {
|
||||||
}) {
|
}) {
|
||||||
const { channel, externalChannelUrl, videosCountLimit, onlyAfter, channelSync } = options
|
const { channel, externalChannelUrl, videosCountLimit, onlyAfter, channelSync } = options
|
||||||
|
|
||||||
|
if (channelSync) {
|
||||||
|
channelSync.state = VideoChannelSyncState.PROCESSING
|
||||||
|
channelSync.lastSyncAt = new Date()
|
||||||
|
await channelSync.save()
|
||||||
|
}
|
||||||
|
|
||||||
const user = await UserModel.loadByChannelActorId(channel.actorId)
|
const user = await UserModel.loadByChannelActorId(channel.actorId)
|
||||||
const youtubeDL = new YoutubeDLWrapper(
|
const youtubeDL = new YoutubeDLWrapper(
|
||||||
externalChannelUrl,
|
externalChannelUrl,
|
||||||
|
@ -70,6 +76,7 @@ export async function synchronizeChannel (options: {
|
||||||
children.push(job)
|
children.push(job)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Will update the channel sync status
|
||||||
const parent: CreateJobArgument = {
|
const parent: CreateJobArgument = {
|
||||||
type: 'after-video-channel-import',
|
type: 'after-video-channel-import',
|
||||||
payload: {
|
payload: {
|
||||||
|
|
|
@ -206,7 +206,8 @@ async function buildYoutubeDLImport (options: {
|
||||||
videoImportAttributes: {
|
videoImportAttributes: {
|
||||||
targetUrl,
|
targetUrl,
|
||||||
state: VideoImportState.PENDING,
|
state: VideoImportState.PENDING,
|
||||||
userId: user.id
|
userId: user.id,
|
||||||
|
videoChannelSyncId: channelSync?.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ export * from './utils'
|
||||||
export * from './video-blacklists'
|
export * from './video-blacklists'
|
||||||
export * from './video-captions'
|
export * from './video-captions'
|
||||||
export * from './video-channels'
|
export * from './video-channels'
|
||||||
|
export * from './video-channel-syncs'
|
||||||
export * from './video-comments'
|
export * from './video-comments'
|
||||||
export * from './video-imports'
|
export * from './video-imports'
|
||||||
export * from './video-ownerships'
|
export * from './video-ownerships'
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import express from 'express'
|
||||||
|
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
|
||||||
|
import { HttpStatusCode } from '@shared/models'
|
||||||
|
|
||||||
|
async function doesVideoChannelSyncIdExist (id: number, res: express.Response) {
|
||||||
|
const sync = await VideoChannelSyncModel.loadWithChannel(+id)
|
||||||
|
|
||||||
|
if (!sync) {
|
||||||
|
res.fail({
|
||||||
|
status: HttpStatusCode.NOT_FOUND_404,
|
||||||
|
message: 'Video channel sync not found'
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
res.locals.videoChannelSync = sync
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
doesVideoChannelSyncIdExist
|
||||||
|
}
|
|
@ -3,10 +3,10 @@ import { body, param } from 'express-validator'
|
||||||
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
|
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
|
||||||
import { logger } from '@server/helpers/logger'
|
import { logger } from '@server/helpers/logger'
|
||||||
import { CONFIG } from '@server/initializers/config'
|
import { CONFIG } from '@server/initializers/config'
|
||||||
import { VideoChannelModel } from '@server/models/video/video-channel'
|
|
||||||
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
|
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
|
||||||
import { HttpStatusCode, VideoChannelSyncCreate } from '@shared/models'
|
import { HttpStatusCode, VideoChannelSyncCreate } from '@shared/models'
|
||||||
import { areValidationErrors, doesVideoChannelIdExist } from '../shared'
|
import { areValidationErrors, doesVideoChannelIdExist } from '../shared'
|
||||||
|
import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs'
|
||||||
|
|
||||||
export const ensureSyncIsEnabled = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
export const ensureSyncIsEnabled = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
|
if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
|
||||||
|
@ -48,18 +48,8 @@ export const ensureSyncExists = [
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
const syncId = parseInt(req.params.id, 10)
|
if (!await doesVideoChannelSyncIdExist(+req.params.id, res)) return
|
||||||
const sync = await VideoChannelSyncModel.loadWithChannel(syncId)
|
if (!await doesVideoChannelIdExist(res.locals.videoChannelSync.videoChannelId, res)) return
|
||||||
|
|
||||||
if (!sync) {
|
|
||||||
return res.fail({
|
|
||||||
status: HttpStatusCode.NOT_FOUND_404,
|
|
||||||
message: 'Synchronization not found'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
res.locals.videoChannelSync = sync
|
|
||||||
res.locals.videoChannel = await VideoChannelModel.loadAndPopulateAccount(sync.videoChannelId)
|
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,9 @@ import { body, param, query } from 'express-validator'
|
||||||
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
|
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
|
||||||
import { CONFIG } from '@server/initializers/config'
|
import { CONFIG } from '@server/initializers/config'
|
||||||
import { MChannelAccountDefault } from '@server/types/models'
|
import { MChannelAccountDefault } from '@server/types/models'
|
||||||
|
import { VideosImportInChannelCreate } from '@shared/models'
|
||||||
import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
|
import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
|
||||||
import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
|
import { isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
|
||||||
import {
|
import {
|
||||||
isVideoChannelDescriptionValid,
|
isVideoChannelDescriptionValid,
|
||||||
isVideoChannelDisplayNameValid,
|
isVideoChannelDisplayNameValid,
|
||||||
|
@ -15,6 +16,7 @@ import { logger } from '../../../helpers/logger'
|
||||||
import { ActorModel } from '../../../models/actor/actor'
|
import { ActorModel } from '../../../models/actor/actor'
|
||||||
import { VideoChannelModel } from '../../../models/video/video-channel'
|
import { VideoChannelModel } from '../../../models/video/video-channel'
|
||||||
import { areValidationErrors, checkUserQuota, doesVideoChannelNameWithHostExist } from '../shared'
|
import { areValidationErrors, checkUserQuota, doesVideoChannelNameWithHostExist } from '../shared'
|
||||||
|
import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs'
|
||||||
|
|
||||||
export const videoChannelsAddValidator = [
|
export const videoChannelsAddValidator = [
|
||||||
body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
|
body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
|
||||||
|
@ -145,11 +147,17 @@ export const videoChannelsListValidator = [
|
||||||
export const videoChannelImportVideosValidator = [
|
export const videoChannelImportVideosValidator = [
|
||||||
body('externalChannelUrl').custom(isUrlValid).withMessage('Should have a valid channel url'),
|
body('externalChannelUrl').custom(isUrlValid).withMessage('Should have a valid channel url'),
|
||||||
|
|
||||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
body('videoChannelSyncId')
|
||||||
|
.optional()
|
||||||
|
.custom(isIdValid).withMessage('Should have a valid channel sync id'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking videoChannelImport parameters', { parameters: req.body })
|
logger.debug('Checking videoChannelImport parameters', { parameters: req.body })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
const body: VideosImportInChannelCreate = req.body
|
||||||
|
|
||||||
if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
|
if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
|
||||||
return res.fail({
|
return res.fail({
|
||||||
status: HttpStatusCode.FORBIDDEN_403,
|
status: HttpStatusCode.FORBIDDEN_403,
|
||||||
|
@ -157,6 +165,8 @@ export const videoChannelImportVideosValidator = [
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (body.videoChannelSyncId && !await doesVideoChannelSyncIdExist(body.videoChannelSyncId, res)) return
|
||||||
|
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { body, param } from 'express-validator'
|
import { body, param, query } from 'express-validator'
|
||||||
import { isResolvingToUnicastOnly } from '@server/helpers/dns'
|
import { isResolvingToUnicastOnly } from '@server/helpers/dns'
|
||||||
import { isPreImportVideoAccepted } from '@server/lib/moderation'
|
import { isPreImportVideoAccepted } from '@server/lib/moderation'
|
||||||
import { Hooks } from '@server/lib/plugins/hooks'
|
import { Hooks } from '@server/lib/plugins/hooks'
|
||||||
|
@ -92,6 +92,20 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const getMyVideoImportsValidator = [
|
||||||
|
query('videoChannelSyncId')
|
||||||
|
.optional()
|
||||||
|
.custom(isIdValid).withMessage('Should have correct videoChannelSync id'),
|
||||||
|
|
||||||
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking getMyVideoImportsValidator parameters', { parameters: req.params })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const videoImportDeleteValidator = [
|
const videoImportDeleteValidator = [
|
||||||
param('id')
|
param('id')
|
||||||
.custom(isIdValid).withMessage('Should have correct import id'),
|
.custom(isIdValid).withMessage('Should have correct import id'),
|
||||||
|
@ -143,7 +157,8 @@ const videoImportCancelValidator = [
|
||||||
export {
|
export {
|
||||||
videoImportAddValidator,
|
videoImportAddValidator,
|
||||||
videoImportCancelValidator,
|
videoImportCancelValidator,
|
||||||
videoImportDeleteValidator
|
videoImportDeleteValidator,
|
||||||
|
getMyVideoImportsValidator
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Op, WhereOptions } from 'sequelize'
|
import { IncludeOptions, Op, WhereOptions } from 'sequelize'
|
||||||
import {
|
import {
|
||||||
AfterUpdate,
|
AfterUpdate,
|
||||||
AllowNull,
|
AllowNull,
|
||||||
|
@ -22,8 +22,17 @@ import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../help
|
||||||
import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
|
import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
|
||||||
import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
|
||||||
import { UserModel } from '../user/user'
|
import { UserModel } from '../user/user'
|
||||||
import { getSort, throwIfNotValid } from '../utils'
|
import { getSort, searchAttribute, throwIfNotValid } from '../utils'
|
||||||
import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
|
import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
|
||||||
|
import { VideoChannelSyncModel } from './video-channel-sync'
|
||||||
|
|
||||||
|
const defaultVideoScope = () => {
|
||||||
|
return VideoModel.scope([
|
||||||
|
VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
|
||||||
|
VideoModelScopeNames.WITH_TAGS,
|
||||||
|
VideoModelScopeNames.WITH_THUMBNAILS
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
@DefaultScope(() => ({
|
@DefaultScope(() => ({
|
||||||
include: [
|
include: [
|
||||||
|
@ -32,11 +41,11 @@ import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: VideoModel.scope([
|
model: defaultVideoScope(),
|
||||||
VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
|
required: false
|
||||||
VideoModelScopeNames.WITH_TAGS,
|
},
|
||||||
VideoModelScopeNames.WITH_THUMBNAILS
|
{
|
||||||
]),
|
model: VideoChannelSyncModel.unscoped(),
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -113,6 +122,18 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
|
||||||
})
|
})
|
||||||
Video: VideoModel
|
Video: VideoModel
|
||||||
|
|
||||||
|
@ForeignKey(() => VideoChannelSyncModel)
|
||||||
|
@Column
|
||||||
|
videoChannelSyncId: number
|
||||||
|
|
||||||
|
@BelongsTo(() => VideoChannelSyncModel, {
|
||||||
|
foreignKey: {
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
onDelete: 'set null'
|
||||||
|
})
|
||||||
|
VideoChannelSync: VideoChannelSyncModel
|
||||||
|
|
||||||
@AfterUpdate
|
@AfterUpdate
|
||||||
static deleteVideoIfFailed (instance: VideoImportModel, options) {
|
static deleteVideoIfFailed (instance: VideoImportModel, options) {
|
||||||
if (instance.state === VideoImportState.FAILED) {
|
if (instance.state === VideoImportState.FAILED) {
|
||||||
|
@ -132,23 +153,44 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
|
||||||
count: number
|
count: number
|
||||||
sort: string
|
sort: string
|
||||||
|
|
||||||
|
search?: string
|
||||||
targetUrl?: string
|
targetUrl?: string
|
||||||
|
videoChannelSyncId?: number
|
||||||
}) {
|
}) {
|
||||||
const { userId, start, count, sort, targetUrl } = options
|
const { userId, start, count, sort, targetUrl, videoChannelSyncId, search } = options
|
||||||
|
|
||||||
const where: WhereOptions = { userId }
|
const where: WhereOptions = { userId }
|
||||||
|
const include: IncludeOptions[] = [
|
||||||
if (targetUrl) where['targetUrl'] = targetUrl
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
distinct: true,
|
|
||||||
include: [
|
|
||||||
{
|
{
|
||||||
attributes: [ 'id' ],
|
attributes: [ 'id' ],
|
||||||
model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query
|
model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: VideoChannelSyncModel.unscoped(),
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
|
|
||||||
|
if (targetUrl) where['targetUrl'] = targetUrl
|
||||||
|
if (videoChannelSyncId) where['videoChannelSyncId'] = videoChannelSyncId
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
include.push({
|
||||||
|
model: defaultVideoScope(),
|
||||||
|
required: true,
|
||||||
|
where: searchAttribute(search, 'name')
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
include.push({
|
||||||
|
model: defaultVideoScope(),
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
distinct: true,
|
||||||
|
include,
|
||||||
offset: start,
|
offset: start,
|
||||||
limit: count,
|
limit: count,
|
||||||
order: getSort(sort),
|
order: getSort(sort),
|
||||||
|
@ -196,6 +238,10 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
|
||||||
? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { tags: this.Video.Tags.map(t => t.name) })
|
? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { tags: this.Video.Tags.map(t => t.name) })
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const videoChannelSync = this.VideoChannelSync
|
||||||
|
? { id: this.VideoChannelSync.id, externalChannelUrl: this.VideoChannelSync.externalChannelUrl }
|
||||||
|
: undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|
||||||
|
@ -210,7 +256,8 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
|
||||||
error: this.error,
|
error: this.error,
|
||||||
updatedAt: this.updatedAt.toISOString(),
|
updatedAt: this.updatedAt.toISOString(),
|
||||||
createdAt: this.createdAt.toISOString(),
|
createdAt: this.createdAt.toISOString(),
|
||||||
video
|
video,
|
||||||
|
videoChannelSync
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import 'mocha'
|
||||||
|
import { FIXTURE_URLS } from '@server/tests/shared'
|
||||||
|
import { areHttpImportTestsDisabled } from '@shared/core-utils'
|
||||||
|
import { HttpStatusCode } from '@shared/models'
|
||||||
|
import { ChannelsCommand, cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
|
||||||
|
|
||||||
|
describe('Test videos import in a channel API validator', function () {
|
||||||
|
let server: PeerTubeServer
|
||||||
|
const userInfo = {
|
||||||
|
accessToken: '',
|
||||||
|
channelName: 'fake_channel',
|
||||||
|
id: -1,
|
||||||
|
videoQuota: -1,
|
||||||
|
videoQuotaDaily: -1
|
||||||
|
}
|
||||||
|
let command: ChannelsCommand
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
server = await createSingleServer(1)
|
||||||
|
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
|
||||||
|
const userCreds = {
|
||||||
|
username: 'fake',
|
||||||
|
password: 'fake_password'
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
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 a bad sync id', async function () {
|
||||||
|
await command.importVideos({
|
||||||
|
channelName: 'super_channel',
|
||||||
|
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||||
|
videoChannelSyncId: 'toto' as any,
|
||||||
|
token: server.accessToken,
|
||||||
|
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a unknown sync id', async function () {
|
||||||
|
await command.importVideos({
|
||||||
|
channelName: 'super_channel',
|
||||||
|
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||||
|
videoChannelSyncId: 42,
|
||||||
|
token: server.accessToken,
|
||||||
|
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests([ server ])
|
||||||
|
})
|
||||||
|
})
|
|
@ -28,6 +28,7 @@ import './video-comments'
|
||||||
import './video-files'
|
import './video-files'
|
||||||
import './video-imports'
|
import './video-imports'
|
||||||
import './video-channel-syncs'
|
import './video-channel-syncs'
|
||||||
|
import './channel-import-videos'
|
||||||
import './video-playlists'
|
import './video-playlists'
|
||||||
import './video-source'
|
import './video-source'
|
||||||
import './video-studio'
|
import './video-studio'
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination, FIXTURE_URLS } from '@server/tests/shared'
|
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
|
||||||
import { areHttpImportTestsDisabled, buildAbsoluteFixturePath } from '@shared/core-utils'
|
import { buildAbsoluteFixturePath } from '@shared/core-utils'
|
||||||
import { HttpStatusCode, VideoChannelUpdate } from '@shared/models'
|
import { HttpStatusCode, VideoChannelUpdate } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
ChannelsCommand,
|
ChannelsCommand,
|
||||||
|
@ -354,115 +354,6 @@ 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 () {
|
describe('When deleting a video channel', function () {
|
||||||
it('Should fail with a non authenticated user', async function () {
|
it('Should fail with a non authenticated user', async function () {
|
||||||
await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
|
|
@ -59,6 +59,15 @@ describe('Test video imports API validator', function () {
|
||||||
await checkBadSortPagination(server.url, myPath, server.accessToken)
|
await checkBadSortPagination(server.url, myPath, server.accessToken)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad videoChannelSyncId param', async function () {
|
||||||
|
await makeGetRequest({
|
||||||
|
url: server.url,
|
||||||
|
path: myPath,
|
||||||
|
query: { videoChannelSyncId: 'toto' },
|
||||||
|
token: server.accessToken
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('Should success with the correct parameters', async function () {
|
it('Should success with the correct parameters', async function () {
|
||||||
await makeGetRequest({ url: server.url, path: myPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
|
await makeGetRequest({ url: server.url, path: myPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { FIXTURE_URLS } from '@server/tests/shared'
|
import { FIXTURE_URLS } from '@server/tests/shared'
|
||||||
import { areHttpImportTestsDisabled } from '@shared/core-utils'
|
import { areHttpImportTestsDisabled } from '@shared/core-utils'
|
||||||
|
@ -29,7 +31,7 @@ describe('Test videos import in a channel', function () {
|
||||||
await server.config.enableChannelSync()
|
await server.config.enableChannelSync()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should import a whole channel', async function () {
|
it('Should import a whole channel without specifying the sync id', async function () {
|
||||||
this.timeout(240_000)
|
this.timeout(240_000)
|
||||||
|
|
||||||
await server.channels.importVideos({ channelName: server.store.channel.name, externalChannelUrl: FIXTURE_URLS.youtubeChannel })
|
await server.channels.importVideos({ channelName: server.store.channel.name, externalChannelUrl: FIXTURE_URLS.youtubeChannel })
|
||||||
|
@ -39,6 +41,74 @@ describe('Test videos import in a channel', function () {
|
||||||
expect(videos.total).to.equal(2)
|
expect(videos.total).to.equal(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('These imports should not have a sync id', async function () {
|
||||||
|
const { total, data } = await server.imports.getMyVideoImports()
|
||||||
|
|
||||||
|
expect(total).to.equal(2)
|
||||||
|
expect(data).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
for (const videoImport of data) {
|
||||||
|
expect(videoImport.videoChannelSync).to.not.exist
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should import a whole channel and specifying the sync id', async function () {
|
||||||
|
this.timeout(240_000)
|
||||||
|
|
||||||
|
{
|
||||||
|
server.store.channel.name = 'channel2'
|
||||||
|
const { id } = await server.channels.create({ attributes: { name: server.store.channel.name } })
|
||||||
|
server.store.channel.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const attributes = {
|
||||||
|
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||||
|
videoChannelId: server.store.channel.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const { videoChannelSync } = await server.channelSyncs.create({ attributes })
|
||||||
|
server.store.videoChannelSync = videoChannelSync
|
||||||
|
|
||||||
|
await waitJobs(server)
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.channels.importVideos({
|
||||||
|
channelName: server.store.channel.name,
|
||||||
|
externalChannelUrl: FIXTURE_URLS.youtubeChannel,
|
||||||
|
videoChannelSyncId: server.store.videoChannelSync.id
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitJobs(server)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('These imports should have a sync id', async function () {
|
||||||
|
const { total, data } = await server.imports.getMyVideoImports()
|
||||||
|
|
||||||
|
expect(total).to.equal(4)
|
||||||
|
expect(data).to.have.lengthOf(4)
|
||||||
|
|
||||||
|
const importsWithSyncId = data.filter(i => !!i.videoChannelSync)
|
||||||
|
expect(importsWithSyncId).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
for (const videoImport of importsWithSyncId) {
|
||||||
|
expect(videoImport.videoChannelSync).to.exist
|
||||||
|
expect(videoImport.videoChannelSync.id).to.equal(server.store.videoChannelSync.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should be able to filter imports by this sync id', async function () {
|
||||||
|
const { total, data } = await server.imports.getMyVideoImports({ videoChannelSyncId: server.store.videoChannelSync.id })
|
||||||
|
|
||||||
|
expect(total).to.equal(2)
|
||||||
|
expect(data).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
for (const videoImport of data) {
|
||||||
|
expect(videoImport.videoChannelSync).to.exist
|
||||||
|
expect(videoImport.videoChannelSync.id).to.equal(server.store.videoChannelSync.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
await server?.kill()
|
await server?.kill()
|
||||||
})
|
})
|
||||||
|
|
|
@ -23,7 +23,10 @@ describe('Test channel synchronizations', function () {
|
||||||
describe('Sync using ' + mode, function () {
|
describe('Sync using ' + mode, function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
let command: ChannelSyncsCommand
|
let command: ChannelSyncsCommand
|
||||||
|
|
||||||
let startTestDate: Date
|
let startTestDate: Date
|
||||||
|
|
||||||
|
let rootChannelSyncId: number
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
username: 'user1',
|
username: 'user1',
|
||||||
|
@ -90,6 +93,7 @@ describe('Test channel synchronizations', function () {
|
||||||
token: server.accessToken,
|
token: server.accessToken,
|
||||||
expectedStatus: HttpStatusCode.OK_200
|
expectedStatus: HttpStatusCode.OK_200
|
||||||
})
|
})
|
||||||
|
rootChannelSyncId = videoChannelSync.id
|
||||||
|
|
||||||
// Ensure any missing video not already fetched will be considered as new
|
// Ensure any missing video not already fetched will be considered as new
|
||||||
await changeDateForSync(videoChannelSync.id, '1970-01-01')
|
await changeDateForSync(videoChannelSync.id, '1970-01-01')
|
||||||
|
@ -208,6 +212,14 @@ describe('Test channel synchronizations', function () {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should list imports of a channel synchronization', async function () {
|
||||||
|
const { total, data } = await server.imports.getMyVideoImports({ videoChannelSyncId: rootChannelSyncId })
|
||||||
|
|
||||||
|
expect(total).to.equal(1)
|
||||||
|
expect(data).to.have.lengthOf(1)
|
||||||
|
expect(data[0].video.name).to.equal('test')
|
||||||
|
})
|
||||||
|
|
||||||
it('Should remove user\'s channel synchronizations', async function () {
|
it('Should remove user\'s channel synchronizations', async function () {
|
||||||
await command.delete({ channelSyncId: userInfo.syncId })
|
await command.delete({ channelSyncId: userInfo.syncId })
|
||||||
|
|
||||||
|
|
|
@ -228,6 +228,15 @@ describe('Test video imports', function () {
|
||||||
expect(videoImports[0].targetUrl).to.equal(FIXTURE_URLS.youtube)
|
expect(videoImports[0].targetUrl).to.equal(FIXTURE_URLS.youtube)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should search in my imports', async function () {
|
||||||
|
const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ search: 'peertube2' })
|
||||||
|
expect(total).to.equal(1)
|
||||||
|
expect(videoImports).to.have.lengthOf(1)
|
||||||
|
|
||||||
|
expect(videoImports[0].magnetUri).to.equal(FIXTURE_URLS.magnet)
|
||||||
|
expect(videoImports[0].video.name).to.equal('super peertube2 video')
|
||||||
|
})
|
||||||
|
|
||||||
it('Should have the video listed on the two instances', async function () {
|
it('Should have the video listed on the two instances', async function () {
|
||||||
this.timeout(120_000)
|
this.timeout(120_000)
|
||||||
|
|
||||||
|
|
|
@ -236,6 +236,8 @@ export interface VideoStudioEditionPayload {
|
||||||
export interface VideoChannelImportPayload {
|
export interface VideoChannelImportPayload {
|
||||||
externalChannelUrl: string
|
externalChannelUrl: string
|
||||||
videoChannelId: number
|
videoChannelId: number
|
||||||
|
|
||||||
|
partOfChannelSyncId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AfterVideoChannelImportPayload {
|
export interface AfterVideoChannelImportPayload {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './video-import-create.model'
|
export * from './video-import-create.model'
|
||||||
export * from './video-import-state.enum'
|
export * from './video-import-state.enum'
|
||||||
export * from './video-import.model'
|
export * from './video-import.model'
|
||||||
|
export * from './videos-import-in-channel-create.model'
|
||||||
|
|
|
@ -16,4 +16,9 @@ export interface VideoImport {
|
||||||
error?: string
|
error?: string
|
||||||
|
|
||||||
video?: Video & { tags: string[] }
|
video?: Video & { tags: string[] }
|
||||||
|
|
||||||
|
videoChannelSync?: {
|
||||||
|
id: number
|
||||||
|
externalChannelUrl: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface VideosImportInChannelCreate {
|
||||||
|
externalChannelUrl: string
|
||||||
|
videoChannelSyncId?: number
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import { ChildProcess, fork } from 'child_process'
|
||||||
import { copy } from 'fs-extra'
|
import { copy } from 'fs-extra'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { parallelTests, randomInt, root } from '@shared/core-utils'
|
import { parallelTests, randomInt, root } from '@shared/core-utils'
|
||||||
import { Video, VideoChannel, VideoCreateResult, VideoDetails } from '@shared/models'
|
import { Video, VideoChannel, VideoChannelSync, VideoCreateResult, VideoDetails } from '@shared/models'
|
||||||
import { BulkCommand } from '../bulk'
|
import { BulkCommand } from '../bulk'
|
||||||
import { CLICommand } from '../cli'
|
import { CLICommand } from '../cli'
|
||||||
import { CustomPagesCommand } from '../custom-pages'
|
import { CustomPagesCommand } from '../custom-pages'
|
||||||
|
@ -80,6 +80,7 @@ export class PeerTubeServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
channel?: VideoChannel
|
channel?: VideoChannel
|
||||||
|
videoChannelSync?: Partial<VideoChannelSync>
|
||||||
|
|
||||||
video?: Video
|
video?: Video
|
||||||
videoCreated?: VideoCreateResult
|
videoCreated?: VideoCreateResult
|
||||||
|
|
|
@ -6,7 +6,8 @@ import {
|
||||||
VideoChannel,
|
VideoChannel,
|
||||||
VideoChannelCreate,
|
VideoChannelCreate,
|
||||||
VideoChannelCreateResult,
|
VideoChannelCreateResult,
|
||||||
VideoChannelUpdate
|
VideoChannelUpdate,
|
||||||
|
VideosImportInChannelCreate
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
import { unwrapBody } from '../requests'
|
import { unwrapBody } from '../requests'
|
||||||
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
import { AbstractCommand, OverrideCommandOptions } from '../shared'
|
||||||
|
@ -182,11 +183,10 @@ export class ChannelsCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
importVideos (options: OverrideCommandOptions & {
|
importVideos (options: OverrideCommandOptions & VideosImportInChannelCreate & {
|
||||||
channelName: string
|
channelName: string
|
||||||
externalChannelUrl: string
|
|
||||||
}) {
|
}) {
|
||||||
const { channelName, externalChannelUrl } = options
|
const { channelName, externalChannelUrl, videoChannelSyncId } = options
|
||||||
|
|
||||||
const path = `/api/v1/video-channels/${channelName}/import-videos`
|
const path = `/api/v1/video-channels/${channelName}/import-videos`
|
||||||
|
|
||||||
|
@ -194,7 +194,7 @@ export class ChannelsCommand extends AbstractCommand {
|
||||||
...options,
|
...options,
|
||||||
|
|
||||||
path,
|
path,
|
||||||
fields: { externalChannelUrl },
|
fields: { externalChannelUrl, videoChannelSyncId },
|
||||||
implicitToken: true,
|
implicitToken: true,
|
||||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||||
})
|
})
|
||||||
|
|
|
@ -57,15 +57,17 @@ export class ImportsCommand extends AbstractCommand {
|
||||||
getMyVideoImports (options: OverrideCommandOptions & {
|
getMyVideoImports (options: OverrideCommandOptions & {
|
||||||
sort?: string
|
sort?: string
|
||||||
targetUrl?: string
|
targetUrl?: string
|
||||||
|
videoChannelSyncId?: number
|
||||||
|
search?: string
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const { sort, targetUrl } = options
|
const { sort, targetUrl, videoChannelSyncId, search } = options
|
||||||
const path = '/api/v1/users/me/videos/imports'
|
const path = '/api/v1/users/me/videos/imports'
|
||||||
|
|
||||||
return this.getRequestBody<ResultList<VideoImport>>({
|
return this.getRequestBody<ResultList<VideoImport>>({
|
||||||
...options,
|
...options,
|
||||||
|
|
||||||
path,
|
path,
|
||||||
query: { sort, targetUrl },
|
query: { sort, targetUrl, videoChannelSyncId, search },
|
||||||
implicitToken: true,
|
implicitToken: true,
|
||||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||||
})
|
})
|
||||||
|
|
|
@ -1187,6 +1187,20 @@ paths:
|
||||||
- $ref: '#/components/parameters/start'
|
- $ref: '#/components/parameters/start'
|
||||||
- $ref: '#/components/parameters/count'
|
- $ref: '#/components/parameters/count'
|
||||||
- $ref: '#/components/parameters/sort'
|
- $ref: '#/components/parameters/sort'
|
||||||
|
-
|
||||||
|
name: targetUrl
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: Filter on import target URL
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
-
|
||||||
|
name: videoChannelSyncId
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
description: Filter on imports created by a specific channel synchronization
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
|
|
Loading…
Reference in New Issue