mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-11-22 08:46:54 -06:00
Support video quota on client
This commit is contained in:
parent
108af66140
commit
ce5496d6a3
@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
<div class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
|
<div class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
|
||||||
Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br />
|
Transcoding is enabled on server. The video quota only take in account <strong>original</strong> video. <br />
|
||||||
In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes }}.
|
In maximum, this user could use ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -13,8 +13,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="file-max-size">(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})</div>
|
<div class="file-max-size">(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})</div>
|
||||||
|
|
||||||
|
<div class="user-quota">
|
||||||
|
<span class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ user.videoQuota | bytes: 0 }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="account-title">Account settings</div>
|
<div class="account-title">Account settings</div>
|
||||||
<my-account-change-password></my-account-change-password>
|
<my-account-change-password></my-account-change-password>
|
||||||
|
|
||||||
<div class="account-title">Videos</div>
|
<div class="account-title">Video settings</div>
|
||||||
<my-account-details [user]="user"></my-account-details>
|
<my-account-details [user]="user"></my-account-details>
|
||||||
|
@ -36,6 +36,15 @@
|
|||||||
top: -10px;
|
top: -10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-quota {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.user-quota-label {
|
||||||
|
font-weight: $font-semibold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.account-title {
|
.account-title {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: $orange-color;
|
color: $orange-color;
|
||||||
|
@ -14,6 +14,7 @@ export class AccountSettingsComponent implements OnInit {
|
|||||||
@ViewChild('avatarfileInput') avatarfileInput
|
@ViewChild('avatarfileInput') avatarfileInput
|
||||||
|
|
||||||
user: User = null
|
user: User = null
|
||||||
|
userVideoQuotaUsed = 0
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
@ -24,6 +25,9 @@ export class AccountSettingsComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.user = this.authService.getUser()
|
this.user = this.authService.getUser()
|
||||||
|
|
||||||
|
this.userService.getMyVideoQuotaUsed()
|
||||||
|
.subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvatarUrl () {
|
getAvatarUrl () {
|
||||||
|
@ -14,7 +14,6 @@ import { User } from '../../../../../shared/models/users'
|
|||||||
import { UserLogin } from '../../../../../shared/models/users/user-login.model'
|
import { UserLogin } from '../../../../../shared/models/users/user-login.model'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
import { RestExtractor } from '../../shared/rest'
|
import { RestExtractor } from '../../shared/rest'
|
||||||
import { UserConstructorHash } from '../../shared/users/user.model'
|
|
||||||
import { AuthStatus } from './auth-status.model'
|
import { AuthStatus } from './auth-status.model'
|
||||||
import { AuthUser } from './auth-user.model'
|
import { AuthUser } from './auth-user.model'
|
||||||
|
|
||||||
@ -178,12 +177,7 @@ export class AuthService {
|
|||||||
this.mergeUserInformation(obj)
|
this.mergeUserInformation(obj)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
res => {
|
res => {
|
||||||
this.user.displayNSFW = res.displayNSFW
|
this.user.patch(res)
|
||||||
this.user.autoPlayVideo = res.autoPlayVideo
|
|
||||||
this.user.role = res.role
|
|
||||||
this.user.videoChannels = res.videoChannels
|
|
||||||
this.user.account = res.account
|
|
||||||
|
|
||||||
this.user.save()
|
this.user.save()
|
||||||
|
|
||||||
this.userInformationLoaded.next(true)
|
this.userInformationLoaded.next(true)
|
||||||
@ -200,24 +194,13 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleLogin (obj: UserLoginWithUserInformation) {
|
private handleLogin (obj: UserLoginWithUserInformation) {
|
||||||
const hashUser: UserConstructorHash = {
|
|
||||||
id: obj.id,
|
|
||||||
username: obj.username,
|
|
||||||
role: obj.role,
|
|
||||||
email: obj.email,
|
|
||||||
displayNSFW: obj.displayNSFW,
|
|
||||||
autoPlayVideo: obj.autoPlayVideo,
|
|
||||||
videoQuota: obj.videoQuota,
|
|
||||||
videoChannels: obj.videoChannels,
|
|
||||||
account: obj.account
|
|
||||||
}
|
|
||||||
const hashTokens = {
|
const hashTokens = {
|
||||||
accessToken: obj.access_token,
|
accessToken: obj.access_token,
|
||||||
tokenType: obj.token_type,
|
tokenType: obj.token_type,
|
||||||
refreshToken: obj.refresh_token
|
refreshToken: obj.refresh_token
|
||||||
}
|
}
|
||||||
|
|
||||||
this.user = new AuthUser(hashUser, hashTokens)
|
this.user = new AuthUser(obj, hashTokens)
|
||||||
this.user.save()
|
this.user.save()
|
||||||
|
|
||||||
this.setStatus(AuthStatus.LoggedIn)
|
this.setStatus(AuthStatus.LoggedIn)
|
||||||
|
@ -60,4 +60,10 @@ export class User implements UserServerModel {
|
|||||||
getAvatarUrl () {
|
getAvatarUrl () {
|
||||||
return Account.GET_ACCOUNT_AVATAR_URL(this.account)
|
return Account.GET_ACCOUNT_AVATAR_URL(this.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patch (obj: UserServerModel) {
|
||||||
|
for (const key of Object.keys(obj)) {
|
||||||
|
this[key] = obj[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,11 +48,10 @@ export class UserService {
|
|||||||
.catch(res => this.restExtractor.handleError(res))
|
.catch(res => this.restExtractor.handleError(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
getMyInformation () {
|
getMyVideoQuotaUsed () {
|
||||||
const url = UserService.BASE_USERS_URL + 'me'
|
const url = UserService.BASE_USERS_URL + '/me/video-quota-used'
|
||||||
|
|
||||||
return this.authHttp.get(url)
|
return this.authHttp.get(url)
|
||||||
.map((userHash: any) => new User(userHash))
|
|
||||||
.catch(res => this.restExtractor.handleError(res))
|
.catch(res => this.restExtractor.handleError(res))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@
|
|||||||
Upload your video
|
Upload your video
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
|
||||||
|
|
||||||
<div *ngIf="!isUploadingVideo" class="upload-video-container">
|
<div *ngIf="!isUploadingVideo" class="upload-video-container">
|
||||||
<div class="upload-video">
|
<div class="upload-video">
|
||||||
<div class="icon icon-upload"></div>
|
<div class="icon icon-upload"></div>
|
||||||
|
@ -2,7 +2,9 @@ import { HttpEventType, HttpResponse } from '@angular/common/http'
|
|||||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms'
|
import { FormBuilder, FormGroup } from '@angular/forms'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
|
import { UserService } from '@app/shared'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
|
import { BytesPipe } from 'ngx-pipes'
|
||||||
import { VideoPrivacy } from '../../../../../shared/models/videos'
|
import { VideoPrivacy } from '../../../../../shared/models/videos'
|
||||||
import { AuthService, ServerService } from '../../core'
|
import { AuthService, ServerService } from '../../core'
|
||||||
import { FormReactive } from '../../shared'
|
import { FormReactive } from '../../shared'
|
||||||
@ -31,12 +33,12 @@ export class VideoAddComponent extends FormReactive implements OnInit {
|
|||||||
uuid: ''
|
uuid: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
error: string = null
|
|
||||||
form: FormGroup
|
form: FormGroup
|
||||||
formErrors: { [ id: string ]: string } = {}
|
formErrors: { [ id: string ]: string } = {}
|
||||||
validationMessages: ValidatorMessage = {}
|
validationMessages: ValidatorMessage = {}
|
||||||
|
|
||||||
userVideoChannels = []
|
userVideoChannels = []
|
||||||
|
userVideoQuotaUsed = 0
|
||||||
videoPrivacies = []
|
videoPrivacies = []
|
||||||
firstStepPrivacyId = 0
|
firstStepPrivacyId = 0
|
||||||
firstStepChannelId = 0
|
firstStepChannelId = 0
|
||||||
@ -46,6 +48,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
private userService: UserService,
|
||||||
private serverService: ServerService,
|
private serverService: ServerService,
|
||||||
private videoService: VideoService
|
private videoService: VideoService
|
||||||
) {
|
) {
|
||||||
@ -67,6 +70,9 @@ export class VideoAddComponent extends FormReactive implements OnInit {
|
|||||||
populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
|
populateAsyncUserVideoChannels(this.authService, this.userVideoChannels)
|
||||||
.then(() => this.firstStepChannelId = this.userVideoChannels[0].id)
|
.then(() => this.firstStepChannelId = this.userVideoChannels[0].id)
|
||||||
|
|
||||||
|
this.userService.getMyVideoQuotaUsed()
|
||||||
|
.subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
|
||||||
|
|
||||||
this.serverService.videoPrivaciesLoaded
|
this.serverService.videoPrivaciesLoaded
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
@ -89,6 +95,18 @@ export class VideoAddComponent extends FormReactive implements OnInit {
|
|||||||
|
|
||||||
uploadFirstStep () {
|
uploadFirstStep () {
|
||||||
const videofile = this.videofileInput.nativeElement.files[0]
|
const videofile = this.videofileInput.nativeElement.files[0]
|
||||||
|
const videoQuota = this.authService.getUser().videoQuota
|
||||||
|
if ((this.userVideoQuotaUsed + videofile.size) > videoQuota) {
|
||||||
|
const bytePipes = new BytesPipe()
|
||||||
|
|
||||||
|
const msg = 'Your video quota is exceeded with this video ' +
|
||||||
|
`(video size: ${bytePipes.transform(videofile.size, 0)}, ` +
|
||||||
|
`used: ${bytePipes.transform(this.userVideoQuotaUsed, 0)}, ` +
|
||||||
|
`quota: ${bytePipes.transform(videoQuota, 0)})`
|
||||||
|
this.notificationsService.error('Error', msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const name = videofile.name.replace(/\.[^/.]+$/, '')
|
const name = videofile.name.replace(/\.[^/.]+$/, '')
|
||||||
const privacy = this.firstStepPrivacyId.toString()
|
const privacy = this.firstStepPrivacyId.toString()
|
||||||
const nsfw = false
|
const nsfw = false
|
||||||
@ -127,8 +145,9 @@ export class VideoAddComponent extends FormReactive implements OnInit {
|
|||||||
|
|
||||||
err => {
|
err => {
|
||||||
// Reset progress
|
// Reset progress
|
||||||
|
this.isUploadingVideo = false
|
||||||
this.videoUploadPercents = 0
|
this.videoUploadPercents = 0
|
||||||
this.error = err.message
|
this.notificationsService.error('Error', err.message)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -152,7 +171,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
|
|||||||
},
|
},
|
||||||
|
|
||||||
err => {
|
err => {
|
||||||
this.error = 'Cannot update the video.'
|
this.notificationsService.error('Error', err.message)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -21,7 +21,6 @@ import { VideoService } from '../../shared/video/video.service'
|
|||||||
export class VideoUpdateComponent extends FormReactive implements OnInit {
|
export class VideoUpdateComponent extends FormReactive implements OnInit {
|
||||||
video: VideoEdit
|
video: VideoEdit
|
||||||
|
|
||||||
error: string = null
|
|
||||||
form: FormGroup
|
form: FormGroup
|
||||||
formErrors: { [ id: string ]: string } = {}
|
formErrors: { [ id: string ]: string } = {}
|
||||||
validationMessages: ValidatorMessage = {}
|
validationMessages: ValidatorMessage = {}
|
||||||
@ -82,7 +81,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||||||
|
|
||||||
err => {
|
err => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
this.error = 'Cannot fetch video.'
|
this.notificationsService.error('Error', err.message)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -108,7 +107,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
|
|||||||
},
|
},
|
||||||
|
|
||||||
err => {
|
err => {
|
||||||
this.error = 'Cannot update the video.'
|
this.notificationsService.error('Error', err.message)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -30,6 +30,11 @@ usersRouter.get('/me',
|
|||||||
asyncMiddleware(getUserInformation)
|
asyncMiddleware(getUserInformation)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
usersRouter.get('/me/video-quota-used',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(getUserVideoQuotaUsed)
|
||||||
|
)
|
||||||
|
|
||||||
usersRouter.get('/me/videos',
|
usersRouter.get('/me/videos',
|
||||||
authenticate,
|
authenticate,
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
@ -183,8 +188,18 @@ async function getUserInformation (req: express.Request, res: express.Response,
|
|||||||
return res.json(user.toFormattedJSON())
|
return res.json(user.toFormattedJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getUserVideoQuotaUsed (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
|
// We did not load channels in res.locals.user
|
||||||
|
const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
|
||||||
|
const videoQuotaUsed = await UserModel.getOriginalVideoFileTotalFromUser(user)
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
videoQuotaUsed
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function getUser (req: express.Request, res: express.Response, next: express.NextFunction) {
|
function getUser (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
return res.json(res.locals.user.toFormattedJSON())
|
return res.json((res.locals.user as UserModel).toFormattedJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) {
|
async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
|
@ -181,7 +181,7 @@ export class UserModel extends Model<UserModel> {
|
|||||||
return UserModel.findOne(query)
|
return UserModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getOriginalVideoFileTotalFromUser (user: UserModel) {
|
static getOriginalVideoFileTotalFromUser (user: UserModel) {
|
||||||
// Don't use sequelize because we need to use a sub query
|
// Don't use sequelize because we need to use a sub query
|
||||||
const query = 'SELECT SUM("size") AS "total" FROM ' +
|
const query = 'SELECT SUM("size") AS "total" FROM ' +
|
||||||
'(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
|
'(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
|
||||||
|
@ -4,7 +4,8 @@ import * as chai from 'chai'
|
|||||||
import 'mocha'
|
import 'mocha'
|
||||||
import { UserRole } from '../../../../shared/index'
|
import { UserRole } from '../../../../shared/index'
|
||||||
import {
|
import {
|
||||||
createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoRating, getUserInformation, getUsersList,
|
createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating, getUserInformation,
|
||||||
|
getUsersList,
|
||||||
getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, registerUser, removeUser, removeVideo,
|
getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, registerUser, removeUser, removeVideo,
|
||||||
runServer, ServerInfo, serverLogin, testVideoImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo
|
runServer, ServerInfo, serverLogin, testVideoImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo
|
||||||
} from '../../utils/index'
|
} from '../../utils/index'
|
||||||
@ -179,11 +180,19 @@ describe('Test users', function () {
|
|||||||
this.timeout(5000)
|
this.timeout(5000)
|
||||||
|
|
||||||
const videoAttributes = {
|
const videoAttributes = {
|
||||||
name: 'super user video'
|
name: 'super user video',
|
||||||
|
fixture: 'video_short.webm'
|
||||||
}
|
}
|
||||||
await uploadVideo(server.url, accessTokenUser, videoAttributes)
|
await uploadVideo(server.url, accessTokenUser, videoAttributes)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should have video quota updated', async function () {
|
||||||
|
const res = await getMyUserVideoQuotaUsed(server.url, accessTokenUser)
|
||||||
|
const data = res.body
|
||||||
|
|
||||||
|
expect(data.videoQuotaUsed).to.equal(218910)
|
||||||
|
})
|
||||||
|
|
||||||
it('Should be able to list my videos', async function () {
|
it('Should be able to list my videos', async function () {
|
||||||
const res = await getMyVideos(server.url, accessTokenUser, 0, 5)
|
const res = await getMyVideos(server.url, accessTokenUser, 0, 5)
|
||||||
expect(res.body.total).to.equal(1)
|
expect(res.body.total).to.equal(1)
|
||||||
|
@ -56,6 +56,17 @@ function getMyUserInformation (url: string, accessToken: string, specialStatus =
|
|||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) {
|
||||||
|
const path = '/api/v1/users/me/video-quota-used'
|
||||||
|
|
||||||
|
return request(url)
|
||||||
|
.get(path)
|
||||||
|
.set('Accept', 'application/json')
|
||||||
|
.set('Authorization', 'Bearer ' + accessToken)
|
||||||
|
.expect(specialStatus)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
}
|
||||||
|
|
||||||
function getUserInformation (url: string, accessToken: string, userId: number) {
|
function getUserInformation (url: string, accessToken: string, userId: number) {
|
||||||
const path = '/api/v1/users/' + userId
|
const path = '/api/v1/users/' + userId
|
||||||
|
|
||||||
@ -192,6 +203,7 @@ export {
|
|||||||
registerUser,
|
registerUser,
|
||||||
getMyUserInformation,
|
getMyUserInformation,
|
||||||
getMyUserVideoRating,
|
getMyUserVideoRating,
|
||||||
|
getMyUserVideoQuotaUsed,
|
||||||
getUsersList,
|
getUsersList,
|
||||||
getUsersListPaginationAndSort,
|
getUsersListPaginationAndSort,
|
||||||
removeUser,
|
removeUser,
|
||||||
|
Loading…
Reference in New Issue
Block a user