diff --git a/airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueInfo.java b/airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueInfo.java index 64f44837..a52c8dc5 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueInfo.java +++ b/airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueInfo.java @@ -33,17 +33,19 @@ public class PlayQueueInfo { private final List entries; private final boolean stopEnabled; private final boolean repeatEnabled; - private final boolean radioEnabled; + private final boolean shuffleRadioEnabled; + private final boolean internetRadioEnabled; private final boolean sendM3U; private final float gain; private int startPlayerAt = -1; private long startPlayerAtPosition; // millis - public PlayQueueInfo(List entries, boolean stopEnabled, boolean repeatEnabled, boolean radioEnabled, boolean sendM3U, float gain) { + public PlayQueueInfo(List entries, boolean stopEnabled, boolean repeatEnabled, boolean shuffleRadioEnabled, boolean internetRadioEnabled, boolean sendM3U, float gain) { this.entries = entries; this.stopEnabled = stopEnabled; this.repeatEnabled = repeatEnabled; - this.radioEnabled = radioEnabled; + this.shuffleRadioEnabled = shuffleRadioEnabled; + this.internetRadioEnabled = internetRadioEnabled; this.sendM3U = sendM3U; this.gain = gain; } @@ -74,8 +76,12 @@ public class PlayQueueInfo { return repeatEnabled; } - public boolean isRadioEnabled() { - return radioEnabled; + public boolean isShuffleRadioEnabled() { + return shuffleRadioEnabled; + } + + public boolean isInternetRadioEnabled() { + return internetRadioEnabled; } public float getGain() { @@ -121,9 +127,27 @@ public class PlayQueueInfo { private final String coverArtUrl; private final String remoteCoverArtUrl; - public Entry(int id, Integer trackNumber, String title, String artist, String album, String genre, Integer year, - String bitRate, Integer duration, String durationAsString, String format, String contentType, String fileSize, - boolean starred, String albumUrl, String streamUrl, String remoteStreamUrl, String coverArtUrl, String remoteCoverArtUrl) { + public Entry( + int id, + Integer trackNumber, + String title, + String artist, + String album, + String genre, + Integer year, + String bitRate, + Integer duration, + String durationAsString, + String format, + String contentType, + String fileSize, + boolean starred, + String albumUrl, + String streamUrl, + String remoteStreamUrl, + String coverArtUrl, + String remoteCoverArtUrl) { + this.id = id; this.trackNumber = trackNumber; this.title = title; @@ -220,5 +244,6 @@ public class PlayQueueInfo { public String getRemoteCoverArtUrl() { return remoteCoverArtUrl; } + } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueService.java b/airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueService.java index 3c4888be..2baf89f7 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueService.java +++ b/airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueService.java @@ -21,6 +21,7 @@ package org.airsonic.player.ajax; import com.google.common.base.Function; import com.google.common.collect.Lists; +import org.airsonic.player.dao.InternetRadioDao; import org.airsonic.player.dao.MediaFileDao; import org.airsonic.player.dao.PlayQueueDao; import org.airsonic.player.domain.*; @@ -28,6 +29,8 @@ import org.airsonic.player.service.*; import org.airsonic.player.service.PlaylistService; import org.airsonic.player.util.StringUtil; import org.directwebremoting.WebContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.servlet.support.RequestContextUtils; @@ -74,7 +77,13 @@ public class PlayQueueService { @Autowired private PlayQueueDao playQueueDao; @Autowired + private InternetRadioDao internetRadioDao; + @Autowired private JWTSecurityService jwtSecurityService; + @Autowired + private InternetRadioService internetRadioService; + + private static final Logger LOG = LoggerFactory.getLogger(PlayQueueService.class); /** * Returns the play queue for the player of the current user. @@ -143,6 +152,7 @@ public class PlayQueueService { String username = securityService.getCurrentUsername(request); Player player = getCurrentPlayer(request, response); PlayQueue playQueue = player.getPlayQueue(); + playQueue.setInternetRadio(null); if (playQueue.getRandomSearchCriteria() != null) { playQueue.addFiles(true, mediaFileService.getRandomSongs(playQueue.getRandomSearchCriteria(), username)); } @@ -232,6 +242,19 @@ public class PlayQueueService { return doPlay(request, player, songs).setStartPlayerAt(0); } + /** + * @param index Start playing at this index, or play whole radio playlist if {@code null}. + */ + public PlayQueueInfo playInternetRadio(int id, Integer index) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + + InternetRadio radio = internetRadioDao.getInternetRadioById(id); + if (!radio.isEnabled()) { throw new Exception("Radio is not enabled"); } + + Player player = resolvePlayer(); + return doPlayInternetRadio(request, player, radio).setStartPlayerAt(0); + } + /** * @param index Start playing at this index, or play whole playlist if {@code null}. */ @@ -418,6 +441,18 @@ public class PlayQueueService { } player.getPlayQueue().addFiles(false, files); player.getPlayQueue().setRandomSearchCriteria(null); + player.getPlayQueue().setInternetRadio(null); + if (player.isJukebox()) { + jukeboxService.play(player); + } + return convert(request, player, true); + } + + private PlayQueueInfo doPlayInternetRadio(HttpServletRequest request, Player player, InternetRadio radio) throws Exception { + internetRadioService.clearInternetRadioSourceCache(radio.getId()); + player.getPlayQueue().clear(); + player.getPlayQueue().setRandomSearchCriteria(null); + player.getPlayQueue().setInternetRadio(radio); if (player.isJukebox()) { jukeboxService.play(player); } @@ -433,6 +468,7 @@ public class PlayQueueService { Player player = getCurrentPlayer(request, response); player.getPlayQueue().addFiles(false, randomFiles); player.getPlayQueue().setRandomSearchCriteria(null); + player.getPlayQueue().setInternetRadio(null); return convert(request, player, true).setStartPlayerAt(0); } @@ -445,6 +481,8 @@ public class PlayQueueService { List similarSongs = lastFmService.getSimilarSongs(artist, count, musicFolders); Player player = getCurrentPlayer(request, response); player.getPlayQueue().addFiles(false, similarSongs); + player.getPlayQueue().setRandomSearchCriteria(null); + player.getPlayQueue().setInternetRadio(null); return convert(request, player, true).setStartPlayerAt(0); } @@ -478,6 +516,7 @@ public class PlayQueueService { playQueue.addFiles(true, files); } playQueue.setRandomSearchCriteria(null); + playQueue.setInternetRadio(null); return playQueue; } @@ -589,7 +628,7 @@ public class PlayQueueService { HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); Player player = getCurrentPlayer(request, response); PlayQueue playQueue = player.getPlayQueue(); - if (playQueue.isRadioEnabled()) { + if (playQueue.isShuffleRadioEnabled()) { playQueue.setRandomSearchCriteria(null); playQueue.setRepeatEnabled(false); } else { @@ -636,20 +675,43 @@ public class PlayQueueService { } private PlayQueueInfo convert(HttpServletRequest request, Player player, boolean serverSidePlaylist, int offset) throws Exception { + String url = NetworkService.getBaseUrl(request); + Locale locale = RequestContextUtils.getLocale(request); + PlayQueue playQueue = player.getPlayQueue(); - /* if (serverSidePlaylist && player.isJukebox()) { - updateJukebox(player, offset); - } */ - boolean isCurrentPlayer = player.getIpAddress() != null && player.getIpAddress().equals(request.getRemoteAddr()); + List entries; + if (playQueue.isInternetRadioEnabled()) { + entries = convertInternetRadio(request, player); + } else { + entries = convertMediaFileList(request, player); + } + boolean isCurrentPlayer = player.getIpAddress() != null && player.getIpAddress().equals(request.getRemoteAddr()); + boolean isStopEnabled = playQueue.getStatus() == PlayQueue.Status.PLAYING && !player.isExternalWithPlaylist(); boolean m3uSupported = player.isExternal() || player.isExternalWithPlaylist(); serverSidePlaylist = player.isAutoControlEnabled() && m3uSupported && isCurrentPlayer && serverSidePlaylist; - Locale locale = RequestContextUtils.getLocale(request); - List entries = new ArrayList(); + float gain = jukeboxService.getGain(player); + + return new PlayQueueInfo( + entries, + isStopEnabled, + playQueue.isRepeatEnabled(), + playQueue.isShuffleRadioEnabled(), + playQueue.isInternetRadioEnabled(), + serverSidePlaylist, + gain + ); + } + + private List convertMediaFileList(HttpServletRequest request, Player player) { + + String url = NetworkService.getBaseUrl(request); + Locale locale = RequestContextUtils.getLocale(request); PlayQueue playQueue = player.getPlayQueue(); + List entries = new ArrayList<>(); for (MediaFile file : playQueue.getFiles()) { String albumUrl = url + "main.view?id=" + file.getId(); @@ -668,12 +730,48 @@ public class PlayQueueService { formatFileSize(file.getFileSize(), locale), starred, albumUrl, streamUrl, remoteStreamUrl, coverArtUrl, remoteCoverArtUrl)); } - boolean isStopEnabled = playQueue.getStatus() == PlayQueue.Status.PLAYING && !player.isExternalWithPlaylist(); - float gain = 0.0f; - gain = jukeboxService.getGain(player); + return entries; + } + + private List convertInternetRadio(HttpServletRequest request, Player player) throws Exception { - return new PlayQueueInfo(entries, isStopEnabled, playQueue.isRepeatEnabled(), playQueue.isRadioEnabled(), serverSidePlaylist, gain); + PlayQueue playQueue = player.getPlayQueue(); + InternetRadio radio = playQueue.getInternetRadio(); + + final String radioHomepageUrl = radio.getHomepageUrl(); + final String radioName = radio.getName(); + + List entries = new ArrayList<>(); + for (InternetRadioSource streamSource: internetRadioService.getInternetRadioSources(radio)) { + // Fake entry id so that the source can be selected in the UI + Integer streamId = -(1+entries.size()); + Integer streamTrackNumber = entries.size(); + String streamUrl = streamSource.getStreamUrl(); + entries.add(new PlayQueueInfo.Entry( + streamId, // Entry id + streamTrackNumber, // Track number + streamUrl, // Track title (use radio stream URL for now) + "", // Track artist + radioName, // Album name (use radio name) + "Internet Radio", // Genre + 0, // Year + "", // Bit rate + 0, // Duration + "", // Duration (as string) + "", // Format + "", // Content Type + "", // File size + false, // Starred + radioHomepageUrl, // Album URL (use radio home page URL) + streamUrl, // Stream URL + streamUrl, // Remote stream URL + null, // Cover art URL + null // Remote cover art URL + )); + } + + return entries; } private String formatFileSize(Long fileSize, Locale locale) { @@ -797,4 +895,8 @@ public class PlayQueueService { public void setJwtSecurityService(JWTSecurityService jwtSecurityService) { this.jwtSecurityService = jwtSecurityService; } + + public void setInternetRadioDao(InternetRadioDao internetRadioDao) { + this.internetRadioDao = internetRadioDao; + } } diff --git a/airsonic-main/src/main/java/org/airsonic/player/controller/RandomPlayQueueController.java b/airsonic-main/src/main/java/org/airsonic/player/controller/RandomPlayQueueController.java index 66b9ca09..995c78be 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/controller/RandomPlayQueueController.java +++ b/airsonic-main/src/main/java/org/airsonic/player/controller/RandomPlayQueueController.java @@ -239,6 +239,7 @@ public class RandomPlayQueueController { if (autoRandom != null) { playQueue.setRandomSearchCriteria(criteria); + playQueue.setInternetRadio(null); } // Render the 'reload' view to reload the play queue and the main page diff --git a/airsonic-main/src/main/java/org/airsonic/player/dao/InternetRadioDao.java b/airsonic-main/src/main/java/org/airsonic/player/dao/InternetRadioDao.java index 1f57bfff..a392a790 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/dao/InternetRadioDao.java +++ b/airsonic-main/src/main/java/org/airsonic/player/dao/InternetRadioDao.java @@ -42,6 +42,17 @@ public class InternetRadioDao extends AbstractDao { private static final String QUERY_COLUMNS = "id, " + INSERT_COLUMNS; private final InternetRadioRowMapper rowMapper = new InternetRadioRowMapper(); + /** + * Returns the internet radio station with the given ID. + * + * @param id The unique internet radio station ID. + * @return The internet radio station with the given ID, or null if no such internet radio exists. + */ + public InternetRadio getInternetRadioById(int id) { + String sql = "select " + QUERY_COLUMNS + " from internet_radio where id=?"; + return queryOne(sql, rowMapper, id); + } + /** * Returns all internet radio stations. * diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/InternetRadioSource.java b/airsonic-main/src/main/java/org/airsonic/player/domain/InternetRadioSource.java new file mode 100644 index 00000000..5058dc4c --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/InternetRadioSource.java @@ -0,0 +1,11 @@ +package org.airsonic.player.domain; + +public class InternetRadioSource { + private String streamUrl; + + public InternetRadioSource(String streamUrl) { + this.streamUrl = streamUrl; + } + + public String getStreamUrl() { return streamUrl; } +} diff --git a/airsonic-main/src/main/java/org/airsonic/player/domain/PlayQueue.java b/airsonic-main/src/main/java/org/airsonic/player/domain/PlayQueue.java index f0232cd0..15d57515 100644 --- a/airsonic-main/src/main/java/org/airsonic/player/domain/PlayQueue.java +++ b/airsonic-main/src/main/java/org/airsonic/player/domain/PlayQueue.java @@ -35,7 +35,9 @@ public class PlayQueue { private boolean repeatEnabled; private String name = "(unnamed)"; private Status status = Status.PLAYING; + private RandomSearchCriteria randomSearchCriteria; + private InternetRadio internetRadio; /** * The index of the current song, or -1 is the end of the playlist is reached. @@ -224,6 +226,8 @@ public class PlayQueue { public synchronized void clear() { makeBackup(); files.clear(); + setRandomSearchCriteria(null); + setInternetRadio(null); index = 0; } @@ -363,13 +367,18 @@ public class PlayQueue { } /** - * Returns whether the playlist is a shuffle radio + * Returns whether the play queue is in shuffle radio mode. * - * @return Whether the playlist is a shuffle radio. + * @return Whether the play queue is a shuffle radio mode. */ - public synchronized boolean isRadioEnabled() { - return this.randomSearchCriteria != null; - } + public synchronized boolean isShuffleRadioEnabled() { return this.randomSearchCriteria != null; } + + /** + * Returns whether the play queue is a internet radio mode. + * + * @return Whether the play queue is a internet radio mode. + */ + public synchronized boolean isInternetRadioEnabled() { return this.internetRadio != null; } /** * Revert the last operation. @@ -407,22 +416,32 @@ public class PlayQueue { } /** - * Returns the criteria used to generate this random playlist. + * Sets the current internet radio + * + * @param internetRadio An internet radio, or null if this is not an internet radio playlist + */ + public void setInternetRadio(InternetRadio internetRadio) { this.internetRadio = internetRadio; } + + /** + * Gets the current internet radio + * + * @return The current internet radio, or null if this is not an internet radio playlist + */ + public InternetRadio getInternetRadio() { return internetRadio; } + + /** + * Returns the criteria used to generate this random playlist * * @return The search criteria, or null if this is not a random playlist. */ - public synchronized RandomSearchCriteria getRandomSearchCriteria() { - return randomSearchCriteria; - } + public synchronized RandomSearchCriteria getRandomSearchCriteria() { return randomSearchCriteria; } /** - * Sets the criteria used to generate this random playlist. + * Sets the criteria used to generate this random playlist * * @param randomSearchCriteria The search criteria, or null if this is not a random playlist. */ - public synchronized void setRandomSearchCriteria(RandomSearchCriteria randomSearchCriteria) { - this.randomSearchCriteria = randomSearchCriteria; - } + public synchronized void setRandomSearchCriteria(RandomSearchCriteria randomSearchCriteria) { this.randomSearchCriteria = randomSearchCriteria; } /** * Returns the total length in bytes. diff --git a/airsonic-main/src/main/java/org/airsonic/player/service/InternetRadioService.java b/airsonic-main/src/main/java/org/airsonic/player/service/InternetRadioService.java new file mode 100644 index 00000000..64071232 --- /dev/null +++ b/airsonic-main/src/main/java/org/airsonic/player/service/InternetRadioService.java @@ -0,0 +1,302 @@ +package org.airsonic.player.service; + +import chameleon.playlist.*; +import org.airsonic.player.domain.InternetRadio; +import org.airsonic.player.domain.InternetRadioSource; +import org.apache.commons.io.input.BoundedInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.*; + +@Service +public class InternetRadioService { + + private static final Logger LOG = LoggerFactory.getLogger(InternetRadioService.class); + + /** + * The maximum number of source URLs in a remote playlist. + */ + private static final int PLAYLIST_REMOTE_MAX_LENGTH = 250; + + /** + * The maximum size, in bytes, for a remote playlist response. + */ + private static final long PLAYLIST_REMOTE_MAX_BYTE_SIZE = 100 * 1024; // 100 kB + + /** + * The maximum number of redirects for a remote playlist response. + */ + private static final int PLAYLIST_REMOTE_MAX_REDIRECTS = 20; + + /** + * A list of cached source URLs for remote playlists. + */ + private final Map> cachedSources; + + /** + * Generic exception class for playlists. + */ + private class PlaylistException extends Exception { + public PlaylistException(String message) { super(message); } + } + + /** + * Exception thrown when the remote playlist is too large to be parsed completely. + */ + private class PlaylistTooLarge extends PlaylistException { + public PlaylistTooLarge(String message) { super(message); } + } + + /** + * Exception thrown when the remote playlist format cannot be determined. + */ + private class PlaylistFormatUnsupported extends PlaylistException { + public PlaylistFormatUnsupported(String message) { + super(message); + } + } + + /** + * Exception thrown when too many redirects occurred when retrieving a remote playlist. + */ + private class PlaylistHasTooManyRedirects extends PlaylistException { + public PlaylistHasTooManyRedirects(String message) { super(message); } + } + + public InternetRadioService() { + this.cachedSources = new HashMap<>(); + } + + /** + * Clear the radio source cache. + */ + public void clearInternetRadioSourceCache() { + cachedSources.clear(); + } + + /** + * Clear the radio source cache for the given radio id + * @param internetRadioId a radio id + */ + public void clearInternetRadioSourceCache(Integer internetRadioId) { + if (internetRadioId != null) { + cachedSources.remove(internetRadioId); + } + } + + /** + * Retrieve a list of sources for the given internet radio. + * + * This method caches the sources using the InternetRadio.getId + * method as a key, until clearInternetRadioSourceCache is called. + * + * @param radio an internet radio + * @return a list of internet radio sources + */ + public List getInternetRadioSources(InternetRadio radio) { + List sources; + if (cachedSources.containsKey(radio.getId())) { + LOG.debug("Got cached sources for internet radio {}!", radio.getStreamUrl()); + sources = cachedSources.get(radio.getId()); + } else { + LOG.debug("Retrieving sources for internet radio {}...", radio.getStreamUrl()); + try { + sources = retrieveInternetRadioSources(radio); + if (sources.isEmpty()) { + LOG.warn("No entries found for internet radio {}.", radio.getStreamUrl()); + } else { + LOG.info("Retrieved playlist for internet radio {}, got {} sources.", radio.getStreamUrl(), sources.size()); + } + } catch (Exception e) { + LOG.error("Failed to retrieve sources for internet radio {}.", radio.getStreamUrl(), e); + sources = new ArrayList<>(); + } + cachedSources.put(radio.getId(), sources); + } + return sources; + } + + /** + * Retrieve a list of sources from the given internet radio + * + * This method uses a default maximum limit of PLAYLIST_REMOTE_MAX_LENGTH sources. + * + * @param radio an internet radio + * @return a list of internet radio sources + * @throws Exception + */ + private List retrieveInternetRadioSources(InternetRadio radio) throws Exception { + return retrieveInternetRadioSources( + radio, + PLAYLIST_REMOTE_MAX_LENGTH, + PLAYLIST_REMOTE_MAX_BYTE_SIZE, + PLAYLIST_REMOTE_MAX_REDIRECTS + ); + } + + /** + * Retrieve a list of sources from the given internet radio. + * + * @param radio an internet radio + * @param maxCount the maximum number of items to read from the remote playlist, or 0 if unlimited + * @param maxByteSize maximum size of the response, in bytes, or 0 if unlimited + * @param maxRedirects maximum number of redirects, or 0 if unlimited + * @return a list of internet radio sources + * @throws Exception + */ + private List retrieveInternetRadioSources(InternetRadio radio, int maxCount, long maxByteSize, int maxRedirects) throws Exception { + // Retrieve the remote playlist + String playlistUrl = radio.getStreamUrl(); + LOG.debug("Parsing internet radio playlist at {}...", playlistUrl); + SpecificPlaylist inputPlaylist = retrievePlaylist(new URL(playlistUrl), maxByteSize, maxRedirects); + + // Retrieve stream URLs + List entries = new ArrayList<>(); + try { + inputPlaylist.toPlaylist().acceptDown(new PlaylistVisitor() { + @Override + public void beginVisitPlaylist(Playlist playlist) throws Exception { + + } + + @Override + public void endVisitPlaylist(Playlist playlist) throws Exception { + + } + + @Override + public void beginVisitParallel(Parallel parallel) throws Exception { + + } + + @Override + public void endVisitParallel(Parallel parallel) throws Exception { + + } + + @Override + public void beginVisitSequence(Sequence sequence) throws Exception { + + } + + @Override + public void endVisitSequence(Sequence sequence) throws Exception { + + } + + @Override + public void beginVisitMedia(Media media) throws Exception { + // Since we're dealing with remote content, we place a hard + // limit on the maximum number of items to load from the playlist, + // in order to avoid parsing erroneous data. + if (maxCount > 0 && entries.size() >= maxCount) { + throw new PlaylistTooLarge("Remote playlist has too many sources (maximum " + maxCount + ")"); + } + String streamUrl = media.getSource().getURI().toString(); + LOG.debug("Got source media at {}", streamUrl); + entries.add(new InternetRadioSource(streamUrl)); + } + + @Override + public void endVisitMedia(Media media) throws Exception { + + } + }); + } catch (PlaylistTooLarge e) { + // Ignore if playlist is too large, but truncate the rest and log a warning. + LOG.warn(e.getMessage()); + } + + return entries; + } + + /** + * Retrieve playlist data from a given URL. + * + * @param url URL to the remote playlist + * @param maxByteSize maximum size of the response, in bytes, or 0 if unlimited + * @param maxRedirects maximum number of redirects, or 0 if unlimited + * @return the remote playlist data + */ + protected SpecificPlaylist retrievePlaylist(URL url, long maxByteSize, int maxRedirects) throws IOException, PlaylistException { + + SpecificPlaylist playlist; + HttpURLConnection urlConnection = connectToURLWithRedirects(url, maxRedirects); + try (InputStream in = urlConnection.getInputStream()) { + String contentEncoding = urlConnection.getContentEncoding(); + if (maxByteSize > 0) { + playlist = SpecificPlaylistFactory.getInstance().readFrom(new BoundedInputStream(in, maxByteSize), contentEncoding); + } else { + playlist = SpecificPlaylistFactory.getInstance().readFrom(in, contentEncoding); + } + } + finally { + urlConnection.disconnect(); + } + if (playlist == null) { + throw new PlaylistFormatUnsupported("Unsupported playlist format " + url.toString()); + } + return playlist; + } + + /** + * Start a new connection to a remote URL, and follow redirects. + * + * @param url the remote URL + * @param maxRedirects maximum number of redirects, or 0 if unlimited + * @return an open connection + */ + protected HttpURLConnection connectToURLWithRedirects(URL url, int maxRedirects) throws IOException, PlaylistException { + + int redirectCount = 0; + URL currentURL = url; + + // Start a first connection. + HttpURLConnection connection = connectToURL(currentURL); + + // While it redirects, follow redirects in new connections. + while (connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM || + connection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP || + connection.getResponseCode() == HttpURLConnection.HTTP_SEE_OTHER) { + + // Check if redirect count is not too large. + redirectCount += 1; + if (maxRedirects > 0 && redirectCount > maxRedirects) { + connection.disconnect(); + throw new PlaylistHasTooManyRedirects(String.format("Too many redirects (%d) for URL %s", redirectCount, url)); + } + + // Reconnect to the new URL. + currentURL = new URL(connection.getHeaderField("Location")); + connection.disconnect(); + connection = connectToURL(currentURL); + } + + // Return the last connection that did not redirect. + return connection; + } + + /** + * Start a new connection to a remote URL. + * + * @param url the remote URL + * @return an open connection + */ + protected HttpURLConnection connectToURL(URL url) throws IOException { + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setAllowUserInteraction(false); + urlConnection.setConnectTimeout(10000); + urlConnection.setDoInput(true); + urlConnection.setDoOutput(false); + urlConnection.setReadTimeout(60000); + urlConnection.setUseCaches(true); + urlConnection.connect(); + return urlConnection; + } +} diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties index 63461c2f..1e9809ec 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_en.properties @@ -99,6 +99,7 @@ playlist.empty=Play queue empty. playlist.toast.appendtoplaylist=Playlist updated. playlist.toast.saveasplaylist=Playlist saved. playlist.toast.saveplayqueue=Play queue saved. +playlist.toast.radioerror=Sorry, an error occurred while trying to load the internet radio. playlist.missing=Missing playlist2.noplaylists=You haven't created any playlists yet. diff --git a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties index 7d7a1ebc..a29afecc 100644 --- a/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties +++ b/airsonic-main/src/main/resources/org/airsonic/player/i18n/ResourceBundle_fr.properties @@ -99,6 +99,7 @@ playlist.empty=Liste de lecture vide playlist.toast.appendtoplaylist=Playlist mise \u00e0 jour. playlist.toast.saveasplaylist=Playlist sauvegard\u00e9e. playlist.toast.saveplayqueue=Liste de lecture sauvegard\u00e9. +playlist.toast.radioerror=D\u00E9sol\u00E9, une erreur s'est produite pendant le chargement de la radio. playlist.missing=Manquant playlist2.noplaylists=Vous n'avez pas encore cr\u00e9er de playlist. diff --git a/airsonic-main/src/main/webapp/WEB-INF/jsp/left.jsp b/airsonic-main/src/main/webapp/WEB-INF/jsp/left.jsp index 81da95b7..fa218716 100644 --- a/airsonic-main/src/main/webapp/WEB-INF/jsp/left.jsp +++ b/airsonic-main/src/main/webapp/WEB-INF/jsp/left.jsp @@ -20,6 +20,11 @@ top.main.location.href = mainLocation; } } + + $('.radio-play').on('click', function(evt) { + top.playQueue.onPlayInternetRadio($(this).data("id"), 0); + evt.preventDefault(); + }); } function updatePlaylists() { @@ -118,19 +123,20 @@

+

- - " alt="" title=""> + + " alt="" title=""> - + ${fn:escapeXml(radio.name)} - ${fn:escapeXml(radio.name)} + ${fn:escapeXml(radio.name)} - +

diff --git a/airsonic-main/src/main/webapp/WEB-INF/jsp/playQueue.jsp b/airsonic-main/src/main/webapp/WEB-INF/jsp/playQueue.jsp index 81c8137a..c5fb8bb2 100644 --- a/airsonic-main/src/main/webapp/WEB-INF/jsp/playQueue.jsp +++ b/airsonic-main/src/main/webapp/WEB-INF/jsp/playQueue.jsp @@ -31,13 +31,30 @@