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 = [
{
title: 'Account videos'
}
}
+ },
+ {
+ path: 'video-imports',
+ component: MyAccountVideoImportsComponent,
+ data: {
+ meta: {
+ title: 'Account video imports'
+ }
+ }
}
]
}
--- /dev/null
+<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>
--- /dev/null
+@import '_variables';
+@import '_mixins';
--- /dev/null
+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)
+ )
+ }
+}
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 ''
}
<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">
+import { TableModule } from 'primeng/table'
import { NgModule } from '@angular/core'
import { SharedModule } from '../shared'
import { MyAccountRoutingModule } from './my-account-routing.module'
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: [
MyAccountVideoChannelsComponent,
MyAccountVideoChannelCreateComponent,
MyAccountVideoChannelUpdateComponent,
- ActorAvatarInfoComponent
+ ActorAvatarInfoComponent,
+ MyAccountVideoImportsComponent
],
exports: [
-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'
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 {
constructor (
private authHttp: HttpClient,
private restService: RestService,
- private restExtractor: RestExtractor
+ private restExtractor: RestExtractor,
+ private serverService: ServerService
) {}
importVideo (targetUrl: string, video: VideoUpdate): Observable<VideoImport> {
.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
+ })
+ )
+ }
}
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()
})
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'
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')
asyncMiddleware(getUserVideoQuotaUsed)
)
+
+usersRouter.get('/me/videos/imports',
+ authenticate,
+ paginationValidator,
+ videoImportsSortValidator,
+ setDefaultSort,
+ setDefaultPagination,
+ asyncMiddleware(getUserVideoImports)
+)
+
usersRouter.get('/me/videos',
authenticate,
paginationValidator,
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({
}
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,
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' ],
// 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)
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)
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)
usersSortValidator,
videoAbusesSortValidator,
videoChannelsSortValidator,
+ videoImportsSortValidator,
videosSearchSortValidator,
videosSortValidator,
blacklistSortValidator,
import {
+ AfterUpdate,
AllowNull,
BelongsTo,
Column,
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: [
required: true
}
]
+ },
+ {
+ model: () => TagModel,
+ required: false
}
]
}
@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'
+ }
}
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 () {
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[] }
}