diff options
author | Chocobozzz <me@florianbigard.com> | 2020-06-23 14:10:17 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-06-23 16:00:49 +0200 |
commit | 67ed6552b831df66713bac9e672738796128d33f (patch) | |
tree | 59c97d41e0b49d75a90aa3de987968ab9b1ff447 /client/src/app/shared/shared-video-playlist/video-playlist.service.ts | |
parent | 0c4bacbff53bc732f5a2677d62a6ead7752e2405 (diff) | |
download | PeerTube-67ed6552b831df66713bac9e672738796128d33f.tar.gz PeerTube-67ed6552b831df66713bac9e672738796128d33f.tar.zst PeerTube-67ed6552b831df66713bac9e672738796128d33f.zip |
Reorganize client shared modules
Diffstat (limited to 'client/src/app/shared/shared-video-playlist/video-playlist.service.ts')
-rw-r--r-- | client/src/app/shared/shared-video-playlist/video-playlist.service.ts | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts new file mode 100644 index 000000000..cc3d04b9e --- /dev/null +++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts | |||
@@ -0,0 +1,355 @@ | |||
1 | import * as debug from 'debug' | ||
2 | import { uniq } from 'lodash-es' | ||
3 | import { asyncScheduler, merge, Observable, of, ReplaySubject, Subject } from 'rxjs' | ||
4 | import { bufferTime, catchError, filter, map, observeOn, share, switchMap, tap } from 'rxjs/operators' | ||
5 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
6 | import { Injectable, NgZone } from '@angular/core' | ||
7 | import { AuthUser, ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core' | ||
8 | import { enterZone, leaveZone, objectToFormData } from '@app/helpers' | ||
9 | import { Account, AccountService, VideoChannel, VideoChannelService } from '@app/shared/shared-main' | ||
10 | import { | ||
11 | ResultList, | ||
12 | VideoExistInPlaylist, | ||
13 | VideoPlaylist as VideoPlaylistServerModel, | ||
14 | VideoPlaylistCreate, | ||
15 | VideoPlaylistElement as ServerVideoPlaylistElement, | ||
16 | VideoPlaylistElementCreate, | ||
17 | VideoPlaylistElementUpdate, | ||
18 | VideoPlaylistReorder, | ||
19 | VideoPlaylistUpdate, | ||
20 | VideosExistInPlaylists | ||
21 | } from '@shared/models' | ||
22 | import { environment } from '../../../environments/environment' | ||
23 | import { VideoPlaylistElement } from './video-playlist-element.model' | ||
24 | import { VideoPlaylist } from './video-playlist.model' | ||
25 | |||
26 | const logger = debug('peertube:playlists:VideoPlaylistService') | ||
27 | |||
28 | export type CachedPlaylist = VideoPlaylist | { id: number, displayName: string } | ||
29 | |||
30 | @Injectable() | ||
31 | export class VideoPlaylistService { | ||
32 | static BASE_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/video-playlists/' | ||
33 | static MY_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/users/me/video-playlists/' | ||
34 | |||
35 | // Use a replay subject because we "next" a value before subscribing | ||
36 | private videoExistsInPlaylistNotifier = new ReplaySubject<number>(1) | ||
37 | private videoExistsInPlaylistCacheSubject = new Subject<VideosExistInPlaylists>() | ||
38 | private readonly videoExistsInPlaylistObservable: Observable<VideosExistInPlaylists> | ||
39 | |||
40 | private videoExistsObservableCache: { [ id: number ]: Observable<VideoExistInPlaylist[]> } = {} | ||
41 | private videoExistsCache: { [ id: number ]: VideoExistInPlaylist[] } = {} | ||
42 | |||
43 | private myAccountPlaylistCache: ResultList<CachedPlaylist> = undefined | ||
44 | private myAccountPlaylistCacheRunning: Observable<ResultList<CachedPlaylist>> | ||
45 | private myAccountPlaylistCacheSubject = new Subject<ResultList<CachedPlaylist>>() | ||
46 | |||
47 | constructor ( | ||
48 | private authHttp: HttpClient, | ||
49 | private serverService: ServerService, | ||
50 | private restExtractor: RestExtractor, | ||
51 | private restService: RestService, | ||
52 | private ngZone: NgZone | ||
53 | ) { | ||
54 | this.videoExistsInPlaylistObservable = merge( | ||
55 | this.videoExistsInPlaylistNotifier.pipe( | ||
56 | // We leave Angular zone so Protractor does not get stuck | ||
57 | bufferTime(500, leaveZone(this.ngZone, asyncScheduler)), | ||
58 | filter(videoIds => videoIds.length !== 0), | ||
59 | map(videoIds => uniq(videoIds)), | ||
60 | observeOn(enterZone(this.ngZone, asyncScheduler)), | ||
61 | switchMap(videoIds => this.doVideosExistInPlaylist(videoIds)), | ||
62 | share() | ||
63 | ), | ||
64 | |||
65 | this.videoExistsInPlaylistCacheSubject | ||
66 | ) | ||
67 | } | ||
68 | |||
69 | listChannelPlaylists (videoChannel: VideoChannel, componentPagination: ComponentPaginationLight): Observable<ResultList<VideoPlaylist>> { | ||
70 | const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost + '/video-playlists' | ||
71 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
72 | |||
73 | let params = new HttpParams() | ||
74 | params = this.restService.addRestGetParams(params, pagination) | ||
75 | |||
76 | return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params }) | ||
77 | .pipe( | ||
78 | switchMap(res => this.extractPlaylists(res)), | ||
79 | catchError(err => this.restExtractor.handleError(err)) | ||
80 | ) | ||
81 | } | ||
82 | |||
83 | listMyPlaylistWithCache (user: AuthUser, search?: string) { | ||
84 | if (!search) { | ||
85 | if (this.myAccountPlaylistCacheRunning) return this.myAccountPlaylistCacheRunning | ||
86 | if (this.myAccountPlaylistCache) return of(this.myAccountPlaylistCache) | ||
87 | } | ||
88 | |||
89 | const obs = this.listAccountPlaylists(user.account, undefined, '-updatedAt', search) | ||
90 | .pipe( | ||
91 | tap(result => { | ||
92 | if (!search) { | ||
93 | this.myAccountPlaylistCacheRunning = undefined | ||
94 | this.myAccountPlaylistCache = result | ||
95 | } | ||
96 | }), | ||
97 | share() | ||
98 | ) | ||
99 | |||
100 | if (!search) this.myAccountPlaylistCacheRunning = obs | ||
101 | return obs | ||
102 | } | ||
103 | |||
104 | listAccountPlaylists ( | ||
105 | account: Account, | ||
106 | componentPagination: ComponentPaginationLight, | ||
107 | sort: string, | ||
108 | search?: string | ||
109 | ): Observable<ResultList<VideoPlaylist>> { | ||
110 | const url = AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-playlists' | ||
111 | const pagination = componentPagination | ||
112 | ? this.restService.componentPaginationToRestPagination(componentPagination) | ||
113 | : undefined | ||
114 | |||
115 | let params = new HttpParams() | ||
116 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
117 | if (search) params = this.restService.addObjectParams(params, { search }) | ||
118 | |||
119 | return this.authHttp.get<ResultList<VideoPlaylist>>(url, { params }) | ||
120 | .pipe( | ||
121 | switchMap(res => this.extractPlaylists(res)), | ||
122 | catchError(err => this.restExtractor.handleError(err)) | ||
123 | ) | ||
124 | } | ||
125 | |||
126 | getVideoPlaylist (id: string | number) { | ||
127 | const url = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + id | ||
128 | |||
129 | return this.authHttp.get<VideoPlaylist>(url) | ||
130 | .pipe( | ||
131 | switchMap(res => this.extractPlaylist(res)), | ||
132 | catchError(err => this.restExtractor.handleError(err)) | ||
133 | ) | ||
134 | } | ||
135 | |||
136 | createVideoPlaylist (body: VideoPlaylistCreate) { | ||
137 | const data = objectToFormData(body) | ||
138 | |||
139 | return this.authHttp.post<{ videoPlaylist: { id: number } }>(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL, data) | ||
140 | .pipe( | ||
141 | tap(res => { | ||
142 | if (!this.myAccountPlaylistCache) return | ||
143 | |||
144 | this.myAccountPlaylistCache.total++ | ||
145 | |||
146 | this.myAccountPlaylistCache.data.push({ | ||
147 | id: res.videoPlaylist.id, | ||
148 | displayName: body.displayName | ||
149 | }) | ||
150 | |||
151 | this.myAccountPlaylistCacheSubject.next(this.myAccountPlaylistCache) | ||
152 | }), | ||
153 | catchError(err => this.restExtractor.handleError(err)) | ||
154 | ) | ||
155 | } | ||
156 | |||
157 | updateVideoPlaylist (videoPlaylist: VideoPlaylist, body: VideoPlaylistUpdate) { | ||
158 | const data = objectToFormData(body) | ||
159 | |||
160 | return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id, data) | ||
161 | .pipe( | ||
162 | map(this.restExtractor.extractDataBool), | ||
163 | tap(() => { | ||
164 | if (!this.myAccountPlaylistCache) return | ||
165 | |||
166 | const playlist = this.myAccountPlaylistCache.data.find(p => p.id === videoPlaylist.id) | ||
167 | playlist.displayName = body.displayName | ||
168 | |||
169 | this.myAccountPlaylistCacheSubject.next(this.myAccountPlaylistCache) | ||
170 | }), | ||
171 | catchError(err => this.restExtractor.handleError(err)) | ||
172 | ) | ||
173 | } | ||
174 | |||
175 | removeVideoPlaylist (videoPlaylist: VideoPlaylist) { | ||
176 | return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id) | ||
177 | .pipe( | ||
178 | map(this.restExtractor.extractDataBool), | ||
179 | tap(() => { | ||
180 | if (!this.myAccountPlaylistCache) return | ||
181 | |||
182 | this.myAccountPlaylistCache.total-- | ||
183 | this.myAccountPlaylistCache.data = this.myAccountPlaylistCache.data | ||
184 | .filter(p => p.id !== videoPlaylist.id) | ||
185 | |||
186 | this.myAccountPlaylistCacheSubject.next(this.myAccountPlaylistCache) | ||
187 | }), | ||
188 | catchError(err => this.restExtractor.handleError(err)) | ||
189 | ) | ||
190 | } | ||
191 | |||
192 | addVideoInPlaylist (playlistId: number, body: VideoPlaylistElementCreate) { | ||
193 | const url = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos' | ||
194 | |||
195 | return this.authHttp.post<{ videoPlaylistElement: { id: number } }>(url, body) | ||
196 | .pipe( | ||
197 | tap(res => { | ||
198 | const existsResult = this.videoExistsCache[body.videoId] | ||
199 | existsResult.push({ | ||
200 | playlistId, | ||
201 | playlistElementId: res.videoPlaylistElement.id, | ||
202 | startTimestamp: body.startTimestamp, | ||
203 | stopTimestamp: body.stopTimestamp | ||
204 | }) | ||
205 | |||
206 | this.runPlaylistCheck(body.videoId) | ||
207 | }), | ||
208 | catchError(err => this.restExtractor.handleError(err)) | ||
209 | ) | ||
210 | } | ||
211 | |||
212 | updateVideoOfPlaylist (playlistId: number, playlistElementId: number, body: VideoPlaylistElementUpdate, videoId: number) { | ||
213 | return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId, body) | ||
214 | .pipe( | ||
215 | map(this.restExtractor.extractDataBool), | ||
216 | tap(() => { | ||
217 | const existsResult = this.videoExistsCache[videoId] | ||
218 | const elem = existsResult.find(e => e.playlistElementId === playlistElementId) | ||
219 | |||
220 | elem.startTimestamp = body.startTimestamp | ||
221 | elem.stopTimestamp = body.stopTimestamp | ||
222 | |||
223 | this.runPlaylistCheck(videoId) | ||
224 | }), | ||
225 | catchError(err => this.restExtractor.handleError(err)) | ||
226 | ) | ||
227 | } | ||
228 | |||
229 | removeVideoFromPlaylist (playlistId: number, playlistElementId: number, videoId?: number) { | ||
230 | return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId) | ||
231 | .pipe( | ||
232 | map(this.restExtractor.extractDataBool), | ||
233 | tap(() => { | ||
234 | if (!videoId) return | ||
235 | |||
236 | this.videoExistsCache[videoId] = this.videoExistsCache[videoId].filter(e => e.playlistElementId !== playlistElementId) | ||
237 | this.runPlaylistCheck(videoId) | ||
238 | }), | ||
239 | catchError(err => this.restExtractor.handleError(err)) | ||
240 | ) | ||
241 | } | ||
242 | |||
243 | reorderPlaylist (playlistId: number, oldPosition: number, newPosition: number) { | ||
244 | const body: VideoPlaylistReorder = { | ||
245 | startPosition: oldPosition, | ||
246 | insertAfterPosition: newPosition | ||
247 | } | ||
248 | |||
249 | return this.authHttp.post(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/reorder', body) | ||
250 | .pipe( | ||
251 | map(this.restExtractor.extractDataBool), | ||
252 | catchError(err => this.restExtractor.handleError(err)) | ||
253 | ) | ||
254 | } | ||
255 | |||
256 | getPlaylistVideos ( | ||
257 | videoPlaylistId: number | string, | ||
258 | componentPagination: ComponentPaginationLight | ||
259 | ): Observable<ResultList<VideoPlaylistElement>> { | ||
260 | const path = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylistId + '/videos' | ||
261 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
262 | |||
263 | let params = new HttpParams() | ||
264 | params = this.restService.addRestGetParams(params, pagination) | ||
265 | |||
266 | return this.authHttp | ||
267 | .get<ResultList<ServerVideoPlaylistElement>>(path, { params }) | ||
268 | .pipe( | ||
269 | switchMap(res => this.extractVideoPlaylistElements(res)), | ||
270 | catchError(err => this.restExtractor.handleError(err)) | ||
271 | ) | ||
272 | } | ||
273 | |||
274 | listenToMyAccountPlaylistsChange () { | ||
275 | return this.myAccountPlaylistCacheSubject.asObservable() | ||
276 | } | ||
277 | |||
278 | listenToVideoPlaylistChange (videoId: number) { | ||
279 | if (this.videoExistsObservableCache[ videoId ]) { | ||
280 | return this.videoExistsObservableCache[ videoId ] | ||
281 | } | ||
282 | |||
283 | const obs = this.videoExistsInPlaylistObservable | ||
284 | .pipe( | ||
285 | map(existsResult => existsResult[ videoId ]), | ||
286 | filter(r => !!r), | ||
287 | tap(result => this.videoExistsCache[ videoId ] = result) | ||
288 | ) | ||
289 | |||
290 | this.videoExistsObservableCache[ videoId ] = obs | ||
291 | return obs | ||
292 | } | ||
293 | |||
294 | runPlaylistCheck (videoId: number) { | ||
295 | logger('Running playlist check.') | ||
296 | |||
297 | if (this.videoExistsCache[videoId]) { | ||
298 | logger('Found cache for %d.', videoId) | ||
299 | |||
300 | return this.videoExistsInPlaylistCacheSubject.next({ [videoId]: this.videoExistsCache[videoId] }) | ||
301 | } | ||
302 | |||
303 | logger('Fetching from network for %d.', videoId) | ||
304 | return this.videoExistsInPlaylistNotifier.next(videoId) | ||
305 | } | ||
306 | |||
307 | extractPlaylists (result: ResultList<VideoPlaylistServerModel>) { | ||
308 | return this.serverService.getServerLocale() | ||
309 | .pipe( | ||
310 | map(translations => { | ||
311 | const playlistsJSON = result.data | ||
312 | const total = result.total | ||
313 | const playlists: VideoPlaylist[] = [] | ||
314 | |||
315 | for (const playlistJSON of playlistsJSON) { | ||
316 | playlists.push(new VideoPlaylist(playlistJSON, translations)) | ||
317 | } | ||
318 | |||
319 | return { data: playlists, total } | ||
320 | }) | ||
321 | ) | ||
322 | } | ||
323 | |||
324 | extractPlaylist (playlist: VideoPlaylistServerModel) { | ||
325 | return this.serverService.getServerLocale() | ||
326 | .pipe(map(translations => new VideoPlaylist(playlist, translations))) | ||
327 | } | ||
328 | |||
329 | extractVideoPlaylistElements (result: ResultList<ServerVideoPlaylistElement>) { | ||
330 | return this.serverService.getServerLocale() | ||
331 | .pipe( | ||
332 | map(translations => { | ||
333 | const elementsJson = result.data | ||
334 | const total = result.total | ||
335 | const elements: VideoPlaylistElement[] = [] | ||
336 | |||
337 | for (const elementJson of elementsJson) { | ||
338 | elements.push(new VideoPlaylistElement(elementJson, translations)) | ||
339 | } | ||
340 | |||
341 | return { total, data: elements } | ||
342 | }) | ||
343 | ) | ||
344 | } | ||
345 | |||
346 | private doVideosExistInPlaylist (videoIds: number[]): Observable<VideosExistInPlaylists> { | ||
347 | const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' | ||
348 | |||
349 | let params = new HttpParams() | ||
350 | params = this.restService.addObjectParams(params, { videoIds }) | ||
351 | |||
352 | return this.authHttp.get<VideoExistInPlaylist>(url, { params, headers: { ignoreLoadingBar: '' } }) | ||
353 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
354 | } | ||
355 | } | ||