From 67ed6552b831df66713bac9e672738796128d33f Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 23 Jun 2020 14:10:17 +0200 Subject: Reorganize client shared modules --- client/src/app/shared/shared-main/video/index.ts | 7 + .../shared/shared-main/video/redundancy.service.ts | 73 ++++ .../shared-main/video/video-details.model.ts | 69 ++++ .../shared/shared-main/video/video-edit.model.ts | 120 +++++++ .../shared-main/video/video-import.service.ts | 100 ++++++ .../shared-main/video/video-ownership.service.ts | 64 ++++ .../app/shared/shared-main/video/video.model.ts | 188 ++++++++++ .../app/shared/shared-main/video/video.service.ts | 380 +++++++++++++++++++++ 8 files changed, 1001 insertions(+) create mode 100644 client/src/app/shared/shared-main/video/index.ts create mode 100644 client/src/app/shared/shared-main/video/redundancy.service.ts create mode 100644 client/src/app/shared/shared-main/video/video-details.model.ts create mode 100644 client/src/app/shared/shared-main/video/video-edit.model.ts create mode 100644 client/src/app/shared/shared-main/video/video-import.service.ts create mode 100644 client/src/app/shared/shared-main/video/video-ownership.service.ts create mode 100644 client/src/app/shared/shared-main/video/video.model.ts create mode 100644 client/src/app/shared/shared-main/video/video.service.ts (limited to 'client/src/app/shared/shared-main/video') diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts new file mode 100644 index 000000000..3053df4ef --- /dev/null +++ b/client/src/app/shared/shared-main/video/index.ts @@ -0,0 +1,7 @@ +export * from './redundancy.service' +export * from './video-details.model' +export * from './video-edit.model' +export * from './video-import.service' +export * from './video-ownership.service' +export * from './video.model' +export * from './video.service' diff --git a/client/src/app/shared/shared-main/video/redundancy.service.ts b/client/src/app/shared/shared-main/video/redundancy.service.ts new file mode 100644 index 000000000..6e839e655 --- /dev/null +++ b/client/src/app/shared/shared-main/video/redundancy.service.ts @@ -0,0 +1,73 @@ +import { SortMeta } from 'primeng/api' +import { concat, Observable } from 'rxjs' +import { catchError, map, toArray } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, RestPagination, RestService } from '@app/core' +import { ResultList, Video, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' +import { environment } from '../../../../environments/environment' + +@Injectable() +export class RedundancyService { + static BASE_REDUNDANCY_URL = environment.apiUrl + '/api/v1/server/redundancy' + + constructor ( + private authHttp: HttpClient, + private restService: RestService, + private restExtractor: RestExtractor + ) { } + + updateRedundancy (host: string, redundancyAllowed: boolean) { + const url = RedundancyService.BASE_REDUNDANCY_URL + '/' + host + + const body = { redundancyAllowed } + + return this.authHttp.put(url, body) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + listVideoRedundancies (options: { + pagination: RestPagination, + sort: SortMeta, + target?: VideoRedundanciesTarget + }): Observable> { + const { pagination, sort, target } = options + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + if (target) params = params.append('target', target) + + return this.authHttp.get>(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { params }) + .pipe( + catchError(res => this.restExtractor.handleError(res)) + ) + } + + addVideoRedundancy (video: Video) { + return this.authHttp.post(RedundancyService.BASE_REDUNDANCY_URL + '/videos', { videoId: video.id }) + .pipe( + catchError(res => this.restExtractor.handleError(res)) + ) + } + + removeVideoRedundancies (redundancy: VideoRedundancy) { + const observables = redundancy.redundancies.streamingPlaylists.map(r => r.id) + .concat(redundancy.redundancies.files.map(r => r.id)) + .map(id => this.removeRedundancy(id)) + + return concat(...observables) + .pipe(toArray()) + } + + private removeRedundancy (redundancyId: number) { + return this.authHttp.delete(RedundancyService.BASE_REDUNDANCY_URL + '/videos/' + redundancyId) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } +} diff --git a/client/src/app/shared/shared-main/video/video-details.model.ts b/client/src/app/shared/shared-main/video/video-details.model.ts new file mode 100644 index 000000000..a1cb051e9 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-details.model.ts @@ -0,0 +1,69 @@ +import { Account } from '@app/shared/shared-main/account/account.model' +import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' +import { + VideoConstant, + VideoDetails as VideoDetailsServerModel, + VideoFile, + VideoState, + VideoStreamingPlaylist, + VideoStreamingPlaylistType +} from '@shared/models' +import { Video } from './video.model' + +export class VideoDetails extends Video implements VideoDetailsServerModel { + descriptionPath: string + support: string + channel: VideoChannel + tags: string[] + files: VideoFile[] + account: Account + commentsEnabled: boolean + downloadEnabled: boolean + + waitTranscoding: boolean + state: VideoConstant + + likesPercent: number + dislikesPercent: number + + trackerUrls: string[] + + streamingPlaylists: VideoStreamingPlaylist[] + + constructor (hash: VideoDetailsServerModel, translations = {}) { + super(hash, translations) + + this.descriptionPath = hash.descriptionPath + this.files = hash.files + this.channel = new VideoChannel(hash.channel) + this.account = new Account(hash.account) + this.tags = hash.tags + this.support = hash.support + this.commentsEnabled = hash.commentsEnabled + this.downloadEnabled = hash.downloadEnabled + + this.trackerUrls = hash.trackerUrls + this.streamingPlaylists = hash.streamingPlaylists + + this.buildLikeAndDislikePercents() + } + + buildLikeAndDislikePercents () { + this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 + this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 + } + + getHlsPlaylist () { + return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) + } + + hasHlsPlaylist () { + return !!this.getHlsPlaylist() + } + + getFiles () { + if (this.files.length === 0) return this.getHlsPlaylist().files + + return this.files + } +} diff --git a/client/src/app/shared/shared-main/video/video-edit.model.ts b/client/src/app/shared/shared-main/video/video-edit.model.ts new file mode 100644 index 000000000..6a529e052 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-edit.model.ts @@ -0,0 +1,120 @@ +import { Video, VideoPrivacy, VideoScheduleUpdate, VideoUpdate } from '@shared/models' + +export class VideoEdit implements VideoUpdate { + static readonly SPECIAL_SCHEDULED_PRIVACY = -1 + + category: number + licence: number + language: string + description: string + name: string + tags: string[] + nsfw: boolean + commentsEnabled: boolean + downloadEnabled: boolean + waitTranscoding: boolean + channelId: number + privacy: VideoPrivacy + support: string + thumbnailfile?: any + previewfile?: any + thumbnailUrl: string + previewUrl: string + uuid?: string + id?: number + scheduleUpdate?: VideoScheduleUpdate + originallyPublishedAt?: Date | string + + constructor ( + video?: Video & { + tags: string[], + commentsEnabled: boolean, + downloadEnabled: boolean, + support: string, + thumbnailUrl: string, + previewUrl: string + }) { + if (video) { + this.id = video.id + this.uuid = video.uuid + this.category = video.category.id + this.licence = video.licence.id + this.language = video.language.id + this.description = video.description + this.name = video.name + this.tags = video.tags + this.nsfw = video.nsfw + this.commentsEnabled = video.commentsEnabled + this.downloadEnabled = video.downloadEnabled + this.waitTranscoding = video.waitTranscoding + this.channelId = video.channel.id + this.privacy = video.privacy.id + this.support = video.support + this.thumbnailUrl = video.thumbnailUrl + this.previewUrl = video.previewUrl + + this.scheduleUpdate = video.scheduledUpdate + this.originallyPublishedAt = video.originallyPublishedAt ? new Date(video.originallyPublishedAt) : null + } + } + + patch (values: { [ id: string ]: string }) { + Object.keys(values).forEach((key) => { + this[ key ] = values[ key ] + }) + + // If schedule publication, the video is private and will be changed to public privacy + if (parseInt(values['privacy'], 10) === VideoEdit.SPECIAL_SCHEDULED_PRIVACY) { + const updateAt = new Date(values['schedulePublicationAt']) + updateAt.setSeconds(0) + + this.privacy = VideoPrivacy.PRIVATE + this.scheduleUpdate = { + updateAt: updateAt.toISOString(), + privacy: VideoPrivacy.PUBLIC + } + } else { + this.scheduleUpdate = null + } + + // Convert originallyPublishedAt to string so that function objectToFormData() works correctly + if (this.originallyPublishedAt) { + const originallyPublishedAt = new Date(values['originallyPublishedAt']) + this.originallyPublishedAt = originallyPublishedAt.toISOString() + } + + // Use the same file than the preview for the thumbnail + if (this.previewfile) { + this.thumbnailfile = this.previewfile + } + } + + toFormPatch () { + const json = { + category: this.category, + licence: this.licence, + language: this.language, + description: this.description, + support: this.support, + name: this.name, + tags: this.tags, + nsfw: this.nsfw, + commentsEnabled: this.commentsEnabled, + downloadEnabled: this.downloadEnabled, + waitTranscoding: this.waitTranscoding, + channelId: this.channelId, + privacy: this.privacy, + originallyPublishedAt: this.originallyPublishedAt + } + + // Special case if we scheduled an update + if (this.scheduleUpdate) { + Object.assign(json, { + privacy: VideoEdit.SPECIAL_SCHEDULED_PRIVACY, + schedulePublicationAt: new Date(this.scheduleUpdate.updateAt.toString()) + }) + } + + return json + } +} diff --git a/client/src/app/shared/shared-main/video/video-import.service.ts b/client/src/app/shared/shared-main/video/video-import.service.ts new file mode 100644 index 000000000..a700abacb --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-import.service.ts @@ -0,0 +1,100 @@ +import { SortMeta } from 'primeng/api' +import { Observable } from 'rxjs' +import { catchError, map, switchMap } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, RestPagination, RestService, ServerService, UserService } from '@app/core' +import { objectToFormData } from '@app/helpers' +import { peertubeTranslate, ResultList, VideoImport, VideoImportCreate, VideoUpdate } from '@shared/models' +import { environment } from '../../../../environments/environment' + +@Injectable() +export class VideoImportService { + private static BASE_VIDEO_IMPORT_URL = environment.apiUrl + '/api/v1/videos/imports/' + + constructor ( + private authHttp: HttpClient, + private restService: RestService, + private restExtractor: RestExtractor, + private serverService: ServerService + ) {} + + importVideoUrl (targetUrl: string, video: VideoUpdate): Observable { + const url = VideoImportService.BASE_VIDEO_IMPORT_URL + + const body = this.buildImportVideoObject(video) + body.targetUrl = targetUrl + + const data = objectToFormData(body) + return this.authHttp.post(url, data) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + + importVideoTorrent (target: string | Blob, video: VideoUpdate): Observable { + const url = VideoImportService.BASE_VIDEO_IMPORT_URL + const body: VideoImportCreate = this.buildImportVideoObject(video) + + if (typeof target === 'string') body.magnetUri = target + else body.torrentfile = target + + const data = objectToFormData(body) + return this.authHttp.post(url, data) + .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 buildImportVideoObject (video: VideoUpdate): VideoImportCreate { + const language = video.language || null + const licence = video.licence || null + const category = video.category || null + const description = video.description || null + const support = video.support || null + const scheduleUpdate = video.scheduleUpdate || null + const originallyPublishedAt = video.originallyPublishedAt || null + + return { + name: video.name, + category, + licence, + language, + support, + description, + channelId: video.channelId, + privacy: video.privacy, + tags: video.tags, + nsfw: video.nsfw, + waitTranscoding: video.waitTranscoding, + commentsEnabled: video.commentsEnabled, + downloadEnabled: video.downloadEnabled, + thumbnailfile: video.thumbnailfile, + previewfile: video.previewfile, + scheduleUpdate, + originallyPublishedAt + } + } + + private extractVideoImports (result: ResultList): Observable> { + return this.serverService.getServerLocale() + .pipe( + map(translations => { + result.data.forEach(d => + d.state.label = peertubeTranslate(d.state.label, translations) + ) + + return result + }) + ) + } +} diff --git a/client/src/app/shared/shared-main/video/video-ownership.service.ts b/client/src/app/shared/shared-main/video/video-ownership.service.ts new file mode 100644 index 000000000..273930a6c --- /dev/null +++ b/client/src/app/shared/shared-main/video/video-ownership.service.ts @@ -0,0 +1,64 @@ +import { SortMeta } from 'primeng/api' +import { Observable } from 'rxjs' +import { catchError, map } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, RestPagination, RestService } from '@app/core' +import { ResultList, VideoChangeOwnership, VideoChangeOwnershipAccept, VideoChangeOwnershipCreate } from '@shared/models' +import { environment } from '../../../../environments/environment' + +@Injectable() +export class VideoOwnershipService { + private static BASE_VIDEO_CHANGE_OWNERSHIP_URL = environment.apiUrl + '/api/v1/videos/' + + constructor ( + private authHttp: HttpClient, + private restService: RestService, + private restExtractor: RestExtractor + ) { + } + + changeOwnership (id: number, username: string) { + const url = VideoOwnershipService.BASE_VIDEO_CHANGE_OWNERSHIP_URL + id + '/give-ownership' + const body: VideoChangeOwnershipCreate = { + username + } + + return this.authHttp.post(url, body) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + getOwnershipChanges (pagination: RestPagination, sort: SortMeta): Observable> { + const url = VideoOwnershipService.BASE_VIDEO_CHANGE_OWNERSHIP_URL + 'ownership' + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + return this.authHttp.get>(url, { params }) + .pipe( + map(res => this.restExtractor.convertResultListDateToHuman(res)), + catchError(res => this.restExtractor.handleError(res)) + ) + } + + acceptOwnership (id: number, input: VideoChangeOwnershipAccept) { + const url = VideoOwnershipService.BASE_VIDEO_CHANGE_OWNERSHIP_URL + 'ownership/' + id + '/accept' + return this.authHttp.post(url, input) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(this.restExtractor.handleError) + ) + } + + refuseOwnership (id: number) { + const url = VideoOwnershipService.BASE_VIDEO_CHANGE_OWNERSHIP_URL + 'ownership/' + id + '/refuse' + return this.authHttp.post(url, {}) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(this.restExtractor.handleError) + ) + } +} diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts new file mode 100644 index 000000000..3e6d6a38d --- /dev/null +++ b/client/src/app/shared/shared-main/video/video.model.ts @@ -0,0 +1,188 @@ +import { AuthUser } from '@app/core' +import { User } from '@app/core/users/user.model' +import { durationToString, getAbsoluteAPIUrl } from '@app/helpers' +import { + Avatar, + peertubeTranslate, + ServerConfig, + UserRight, + Video as VideoServerModel, + VideoConstant, + VideoPrivacy, + VideoScheduleUpdate, + VideoState +} from '@shared/models' +import { environment } from '../../../../environments/environment' +import { Actor } from '../account/actor.model' + +export class Video implements VideoServerModel { + byVideoChannel: string + byAccount: string + + accountAvatarUrl: string + videoChannelAvatarUrl: string + + createdAt: Date + updatedAt: Date + publishedAt: Date + originallyPublishedAt: Date | string + category: VideoConstant + licence: VideoConstant + language: VideoConstant + privacy: VideoConstant + description: string + duration: number + durationLabel: string + id: number + uuid: string + isLocal: boolean + name: string + serverHost: string + thumbnailPath: string + thumbnailUrl: string + + previewPath: string + previewUrl: string + + embedPath: string + embedUrl: string + + url?: string + + views: number + likes: number + dislikes: number + nsfw: boolean + + originInstanceUrl: string + originInstanceHost: string + + waitTranscoding?: boolean + state?: VideoConstant + scheduledUpdate?: VideoScheduleUpdate + blacklisted?: boolean + blockedReason?: string + + account: { + id: number + name: string + displayName: string + url: string + host: string + avatar?: Avatar + } + + channel: { + id: number + name: string + displayName: string + url: string + host: string + avatar?: Avatar + } + + userHistory?: { + currentTime: number + } + + static buildClientUrl (videoUUID: string) { + return '/videos/watch/' + videoUUID + } + + constructor (hash: VideoServerModel, translations = {}) { + const absoluteAPIUrl = getAbsoluteAPIUrl() + + this.createdAt = new Date(hash.createdAt.toString()) + this.publishedAt = new Date(hash.publishedAt.toString()) + this.category = hash.category + this.licence = hash.licence + this.language = hash.language + this.privacy = hash.privacy + this.waitTranscoding = hash.waitTranscoding + this.state = hash.state + this.description = hash.description + + this.duration = hash.duration + this.durationLabel = durationToString(hash.duration) + + this.id = hash.id + this.uuid = hash.uuid + + this.isLocal = hash.isLocal + this.name = hash.name + + this.thumbnailPath = hash.thumbnailPath + this.thumbnailUrl = hash.thumbnailUrl || (absoluteAPIUrl + hash.thumbnailPath) + + this.previewPath = hash.previewPath + this.previewUrl = hash.previewUrl || (absoluteAPIUrl + hash.previewPath) + + this.embedPath = hash.embedPath + this.embedUrl = hash.embedUrl || (environment.embedUrl + hash.embedPath) + + this.url = hash.url + + this.views = hash.views + this.likes = hash.likes + this.dislikes = hash.dislikes + + this.nsfw = hash.nsfw + + this.account = hash.account + this.channel = hash.channel + + this.byAccount = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host) + this.byVideoChannel = Actor.CREATE_BY_STRING(hash.channel.name, hash.channel.host) + this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account) + this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.channel) + + this.category.label = peertubeTranslate(this.category.label, translations) + this.licence.label = peertubeTranslate(this.licence.label, translations) + this.language.label = peertubeTranslate(this.language.label, translations) + this.privacy.label = peertubeTranslate(this.privacy.label, translations) + + this.scheduledUpdate = hash.scheduledUpdate + this.originallyPublishedAt = hash.originallyPublishedAt ? new Date(hash.originallyPublishedAt.toString()) : null + + if (this.state) this.state.label = peertubeTranslate(this.state.label, translations) + + this.blacklisted = hash.blacklisted + this.blockedReason = hash.blacklistedReason + + this.userHistory = hash.userHistory + + this.originInstanceHost = this.account.host + this.originInstanceUrl = 'https://' + this.originInstanceHost + } + + isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { + // Video is not NSFW, skip + if (this.nsfw === false) return false + + // Return user setting if logged in + if (user) return user.nsfwPolicy !== 'display' + + // Return default instance config + return serverConfig.instance.defaultNSFWPolicy !== 'display' + } + + isRemovableBy (user: AuthUser) { + return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) + } + + isBlockableBy (user: AuthUser) { + return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true + } + + isUnblockableBy (user: AuthUser) { + return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true + } + + isUpdatableBy (user: AuthUser) { + return user && this.isLocal === true && (this.account.name === user.username || user.hasRight(UserRight.UPDATE_ANY_VIDEO)) + } + + canBeDuplicatedBy (user: AuthUser) { + return user && this.isLocal === false && user.hasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES) + } +} diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts new file mode 100644 index 000000000..20d13fa10 --- /dev/null +++ b/client/src/app/shared/shared-main/video/video.service.ts @@ -0,0 +1,380 @@ +import { FfprobeData } from 'fluent-ffmpeg' +import { Observable } from 'rxjs' +import { catchError, map, switchMap } from 'rxjs/operators' +import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' +import { objectToFormData } from '@app/helpers' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { + FeedFormat, + NSFWPolicyType, + ResultList, + UserVideoRate, + UserVideoRateType, + UserVideoRateUpdate, + Video as VideoServerModel, + VideoConstant, + VideoDetails as VideoDetailsServerModel, + VideoFilter, + VideoPrivacy, + VideoSortField, + VideoUpdate +} from '@shared/models' +import { environment } from '../../../../environments/environment' +import { Account, AccountService } from '../account' +import { VideoChannel, VideoChannelService } from '../video-channel' +import { VideoDetails } from './video-details.model' +import { VideoEdit } from './video-edit.model' +import { Video } from './video.model' + +export interface VideosProvider { + getVideos (parameters: { + videoPagination: ComponentPaginationLight, + sort: VideoSortField, + filter?: VideoFilter, + categoryOneOf?: number[], + languageOneOf?: string[] + nsfwPolicy: NSFWPolicyType + }): Observable> +} + +@Injectable() +export class VideoService implements VideosProvider { + static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' + static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private restService: RestService, + private serverService: ServerService, + private i18n: I18n + ) {} + + getVideoViewUrl (uuid: string) { + return VideoService.BASE_VIDEO_URL + uuid + '/views' + } + + getUserWatchingVideoUrl (uuid: string) { + return VideoService.BASE_VIDEO_URL + uuid + '/watching' + } + + getVideo (options: { videoId: string }): Observable { + return this.serverService.getServerLocale() + .pipe( + switchMap(translations => { + return this.authHttp.get(VideoService.BASE_VIDEO_URL + options.videoId) + .pipe(map(videoHash => ({ videoHash, translations }))) + }), + map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + updateVideo (video: VideoEdit) { + const language = video.language || null + const licence = video.licence || null + const category = video.category || null + const description = video.description || null + const support = video.support || null + const scheduleUpdate = video.scheduleUpdate || null + const originallyPublishedAt = video.originallyPublishedAt || null + + const body: VideoUpdate = { + name: video.name, + category, + licence, + language, + support, + description, + channelId: video.channelId, + privacy: video.privacy, + tags: video.tags, + nsfw: video.nsfw, + waitTranscoding: video.waitTranscoding, + commentsEnabled: video.commentsEnabled, + downloadEnabled: video.downloadEnabled, + thumbnailfile: video.thumbnailfile, + previewfile: video.previewfile, + scheduleUpdate, + originallyPublishedAt + } + + const data = objectToFormData(body) + + return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, data) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + uploadVideo (video: FormData) { + const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true }) + + return this.authHttp + .request<{ video: { id: number, uuid: string } }>(req) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + getMyVideos (videoPagination: ComponentPaginationLight, sort: VideoSortField, search?: string): Observable> { + const pagination = this.restService.componentPaginationToRestPagination(videoPagination) + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + params = this.restService.addObjectParams(params, { search }) + + return this.authHttp + .get>(UserService.BASE_USERS_URL + 'me/videos', { params }) + .pipe( + switchMap(res => this.extractVideos(res)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + getAccountVideos ( + account: Account, + videoPagination: ComponentPaginationLight, + sort: VideoSortField + ): Observable> { + const pagination = this.restService.componentPaginationToRestPagination(videoPagination) + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + return this.authHttp + .get>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params }) + .pipe( + switchMap(res => this.extractVideos(res)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + getVideoChannelVideos ( + videoChannel: VideoChannel, + videoPagination: ComponentPaginationLight, + sort: VideoSortField, + nsfwPolicy?: NSFWPolicyType + ): Observable> { + const pagination = this.restService.componentPaginationToRestPagination(videoPagination) + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + if (nsfwPolicy) { + params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) + } + + return this.authHttp + .get>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params }) + .pipe( + switchMap(res => this.extractVideos(res)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + getVideos (parameters: { + videoPagination: ComponentPaginationLight, + sort: VideoSortField, + filter?: VideoFilter, + categoryOneOf?: number[], + languageOneOf?: string[], + skipCount?: boolean, + nsfwPolicy?: NSFWPolicyType + }): Observable> { + const { videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy } = parameters + + const pagination = this.restService.componentPaginationToRestPagination(videoPagination) + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + if (filter) params = params.set('filter', filter) + if (skipCount) params = params.set('skipCount', skipCount + '') + + if (nsfwPolicy) { + params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) + } + + if (languageOneOf) { + for (const l of languageOneOf) { + params = params.append('languageOneOf[]', l) + } + } + + if (categoryOneOf) { + for (const c of categoryOneOf) { + params = params.append('categoryOneOf[]', c + '') + } + } + + return this.authHttp + .get>(VideoService.BASE_VIDEO_URL, { params }) + .pipe( + switchMap(res => this.extractVideos(res)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + buildBaseFeedUrls (params: HttpParams) { + const feeds = [ + { + format: FeedFormat.RSS, + label: 'rss 2.0', + url: VideoService.BASE_FEEDS_URL + FeedFormat.RSS.toLowerCase() + }, + { + format: FeedFormat.ATOM, + label: 'atom 1.0', + url: VideoService.BASE_FEEDS_URL + FeedFormat.ATOM.toLowerCase() + }, + { + format: FeedFormat.JSON, + label: 'json 1.0', + url: VideoService.BASE_FEEDS_URL + FeedFormat.JSON.toLowerCase() + } + ] + + if (params && params.keys().length !== 0) { + for (const feed of feeds) { + feed.url += '?' + params.toString() + } + } + + return feeds + } + + getVideoFeedUrls (sort: VideoSortField, filter?: VideoFilter, categoryOneOf?: number[]) { + let params = this.restService.addRestGetParams(new HttpParams(), undefined, sort) + + if (filter) params = params.set('filter', filter) + + if (categoryOneOf) { + for (const c of categoryOneOf) { + params = params.append('categoryOneOf[]', c + '') + } + } + + return this.buildBaseFeedUrls(params) + } + + getAccountFeedUrls (accountId: number) { + let params = this.restService.addRestGetParams(new HttpParams()) + params = params.set('accountId', accountId.toString()) + + return this.buildBaseFeedUrls(params) + } + + getVideoChannelFeedUrls (videoChannelId: number) { + let params = this.restService.addRestGetParams(new HttpParams()) + params = params.set('videoChannelId', videoChannelId.toString()) + + return this.buildBaseFeedUrls(params) + } + + getVideoFileMetadata (metadataUrl: string) { + return this.authHttp + .get(metadataUrl) + .pipe( + catchError(err => this.restExtractor.handleError(err)) + ) + } + + removeVideo (id: number) { + return this.authHttp + .delete(VideoService.BASE_VIDEO_URL + id) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + loadCompleteDescription (descriptionPath: string) { + return this.authHttp + .get<{ description: string }>(environment.apiUrl + descriptionPath) + .pipe( + map(res => res.description), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + setVideoLike (id: number) { + return this.setVideoRate(id, 'like') + } + + setVideoDislike (id: number) { + return this.setVideoRate(id, 'dislike') + } + + unsetVideoLike (id: number) { + return this.setVideoRate(id, 'none') + } + + getUserVideoRating (id: number) { + const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' + + return this.authHttp.get(url) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + extractVideos (result: ResultList) { + return this.serverService.getServerLocale() + .pipe( + map(translations => { + const videosJson = result.data + const totalVideos = result.total + const videos: Video[] = [] + + for (const videoJson of videosJson) { + videos.push(new Video(videoJson, translations)) + } + + return { total: totalVideos, data: videos } + }) + ) + } + + explainedPrivacyLabels (privacies: VideoConstant[]) { + const base = [ + { + id: VideoPrivacy.PRIVATE, + label: this.i18n('Only I can see this video') + }, + { + id: VideoPrivacy.UNLISTED, + label: this.i18n('Only people with the private link can see this video') + }, + { + id: VideoPrivacy.PUBLIC, + label: this.i18n('Anyone can see this video') + }, + { + id: VideoPrivacy.INTERNAL, + label: this.i18n('Only users of this instance can see this video') + } + ] + + return base.filter(o => !!privacies.find(p => p.id === o.id)) + } + + nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) { + return nsfwPolicy === 'do_not_list' + ? 'false' + : 'both' + } + + private setVideoRate (id: number, rateType: UserVideoRateType) { + const url = VideoService.BASE_VIDEO_URL + id + '/rate' + const body: UserVideoRateUpdate = { + rating: rateType + } + + return this.authHttp + .put(url, body) + .pipe( + map(this.restExtractor.extractDataBool), + catchError(err => this.restExtractor.handleError(err)) + ) + } +} -- cgit v1.2.3