]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability to list imports of a channel sync
authorChocobozzz <me@florianbigard.com>
Wed, 10 Aug 2022 09:51:13 +0000 (11:51 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 10 Aug 2022 12:32:00 +0000 (14:32 +0200)
37 files changed:
client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html
client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts
client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts
client/src/app/+my-library/my-video-imports/my-video-imports.component.html
client/src/app/+my-library/my-video-imports/my-video-imports.component.scss
client/src/app/+my-library/my-video-imports/my-video-imports.component.ts
client/src/app/shared/shared-main/video-channel/video-channel.service.ts
client/src/app/shared/shared-main/video/video-import.service.ts
server/controllers/api/users/me.ts
server/controllers/api/video-channel.ts
server/initializers/constants.ts
server/initializers/migrations/0735-video-channel-sync-import-foreign-key.ts [new file with mode: 0644]
server/lib/job-queue/handlers/video-channel-import.ts
server/lib/schedulers/video-channel-sync-latest-scheduler.ts
server/lib/sync-channel.ts
server/lib/video-import.ts
server/middlewares/validators/shared/index.ts
server/middlewares/validators/shared/video-channel-syncs.ts [new file with mode: 0644]
server/middlewares/validators/videos/video-channel-sync.ts
server/middlewares/validators/videos/video-channels.ts
server/middlewares/validators/videos/video-imports.ts
server/models/video/video-import.ts
server/tests/api/check-params/channel-import-videos.ts [new file with mode: 0644]
server/tests/api/check-params/index.ts
server/tests/api/check-params/video-channels.ts
server/tests/api/check-params/video-imports.ts
server/tests/api/videos/channel-import-videos.ts
server/tests/api/videos/video-channel-syncs.ts
server/tests/api/videos/video-imports.ts
shared/models/server/job.model.ts
shared/models/videos/import/index.ts
shared/models/videos/import/video-import.model.ts
shared/models/videos/import/videos-import-in-channel-create.model.ts [new file with mode: 0644]
shared/server-commands/server/server.ts
shared/server-commands/videos/channels-command.ts
shared/server-commands/videos/imports-command.ts
support/doc/api/openapi.yaml

index 5141607b155681170e67d75b9c18be86581e23da..c2fed8112f6e79d0afc26c6eddc9d992e834972b 100644 (file)
 
   <ng-template pTemplate="header">
     <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="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="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></th>
     </tr>
   </ng-template>
 
 
       <td>{{ videoChannelSync.createdAt | 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>
   </ng-template>
 </p-table>
index 81bdaf9f248233f7320fd622e968f90768c1dd9e..0c429e5dd1fecf45f22bf8ff7d5559d630650240 100644 (file)
@@ -100,7 +100,7 @@ export class MyVideoChannelSyncsComponent extends RestTable implements OnInit {
   }
 
   fullySynchronize (videoChannelSync: VideoChannelSync) {
-    this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl)
+    this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl, videoChannelSync.id)
       .subscribe({
         next: () => {
           this.notifier.success($localize`Full synchronization requested successfully for ${videoChannelSync.channel.displayName}.`)
index 83658260960c4ad4cadf240e60ece454c9cfb7bb..9ceb6dfd19932b93f4e24ed585b8e4069050ae0b 100644 (file)
@@ -59,7 +59,7 @@ export class VideoChannelSyncEditComponent extends FormReactive implements OnIni
     this.videoChannelSyncService.createSync(videoChannelSyncCreate)
       .pipe(mergeMap(({ videoChannelSync }) => {
         return importExistingVideos
-          ? this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl)
+          ? this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl, videoChannelSync.id)
           : Promise.resolve(null)
       }))
       .subscribe({
index fb0f6f5a392b2ab644827c7142f4e4cd63fb2436..866cd1a72d092c75370cf279acbb362f7c84b4f0 100644 (file)
@@ -3,9 +3,18 @@
   <ng-container i18n>My imports</ng-container>
 </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
   [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
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} imports"
   [expandedRowKeys]="expandedRows"
index 7acacd47f53d83feeb756c280ef1850cedce8a50..d9b12151eadb41a66d6dd78e3e15d5d99228373c 100644 (file)
@@ -8,3 +8,9 @@ pre {
 .video-import-error {
   color: #ff0000;
 }
+
+.button-link {
+  @include peertube-button-link;
+  @include grey-button;
+  @include button-with-icon(18px, 3px, -1px);
+}
index f01558061f786176962fde6c2af054d51d86e203..46d689bd1a30fd457f0a9f207505aa22ddb0f9a0 100644 (file)
@@ -33,12 +33,16 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
     switch (state) {
       case VideoImportState.FAILED:
         return 'badge-red'
+
       case VideoImportState.REJECTED:
         return 'badge-banned'
+
       case VideoImportState.PENDING:
         return 'badge-yellow'
+
       case VideoImportState.PROCESSING:
         return 'badge-blue'
+
       default:
         return 'badge-green'
     }
@@ -87,7 +91,7 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
   }
 
   protected reloadData () {
-    this.videoImportService.getMyVideoImports(this.pagination, this.sort)
+    this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search)
         .subscribe({
           next: resultList => {
             this.videoImports = resultList.data
index fa97025acb7fb6628b9ef54906ac93a2b6eaf98a..5e3985526a06e9cedfe53c4ca3f34dd794fa32fc 100644 (file)
@@ -3,7 +3,14 @@ import { catchError, map, tap } from 'rxjs/operators'
 import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/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 { Account } from '../account'
 import { AccountService } from '../account/account.service'
@@ -96,9 +103,15 @@ export class VideoChannelService {
                .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'
-    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)))
   }
 }
index 0a610ab1f530ef40c7efd17cb64f631a8bab57fc..f9720033a0bf16642b88526f19b54da589198f05 100644 (file)
@@ -43,10 +43,23 @@ export class VideoImportService {
                .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()
     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
                .get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params })
                .pipe(
index 595abcf959bd9ca4dcda8309cac27c3967d96161..00f580ee959f78abfa4977300116013d483acd43 100644 (file)
@@ -25,7 +25,13 @@ import {
   usersUpdateMeValidator,
   usersVideoRatingValidator
 } 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 { AccountModel } from '../../../models/account/account'
 import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
@@ -60,6 +66,7 @@ meRouter.get('/me/videos/imports',
   videoImportsSortValidator,
   setDefaultSort,
   setDefaultPagination,
+  getMyVideoImportsValidator,
   asyncMiddleware(getUserVideoImports)
 )
 
@@ -138,7 +145,7 @@ async function getUserVideoImports (req: express.Request, res: express.Response)
   const resultList = await VideoImportModel.listUserVideoImportsForApi({
     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))
index 89c7181bd86c2fc039deed9f0c8ed9027561a2d4..94285a78d203671247d15e3f1101d2b2850f3d4f 100644 (file)
@@ -6,7 +6,7 @@ import { ActorFollowModel } from '@server/models/actor/actor-follow'
 import { getServerActor } from '@server/models/application/application'
 import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
 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 { resetSequelizeInstance } from '../../helpers/database-utils'
 import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
@@ -166,7 +166,7 @@ videoChannelRouter.get('/:nameWithHost/followers',
 videoChannelRouter.post('/:nameWithHost/import-videos',
   authenticate,
   asyncMiddleware(videoChannelsNameWithHostValidator),
-  videoChannelImportVideosValidator,
+  asyncMiddleware(videoChannelImportVideosValidator),
   ensureIsLocalChannel,
   ensureCanManageChannel,
   asyncMiddleware(ensureChannelOwnerCanUpload),
@@ -418,13 +418,14 @@ async function listVideoChannelFollowers (req: express.Request, res: express.Res
 }
 
 async function importVideosInChannel (req: express.Request, res: express.Response) {
-  const { externalChannelUrl } = req.body
+  const { externalChannelUrl } = req.body as VideosImportInChannelCreate
 
   await JobQueue.Instance.createJob({
     type: 'video-channel-import',
     payload: {
       externalChannelUrl,
-      videoChannelId: res.locals.videoChannel.id
+      videoChannelId: res.locals.videoChannel.id,
+      partOfChannelSyncId: res.locals.videoChannelSync?.id
     }
   })
 
index 697a64d426021705fe0fcc2fa682ee3300d9d8e0..c2289ef360cb19b394437b8c913e1c27357f43ab 100644 (file)
@@ -25,7 +25,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 730
+const LAST_MIGRATION_VERSION = 735
 
 // ---------------------------------------------------------------------------
 
diff --git a/server/initializers/migrations/0735-video-channel-sync-import-foreign-key.ts b/server/initializers/migrations/0735-video-channel-sync-import-foreign-key.ts
new file mode 100644 (file)
index 0000000..ffe0b11
--- /dev/null
@@ -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
+}
index 9bdb2d269b8ddf10090100916c3a53aa198b629b..9aaad659e79b2bbcd63803bb78f4b420dd9282d3 100644 (file)
@@ -3,6 +3,8 @@ import { logger } from '@server/helpers/logger'
 import { CONFIG } from '@server/initializers/config'
 import { synchronizeChannel } from '@server/lib/sync-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'
 
 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
   if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
-    logger.error('Cannot import channel as the HTTP upload is disabled')
-    return
+    throw new Error('Cannot import channel as the HTTP upload is disabled')
   }
 
   if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
-    logger.error('Cannot import channel as the synchronization is disabled')
-    return
+    throw new Error('Cannot import channel as the synchronization is disabled')
+  }
+
+  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)
@@ -28,7 +37,8 @@ export async function processVideoChannelImport (job: Job) {
 
     await synchronizeChannel({
       channel: videoChannel,
-      externalChannelUrl: payload.externalChannelUrl
+      externalChannelUrl: payload.externalChannelUrl,
+      channelSync
     })
   } catch (err) {
     logger.error(`Failed to import channel ${videoChannel.name}`, { err })
index fd9a3529914b628285c30513403e5d2d5c36317f..491ddaa87149aeafc944d987876d482a70282f81 100644 (file)
@@ -36,10 +36,6 @@ export class VideoChannelSyncLatestScheduler extends AbstractScheduler {
 
         const onlyAfter = sync.lastSyncAt || sync.createdAt
 
-        sync.state = VideoChannelSyncState.PROCESSING
-        sync.lastSyncAt = new Date()
-        await sync.save()
-
         await synchronizeChannel({
           channel,
           externalChannelUrl: sync.externalChannelUrl,
index 50f80e6f9273c94de64d0cff33033a406902979b..eb5ca17031e004a981d2c2b0183cbaa3daa7ae52 100644 (file)
@@ -18,6 +18,12 @@ export async function synchronizeChannel (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 youtubeDL = new YoutubeDLWrapper(
     externalChannelUrl,
@@ -70,6 +76,7 @@ export async function synchronizeChannel (options: {
     children.push(job)
   }
 
+  // Will update the channel sync status
   const parent: CreateJobArgument = {
     type: 'after-video-channel-import',
     payload: {
index fb9306967fcd3c707be73bee18a5da8119ba0d61..de95116aa6d06cb1b495b19c0536801d9f7ddf0a 100644 (file)
@@ -206,7 +206,8 @@ async function buildYoutubeDLImport (options: {
     videoImportAttributes: {
       targetUrl,
       state: VideoImportState.PENDING,
-      userId: user.id
+      userId: user.id,
+      videoChannelSyncId: channelSync?.id
     }
   })
 
index fa89d05f2ed1f95c9f8bc65726eba9ca5b58a901..bbd03b248f0cbdec8b58e48501d5c88d5e12cd4b 100644 (file)
@@ -4,6 +4,7 @@ export * from './utils'
 export * from './video-blacklists'
 export * from './video-captions'
 export * from './video-channels'
+export * from './video-channel-syncs'
 export * from './video-comments'
 export * from './video-imports'
 export * from './video-ownerships'
diff --git a/server/middlewares/validators/shared/video-channel-syncs.ts b/server/middlewares/validators/shared/video-channel-syncs.ts
new file mode 100644 (file)
index 0000000..a6e51eb
--- /dev/null
@@ -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
+}
index b184982431c6efe3c5851f4038ddc994a19b474f..081f09bba1bb586c81ef5717a0d1cd52dafeb1a2 100644 (file)
@@ -3,10 +3,10 @@ import { body, param } from 'express-validator'
 import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
 import { logger } from '@server/helpers/logger'
 import { CONFIG } from '@server/initializers/config'
-import { VideoChannelModel } from '@server/models/video/video-channel'
 import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
 import { HttpStatusCode, VideoChannelSyncCreate } from '@shared/models'
 import { areValidationErrors, doesVideoChannelIdExist } from '../shared'
+import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs'
 
 export const ensureSyncIsEnabled = (req: express.Request, res: express.Response, next: express.NextFunction) => {
   if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
@@ -48,18 +48,8 @@ export const ensureSyncExists = [
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     if (areValidationErrors(req, res)) return
 
-    const syncId = parseInt(req.params.id, 10)
-    const sync = await VideoChannelSyncModel.loadWithChannel(syncId)
-
-    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)
+    if (!await doesVideoChannelSyncIdExist(+req.params.id, res)) return
+    if (!await doesVideoChannelIdExist(res.locals.videoChannelSync.videoChannelId, res)) return
 
     return next()
   }
index 88f8b814d8479f548da5ae8a50cf45753a61b68e..d53c777fac8ac7a20f59f602de31c943d57025c8 100644 (file)
@@ -3,8 +3,9 @@ import { body, param, query } from 'express-validator'
 import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
 import { CONFIG } from '@server/initializers/config'
 import { MChannelAccountDefault } from '@server/types/models'
+import { VideosImportInChannelCreate } from '@shared/models'
 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 {
   isVideoChannelDescriptionValid,
   isVideoChannelDisplayNameValid,
@@ -15,6 +16,7 @@ import { logger } from '../../../helpers/logger'
 import { ActorModel } from '../../../models/actor/actor'
 import { VideoChannelModel } from '../../../models/video/video-channel'
 import { areValidationErrors, checkUserQuota, doesVideoChannelNameWithHostExist } from '../shared'
+import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs'
 
 export const videoChannelsAddValidator = [
   body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
@@ -145,11 +147,17 @@ export const videoChannelsListValidator = [
 export const videoChannelImportVideosValidator = [
   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 })
 
     if (areValidationErrors(req, res)) return
 
+    const body: VideosImportInChannelCreate = req.body
+
     if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
       return res.fail({
         status: HttpStatusCode.FORBIDDEN_403,
@@ -157,6 +165,8 @@ export const videoChannelImportVideosValidator = [
       })
     }
 
+    if (body.videoChannelSyncId && !await doesVideoChannelSyncIdExist(body.videoChannelSyncId, res)) return
+
     return next()
   }
 ]
index 9c6d213c429d47baf2206924d8cfd0ba2833166a..3115acb21a8d6631c0d5090a77a7aa56462ca017 100644 (file)
@@ -1,5 +1,5 @@
 import express from 'express'
-import { body, param } from 'express-validator'
+import { body, param, query } from 'express-validator'
 import { isResolvingToUnicastOnly } from '@server/helpers/dns'
 import { isPreImportVideoAccepted } from '@server/lib/moderation'
 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 = [
   param('id')
     .custom(isIdValid).withMessage('Should have correct import id'),
@@ -143,7 +157,8 @@ const videoImportCancelValidator = [
 export {
   videoImportAddValidator,
   videoImportCancelValidator,
-  videoImportDeleteValidator
+  videoImportDeleteValidator,
+  getMyVideoImportsValidator
 }
 
 // ---------------------------------------------------------------------------
index b8e941623bc3545e2c90738c8faecb64877df60b..da6b92c7a1e6672dffbc3135ff713cba2c8a2d06 100644 (file)
@@ -1,4 +1,4 @@
-import { Op, WhereOptions } from 'sequelize'
+import { IncludeOptions, Op, WhereOptions } from 'sequelize'
 import {
   AfterUpdate,
   AllowNull,
@@ -22,8 +22,17 @@ import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../help
 import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
 import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
 import { UserModel } from '../user/user'
-import { getSort, throwIfNotValid } from '../utils'
+import { getSort, searchAttribute, throwIfNotValid } from '../utils'
 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(() => ({
   include: [
@@ -32,11 +41,11 @@ import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
       required: true
     },
     {
-      model: VideoModel.scope([
-        VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
-        VideoModelScopeNames.WITH_TAGS,
-        VideoModelScopeNames.WITH_THUMBNAILS
-      ]),
+      model: defaultVideoScope(),
+      required: false
+    },
+    {
+      model: VideoChannelSyncModel.unscoped(),
       required: false
     }
   ]
@@ -113,6 +122,18 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
   })
   Video: VideoModel
 
+  @ForeignKey(() => VideoChannelSyncModel)
+  @Column
+  videoChannelSyncId: number
+
+  @BelongsTo(() => VideoChannelSyncModel, {
+    foreignKey: {
+      allowNull: true
+    },
+    onDelete: 'set null'
+  })
+  VideoChannelSync: VideoChannelSyncModel
+
   @AfterUpdate
   static deleteVideoIfFailed (instance: VideoImportModel, options) {
     if (instance.state === VideoImportState.FAILED) {
@@ -132,23 +153,44 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
     count: number
     sort: string
 
+    search?: 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 include: IncludeOptions[] = [
+      {
+        attributes: [ 'id' ],
+        model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query
+        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: [
-        {
-          attributes: [ 'id' ],
-          model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query
-          required: true
-        }
-      ],
+      include,
       offset: start,
       limit: count,
       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) })
       : undefined
 
+    const videoChannelSync = this.VideoChannelSync
+      ? { id: this.VideoChannelSync.id, externalChannelUrl: this.VideoChannelSync.externalChannelUrl }
+      : undefined
+
     return {
       id: this.id,
 
@@ -210,7 +256,8 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
       error: this.error,
       updatedAt: this.updatedAt.toISOString(),
       createdAt: this.createdAt.toISOString(),
-      video
+      video,
+      videoChannelSync
     }
   }
 
diff --git a/server/tests/api/check-params/channel-import-videos.ts b/server/tests/api/check-params/channel-import-videos.ts
new file mode 100644 (file)
index 0000000..90d61f2
--- /dev/null
@@ -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 ])
+  })
+})
index 5f1168b53952d58b82da6f43003eaa18f763c455..149305f49e20fd469828a113d712cd7eabf14e40 100644 (file)
@@ -28,6 +28,7 @@ import './video-comments'
 import './video-files'
 import './video-imports'
 import './video-channel-syncs'
+import './channel-import-videos'
 import './video-playlists'
 import './video-source'
 import './video-studio'
index 337ea1dd46a66f89e320467fe8a3a7ab04d14af0..9024126c034760363c2d4147cd773a7847f42807 100644 (file)
@@ -3,8 +3,8 @@
 import 'mocha'
 import * as chai from 'chai'
 import { omit } from 'lodash'
-import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination, FIXTURE_URLS } from '@server/tests/shared'
-import { areHttpImportTestsDisabled, buildAbsoluteFixturePath } from '@shared/core-utils'
+import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
+import { buildAbsoluteFixturePath } from '@shared/core-utils'
 import { HttpStatusCode, VideoChannelUpdate } from '@shared/models'
 import {
   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 () {
     it('Should fail with a non authenticated user', async function () {
       await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
index 5cdd0d925ca9f55f48c651b707a768e722f9531c..85382b261e5a04061936e65435a05b0d15cc46da 100644 (file)
@@ -59,6 +59,15 @@ describe('Test video imports API validator', function () {
       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 () {
       await makeGetRequest({ url: server.url, path: myPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
     })
index f7540e1ba1d25902aca1cb63e8d3f2da08add5ed..7cfd02fbb4855319ac55c720797cc7f8a11bda34 100644 (file)
@@ -1,3 +1,5 @@
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
 import { expect } from 'chai'
 import { FIXTURE_URLS } from '@server/tests/shared'
 import { areHttpImportTestsDisabled } from '@shared/core-utils'
@@ -29,7 +31,7 @@ describe('Test videos import in a channel', function () {
         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)
 
         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)
       })
 
+      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 () {
         await server?.kill()
       })
index 229c01f6800ad97e780a37a59e645ff33c71cf51..835d3cb09627afe1fa21a7319f845ac6ea6fb9b5 100644 (file)
@@ -23,7 +23,10 @@ describe('Test channel synchronizations', function () {
     describe('Sync using ' + mode, function () {
       let server: PeerTubeServer
       let command: ChannelSyncsCommand
+
       let startTestDate: Date
+
+      let rootChannelSyncId: number
       const userInfo = {
         accessToken: '',
         username: 'user1',
@@ -90,6 +93,7 @@ describe('Test channel synchronizations', function () {
           token: server.accessToken,
           expectedStatus: HttpStatusCode.OK_200
         })
+        rootChannelSyncId = videoChannelSync.id
 
         // Ensure any missing video not already fetched will be considered as new
         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 () {
         await command.delete({ channelSyncId: userInfo.syncId })
 
index a487062a288d5cca6c1caf06f70a39bbc96e76fb..f082d4bd7d2446a994ac82c8bcebbb8ccdda9abd 100644 (file)
@@ -228,6 +228,15 @@ describe('Test video imports', function () {
         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 () {
         this.timeout(120_000)
 
index ba1f83684970fbf9aa1767ccd192b27976dd058b..9c0b5ea569256851d6587ab5900dcdcdf66b10bd 100644 (file)
@@ -236,6 +236,8 @@ export interface VideoStudioEditionPayload {
 export interface VideoChannelImportPayload {
   externalChannelUrl: string
   videoChannelId: number
+
+  partOfChannelSyncId?: number
 }
 
 export interface AfterVideoChannelImportPayload {
index 8884ee8f295ea4e91feb59da21f40dc64c8053d9..b38a67b5f8f9227aa230b6ce617f5f3c9f810257 100644 (file)
@@ -1,3 +1,4 @@
 export * from './video-import-create.model'
 export * from './video-import-state.enum'
 export * from './video-import.model'
+export * from './videos-import-in-channel-create.model'
index 92856c70f5262bb3a4f159a9c1c6de3c7e7ecbee..6aed7a91a1013472f67aa4370e65d36c4ee49de6 100644 (file)
@@ -16,4 +16,9 @@ export interface VideoImport {
   error?: string
 
   video?: Video & { tags: string[] }
+
+  videoChannelSync?: {
+    id: number
+    externalChannelUrl: string
+  }
 }
diff --git a/shared/models/videos/import/videos-import-in-channel-create.model.ts b/shared/models/videos/import/videos-import-in-channel-create.model.ts
new file mode 100644 (file)
index 0000000..fbfef63
--- /dev/null
@@ -0,0 +1,4 @@
+export interface VideosImportInChannelCreate {
+  externalChannelUrl: string
+  videoChannelSyncId?: number
+}
index 7acbc978fc892da569d76d9561971ee32a170753..c05d16ad2cb87285248adff27a1f30aa4de135a3 100644 (file)
@@ -2,7 +2,7 @@ import { ChildProcess, fork } from 'child_process'
 import { copy } from 'fs-extra'
 import { join } from 'path'
 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 { CLICommand } from '../cli'
 import { CustomPagesCommand } from '../custom-pages'
@@ -80,6 +80,7 @@ export class PeerTubeServer {
     }
 
     channel?: VideoChannel
+    videoChannelSync?: Partial<VideoChannelSync>
 
     video?: Video
     videoCreated?: VideoCreateResult
index a688a120fb75d1c9f56c6c748126fe43196c4213..385d0fe73ea9027b55f062bc5d0c72d16666a853 100644 (file)
@@ -6,7 +6,8 @@ import {
   VideoChannel,
   VideoChannelCreate,
   VideoChannelCreateResult,
-  VideoChannelUpdate
+  VideoChannelUpdate,
+  VideosImportInChannelCreate
 } from '@shared/models'
 import { unwrapBody } from '../requests'
 import { AbstractCommand, OverrideCommandOptions } from '../shared'
@@ -182,11 +183,10 @@ export class ChannelsCommand extends AbstractCommand {
     })
   }
 
-  importVideos (options: OverrideCommandOptions & {
+  importVideos (options: OverrideCommandOptions & VideosImportInChannelCreate & {
     channelName: string
-    externalChannelUrl: string
   }) {
-    const { channelName, externalChannelUrl } = options
+    const { channelName, externalChannelUrl, videoChannelSyncId } = options
 
     const path = `/api/v1/video-channels/${channelName}/import-videos`
 
@@ -194,7 +194,7 @@ export class ChannelsCommand extends AbstractCommand {
       ...options,
 
       path,
-      fields: { externalChannelUrl },
+      fields: { externalChannelUrl, videoChannelSyncId },
       implicitToken: true,
       defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
     })
index c931ac481c2e7179841a53131194c0bc45105a44..07d810ec1f49e10c452b156c6962e78168e78bc4 100644 (file)
@@ -57,15 +57,17 @@ export class ImportsCommand extends AbstractCommand {
   getMyVideoImports (options: OverrideCommandOptions & {
     sort?: 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'
 
     return this.getRequestBody<ResultList<VideoImport>>({
       ...options,
 
       path,
-      query: { sort, targetUrl },
+      query: { sort, targetUrl, videoChannelSyncId, search },
       implicitToken: true,
       defaultExpectedStatus: HttpStatusCode.OK_200
     })
index ac8cde5653a8a97224e0d1f7e37f5312a63caa94..c4bc507fdc05a95092540e30f425bbedf572e5b9 100644 (file)
@@ -1187,6 +1187,20 @@ paths:
         - $ref: '#/components/parameters/start'
         - $ref: '#/components/parameters/count'
         - $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:
         '200':
           description: successful operation