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[] }
}