diff options
Diffstat (limited to 'client/src/app/shared/video-playlist/video-playlist.service.ts')
-rw-r--r-- | client/src/app/shared/video-playlist/video-playlist.service.ts | 144 |
1 files changed, 125 insertions, 19 deletions
diff --git a/client/src/app/shared/video-playlist/video-playlist.service.ts b/client/src/app/shared/video-playlist/video-playlist.service.ts index d78fdc09f..c5b87fc11 100644 --- a/client/src/app/shared/video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/video-playlist/video-playlist.service.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { bufferTime, catchError, distinctUntilChanged, filter, first, map, share, switchMap } from 'rxjs/operators' | 1 | import { bufferTime, catchError, filter, map, share, switchMap, tap } from 'rxjs/operators' |
2 | import { Injectable } from '@angular/core' | 2 | import { Injectable } from '@angular/core' |
3 | import { Observable, ReplaySubject, Subject } from 'rxjs' | 3 | import { merge, Observable, of, ReplaySubject, Subject } from 'rxjs' |
4 | import { RestExtractor } from '../rest/rest-extractor.service' | 4 | import { RestExtractor } from '../rest/rest-extractor.service' |
5 | import { HttpClient, HttpParams } from '@angular/common/http' | 5 | import { HttpClient, HttpParams } from '@angular/common/http' |
6 | import { ResultList, VideoPlaylistElementCreate, VideoPlaylistElementUpdate } from '../../../../../shared' | 6 | import { ResultList, VideoPlaylistElementCreate, VideoPlaylistElementUpdate } from '../../../../../shared' |
@@ -11,16 +11,22 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | |||
11 | import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' | 11 | import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' |
12 | import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model' | 12 | import { VideoPlaylistUpdate } from '@shared/models/videos/playlist/video-playlist-update.model' |
13 | import { objectToFormData } from '@app/shared/misc/utils' | 13 | import { objectToFormData } from '@app/shared/misc/utils' |
14 | import { ServerService } from '@app/core' | 14 | import { AuthUser, ServerService } from '@app/core' |
15 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 15 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
16 | import { AccountService } from '@app/shared/account/account.service' | 16 | import { AccountService } from '@app/shared/account/account.service' |
17 | import { Account } from '@app/shared/account/account.model' | 17 | import { Account } from '@app/shared/account/account.model' |
18 | import { RestService } from '@app/shared/rest' | 18 | import { RestService } from '@app/shared/rest' |
19 | import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' | 19 | import { VideoExistInPlaylist, VideosExistInPlaylists } from '@shared/models/videos/playlist/video-exist-in-playlist.model' |
20 | import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model' | 20 | import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model' |
21 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 21 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
22 | import { VideoPlaylistElement as ServerVideoPlaylistElement } from '@shared/models/videos/playlist/video-playlist-element.model' | 22 | import { VideoPlaylistElement as ServerVideoPlaylistElement } from '@shared/models/videos/playlist/video-playlist-element.model' |
23 | import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' | 23 | import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' |
24 | import { uniq } from 'lodash-es' | ||
25 | import * as debug from 'debug' | ||
26 | |||
27 | const logger = debug('peertube:playlists:VideoPlaylistService') | ||
28 | |||
29 | type CachedPlaylist = VideoPlaylist | { id: number, displayName: string } | ||
24 | 30 | ||
25 | @Injectable() | 31 | @Injectable() |
26 | export class VideoPlaylistService { | 32 | export class VideoPlaylistService { |
@@ -28,8 +34,15 @@ export class VideoPlaylistService { | |||
28 | static MY_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/users/me/video-playlists/' | 34 | static MY_VIDEO_PLAYLIST_URL = environment.apiUrl + '/api/v1/users/me/video-playlists/' |
29 | 35 | ||
30 | // Use a replay subject because we "next" a value before subscribing | 36 | // Use a replay subject because we "next" a value before subscribing |
31 | private videoExistsInPlaylistSubject: Subject<number> = new ReplaySubject(1) | 37 | private videoExistsInPlaylistNotifier = new ReplaySubject<number>(1) |
32 | private readonly videoExistsInPlaylistObservable: Observable<VideoExistInPlaylist> | 38 | private videoExistsInPlaylistCacheSubject = new Subject<VideosExistInPlaylists>() |
39 | private readonly videoExistsInPlaylistObservable: Observable<VideosExistInPlaylists> | ||
40 | |||
41 | private videoExistsObservableCache: { [ id: number ]: Observable<VideoExistInPlaylist[]> } = {} | ||
42 | private videoExistsCache: { [ id: number ]: VideoExistInPlaylist[] } = {} | ||
43 | |||
44 | private myAccountPlaylistCache: ResultList<CachedPlaylist> = undefined | ||
45 | private myAccountPlaylistCacheSubject = new Subject<ResultList<CachedPlaylist>>() | ||
33 | 46 | ||
34 | constructor ( | 47 | constructor ( |
35 | private authHttp: HttpClient, | 48 | private authHttp: HttpClient, |
@@ -37,12 +50,16 @@ export class VideoPlaylistService { | |||
37 | private restExtractor: RestExtractor, | 50 | private restExtractor: RestExtractor, |
38 | private restService: RestService | 51 | private restService: RestService |
39 | ) { | 52 | ) { |
40 | this.videoExistsInPlaylistObservable = this.videoExistsInPlaylistSubject.pipe( | 53 | this.videoExistsInPlaylistObservable = merge( |
41 | distinctUntilChanged(), | 54 | this.videoExistsInPlaylistNotifier.pipe( |
42 | bufferTime(500), | 55 | bufferTime(500), |
43 | filter(videoIds => videoIds.length !== 0), | 56 | filter(videoIds => videoIds.length !== 0), |
44 | switchMap(videoIds => this.doVideosExistInPlaylist(videoIds)), | 57 | map(videoIds => uniq(videoIds)), |
45 | share() | 58 | switchMap(videoIds => this.doVideosExistInPlaylist(videoIds)), |
59 | share() | ||
60 | ), | ||
61 | |||
62 | this.videoExistsInPlaylistCacheSubject | ||
46 | ) | 63 | ) |
47 | } | 64 | } |
48 | 65 | ||
@@ -60,6 +77,17 @@ export class VideoPlaylistService { | |||
60 | ) | 77 | ) |
61 | } | 78 | } |
62 | 79 | ||
80 | listMyPlaylistWithCache (user: AuthUser, search?: string) { | ||
81 | if (!search && this.myAccountPlaylistCache) return of(this.myAccountPlaylistCache) | ||
82 | |||
83 | return this.listAccountPlaylists(user.account, undefined, '-updatedAt', search) | ||
84 | .pipe( | ||
85 | tap(result => { | ||
86 | if (!search) this.myAccountPlaylistCache = result | ||
87 | }) | ||
88 | ) | ||
89 | } | ||
90 | |||
63 | listAccountPlaylists ( | 91 | listAccountPlaylists ( |
64 | account: Account, | 92 | account: Account, |
65 | componentPagination: ComponentPagination, | 93 | componentPagination: ComponentPagination, |
@@ -97,6 +125,16 @@ export class VideoPlaylistService { | |||
97 | 125 | ||
98 | return this.authHttp.post<{ videoPlaylist: { id: number } }>(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL, data) | 126 | return this.authHttp.post<{ videoPlaylist: { id: number } }>(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL, data) |
99 | .pipe( | 127 | .pipe( |
128 | tap(res => { | ||
129 | this.myAccountPlaylistCache.total++ | ||
130 | |||
131 | this.myAccountPlaylistCache.data.push({ | ||
132 | id: res.videoPlaylist.id, | ||
133 | displayName: body.displayName | ||
134 | }) | ||
135 | |||
136 | this.myAccountPlaylistCacheSubject.next(this.myAccountPlaylistCache) | ||
137 | }), | ||
100 | catchError(err => this.restExtractor.handleError(err)) | 138 | catchError(err => this.restExtractor.handleError(err)) |
101 | ) | 139 | ) |
102 | } | 140 | } |
@@ -107,6 +145,12 @@ export class VideoPlaylistService { | |||
107 | return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id, data) | 145 | return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id, data) |
108 | .pipe( | 146 | .pipe( |
109 | map(this.restExtractor.extractDataBool), | 147 | map(this.restExtractor.extractDataBool), |
148 | tap(() => { | ||
149 | const playlist = this.myAccountPlaylistCache.data.find(p => p.id === videoPlaylist.id) | ||
150 | playlist.displayName = body.displayName | ||
151 | |||
152 | this.myAccountPlaylistCacheSubject.next(this.myAccountPlaylistCache) | ||
153 | }), | ||
110 | catchError(err => this.restExtractor.handleError(err)) | 154 | catchError(err => this.restExtractor.handleError(err)) |
111 | ) | 155 | ) |
112 | } | 156 | } |
@@ -115,6 +159,13 @@ export class VideoPlaylistService { | |||
115 | return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id) | 159 | return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylist.id) |
116 | .pipe( | 160 | .pipe( |
117 | map(this.restExtractor.extractDataBool), | 161 | map(this.restExtractor.extractDataBool), |
162 | tap(() => { | ||
163 | this.myAccountPlaylistCache.total-- | ||
164 | this.myAccountPlaylistCache.data = this.myAccountPlaylistCache.data | ||
165 | .filter(p => p.id !== videoPlaylist.id) | ||
166 | |||
167 | this.myAccountPlaylistCacheSubject.next(this.myAccountPlaylistCache) | ||
168 | }), | ||
118 | catchError(err => this.restExtractor.handleError(err)) | 169 | catchError(err => this.restExtractor.handleError(err)) |
119 | ) | 170 | ) |
120 | } | 171 | } |
@@ -123,21 +174,49 @@ export class VideoPlaylistService { | |||
123 | const url = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos' | 174 | const url = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos' |
124 | 175 | ||
125 | return this.authHttp.post<{ videoPlaylistElement: { id: number } }>(url, body) | 176 | return this.authHttp.post<{ videoPlaylistElement: { id: number } }>(url, body) |
126 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 177 | .pipe( |
178 | tap(res => { | ||
179 | const existsResult = this.videoExistsCache[body.videoId] | ||
180 | existsResult.push({ | ||
181 | playlistId, | ||
182 | playlistElementId: res.videoPlaylistElement.id, | ||
183 | startTimestamp: body.startTimestamp, | ||
184 | stopTimestamp: body.stopTimestamp | ||
185 | }) | ||
186 | |||
187 | this.runPlaylistCheck(body.videoId) | ||
188 | }), | ||
189 | catchError(err => this.restExtractor.handleError(err)) | ||
190 | ) | ||
127 | } | 191 | } |
128 | 192 | ||
129 | updateVideoOfPlaylist (playlistId: number, playlistElementId: number, body: VideoPlaylistElementUpdate) { | 193 | updateVideoOfPlaylist (playlistId: number, playlistElementId: number, body: VideoPlaylistElementUpdate, videoId: number) { |
130 | return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId, body) | 194 | return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId, body) |
131 | .pipe( | 195 | .pipe( |
132 | map(this.restExtractor.extractDataBool), | 196 | map(this.restExtractor.extractDataBool), |
197 | tap(() => { | ||
198 | const existsResult = this.videoExistsCache[videoId] | ||
199 | const elem = existsResult.find(e => e.playlistElementId === playlistElementId) | ||
200 | |||
201 | elem.startTimestamp = body.startTimestamp | ||
202 | elem.stopTimestamp = body.stopTimestamp | ||
203 | |||
204 | this.runPlaylistCheck(videoId) | ||
205 | }), | ||
133 | catchError(err => this.restExtractor.handleError(err)) | 206 | catchError(err => this.restExtractor.handleError(err)) |
134 | ) | 207 | ) |
135 | } | 208 | } |
136 | 209 | ||
137 | removeVideoFromPlaylist (playlistId: number, playlistElementId: number) { | 210 | removeVideoFromPlaylist (playlistId: number, playlistElementId: number, videoId?: number) { |
138 | return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId) | 211 | return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId) |
139 | .pipe( | 212 | .pipe( |
140 | map(this.restExtractor.extractDataBool), | 213 | map(this.restExtractor.extractDataBool), |
214 | tap(() => { | ||
215 | if (!videoId) return | ||
216 | |||
217 | this.videoExistsCache[videoId] = this.videoExistsCache[videoId].filter(e => e.playlistElementId !== playlistElementId) | ||
218 | this.runPlaylistCheck(videoId) | ||
219 | }), | ||
141 | catchError(err => this.restExtractor.handleError(err)) | 220 | catchError(err => this.restExtractor.handleError(err)) |
142 | ) | 221 | ) |
143 | } | 222 | } |
@@ -173,10 +252,37 @@ export class VideoPlaylistService { | |||
173 | ) | 252 | ) |
174 | } | 253 | } |
175 | 254 | ||
176 | doesVideoExistInPlaylist (videoId: number) { | 255 | listenToMyAccountPlaylistsChange () { |
177 | this.videoExistsInPlaylistSubject.next(videoId) | 256 | return this.myAccountPlaylistCacheSubject.asObservable() |
257 | } | ||
258 | |||
259 | listenToVideoPlaylistChange (videoId: number) { | ||
260 | if (this.videoExistsObservableCache[ videoId ]) { | ||
261 | return this.videoExistsObservableCache[ videoId ] | ||
262 | } | ||
263 | |||
264 | const obs = this.videoExistsInPlaylistObservable | ||
265 | .pipe( | ||
266 | map(existsResult => existsResult[ videoId ]), | ||
267 | filter(r => !!r), | ||
268 | tap(result => this.videoExistsCache[ videoId ] = result) | ||
269 | ) | ||
270 | |||
271 | this.videoExistsObservableCache[ videoId ] = obs | ||
272 | return obs | ||
273 | } | ||
274 | |||
275 | runPlaylistCheck (videoId: number) { | ||
276 | logger('Running playlist check.') | ||
277 | |||
278 | if (this.videoExistsCache[videoId]) { | ||
279 | logger('Found cache for %d.', videoId) | ||
280 | |||
281 | return this.videoExistsInPlaylistCacheSubject.next({ [videoId]: this.videoExistsCache[videoId] }) | ||
282 | } | ||
178 | 283 | ||
179 | return this.videoExistsInPlaylistObservable.pipe(first()) | 284 | logger('Fetching from network for %d.', videoId) |
285 | return this.videoExistsInPlaylistNotifier.next(videoId) | ||
180 | } | 286 | } |
181 | 287 | ||
182 | extractPlaylists (result: ResultList<VideoPlaylistServerModel>) { | 288 | extractPlaylists (result: ResultList<VideoPlaylistServerModel>) { |
@@ -218,7 +324,7 @@ export class VideoPlaylistService { | |||
218 | ) | 324 | ) |
219 | } | 325 | } |
220 | 326 | ||
221 | private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> { | 327 | private doVideosExistInPlaylist (videoIds: number[]): Observable<VideosExistInPlaylists> { |
222 | const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' | 328 | const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' |
223 | 329 | ||
224 | let params = new HttpParams() | 330 | let params = new HttpParams() |