Add ability to list video imports

pull/904/merge
Chocobozzz 2018-08-02 17:48:50 +02:00
parent 299474e827
commit ed31c05985
17 changed files with 283 additions and 19 deletions

View File

@ -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'
}
}
}
]
}

View File

@ -0,0 +1,37 @@
<p-table
[value]="videoImports" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
>
<ng-template pTemplate="header">
<tr>
<th i18n>URL</th>
<th i18n>Video</th>
<th i18n style="width: 150px">State</th>
<th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th></th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-videoImport>
<tr>
<td>
<a [href]="videoImport.targetUrl" target="_blank" rel="noopener noreferrer">{{ videoImport.targetUrl }}</a>
</td>
<td *ngIf="isVideoImportPending(videoImport)">
{{ videoImport.video.name }}
</td>
<td *ngIf="isVideoImportSuccess(videoImport)">
<a [href]="getVideoUrl(videoImport.video)" target="_blank" rel="noopener noreferrer">{{ videoImport.video.name }}</a>
</td>
<td *ngIf="isVideoImportFailed(videoImport)"></td>
<td>{{ videoImport.state.label }}</td>
<td>{{ videoImport.createdAt }}</td>
<td class="action-cell">
<my-edit-button *ngIf="isVideoImportSuccess(videoImport)" [routerLink]="getEditVideoUrl(videoImport.video)"></my-edit-button>
</td>
</tr>
</ng-template>
</p-table>

View File

@ -0,0 +1,2 @@
@import '_variables';
@import '_mixins';

View File

@ -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)
)
}
}

View File

@ -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 ''
}

View File

@ -5,6 +5,8 @@
<a i18n routerLink="/my-account/video-channels" routerLinkActive="active" class="title-page">My video channels</a>
<a i18n routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a>
<a i18n routerLink="/my-account/video-imports" routerLinkActive="active" class="title-page">My video imports</a>
</div>
<div class="margin-content">

View File

@ -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: [

View File

@ -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<VideoImport> {
@ -53,4 +60,29 @@ export class VideoImportService {
.pipe(catchError(res => this.restExtractor.handleError(res)))
}
getMyVideoImports (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoImport>> {
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
return this.authHttp
.get<ResultList<VideoImport>>(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<VideoImport>): Observable<ResultList<VideoImport>> {
return this.serverService.localeObservable
.pipe(
map(translations => {
result.data.forEach(d =>
d.state.label = peertubeTranslate(d.state.label, translations)
)
return result
})
)
}
}

View File

@ -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()
})

View File

@ -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({

View File

@ -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,

View File

@ -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' ],

View File

@ -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)

View File

@ -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,

View File

@ -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<VideoImportModel> {
@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'
}
}

View File

@ -1569,21 +1569,25 @@ export class VideoModel extends Model<VideoModel> {
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 () {

View File

@ -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<VideoImportState>
video: Video & { tags: string[] }
video?: Video & { tags: string[] }
}