-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 } from '@app/core'
+import { AuthService, ComponentPaginationLight, RestExtractor, RestService, ServerService, UserService } from '@app/core'
import { objectToFormData } from '@app/helpers'
import {
+ BooleanBothQuery,
FeedFormat,
NSFWPolicyType,
ResultList,
UserVideoRateType,
UserVideoRateUpdate,
Video as VideoServerModel,
+ VideoChannel as VideoChannelServerModel,
VideoConstant,
VideoDetails as VideoDetailsServerModel,
VideoFileMetadata,
- VideoFilter,
+ VideoInclude,
VideoPrivacy,
VideoSortField,
+ 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'
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<ResultList<Video>>
+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,
) {}
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<VideoDetails> {
return this.serverService.getServerLocale()
.pipe(
switchMap(translations => {
- return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + options.videoId)
+ return this.authHttp.get<VideoDetailsServerModel>(`${VideoService.BASE_VIDEO_URL}/${options.videoId}`)
.pipe(map(videoHash => ({ videoHash, translations })))
}),
map(({ videoHash, translations }) => new VideoDetails(videoHash, translations)),
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<ResultList<Video>> {
- const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
+ getMyVideos (options: {
+ videoPagination: ComponentPaginationLight
+ sort: VideoSortField
+ userChannels?: VideoChannelServerModel[]
+ search?: string
+ }): Observable<ResultList<Video>> {
+ const { videoPagination, sort, userChannels = [], search } = options
+
+ const pagination = this.restService.componentToRestPagination(videoPagination)
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
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
+ }
}
})
)
}
- getAccountVideos (parameters: {
- account: Account,
- videoPagination: ComponentPaginationLight,
- sort: VideoSortField
- nsfwPolicy?: NSFWPolicyType
- videoFilter?: VideoFilter
+ getAccountVideos (parameters: CommonVideoParams & {
+ account: Pick<Account, 'nameWithHost'>
search?: string
}): Observable<ResultList<Video>> {
- const { account, videoPagination, sort, videoFilter, nsfwPolicy, search } = parameters
-
- const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
+ const { account, search } = parameters
let params = new HttpParams()
- params = this.restService.addRestGetParams(params, pagination, sort)
+ params = this.buildCommonVideosParams({ params, ...parameters })
- if (nsfwPolicy) {
- params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
- }
-
- if (videoFilter) {
- params = params.set('filter', videoFilter)
- }
-
- if (search) {
- params = params.set('search', search)
- }
+ if (search) params = params.set('search', search)
return this.authHttp
.get<ResultList<Video>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/videos', { params })
)
}
- getVideoChannelVideos (parameters: {
- videoChannel: VideoChannel,
- videoPagination: ComponentPaginationLight,
- sort: VideoSortField,
- nsfwPolicy?: NSFWPolicyType
- videoFilter?: VideoFilter
+ getVideoChannelVideos (parameters: CommonVideoParams & {
+ videoChannel: Pick<VideoChannel, 'nameWithHost'>
}): Observable<ResultList<Video>> {
- 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<ResultList<Video>>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/videos', { params })
)
}
- getVideos (parameters: {
- videoPagination: ComponentPaginationLight,
- sort: VideoSortField,
- filter?: VideoFilter,
- categoryOneOf?: number[],
- languageOneOf?: string[],
- skipCount?: boolean,
- nsfwPolicy?: NSFWPolicyType
- }): Observable<ResultList<Video>> {
- const { videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfwPolicy } = parameters
-
- const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
-
+ getVideos (parameters: CommonVideoParams): Observable<ResultList<Video>> {
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<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
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) {
)
}
- 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 = Array.isArray(idArg) ? idArg : [ 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))
+ )
+ }
+
+ 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) {
)
}
- 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<UserVideoRate>(url)
)
}
- explainedPrivacyLabels (privacies: VideoConstant<VideoPrivacy>[], defaultPrivacyId = VideoPrivacy.PUBLIC) {
- 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<VideoPrivacy>[], 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,
- const videoPrivacies = 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
+ description: descriptions[p.id]
+ }
+ })
return {
- defaultPrivacyId: videoPrivacies.find(p => p.id === defaultPrivacyId)?.id || videoPrivacies[0].id,
- videoPrivacies
+ videoPrivacies,
+ defaultPrivacyId: serverPrivacies.find(p => p.id === defaultPrivacyId)?.id || serverPrivacies[0].id
}
}
+ getHighestAvailablePrivacy (serverPrivacies: VideoConstant<VideoPrivacy>[]) {
+ 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) {
return nsfwPolicy === 'do_not_list'
? 'false'
: '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)))
}
}