public interface Jotify extends Runnable, Player {
	/**
	 * Login to Spotify using the specified username and password.
	 * 
	 * @param username Username to use.
	 * @param password Corresponding password.
	 * 
	 * @throws ConnectionException
	 * @throws AuthenticationException
	 */
	public void login(String username, String password) throws ConnectionException, AuthenticationException;
	
	/**
	 *  Closes the connection to a Spotify server.
	 *  
	 *  @throws ConnectionException
	 */
	public void close() throws ConnectionException;
	
	/**
	 * Get user info.
	 * 
	 * @return A {@link User} object.
	 * 
	 * @throws IllegalStateException
	 * 
	 * @see User
	 */
	public User user() throws TimeoutException;
	
	/**
	 * Fetch a toplist.
	 * 
	 * @param type     A toplist type. e.g. "artist", "album" or "track".
	 * @param region   A region code or null. e.g. "SE" or "DE".
	 * @param username A username or null.
	 * 
	 * @return A {@link Result} object.
	 * 
	 * @see Result
	 */
	public Result toplist(String type, String region, String username) throws TimeoutException;
	
	/**
	 * Search for an artist, album or track.
	 * 
	 * @param query Your search query.
	 * 
	 * @return A {@link Result} object.
	 * 
	 * @see Result
	 */
	public Result search(String query) throws TimeoutException;
	
	/**
	 * Get an image (e.g. artist portrait or cover) by requesting
	 * it from the server or loading it from the local cache, if
	 * available.
	 * 
	 * @param id Id of the image to get.
	 * 
	 * @return An {@link Image} or null if the request failed.
	 * 
	 * @see Image
	 */
	public Image image(String id) throws TimeoutException;
	
	/**
	 * Browse artist info.
	 * 
	 * @param artist An {@link Artist} object identifying the artist to browse.
	 * 
	 * @return A new {@link Artist} object holding more information about
	 *         the artist or null on failure.
	 * 
	 * @see Artist
	 */
	public Artist browse(Artist artist) throws TimeoutException;
	
	/**
	 * Browse album info.
	 * 
	 * @param album An {@link Album} object identifying the album to browse.
	 * 
	 * @return A new {@link Album} object holding more information about
	 *         the album or null on failure.
	 * 
	 * @see Album
	 */
	public Album browse(Album album) throws TimeoutException;
	
	/**
	 * Browse track info.
	 * 
	 * @param track A {@link Track} object identifying the track to browse.
	 * 
	 * @return A {@link Track} object or null on failure.
	 * 
	 * @see Track
	 */
	public Track browse(Track track) throws TimeoutException;
	
	/**
	 * Browse information for multiple tracks.
	 * 
	 * @param tracks A {@link List} of {@link Track} objects identifying
	 *               the tracks to browse.
	 * 
	 * @return A list of {@link Track} objects or null on failure.
	 * 
	 * @see Track
	 */
	public List<Track> browse(List<Track> tracks) throws TimeoutException;
	
	/**
	 * Browse artist info by id.
	 * 
	 * @param id An id identifying the artist to browse.
	 * 
	 * @return An {@link Artist} object holding more information about
	 *         the artist or null on failure.
	 * 
	 * @see Artist
	 */
	public Artist browseArtist(String id) throws TimeoutException;
	
	/**
	 * Browse album info by id.
	 * 
	 * @param id An id identifying the album to browse.
	 * 
	 * @return An {@link Album} object holding more information about
	 *         the album or null on failure.
	 * 
	 * @see Album
	 */
	public Album browseAlbum(String id) throws TimeoutException;
	
	/**
	 * Browse track info by id.
	 * 
	 * @param id An id identifying the track to browse.
	 * 
	 * @return A {@link Track} object or null on failure.
	 * 
	 * @see Track
	 */
	public Track browseTrack(String id) throws TimeoutException;
	
	/**
	 * Browse information for multiple tracks by id.
	 * 
	 * @param ids A {@link List} of ids identifying the tracks to browse.
	 * 
	 * @return A list of {@link Track} objects or null on failure.
	 * 
	 * @see Track
	 */
	public List<Track> browseTracks(List<String> ids) throws TimeoutException;
	
	/**
	 * Get stored user playlists.
	 * 
	 * @return A {@link PlaylistContainer} holding {@link Playlist} objects
	 *         or an empty {@link PlaylistContainer} on failure.
	 *         Note: {@link Playlist} objects only hold id and author and need
	 *         to be loaded using {@link #playlist(String)}.
	 * 
	 * @see PlaylistContainer
	 */
	public PlaylistContainer playlistContainer() throws TimeoutException;
	
	/**
	 * Add a playlist to a playlist container.
	 * 
	 * @param playlistContainer A {@link PlaylistContainer} to add the playlist to.
	 * @param playlist          The {@link Playlist} to be added.
	 * 
	 * @return true on success and false on failure.
	 * 
	 * @see PlaylistContainer
	 */
	public boolean playlistContainerAddPlaylist(PlaylistContainer playlistContainer, Playlist playlist) throws TimeoutException;
	
	/**
	 * Add a playlist to a playlist container.
	 * 
	 * @param playlistContainer The playlist container.
	 * @param playlist          The playlist to be added.
	 * @param position          The target position of the added playlist.
	 * 
	 * @return true on success and false on failure.
	 */
	public boolean playlistContainerAddPlaylist(PlaylistContainer playlistContainer, Playlist playlist, int position) throws TimeoutException;
	
	/**
	 * Add multiple playlists to a playlist container.
	 * 
	 * @param playlistContainer The playlist container.
	 * @param playlists         A {@link List} of playlists to be added.
	 * @param position          The target position of the added playlists.
	 * 
	 * @return true on success and false on failure.
	 */
	public boolean playlistContainerAddPlaylists(PlaylistContainer playlistContainer, List<Playlist> playlists, int position) throws TimeoutException;
	
	/**
	 * Remove a playlist from a playlist container.
	 * 
	 * @param playlistContainer The playlist container.
	 * @param position          The position of the playlist to remove.
	 * 
	 * @return true on success and false on failure.
	 */
	public boolean playlistContainerRemovePlaylist(PlaylistContainer playlistContainer, int position) throws TimeoutException;
	
	/**
	 * Remove multiple playlists from a playlist container.
	 * 
	 * @param playlistContainer The playlist container.
	 * @param position          The position of the tracks to remove.
	 * @param count             The number of track to remove.
	 * 
	 * @return true on success and false on failure.
	 */
	public boolean playlistContainerRemovePlaylists(PlaylistContainer playlistContainer, int position, int count) throws TimeoutException;
	
	/**
	 * Get a playlist.
	 * 
	 * @param id     Id of the playlist to load.
	 * @param cached Whether to use a cached version if available or not.
	 * 
	 * @return A {@link Playlist} object or null on failure.
	 * 
	 * @see Playlist
	 */
	public Playlist playlist(String id, boolean cached) throws TimeoutException;
	
	/**
	 * Get a playlist.
	 * 
	 * @param id Id of the playlist to load.
	 * 
	 * @return A {@link Playlist} object or null on failure.
	 * 
	 * @see Playlist
	 */
	public Playlist playlist(String id) throws TimeoutException;
	
	/**
	 * Create a playlist.
	 * 
	 * @param name          The name of the playlist to create.
	 * @param collaborative If the playlist shall be collaborative.
	 * @param description   A description of the playlist.
	 * @param picture       An image id to associate with this playlist.
	 * 
	 * @return A {@link Playlist} object or null on failure.
	 * 
	 * @see Playlist
	 */
	public Playlist playlistCreate(String name, boolean collaborative, String description, String picture) throws TimeoutException;
	
	/**
	 * Create a playlist.
	 * 
	 * @param name The name of the playlist to create.
	 * 
	 * @return A {@link Playlist} object or null on failure.
	 * 
	 * @see Playlist
	 */
	public Playlist playlistCreate(String name) throws TimeoutException;
	
	/**
	 * Create a playlist from a given album.
	 * 
	 * @param sourceAlbum An {@link Album} object
	 * 
	 * @return A {@link Playlist} object or null on failure.
	 * 
	 * @see Playlist
	 * @see Album
	 */
	public Playlist playlistCreate(Album sourceAlbum) throws TimeoutException;
	
	/**
	 * Destroy a playlist (Note: It will not be destroyed on the server immediately).
	 * 
	 * @param playlist The playlist to destroy.
	 * 
	 * @return true if the playlist was successfully destroyed, false otherwise.
	 * 
	 * @see Playlist
	 */
	public boolean playlistDestroy(Playlist playlist) throws TimeoutException;
	
	/**
	 * Add a track to a playlist.
	 * 
	 * @param playlist The playlist.
	 * @param track    The track to be added.
	 * 
	 * @return true on success and false on failure.
	 */
	public boolean playlistAddTrack(Playlist playlist, Track track) throws TimeoutException;
	
	/**
	 * Add a track to a playlist.
	 * 
	 * @param playlist The playlist.
	 * @param track    The track to be added.
	 * @param position The target position of the added track.
	 * 
	 * @return true on success and false on failure.
	 */
	public boolean playlistAddTrack(Playlist playlist, Track track, int position) throws TimeoutException;
	
	/**
	 * Add multiple tracks to a playlist.
	 * 
	 * @param playlist The playlist.
	 * @param tracks   A {@link List} of tracks to be added.
	 * @param position The target position of the added track.
	 * 
	 * @return true on success and false on failure.
	 */
	public boolean playlistAddTracks(Playlist playlist, List<Track> tracks, int position) throws TimeoutException;
	
	/**
	 * Remove a track from a playlist.
	 * 
	 * @param playlist The playlist.
	 * @param position The position of the track to remove.
	 * 
	 * @return true on success and false on failure.
	 */
	public boolean playlistRemoveTrack(Playlist playlist, int position) throws TimeoutException;
	
	/**
	 * Remove multiple tracks from a playlist.
	 * 
	 * @param playlist The playlist.
	 * @param position The position of the tracks to remove.
	 * @param count    The number of track to remove.
	 * 
	 * @return true on success and false on failure.
	 */
	public boolean playlistRemoveTracks(Playlist playlist, int position, int count) throws TimeoutException;
	
	/**
	 * Rename a playlist.
	 * 
	 * @param playlist The {@link Playlist} to rename.
	 * @param name     The new name for the playlist.
	 * 
	 * @return true on success or false on failure.
	 * 
	 * @see Playlist
	 */
	public boolean playlistRename(Playlist playlist, String name) throws TimeoutException;
	
	/**
	 * Set playlist collaboration.
	 * 
	 * @param playlist      The {@link Playlist} to change.
	 * @param collaborative Whether it should be collaborative or not.
	 * 
	 * @return true on success or false on failure.
	 * 
	 * @see Playlist
	 */
	public boolean playlistSetCollaborative(Playlist playlist, boolean collaborative) throws TimeoutException;
	
	/**
	 * Set playlist information.
	 * 
	 * @param playlist    The {@link Playlist} to change.
	 * @param description The description to set.
	 * @param picture     The picture to set.
	 * 
	 * @return true on success or false on failure.
	 * 
	 * @see Playlist
	 */
	public boolean playlistSetInformation(Playlist playlist, String description, String picture) throws TimeoutException;
} public class JotifyApplication {
	/* Application frame and broadcast. */
	private JotifyFrame       frame;
	private JotifyBroadcast   broadcast;
	private JotifyPreferences settings;
	private JotifyPlayer      player;
	private TrayIcon          trayIcon;
	
	/* Jotify API. */
	private final Jotify jotify;
	
	public JotifyApplication(final Jotify jotify){
		this.broadcast = JotifyBroadcast.getInstance();
		this.frame     = null; /* Created in initialize method. */
		this.settings  = null; /* Created in initialize method. */
		this.player    = null; /* Created in initialize method. */
		this.jotify    = jotify;
		this.trayIcon  = null; /* Created in initialize method. */
	}
	
	public void initialize(){
		/* Load settings. */
		this.settings = JotifyPreferences.getInstance();
		this.settings.load();
		
		/* Get login credentials from settings. */
		final JotifyLoginCredentials credentials = this.settings.getLoginCredentials();
		
		/* Show login dialog. */
		JotifyLoginDialog.showDialog();
		
		/* Login process. Repeat if login failed. */
		while(true){
			/* Ask for login credentials if they're not marked as remebmered. */
			if(!credentials.getRemember()){
				JotifyLoginDialog.getLoginCredentials(credentials);
			}
			/* If credentials are present, just show loader. */
			else{
				JotifyLoginDialog.setLoginCredentials(credentials);
			}
			
			/* Hide message and show loader. */
			JotifyLoginDialog.hideMessage();
			JotifyLoginDialog.showLoader();
			JotifyLoginDialog.updateDialog();
			
			try{
				/* Try to login by getting the default Jotify instance. */
				this.jotify.login(credentials.getUsername(), credentials.getPassword());
				
				/* Check if user is premium. */
				if(!this.jotify.user().isPremium()){
					this.jotify.close();
					
					throw new AuthenticationException("You need a premium account in order to use jotify!");
				}
				
				/* Create player. */
				this.player = new JotifyPlayer(this.jotify);
				
				this.broadcast.addControlListener(this.player);
				
				/* Create scrobbler if enabled. */
				if(this.settings.getBoolean("lastfm.enabled", false)){
					JotifyScrobbler scrobbler = new JotifyScrobbler(
						this.settings.getString("lastfm.username"),
						this.settings.getString("lastfm.password")
					);
					this.player.addPlaybackListener(scrobbler);
				}
				
				/* Save login credentials (they are valid!). */
				this.settings.setLoginCredentials(credentials);
				this.settings.save();
				
				/* Create and show main application frame. Center it on screen. */
				this.frame = new JotifyFrame(this.jotify);
				this.frame.setVisible(true);
				this.frame.setLocationRelativeTo(null);
				
				/* Add a system tray icon. */
				if(SystemTray.isSupported()){
					this.trayIcon = new TrayIcon(
						new ImageIcon(JotifyApplication.class.getResource("images/icon_128.png")).getImage(),
						"Jotify"
					);
					
					/* Add a popup menu to the system tray icon (it for some reason blocks the whole application when shown). */
					final PopupMenu menu       = new PopupMenu();
					final MenuItem  trackItem  = new MenuItem("No track playing");
					final MenuItem  openItem   = new MenuItem("Open Jotify");
					//final MenuItem  playItem   = new MenuItem("Play/Pause");
					//final MenuItem  nextItem   = new MenuItem("Next");
					//final MenuItem  prevItem   = new MenuItem("Previous");
					final MenuItem  logoutItem = new MenuItem("Logout '" + this.jotify.user().getName() + "'");
					final MenuItem  exitItem   = new MenuItem("Exit");
					
					trackItem.setEnabled(false);
					//nextItem.setEnabled(false);
					//prevItem.setEnabled(false);
					
					openItem.addActionListener(new ActionListener(){
						public void actionPerformed(ActionEvent e){
							frame.setExtendedState(frame.getExtendedState() &~ JFrame.ICONIFIED);
							frame.requestFocus();
						}
					});
					
					logoutItem.addActionListener(new ActionListener(){
						public void actionPerformed(ActionEvent e){
							credentials.setRemember(false);
							
							/* Save login credentials (they are valid!). */
							settings.setLoginCredentials(credentials);
							settings.save();
							
							System.exit(0);
						}
					});
					
					exitItem.addActionListener(new ActionListener(){
						public void actionPerformed(ActionEvent e){
							System.exit(0);
						}
					});
					
					menu.add(trackItem);
					menu.addSeparator();
					menu.add(openItem);
					//menu.addSeparator();
					//menu.add(playItem);
					//menu.add(nextItem);
					//menu.add(prevItem);
					menu.addSeparator();
					menu.add(logoutItem);
					menu.add(exitItem);
					
					this.player.addPlaybackListener(new PlaybackListener(){
						public void playbackFinished(Track track){}
						public void playbackPosition(Track track, int position){}
						public void playbackResumed(Track track){}
						public void playbackStarted(Track track){
							trackItem.setLabel(track.getArtist().getName() + " - " + track.getTitle());
						}
						public void playbackStopped(Track track){}
					});
					
					this.trayIcon.setImageAutoSize(true);
					this.trayIcon.setPopupMenu(menu);
					
					try{
						SystemTray.getSystemTray().add(this.trayIcon);
					}
					catch(AWTException e){
						/* Couldn't add system tray icon. */
					}
				}
				
				/* Load playlists in a separate thread. TODO: retry on fail. */
				new Thread("Playlist-Loading-Thread"){
					public void run(){
						/* Get information about account playlists. */
						PlaylistContainer playlists = null;
						boolean           cached    = true;
						
						while(true){
							try{
								playlists = jotify.playlistContainer();
								
								break;
							}
							catch(TimeoutException e){
								continue;
							}
						}
						
						/* Fire playlist added events. */
						for(Playlist playlist : playlists){
							broadcast.firePlaylistAdded(playlist);
						}
						
						/* Check for changes in playlists. */
						if(playlists.getRevision() > settings.getPlaylistsRevision()){
							settings.setPlaylistsRevision(playlists.getRevision());
							settings.save();
							
							cached = false;
						}
						
						/* Get details for each playlist. */
						for(Playlist playlist : playlists){
							/* Get playlist details. */
							while(true){
								try{
									playlist = jotify.playlist(playlist.getId(), cached);
									
									break;
								}
								catch(TimeoutException e){
									continue;
								}
							}
							
							/* If playlist contains tracks, browse for track information. */
							if(!playlist.getTracks().isEmpty()){
								int totalTracks = playlist.getTracks().size();
								int numTracks   = 200;
								int numRequests = (int)(totalTracks / numTracks) + 1;
								
								/* Browse for 200 tracks at a time tracks and add them to the playlist. */
								for(int i = 0; i < numRequests; i++){
									List<Track> tracks = null;
									
									while(true){
										try{
											tracks = jotify.browse(
												playlist.getTracks().subList(
													i * numTracks,
													Math.min((i + 1) * numTracks, totalTracks)
												)
											);
											
											break;
										}
										catch(TimeoutException e){
											continue;
										}
									}
									
									/* Add track information to playlist (also works with duplicate tracks). */
									for(Track track : tracks){
		  								for(int j = 0; j < playlist.getTracks().size(); j++){
		  									if(track.equals(playlist.getTracks().get(j))){
		  										playlist.getTracks().set(j, track);
		  									}
		  								}
		  							}
								}
							}
							
							/* Fire playlist updated events. */
							broadcast.firePlaylistUpdated(playlist);
						}
					}
				}.start();
				
				/* We're logged in now. Don't ask for credentials again. */
				break;
			}
			/* If we got a connection error, show it. */
			catch(ConnectionException e){
				if(e.getCause() != null){
					JotifyLoginDialog.showErrorMessage(e.getCause().getMessage());
				}
				else{
					JotifyLoginDialog.showErrorMessage(e.getMessage());
				}
				
				JotifyLoginDialog.hideLoader();
				JotifyLoginDialog.updateDialog();
			}
			/* If we got an authentication error, show it. */
			catch(AuthenticationException e){
				if(e.getCause() != null){
					JotifyLoginDialog.showErrorMessage(e.getCause().getMessage());
				}
				else{
					JotifyLoginDialog.showErrorMessage(e.getMessage());
				}
				
				JotifyLoginDialog.hideLoader();
				JotifyLoginDialog.updateDialog();
				
				/* Don't check "Remember me", if login failed. */
				credentials.setRemember(false);
			}
			/* If we got a timeout. */
			catch(TimeoutException e){
				JotifyLoginDialog.showErrorMessage(e.getMessage());
				
				JotifyLoginDialog.hideLoader();
				JotifyLoginDialog.updateDialog();
			}
		}
		
		/* Hide login dialog. */
		JotifyLoginDialog.hideDialog();
	}
	
	/* Main entry point of program. Create application and initialize it. */
	public static void main(String[] args) throws Exception {
		JotifyApplication application = new JotifyApplication(new JotifyPool());
		
		application.initialize();
	}
} public class JotifyPlaybackQueue {
	private LinkedList<Track> queue;
	private LinkedList<Track> tracks;
	private LinkedList<Track> history;
	private Track             current;
	
	public JotifyPlaybackQueue(){
		this.queue   = new LinkedList<Track>();
		this.tracks  = new LinkedList<Track>();
		this.history = new LinkedList<Track>();
		this.current = null;
	}
	
	public void queue(Track track){
		this.queue.add(track);
	}
	
	public void add(Track track){
		this.tracks.add(track);
	}
	
	public void addAll(List<Track> tracks){
		this.tracks.addAll(tracks);
	}
	
	public void clear(){
		this.tracks.clear();
		
		this.current = null;
	}
	
	public void clearAll(){
		this.queue.clear();
		this.tracks.clear();
		this.history.clear();
		
		this.current = null;
	}
	
	public void shuffle(){
		Collections.shuffle(this.tracks);
	}
	
	public boolean hasCurrent(){
		return this.current != null;
	}
	
	public boolean hasNext(){
		return !this.queue.isEmpty() || !this.tracks.isEmpty();
	}
	
	public boolean hasPrevious(){
		return !this.history.isEmpty();
	}
	
	public Track current(){
		return this.current;
	}
	
	public Track next(){
		if(this.current != null){
			this.history.addFirst(this.current);
		}
		
		if(!this.queue.isEmpty()){
			this.current = this.queue.removeFirst();
		}
		else if(!this.tracks.isEmpty()){
			this.current = this.tracks.removeFirst();
		}
		else{
			this.current = null;
		}
		
		return this.current;
	}
	
	public Track previous(){
		if(this.current != null){
			this.tracks.addFirst(this.current);
		}
		
		if(!this.history.isEmpty()){
			this.current = this.history.removeFirst();
		}
		else{
			this.current = null;
		}
		
		return this.current;
	}
	
	public List<Track> getQueue(){
		return this.queue;
	}
	
	public List<Track> getTracks(){
		return this.tracks;
	}
	
	public List<Track> getHistory(){
		return this.history;
	}
} public interface BrowseListener {
	public void browsedArtist(Artist artist);
	public void browsedAlbum(Album album);
	public void browsedTracks(Result result);
} public class JotifyBroadcast {
	private List<PlaylistListener> playlistListeners;
	private List<QueueListener>    queueListeners;
	private List<SearchListener>   searchListeners;
	private List<BrowseListener>   browseListeners;
	private List<ControlListener>  controlListeners;
	private List<PlayerListener>   playerListeners;
	
	private List<ClearSelectionListener> clearSelectionListener;
	
	private static JotifyBroadcast instance;
	
	static {
		instance = new JotifyBroadcast();
	}
	
	public static JotifyBroadcast getInstance(){
		return instance;
	}
	
	private JotifyBroadcast(){
		this.playlistListeners = new ArrayList<PlaylistListener>();
		this.queueListeners    = new ArrayList<QueueListener>();
		this.searchListeners   = new ArrayList<SearchListener>();
		this.browseListeners   = new ArrayList<BrowseListener>();
		this.controlListeners  = new ArrayList<ControlListener>();
		this.playerListeners   = new ArrayList<PlayerListener>();

		this.clearSelectionListener = new ArrayList<ClearSelectionListener>();
	}
	
	public void addPlaylistListener(PlaylistListener listener){
		this.playlistListeners.add(listener);
	}
	
	public void addQueueListener(QueueListener listener){
		this.queueListeners.add(listener);
	}
	
	public void addSearchListener(SearchListener listener){
		this.searchListeners.add(listener);
	}
	
	public void addBrowseListener(BrowseListener listener){
		this.browseListeners.add(listener);
	}
	
	public void addControlListener(ControlListener listener){
		this.controlListeners.add(listener);
	}
	
	public void addPlayerListener(PlayerListener listener){
		this.playerListeners.add(listener);
	}
	
	public void addClearSelectionListener(ClearSelectionListener listener){
		this.clearSelectionListener.add(listener);
	}
	
	public void firePlaylistAdded(Playlist playlist) {
		for(PlaylistListener listener : this.playlistListeners){
			listener.playlistAdded(playlist);
		}
	}
	
	public void firePlaylistRemoved(Playlist playlist) {
		for(PlaylistListener listener : this.playlistListeners){
			listener.playlistRemoved(playlist);
		}
	}
	
	public void firePlaylistUpdated(Playlist playlist) {
		for(PlaylistListener listener : this.playlistListeners){
			listener.playlistUpdated(playlist);
		}
	}
	
	public void firePlaylistSelected(Playlist playlist) {
		for(PlaylistListener listener : this.playlistListeners){
			listener.playlistSelected(playlist);
		}
	}
	
	public void fireQueueSelected(JotifyPlaybackQueue queue) {
		for(QueueListener listener : this.queueListeners){
			listener.queueSelected(queue);
		}
	}
	
	public void fireQueueUpdated(JotifyPlaybackQueue queue) {
		for(QueueListener listener : this.queueListeners){
			listener.queueUpdated(queue);
		}
	}
	
	public void fireSearchResultReceived(Result result){
		for(SearchListener listener : this.searchListeners){
			listener.searchResultReceived(result);
		}
	}
	
	public void fireSearchResultSelected(Result result){
		for(SearchListener listener : this.searchListeners){
			listener.searchResultSelected(result);
		}
	}
	
	public void fireBrowsedArtist(Artist artist){
		for(BrowseListener listener : this.browseListeners){
			listener.browsedArtist(artist);
		}
	}
	
	public void fireBrowsedAlbum(Album album){
		for(BrowseListener listener : this.browseListeners){
			listener.browsedAlbum(album);
		}
	}
	
	public void fireBrowsedTracks(Result result){
		for(BrowseListener listener : this.browseListeners){
			listener.browsedTracks(result);
		}
	}
	
	public void fireControlPlay(){
		for(ControlListener listener : this.controlListeners){
			listener.controlPlay();
		}
	}
	
	public void fireControlPause(){
		for(ControlListener listener : this.controlListeners){
			listener.controlPause();
		}
	}
	
	public void fireControlPrevious(){
		for(ControlListener listener : this.controlListeners){
			listener.controlPrevious();
		}
	}
	
	public void fireControlNext(){
		for(ControlListener listener : this.controlListeners){
			listener.controlNext();
		}
	}
	
	public void fireControlVolume(float volume){
		for(ControlListener listener : this.controlListeners){
			listener.controlVolume(volume);
		}
	}
	
	public void fireControlSeek(float percent){
		for(ControlListener listener : this.controlListeners){
			listener.controlSeek(percent);
		}
	}
	
	public void fireControlSelect(Track track){
		for(ControlListener listener : this.controlListeners){
			listener.controlSelect(track);
		}
	}
	
	public void fireControlSelect(List<Track> tracks){
		for(ControlListener listener : this.controlListeners){
			listener.controlSelect(tracks);
		}
	}
	
	public void fireControlQueue(Track track){
		for(ControlListener listener : this.controlListeners){
			listener.controlQueue(track);
		}
	}
	
	public void firePlayerTrackChanged(Track track){
		for(PlayerListener listener : this.playerListeners){
			listener.playerTrackChanged(track);
		}
	}
	
	public void firePlayerStatusChanged(Status status){
		for(PlayerListener listener : this.playerListeners){
			listener.playerStatusChanged(status);
		}
	}
	
	public void firePlayerPositionChanged(int position){
		for(PlayerListener listener : this.playerListeners){
			listener.playerPositionChanged(position);
		}
	}
	
	public void fireClearSelection(){
		for(ClearSelectionListener listener : this.clearSelectionListener){
			listener.clearSelection();
		}
	}
} public interface PlaylistListener {
	public void playlistAdded(Playlist playlist);
	public void playlistUpdated(Playlist playlist);
	public void playlistRemoved(Playlist playlist);
	public void playlistSelected(Playlist playlist);
} public interface SearchListener {
	public void searchResultReceived(Result result);
	public void searchResultSelected(Result result);
} @SuppressWarnings("serial")
public class JotifyTableModel<T extends Track> extends AbstractTableModel {
	protected List<Column> columns;
	protected List<T>      rows;
	
	public JotifyTableModel(){
		this.columns = new ArrayList<Column>();
		this.rows    = new ArrayList<T>();
		
		/* Add columns. */		
		this.columns.add(new Column("Track",      String.class));
		this.columns.add(new Column("Artist",     String.class));
		this.columns.add(new Column("Time",       String.class));
		this.columns.add(new Column("Popularity", JotifyPopularityIndicator.class));
		this.columns.add(new Column("Album",      String.class));
	}
	
	public int getColumnCount(){
		return this.columns.size();
	}
	
	public int getRowCount(){
		return this.rows.size();
	}
	
	public void add(T element){
		int row = this.rows.size();
		
		if(this.rows.add(element)){
			this.fireTableRowsInserted(row, row);
		}
	}
	
	public void addAll(Collection<T> elements){
		int row = this.rows.size();
		
		if(this.rows.addAll(elements)){
			this.fireTableRowsInserted(row, row + elements.size() - 1);
		}
	}
	
	public void remove(T element){
		int row = this.rows.indexOf(element);
		
		this.remove(row);
	}
	
	public void remove(int row){
		if(this.rows.remove(row) != null){
			this.fireTableRowsDeleted(row, row);
		}
	}
	
	public void removeAll(){
		this.rows.clear();
		
		this.fireTableDataChanged();
	}
	
	public T get(int row){
		return this.rows.get(row);
	}
	
	public List<T> getSubList(int from, int to){
		return this.rows.subList(from, to);
	}
	
	public List<T> getAll(){
		return this.rows;
	}
	
	public Object getValueAt(int row, int col){
	  if (this.rows.size() < row) {
	    return null;
	  }
	  
		T element = this.rows.get(row);
		
		switch(col){
			case 0:
				return element.getTitle();
			case 1:
				if(element.getArtist() != null){
					return element.getArtist().getName();
				}
				else{
					return null;
				}
			case 2:
				return TimeFormatter.formatSeconds(element.getLength() / 1000);
			case 3:
				return element.getPopularity();
			case 4:
				if(element.getAlbum() != null){
					return element.getAlbum().getName();
				}
				else{
					return null;
				}
			default:
				return null;
		}
	}
	
	public Class<?> getColumnClass(int col){
		return this.columns.get(col).getColumnClass();
	}
	
	public String getColumnName(int col){
		return this.columns.get(col).getName();
	}
	
	public int findColumn(String name){
		return this.columns.indexOf(name);
	}
	
	public boolean isCellEditable(int row, int col){
		return false;
	}
	
	private class Column {
		private String   name;
		private Class<?> columnClass;
		
		public Column(String name, Class<?> columnClass){
			this.name        = name;
			this.columnClass = columnClass;
		}
		
		public String getName(){
			return this.name;
		}
		
		public Class<?> getColumnClass(){
			return this.columnClass;
		}
	}
} public class TrackListTransferable implements Transferable {
	public static final DataFlavor TRACKLIST_FLAVOR = new DataFlavor(List.class, "Jotify track list");
	
	private final List<Track> trackList;
	
	public TrackListTransferable(final List<Track> trackList){
		this.trackList = trackList;
	}
	
	public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
		if(flavor.equals(DataFlavor.stringFlavor)){
			String ids = "";
			
			for(int i = 0; i < this.trackList.size(); i++){
				ids += ((i > 0)?"\n":"") + this.trackList.get(i).getId();
			}
			
			return ids;
		}
		
		if(flavor.equals(TRACKLIST_FLAVOR)){
			return this.trackList;
		}
		
		throw new UnsupportedFlavorException(flavor);
	}
	
	public DataFlavor[] getTransferDataFlavors(){
		return new DataFlavor[]{DataFlavor.stringFlavor, TRACKLIST_FLAVOR};
	}
	
	public boolean isDataFlavorSupported(DataFlavor flavor){
		return flavor.equals(TRACKLIST_FLAVOR) || flavor.equals(DataFlavor.stringFlavor);
	}
} public class Album extends Media {
	/**
	 * Name of this album.
	 */
	private String name;
	
	/**
	 * Artist of this album.
	 */
	private Artist artist;
	
	/**
	 * The identifier for this albums cover image (32-character string).
	 */
	private String cover;
	
	/**
	 * The type of this album (compilation, album, single).
	 */
	private String type;
	
	/**
	 * The review of this album.
	 */
	private String review;
	
	/**
	 * Release year of this album.
	 */
	private int year;
	
	/**
	 * A {@link List} of discs of this album.
	 * 
	 * @see Disc
	 */
	private List<Disc> discs;
	
	/**
	 * Similar albums of this album.
	 */
	private List<Album> similarAlbums;
	
	/**
	 * Creates an empty {@link Album} object.
	 */
	public Album(){
		this.name          = null;
		this.artist        = null;
		this.cover         = null;
		this.type          = null;
		this.review        = null;
		this.year          = -1;
		this.discs         = new ArrayList<Disc>();
		this.similarAlbums = new ArrayList<Album>();
	}
	
	/**
	 * Creates an {@link Album} object with the specified {@code id}.
	 * 
	 * @param id A 32-character hex string or a Spotify URI.
	 */
	public Album(String id){
		this(id, null, null);
	}
	
	/**
	 * Creates an {@link Album} object with the specified {@code id}, {@code name} and {@code artist}.
	 * 
	 * @param id     A 32-character hex string or a Spotify URI.
	 * @param name   Name of the album.
	 * @param artist Artist of the album.
	 */
	public Album(String id, String name, Artist artist){
		super(id);
		
		/* Set object properties. */
		this.name          = name;
		this.artist        = artist;
		this.cover         = null;
		this.type          = null;
		this.review        = null;
		this.year          = -1;
		this.discs         = new ArrayList<Disc>();
		this.similarAlbums = new ArrayList<Album>();
	}
	
	/**
	 * Create a link from this album.
	 * 
	 * @return A {@link Link} object which can then
	 * 		   be used to retreive the Spotify URI.
	 */
	public Link getLink(){
		return Link.create(this);
	}
	
	/**
	 * Get the albums name.
	 * 
	 * @return The name of this album.
	 */
	public String getName(){
		return this.name;
	}
	
	/**
	 * Set the albums name.
	 * 
	 * @param name The desired name of this album.
	 */
	public void setName(String name){
		this.name = name;
	}
	
	/**
	 * Get the albums artist.
	 * 
	 * @return An {@link Artist} object.
	 */
	public Artist getArtist(){
		return this.artist;
	}
	
	/**
	 * Set the albums artist.
	 * 
	 * @param artist The desired {@link Artist} of this album.
	 */
	public void setArtist(Artist artist){
		this.artist = artist;
	}
	
	/**
	 * Get the albums cover image identifier.
	 * 
	 * @return A 32-character image identifier.
	 */
	public String getCover(){
		return this.cover;
	}
	
	/**
	 * Set the albums cover image identifier.
	 * 
	 * @param cover A 40-character image identifier.
	 */
	public void setCover(String cover){
		/* Check if portrait id string is valid. */
		if(cover == null || cover.length() != 40 || !Hex.isHex(cover)){
			throw new IllegalArgumentException("Expecting a 40-character hex string.");
		}
		
		this.cover = cover;
	}
	
	/**
	 * Get the albums type.
	 * 
	 * @return One of "compilation", "album" or "single".
	 */
	public String getType(){
		return this.type;
	}
	
	/**
	 * Set the albums type.
	 * 
	 * @param type One of "compilation", "album" or "single".
	 */
	public void setType(String type){
		this.type = type;
	}
	
	/**
	 * Get the albums review.
	 * 
	 * @return A review of this album.
	 */
	public String getReview(){
		return this.review;
	}
	
	/**
	 * Set the albums review.
	 * 
	 * @param review A review of this album.
	 */
	public void setReview(String review){
		this.review = review;
	}
	
	/**
	 * Get the albums release year.
	 * 
	 * @return An integer denoting the release year or -1 if not available.
	 */
	public int getYear(){
		return this.year;
	}
	
	/**
	 * Set the albums release year.
	 * 
	 * @param year A positive integer denoting the release year.
	 */
	public void setYear(int year){
		/* Check if year is valid. Years B.C. are not supported :-P */
		if(year < 0){
			throw new IllegalArgumentException("Expecting a positive year.");
		}
		
		this.year = year;
	}
	
	/**
	 * Get discs of this album.
	 * 
	 * @return A {@link List} of {@link Disc} objects.
	 */
	public List<Disc> getDiscs(){
		return this.discs;
	}
	
	/**
	 * Set discs of this album.
	 * 
	 * @param discs A {@link List} of {@link Disc} objects.
	 */
	public void setDiscs(List<Disc> discs){
		this.discs = discs;
	}
	
	/**
	 * Set discs for this album.
	 * 
	 * @param discs A {@link List} of {@link Disc} objects.
	 */
	public void setTracks(List<Disc> discs){
		this.discs = discs;
	}
	
	/**
	 * Get tracks of this album. This automatically merges
	 * tracks from this albums discs.
	 * 
	 * @return A {@link List} of {@link Track} objects.
	 */
	public List<Track> getTracks(){
		List<Track> tracks = new ArrayList<Track>();
		
		for(Disc disc : this.discs){
			tracks.addAll(disc.getTracks());
		}
		
		return tracks;
	}
	
	/**
	 * Get similar albums for this album.
	 * 
	 * @return A {@link List} of {@link Album} objects.
	 */
	public List<Album> getSimilarAlbums(){
		return this.similarAlbums;
	}
	
	/**
	 * Set similar albums for this album.
	 * 
	 * @param similarAlbums A {@link List} of {@link Album} objects.
	 */
	public void setSimilarAlbums(List<Album> similarAlbums){
		this.similarAlbums = similarAlbums;
	}
	
	/**
	 * Determines if an object is equal to this {@link Album} object.
	 * If both objects are {@link Album} objects, it will compare their identifiers.
	 * 
	 * @param o Another object to compare.
	 * 
	 * @return true of the objects are equal, false otherwise.
	 */
	public boolean equals(Object o){
		if(o instanceof Album){
			Album a = (Album)o;
			
			if(this.id.equals(a.id)){
				return true;
			}
			
			for(String id : this.redirects){
				if(id.equals(a.id)){
					return true;
				}
			}
		}
		
		return false;
	}
	
	/**
	 * Return the hash code of this {@link Album} object. This will give the value returned
	 * by the {@code hashCode} method of the identifier string.
	 * 
	 * @return The {@link Album} objects hash code.
	 */
	public int hashCode(){
		return (this.id != null) ? this.id.hashCode() : 0;
	}
	
	public String toString(){
		return String.format("[Album: %s, %s]", this.artist, this.name);
	}
} public class Artist extends Media {
	/**
	 * Name of this artist.
	 */
	private String name;
	
	/**
	 * The identifier for this artists portrait image (40-character string).
	 */
	private String portrait;
	
	/**
	 * A {@link List} of genres.
	 */
	private List<String> genres;
	
	/**
	 * A {@link List} of years active.
	 */
	private List<String> yearsActive;
	
	/**
	 * A {@link List} of biographies.
	 */
	private List<Biography> bios;
	
	/**
	 * A {@link List} of albums.
	 */
	private List<Album> albums;
	
	/**
	 * A {@link List} of similar artists.
	 */
	private List<Artist> similarArtists;
	
	/**
	 * Creates an empty {@link Artist} object.
	 */
	public Artist(){
		this.name           = null;
		this.portrait       = null;
		this.genres         = new ArrayList<String>();
		this.yearsActive    = new ArrayList<String>();
		this.bios           = new ArrayList<Biography>();
		this.albums         = new ArrayList<Album>();
		this.similarArtists = new ArrayList<Artist>();
	}
	
	/**
	 * Creates an {@link Artist} object with the specified {@code id}.
	 * 
	 * @param id A 32-character hex string or a Spotify URI.
	 */
	public Artist(String id){
		this(id, null);
	}
	
	/**
	 * Creates an {@link Artist} object with the specified {@code id} and {@code name}.
	 * 
	 * @param id   A 32-character hex string or a Spotify URI.
	 * @param name Name of the artist or {@code null}.
	 */
	public Artist(String id, String name){
		super(id);
		
		/* Set object properties. */
		this.name           = name;
		this.portrait       = null;
		this.genres         = new ArrayList<String>();
		this.yearsActive    = new ArrayList<String>();
		this.bios           = new ArrayList<Biography>();
		this.similarArtists = new ArrayList<Artist>();
		this.albums         = new ArrayList<Album>();
	}
	
	/**
	 * Create a link from this artist.
	 * 
	 * @return A {@link Link} object which can then
	 * 		   be used to retreive the Spotify URI.
	 */
	public Link getLink(){
		return Link.create(this);
	}
	
	/**
	 * Get the artists name.
	 * 
	 * @return The name of this artist.
	 */
	public String getName(){
		return this.name;
	}
	
	/**
	 * Set the artists name.
	 * 
	 * @param name The desired name of this artist.
	 */
	public void setName(String name){
		this.name = name;
	}
	
	/**
	 * Get the artists portrait image identifier.
	 * 
	 * @return A 40-character hex string.
	 */
	public String getPortrait(){
		return this.portrait;
	}
	
	/**
	 * Set the artists portrait image identifier.
	 * 
	 * @param portrait A 40-character hex string.
	 */
	public void setPortrait(String portrait){
		this.portrait = portrait;
	}
	
	/**
	 * Get genres for this artist.
	 * 
	 * @return A {@link List} of genres.
	 */
	public List<String> getGenres(){
		return this.genres;
	}
	
	/**
	 * Set genres for this artist.
	 * 
	 * @param genres A {@link List} of genres.
	 */
	public void setGenres(List<String> genres){
		this.genres = genres;
	}
	
	/**
	 * Get active years for this artist.
	 * 
	 * @return A {@link List} of active years.
	 */
	public List<String> getYearsActive(){
		return this.yearsActive;
	}
	
	/**
	 * Set active years for this artist.
	 * 
	 * @param yearsActive A {@link List} of active years.
	 */
	public void setYearsActive(List<String> yearsActive){
		this.yearsActive = yearsActive;
	}
	
	/**
	 * Get biographies for this artist.
	 * 
	 * @return A {@link List} of {@link Biography} objects.
	 */
	public List<Biography> getBios(){
		return this.bios;
	}
	
	/**
	 * Set biographies for this artist.
	 * 
	 * @param bios A {@link List} of {@link Biography} objects.
	 */
	public void setBios(List<Biography> bios){
		this.bios = bios;
	}
	
	/**
	 * Get albums for this artist.
	 * 
	 * @return A {@link List} of {@link Album} objects.
	 */
	public List<Album> getAlbums(){
		return this.albums;
	}
	
	/**
	 * Set albums for this artist.
	 * 
	 * @param albums A {@link List} of {@link Album} objects.
	 */
	public void setAlbums(List<Album> albums){
		this.albums = albums;
	}
	
	/**
	 * Get similar artists for this artist.
	 * 
	 * @return A {@link List} of {@link Artist} objects.
	 */
	public List<Artist> getSimilarArtists(){
		return this.similarArtists;
	}
	
	/**
	 * Set similar artists for this artist.
	 * 
	 * @param similarArtists A {@link List} of {@link Artist} objects.
	 */
	public void setSimilarArtists(List<Artist> similarArtists){
		this.similarArtists = similarArtists;
	}
	
	/**
	 * Determines if an object is equal to this {@link Artist} object.
	 * If both objects are {@link Artist} objects, it will compare their identifiers.
	 * 
	 * @param o Another object to compare.
	 * 
	 * @return true of the objects are equal, false otherwise.
	 */
	public boolean equals(Object o){
		if(o instanceof Artist){
			Artist a = (Artist)o;
			
			if(this.id.equals(a.id)){
				return true;
			}
			
			for(String id : this.redirects){
				if(id.equals(a.id)){
					return true;
				}
			}
		}
		
		return false;
	}
	
	/**
	 * Return the hash code of this {@link Artist} object. This will give the value returned
	 * by the {@code hashCode} method of the identifier string.
	 * 
	 * @return The {@link Artist} objects hash code.
	 */
	public int hashCode(){
		return (this.id != null) ? this.id.hashCode() : 0;
	}
	
	public String toString(){
		return String.format("[Artist: %s]", this.name);
	}
} public class Link {
	/**
	 * Different possible link types.
	 */
	public enum Type {
		ARTIST, ALBUM, TRACK, PLAYLIST, SEARCH;
		
		/**
		 * Returns the lower-case name of this enum constant.
		 * 
		 * @return The lower-case name of this enum constant.
		 */
		public String toString(){
			 return this.name().toLowerCase();
		}
	}
	
	/**
	 * An exception that is thrown if parsing a Spotify URI fails. 
	 */
	public class InvalidSpotifyURIException extends Exception {
		private static final long serialVersionUID = 1L;
	}
	
	/**
	 * A regular expression to match artist, album and track URIs:
	 * 
	 * <pre>spotify:(artist|album|track):([0-9A-Za-z]{22})</pre>
	 */
	private static final Pattern mediaPattern = Pattern.compile("spotify:(artist|album|track):([0-9A-Za-z]{22})");
	
	/**
	 * A regular expression to match playlist URIs:
	 * 
	 * <pre>spotify:user:([^:]+):playlist:([0-9A-Za-z]{22})</pre>
	 */
	private static final Pattern playlistPattern = Pattern.compile("spotify:user:([^:]+):playlist:([0-9A-Za-z]{22})");
	
	/**
	 * A regular expression to match search URIs:
	 * 
	 * <pre>spotify:search:([^\\s]+)</pre>
	 */
	private static final Pattern searchPattern = Pattern.compile("spotify:search:([^\\s]+)");
	
	/**
	 * The {@link Link.Type} of this link.
	 */
	private Type type;
	
	/**
	 * The id of this link, if it's an artist, album or track link.
	 */
	private String id;
	
	/**
	 * The user of a playlist link.
	 */
	private String user;
	
	/**
	 * The search query of a search link.
	 */
	private String query;
	
	/**
	 * Create a {@link Link} using the given parameters.
	 * 
	 * @param type The {@link Link.Type} to use.
	 * @param id   The id to set (for artists, albums and tracks) or {@code null}.
	 * @param user The user to set (for playlists) or {@code null}.
	 * @param query The search query to set (for search) or {@code null}.
	 * 
	 * @return A {@link Link} object.
	 * 
	 * @throws InvalidSpotifyURIException If the Spotify URI is invalid.
	 */
	private Link(Type type, String id, String user, String query){
		this.type  = type;
		this.id    = id;
		this.user  = user;
		this.query = query;
	}
	
	/**
	 * Create a {@link Link} from a Spotify URI.
	 * 
	 * @param uri A Spotify URI to parse.
	 * 
	 * @return A {@link Link} object.
	 * 
	 * @throws InvalidSpotifyURIException If the Spotify URI is invalid.
	 */
	private Link(String uri) throws InvalidSpotifyURIException {
		/* Regex for matching Spotify URIs. */
		Matcher mediaMatcher    = mediaPattern.matcher(uri);
		Matcher playlistMatcher = playlistPattern.matcher(uri);
		Matcher searchMatcher   = searchPattern.matcher(uri);
		
		/* Check if URI matches artist/album/track pattern. */
		if(mediaMatcher.matches()){
			String type = mediaMatcher.group(1);
			
			if(type.equals("artist")){
				this.type = Type.ARTIST;
			}
			else if(type.equals("album")){
				this.type = Type.ALBUM;
			}
			else if(type.equals("track")){
				this.type = Type.TRACK;
			}
			else{
				throw new InvalidSpotifyURIException();
			}
			
			this.id = Link.toHex(mediaMatcher.group(2));
		}
		/* Check if URI matches playlist pattern. */
		else if(playlistMatcher.matches()){
			this.type = Type.PLAYLIST;
			this.user = playlistMatcher.group(1);
			this.id   = Link.toHex(playlistMatcher.group(2));
		}
		/* Check if URI matches search pattern. */
		else if(searchMatcher.matches()){
			this.type = Type.SEARCH;
			
			try{
				this.query = URLDecoder.decode(searchMatcher.group(1), "UTF-8");
			}
			catch(UnsupportedEncodingException e){
				throw new InvalidSpotifyURIException();
			}
		}
		/* If nothing was matched. */
		else{
			throw new InvalidSpotifyURIException();
		}
	}
	
	/**
	 * Get the {@link Link.Type} of this link.
	 * 
	 * @return A {@link Link.Type}.
	 */
	public Type getType(){
		return this.type;
	}
	
	/**
	 * Check if this link is an artist link.
	 * 
	 * @return true if this link is an artist link, false otherwise.
	 */
	public boolean isArtistLink(){
		return this.type.equals(Type.ARTIST);
	}
	
	/**
	 * Check if this link is an album link.
	 * 
	 * @return true if this link is an album link, false otherwise.
	 */
	public boolean isAlbumLink(){
		return this.type.equals(Type.ALBUM);
	}
	
	/**
	 * Check if this link is a track link.
	 * 
	 * @return true if this link is a track link, false otherwise.
	 */
	public boolean isTrackLink(){
		return this.type.equals(Type.TRACK);
	}
	
	/**
	 * Check if this link is a playlist link.
	 * 
	 * @return true if this link is a playlist link, false otherwise.
	 */
	public boolean isPlaylistLink(){
		return this.type.equals(Type.PLAYLIST);
	}
	
	/**
	 * Check if this link is a search link.
	 * 
	 * @return true if this link is a search link, false otherwise.
	 */
	public boolean isSearchLink(){
		return this.type.equals(Type.SEARCH);
	}
	
	/**
	 * Get the id of this link.
	 * 
	 * @return A 32-character hex id.
	 * 
	 * @throws IllegalStateException If the link doesn't have an id.
	 */
	public String getId(){
		if(this.id == null){
			throw new IllegalStateException("Link doesn't have an id!");
		}
		
		return this.id;
	}
	
	/**
	 * Get the user of this playlist link.
	 * 
	 * @return A username.
	 * 
	 * @throws IllegalStateException If the link is not a playlist link.
	 */
	public String getUser(){
		if(!this.isPlaylistLink()){
			throw new IllegalStateException("Link is not a playlist link!");
		}
		
		return this.user;
	}
	
	/**
	 * Get the query of this search link.
	 * 
	 * @return A search query.
	 * 
	 * @throws IllegalStateException If the link is not a search link.
	 */
	public String getQuery(){
		if(!this.isSearchLink()){
			throw new IllegalStateException("Link is not a search link!");
		}
		
		return this.query;
	}
	
	/**
	 * Returns the Spotify URI representation of this link.
	 * 
	 * @return A Spotify URI (or {@code null} if an
	 * 		   {@link UnsupportedEncodingException} occurs).
	 */
	public String asString(){
		if(this.isPlaylistLink()){
			return String.format(
				"spotify:user:%s:playlist:%s",
				this.user, Link.toBase62(this.id)
			);
		}
		else if(this.isSearchLink()){
			try{
				return String.format(
					"spotify:search:%s",
					URLEncoder.encode(this.query, "UTF-8")
				);
			}
			catch(UnsupportedEncodingException e){
				return null;
			}
		}
		else{
			return String.format(
				"spotify:%s:%s", this.type,
				Link.toBase62(this.id)
			);
		}
	}
	
	/**
	 * Returns the HTTP Spotify URI representation of this link.
	 * 
	 * @return A HTTP Spotify URI (or {@code null} if an
	 * 		   {@link UnsupportedEncodingException} occurs).
	 */
	public String asHTTPLink(){
		if(this.isPlaylistLink()){
			return String.format(
				"http://open.spotify.com/user/%s/playlist/%s",
				this.user, Link.toBase62(this.id)
			);
		}
		else if(this.isSearchLink()){
			try{
				return String.format(
					"http://open.spotify.com/search/%s",
					URLEncoder.encode(this.query, "UTF-8")
				);
			}
			catch(UnsupportedEncodingException e){
				return null;
			}
		}
		else{
			return String.format(
				"http://open.spotify.com/%s/%s",
				this.type, Link.toBase62(this.id)
			);
		}
	}
	
	/**
	 * Returns the string representation of this link.
	 * 
	 * @return A Spotify URI string (or {@code null} if an
	 * 		   {@link UnsupportedEncodingException} occurs).
	 */
	public String toString(){
		return this.asString();
	}
	
	/**
	 * Create a {@link Link} from a Spotify URI.
	 * 
	 * @param uri A Spotify URI to parse.
	 * 
	 * @return A {@link Link} object.
	 * 
	 * @throws InvalidSpotifyURIException If the Spotify URI is invalid.
	 */
	public static Link create(String uri) throws InvalidSpotifyURIException {
		return new Link(uri);
	}
	
	/**
	 * Create a {@link Link} from an {@link Artist}.
	 * 
	 * @param artist An {@link Artist} object.
	 * 
	 * @return A {@link Link} object.
	 */
	public static Link create(Artist artist){
		return new Link(Type.ARTIST, artist.getId(), null, null);
	}
	
	/**
	 * Create a {@link Link} from an {@link Album}.
	 * 
	 * @param album An {@link Album} object.
	 * 
	 * @return A {@link Link} object.
	 */
	public static Link create(Album album){
		return new Link(Type.ALBUM, album.getId(), null, null);
	}
	
	/**
	 * Create a {@link Link} from a {@link Track}.
	 * 
	 * @param track A {@link Track} object.
	 * 
	 * @return A {@link Link} object.
	 */
	public static Link create(Track track){
		return new Link(Type.TRACK, track.getId(), null, null);
	}
	
	/**
	 * Create a {@link Link} from a {@link Playlist}.
	 * 
	 * @param playlist A {@link Playlist} object.
	 * 
	 * @return A {@link Link} object.
	 */
	public static Link create(Playlist playlist){
		return new Link(Type.PLAYLIST, playlist.getId(), playlist.getAuthor(), null);
	}
	
	/**
	 * Create a {@link Link} from a search {@link Result}.
	 * 
	 * @param result A search {@link Result} object.
	 * 
	 * @return A {@link Link} object.
	 */
	public static Link create(Result result){
		return new Link(Type.SEARCH, null, null, result.getQuery());
	}
	
	/**
	 * Convert a base-62 encoded id into a hexadecimal id.
	 * 
	 * @param base62 A base-62 encoded id.
	 * 
	 * @return A hexadecimal id.
	 */
	private static String toHex(String base62){
		StringBuffer hex = new StringBuffer(BaseConvert.convert(base62, 62, 16));
		
		/* Prepend zeroes until hexadecimal string length is 32. */
		while(hex.length() < 32){
			hex.insert(0, '0');
		}
		
		return hex.toString();
	}
	
	/**
	 * Convert a hexadecimal id into a base-62 encoded id.
	 * 
	 * @param hex A hexadecimal id.
	 * 
	 * @return A base-62 encoded id.
	 */
	private static String toBase62(String hex){
		StringBuffer uri = new StringBuffer(BaseConvert.convert(hex, 16, 62));
		
		/* Prepend zeroes until base-62 string length is 22. */
		while(uri.length() < 22){
			uri.insert(0, '0');
		}
		
		return uri.toString();
	}
} public class Playlist implements Iterable<Track> {
	private String      id;
	private String      name;
	private String      author;
	private List<Track> tracks;
	private long        revision;
	private long        checksum;
	private boolean     collaborative;
	private String      description;
	private String      picture;
	
	public Playlist(){
		this.id            = null;
		this.name          = null;
		this.author        = null;
		this.tracks        = new ArrayList<Track>();
		this.revision      = -1;
		this.checksum      = -1;
		this.collaborative = false;
		this.description   = null;
		this.picture       = null;
	}
	
	public Playlist(String id){
		this(id, null, null, false);
	}
	
	public Playlist(String id, String name, String author, boolean collaborative){
		/* Check if id is a 32-character hex string. */
		if(id.length() == 32 && Hex.isHex(id)){
			this.id = id;
		}
		/* Otherwise try to parse it as a Spotify URI. */
		else{
			try{
				this.id = Link.create(id).getId();
			}
			catch(InvalidSpotifyURIException e){
				throw new IllegalArgumentException(
					"Given id is neither a 32-character" +
					"hex string nor a valid Spotify URI.", e
				);
			}
		}
		
		/* Set other playlist properties. */
		this.name          = name;
		this.author        = author;
		this.tracks        = new ArrayList<Track>();
		this.revision      = -1;
		this.checksum      = -1;
		this.collaborative = collaborative;
		this.description   = null;
		this.picture       = null;
	}
	
	public String getId(){
		return this.id;
	}
	
	public void setId(String id){
		this.id = id;
	}
	
	public String getName(){
		return this.name;
	}
	
	public void setName(String name){
		this.name = name;
	}
	
	public String getAuthor(){
		return this.author;
	}
	
	public void setAuthor(String author){
		this.author = author;
	}
	
	public List<Track> getTracks(){
		return this.tracks;
	}
	
	public void setTracks(List<Track> tracks){
		this.tracks = tracks;
	}
	
	public boolean hasTracks(){
		return !this.tracks.isEmpty();
	}
	
	public long getRevision(){
		return this.revision;
	}
	
	public void setRevision(long revision){
		this.revision = revision;
	}
	
	public boolean isCollaborative(){
		return this.collaborative;
	}
	
	public void setCollaborative(boolean collaborative){
		this.collaborative = collaborative;
	}
	
	public String getDescription(){
		return this.description;
	}
	
	public void setDescription(String description){
		this.description = description;
	}
	
	public String getPicture(){
		return this.picture;
	}
	
	public void setPicture(String picture){
		this.picture = picture;
	}
	
	/**
	 * Get and update the checksum of this playlist.
	 * 
	 * @return The checksum.
	 */
	public long getChecksum(){
		Checksum checksum = new Checksum(); 
		
		for(Track track : this.tracks){
			checksum.update(track);
		}
		
		this.checksum = checksum.getValue();
		
		return this.checksum;
	}
	
	/**
	 * Set the current checksum of this playlist.
	 * 
	 * @param checksum The current checksum.
	 */
	public void setChecksum(long checksum){
		this.checksum = checksum;
	}
	
	/**
	 * Create a link from this playlist.
	 * 
	 * @return A {@link Link} object which can then
	 * 		   be used to retreive the Spotify URI.
	 */
	public Link getLink(){
		return Link.create(this);
	}
	
	public Iterator<Track> iterator(){
		return this.tracks.iterator();
	}
	
	public static Playlist fromResult(String name, String author, Result result){
		Playlist playlist = new Playlist();
		
		playlist.name   = name;
		playlist.author = author;
		
		for(Track track : result.getTracks()){
			playlist.tracks.add(track);
		}
		
		return playlist;
	}
	
	public boolean equals(Object o){
		if(o instanceof Playlist){
			Playlist p = (Playlist)o;
			
			return this.id.equals(p.id);
		}
		
		return false;
	}
	
	public int hashCode(){
		return (this.id != null) ? this.id.hashCode() : 0;
	}
	
	public String toString(){
		return String.format("[Playlist: %s, %s]", this.author, this.name, this.revision);
	}
} public class Result {
	private String       query;
	private String       suggestion;
	private int          totalArtists;
	private int          totalAlbums;
	private int          totalTracks;
	private List<Artist> artists;
	private List<Album>  albums;
	private List<Track>  tracks;
	
	public Result(){
		this.query        = null;
		this.suggestion   = null;
		this.totalArtists = 0;
		this.totalAlbums  = 0;
		this.totalTracks  = 0;
		this.artists      = new ArrayList<Artist>();
		this.albums       = new ArrayList<Album>();
		this.tracks       = new ArrayList<Track>();
	}
	
	/**
	 * Create a link from this search result.
	 * 
	 * @return A {@link Link} object which can then
	 * 		   be used to retreive the Spotify URI.
	 */
	public Link getLink(){
		return Link.create(this);
	}
	
	public String getQuery(){
		return this.query;
	}
	
	public void setQuery(String query){
		this.query = query;
	}
	
	public String getSuggestion(){
		return this.suggestion;
	}
	
	public void setSuggestion(String suggestion){
		this.suggestion = suggestion;
	}
	
	public int getTotalArtists(){
		return this.totalArtists;
	}
	
	public void setTotalArtists(int totalArtists){
		this.totalArtists = totalArtists;
	}
	
	public int getTotalAlbums(){
		return this.totalAlbums;
	}
	
	public void setTotalAlbums(int totalAlbums){
		this.totalAlbums = totalAlbums;
	}
	
	public int getTotalTracks(){
		return this.totalTracks;
	}
	
	public void setTotalTracks(int totalTracks){
		this.totalTracks = totalTracks;
	}
	
	public List<Artist> getArtists(){
		return this.artists;
	}
	
	public void setArtists(List<Artist> artists){
		this.artists = artists;
	}
	
	public void addArtist(Artist artist){
		this.artists.add(artist);
	}
	
	public List<Album> getAlbums(){
		return this.albums;
	}
	
	public void setAlbums(List<Album> albums){
		this.albums = albums;
	}
	
	public void addAlbum(Album album){
		this.albums.add(album);
	}
	
	public List<Track> getTracks(){
		return this.tracks;
	}
	
	public void setTracks(List<Track> tracks){
		this.tracks = tracks;
	}
	
	public void addTrack(Track track){
		this.tracks.add(track);
	}
	
	public boolean equals(Object o){
		if(o instanceof Result){
			return this.query.equals(((Result)o).query);			
		}
		
		return false;
	}
	
	public int hashCode(){
		return (this.query != null) ? this.query.hashCode() : 0;
	}
} public class Track extends Media {
	/**
	 * Title of this track.
	 */
	private String title;
	
	/**
	 * {@link Artist} of this track.
	 */
	private Artist artist;
	
	/**
	 * {@link Album} this track belongs to.
	 */
	private Album album;
	
	/**
	 * Release year of this track.
	 */
	private int year;
	
	/**
	 * Track number on a certain disk.
	 */
	private int trackNumber;
	
	/**
	 * Length of this track in seconds.
	 */
	private int length;
	
	/**
	 * Files available for this track.
	 */
	private List<File> files;
	
	/**
	 * The identifier for this tracks cover image (32-character string).
	 */
	private String cover;
	
	/**
	 * Similar tracks of this track.
	 */
	private List<Track> similarTracks;
	
	/**
	 * If this track is explicit.
	 */
	private boolean explicit;
	
	/**
	 * Creates an empty {@link Track} object.
	 */
	public Track(){
		this.title         = null;
		this.artist        = null;
		this.album         = null;
		this.year          = -1;
		this.trackNumber   = -1;
		this.length        = -1;
		this.files         = new ArrayList<File>();
		this.cover         = null;
		this.similarTracks = new ArrayList<Track>();
	}
	
	/**
	 * Creates a {@link Track} object with the specified {@code id}.
	 * 
	 * @param id A 32-character hex string or a Spotify URI.
	 */
	public Track(String id){
		this(id, null, null, null);
	}
	
	/**
	 * Creates a {@link Track} object with the specified {@code id},
	 * {@code title},{@code artist} and {@code album}.
	 * 
	 * @param id     A 32-character hex string or a Spotify URI.
	 * @param title  Title of the track.
	 * @param artist Artist of the track.
	 * @param album  Album of the track.
	 */
	public Track(String id, String title, Artist artist, Album album){
		super(id);
		
		/* Set object properties. */
		this.title         = title;
		this.artist        = artist;
		this.album         = album;
		this.year          = -1;
		this.trackNumber   = -1;
		this.length        = -1;
		this.files         = new ArrayList<File>();
		this.cover         = null;
		this.similarTracks = new ArrayList<Track>();
	}
	
	/**
	 * Create a link from this track.
	 * 
	 * @return A {@link Link} object which can then
	 * 		   be used to retreive the Spotify URI.
	 */
	public Link getLink(){
		return Link.create(this);
	}
	
	/**
	 * Get the tracks title.
	 * 
	 * @return The title of this track.
	 */
	public String getTitle(){
		return this.title;
	}
	
	/**
	 * Set the tracks title.
	 * 
	 * @param title The desired name of this track.
	 */
	public void setTitle(String title){
		this.title = title;
	}
	
	/**
	 * Get the tracks artist.
	 * 
	 * @return An {@link Artist} object.
	 */
	public Artist getArtist(){
		return this.artist;
	}
	
	/**
	 * Set the tracks artist.
	 * 
	 * @param artist The desired {@link Artist} of this track.
	 */
	public void setArtist(Artist artist){
		this.artist = artist;
	}
	
	/**
	 * Get the tracks album.
	 * 
	 * @return An {@link Album} object.
	 */
	public Album getAlbum(){
		return this.album;
	}
	
	/**
	 * Set the tracks album.
	 * 
	 * @param album The desired {@link Album} of this track.
	 */
	public void setAlbum(Album album){
		this.album = album;
	}
	
	/**
	 * Get the tracks release year.
	 * 
	 * @return An integer denoting the release year or -1 if not available.
	 */
	public int getYear(){
		return this.year;
	}
	
	/**
	 * Set the tracks release year.
	 * 
	 * @param year A positive integer denoting the release year.
	 */
	public void setYear(int year){
		/* Check if year is valid. Years B.C. are not supported :-P */
		if(year < 0){
			throw new IllegalArgumentException("Expecting a positive year.");
		}
		
		this.year = year;
	}
	
	/**
	 * Get the tracks number on a certain disk.
	 * 
	 * @return An integer denoting the track number or -1 if not available.
	 */
	public int getTrackNumber(){
		return this.trackNumber;
	}
	
	/**
	 * Set the tracks number on a certain disk.
	 * 
	 * @param trackNumber A positive integer greater than zero.
	 */
	public void setTrackNumber(int trackNumber){
		/* Check if track number is valid. */
		if(trackNumber <= 0){
			throw new IllegalArgumentException("Expecting a track number greater than zero.");
		}
		
		this.trackNumber = trackNumber;
	}
	
	/**
	 * Get the tracks length in milliseconds.
	 * 
	 * @return An integer representing the length in milliseconds or -1 if not available.
	 */
	public int getLength(){
		return this.length;
	}
	
	/**
	 * Set the tracks length in milliseconds.
	 */
	public void setLength(int length){
		/* Check if length is valid. */
		if(length <= 0){
			throw new IllegalArgumentException("Expecting a length greater than zero.");
		}
		
		this.length = length;
	}
	
	/**
	 * Get a list of files of this track.
	 * 
	 * @return A {@link List} of {@link File} objects.
	 */
	public List<File> getFiles(){
		return this.files;
	}
	
	/**
	 * Set the list of {@link File} objects for this track.
	 * 
	 * @param files A {@link List} of {@link File} objects.
	 */
	public void setFiles(List<File> files){
		this.files = files;
	}
	
	/**
	 * Add a {@link File} to the list of files.
	 * 
	 * @param file The {@link File} to add.
	 */
	public void addFile(File file){
		this.files.add(file);
	}
	
	/**
	 * Get a file of this track that matches the given bitrate best.
	 * 
	 * @param bitrate A bitrate to match files against (e.g. 160000,
	 * 				  to choose a 160 kbps file).
	 * 
	 * @return A {@link File} object or null if no files are available.
	 */
	public File getFile(int bitrate){
		File result = null;
		int  min    = -1;
		int  diff;
		
		/* Make sure files are sorted (highest bitrate last). */
		Collections.sort(this.files);
		
		/* Pick the best-match. */
		for(File file : this.files){
			diff = Math.abs(file.getBitrate() - bitrate);
			
			if(min == -1 || diff <= min){
				min    = diff;
				result = file;
			}
		}
		
		return result;
	}
	
	/**
	 * Get the tracks cover image identifier.
	 * 
	 * @return A 32-character image identifier.
	 */
	public String getCover(){
		return this.cover;
	}
	
	/**
	 * Set the tracks cover image identifier.
	 * 
	 * @param cover A 32-character image identifier.
	 */
	public void setCover(String cover){
		this.cover = cover;
	}
	
	/**
	 * Get similar tracks for this track.
	 * 
	 * @return A {@link List} of {@link Track} objects.
	 */
	public List<Track> getSimilarTracks(){
		return this.similarTracks;
	}
	
	/**
	 * Set similar tracks for this track.
	 * 
	 * @param similarTracks A {@link List} of {@link Track} objects.
	 */
	public void setSimilarTracks(List<Track> similarTracks){
		this.similarTracks = similarTracks;
	}
	
	/**
	 * Return if this track is explicit.
	 * 
	 * @return A boolean value.
	 */
	public boolean isExplicit(){
		return this.explicit;
	}
	
	/**
	 * Set if this track is explicit.
	 * 
	 * @param explicit A boolean value.
	 */
	public void setExplicit(boolean explicit){
		this.explicit = explicit;
	}
		
	
	/**
	 * Determines if an object is equal to this {@link Track} object.
	 * If both objects are {@link Track} objects, it will compare their identifiers.
	 * 
	 * @param o Another object to compare.
	 * 
	 * @return true of the objects are equal, false otherwise.
	 */
	public boolean equals(Object o){
		if(o instanceof Track){
			Track t = (Track)o;
			
			if(this.id.equals(t.id)){
				return true;
			}
			
			for(String id : this.redirects){
				if(id.equals(t.id)){
					return true;
				}
			}
		}
		
		return false;
	}
	
	/**
	 * Return the hash code of this {@link Track} object. This will give the value returned
	 * by the {@code hashCode} method of the identifier string.
	 * 
	 * @return The {@link Track} objects hash code.
	 */
	public int hashCode(){
		return (this.id != null) ? this.id.hashCode() : 0;
	}
	
	public String toString(){
		return String.format("[Track: %s, %s, %s]", this.artist, this.album, this.title);
	}
} public class InvalidSpotifyURIException extends Exception {
	private static final long serialVersionUID = 1L;
}
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.TransferHandler;
import javax.swing.border.EmptyBorder;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.event.HyperlinkEvent.EventType;
import de.felixbruns.jotify.api.Jotify;
import de.felixbruns.jotify.gui.JotifyApplication;
import de.felixbruns.jotify.gui.JotifyPlaybackQueue;
import de.felixbruns.jotify.gui.listeners.BrowseListener;
import de.felixbruns.jotify.gui.listeners.JotifyBroadcast;
import de.felixbruns.jotify.gui.listeners.PlaylistListener;
import de.felixbruns.jotify.gui.listeners.QueueListener;
import de.felixbruns.jotify.gui.listeners.SearchListener;
import de.felixbruns.jotify.gui.swing.components.JotifyTable;
import de.felixbruns.jotify.gui.swing.components.JotifyTableModel;
import de.felixbruns.jotify.gui.swing.dnd.TrackListTransferable;
import de.felixbruns.jotify.api.media.Album;
import de.felixbruns.jotify.api.media.Artist;
import de.felixbruns.jotify.api.media.Link;
import de.felixbruns.jotify.api.media.Playlist;
import de.felixbruns.jotify.api.media.Result;
import de.felixbruns.jotify.api.media.Track;
import de.felixbruns.jotify.api.media.Link.InvalidSpotifyURIException;
package de.felixbruns.jotify.gui.swing.panels;




@SuppressWarnings("serial")
public class JotifyContentPanel extends JPanel implements HyperlinkListener, PlaylistListener, QueueListener, SearchListener, BrowseListener {
	private JotifyBroadcast         broadcast;
	private JEditorPane             infoPane;
	private JLabel                  imageLabel;
	private JScrollPane             scrollPane;
	private JTable                  table;
	private JotifyTableModel<Track> tableModel;
	
	private ImageIcon coverImage;
	private boolean   isShowingQueue;
	
	private final Jotify jotify;
	
	public JotifyContentPanel(final Jotify jotify){
		this.broadcast = JotifyBroadcast.getInstance();
		this.jotify    = jotify;
		
		this.coverImage = new ImageIcon(
			new ImageIcon(
				JotifyApplication.class.getResource("images/cover.png")