View Javadoc

1   /***
2    *     Ambient - A music player for the Android platform
3    Copyright (C) 2007 Martin Vysny
4    
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9    
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14  
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  package sk.baka.ambient;
19  
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.Map;
23  
24  import sk.baka.ambient.AppState.IAppStateAware;
25  import sk.baka.ambient.collection.IDynamicPlaylistTrackProvider;
26  import sk.baka.ambient.collection.TrackMetadataBean;
27  import sk.baka.ambient.collection.TrackOriginEnum;
28  import sk.baka.ambient.collection.local.MediaStoreTrackProvider;
29  import sk.baka.ambient.commons.Interval;
30  import sk.baka.ambient.playerservice.IPlayer;
31  import sk.baka.ambient.playerservice.IPlayerListener;
32  import sk.baka.ambient.playerservice.PlayerService;
33  import sk.baka.ambient.playerservice.PlayerStateEnum;
34  import sk.baka.ambient.playlist.DynamicPlaylistStrategy;
35  import sk.baka.ambient.playlist.IPlaylistStrategy;
36  import sk.baka.ambient.playlist.PlaylistItem;
37  import sk.baka.ambient.playlist.Random;
38  import sk.baka.ambient.playlist.Repeat;
39  import sk.baka.ambient.playlist.StaticPlaylistStrategy;
40  import sk.baka.ambient.playlist.Utils;
41  import android.content.ComponentName;
42  import android.content.Context;
43  import android.content.Intent;
44  import android.content.ServiceConnection;
45  import android.os.Handler;
46  import android.os.IBinder;
47  
48  /***
49   * This is THE component :) Combines player and playlist. Emits
50   * {@link IPlaylistPlayerListener} messages.
51   * 
52   * @author Martin Vysny
53   */
54  public final class PlaylistPlayer implements IPlaylistStrategy, IAppStateAware {
55  	/***
56  	 * The player service invocator.
57  	 */
58  	IPlayer playerService;
59  
60  	/***
61  	 * Receives value of {@link #playerService}.
62  	 */
63  	private final ServiceConnection serviceConnection = new ServiceConnection() {
64  		public void onServiceConnected(ComponentName name, IBinder service) {
65  			playerService = IPlayer.Stub.asInterface(service);
66  			// okay, player connected. we can now restore the state if needed
67  			final AppState state = app.getStateHandler().getStartupState();
68  			reinit(state);
69  		}
70  
71  		public void onServiceDisconnected(ComponentName arg0) {
72  			// do nothing, we will never be disconnected from a local service
73  		}
74  	};
75  
76  	/***
77  	 * Creates new player instance.
78  	 * 
79  	 * @param app
80  	 *            the application instance.
81  	 */
82  	PlaylistPlayer(final AmbientApplication app) {
83  		app.bindService(new Intent(app, PlayerService.class),
84  				serviceConnection, Context.BIND_AUTO_CREATE);
85  		app.getBus().addHandler(listener);
86  		this.app = app;
87  		errHandler = new PlaylistPlayerErrorHandler(this);
88  	}
89  
90  	private final AmbientApplication app;
91  	
92  	private final PlaylistPlayerErrorHandler errHandler;
93  
94  	/***
95  	 * Stops the playback. Does nothing if the player is inactive.
96  	 */
97  	public void stop() {
98  		try {
99  			playerService.stop();
100 		} catch (Exception e) {
101 			throw new RuntimeException(e);
102 		}
103 		errHandler.cancelLocationResolving();
104 		invokePlaybackStateChanged();
105 	}
106 
107 	/***
108 	 * Returns the playback state.
109 	 * 
110 	 * @return the playback state.
111 	 */
112 	public PlayerStateEnum getPlaybackState() {
113 		if (playerService == null) {
114 			// this is only possible when the application is starting. The
115 			// playback will be stopped.
116 			return PlayerStateEnum.Stopped;
117 		}
118 		try {
119 			return PlayerStateEnum.values()[playerService.getPlaybackState()];
120 		} catch (Exception e) {
121 			throw new RuntimeException(e);
122 		}
123 	}
124 
125 	/***
126 	 * Pauses the player, or resumes playback if the player was paused. Does
127 	 * nothing if the player is stopped.
128 	 */
129 	public void pause() {
130 		try {
131 			playerService.pause();
132 		} catch (Exception e) {
133 			throw new RuntimeException(e);
134 		}
135 		invokePlaybackStateChanged();
136 	}
137 
138 	/***
139 	 * Seeks to given position. If the position is invalid then the result is
140 	 * undefined. Does nothing if the playback is stopped.
141 	 * 
142 	 * @param position
143 	 *            Seek to this position in milliseconds.
144 	 */
145 	public void seek(int position) {
146 		try {
147 			final boolean seekChanged = playerService.seek(position);
148 			if (seekChanged) {
149 				getBusInvocator().trackPositionChanged(position,
150 						getPlaybackState() == PlayerStateEnum.Playing);
151 			}
152 		} catch (Exception e) {
153 			throw new RuntimeException(e);
154 		}
155 	}
156 
157 	/***
158 	 * The playlist backend.
159 	 */
160 	private IPlaylistStrategy playlist;
161 
162 	/***
163 	 * Adds track to the playlist after given track
164 	 * 
165 	 * @param index
166 	 *            insert given track after track with this index.
167 	 * 
168 	 * @param track
169 	 *            the track to add.
170 	 */
171 	public void add(int index, TrackMetadataBean track) {
172 		add(index, Collections.singletonList(track));
173 	}
174 
175 	public void add(int index, List<TrackMetadataBean> tracks) {
176 		playlist.add(index, tracks);
177 		// TODO vyzivus: compute the interval of added tracks correctly
178 		getBusInvocator().playlistChanged(null);
179 	}
180 
181 	private IPlaylistPlayerListener getBusInvocator() {
182 		return app.getBus().getInvocator(IPlaylistPlayerListener.class, true);
183 	}
184 
185 	/***
186 	 * Adds track to the end of the playlist.
187 	 * 
188 	 * @param track
189 	 *            the track to add.
190 	 */
191 	public void add(TrackMetadataBean track) {
192 		add(playlist.size(), track);
193 	}
194 
195 	public void clearQueue() {
196 		playlist.clearQueue();
197 		getBusInvocator().playlistChanged(null);
198 	}
199 
200 	public void dequeue(int track) {
201 		playlist.dequeue(track);
202 	}
203 
204 	public int getCurrentlyPlaying() {
205 		return playlist.getCurrentlyPlaying();
206 	}
207 
208 	/***
209 	 * Returns currently playing track.
210 	 * 
211 	 * @return current track or <code>null</code> if no track is being played (
212 	 *         {@link #getCurrentlyPlaying()} returns <code>-1</code>).
213 	 */
214 	public TrackMetadataBean getCurrentlyPlayingTrack() {
215 		if (playlist == null)
216 			return null;
217 		final int current = playlist.getCurrentlyPlaying();
218 		return current < 0 ? null : playlist.getPlayItems().get(current)
219 				.getTrack();
220 	}
221 
222 	/***
223 	 * Returns currently playing track.
224 	 * 
225 	 * @return current track or <code>null</code> if no track is being played (
226 	 *         {@link #getCurrentlyPlaying()} returns <code>-1</code>).
227 	 */
228 	public PlaylistItem getCurrentlyPlayingItem() {
229 		final int current = playlist.getCurrentlyPlaying();
230 		return current < 0 ? null : playlist.getPlayItems().get(current);
231 	}
232 
233 	public List<PlaylistItem> getPlayItems() {
234 		return playlist.getPlayItems();
235 	}
236 
237 	public int next() {
238 		final int track = playlist.next();
239 		final boolean playing = getPlaybackState() != PlayerStateEnum.Stopped;
240 		if (playing) {
241 			psPlay(track, 0);
242 		}
243 		invokeTrackChanged(playing, 0);
244 		return track;
245 	}
246 
247 	public int play(int trackToPlay) {
248 		return playWithSeek(trackToPlay, 0);
249 	}
250 
251 	/***
252 	 * Plays given track. The playback continues from this track forward, unless
253 	 * random play is activated.
254 	 * 
255 	 * @param trackToPlay
256 	 *            the track to play. Index to the {@link #getPlayItems()} list.
257 	 *            If <code>-1</code> then the current track pointer is moved
258 	 *            before first track and the playback is stopped.
259 	 * @param startSeekMillis
260 	 *            start playback here. Ignored when the stream is not seekable.
261 	 * @return the track that is being played.
262 	 */
263 	public int playWithSeek(final int trackToPlay, final int startSeekMillis) {
264 		final int track = playlist.play(trackToPlay);
265 		psPlay(track, startSeekMillis);
266 		invokeTrackChanged(true, startSeekMillis);
267 		return track;
268 	}
269 
270 	/***
271 	 * Plays current track from the beginning.
272 	 */
273 	public void play() {
274 		playWithSeek(0);
275 	}
276 
277 	/***
278 	 * Plays current track from the beginning.
279 	 * 
280 	 * @param startSeekMillis
281 	 *            start playback here. Ignored when the stream is not seekable.
282 	 */
283 	public void playWithSeek(final int startSeekMillis) {
284 		playWithSeek(getCurrentlyPlaying(), startSeekMillis);
285 	}
286 
287 	public int previous() {
288 		final int track = playlist.previous();
289 		final boolean playing = getPlaybackState() != PlayerStateEnum.Stopped;
290 		if (playing) {
291 			psPlay(track, 0);
292 		}
293 		invokeTrackChanged(playing, 0);
294 		return track;
295 	}
296 
297 	/***
298 	 * Requests the player service to play given track. The {@link #playlist} is not updated.
299 	 * @param track the track to play.
300 	 * @param startSeekMillis start seek time, in milliseconds.
301 	 */
302 	void psPlay(int track, int startSeekMillis) {
303 		if (track < 0) {
304 			stop();
305 			return;
306 		}
307 		final TrackMetadataBean bean = playlist.getPlayItems().get(track)
308 				.getTrack();
309 		try {
310 			playerService.play(bean.getLocation(), bean.getOrigin().ordinal(),
311 					startSeekMillis);
312 		} catch (Exception e) {
313 			throw new RuntimeException(e);
314 		}
315 	}
316 
317 	/***
318 	 * Queues given track, after all other queued tracks. Does nothing if the
319 	 * track is already queued.
320 	 * 
321 	 * @param track
322 	 *            the track to queue, index to the {@link #getPlayItems()} list.
323 	 */
324 	public void queue(int track) {
325 		queue(Interval.fromItem(track));
326 	}
327 
328 	public void queue(final Interval tracks) {
329 		if (tracks.isEmpty())
330 			return;
331 		playlist.queue(tracks);
332 		getBusInvocator().playlistChanged(null);
333 	}
334 
335 	public void reinit() {
336 		playlist.reinit();
337 	}
338 
339 	/***
340 	 * Removes a track with given index from the playlist.
341 	 * 
342 	 * @param index
343 	 *            the index.
344 	 * @return the removed track. If currently played track is removed then the
345 	 *         playlist should move to play a next song.
346 	 */
347 	public PlaylistItem remove(final int index) {
348 		final PlaylistItem result = playlist.getPlayItems().get(index);
349 		playlist.remove(Interval.fromItem(index));
350 		getBusInvocator().playlistChanged(Interval.EMPTY);
351 		return result;
352 	}
353 
354 	/***
355 	 * Returns <code>true</code> if the playlist is a dynamic playlist.
356 	 * 
357 	 * @return <code>true</code> if current playlist is dynamic.
358 	 */
359 	public boolean isDynamic() {
360 		return playlist instanceof DynamicPlaylistStrategy;
361 	}
362 
363 	public void remove(Interval interval) {
364 		if (interval.isEmpty())
365 			return;
366 		playlist.remove(interval);
367 		getBusInvocator().playlistChanged(Interval.EMPTY);
368 	}
369 
370 	public void setRandom(Random random) {
371 		setRandomRepeat(random, repeat);
372 	}
373 
374 	public Random getRandom() {
375 		return random;
376 	}
377 
378 	public void shuffle() {
379 		playlist.shuffle();
380 		getBusInvocator().playlistChanged(null);
381 	}
382 
383 	public int size() {
384 		return playlist.size();
385 	}
386 
387 	public void sortByAlbumOrder() {
388 		playlist.sortByAlbumOrder();
389 		getBusInvocator().playlistChanged(null);
390 	}
391 
392 	/***
393 	 * Changes the playlist backend to be a dynamic playlist. The playlist is
394 	 * populated automatically.
395 	 */
396 	public void dynamicPlaylist() {
397 		if (playlist instanceof DynamicPlaylistStrategy) {
398 			return;
399 		}
400 		// TODO vyzivus maybe make this configurable?
401 		final IDynamicPlaylistTrackProvider provider = new MediaStoreTrackProvider(
402 				Random.ALBUM);
403 		playlist = new DynamicPlaylistStrategy(provider, playlist, app
404 				.getStateHandler().getConfig().dynamicHistorySize, app
405 				.getStateHandler().getConfig().dynamicUpcomingSize);
406 		getBusInvocator().playlistChanged(null);
407 	}
408 
409 	/***
410 	 * Changes the playlist backend to be an empty static playlist.
411 	 */
412 	public void staticPlaylist() {
413 		if (playlist instanceof StaticPlaylistStrategy)
414 			return;
415 		playlist = new StaticPlaylistStrategy(playlist);
416 	}
417 
418 	/***
419 	 * Closes and deactivates the player. The instance is no more usable.
420 	 */
421 	public void close() {
422 		errHandler.close();
423 		app.unbindService(serviceConnection);
424 		app.getBus().removeHandler(listener);
425 	}
426 
427 	private final Listener listener = new Listener();
428 
429 	private final Handler handler = AmbientApplication.getHandler();
430 
431 	private final class Listener implements IPlayerListener,
432 			IApplicationListener, Runnable {
433 
434 		public void stateChanged(AppState state) {
435 			// do nothing
436 		}
437 
438 		public void started(String file, int duration, int currentPosition) {
439 			errHandler.handleSuccess();
440 			getBusInvocator().playbackStateChanged(PlayerStateEnum.Playing);
441 		}
442 
443 		private boolean isQueued(int track) {
444 			if (track < 0) {
445 				return false;
446 			}
447 			return playlist.getPlayItems().get(track).getQueueOrder() > 0;
448 		}
449 
450 		public void stopped(String error, boolean missing,
451 				TrackOriginEnum origin) {
452 			// on error show notification
453 			if (error != null) {
454 				errHandler.handleError(error, missing, origin);
455 				return;
456 			}
457 			// tell the player to play next song from the playlist.
458 			if (repeat == Repeat.TRACK) {
459 				playWithGap();
460 				return;
461 			}
462 			final TrackMetadataBean prevTrack = getCurrentlyPlayingTrack();
463 			int track = playlist.peekNext();
464 			final boolean isNextQueued = isQueued(track);
465 			if ((repeat == Repeat.ALBUM) && (prevTrack != null)
466 					&& !isNextQueued) {
467 				if (isDynamic()) {
468 					// TODO vyzivus
469 				} else {
470 					// the album is going to be switched?
471 					final TrackMetadataBean newTrack = track == -1 ? null
472 							: playlist.getPlayItems().get(track).getTrack();
473 					if ((newTrack == null)
474 							|| (!newTrack.getAlbum().equals(
475 									prevTrack.getAlbum()))) {
476 						// find first song of given album.
477 						final List<PlaylistItem> tracks = Utils.filterOnAlbum(
478 								playlist.getPlayItems(), prevTrack.getAlbum());
479 						if (!tracks.isEmpty()) {
480 							Utils.sortPlaylistByAlbumOrder(tracks);
481 							final PlaylistItem first = tracks.get(0);
482 							final int firstIndex = playlist.getPlayItems()
483 									.indexOf(first);
484 							playWithGap(firstIndex);
485 							return;
486 						}
487 					}
488 				}
489 				// continue normally with the next track.
490 			}
491 			track = playlist.next();
492 			if (track != -1) {
493 				playWithGap();
494 				return;
495 			}
496 			// end of the playlist.
497 			if ((repeat == Repeat.NO) || (playlist.size() == 0)) {
498 				// nothing more to play
499 				invokePlaybackStateChanged();
500 				return;
501 			}
502 			// try playing first track.
503 			if (next() != -1) {
504 				playWithGap();
505 			}
506 		}
507 
508 		public void configChanged(ConfigurationBean config) {
509 			if (isDynamic()) {
510 				final DynamicPlaylistStrategy p = (DynamicPlaylistStrategy) playlist;
511 				p.setHistoryLength(config.dynamicHistorySize);
512 				p.setUpcomingTrackCount(config.dynamicUpcomingSize);
513 				getBusInvocator().playlistChanged(null);
514 			}
515 		}
516 
517 		private void playWithGap() {
518 			playWithGap(null);
519 		}
520 
521 		private volatile Integer desiredTrack;
522 
523 		private void playWithGap(final Integer track) {
524 			desiredTrack = track;
525 			final long gap = app.getStateHandler().getConfig().playerGap;
526 			if (gap == 0) {
527 				run();
528 				return;
529 			}
530 			handler.removeCallbacks(this);
531 			handler.postDelayed(this, gap);
532 		}
533 
534 		public void run() {
535 			if (desiredTrack == null) {
536 				play();
537 			} else {
538 				play(desiredTrack);
539 			}
540 		}
541 
542 		public void clipboardChanged() {
543 			// do nothing
544 		}
545 
546 		public void radioNewTrack(String name) {
547 			// do nothing
548 		}
549 
550 		public void buffered(byte percent) {
551 			// do nothing
552 		}
553 
554 		public void offline(boolean offline) {
555 			if (offline) {
556 				final TrackMetadataBean track = getCurrentlyPlayingTrack();
557 				if ((track != null) && !track.isLocal()) {
558 					stop();
559 				}
560 			}
561 		}
562 	}
563 
564 	/***
565 	 * The repeat mode.
566 	 */
567 	private Repeat repeat = Repeat.NO;
568 
569 	/***
570 	 * Random mode as selected by user.
571 	 */
572 	private Random random = Random.NONE;
573 
574 	private void setRandomRepeat(final Random random, final Repeat repeat) {
575 		if (random == null)
576 			throw new IllegalArgumentException("null random");
577 		if (repeat == null)
578 			throw new IllegalArgumentException("null repeat");
579 		// static playlist: if repeat album is selected, a special processing
580 		// needs to be employed, to ensure that the entire disc is played
581 		// correctly.
582 		Random strategyRandom = random;
583 		if (!isDynamic() && (repeat == Repeat.ALBUM)) {
584 			// we need to group the playorder by albums, to make sure that the
585 			// entire disc is played correctly.
586 			if (random == Random.NONE) {
587 				strategyRandom = Random.ALBUM_PLAYLIST;
588 			} else if (random == Random.TRACK) {
589 				strategyRandom = Random.ALBUM_TRACK;
590 			}
591 		}
592 		playlist.setRandom(strategyRandom);
593 		if (this.random != random) {
594 			this.random = random;
595 			getBusInvocator().randomChanged(random);
596 			if (isDynamic()) {
597 				// the playlist may have been changed as well
598 				getBusInvocator().playlistChanged(null);
599 			}
600 		}
601 		if (this.repeat != repeat) {
602 			this.repeat = repeat;
603 			getBusInvocator().repeatChanged(repeat);
604 		}
605 	}
606 
607 	/***
608 	 * Sets the repeat mode.
609 	 * 
610 	 * @param repeat
611 	 *            repeat mode, must not be <code>null</code>.
612 	 */
613 	public void setRepeat(final Repeat repeat) {
614 		setRandomRepeat(random, repeat);
615 	}
616 
617 	/***
618 	 * Returns the repeat mode.
619 	 * 
620 	 * @return repeat mode, must not be <code>null</code>.
621 	 */
622 	public Repeat getRepeat() {
623 		return repeat;
624 	}
625 
626 	private void invokePlaybackStateChanged() {
627 		final PlayerStateEnum state = getPlaybackState();
628 		getBusInvocator().playbackStateChanged(state);
629 	}
630 
631 	private void invokeTrackChanged(final boolean play, final int positionMillis) {
632 		final PlaylistItem item = getCurrentlyPlayingItem();
633 		getBusInvocator().trackChanged(item, play, positionMillis);
634 		if (isDynamic()) {
635 			getBusInvocator().playlistChanged(null);
636 		}
637 	}
638 
639 	/***
640 	 * Returns current position in millis of the playback. Returns 0 if the
641 	 * playback is stopped.
642 	 * 
643 	 * @return current player position in millis.
644 	 */
645 	public int getPosition() {
646 		try {
647 			return playerService.getPosition();
648 		} catch (Exception e) {
649 			throw new RuntimeException(e);
650 		}
651 	}
652 
653 	public List<Integer> getQueue() {
654 		return playlist.getQueue();
655 	}
656 
657 	public Interval move(Interval interval, int targetIndex) {
658 		if (interval.isEmpty() || interval.contains(targetIndex))
659 			return interval;
660 		final Interval result = playlist.move(interval, targetIndex);
661 		getBusInvocator().playlistChanged(result);
662 		return result;
663 	}
664 
665 	/***
666 	 * Finds given track in the playlist and activates it. If the track is not
667 	 * found then track is deselected.
668 	 * 
669 	 * @param track
670 	 *            the track to activate, may be <code>null</code>.
671 	 */
672 	private void setActive(final TrackMetadataBean track) {
673 		if ((track == null) || isDynamic()) {
674 			playlist.play(-1);
675 		} else {
676 			final List<PlaylistItem> items = playlist.getPlayItems();
677 			for (int i = 0; i < items.size(); i++) {
678 				final PlaylistItem item = items.get(i);
679 				if (item.getTrack().equals(track)) {
680 					playlist.play(i);
681 					invokeTrackChanged(false, 0);
682 					return;
683 				}
684 			}
685 			playlist.play(-1);
686 		}
687 		invokeTrackChanged(false, 0);
688 	}
689 
690 	/***
691 	 * Reinits the player with given playlist strategy.
692 	 * 
693 	 * @param strategy
694 	 *            the deserialized strategy
695 	 */
696 	public void reinit(final IPlaylistStrategy strategy) {
697 		TrackMetadataBean track = getCurrentlyPlayingTrack();
698 		final Random prevRandom = getRandom();
699 		playlist = strategy;
700 		playlist.clearQueue();
701 		playlist.setRandom(prevRandom);
702 		setActive(track);
703 		postReinit();
704 	}
705 
706 	private void postReinit() {
707 		// init the dynamic playlist if needed
708 		if (isDynamic()) {
709 			final DynamicPlaylistStrategy s = (DynamicPlaylistStrategy) playlist;
710 			s
711 					.setHistoryLength(app.getStateHandler().getConfig().dynamicHistorySize);
712 			s
713 					.setUpcomingTrackCount(app.getStateHandler().getConfig().dynamicUpcomingSize);
714 		}
715 		getBusInvocator().playlistChanged(null);
716 	}
717 
718 	public void reinit(AppState state) {
719 		playlist = state.playlist;
720 		if (playlist == null) {
721 			staticPlaylist();
722 		}
723 		setRandomRepeat(state.random != null ? state.random : Random.NONE,
724 				state.repeat != null ? state.repeat : Repeat.NO);
725 		// configure the player
726 		stop();
727 		if (state.state != PlayerStateEnum.Stopped) {
728 			playWithSeek(state.position);
729 			if (state.state == PlayerStateEnum.Paused) {
730 				pause();
731 			}
732 		} else {
733 			invokeTrackChanged(false, 0);
734 		}
735 		postReinit();
736 	}
737 
738 	public void storeState(AppState result) {
739 		final boolean store = app.getStateHandler().getConfig().rememberPlaylist;
740 		result.playlist = store ? playlist : null;
741 		result.position = store ? getPosition() : 0;
742 		result.repeat = getRepeat();
743 		result.random = getRandom();
744 		result.state = store ? getPlaybackState() : PlayerStateEnum.Stopped;
745 	}
746 
747 	public Interval moveBy(Interval interval, int delta) {
748 		if (delta == 0)
749 			return interval;
750 		final Interval result = playlist.moveBy(interval, delta);
751 		getBusInvocator().playlistChanged(result);
752 		return result;
753 	}
754 
755 	public int peekNext() {
756 		return playlist.peekNext();
757 	}
758 
759 	public void replaceLocations(Map<String, String> locationMap) {
760 		playlist.replaceLocations(locationMap);
761 	}
762 }