import { Observable, of, Subject } from 'rxjs' import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators' import { HttpClient } from '@angular/common/http' import { Inject, Injectable, LOCALE_ID } from '@angular/core' import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers' import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models' import { environment } from '../../../environments/environment' @Injectable() export class ServerService { private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' private static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/' private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats' configReloaded = new Subject() private localeObservable: Observable private videoLicensesObservable: Observable[]> private videoCategoriesObservable: Observable[]> private videoPrivaciesObservable: Observable[]> private videoPlaylistPrivaciesObservable: Observable[]> private videoLanguagesObservable: Observable[]> private configObservable: Observable private configReset = false private configLoaded = false private config: ServerConfig = { instance: { name: 'PeerTube', shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' + 'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.', isNSFW: false, defaultNSFWPolicy: 'do_not_list' as 'do_not_list', defaultClientRoute: '', customizations: { javascript: '', css: '' } }, plugin: { registered: [], registeredExternalAuths: [], registeredIdAndPassAuths: [] }, theme: { registered: [], default: 'default' }, email: { enabled: false }, contactForm: { enabled: false }, serverVersion: 'Unknown', signup: { allowed: false, allowedForCurrentIP: false, requiresEmailVerification: false, minimumAge: 16 }, transcoding: { profile: 'default', availableProfiles: [ 'default' ], enabledResolutions: [], hls: { enabled: false }, webtorrent: { enabled: true } }, live: { enabled: false, allowReplay: true, maxDuration: null, maxInstanceLives: -1, maxUserLives: -1, transcoding: { enabled: false, profile: 'default', availableProfiles: [ 'default' ], enabledResolutions: [] }, rtmp: { port: 1935 } }, avatar: { file: { size: { max: 0 }, extensions: [] } }, banner: { file: { size: { max: 0 }, extensions: [] } }, video: { image: { size: { max: 0 }, extensions: [] }, file: { extensions: [] } }, videoCaption: { file: { size: { max: 0 }, extensions: [] } }, user: { videoQuota: -1, videoQuotaDaily: -1 }, import: { videos: { http: { enabled: false }, torrent: { enabled: false } } }, trending: { videos: { intervalDays: 0, algorithms: { enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ], default: 'most-viewed' } } }, autoBlacklist: { videos: { ofUsers: { enabled: false } } }, tracker: { enabled: true }, followings: { instance: { autoFollowIndex: { indexUrl: 'https://instances.joinpeertube.org' } } }, broadcastMessage: { enabled: false, message: '', level: 'info', dismissable: false }, search: { remoteUri: { users: true, anonymous: false }, searchIndex: { enabled: false, url: '', disableLocalSearch: false, isDefaultSearch: false } }, homepage: { enabled: false } } constructor ( private http: HttpClient, @Inject(LOCALE_ID) private localeId: string ) { this.loadConfigLocally() } getServerVersionAndCommit () { const serverVersion = this.config.serverVersion const commit = this.config.serverCommit || '' let result = serverVersion if (commit) result += '...' + commit return result } resetConfig () { this.configLoaded = false this.configReset = true // Notify config update return this.getConfig() } getConfig () { if (this.configLoaded) return of(this.config) if (!this.configObservable) { this.configObservable = this.http.get(ServerService.BASE_CONFIG_URL) .pipe( tap(config => { this.config = config this.configLoaded = true }), tap(config => { if (this.configReset) { this.configReloaded.next(config) this.configReset = false } }), share() ) } return this.configObservable } getTmpConfig () { return this.config } getVideoCategories () { if (!this.videoCategoriesObservable) { this.videoCategoriesObservable = this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'categories', true) } return this.videoCategoriesObservable.pipe(first()) } getVideoLicences () { if (!this.videoLicensesObservable) { this.videoLicensesObservable = this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'licences') } return this.videoLicensesObservable.pipe(first()) } getVideoLanguages () { if (!this.videoLanguagesObservable) { this.videoLanguagesObservable = this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'languages', true) } return this.videoLanguagesObservable.pipe(first()) } getVideoPrivacies () { if (!this.videoPrivaciesObservable) { this.videoPrivaciesObservable = this.loadAttributeEnum(ServerService.BASE_VIDEO_URL, 'privacies') } return this.videoPrivaciesObservable.pipe(first()) } getVideoPlaylistPrivacies () { if (!this.videoPlaylistPrivaciesObservable) { this.videoPlaylistPrivaciesObservable = this.loadAttributeEnum(ServerService.BASE_VIDEO_PLAYLIST_URL, 'privacies') } return this.videoPlaylistPrivaciesObservable.pipe(first()) } getServerLocale () { if (!this.localeObservable) { const completeLocale = isOnDevLocale() ? getDevLocale() : getCompleteLocale(this.localeId) // Default locale, nothing to translate if (isDefaultLocale(completeLocale)) { this.localeObservable = of({}).pipe(shareReplay()) } else { this.localeObservable = this.http .get(ServerService.BASE_LOCALE_URL + completeLocale + '/server.json') .pipe(shareReplay()) } } return this.localeObservable.pipe(first()) } getServerStats () { return this.http.get(ServerService.BASE_STATS_URL) } getDefaultSearchTarget (): Promise { return this.getConfig().pipe( map(config => { const searchIndexConfig = config.search.searchIndex if (searchIndexConfig.enabled && (searchIndexConfig.isDefaultSearch || searchIndexConfig.disableLocalSearch)) { return 'search-index' } return 'local' }) ).toPromise() } private loadAttributeEnum ( baseUrl: string, attributeName: 'categories' | 'licences' | 'languages' | 'privacies', sort = false ) { return this.getServerLocale() .pipe( switchMap(translations => { return this.http.get<{ [ id: string ]: string }>(baseUrl + attributeName) .pipe(map(data => ({ data, translations }))) }), map(({ data, translations }) => { const hashToPopulate: VideoConstant[] = Object.keys(data) .map(dataKey => { const label = data[ dataKey ] const id = attributeName === 'languages' ? dataKey as T : parseInt(dataKey, 10) as T return { id, label: peertubeTranslate(label, translations) } }) if (sort === true) sortBy(hashToPopulate, 'label') return hashToPopulate }), shareReplay() ) } private loadConfigLocally () { const configString = window['PeerTubeServerConfig'] if (!configString) return try { const parsed = JSON.parse(configString) Object.assign(this.config, parsed) } catch (err) { console.error('Cannot parse config saved in from index.html.', err) } } }