]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - client/src/app/shared/shared-main/video/video.service.ts
Add Podcast RSS feeds (#5487)
[github/Chocobozzz/PeerTube.git] / client / src / app / shared / shared-main / video / video.service.ts
index 04a39be0e7976ad904172c7477e11ba79eb08b4b..78a49567f91746d77845efa035a2f5753ca34404 100644 (file)
@@ -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 } 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,14 +15,17 @@ import {
   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'
@@ -28,24 +34,31 @@ 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<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 PODCAST_FEEDS_URL = environment.apiUrl + '/feeds/podcast/videos.xml'
   static BASE_SUBSCRIPTION_FEEDS_URL = environment.apiUrl + '/feeds/subscriptions.'
 
   constructor (
+    private auth: AuthService,
     private authHttp: HttpClient,
     private restExtractor: RestExtractor,
     private restService: RestService,
@@ -53,18 +66,14 @@ export class VideoService implements VideosProvider {
   ) {}
 
   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)),
@@ -104,23 +113,27 @@ 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<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)
@@ -130,6 +143,16 @@ export class VideoService implements VideosProvider {
         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
+          }
         }
       })
 
@@ -144,32 +167,16 @@ export class VideoService implements VideosProvider {
                )
   }
 
-  getAccountVideos (parameters: {
-    account: Pick<Account, 'nameWithHost'>,
-    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)
-
-    if (nsfwPolicy) {
-      params = params.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
-    }
-
-    if (videoFilter) {
-      params = params.set('filter', videoFilter)
-    }
+    params = this.buildCommonVideosParams({ params, ...parameters })
 
-    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 })
@@ -179,27 +186,13 @@ export class VideoService implements VideosProvider {
                )
   }
 
-  getVideoChannelVideos (parameters: {
-    videoChannel: Pick<VideoChannel, 'nameWithHost'>,
-    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 })
@@ -209,40 +202,9 @@ export class VideoService implements VideosProvider {
                )
   }
 
-  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 })
@@ -280,10 +242,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) {
@@ -305,7 +267,15 @@ export class VideoService implements VideosProvider {
     let params = this.restService.addRestGetParams(new HttpParams())
     params = params.set('videoChannelId', videoChannelId.toString())
 
-    return this.buildBaseFeedUrls(params)
+    const feedUrls = this.buildBaseFeedUrls(params)
+
+    feedUrls.push({
+      format: FeedFormat.RSS,
+      label: 'podcast rss 2.0',
+      url: VideoService.PODCAST_FEEDS_URL + `?videoChannelId=${videoChannelId}`
+    })
+
+    return feedUrls
   }
 
   getVideoSubscriptionFeedUrls (accountId: number, feedToken: string) {
@@ -324,13 +294,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) {
@@ -342,19 +339,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<UserVideoRate>(url)
@@ -418,17 +429,84 @@ export class VideoService implements VideosProvider {
       : 'both'
   }
 
-  private setVideoRate (id: number, rateType: UserVideoRateType) {
-    const url = VideoService.BASE_VIDEO_URL + id + '/rate'
+  // Choose if we display by default the account or the channel
+  buildDefaultOwnerDisplayType (video: Video) {
+    const accountName = video.account.name
+
+    // If the video channel name is an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
+    // Or has not been customized (default created channel display name)
+    // -> Use the account name
+    if (
+      video.channel.displayName === `Default ${accountName} channel` ||
+      video.channel.displayName === `Main ${accountName} channel` ||
+      video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
+    ) {
+      return 'account' as 'account'
+    }
+
+    return 'videoChannel' as 'videoChannel'
+  }
+
+  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)))
   }
 }