aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-08-10 11:51:13 +0200
committerChocobozzz <me@florianbigard.com>2022-08-10 14:32:00 +0200
commita3b472a12ec6e57dbe2f650419f8064864686eab (patch)
treef36559488e34493c029b686772e986902150a647
parent0567049a9819d67070aa6d548a75a7e632a4aaa4 (diff)
downloadPeerTube-a3b472a12ec6e57dbe2f650419f8064864686eab.tar.gz
PeerTube-a3b472a12ec6e57dbe2f650419f8064864686eab.tar.zst
PeerTube-a3b472a12ec6e57dbe2f650419f8064864686eab.zip
Add ability to list imports of a channel sync
-rw-r--r--client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html9
-rw-r--r--client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts2
-rw-r--r--client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts2
-rw-r--r--client/src/app/+my-library/my-video-imports/my-video-imports.component.html11
-rw-r--r--client/src/app/+my-library/my-video-imports/my-video-imports.component.scss6
-rw-r--r--client/src/app/+my-library/my-video-imports/my-video-imports.component.ts6
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts19
-rw-r--r--client/src/app/shared/shared-main/video/video-import.service.ts15
-rw-r--r--server/controllers/api/users/me.ts11
-rw-r--r--server/controllers/api/video-channel.ts9
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0735-video-channel-sync-import-foreign-key.ts32
-rw-r--r--server/lib/job-queue/handlers/video-channel-import.ts20
-rw-r--r--server/lib/schedulers/video-channel-sync-latest-scheduler.ts4
-rw-r--r--server/lib/sync-channel.ts7
-rw-r--r--server/lib/video-import.ts3
-rw-r--r--server/middlewares/validators/shared/index.ts1
-rw-r--r--server/middlewares/validators/shared/video-channel-syncs.ts24
-rw-r--r--server/middlewares/validators/videos/video-channel-sync.ts16
-rw-r--r--server/middlewares/validators/videos/video-channels.ts14
-rw-r--r--server/middlewares/validators/videos/video-imports.ts19
-rw-r--r--server/models/video/video-import.ts79
-rw-r--r--server/tests/api/check-params/channel-import-videos.ts172
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/video-channels.ts113
-rw-r--r--server/tests/api/check-params/video-imports.ts9
-rw-r--r--server/tests/api/videos/channel-import-videos.ts72
-rw-r--r--server/tests/api/videos/video-channel-syncs.ts12
-rw-r--r--server/tests/api/videos/video-imports.ts9
-rw-r--r--shared/models/server/job.model.ts2
-rw-r--r--shared/models/videos/import/index.ts1
-rw-r--r--shared/models/videos/import/video-import.model.ts5
-rw-r--r--shared/models/videos/import/videos-import-in-channel-create.model.ts4
-rw-r--r--shared/server-commands/server/server.ts3
-rw-r--r--shared/server-commands/videos/channels-command.ts10
-rw-r--r--shared/server-commands/videos/imports-command.ts6
-rw-r--r--support/doc/api/openapi.yaml14
37 files changed, 565 insertions, 179 deletions
diff --git a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html
index 5141607b1..c2fed8112 100644
--- a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html
+++ b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html
@@ -30,12 +30,13 @@
30 30
31 <ng-template pTemplate="header"> 31 <ng-template pTemplate="header">
32 <tr> 32 <tr>
33 <th style="width: 10%"><my-global-icon iconName="columns"></my-global-icon></th> 33 <th style="width: 10%"></th>
34 <th style="width: 25%" i18n pSortableColumn="externalChannelUrl">External Channel <p-sortIcon field="externalChannelUrl"></p-sortIcon></th> 34 <th style="width: 25%" i18n pSortableColumn="externalChannelUrl">External Channel <p-sortIcon field="externalChannelUrl"></p-sortIcon></th>
35 <th style="width: 25%" i18n pSortableColumn="videoChannel">Channel <p-sortIcon field="videoChannel"></p-sortIcon></th> 35 <th style="width: 25%" i18n pSortableColumn="videoChannel">Channel <p-sortIcon field="videoChannel"></p-sortIcon></th>
36 <th style="width: 10%" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th> 36 <th style="width: 10%" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
37 <th style="width: 10%" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 37 <th style="width: 10%" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
38 <th style="width: 10%" i18n pSortableColumn="lastSyncAt">Last synchronization at <p-sortIcon field="lastSyncAt"></p-sortIcon></th> 38 <th style="width: 10%" i18n pSortableColumn="lastSyncAt">Last synchronization at <p-sortIcon field="lastSyncAt"></p-sortIcon></th>
39 <th></th>
39 </tr> 40 </tr>
40 </ng-template> 41 </ng-template>
41 42
@@ -78,6 +79,12 @@
78 79
79 <td>{{ videoChannelSync.createdAt | date: 'short' }}</td> 80 <td>{{ videoChannelSync.createdAt | date: 'short' }}</td>
80 <td>{{ videoChannelSync.lastSyncAt | date: 'short' }}</td> 81 <td>{{ videoChannelSync.lastSyncAt | date: 'short' }}</td>
82
83 <td>
84 <a i18n routerLink="/my-library/video-imports" [queryParams]="{ search: 'videoChannelSyncId:' + videoChannelSync.id }" class="peertube-button-link grey-button">
85 List imports
86 </a>
87 </td>
81 </tr> 88 </tr>
82 </ng-template> 89 </ng-template>
83</p-table> 90</p-table>
diff --git a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts
index 81bdaf9f2..0c429e5dd 100644
--- a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts
+++ b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts
@@ -100,7 +100,7 @@ export class MyVideoChannelSyncsComponent extends RestTable implements OnInit {
100 } 100 }
101 101
102 fullySynchronize (videoChannelSync: VideoChannelSync) { 102 fullySynchronize (videoChannelSync: VideoChannelSync) {
103 this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl) 103 this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl, videoChannelSync.id)
104 .subscribe({ 104 .subscribe({
105 next: () => { 105 next: () => {
106 this.notifier.success($localize`Full synchronization requested successfully for ${videoChannelSync.channel.displayName}.`) 106 this.notifier.success($localize`Full synchronization requested successfully for ${videoChannelSync.channel.displayName}.`)
diff --git a/client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts b/client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts
index 836582609..9ceb6dfd1 100644
--- a/client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts
+++ b/client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts
@@ -59,7 +59,7 @@ export class VideoChannelSyncEditComponent extends FormReactive implements OnIni
59 this.videoChannelSyncService.createSync(videoChannelSyncCreate) 59 this.videoChannelSyncService.createSync(videoChannelSyncCreate)
60 .pipe(mergeMap(({ videoChannelSync }) => { 60 .pipe(mergeMap(({ videoChannelSync }) => {
61 return importExistingVideos 61 return importExistingVideos
62 ? this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl) 62 ? this.videoChannelService.importVideos(videoChannelSync.channel.name, videoChannelSync.externalChannelUrl, videoChannelSync.id)
63 : Promise.resolve(null) 63 : Promise.resolve(null)
64 })) 64 }))
65 .subscribe({ 65 .subscribe({
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html
index fb0f6f5a3..866cd1a72 100644
--- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.html
+++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.html
@@ -3,9 +3,18 @@
3 <ng-container i18n>My imports</ng-container> 3 <ng-container i18n>My imports</ng-container>
4</h1> 4</h1>
5 5
6<div class="mb-4 d-flex justify-content-between">
7 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
8
9 <a routerLink="/my-library/video-channel-syncs" class="button-link">
10 <my-global-icon iconName="repeat" aria-hidden="true"></my-global-icon>
11 <ng-container i18n>My synchronizations</ng-container>
12 </a>
13</div>
14
6<p-table 15<p-table
7 [value]="videoImports" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" 16 [value]="videoImports" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
8 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" 17 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" dataKey="id"
9 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 18 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
10 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} imports" 19 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} imports"
11 [expandedRowKeys]="expandedRows" 20 [expandedRowKeys]="expandedRows"
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.scss b/client/src/app/+my-library/my-video-imports/my-video-imports.component.scss
index 7acacd47f..d9b12151e 100644
--- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.scss
+++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.scss
@@ -8,3 +8,9 @@ pre {
8.video-import-error { 8.video-import-error {
9 color: #ff0000; 9 color: #ff0000;
10} 10}
11
12.button-link {
13 @include peertube-button-link;
14 @include grey-button;
15 @include button-with-icon(18px, 3px, -1px);
16}
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts
index f01558061..46d689bd1 100644
--- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts
+++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts
@@ -33,12 +33,16 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
33 switch (state) { 33 switch (state) {
34 case VideoImportState.FAILED: 34 case VideoImportState.FAILED:
35 return 'badge-red' 35 return 'badge-red'
36
36 case VideoImportState.REJECTED: 37 case VideoImportState.REJECTED:
37 return 'badge-banned' 38 return 'badge-banned'
39
38 case VideoImportState.PENDING: 40 case VideoImportState.PENDING:
39 return 'badge-yellow' 41 return 'badge-yellow'
42
40 case VideoImportState.PROCESSING: 43 case VideoImportState.PROCESSING:
41 return 'badge-blue' 44 return 'badge-blue'
45
42 default: 46 default:
43 return 'badge-green' 47 return 'badge-green'
44 } 48 }
@@ -87,7 +91,7 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
87 } 91 }
88 92
89 protected reloadData () { 93 protected reloadData () {
90 this.videoImportService.getMyVideoImports(this.pagination, this.sort) 94 this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search)
91 .subscribe({ 95 .subscribe({
92 next: resultList => { 96 next: resultList => {
93 this.videoImports = resultList.data 97 this.videoImports = resultList.data
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
index fa97025ac..5e3985526 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
@@ -3,7 +3,14 @@ import { catchError, map, tap } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' 5import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
6import { ActorImage, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models' 6import {
7 ActorImage,
8 ResultList,
9 VideoChannel as VideoChannelServer,
10 VideoChannelCreate,
11 VideoChannelUpdate,
12 VideosImportInChannelCreate
13} from '@shared/models'
7import { environment } from '../../../../environments/environment' 14import { environment } from '../../../../environments/environment'
8import { Account } from '../account' 15import { Account } from '../account'
9import { AccountService } from '../account/account.service' 16import { AccountService } from '../account/account.service'
@@ -96,9 +103,15 @@ export class VideoChannelService {
96 .pipe(catchError(err => this.restExtractor.handleError(err))) 103 .pipe(catchError(err => this.restExtractor.handleError(err)))
97 } 104 }
98 105
99 importVideos (videoChannelName: string, externalChannelUrl: string) { 106 importVideos (videoChannelName: string, externalChannelUrl: string, syncId?: number) {
100 const path = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/import-videos' 107 const path = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/import-videos'
101 return this.authHttp.post(path, { externalChannelUrl }) 108
109 const body: VideosImportInChannelCreate = {
110 externalChannelUrl,
111 videoChannelSyncId: syncId
112 }
113
114 return this.authHttp.post(path, body)
102 .pipe(catchError(err => this.restExtractor.handleError(err))) 115 .pipe(catchError(err => this.restExtractor.handleError(err)))
103 } 116 }
104} 117}
diff --git a/client/src/app/shared/shared-main/video/video-import.service.ts b/client/src/app/shared/shared-main/video/video-import.service.ts
index 0a610ab1f..f9720033a 100644
--- a/client/src/app/shared/shared-main/video/video-import.service.ts
+++ b/client/src/app/shared/shared-main/video/video-import.service.ts
@@ -43,10 +43,23 @@ export class VideoImportService {
43 .pipe(catchError(res => this.restExtractor.handleError(res))) 43 .pipe(catchError(res => this.restExtractor.handleError(res)))
44 } 44 }
45 45
46 getMyVideoImports (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoImport>> { 46 getMyVideoImports (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<VideoImport>> {
47 let params = new HttpParams() 47 let params = new HttpParams()
48 params = this.restService.addRestGetParams(params, pagination, sort) 48 params = this.restService.addRestGetParams(params, pagination, sort)
49 49
50 if (search) {
51 const filters = this.restService.parseQueryStringFilter(search, {
52 videoChannelSyncId: {
53 prefix: 'videoChannelSyncId:'
54 },
55 targetUrl: {
56 prefix: 'targetUrl:'
57 }
58 })
59
60 params = this.restService.addObjectParams(params, filters)
61 }
62
50 return this.authHttp 63 return this.authHttp
51 .get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params }) 64 .get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params })
52 .pipe( 65 .pipe(
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 595abcf95..00f580ee9 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -25,7 +25,13 @@ import {
25 usersUpdateMeValidator, 25 usersUpdateMeValidator,
26 usersVideoRatingValidator 26 usersVideoRatingValidator
27} from '../../../middlewares' 27} from '../../../middlewares'
28import { deleteMeValidator, usersVideosValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' 28import {
29 deleteMeValidator,
30 getMyVideoImportsValidator,
31 usersVideosValidator,
32 videoImportsSortValidator,
33 videosSortValidator
34} from '../../../middlewares/validators'
29import { updateAvatarValidator } from '../../../middlewares/validators/actor-image' 35import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
30import { AccountModel } from '../../../models/account/account' 36import { AccountModel } from '../../../models/account/account'
31import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 37import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
@@ -60,6 +66,7 @@ meRouter.get('/me/videos/imports',
60 videoImportsSortValidator, 66 videoImportsSortValidator,
61 setDefaultSort, 67 setDefaultSort,
62 setDefaultPagination, 68 setDefaultPagination,
69 getMyVideoImportsValidator,
63 asyncMiddleware(getUserVideoImports) 70 asyncMiddleware(getUserVideoImports)
64) 71)
65 72
@@ -138,7 +145,7 @@ async function getUserVideoImports (req: express.Request, res: express.Response)
138 const resultList = await VideoImportModel.listUserVideoImportsForApi({ 145 const resultList = await VideoImportModel.listUserVideoImportsForApi({
139 userId: user.id, 146 userId: user.id,
140 147
141 ...pick(req.query, [ 'targetUrl', 'start', 'count', 'sort' ]) 148 ...pick(req.query, [ 'targetUrl', 'start', 'count', 'sort', 'search', 'videoChannelSyncId' ])
142 }) 149 })
143 150
144 return res.json(getFormattedObjects(resultList.data, resultList.total)) 151 return res.json(getFormattedObjects(resultList.data, resultList.total))
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 89c7181bd..94285a78d 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -6,7 +6,7 @@ import { ActorFollowModel } from '@server/models/actor/actor-follow'
6import { getServerActor } from '@server/models/application/application' 6import { getServerActor } from '@server/models/application/application'
7import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' 7import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils'
8import { MChannelBannerAccountDefault } from '@server/types/models' 8import { MChannelBannerAccountDefault } from '@server/types/models'
9import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate } from '@shared/models' 9import { ActorImageType, HttpStatusCode, VideoChannelCreate, VideoChannelUpdate, VideosImportInChannelCreate } from '@shared/models'
10import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' 10import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
11import { resetSequelizeInstance } from '../../helpers/database-utils' 11import { resetSequelizeInstance } from '../../helpers/database-utils'
12import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 12import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
@@ -166,7 +166,7 @@ videoChannelRouter.get('/:nameWithHost/followers',
166videoChannelRouter.post('/:nameWithHost/import-videos', 166videoChannelRouter.post('/:nameWithHost/import-videos',
167 authenticate, 167 authenticate,
168 asyncMiddleware(videoChannelsNameWithHostValidator), 168 asyncMiddleware(videoChannelsNameWithHostValidator),
169 videoChannelImportVideosValidator, 169 asyncMiddleware(videoChannelImportVideosValidator),
170 ensureIsLocalChannel, 170 ensureIsLocalChannel,
171 ensureCanManageChannel, 171 ensureCanManageChannel,
172 asyncMiddleware(ensureChannelOwnerCanUpload), 172 asyncMiddleware(ensureChannelOwnerCanUpload),
@@ -418,13 +418,14 @@ async function listVideoChannelFollowers (req: express.Request, res: express.Res
418} 418}
419 419
420async function importVideosInChannel (req: express.Request, res: express.Response) { 420async function importVideosInChannel (req: express.Request, res: express.Response) {
421 const { externalChannelUrl } = req.body 421 const { externalChannelUrl } = req.body as VideosImportInChannelCreate
422 422
423 await JobQueue.Instance.createJob({ 423 await JobQueue.Instance.createJob({
424 type: 'video-channel-import', 424 type: 'video-channel-import',
425 payload: { 425 payload: {
426 externalChannelUrl, 426 externalChannelUrl,
427 videoChannelId: res.locals.videoChannel.id 427 videoChannelId: res.locals.videoChannel.id,
428 partOfChannelSyncId: res.locals.videoChannelSync?.id
428 } 429 }
429 }) 430 })
430 431
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 697a64d42..c2289ef36 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -25,7 +25,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
25 25
26// --------------------------------------------------------------------------- 26// ---------------------------------------------------------------------------
27 27
28const LAST_MIGRATION_VERSION = 730 28const LAST_MIGRATION_VERSION = 735
29 29
30// --------------------------------------------------------------------------- 30// ---------------------------------------------------------------------------
31 31
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
index 000000000..ffe0b11ab
--- /dev/null
+++ b/server/initializers/migrations/0735-video-channel-sync-import-foreign-key.ts
@@ -0,0 +1,32 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 await utils.queryInterface.addColumn('videoImport', 'videoChannelSyncId', {
10 type: Sequelize.INTEGER,
11 defaultValue: null,
12 allowNull: true,
13 references: {
14 model: 'videoChannelSync',
15 key: 'id'
16 },
17 onUpdate: 'CASCADE',
18 onDelete: 'SET NULL'
19 }, { transaction: utils.transaction })
20}
21
22async function down (utils: {
23 queryInterface: Sequelize.QueryInterface
24 transaction: Sequelize.Transaction
25}) {
26 await utils.queryInterface.dropTable('videoChannelSync', { transaction: utils.transaction })
27}
28
29export {
30 up,
31 down
32}
diff --git a/server/lib/job-queue/handlers/video-channel-import.ts b/server/lib/job-queue/handlers/video-channel-import.ts
index 9bdb2d269..9aaad659e 100644
--- a/server/lib/job-queue/handlers/video-channel-import.ts
+++ b/server/lib/job-queue/handlers/video-channel-import.ts
@@ -3,6 +3,8 @@ import { logger } from '@server/helpers/logger'
3import { CONFIG } from '@server/initializers/config' 3import { CONFIG } from '@server/initializers/config'
4import { synchronizeChannel } from '@server/lib/sync-channel' 4import { synchronizeChannel } from '@server/lib/sync-channel'
5import { VideoChannelModel } from '@server/models/video/video-channel' 5import { VideoChannelModel } from '@server/models/video/video-channel'
6import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
7import { MChannelSync } from '@server/types/models'
6import { VideoChannelImportPayload } from '@shared/models' 8import { VideoChannelImportPayload } from '@shared/models'
7 9
8export async function processVideoChannelImport (job: Job) { 10export async function processVideoChannelImport (job: Job) {
@@ -12,13 +14,20 @@ export async function processVideoChannelImport (job: Job) {
12 14
13 // Channel import requires only http upload to be allowed 15 // Channel import requires only http upload to be allowed
14 if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) { 16 if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
15 logger.error('Cannot import channel as the HTTP upload is disabled') 17 throw new Error('Cannot import channel as the HTTP upload is disabled')
16 return
17 } 18 }
18 19
19 if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) { 20 if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
20 logger.error('Cannot import channel as the synchronization is disabled') 21 throw new Error('Cannot import channel as the synchronization is disabled')
21 return 22 }
23
24 let channelSync: MChannelSync
25 if (payload.partOfChannelSyncId) {
26 channelSync = await VideoChannelSyncModel.loadWithChannel(payload.partOfChannelSyncId)
27
28 if (!channelSync) {
29 throw new Error('Unlnown channel sync specified in videos channel import')
30 }
22 } 31 }
23 32
24 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(payload.videoChannelId) 33 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(payload.videoChannelId)
@@ -28,7 +37,8 @@ export async function processVideoChannelImport (job: Job) {
28 37
29 await synchronizeChannel({ 38 await synchronizeChannel({
30 channel: videoChannel, 39 channel: videoChannel,
31 externalChannelUrl: payload.externalChannelUrl 40 externalChannelUrl: payload.externalChannelUrl,
41 channelSync
32 }) 42 })
33 } catch (err) { 43 } catch (err) {
34 logger.error(`Failed to import channel ${videoChannel.name}`, { err }) 44 logger.error(`Failed to import channel ${videoChannel.name}`, { err })
diff --git a/server/lib/schedulers/video-channel-sync-latest-scheduler.ts b/server/lib/schedulers/video-channel-sync-latest-scheduler.ts
index fd9a35299..491ddaa87 100644
--- a/server/lib/schedulers/video-channel-sync-latest-scheduler.ts
+++ b/server/lib/schedulers/video-channel-sync-latest-scheduler.ts
@@ -36,10 +36,6 @@ export class VideoChannelSyncLatestScheduler extends AbstractScheduler {
36 36
37 const onlyAfter = sync.lastSyncAt || sync.createdAt 37 const onlyAfter = sync.lastSyncAt || sync.createdAt
38 38
39 sync.state = VideoChannelSyncState.PROCESSING
40 sync.lastSyncAt = new Date()
41 await sync.save()
42
43 await synchronizeChannel({ 39 await synchronizeChannel({
44 channel, 40 channel,
45 externalChannelUrl: sync.externalChannelUrl, 41 externalChannelUrl: sync.externalChannelUrl,
diff --git a/server/lib/sync-channel.ts b/server/lib/sync-channel.ts
index 50f80e6f9..eb5ca1703 100644
--- a/server/lib/sync-channel.ts
+++ b/server/lib/sync-channel.ts
@@ -18,6 +18,12 @@ export async function synchronizeChannel (options: {
18}) { 18}) {
19 const { channel, externalChannelUrl, videosCountLimit, onlyAfter, channelSync } = options 19 const { channel, externalChannelUrl, videosCountLimit, onlyAfter, channelSync } = options
20 20
21 if (channelSync) {
22 channelSync.state = VideoChannelSyncState.PROCESSING
23 channelSync.lastSyncAt = new Date()
24 await channelSync.save()
25 }
26
21 const user = await UserModel.loadByChannelActorId(channel.actorId) 27 const user = await UserModel.loadByChannelActorId(channel.actorId)
22 const youtubeDL = new YoutubeDLWrapper( 28 const youtubeDL = new YoutubeDLWrapper(
23 externalChannelUrl, 29 externalChannelUrl,
@@ -70,6 +76,7 @@ export async function synchronizeChannel (options: {
70 children.push(job) 76 children.push(job)
71 } 77 }
72 78
79 // Will update the channel sync status
73 const parent: CreateJobArgument = { 80 const parent: CreateJobArgument = {
74 type: 'after-video-channel-import', 81 type: 'after-video-channel-import',
75 payload: { 82 payload: {
diff --git a/server/lib/video-import.ts b/server/lib/video-import.ts
index fb9306967..de95116aa 100644
--- a/server/lib/video-import.ts
+++ b/server/lib/video-import.ts
@@ -206,7 +206,8 @@ async function buildYoutubeDLImport (options: {
206 videoImportAttributes: { 206 videoImportAttributes: {
207 targetUrl, 207 targetUrl,
208 state: VideoImportState.PENDING, 208 state: VideoImportState.PENDING,
209 userId: user.id 209 userId: user.id,
210 videoChannelSyncId: channelSync?.id
210 } 211 }
211 }) 212 })
212 213
diff --git a/server/middlewares/validators/shared/index.ts b/server/middlewares/validators/shared/index.ts
index fa89d05f2..bbd03b248 100644
--- a/server/middlewares/validators/shared/index.ts
+++ b/server/middlewares/validators/shared/index.ts
@@ -4,6 +4,7 @@ export * from './utils'
4export * from './video-blacklists' 4export * from './video-blacklists'
5export * from './video-captions' 5export * from './video-captions'
6export * from './video-channels' 6export * from './video-channels'
7export * from './video-channel-syncs'
7export * from './video-comments' 8export * from './video-comments'
8export * from './video-imports' 9export * from './video-imports'
9export * from './video-ownerships' 10export * 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
index 000000000..a6e51eb97
--- /dev/null
+++ b/server/middlewares/validators/shared/video-channel-syncs.ts
@@ -0,0 +1,24 @@
1import express from 'express'
2import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
3import { HttpStatusCode } from '@shared/models'
4
5async function doesVideoChannelSyncIdExist (id: number, res: express.Response) {
6 const sync = await VideoChannelSyncModel.loadWithChannel(+id)
7
8 if (!sync) {
9 res.fail({
10 status: HttpStatusCode.NOT_FOUND_404,
11 message: 'Video channel sync not found'
12 })
13 return false
14 }
15
16 res.locals.videoChannelSync = sync
17 return true
18}
19
20// ---------------------------------------------------------------------------
21
22export {
23 doesVideoChannelSyncIdExist
24}
diff --git a/server/middlewares/validators/videos/video-channel-sync.ts b/server/middlewares/validators/videos/video-channel-sync.ts
index b18498243..081f09bba 100644
--- a/server/middlewares/validators/videos/video-channel-sync.ts
+++ b/server/middlewares/validators/videos/video-channel-sync.ts
@@ -3,10 +3,10 @@ import { body, param } from 'express-validator'
3import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc' 3import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
4import { logger } from '@server/helpers/logger' 4import { logger } from '@server/helpers/logger'
5import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
6import { VideoChannelModel } from '@server/models/video/video-channel'
7import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync' 6import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync'
8import { HttpStatusCode, VideoChannelSyncCreate } from '@shared/models' 7import { HttpStatusCode, VideoChannelSyncCreate } from '@shared/models'
9import { areValidationErrors, doesVideoChannelIdExist } from '../shared' 8import { areValidationErrors, doesVideoChannelIdExist } from '../shared'
9import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs'
10 10
11export const ensureSyncIsEnabled = (req: express.Request, res: express.Response, next: express.NextFunction) => { 11export const ensureSyncIsEnabled = (req: express.Request, res: express.Response, next: express.NextFunction) => {
12 if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) { 12 if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
@@ -48,18 +48,8 @@ export const ensureSyncExists = [
48 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 48 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 if (areValidationErrors(req, res)) return 49 if (areValidationErrors(req, res)) return
50 50
51 const syncId = parseInt(req.params.id, 10) 51 if (!await doesVideoChannelSyncIdExist(+req.params.id, res)) return
52 const sync = await VideoChannelSyncModel.loadWithChannel(syncId) 52 if (!await doesVideoChannelIdExist(res.locals.videoChannelSync.videoChannelId, res)) return
53
54 if (!sync) {
55 return res.fail({
56 status: HttpStatusCode.NOT_FOUND_404,
57 message: 'Synchronization not found'
58 })
59 }
60
61 res.locals.videoChannelSync = sync
62 res.locals.videoChannel = await VideoChannelModel.loadAndPopulateAccount(sync.videoChannelId)
63 53
64 return next() 54 return next()
65 } 55 }
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index 88f8b814d..d53c777fa 100644
--- a/server/middlewares/validators/videos/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -3,8 +3,9 @@ import { body, param, query } from 'express-validator'
3import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc' 3import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
4import { CONFIG } from '@server/initializers/config' 4import { CONFIG } from '@server/initializers/config'
5import { MChannelAccountDefault } from '@server/types/models' 5import { MChannelAccountDefault } from '@server/types/models'
6import { VideosImportInChannelCreate } from '@shared/models'
6import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' 7import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
7import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' 8import { isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
8import { 9import {
9 isVideoChannelDescriptionValid, 10 isVideoChannelDescriptionValid,
10 isVideoChannelDisplayNameValid, 11 isVideoChannelDisplayNameValid,
@@ -15,6 +16,7 @@ import { logger } from '../../../helpers/logger'
15import { ActorModel } from '../../../models/actor/actor' 16import { ActorModel } from '../../../models/actor/actor'
16import { VideoChannelModel } from '../../../models/video/video-channel' 17import { VideoChannelModel } from '../../../models/video/video-channel'
17import { areValidationErrors, checkUserQuota, doesVideoChannelNameWithHostExist } from '../shared' 18import { areValidationErrors, checkUserQuota, doesVideoChannelNameWithHostExist } from '../shared'
19import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs'
18 20
19export const videoChannelsAddValidator = [ 21export const videoChannelsAddValidator = [
20 body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'), 22 body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
@@ -145,11 +147,17 @@ export const videoChannelsListValidator = [
145export const videoChannelImportVideosValidator = [ 147export const videoChannelImportVideosValidator = [
146 body('externalChannelUrl').custom(isUrlValid).withMessage('Should have a valid channel url'), 148 body('externalChannelUrl').custom(isUrlValid).withMessage('Should have a valid channel url'),
147 149
148 (req: express.Request, res: express.Response, next: express.NextFunction) => { 150 body('videoChannelSyncId')
151 .optional()
152 .custom(isIdValid).withMessage('Should have a valid channel sync id'),
153
154 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
149 logger.debug('Checking videoChannelImport parameters', { parameters: req.body }) 155 logger.debug('Checking videoChannelImport parameters', { parameters: req.body })
150 156
151 if (areValidationErrors(req, res)) return 157 if (areValidationErrors(req, res)) return
152 158
159 const body: VideosImportInChannelCreate = req.body
160
153 if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) { 161 if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
154 return res.fail({ 162 return res.fail({
155 status: HttpStatusCode.FORBIDDEN_403, 163 status: HttpStatusCode.FORBIDDEN_403,
@@ -157,6 +165,8 @@ export const videoChannelImportVideosValidator = [
157 }) 165 })
158 } 166 }
159 167
168 if (body.videoChannelSyncId && !await doesVideoChannelSyncIdExist(body.videoChannelSyncId, res)) return
169
160 return next() 170 return next()
161 } 171 }
162] 172]
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts
index 9c6d213c4..3115acb21 100644
--- a/server/middlewares/validators/videos/video-imports.ts
+++ b/server/middlewares/validators/videos/video-imports.ts
@@ -1,5 +1,5 @@
1import express from 'express' 1import express from 'express'
2import { body, param } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { isResolvingToUnicastOnly } from '@server/helpers/dns' 3import { isResolvingToUnicastOnly } from '@server/helpers/dns'
4import { isPreImportVideoAccepted } from '@server/lib/moderation' 4import { isPreImportVideoAccepted } from '@server/lib/moderation'
5import { Hooks } from '@server/lib/plugins/hooks' 5import { Hooks } from '@server/lib/plugins/hooks'
@@ -92,6 +92,20 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
92 } 92 }
93]) 93])
94 94
95const getMyVideoImportsValidator = [
96 query('videoChannelSyncId')
97 .optional()
98 .custom(isIdValid).withMessage('Should have correct videoChannelSync id'),
99
100 (req: express.Request, res: express.Response, next: express.NextFunction) => {
101 logger.debug('Checking getMyVideoImportsValidator parameters', { parameters: req.params })
102
103 if (areValidationErrors(req, res)) return
104
105 return next()
106 }
107]
108
95const videoImportDeleteValidator = [ 109const videoImportDeleteValidator = [
96 param('id') 110 param('id')
97 .custom(isIdValid).withMessage('Should have correct import id'), 111 .custom(isIdValid).withMessage('Should have correct import id'),
@@ -143,7 +157,8 @@ const videoImportCancelValidator = [
143export { 157export {
144 videoImportAddValidator, 158 videoImportAddValidator,
145 videoImportCancelValidator, 159 videoImportCancelValidator,
146 videoImportDeleteValidator 160 videoImportDeleteValidator,
161 getMyVideoImportsValidator
147} 162}
148 163
149// --------------------------------------------------------------------------- 164// ---------------------------------------------------------------------------
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts
index b8e941623..da6b92c7a 100644
--- a/server/models/video/video-import.ts
+++ b/server/models/video/video-import.ts
@@ -1,4 +1,4 @@
1import { Op, WhereOptions } from 'sequelize' 1import { IncludeOptions, Op, WhereOptions } from 'sequelize'
2import { 2import {
3 AfterUpdate, 3 AfterUpdate,
4 AllowNull, 4 AllowNull,
@@ -22,8 +22,17 @@ import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../help
22import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' 22import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
23import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' 23import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
24import { UserModel } from '../user/user' 24import { UserModel } from '../user/user'
25import { getSort, throwIfNotValid } from '../utils' 25import { getSort, searchAttribute, throwIfNotValid } from '../utils'
26import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' 26import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
27import { VideoChannelSyncModel } from './video-channel-sync'
28
29const defaultVideoScope = () => {
30 return VideoModel.scope([
31 VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
32 VideoModelScopeNames.WITH_TAGS,
33 VideoModelScopeNames.WITH_THUMBNAILS
34 ])
35}
27 36
28@DefaultScope(() => ({ 37@DefaultScope(() => ({
29 include: [ 38 include: [
@@ -32,11 +41,11 @@ import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
32 required: true 41 required: true
33 }, 42 },
34 { 43 {
35 model: VideoModel.scope([ 44 model: defaultVideoScope(),
36 VideoModelScopeNames.WITH_ACCOUNT_DETAILS, 45 required: false
37 VideoModelScopeNames.WITH_TAGS, 46 },
38 VideoModelScopeNames.WITH_THUMBNAILS 47 {
39 ]), 48 model: VideoChannelSyncModel.unscoped(),
40 required: false 49 required: false
41 } 50 }
42 ] 51 ]
@@ -113,6 +122,18 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
113 }) 122 })
114 Video: VideoModel 123 Video: VideoModel
115 124
125 @ForeignKey(() => VideoChannelSyncModel)
126 @Column
127 videoChannelSyncId: number
128
129 @BelongsTo(() => VideoChannelSyncModel, {
130 foreignKey: {
131 allowNull: true
132 },
133 onDelete: 'set null'
134 })
135 VideoChannelSync: VideoChannelSyncModel
136
116 @AfterUpdate 137 @AfterUpdate
117 static deleteVideoIfFailed (instance: VideoImportModel, options) { 138 static deleteVideoIfFailed (instance: VideoImportModel, options) {
118 if (instance.state === VideoImportState.FAILED) { 139 if (instance.state === VideoImportState.FAILED) {
@@ -132,23 +153,44 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
132 count: number 153 count: number
133 sort: string 154 sort: string
134 155
156 search?: string
135 targetUrl?: string 157 targetUrl?: string
158 videoChannelSyncId?: number
136 }) { 159 }) {
137 const { userId, start, count, sort, targetUrl } = options 160 const { userId, start, count, sort, targetUrl, videoChannelSyncId, search } = options
138 161
139 const where: WhereOptions = { userId } 162 const where: WhereOptions = { userId }
163 const include: IncludeOptions[] = [
164 {
165 attributes: [ 'id' ],
166 model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query
167 required: true
168 },
169 {
170 model: VideoChannelSyncModel.unscoped(),
171 required: false
172 }
173 ]
140 174
141 if (targetUrl) where['targetUrl'] = targetUrl 175 if (targetUrl) where['targetUrl'] = targetUrl
176 if (videoChannelSyncId) where['videoChannelSyncId'] = videoChannelSyncId
177
178 if (search) {
179 include.push({
180 model: defaultVideoScope(),
181 required: true,
182 where: searchAttribute(search, 'name')
183 })
184 } else {
185 include.push({
186 model: defaultVideoScope(),
187 required: false
188 })
189 }
142 190
143 const query = { 191 const query = {
144 distinct: true, 192 distinct: true,
145 include: [ 193 include,
146 {
147 attributes: [ 'id' ],
148 model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query
149 required: true
150 }
151 ],
152 offset: start, 194 offset: start,
153 limit: count, 195 limit: count,
154 order: getSort(sort), 196 order: getSort(sort),
@@ -196,6 +238,10 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
196 ? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { tags: this.Video.Tags.map(t => t.name) }) 238 ? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { tags: this.Video.Tags.map(t => t.name) })
197 : undefined 239 : undefined
198 240
241 const videoChannelSync = this.VideoChannelSync
242 ? { id: this.VideoChannelSync.id, externalChannelUrl: this.VideoChannelSync.externalChannelUrl }
243 : undefined
244
199 return { 245 return {
200 id: this.id, 246 id: this.id,
201 247
@@ -210,7 +256,8 @@ export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportMo
210 error: this.error, 256 error: this.error,
211 updatedAt: this.updatedAt.toISOString(), 257 updatedAt: this.updatedAt.toISOString(),
212 createdAt: this.createdAt.toISOString(), 258 createdAt: this.createdAt.toISOString(),
213 video 259 video,
260 videoChannelSync
214 } 261 }
215 } 262 }
216 263
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
index 000000000..90d61f20a
--- /dev/null
+++ b/server/tests/api/check-params/channel-import-videos.ts
@@ -0,0 +1,172 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { FIXTURE_URLS } from '@server/tests/shared'
5import { areHttpImportTestsDisabled } from '@shared/core-utils'
6import { HttpStatusCode } from '@shared/models'
7import { ChannelsCommand, cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
8
9describe('Test videos import in a channel API validator', function () {
10 let server: PeerTubeServer
11 const userInfo = {
12 accessToken: '',
13 channelName: 'fake_channel',
14 id: -1,
15 videoQuota: -1,
16 videoQuotaDaily: -1
17 }
18 let command: ChannelsCommand
19
20 // ---------------------------------------------------------------
21
22 before(async function () {
23 this.timeout(30000)
24
25 server = await createSingleServer(1)
26
27 await setAccessTokensToServers([ server ])
28
29 const userCreds = {
30 username: 'fake',
31 password: 'fake_password'
32 }
33
34 {
35 const user = await server.users.create({ username: userCreds.username, password: userCreds.password })
36 userInfo.id = user.id
37 userInfo.accessToken = await server.login.getAccessToken(userCreds)
38 }
39
40 command = server.channels
41 })
42
43 it('Should fail when HTTP upload is disabled', async function () {
44 await server.config.disableImports()
45
46 await command.importVideos({
47 channelName: 'super_channel',
48 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
49 token: server.accessToken,
50 expectedStatus: HttpStatusCode.FORBIDDEN_403
51 })
52
53 await server.config.enableImports()
54 })
55
56 it('Should fail when externalChannelUrl is not provided', async function () {
57 await command.importVideos({
58 channelName: 'super_channel',
59 externalChannelUrl: null,
60 token: server.accessToken,
61 expectedStatus: HttpStatusCode.BAD_REQUEST_400
62 })
63 })
64
65 it('Should fail when externalChannelUrl is malformed', async function () {
66 await command.importVideos({
67 channelName: 'super_channel',
68 externalChannelUrl: 'not-a-url',
69 token: server.accessToken,
70 expectedStatus: HttpStatusCode.BAD_REQUEST_400
71 })
72 })
73
74 it('Should fail with a bad sync id', async function () {
75 await command.importVideos({
76 channelName: 'super_channel',
77 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
78 videoChannelSyncId: 'toto' as any,
79 token: server.accessToken,
80 expectedStatus: HttpStatusCode.BAD_REQUEST_400
81 })
82 })
83
84 it('Should fail with a unknown sync id', async function () {
85 await command.importVideos({
86 channelName: 'super_channel',
87 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
88 videoChannelSyncId: 42,
89 token: server.accessToken,
90 expectedStatus: HttpStatusCode.NOT_FOUND_404
91 })
92 })
93
94 it('Should fail with no authentication', async function () {
95 await command.importVideos({
96 channelName: 'super_channel',
97 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
98 token: null,
99 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
100 })
101 })
102
103 it('Should fail when sync is not owned by the user', async function () {
104 await command.importVideos({
105 channelName: 'super_channel',
106 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
107 token: userInfo.accessToken,
108 expectedStatus: HttpStatusCode.FORBIDDEN_403
109 })
110 })
111
112 it('Should fail when the user has no quota', async function () {
113 await server.users.update({
114 userId: userInfo.id,
115 videoQuota: 0
116 })
117
118 await command.importVideos({
119 channelName: 'fake_channel',
120 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
121 token: userInfo.accessToken,
122 expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
123 })
124
125 await server.users.update({
126 userId: userInfo.id,
127 videoQuota: userInfo.videoQuota
128 })
129 })
130
131 it('Should fail when the user has no daily quota', async function () {
132 await server.users.update({
133 userId: userInfo.id,
134 videoQuotaDaily: 0
135 })
136
137 await command.importVideos({
138 channelName: 'fake_channel',
139 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
140 token: userInfo.accessToken,
141 expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
142 })
143
144 await server.users.update({
145 userId: userInfo.id,
146 videoQuotaDaily: userInfo.videoQuotaDaily
147 })
148 })
149
150 it('Should succeed when sync is run by its owner', async function () {
151 if (!areHttpImportTestsDisabled()) return
152
153 await command.importVideos({
154 channelName: 'fake_channel',
155 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
156 token: userInfo.accessToken
157 })
158 })
159
160 it('Should succeed when sync is run with root and for another user\'s channel', async function () {
161 if (!areHttpImportTestsDisabled()) return
162
163 await command.importVideos({
164 channelName: 'fake_channel',
165 externalChannelUrl: FIXTURE_URLS.youtubeChannel
166 })
167 })
168
169 after(async function () {
170 await cleanupTests([ server ])
171 })
172})
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 5f1168b53..149305f49 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -28,6 +28,7 @@ import './video-comments'
28import './video-files' 28import './video-files'
29import './video-imports' 29import './video-imports'
30import './video-channel-syncs' 30import './video-channel-syncs'
31import './channel-import-videos'
31import './video-playlists' 32import './video-playlists'
32import './video-source' 33import './video-source'
33import './video-studio' 34import './video-studio'
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts
index 337ea1dd4..9024126c0 100644
--- a/server/tests/api/check-params/video-channels.ts
+++ b/server/tests/api/check-params/video-channels.ts
@@ -3,8 +3,8 @@
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { omit } from 'lodash' 5import { omit } from 'lodash'
6import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination, FIXTURE_URLS } from '@server/tests/shared' 6import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@server/tests/shared'
7import { areHttpImportTestsDisabled, buildAbsoluteFixturePath } from '@shared/core-utils' 7import { buildAbsoluteFixturePath } from '@shared/core-utils'
8import { HttpStatusCode, VideoChannelUpdate } from '@shared/models' 8import { HttpStatusCode, VideoChannelUpdate } from '@shared/models'
9import { 9import {
10 ChannelsCommand, 10 ChannelsCommand,
@@ -354,115 +354,6 @@ describe('Test video channels API validator', function () {
354 }) 354 })
355 }) 355 })
356 356
357 describe('When triggering full synchronization', function () {
358
359 it('Should fail when HTTP upload is disabled', async function () {
360 await server.config.disableImports()
361
362 await command.importVideos({
363 channelName: 'super_channel',
364 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
365 token: server.accessToken,
366 expectedStatus: HttpStatusCode.FORBIDDEN_403
367 })
368
369 await server.config.enableImports()
370 })
371
372 it('Should fail when externalChannelUrl is not provided', async function () {
373 await command.importVideos({
374 channelName: 'super_channel',
375 externalChannelUrl: null,
376 token: server.accessToken,
377 expectedStatus: HttpStatusCode.BAD_REQUEST_400
378 })
379 })
380
381 it('Should fail when externalChannelUrl is malformed', async function () {
382 await command.importVideos({
383 channelName: 'super_channel',
384 externalChannelUrl: 'not-a-url',
385 token: server.accessToken,
386 expectedStatus: HttpStatusCode.BAD_REQUEST_400
387 })
388 })
389
390 it('Should fail with no authentication', async function () {
391 await command.importVideos({
392 channelName: 'super_channel',
393 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
394 token: null,
395 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
396 })
397 })
398
399 it('Should fail when sync is not owned by the user', async function () {
400 await command.importVideos({
401 channelName: 'super_channel',
402 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
403 token: userInfo.accessToken,
404 expectedStatus: HttpStatusCode.FORBIDDEN_403
405 })
406 })
407
408 it('Should fail when the user has no quota', async function () {
409 await server.users.update({
410 userId: userInfo.id,
411 videoQuota: 0
412 })
413
414 await command.importVideos({
415 channelName: 'fake_channel',
416 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
417 token: userInfo.accessToken,
418 expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
419 })
420
421 await server.users.update({
422 userId: userInfo.id,
423 videoQuota: userInfo.videoQuota
424 })
425 })
426
427 it('Should fail when the user has no daily quota', async function () {
428 await server.users.update({
429 userId: userInfo.id,
430 videoQuotaDaily: 0
431 })
432
433 await command.importVideos({
434 channelName: 'fake_channel',
435 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
436 token: userInfo.accessToken,
437 expectedStatus: HttpStatusCode.PAYLOAD_TOO_LARGE_413
438 })
439
440 await server.users.update({
441 userId: userInfo.id,
442 videoQuotaDaily: userInfo.videoQuotaDaily
443 })
444 })
445
446 it('Should succeed when sync is run by its owner', async function () {
447 if (!areHttpImportTestsDisabled()) return
448
449 await command.importVideos({
450 channelName: 'fake_channel',
451 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
452 token: userInfo.accessToken
453 })
454 })
455
456 it('Should succeed when sync is run with root and for another user\'s channel', async function () {
457 if (!areHttpImportTestsDisabled()) return
458
459 await command.importVideos({
460 channelName: 'fake_channel',
461 externalChannelUrl: FIXTURE_URLS.youtubeChannel
462 })
463 })
464 })
465
466 describe('When deleting a video channel', function () { 357 describe('When deleting a video channel', function () {
467 it('Should fail with a non authenticated user', async function () { 358 it('Should fail with a non authenticated user', async function () {
468 await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) 359 await command.delete({ token: 'coucou', channelName: 'super_channel', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts
index 5cdd0d925..85382b261 100644
--- a/server/tests/api/check-params/video-imports.ts
+++ b/server/tests/api/check-params/video-imports.ts
@@ -59,6 +59,15 @@ describe('Test video imports API validator', function () {
59 await checkBadSortPagination(server.url, myPath, server.accessToken) 59 await checkBadSortPagination(server.url, myPath, server.accessToken)
60 }) 60 })
61 61
62 it('Should fail with a bad videoChannelSyncId param', async function () {
63 await makeGetRequest({
64 url: server.url,
65 path: myPath,
66 query: { videoChannelSyncId: 'toto' },
67 token: server.accessToken
68 })
69 })
70
62 it('Should success with the correct parameters', async function () { 71 it('Should success with the correct parameters', async function () {
63 await makeGetRequest({ url: server.url, path: myPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken }) 72 await makeGetRequest({ url: server.url, path: myPath, expectedStatus: HttpStatusCode.OK_200, token: server.accessToken })
64 }) 73 })
diff --git a/server/tests/api/videos/channel-import-videos.ts b/server/tests/api/videos/channel-import-videos.ts
index f7540e1ba..7cfd02fbb 100644
--- a/server/tests/api/videos/channel-import-videos.ts
+++ b/server/tests/api/videos/channel-import-videos.ts
@@ -1,3 +1,5 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
1import { expect } from 'chai' 3import { expect } from 'chai'
2import { FIXTURE_URLS } from '@server/tests/shared' 4import { FIXTURE_URLS } from '@server/tests/shared'
3import { areHttpImportTestsDisabled } from '@shared/core-utils' 5import { areHttpImportTestsDisabled } from '@shared/core-utils'
@@ -29,7 +31,7 @@ describe('Test videos import in a channel', function () {
29 await server.config.enableChannelSync() 31 await server.config.enableChannelSync()
30 }) 32 })
31 33
32 it('Should import a whole channel', async function () { 34 it('Should import a whole channel without specifying the sync id', async function () {
33 this.timeout(240_000) 35 this.timeout(240_000)
34 36
35 await server.channels.importVideos({ channelName: server.store.channel.name, externalChannelUrl: FIXTURE_URLS.youtubeChannel }) 37 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 () {
39 expect(videos.total).to.equal(2) 41 expect(videos.total).to.equal(2)
40 }) 42 })
41 43
44 it('These imports should not have a sync id', async function () {
45 const { total, data } = await server.imports.getMyVideoImports()
46
47 expect(total).to.equal(2)
48 expect(data).to.have.lengthOf(2)
49
50 for (const videoImport of data) {
51 expect(videoImport.videoChannelSync).to.not.exist
52 }
53 })
54
55 it('Should import a whole channel and specifying the sync id', async function () {
56 this.timeout(240_000)
57
58 {
59 server.store.channel.name = 'channel2'
60 const { id } = await server.channels.create({ attributes: { name: server.store.channel.name } })
61 server.store.channel.id = id
62 }
63
64 {
65 const attributes = {
66 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
67 videoChannelId: server.store.channel.id
68 }
69
70 const { videoChannelSync } = await server.channelSyncs.create({ attributes })
71 server.store.videoChannelSync = videoChannelSync
72
73 await waitJobs(server)
74 }
75
76 await server.channels.importVideos({
77 channelName: server.store.channel.name,
78 externalChannelUrl: FIXTURE_URLS.youtubeChannel,
79 videoChannelSyncId: server.store.videoChannelSync.id
80 })
81
82 await waitJobs(server)
83 })
84
85 it('These imports should have a sync id', async function () {
86 const { total, data } = await server.imports.getMyVideoImports()
87
88 expect(total).to.equal(4)
89 expect(data).to.have.lengthOf(4)
90
91 const importsWithSyncId = data.filter(i => !!i.videoChannelSync)
92 expect(importsWithSyncId).to.have.lengthOf(2)
93
94 for (const videoImport of importsWithSyncId) {
95 expect(videoImport.videoChannelSync).to.exist
96 expect(videoImport.videoChannelSync.id).to.equal(server.store.videoChannelSync.id)
97 }
98 })
99
100 it('Should be able to filter imports by this sync id', async function () {
101 const { total, data } = await server.imports.getMyVideoImports({ videoChannelSyncId: server.store.videoChannelSync.id })
102
103 expect(total).to.equal(2)
104 expect(data).to.have.lengthOf(2)
105
106 for (const videoImport of data) {
107 expect(videoImport.videoChannelSync).to.exist
108 expect(videoImport.videoChannelSync.id).to.equal(server.store.videoChannelSync.id)
109 }
110 })
111
42 after(async function () { 112 after(async function () {
43 await server?.kill() 113 await server?.kill()
44 }) 114 })
diff --git a/server/tests/api/videos/video-channel-syncs.ts b/server/tests/api/videos/video-channel-syncs.ts
index 229c01f68..835d3cb09 100644
--- a/server/tests/api/videos/video-channel-syncs.ts
+++ b/server/tests/api/videos/video-channel-syncs.ts
@@ -23,7 +23,10 @@ describe('Test channel synchronizations', function () {
23 describe('Sync using ' + mode, function () { 23 describe('Sync using ' + mode, function () {
24 let server: PeerTubeServer 24 let server: PeerTubeServer
25 let command: ChannelSyncsCommand 25 let command: ChannelSyncsCommand
26
26 let startTestDate: Date 27 let startTestDate: Date
28
29 let rootChannelSyncId: number
27 const userInfo = { 30 const userInfo = {
28 accessToken: '', 31 accessToken: '',
29 username: 'user1', 32 username: 'user1',
@@ -90,6 +93,7 @@ describe('Test channel synchronizations', function () {
90 token: server.accessToken, 93 token: server.accessToken,
91 expectedStatus: HttpStatusCode.OK_200 94 expectedStatus: HttpStatusCode.OK_200
92 }) 95 })
96 rootChannelSyncId = videoChannelSync.id
93 97
94 // Ensure any missing video not already fetched will be considered as new 98 // Ensure any missing video not already fetched will be considered as new
95 await changeDateForSync(videoChannelSync.id, '1970-01-01') 99 await changeDateForSync(videoChannelSync.id, '1970-01-01')
@@ -208,6 +212,14 @@ describe('Test channel synchronizations', function () {
208 } 212 }
209 }) 213 })
210 214
215 it('Should list imports of a channel synchronization', async function () {
216 const { total, data } = await server.imports.getMyVideoImports({ videoChannelSyncId: rootChannelSyncId })
217
218 expect(total).to.equal(1)
219 expect(data).to.have.lengthOf(1)
220 expect(data[0].video.name).to.equal('test')
221 })
222
211 it('Should remove user\'s channel synchronizations', async function () { 223 it('Should remove user\'s channel synchronizations', async function () {
212 await command.delete({ channelSyncId: userInfo.syncId }) 224 await command.delete({ channelSyncId: userInfo.syncId })
213 225
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts
index a487062a2..f082d4bd7 100644
--- a/server/tests/api/videos/video-imports.ts
+++ b/server/tests/api/videos/video-imports.ts
@@ -228,6 +228,15 @@ describe('Test video imports', function () {
228 expect(videoImports[0].targetUrl).to.equal(FIXTURE_URLS.youtube) 228 expect(videoImports[0].targetUrl).to.equal(FIXTURE_URLS.youtube)
229 }) 229 })
230 230
231 it('Should search in my imports', async function () {
232 const { total, data: videoImports } = await servers[0].imports.getMyVideoImports({ search: 'peertube2' })
233 expect(total).to.equal(1)
234 expect(videoImports).to.have.lengthOf(1)
235
236 expect(videoImports[0].magnetUri).to.equal(FIXTURE_URLS.magnet)
237 expect(videoImports[0].video.name).to.equal('super peertube2 video')
238 })
239
231 it('Should have the video listed on the two instances', async function () { 240 it('Should have the video listed on the two instances', async function () {
232 this.timeout(120_000) 241 this.timeout(120_000)
233 242
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts
index ba1f83684..9c0b5ea56 100644
--- a/shared/models/server/job.model.ts
+++ b/shared/models/server/job.model.ts
@@ -236,6 +236,8 @@ export interface VideoStudioEditionPayload {
236export interface VideoChannelImportPayload { 236export interface VideoChannelImportPayload {
237 externalChannelUrl: string 237 externalChannelUrl: string
238 videoChannelId: number 238 videoChannelId: number
239
240 partOfChannelSyncId?: number
239} 241}
240 242
241export interface AfterVideoChannelImportPayload { 243export interface AfterVideoChannelImportPayload {
diff --git a/shared/models/videos/import/index.ts b/shared/models/videos/import/index.ts
index 8884ee8f2..b38a67b5f 100644
--- a/shared/models/videos/import/index.ts
+++ b/shared/models/videos/import/index.ts
@@ -1,3 +1,4 @@
1export * from './video-import-create.model' 1export * from './video-import-create.model'
2export * from './video-import-state.enum' 2export * from './video-import-state.enum'
3export * from './video-import.model' 3export * from './video-import.model'
4export * from './videos-import-in-channel-create.model'
diff --git a/shared/models/videos/import/video-import.model.ts b/shared/models/videos/import/video-import.model.ts
index 92856c70f..6aed7a91a 100644
--- a/shared/models/videos/import/video-import.model.ts
+++ b/shared/models/videos/import/video-import.model.ts
@@ -16,4 +16,9 @@ export interface VideoImport {
16 error?: string 16 error?: string
17 17
18 video?: Video & { tags: string[] } 18 video?: Video & { tags: string[] }
19
20 videoChannelSync?: {
21 id: number
22 externalChannelUrl: string
23 }
19} 24}
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
index 000000000..fbfef63f8
--- /dev/null
+++ b/shared/models/videos/import/videos-import-in-channel-create.model.ts
@@ -0,0 +1,4 @@
1export interface VideosImportInChannelCreate {
2 externalChannelUrl: string
3 videoChannelSyncId?: number
4}
diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts
index 7acbc978f..c05d16ad2 100644
--- a/shared/server-commands/server/server.ts
+++ b/shared/server-commands/server/server.ts
@@ -2,7 +2,7 @@ import { ChildProcess, fork } from 'child_process'
2import { copy } from 'fs-extra' 2import { copy } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { parallelTests, randomInt, root } from '@shared/core-utils' 4import { parallelTests, randomInt, root } from '@shared/core-utils'
5import { Video, VideoChannel, VideoCreateResult, VideoDetails } from '@shared/models' 5import { Video, VideoChannel, VideoChannelSync, VideoCreateResult, VideoDetails } from '@shared/models'
6import { BulkCommand } from '../bulk' 6import { BulkCommand } from '../bulk'
7import { CLICommand } from '../cli' 7import { CLICommand } from '../cli'
8import { CustomPagesCommand } from '../custom-pages' 8import { CustomPagesCommand } from '../custom-pages'
@@ -80,6 +80,7 @@ export class PeerTubeServer {
80 } 80 }
81 81
82 channel?: VideoChannel 82 channel?: VideoChannel
83 videoChannelSync?: Partial<VideoChannelSync>
83 84
84 video?: Video 85 video?: Video
85 videoCreated?: VideoCreateResult 86 videoCreated?: VideoCreateResult
diff --git a/shared/server-commands/videos/channels-command.ts b/shared/server-commands/videos/channels-command.ts
index a688a120f..385d0fe73 100644
--- a/shared/server-commands/videos/channels-command.ts
+++ b/shared/server-commands/videos/channels-command.ts
@@ -6,7 +6,8 @@ import {
6 VideoChannel, 6 VideoChannel,
7 VideoChannelCreate, 7 VideoChannelCreate,
8 VideoChannelCreateResult, 8 VideoChannelCreateResult,
9 VideoChannelUpdate 9 VideoChannelUpdate,
10 VideosImportInChannelCreate
10} from '@shared/models' 11} from '@shared/models'
11import { unwrapBody } from '../requests' 12import { unwrapBody } from '../requests'
12import { AbstractCommand, OverrideCommandOptions } from '../shared' 13import { AbstractCommand, OverrideCommandOptions } from '../shared'
@@ -182,11 +183,10 @@ export class ChannelsCommand extends AbstractCommand {
182 }) 183 })
183 } 184 }
184 185
185 importVideos (options: OverrideCommandOptions & { 186 importVideos (options: OverrideCommandOptions & VideosImportInChannelCreate & {
186 channelName: string 187 channelName: string
187 externalChannelUrl: string
188 }) { 188 }) {
189 const { channelName, externalChannelUrl } = options 189 const { channelName, externalChannelUrl, videoChannelSyncId } = options
190 190
191 const path = `/api/v1/video-channels/${channelName}/import-videos` 191 const path = `/api/v1/video-channels/${channelName}/import-videos`
192 192
@@ -194,7 +194,7 @@ export class ChannelsCommand extends AbstractCommand {
194 ...options, 194 ...options,
195 195
196 path, 196 path,
197 fields: { externalChannelUrl }, 197 fields: { externalChannelUrl, videoChannelSyncId },
198 implicitToken: true, 198 implicitToken: true,
199 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 199 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
200 }) 200 })
diff --git a/shared/server-commands/videos/imports-command.ts b/shared/server-commands/videos/imports-command.ts
index c931ac481..07d810ec1 100644
--- a/shared/server-commands/videos/imports-command.ts
+++ b/shared/server-commands/videos/imports-command.ts
@@ -57,15 +57,17 @@ export class ImportsCommand extends AbstractCommand {
57 getMyVideoImports (options: OverrideCommandOptions & { 57 getMyVideoImports (options: OverrideCommandOptions & {
58 sort?: string 58 sort?: string
59 targetUrl?: string 59 targetUrl?: string
60 videoChannelSyncId?: number
61 search?: string
60 } = {}) { 62 } = {}) {
61 const { sort, targetUrl } = options 63 const { sort, targetUrl, videoChannelSyncId, search } = options
62 const path = '/api/v1/users/me/videos/imports' 64 const path = '/api/v1/users/me/videos/imports'
63 65
64 return this.getRequestBody<ResultList<VideoImport>>({ 66 return this.getRequestBody<ResultList<VideoImport>>({
65 ...options, 67 ...options,
66 68
67 path, 69 path,
68 query: { sort, targetUrl }, 70 query: { sort, targetUrl, videoChannelSyncId, search },
69 implicitToken: true, 71 implicitToken: true,
70 defaultExpectedStatus: HttpStatusCode.OK_200 72 defaultExpectedStatus: HttpStatusCode.OK_200
71 }) 73 })
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index ac8cde565..c4bc507fd 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -1187,6 +1187,20 @@ paths:
1187 - $ref: '#/components/parameters/start' 1187 - $ref: '#/components/parameters/start'
1188 - $ref: '#/components/parameters/count' 1188 - $ref: '#/components/parameters/count'
1189 - $ref: '#/components/parameters/sort' 1189 - $ref: '#/components/parameters/sort'
1190 -
1191 name: targetUrl
1192 in: query
1193 required: false
1194 description: Filter on import target URL
1195 schema:
1196 type: string
1197 -
1198 name: videoChannelSyncId
1199 in: query
1200 required: false
1201 description: Filter on imports created by a specific channel synchronization
1202 schema:
1203 type: number
1190 responses: 1204 responses:
1191 '200': 1205 '200':
1192 description: successful operation 1206 description: successful operation