diff options
17 files changed, 283 insertions, 19 deletions
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index 91b464f75..6f0806e8a 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts | |||
@@ -8,6 +8,7 @@ import { MyAccountVideosComponent } from './my-account-videos/my-account-videos. | |||
8 | import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component' | 8 | import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component' |
9 | import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' | 9 | import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' |
10 | import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' | 10 | import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' |
11 | import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' | ||
11 | 12 | ||
12 | const myAccountRoutes: Routes = [ | 13 | const myAccountRoutes: Routes = [ |
13 | { | 14 | { |
@@ -64,6 +65,15 @@ const myAccountRoutes: Routes = [ | |||
64 | title: 'Account videos' | 65 | title: 'Account videos' |
65 | } | 66 | } |
66 | } | 67 | } |
68 | }, | ||
69 | { | ||
70 | path: 'video-imports', | ||
71 | component: MyAccountVideoImportsComponent, | ||
72 | data: { | ||
73 | meta: { | ||
74 | title: 'Account video imports' | ||
75 | } | ||
76 | } | ||
67 | } | 77 | } |
68 | ] | 78 | ] |
69 | } | 79 | } |
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html new file mode 100644 index 000000000..74ca33fa3 --- /dev/null +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html | |||
@@ -0,0 +1,37 @@ | |||
1 | <p-table | ||
2 | [value]="videoImports" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
3 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" | ||
4 | > | ||
5 | <ng-template pTemplate="header"> | ||
6 | <tr> | ||
7 | <th i18n>URL</th> | ||
8 | <th i18n>Video</th> | ||
9 | <th i18n style="width: 150px">State</th> | ||
10 | <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
11 | <th></th> | ||
12 | </tr> | ||
13 | </ng-template> | ||
14 | |||
15 | <ng-template pTemplate="body" let-videoImport> | ||
16 | <tr> | ||
17 | <td> | ||
18 | <a [href]="videoImport.targetUrl" target="_blank" rel="noopener noreferrer">{{ videoImport.targetUrl }}</a> | ||
19 | </td> | ||
20 | |||
21 | <td *ngIf="isVideoImportPending(videoImport)"> | ||
22 | {{ videoImport.video.name }} | ||
23 | </td> | ||
24 | <td *ngIf="isVideoImportSuccess(videoImport)"> | ||
25 | <a [href]="getVideoUrl(videoImport.video)" target="_blank" rel="noopener noreferrer">{{ videoImport.video.name }}</a> | ||
26 | </td> | ||
27 | <td *ngIf="isVideoImportFailed(videoImport)"></td> | ||
28 | |||
29 | <td>{{ videoImport.state.label }}</td> | ||
30 | <td>{{ videoImport.createdAt }}</td> | ||
31 | |||
32 | <td class="action-cell"> | ||
33 | <my-edit-button *ngIf="isVideoImportSuccess(videoImport)" [routerLink]="getEditVideoUrl(videoImport.video)"></my-edit-button> | ||
34 | </td> | ||
35 | </tr> | ||
36 | </ng-template> | ||
37 | </p-table> | ||
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss new file mode 100644 index 000000000..5e6774739 --- /dev/null +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss | |||
@@ -0,0 +1,2 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts new file mode 100644 index 000000000..31ccb0bc8 --- /dev/null +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts | |||
@@ -0,0 +1,66 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { RestPagination, RestTable } from '@app/shared' | ||
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | ||
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { ConfirmService } from '@app/core' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { VideoImport, VideoImportState } from '../../../../../shared/models/videos' | ||
8 | import { VideoImportService } from '@app/shared/video-import' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-account-video-imports', | ||
12 | templateUrl: './my-account-video-imports.component.html', | ||
13 | styleUrls: [ './my-account-video-imports.component.scss' ] | ||
14 | }) | ||
15 | export class MyAccountVideoImportsComponent extends RestTable implements OnInit { | ||
16 | videoImports: VideoImport[] = [] | ||
17 | totalRecords = 0 | ||
18 | rowsPerPage = 10 | ||
19 | sort: SortMeta = { field: 'createdAt', order: 1 } | ||
20 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
21 | |||
22 | constructor ( | ||
23 | private notificationsService: NotificationsService, | ||
24 | private confirmService: ConfirmService, | ||
25 | private videoImportService: VideoImportService, | ||
26 | private i18n: I18n | ||
27 | ) { | ||
28 | super() | ||
29 | } | ||
30 | |||
31 | ngOnInit () { | ||
32 | this.loadSort() | ||
33 | } | ||
34 | |||
35 | isVideoImportSuccess (videoImport: VideoImport) { | ||
36 | return videoImport.state.id === VideoImportState.SUCCESS | ||
37 | } | ||
38 | |||
39 | isVideoImportPending (videoImport: VideoImport) { | ||
40 | return videoImport.state.id === VideoImportState.PENDING | ||
41 | } | ||
42 | |||
43 | isVideoImportFailed (videoImport: VideoImport) { | ||
44 | return videoImport.state.id === VideoImportState.FAILED | ||
45 | } | ||
46 | |||
47 | getVideoUrl (video: { uuid: string }) { | ||
48 | return '/videos/watch/' + video.uuid | ||
49 | } | ||
50 | |||
51 | getEditVideoUrl (video: { uuid: string }) { | ||
52 | return '/videos/update/' + video.uuid | ||
53 | } | ||
54 | |||
55 | protected loadData () { | ||
56 | this.videoImportService.getMyVideoImports(this.pagination, this.sort) | ||
57 | .subscribe( | ||
58 | resultList => { | ||
59 | this.videoImports = resultList.data | ||
60 | this.totalRecords = resultList.total | ||
61 | }, | ||
62 | |||
63 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
64 | ) | ||
65 | } | ||
66 | } | ||
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts index 54830c75e..01e1ef1da 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts | |||
@@ -145,6 +145,8 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni | |||
145 | suffix = this.i18n('Waiting transcoding') | 145 | suffix = this.i18n('Waiting transcoding') |
146 | } else if (video.state.id === VideoState.TO_TRANSCODE) { | 146 | } else if (video.state.id === VideoState.TO_TRANSCODE) { |
147 | suffix = this.i18n('To transcode') | 147 | suffix = this.i18n('To transcode') |
148 | } else if (video.state.id === VideoState.TO_IMPORT) { | ||
149 | suffix = this.i18n('To import') | ||
148 | } else { | 150 | } else { |
149 | return '' | 151 | return '' |
150 | } | 152 | } |
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html index 48db55ad3..f67245d85 100644 --- a/client/src/app/+my-account/my-account.component.html +++ b/client/src/app/+my-account/my-account.component.html | |||
@@ -5,6 +5,8 @@ | |||
5 | <a i18n routerLink="/my-account/video-channels" routerLinkActive="active" class="title-page">My video channels</a> | 5 | <a i18n routerLink="/my-account/video-channels" routerLinkActive="active" class="title-page">My video channels</a> |
6 | 6 | ||
7 | <a i18n routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a> | 7 | <a i18n routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a> |
8 | |||
9 | <a i18n routerLink="/my-account/video-imports" routerLinkActive="active" class="title-page">My video imports</a> | ||
8 | </div> | 10 | </div> |
9 | 11 | ||
10 | <div class="margin-content"> | 12 | <div class="margin-content"> |
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 2088273e6..5403ab649 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { TableModule } from 'primeng/table' | ||
1 | import { NgModule } from '@angular/core' | 2 | import { NgModule } from '@angular/core' |
2 | import { SharedModule } from '../shared' | 3 | import { SharedModule } from '../shared' |
3 | import { MyAccountRoutingModule } from './my-account-routing.module' | 4 | import { MyAccountRoutingModule } from './my-account-routing.module' |
@@ -11,11 +12,13 @@ import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-vid | |||
11 | import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' | 12 | import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' |
12 | import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' | 13 | import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' |
13 | import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' | 14 | import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' |
15 | import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' | ||
14 | 16 | ||
15 | @NgModule({ | 17 | @NgModule({ |
16 | imports: [ | 18 | imports: [ |
17 | MyAccountRoutingModule, | 19 | MyAccountRoutingModule, |
18 | SharedModule | 20 | SharedModule, |
21 | TableModule | ||
19 | ], | 22 | ], |
20 | 23 | ||
21 | declarations: [ | 24 | declarations: [ |
@@ -28,7 +31,8 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i | |||
28 | MyAccountVideoChannelsComponent, | 31 | MyAccountVideoChannelsComponent, |
29 | MyAccountVideoChannelCreateComponent, | 32 | MyAccountVideoChannelCreateComponent, |
30 | MyAccountVideoChannelUpdateComponent, | 33 | MyAccountVideoChannelUpdateComponent, |
31 | ActorAvatarInfoComponent | 34 | ActorAvatarInfoComponent, |
35 | MyAccountVideoImportsComponent | ||
32 | ], | 36 | ], |
33 | 37 | ||
34 | exports: [ | 38 | exports: [ |
diff --git a/client/src/app/shared/video-import/video-import.service.ts b/client/src/app/shared/video-import/video-import.service.ts index b4709866a..59b58ab38 100644 --- a/client/src/app/shared/video-import/video-import.service.ts +++ b/client/src/app/shared/video-import/video-import.service.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { catchError } from 'rxjs/operators' | 1 | import { catchError, map, switchMap } from 'rxjs/operators' |
2 | import { HttpClient } from '@angular/common/http' | 2 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { Observable } from 'rxjs' | 4 | import { Observable } from 'rxjs' |
5 | import { VideoImport } from '../../../../../shared' | 5 | import { VideoImport } from '../../../../../shared' |
@@ -8,6 +8,12 @@ import { RestExtractor, RestService } from '../rest' | |||
8 | import { VideoImportCreate } from '../../../../../shared/models/videos/video-import-create.model' | 8 | import { VideoImportCreate } from '../../../../../shared/models/videos/video-import-create.model' |
9 | import { objectToFormData } from '@app/shared/misc/utils' | 9 | import { objectToFormData } from '@app/shared/misc/utils' |
10 | import { VideoUpdate } from '../../../../../shared/models/videos' | 10 | import { VideoUpdate } from '../../../../../shared/models/videos' |
11 | import { ResultList } from '../../../../../shared/models/result-list.model' | ||
12 | import { UserService } from '@app/shared/users/user.service' | ||
13 | import { SortMeta } from 'primeng/components/common/sortmeta' | ||
14 | import { RestPagination } from '@app/shared/rest' | ||
15 | import { ServerService } from '@app/core' | ||
16 | import { peertubeTranslate } from '@app/shared/i18n/i18n-utils' | ||
11 | 17 | ||
12 | @Injectable() | 18 | @Injectable() |
13 | export class VideoImportService { | 19 | export class VideoImportService { |
@@ -16,7 +22,8 @@ export class VideoImportService { | |||
16 | constructor ( | 22 | constructor ( |
17 | private authHttp: HttpClient, | 23 | private authHttp: HttpClient, |
18 | private restService: RestService, | 24 | private restService: RestService, |
19 | private restExtractor: RestExtractor | 25 | private restExtractor: RestExtractor, |
26 | private serverService: ServerService | ||
20 | ) {} | 27 | ) {} |
21 | 28 | ||
22 | importVideo (targetUrl: string, video: VideoUpdate): Observable<VideoImport> { | 29 | importVideo (targetUrl: string, video: VideoUpdate): Observable<VideoImport> { |
@@ -53,4 +60,29 @@ export class VideoImportService { | |||
53 | .pipe(catchError(res => this.restExtractor.handleError(res))) | 60 | .pipe(catchError(res => this.restExtractor.handleError(res))) |
54 | } | 61 | } |
55 | 62 | ||
63 | getMyVideoImports (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoImport>> { | ||
64 | let params = new HttpParams() | ||
65 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
66 | |||
67 | return this.authHttp | ||
68 | .get<ResultList<VideoImport>>(UserService.BASE_USERS_URL + '/me/videos/imports', { params }) | ||
69 | .pipe( | ||
70 | switchMap(res => this.extractVideoImports(res)), | ||
71 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | ||
72 | catchError(err => this.restExtractor.handleError(err)) | ||
73 | ) | ||
74 | } | ||
75 | |||
76 | private extractVideoImports (result: ResultList<VideoImport>): Observable<ResultList<VideoImport>> { | ||
77 | return this.serverService.localeObservable | ||
78 | .pipe( | ||
79 | map(translations => { | ||
80 | result.data.forEach(d => | ||
81 | d.state.label = peertubeTranslate(d.state.label, translations) | ||
82 | ) | ||
83 | |||
84 | return result | ||
85 | }) | ||
86 | ) | ||
87 | } | ||
56 | } | 88 | } |
@@ -152,7 +152,7 @@ app.use(function (err, req, res, next) { | |||
152 | error = err.stack || err.message || err | 152 | error = err.stack || err.message || err |
153 | } | 153 | } |
154 | 154 | ||
155 | logger.error('Error in controller.', { error }) | 155 | logger.error('Error in controller.', { err: error }) |
156 | return res.status(err.status || 500).end() | 156 | return res.status(err.status || 500).end() |
157 | }) | 157 | }) |
158 | 158 | ||
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index dbe736bff..6e5f9913e 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts | |||
@@ -29,7 +29,12 @@ import { | |||
29 | usersUpdateValidator, | 29 | usersUpdateValidator, |
30 | usersVideoRatingValidator | 30 | usersVideoRatingValidator |
31 | } from '../../middlewares' | 31 | } from '../../middlewares' |
32 | import { usersAskResetPasswordValidator, usersResetPasswordValidator, videosSortValidator } from '../../middlewares/validators' | 32 | import { |
33 | usersAskResetPasswordValidator, | ||
34 | usersResetPasswordValidator, | ||
35 | videoImportsSortValidator, | ||
36 | videosSortValidator | ||
37 | } from '../../middlewares/validators' | ||
33 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 38 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
34 | import { UserModel } from '../../models/account/user' | 39 | import { UserModel } from '../../models/account/user' |
35 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' | 40 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' |
@@ -40,6 +45,7 @@ import { UserVideoQuota } from '../../../shared/models/users/user-video-quota.mo | |||
40 | import { updateAvatarValidator } from '../../middlewares/validators/avatar' | 45 | import { updateAvatarValidator } from '../../middlewares/validators/avatar' |
41 | import { updateActorAvatarFile } from '../../lib/avatar' | 46 | import { updateActorAvatarFile } from '../../lib/avatar' |
42 | import { auditLoggerFactory, UserAuditView } from '../../helpers/audit-logger' | 47 | import { auditLoggerFactory, UserAuditView } from '../../helpers/audit-logger' |
48 | import { VideoImportModel } from '../../models/video/video-import' | ||
43 | 49 | ||
44 | const auditLogger = auditLoggerFactory('users') | 50 | const auditLogger = auditLoggerFactory('users') |
45 | 51 | ||
@@ -62,6 +68,16 @@ usersRouter.get('/me/video-quota-used', | |||
62 | asyncMiddleware(getUserVideoQuotaUsed) | 68 | asyncMiddleware(getUserVideoQuotaUsed) |
63 | ) | 69 | ) |
64 | 70 | ||
71 | |||
72 | usersRouter.get('/me/videos/imports', | ||
73 | authenticate, | ||
74 | paginationValidator, | ||
75 | videoImportsSortValidator, | ||
76 | setDefaultSort, | ||
77 | setDefaultPagination, | ||
78 | asyncMiddleware(getUserVideoImports) | ||
79 | ) | ||
80 | |||
65 | usersRouter.get('/me/videos', | 81 | usersRouter.get('/me/videos', |
66 | authenticate, | 82 | authenticate, |
67 | paginationValidator, | 83 | paginationValidator, |
@@ -178,6 +194,18 @@ async function getUserVideos (req: express.Request, res: express.Response, next: | |||
178 | return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes })) | 194 | return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes })) |
179 | } | 195 | } |
180 | 196 | ||
197 | async function getUserVideoImports (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
198 | const user = res.locals.oauth.token.User as UserModel | ||
199 | const resultList = await VideoImportModel.listUserVideoImportsForApi( | ||
200 | user.Account.id, | ||
201 | req.query.start as number, | ||
202 | req.query.count as number, | ||
203 | req.query.sort | ||
204 | ) | ||
205 | |||
206 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
207 | } | ||
208 | |||
181 | async function createUser (req: express.Request, res: express.Response) { | 209 | async function createUser (req: express.Request, res: express.Response) { |
182 | const body: UserCreate = req.body | 210 | const body: UserCreate = req.body |
183 | const userToCreate = new UserModel({ | 211 | const userToCreate = new UserModel({ |
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index 74d3e213b..43156bb22 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts | |||
@@ -95,7 +95,7 @@ function titleTruncation (title: string) { | |||
95 | } | 95 | } |
96 | 96 | ||
97 | function descriptionTruncation (description: string) { | 97 | function descriptionTruncation (description: string) { |
98 | if (!description) return undefined | 98 | if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined |
99 | 99 | ||
100 | return truncate(description, { | 100 | return truncate(description, { |
101 | 'length': CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max, | 101 | 'length': CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max, |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index cc363d4f2..feb45e4d0 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -37,6 +37,7 @@ const SORTABLE_COLUMNS = { | |||
37 | VIDEO_ABUSES: [ 'id', 'createdAt' ], | 37 | VIDEO_ABUSES: [ 'id', 'createdAt' ], |
38 | VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], | 38 | VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], |
39 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ], | 39 | VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ], |
40 | VIDEO_IMPORTS: [ 'createdAt' ], | ||
40 | VIDEO_COMMENT_THREADS: [ 'createdAt' ], | 41 | VIDEO_COMMENT_THREADS: [ 'createdAt' ], |
41 | BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], | 42 | BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], |
42 | FOLLOWERS: [ 'createdAt' ], | 43 | FOLLOWERS: [ 'createdAt' ], |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 2f219e986..5a7722153 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -35,7 +35,7 @@ async function processVideoImport (job: Bull.Job) { | |||
35 | 35 | ||
36 | // Get information about this video | 36 | // Get information about this video |
37 | const { videoFileResolution } = await getVideoFileResolution(tempVideoPath) | 37 | const { videoFileResolution } = await getVideoFileResolution(tempVideoPath) |
38 | const fps = await getVideoFileFPS(tempVideoPath) | 38 | const fps = await getVideoFileFPS(tempVideoPath + 's') |
39 | const stats = await statPromise(tempVideoPath) | 39 | const stats = await statPromise(tempVideoPath) |
40 | const duration = await getDurationFromVideoFile(tempVideoPath) | 40 | const duration = await getDurationFromVideoFile(tempVideoPath) |
41 | 41 | ||
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 00bde548c..d85611773 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -8,6 +8,7 @@ const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) | |||
8 | const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) | 8 | const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) |
9 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) | 9 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) |
10 | const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) | 10 | const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) |
11 | const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS) | ||
11 | const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) | 12 | const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) |
12 | const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) | 13 | const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) |
13 | const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) | 14 | const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) |
@@ -19,6 +20,7 @@ const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | |||
19 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) | 20 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) |
20 | const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) | 21 | const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) |
21 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) | 22 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) |
23 | const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) | ||
22 | const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) | 24 | const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) |
23 | const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) | 25 | const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) |
24 | const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) | 26 | const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) |
@@ -32,6 +34,7 @@ export { | |||
32 | usersSortValidator, | 34 | usersSortValidator, |
33 | videoAbusesSortValidator, | 35 | videoAbusesSortValidator, |
34 | videoChannelsSortValidator, | 36 | videoChannelsSortValidator, |
37 | videoImportsSortValidator, | ||
35 | videosSearchSortValidator, | 38 | videosSearchSortValidator, |
36 | videosSortValidator, | 39 | videosSortValidator, |
37 | blacklistSortValidator, | 40 | blacklistSortValidator, |
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index 89eeafd6a..6b8a16b65 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { | 1 | import { |
2 | AfterUpdate, | ||
2 | AllowNull, | 3 | AllowNull, |
3 | BelongsTo, | 4 | BelongsTo, |
4 | Column, | 5 | Column, |
@@ -12,13 +13,14 @@ import { | |||
12 | Table, | 13 | Table, |
13 | UpdatedAt | 14 | UpdatedAt |
14 | } from 'sequelize-typescript' | 15 | } from 'sequelize-typescript' |
15 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 16 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers' |
16 | import { throwIfNotValid } from '../utils' | 17 | import { getSort, throwIfNotValid } from '../utils' |
17 | import { VideoModel } from './video' | 18 | import { VideoModel } from './video' |
18 | import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports' | 19 | import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports' |
19 | import { VideoImport, VideoImportState } from '../../../shared' | 20 | import { VideoImport, VideoImportState } from '../../../shared' |
20 | import { VideoChannelModel } from './video-channel' | 21 | import { VideoChannelModel } from './video-channel' |
21 | import { AccountModel } from '../account/account' | 22 | import { AccountModel } from '../account/account' |
23 | import { TagModel } from './tag' | ||
22 | 24 | ||
23 | @DefaultScope({ | 25 | @DefaultScope({ |
24 | include: [ | 26 | include: [ |
@@ -35,6 +37,10 @@ import { AccountModel } from '../account/account' | |||
35 | required: true | 37 | required: true |
36 | } | 38 | } |
37 | ] | 39 | ] |
40 | }, | ||
41 | { | ||
42 | model: () => TagModel, | ||
43 | required: false | ||
38 | } | 44 | } |
39 | ] | 45 | ] |
40 | } | 46 | } |
@@ -79,27 +85,89 @@ export class VideoImportModel extends Model<VideoImportModel> { | |||
79 | 85 | ||
80 | @BelongsTo(() => VideoModel, { | 86 | @BelongsTo(() => VideoModel, { |
81 | foreignKey: { | 87 | foreignKey: { |
82 | allowNull: false | 88 | allowNull: true |
83 | }, | 89 | }, |
84 | onDelete: 'CASCADE' | 90 | onDelete: 'set null' |
85 | }) | 91 | }) |
86 | Video: VideoModel | 92 | Video: VideoModel |
87 | 93 | ||
94 | @AfterUpdate | ||
95 | static deleteVideoIfFailed (instance: VideoImportModel, options) { | ||
96 | if (instance.state === VideoImportState.FAILED) { | ||
97 | return instance.Video.destroy({ transaction: options.transaction }) | ||
98 | } | ||
99 | |||
100 | return undefined | ||
101 | } | ||
102 | |||
88 | static loadAndPopulateVideo (id: number) { | 103 | static loadAndPopulateVideo (id: number) { |
89 | return VideoImportModel.findById(id) | 104 | return VideoImportModel.findById(id) |
90 | } | 105 | } |
91 | 106 | ||
107 | static listUserVideoImportsForApi (accountId: number, start: number, count: number, sort: string) { | ||
108 | const query = { | ||
109 | offset: start, | ||
110 | limit: count, | ||
111 | order: getSort(sort), | ||
112 | include: [ | ||
113 | { | ||
114 | model: VideoModel, | ||
115 | required: true, | ||
116 | include: [ | ||
117 | { | ||
118 | model: VideoChannelModel, | ||
119 | required: true, | ||
120 | include: [ | ||
121 | { | ||
122 | model: AccountModel, | ||
123 | required: true, | ||
124 | where: { | ||
125 | id: accountId | ||
126 | } | ||
127 | } | ||
128 | ] | ||
129 | }, | ||
130 | { | ||
131 | model: TagModel, | ||
132 | required: false | ||
133 | } | ||
134 | ] | ||
135 | } | ||
136 | ] | ||
137 | } | ||
138 | |||
139 | return VideoImportModel.unscoped() | ||
140 | .findAndCountAll(query) | ||
141 | .then(({ rows, count }) => { | ||
142 | return { | ||
143 | data: rows, | ||
144 | total: count | ||
145 | } | ||
146 | }) | ||
147 | } | ||
148 | |||
92 | toFormattedJSON (): VideoImport { | 149 | toFormattedJSON (): VideoImport { |
93 | const videoFormatOptions = { | 150 | const videoFormatOptions = { |
94 | additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true } | 151 | additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true } |
95 | } | 152 | } |
96 | const video = Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { | 153 | const video = this.Video |
97 | tags: this.Video.Tags.map(t => t.name) | 154 | ? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { |
98 | }) | 155 | tags: this.Video.Tags.map(t => t.name) |
156 | }) | ||
157 | : undefined | ||
99 | 158 | ||
100 | return { | 159 | return { |
101 | targetUrl: this.targetUrl, | 160 | targetUrl: this.targetUrl, |
161 | state: { | ||
162 | id: this.state, | ||
163 | label: VideoImportModel.getStateLabel(this.state) | ||
164 | }, | ||
165 | updatedAt: this.updatedAt.toISOString(), | ||
166 | createdAt: this.createdAt.toISOString(), | ||
102 | video | 167 | video |
103 | } | 168 | } |
104 | } | 169 | } |
170 | private static getStateLabel (id: number) { | ||
171 | return VIDEO_IMPORT_STATES[id] || 'Unknown' | ||
172 | } | ||
105 | } | 173 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 459fcb31e..f32010014 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1569,21 +1569,25 @@ export class VideoModel extends Model<VideoModel> { | |||
1569 | removeThumbnail () { | 1569 | removeThumbnail () { |
1570 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | 1570 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) |
1571 | return unlinkPromise(thumbnailPath) | 1571 | return unlinkPromise(thumbnailPath) |
1572 | .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err })) | ||
1572 | } | 1573 | } |
1573 | 1574 | ||
1574 | removePreview () { | 1575 | removePreview () { |
1575 | // Same name than video thumbnail | 1576 | const previewPath = join(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) |
1576 | return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) | 1577 | return unlinkPromise(previewPath) |
1578 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) | ||
1577 | } | 1579 | } |
1578 | 1580 | ||
1579 | removeFile (videoFile: VideoFileModel) { | 1581 | removeFile (videoFile: VideoFileModel) { |
1580 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | 1582 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) |
1581 | return unlinkPromise(filePath) | 1583 | return unlinkPromise(filePath) |
1584 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | ||
1582 | } | 1585 | } |
1583 | 1586 | ||
1584 | removeTorrent (videoFile: VideoFileModel) { | 1587 | removeTorrent (videoFile: VideoFileModel) { |
1585 | const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) | 1588 | const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) |
1586 | return unlinkPromise(torrentPath) | 1589 | return unlinkPromise(torrentPath) |
1590 | .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) | ||
1587 | } | 1591 | } |
1588 | 1592 | ||
1589 | getActivityStreamDuration () { | 1593 | getActivityStreamDuration () { |
diff --git a/shared/models/videos/video-import.model.ts b/shared/models/videos/video-import.model.ts index 858108599..b23e6b245 100644 --- a/shared/models/videos/video-import.model.ts +++ b/shared/models/videos/video-import.model.ts | |||
@@ -1,7 +1,12 @@ | |||
1 | import { Video } from './video.model' | 1 | import { Video } from './video.model' |
2 | import { VideoConstant } from './video-constant.model' | ||
3 | import { VideoImportState } from '../../index' | ||
2 | 4 | ||
3 | export interface VideoImport { | 5 | export interface VideoImport { |
4 | targetUrl: string | 6 | targetUrl: string |
7 | createdAt: string | ||
8 | updatedAt: string | ||
9 | state: VideoConstant<VideoImportState> | ||
5 | 10 | ||
6 | video: Video & { tags: string[] } | 11 | video?: Video & { tags: string[] } |
7 | } | 12 | } |