From ed31c059851a30bd5ba9999f8ecb3822d576b9f4 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 2 Aug 2018 17:48:50 +0200 Subject: [PATCH] Add ability to list video imports --- .../+my-account/my-account-routing.module.ts | 10 +++ .../my-account-video-imports.component.html | 37 +++++++++ .../my-account-video-imports.component.scss | 2 + .../my-account-video-imports.component.ts | 66 +++++++++++++++ .../my-account-videos.component.ts | 2 + .../app/+my-account/my-account.component.html | 2 + .../src/app/+my-account/my-account.module.ts | 8 +- .../video-import/video-import.service.ts | 38 ++++++++- server.ts | 2 +- server/controllers/api/users.ts | 30 ++++++- server/helpers/youtube-dl.ts | 2 +- server/initializers/constants.ts | 1 + server/lib/job-queue/handlers/video-import.ts | 2 +- server/middlewares/validators/sort.ts | 3 + server/models/video/video-import.ts | 82 +++++++++++++++++-- server/models/video/video.ts | 8 +- shared/models/videos/video-import.model.ts | 7 +- 17 files changed, 283 insertions(+), 19 deletions(-) create mode 100644 client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.html create mode 100644 client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.scss create mode 100644 client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts 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. import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component' import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' +import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' const myAccountRoutes: Routes = [ { @@ -64,6 +65,15 @@ const myAccountRoutes: Routes = [ title: 'Account videos' } } + }, + { + path: 'video-imports', + component: MyAccountVideoImportsComponent, + data: { + meta: { + title: 'Account video imports' + } + } } ] } 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 @@ + + + + URL + Video + State + Created + + + + + + + + {{ videoImport.targetUrl }} + + + + {{ videoImport.video.name }} + + + {{ videoImport.video.name }} + + + + {{ videoImport.state.label }} + {{ videoImport.createdAt }} + + + + + + + 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 @@ +@import '_variables'; +@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 @@ +import { Component, OnInit } from '@angular/core' +import { RestPagination, RestTable } from '@app/shared' +import { SortMeta } from 'primeng/components/common/sortmeta' +import { NotificationsService } from 'angular2-notifications' +import { ConfirmService } from '@app/core' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { VideoImport, VideoImportState } from '../../../../../shared/models/videos' +import { VideoImportService } from '@app/shared/video-import' + +@Component({ + selector: 'my-account-video-imports', + templateUrl: './my-account-video-imports.component.html', + styleUrls: [ './my-account-video-imports.component.scss' ] +}) +export class MyAccountVideoImportsComponent extends RestTable implements OnInit { + videoImports: VideoImport[] = [] + totalRecords = 0 + rowsPerPage = 10 + sort: SortMeta = { field: 'createdAt', order: 1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + + constructor ( + private notificationsService: NotificationsService, + private confirmService: ConfirmService, + private videoImportService: VideoImportService, + private i18n: I18n + ) { + super() + } + + ngOnInit () { + this.loadSort() + } + + isVideoImportSuccess (videoImport: VideoImport) { + return videoImport.state.id === VideoImportState.SUCCESS + } + + isVideoImportPending (videoImport: VideoImport) { + return videoImport.state.id === VideoImportState.PENDING + } + + isVideoImportFailed (videoImport: VideoImport) { + return videoImport.state.id === VideoImportState.FAILED + } + + getVideoUrl (video: { uuid: string }) { + return '/videos/watch/' + video.uuid + } + + getEditVideoUrl (video: { uuid: string }) { + return '/videos/update/' + video.uuid + } + + protected loadData () { + this.videoImportService.getMyVideoImports(this.pagination, this.sort) + .subscribe( + resultList => { + this.videoImports = resultList.data + this.totalRecords = resultList.total + }, + + err => this.notificationsService.error(this.i18n('Error'), err.message) + ) + } +} 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 suffix = this.i18n('Waiting transcoding') } else if (video.state.id === VideoState.TO_TRANSCODE) { suffix = this.i18n('To transcode') + } else if (video.state.id === VideoState.TO_IMPORT) { + suffix = this.i18n('To import') } else { return '' } 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 @@ My video channels My videos + + My video imports
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 @@ +import { TableModule } from 'primeng/table' import { NgModule } from '@angular/core' import { SharedModule } from '../shared' import { MyAccountRoutingModule } from './my-account-routing.module' @@ -11,11 +12,13 @@ import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-vid import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component' import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' +import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' @NgModule({ imports: [ MyAccountRoutingModule, - SharedModule + SharedModule, + TableModule ], declarations: [ @@ -28,7 +31,8 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i MyAccountVideoChannelsComponent, MyAccountVideoChannelCreateComponent, MyAccountVideoChannelUpdateComponent, - ActorAvatarInfoComponent + ActorAvatarInfoComponent, + MyAccountVideoImportsComponent ], 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 @@ -import { catchError } from 'rxjs/operators' -import { HttpClient } from '@angular/common/http' +import { catchError, map, switchMap } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { Observable } from 'rxjs' import { VideoImport } from '../../../../../shared' @@ -8,6 +8,12 @@ import { RestExtractor, RestService } from '../rest' import { VideoImportCreate } from '../../../../../shared/models/videos/video-import-create.model' import { objectToFormData } from '@app/shared/misc/utils' import { VideoUpdate } from '../../../../../shared/models/videos' +import { ResultList } from '../../../../../shared/models/result-list.model' +import { UserService } from '@app/shared/users/user.service' +import { SortMeta } from 'primeng/components/common/sortmeta' +import { RestPagination } from '@app/shared/rest' +import { ServerService } from '@app/core' +import { peertubeTranslate } from '@app/shared/i18n/i18n-utils' @Injectable() export class VideoImportService { @@ -16,7 +22,8 @@ export class VideoImportService { constructor ( private authHttp: HttpClient, private restService: RestService, - private restExtractor: RestExtractor + private restExtractor: RestExtractor, + private serverService: ServerService ) {} importVideo (targetUrl: string, video: VideoUpdate): Observable { @@ -53,4 +60,29 @@ export class VideoImportService { .pipe(catchError(res => this.restExtractor.handleError(res))) } + getMyVideoImports (pagination: RestPagination, sort: SortMeta): Observable> { + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + return this.authHttp + .get>(UserService.BASE_USERS_URL + '/me/videos/imports', { params }) + .pipe( + switchMap(res => this.extractVideoImports(res)), + map(res => this.restExtractor.convertResultListDateToHuman(res)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + private extractVideoImports (result: ResultList): Observable> { + return this.serverService.localeObservable + .pipe( + map(translations => { + result.data.forEach(d => + d.state.label = peertubeTranslate(d.state.label, translations) + ) + + return result + }) + ) + } } diff --git a/server.ts b/server.ts index 9aaa64dbf..9094ac943 100644 --- a/server.ts +++ b/server.ts @@ -152,7 +152,7 @@ app.use(function (err, req, res, next) { error = err.stack || err.message || err } - logger.error('Error in controller.', { error }) + logger.error('Error in controller.', { err: error }) return res.status(err.status || 500).end() }) 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 { usersUpdateValidator, usersVideoRatingValidator } from '../../middlewares' -import { usersAskResetPasswordValidator, usersResetPasswordValidator, videosSortValidator } from '../../middlewares/validators' +import { + usersAskResetPasswordValidator, + usersResetPasswordValidator, + videoImportsSortValidator, + videosSortValidator +} from '../../middlewares/validators' import { AccountVideoRateModel } from '../../models/account/account-video-rate' import { UserModel } from '../../models/account/user' import { OAuthTokenModel } from '../../models/oauth/oauth-token' @@ -40,6 +45,7 @@ import { UserVideoQuota } from '../../../shared/models/users/user-video-quota.mo import { updateAvatarValidator } from '../../middlewares/validators/avatar' import { updateActorAvatarFile } from '../../lib/avatar' import { auditLoggerFactory, UserAuditView } from '../../helpers/audit-logger' +import { VideoImportModel } from '../../models/video/video-import' const auditLogger = auditLoggerFactory('users') @@ -62,6 +68,16 @@ usersRouter.get('/me/video-quota-used', asyncMiddleware(getUserVideoQuotaUsed) ) + +usersRouter.get('/me/videos/imports', + authenticate, + paginationValidator, + videoImportsSortValidator, + setDefaultSort, + setDefaultPagination, + asyncMiddleware(getUserVideoImports) +) + usersRouter.get('/me/videos', authenticate, paginationValidator, @@ -178,6 +194,18 @@ async function getUserVideos (req: express.Request, res: express.Response, next: return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes })) } +async function getUserVideoImports (req: express.Request, res: express.Response, next: express.NextFunction) { + const user = res.locals.oauth.token.User as UserModel + const resultList = await VideoImportModel.listUserVideoImportsForApi( + user.Account.id, + req.query.start as number, + req.query.count as number, + req.query.sort + ) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + async function createUser (req: express.Request, res: express.Response) { const body: UserCreate = req.body 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) { } function descriptionTruncation (description: string) { - if (!description) return undefined + if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined return truncate(description, { '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 = { VIDEO_ABUSES: [ 'id', 'createdAt' ], VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes' ], + VIDEO_IMPORTS: [ 'createdAt' ], VIDEO_COMMENT_THREADS: [ 'createdAt' ], BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], 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) { // Get information about this video const { videoFileResolution } = await getVideoFileResolution(tempVideoPath) - const fps = await getVideoFileFPS(tempVideoPath) + const fps = await getVideoFileFPS(tempVideoPath + 's') const stats = await statPromise(tempVideoPath) const duration = await getDurationFromVideoFile(tempVideoPath) 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) const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH) +const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS) const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) @@ -19,6 +20,7 @@ const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) +const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) @@ -32,6 +34,7 @@ export { usersSortValidator, videoAbusesSortValidator, videoChannelsSortValidator, + videoImportsSortValidator, videosSearchSortValidator, videosSortValidator, 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 @@ import { + AfterUpdate, AllowNull, BelongsTo, Column, @@ -12,13 +13,14 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { CONSTRAINTS_FIELDS } from '../../initializers' -import { throwIfNotValid } from '../utils' +import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers' +import { getSort, throwIfNotValid } from '../utils' import { VideoModel } from './video' import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports' import { VideoImport, VideoImportState } from '../../../shared' import { VideoChannelModel } from './video-channel' import { AccountModel } from '../account/account' +import { TagModel } from './tag' @DefaultScope({ include: [ @@ -35,6 +37,10 @@ import { AccountModel } from '../account/account' required: true } ] + }, + { + model: () => TagModel, + required: false } ] } @@ -79,27 +85,89 @@ export class VideoImportModel extends Model { @BelongsTo(() => VideoModel, { foreignKey: { - allowNull: false + allowNull: true }, - onDelete: 'CASCADE' + onDelete: 'set null' }) Video: VideoModel + @AfterUpdate + static deleteVideoIfFailed (instance: VideoImportModel, options) { + if (instance.state === VideoImportState.FAILED) { + return instance.Video.destroy({ transaction: options.transaction }) + } + + return undefined + } + static loadAndPopulateVideo (id: number) { return VideoImportModel.findById(id) } + static listUserVideoImportsForApi (accountId: number, start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: getSort(sort), + include: [ + { + model: VideoModel, + required: true, + include: [ + { + model: VideoChannelModel, + required: true, + include: [ + { + model: AccountModel, + required: true, + where: { + id: accountId + } + } + ] + }, + { + model: TagModel, + required: false + } + ] + } + ] + } + + return VideoImportModel.unscoped() + .findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } + toFormattedJSON (): VideoImport { const videoFormatOptions = { additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true } } - const video = Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { - tags: this.Video.Tags.map(t => t.name) - }) + const video = this.Video + ? Object.assign(this.Video.toFormattedJSON(videoFormatOptions), { + tags: this.Video.Tags.map(t => t.name) + }) + : undefined return { targetUrl: this.targetUrl, + state: { + id: this.state, + label: VideoImportModel.getStateLabel(this.state) + }, + updatedAt: this.updatedAt.toISOString(), + createdAt: this.createdAt.toISOString(), video } } + private static getStateLabel (id: number) { + return VIDEO_IMPORT_STATES[id] || 'Unknown' + } } 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 { removeThumbnail () { const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) return unlinkPromise(thumbnailPath) + .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err })) } removePreview () { - // Same name than video thumbnail - return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) + const previewPath = join(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) + return unlinkPromise(previewPath) + .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) } removeFile (videoFile: VideoFileModel) { const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) return unlinkPromise(filePath) + .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) } removeTorrent (videoFile: VideoFileModel) { const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) return unlinkPromise(torrentPath) + .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) } 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 @@ import { Video } from './video.model' +import { VideoConstant } from './video-constant.model' +import { VideoImportState } from '../../index' export interface VideoImport { targetUrl: string + createdAt: string + updatedAt: string + state: VideoConstant - video: Video & { tags: string[] } + video?: Video & { tags: string[] } } -- 2.41.0