X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=client%2Fsrc%2Fapp%2Fshared%2Fshared-main%2Fvideo%2Fvideo.service.ts;h=8c8b1e08ff701a6ebef0cc5c9c1cbadb1e88b900;hb=1bb4c9ab2e8b3b3022351b33a82a5e527fa5d4d7;hp=b81540e8ddeabd1cb1356fadcbc2fd90941b0bd1;hpb=afff310e50f2fa8419bb4242470cbde46ab54463;p=github%2FChocobozzz%2FPeerTube.git diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index b81540e8d..8c8b1e08f 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts @@ -1,10 +1,13 @@ -import { Observable } from 'rxjs' -import { catchError, map, switchMap } from 'rxjs/operators' +import { SortMeta } from 'primeng/api' +import { from, Observable, of } from 'rxjs' +import { catchError, concatMap, map, switchMap, toArray } from 'rxjs/operators' import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http' import { Injectable } from '@angular/core' -import { ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService, AuthService } from '@app/core' +import { AuthService, ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core' import { objectToFormData } from '@app/helpers' +import { arrayify } from '@shared/core-utils' import { + BooleanBothQuery, FeedFormat, NSFWPolicyType, ResultList, @@ -12,15 +15,17 @@ import { UserVideoRateType, UserVideoRateUpdate, Video as VideoServerModel, + VideoChannel as VideoChannelServerModel, VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFileMetadata, - VideoFilter, + VideoInclude, VideoPrivacy, VideoSortField, - VideoUpdate, - VideoCreate + VideoTranscodingCreate, + VideoUpdate } from '@shared/models' +import { VideoSource } from '@shared/models/videos/video-source' import { environment } from '../../../../environments/environment' import { Account } from '../account/account.model' import { AccountService } from '../account/account.service' @@ -29,43 +34,45 @@ 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> +export type CommonVideoParams = { + videoPagination?: ComponentPaginationLight + sort: VideoSortField | SortMeta + include?: VideoInclude + isLocal?: boolean + categoryOneOf?: number[] + languageOneOf?: string[] + privacyOneOf?: VideoPrivacy[] + isLive?: boolean + skipCount?: boolean + + // FIXME: remove? + nsfwPolicy?: NSFWPolicyType + nsfw?: BooleanBothQuery } @Injectable() -export class VideoService implements VideosProvider { - static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' +export class VideoService { + static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos' static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.' + static BASE_SUBSCRIPTION_FEEDS_URL = environment.apiUrl + '/feeds/subscriptions.' constructor ( + private auth: AuthService, private authHttp: HttpClient, private restExtractor: RestExtractor, private restService: RestService, - private serverService: ServerService, - private authService: AuthService + private serverService: ServerService ) {} getVideoViewUrl (uuid: string) { - return VideoService.BASE_VIDEO_URL + uuid + '/views' - } - - getUserWatchingVideoUrl (uuid: string) { - return VideoService.BASE_VIDEO_URL + uuid + '/watching' + return `${VideoService.BASE_VIDEO_URL}/${uuid}/views` } getVideo (options: { videoId: string }): Observable { return this.serverService.getServerLocale() .pipe( switchMap(translations => { - return this.authHttp.get(VideoService.BASE_VIDEO_URL + options.videoId) + return this.authHttp.get(`${VideoService.BASE_VIDEO_URL}/${options.videoId}`) .pipe(map(videoHash => ({ videoHash, translations }))) }), map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)), @@ -105,27 +112,51 @@ export class VideoService implements VideosProvider { 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)) - ) + return this.authHttp.put(`${VideoService.BASE_VIDEO_URL}/${video.id}`, data) + .pipe(catchError(err => this.restExtractor.handleError(err))) } uploadVideo (video: FormData) { - const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true }) + 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) + getMyVideos (options: { + videoPagination: ComponentPaginationLight + sort: VideoSortField + userChannels?: VideoChannelServerModel[] + search?: string + }): Observable> { + const { videoPagination, sort, userChannels = [], search } = options + + const pagination = this.restService.componentToRestPagination(videoPagination) let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) - params = this.restService.addObjectParams(params, { search }) + + if (search) { + const filters = this.restService.parseQueryStringFilter(search, { + isLive: { + prefix: 'isLive:', + isBoolean: true + }, + channelId: { + prefix: 'channel:', + handler: (name: string) => { + const channel = userChannels.find(c => c.name === name) + + if (channel) return channel.id + + return undefined + } + } + }) + + params = this.restService.addObjectParams(params, filters) + } return this.authHttp .get>(UserService.BASE_USERS_URL + 'me/videos', { params }) @@ -135,27 +166,16 @@ export class VideoService implements VideosProvider { ) } - getAccountVideos (parameters: { - account: Account, - videoPagination: ComponentPaginationLight, - sort: VideoSortField - nsfwPolicy?: NSFWPolicyType - videoFilter?: VideoFilter + getAccountVideos (parameters: CommonVideoParams & { + account: Pick + search?: string }): Observable> { - const { account, videoPagination, sort, videoFilter, nsfwPolicy } = parameters - - const pagination = this.restService.componentPaginationToRestPagination(videoPagination) + const { account, search } = parameters let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - - if (nsfwPolicy) { - params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) - } + params = this.buildCommonVideosParams({ params, ...parameters }) - if (videoFilter) { - params = params.set('filter', videoFilter) - } + if (search) params = params.set('search', search) return this.authHttp .get>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params }) @@ -165,27 +185,13 @@ export class VideoService implements VideosProvider { ) } - getVideoChannelVideos (parameters: { - videoChannel: VideoChannel, - videoPagination: ComponentPaginationLight, - sort: VideoSortField, - nsfwPolicy?: NSFWPolicyType - videoFilter?: VideoFilter + getVideoChannelVideos (parameters: CommonVideoParams & { + videoChannel: Pick }): Observable> { - const { videoChannel, videoPagination, sort, nsfwPolicy, videoFilter } = parameters - - const pagination = this.restService.componentPaginationToRestPagination(videoPagination) + const { videoChannel } = parameters let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - - if (nsfwPolicy) { - params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) - } - - if (videoFilter) { - params = params.set('filter', videoFilter) - } + params = this.buildCommonVideosParams({ params, ...parameters }) return this.authHttp .get>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params }) @@ -195,40 +201,9 @@ export class VideoService implements VideosProvider { ) } - 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) - + getVideos (parameters: CommonVideoParams): Observable> { 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 + '') - } - } + params = this.buildCommonVideosParams({ params, ...parameters }) return this.authHttp .get>(VideoService.BASE_VIDEO_URL, { params }) @@ -238,22 +213,22 @@ export class VideoService implements VideosProvider { ) } - buildBaseFeedUrls (params: HttpParams) { + buildBaseFeedUrls (params: HttpParams, base = VideoService.BASE_FEEDS_URL) { const feeds = [ { format: FeedFormat.RSS, label: 'media rss 2.0', - url: VideoService.BASE_FEEDS_URL + FeedFormat.RSS.toLowerCase() + url: base + FeedFormat.RSS.toLowerCase() }, { format: FeedFormat.ATOM, label: 'atom 1.0', - url: VideoService.BASE_FEEDS_URL + FeedFormat.ATOM.toLowerCase() + url: base + FeedFormat.ATOM.toLowerCase() }, { format: FeedFormat.JSON, label: 'json 1.0', - url: VideoService.BASE_FEEDS_URL + FeedFormat.JSON.toLowerCase() + url: base + FeedFormat.JSON.toLowerCase() } ] @@ -266,10 +241,10 @@ export class VideoService implements VideosProvider { return feeds } - getVideoFeedUrls (sort: VideoSortField, filter?: VideoFilter, categoryOneOf?: number[]) { + getVideoFeedUrls (sort: VideoSortField, isLocal: boolean, categoryOneOf?: number[]) { let params = this.restService.addRestGetParams(new HttpParams(), undefined, sort) - if (filter) params = params.set('filter', filter) + if (isLocal) params = params.set('isLocal', isLocal) if (categoryOneOf) { for (const c of categoryOneOf) { @@ -294,14 +269,12 @@ export class VideoService implements VideosProvider { return this.buildBaseFeedUrls(params) } - async getVideoSubscriptionFeedUrls (accountId: number) { + getVideoSubscriptionFeedUrls (accountId: number, feedToken: string) { let params = this.restService.addRestGetParams(new HttpParams()) params = params.set('accountId', accountId.toString()) - - const { feedToken } = await this.authService.getScopedTokens() params = params.set('token', feedToken) - return this.buildBaseFeedUrls(params) + return this.buildBaseFeedUrls(params, VideoService.BASE_SUBSCRIPTION_FEEDS_URL) } getVideoFileMetadata (metadataUrl: string) { @@ -312,13 +285,40 @@ export class VideoService implements VideosProvider { ) } - removeVideo (id: number) { - return this.authHttp - .delete(VideoService.BASE_VIDEO_URL + id) - .pipe( - map(this.restExtractor.extractDataBool), - catchError(err => this.restExtractor.handleError(err)) - ) + removeVideo (idArg: number | number[]) { + const ids = arrayify(idArg) + + return from(ids) + .pipe( + concatMap(id => this.authHttp.delete(`${VideoService.BASE_VIDEO_URL}/${id}`)), + toArray(), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + removeVideoFiles (videoIds: (number | string)[], type: 'hls' | 'webtorrent') { + return from(videoIds) + .pipe( + concatMap(id => this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + id + '/' + type)), + toArray(), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + removeFile (videoId: number | string, fileId: number, type: 'hls' | 'webtorrent') { + return this.authHttp.delete(VideoService.BASE_VIDEO_URL + '/' + videoId + '/' + type + '/' + fileId) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + runTranscoding (videoIds: (number | string)[], type: 'hls' | 'webtorrent') { + const body: VideoTranscodingCreate = { transcodingType: type } + + return from(videoIds) + .pipe( + concatMap(id => this.authHttp.post(VideoService.BASE_VIDEO_URL + '/' + id + '/transcoding', body)), + toArray(), + catchError(err => this.restExtractor.handleError(err)) + ) } loadCompleteDescription (descriptionPath: string) { @@ -330,19 +330,33 @@ export class VideoService implements VideosProvider { ) } - setVideoLike (id: number) { + getSource (videoId: number) { + return this.authHttp + .get<{ source: VideoSource }>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/source') + .pipe( + catchError(err => { + if (err.status === 404) { + return of(undefined) + } + + this.restExtractor.handleError(err) + }) + ) + } + + setVideoLike (id: string) { return this.setVideoRate(id, 'like') } - setVideoDislike (id: number) { + setVideoDislike (id: string) { return this.setVideoRate(id, 'dislike') } - unsetVideoLike (id: number) { + unsetVideoLike (id: string) { return this.setVideoRate(id, 'none') } - getUserVideoRating (id: number) { + getUserVideoRating (id: string) { const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating' return this.authHttp.get(url) @@ -366,29 +380,38 @@ export class VideoService implements VideosProvider { ) } - explainedPrivacyLabels (privacies: VideoConstant[]) { - const base = [ - { - id: VideoPrivacy.PRIVATE, - description: $localize`Only I can see this video` - }, - { - id: VideoPrivacy.UNLISTED, - description: $localize`Only shareable via a private link` - }, - { - id: VideoPrivacy.PUBLIC, - description: $localize`Anyone can see this video` - }, - { - id: VideoPrivacy.INTERNAL, - description: $localize`Only users of this instance can see this video` + explainedPrivacyLabels (serverPrivacies: VideoConstant[], defaultPrivacyId = VideoPrivacy.PUBLIC) { + const descriptions = { + [VideoPrivacy.PRIVATE]: $localize`Only I can see this video`, + [VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`, + [VideoPrivacy.PUBLIC]: $localize`Anyone can see this video`, + [VideoPrivacy.INTERNAL]: $localize`Only users of this instance can see this video` + } + + const videoPrivacies = serverPrivacies.map(p => { + return { + ...p, + + description: descriptions[p.id] } - ] + }) - return base - .filter(o => !!privacies.find(p => p.id === o.id)) // filter down to privacies that where in the input - .map(o => ({ ...privacies[o.id - 1], ...o })) // merge the input privacies that contain a label, and extend them with a description + return { + videoPrivacies, + defaultPrivacyId: serverPrivacies.find(p => p.id === defaultPrivacyId)?.id || serverPrivacies[0].id + } + } + + getHighestAvailablePrivacy (serverPrivacies: VideoConstant[]) { + const order = [ VideoPrivacy.PRIVATE, VideoPrivacy.INTERNAL, VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC ] + + for (const privacy of order) { + if (serverPrivacies.find(p => p.id === privacy)) { + return privacy + } + } + + throw new Error('No highest privacy available') } nsfwPolicyToParam (nsfwPolicy: NSFWPolicyType) { @@ -397,17 +420,66 @@ export class VideoService implements VideosProvider { : 'both' } - private setVideoRate (id: number, rateType: UserVideoRateType) { - const url = VideoService.BASE_VIDEO_URL + id + '/rate' + buildCommonVideosParams (options: CommonVideoParams & { params: HttpParams }) { + const { + params, + videoPagination, + sort, + isLocal, + include, + categoryOneOf, + languageOneOf, + privacyOneOf, + skipCount, + nsfwPolicy, + isLive, + nsfw + } = options + + const pagination = videoPagination + ? this.restService.componentToRestPagination(videoPagination) + : undefined + + let newParams = this.restService.addRestGetParams(params, pagination, this.buildListSort(sort)) + + if (skipCount) newParams = newParams.set('skipCount', skipCount + '') + + if (isLocal !== undefined) newParams = newParams.set('isLocal', isLocal) + if (include !== undefined) newParams = newParams.set('include', include) + if (isLive !== undefined) newParams = newParams.set('isLive', isLive) + if (nsfw !== undefined) newParams = newParams.set('nsfw', nsfw) + if (nsfwPolicy !== undefined) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) + if (languageOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'languageOneOf', languageOneOf) + if (categoryOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'categoryOneOf', categoryOneOf) + if (privacyOneOf !== undefined) newParams = this.restService.addArrayParams(newParams, 'privacyOneOf', privacyOneOf) + + return newParams + } + + private buildListSort (sortArg: VideoSortField | SortMeta) { + const sort = this.restService.buildSortString(sortArg) + + if (typeof sort === 'string') { + // Silently use the best algorithm for logged in users if they chose the hot algorithm + if ( + this.auth.isLoggedIn() && + (sort === 'hot' || sort === '-hot') + ) { + return sort.replace('hot', 'best') + } + + return sort + } + } + + private setVideoRate (id: string, 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)) - ) + .pipe(catchError(err => this.restExtractor.handleError(err))) } }