View Javadoc

1   package org.musicontroller;
2   
3   import java.util.LinkedList;
4   import java.util.List;
5   import java.util.concurrent.CopyOnWriteArrayList;
6   
7   import org.apache.log4j.Logger;
8   import org.musicontroller.core.Band;
9   import org.musicontroller.core.Event;
10  import org.musicontroller.core.Keyword;
11  import org.musicontroller.core.Keywordbag;
12  import org.musicontroller.core.Song;
13  import org.musicontroller.security.IUser;
14  import org.musicontroller.songselection.CandidateSelector;
15  import org.musicontroller.songselection.LastPlayedContainer;
16  import org.musicontroller.songselection.SongSelector;
17  
18  public class DJImpl implements DJ, SongChangeListener {
19  	private static Logger LOG = Logger.getLogger(DJImpl.class);
20  	
21  	private static MusiController _musicontroller;
22  	private SongSelector _songselector;
23  	private CandidateSelector _candidateselector;
24  	private List<Long> _requests;
25  	private List<Long> _prediction;
26  	private IUser _user;
27  	private boolean _mustskip;
28  	private long _nextMandatoryRequest;
29  	private boolean _playing;
30  	private LastPlayedContainer _lastplayed;
31  	/**
32  	 * A non DB-related version of the Song currently being played. Never null.
33  	 */
34  	private Song _currentSong;
35  	private int _millisecondsplayed;
36  	
37  	public DJImpl() {
38  		_requests = new CopyOnWriteArrayList<Long>();
39  		_user = null;
40  		_mustskip = false;
41  		_nextMandatoryRequest = -1L;
42  		_playing = false;
43  		_prediction = null;
44  		_lastplayed = new LastPlayedContainer();
45  		_currentSong = new Song();
46  		_currentSong.setBand(new Band());
47  	}
48  	
49  	/*
50  	 * (non-Javadoc)
51  	 * @see org.musicontroller.DJ#setPlaying(boolean)
52  	 */
53  	public void setPlaying(boolean playing) {
54  		_playing = playing;
55  	}
56  	
57  	/*
58  	 * (non-Javadoc)
59  	 * @see org.musicontroller.DJ#setMusiController(org.musicontroller.MusiController)
60  	 */
61  	public void setMusiController(MusiController musicontroller) {
62  		_musicontroller = musicontroller;
63  	}
64  	
65  	/*
66  	 * (non-Javadoc)
67  	 * @see org.musicontroller.DJ#getMusiController()
68  	 */
69  	public MusiController getMusiController() {
70  		return _musicontroller;
71  	}
72  
73  	/*
74  	 * (non-Javadoc)
75  	 * @see org.musicontroller.DJ#getSongSelector()
76  	 */
77  	public SongSelector getSongSelector() {
78  		return _songselector;
79  	}
80  
81  	/*
82  	 * (non-Javadoc)
83  	 * @see org.musicontroller.DJ#setSongSelector(org.musicontroller.songselection.SongSelector)
84  	 */
85  	public void setSongSelector(SongSelector selector) {
86  		this._songselector = selector;
87  	}
88  
89  	/*
90  	 * (non-Javadoc)
91  	 * @see org.musicontroller.DJ#getCandidateSelector()
92  	 */
93  	public CandidateSelector getCandidateSelector() {
94  		return _candidateselector;
95  	}
96  
97  	/*
98  	 * (non-Javadoc)
99  	 * @see org.musicontroller.DJ#setCandidateSelector(org.musicontroller.songselection.CandidateSelector)
100 	 */
101 	public void setCandidateSelector(CandidateSelector selector) {
102 		_candidateselector = selector;
103 	}
104 	
105 	/**
106 	 * Selects a list of Songs that were chosen from the pool. Rules are applied
107 	 * in the following order: 
108 	 * <ol>
109 	 * <li>If there has been a mandatory request, that request is returned.</li>
110 	 * <li>If we have already made predictions, we return the first prediction/</li>
111 	 * <li>If the requests parameter is set, those requests are returned</li>
112 	 * <li>A new random selection from the pool is returned.</ul>
113 	 * </ol>
114 	 * @param requests An optional list of requests.
115 	 * @return A list of songs, with the above rules applied.
116 	 * @see #peek(int)
117 	 */
118 	private List<Song> selectCandidates(final List<Long> requests) {
119 		// If we have a mandatory request, it preceeds all other requests.
120 		List<Long> reqs;
121 		if (_nextMandatoryRequest!=-1L) {
122 			reqs = new LinkedList<Long>();
123 			reqs.add(_nextMandatoryRequest);
124 		} else {
125 			if (_prediction!=null && _prediction.size()>0) {
126 				//If we made a prediction, it preceeds 'free' selection
127 				reqs = new LinkedList<Long>();
128 				reqs.add(_prediction.get(0));
129 				_prediction.remove(0);
130 			} else {
131 				reqs = requests;
132 			}
133 		}
134 		return _candidateselector.selectCandidates(reqs);
135 	}
136 	
137 	/*
138 	 * (non-Javadoc)
139 	 * @see org.musicontroller.DJ#choose()
140 	 * This method is synchronized to prevent simultaneous access to the 
141 	 * internal lists of requests and predictions.
142 	 */
143 	public synchronized Song choose() {
144 		if(getUser()==null) {
145 			LOG.error("DJ has no user attached. The DJ can not play songs.");
146 			return null;			
147 		}
148 		if(getMusiController()==null) {
149 			LOG.error("DJ for user "+getUser().getName()+" has no MusiController attached. The DJ can not play songs.");
150 			return null;
151 		}
152 		Song song = null;
153 		try {
154 			song = _songselector.selectSong(selectCandidates(_requests),_lastplayed,_user);
155 			_nextMandatoryRequest=-1L; //If there was a mandatory request, it has been chosen, so reset this value.
156 			// The song selector might return NULL if there are no more unplayed songs.
157 			if(song!=null) {
158 				_lastplayed.addToLastPlayed(song.getId());
159 				_candidateselector.removeFromCandidates(song);
160 				removeFromRequestsandPredictions(song.getId());
161 				setCurrentSong(song);
162 			}
163 		} catch (Exception e) {
164 			LOG.error(e);
165 		}
166 		return song;
167 	}
168 
169 	/**
170 	 * Sets the internal _currentSong variable with a copy of a Song object.
171 	 * The internal _currentSong is and remains DB-independent.
172 	 * @param song The new Song that is currently being played.
173 	 */
174 	private void setCurrentSong(Song song) {
175 		_currentSong.setId(song.getId());
176 		_currentSong.setName(song.getName());
177 		_currentSong.getBand().setId(song.getBand().getId());
178 		_currentSong.getBand().setName(song.getBand().getName());
179 		_currentSong.setLength(song.getLength());
180 		
181 		List<Keyword> currentKeywords = new LinkedList<Keyword>();
182 		if (song.getKeywordbag()!=null) {
183 			for(Keyword kw : song.getKeywordbag().getKeywords()) {
184 				Keyword currentkw = new Keyword();
185 				currentkw.setId(kw.getId());
186 				currentkw.setName(kw.getName());
187 				currentKeywords.add(kw);
188 			}
189 		}
190 		Keywordbag currentKeywordbag = new Keywordbag();
191 		currentKeywordbag.setKeywords(currentKeywords);
192 		_currentSong.setKeywordbag(currentKeywordbag);
193 	}
194 	
195 	/**
196 	 * Marks a Song as played. This adds a played-Event to the Song for the
197 	 * current user. As a side effect, the Band- and Keywordpopularity lists
198 	 * are updated.
199 	 * @param songid The id of the Song that has just been played.
200 	 */
201 	private void markAsPlayed(long songid) {
202 		getMusiController().saveSongEvent(songid,Event.played,_user.getId());
203 		_songselector.addBandPlay(_currentSong.getBand().getId());
204 		for (Keyword kw : _currentSong.getKeywordbag().getKeywords()) {
205 			_songselector.addKeywordPlay(kw.getId());
206 		}
207 	}
208 	
209 	/**
210 	 * Marks a Song as skipped. This adds a skipped-Event to the Song for the
211 	 * current user. As a side effect, the Band- and Keywordpopularity lists
212 	 * are updated.
213 	 * @param songid The id of the Song that has just been skipped.
214 	 */
215 	private void markAsSkipped(long songid) {
216 		_lastplayed.markLastSongAsSkipped();
217 		getMusiController().saveSongEvent(songid,Event.skipped,_user.getId());
218 		_songselector.addBandSkip(_currentSong.getBand().getId());
219 		for (Keyword kw : _currentSong.getKeywordbag().getKeywords()) {
220 			_songselector.addKeywordSkip(kw.getId());
221 		}
222 	}
223 	
224 	/**
225 	 * Marks a song as requested. This adds a requested-Event to the Song for the
226 	 * current user.
227 	 * @param songid The id of the Song that has just been requested.
228 	 */
229 	private void markAsRequested(long songid) {
230 		getMusiController().saveSongEvent(songid,Event.requested,_user.getId());
231 	}
232 	
233 	/*
234 	 * (non-Javadoc)
235 	 * @see org.musicontroller.DJ#requestSong(long)
236 	 * This method is synchronized to prevent simultaneous access to the 
237 	 * internal lists of requests and predictions.
238 	 */
239 	public synchronized void requestSong(long songid) {
240 		if(_user==null) {
241 			LOG.info("Song request denied: user not logged on.");
242 			return;
243 		}
244 		_requests.add(songid);
245 		markAsRequested(songid);
246 		_prediction = null;
247 	}
248 	
249 	/*
250 	 * (non-Javadoc)
251 	 * @see org.musicontroller.DJ#unrequestSong(long)
252 	 * This method is synchronized to prevent simultaneous access to the 
253 	 * internal lists of requests and predictions.
254 	 */
255 	public synchronized void unrequestSong(long songid) {
256 		removeFromRequestsandPredictions(songid);
257 		markAsSkipped(songid);
258 	}
259 
260 	/**
261 	 * Removes a song-ID from the requests and predictions. To be used when
262 	 * a Song has been selected by the SongSelector.
263 	 * @param songid The id to remove.
264 	 */
265 	private void removeFromRequestsandPredictions(long songid) {
266 		_requests.remove(songid);
267 			
268 		if (_prediction!=null) {
269 			//If a song was unrequested that was not requested but predicted
270 			//The behaviour is the same: the song disappears from the list
271 			//of upcoming songs.
272 			_prediction.remove(songid);
273 		}
274 	}
275 	
276 	/*
277 	 * (non-Javadoc)
278 	 * @see org.musicontroller.DJ#skipSong()
279 	 */
280 	public void skipSong() {
281 		_mustskip = true;
282 	}
283 	
284 	/*
285 	 * (non-Javadoc)
286 	 * @see org.musicontroller.DJ#mustSkip()
287 	 */
288 	public boolean mustSkip() {
289 		return _mustskip;
290 	}
291 	
292 	/*
293 	 * (non-Javadoc)
294 	 * @see org.musicontroller.DJ#confirmSkip()
295 	 */
296 	public void confirmSkip() {
297 		_mustskip = false;
298 		markAsSkipped(_currentSong.getId());
299 	}
300 	
301 	/*
302 	 * (non-Javadoc)
303 	 * @see org.musicontroller.DJ#confirmPlay()
304 	 */
305 	public void confirmPlay() {
306 		markAsPlayed(_currentSong.getId());
307 	}
308 	
309 	/*
310 	 * (non-Javadoc)
311 	 * @see org.musicontroller.DJ#playSong(long)
312 	 */
313 	public void playSong(long songid) {
314 		_nextMandatoryRequest = songid;
315 		skipSong();
316 	}
317 	
318 	/*
319 	 * (non-Javadoc)
320 	 * @see org.musicontroller.DJ#getRequests()
321 	 */
322 	public List<Long> getRequests() {
323 		return _requests;
324 	}
325 
326 	/*
327 	 * (non-Javadoc)
328 	 * @see org.musicontroller.DJ#setUser(org.musicontroller.security.IUser)
329 	 */
330 	public void setUser(IUser user) {
331 		_user = user;
332 	}
333 	
334 	/*
335 	 * (non-Javadoc)
336 	 * @see org.musicontroller.DJ#getUser()
337 	 */
338 	public IUser getUser() {
339 		return _user;
340 	}
341 	
342 	/*
343 	 * (non-Javadoc)
344 	 * @see org.musicontroller.DJ#getCurrentSongId()
345 	 */
346 	public Song getCurrentSong() {
347 		return _playing ? _currentSong : null;
348 	}
349 	
350 	/*
351 	 * This method is synchronized, because it is expensive. We don't want to
352 	 * execute it more than needed. The result is cached anyway, to the
353 	 * subsequent call to this method returns more or less instantly.
354 	 * 
355 	 * (non-Javadoc)
356 	 * @see org.musicontroller.DJ#peek(int)
357 	 */
358 	public synchronized List<Long> peek(int count) {
359 		List<Long> prediction = _prediction==null ? new LinkedList<Long>() : _prediction;
360 		
361 		//We predict a minimum of count songs, but requests are always predicted.
362 		count = Math.max(count, _requests.size());
363 
364 		int toPredict = count - prediction.size();
365 		if (toPredict==0) return _prediction;
366 		LOG.debug("Asked for "+toPredict+" new predictions.");
367 
368 		//Create a list of requests that are not predicted yet.
369 		List<Long> requestsCopy = new LinkedList<Long>();
370 		for (Long l : _requests) {
371 			if (!prediction.contains(l)) requestsCopy.add(l);
372 		}
373 
374 		LastPlayedContainer lastplayedCopy = null;
375 		try {
376 			lastplayedCopy = _lastplayed.clone();
377 		} catch (CloneNotSupportedException e) {
378 			LOG.error("Error cloning lastplayedcontainer, continueing with a new one : " + e);
379 			lastplayedCopy = new LastPlayedContainer();
380 		}
381 		//Add the predictions we already made to the lastplayed-list.
382 		for (Long l : prediction) {
383 			lastplayedCopy.addToLastPlayed(l);
384 		}
385 	
386 		_prediction=null; //Should be null while predicting to kill side-effects
387 		
388 		//First, predict the order of all requests
389 		while (requestsCopy.size()>0) {
390 			Song song = getSongSelector().selectSong(selectCandidates(requestsCopy),lastplayedCopy,_user);
391 			if (song!=null) {
392 				lastplayedCopy.addToLastPlayed(song.getId());
393 				requestsCopy.remove(song.getId());
394 				prediction.add(song.getId());
395 			}
396 			toPredict--;
397 		}
398 		
399 		if (toPredict>0) { //Only enter when we need some more predictions.
400 			//For the rest of the predictions, we use Min(toPredict*10,100) candidates.
401 			//We can use the _candidateselector here, because we already predicted
402 			//the requests-order.
403 			int toSelect = Math.min(100, _candidateselector.getCandidateCount()*toPredict);
404 			List<Song> candidates = _candidateselector.selectCandidates(toSelect);
405 			while (toPredict>0) {
406 				Song song = getSongSelector().selectSong(candidates,lastplayedCopy,_user);
407 				if (song!=null) {
408 					lastplayedCopy.addToLastPlayed(song.getId(), true);
409 					requestsCopy.remove(song.getId());
410 					prediction.add(song.getId());
411 					candidates.remove(song);
412 				}
413 				toPredict--;
414 			}
415 		}
416 		_prediction = prediction;
417 		
418 		return _prediction;
419 	}
420 
421 	/*
422 	 * (non-Javadoc)
423 	 * @see org.musicontroller.DJ#setPlayingTime(int)
424 	 */
425 	public void setPlayingTime(int millis) {
426 		_millisecondsplayed = millis;
427 	}
428 	
429 	/*
430 	 * (non-Javadoc)
431 	 * @see org.musicontroller.DJ#getPlayingTime()
432 	 */
433 	public int getPlayingTime() {
434 		return _millisecondsplayed;
435 	}
436 
437 	/*
438 	 * (non-Javadoc)
439 	 * @see org.musicontroller.SongChangeListener#onSongAdded(org.musicontroller.core.Song)
440 	 */
441 	public void onSongAdded(Song song) {
442 		_candidateselector.addToCandidates(song);
443 	}
444 
445 	/*
446 	 * (non-Javadoc)
447 	 * @see org.musicontroller.SongChangeListener#onSongChanged(org.musicontroller.core.Song)
448 	 */
449 	public void onSongChanged(Song song) {
450 		if (_currentSong.getId()==song.getId()) {
451 			setCurrentSong(song);
452 		}
453 	}
454 
455 	/*
456 	 * (non-Javadoc)
457 	 * @see org.musicontroller.SongChangeListener#onSongDeleted(org.musicontroller.core.Song)
458 	 * This method is synchronized to prevent simultaneous access to the 
459 	 * internal lists of requests and predictions.
460 	 */
461 	public synchronized void onSongDeleted(Song song) {
462 		_candidateselector.removeFromCandidates(song);
463 		removeFromRequestsandPredictions(song.getId());
464 	}
465 
466 }