Merge remote-tracking branch 'origin/pr/985'
This commit is contained in:
@@ -33,17 +33,19 @@ public class PlayQueueInfo {
|
||||
private final List<Entry> 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<Entry> entries, boolean stopEnabled, boolean repeatEnabled, boolean radioEnabled, boolean sendM3U, float gain) {
|
||||
public PlayQueueInfo(List<Entry> 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MediaFile> 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);
|
||||
|
||||
/* if (serverSidePlaylist && player.isJukebox()) {
|
||||
updateJukebox(player, offset);
|
||||
} */
|
||||
boolean isCurrentPlayer = player.getIpAddress() != null && player.getIpAddress().equals(request.getRemoteAddr());
|
||||
|
||||
boolean m3uSupported = player.isExternal() || player.isExternalWithPlaylist();
|
||||
serverSidePlaylist = player.isAutoControlEnabled() && m3uSupported && isCurrentPlayer && serverSidePlaylist;
|
||||
Locale locale = RequestContextUtils.getLocale(request);
|
||||
|
||||
List<PlayQueueInfo.Entry> entries = new ArrayList<PlayQueueInfo.Entry>();
|
||||
PlayQueue playQueue = player.getPlayQueue();
|
||||
|
||||
List<PlayQueueInfo.Entry> 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;
|
||||
|
||||
float gain = jukeboxService.getGain(player);
|
||||
|
||||
return new PlayQueueInfo(
|
||||
entries,
|
||||
isStopEnabled,
|
||||
playQueue.isRepeatEnabled(),
|
||||
playQueue.isShuffleRadioEnabled(),
|
||||
playQueue.isInternetRadioEnabled(),
|
||||
serverSidePlaylist,
|
||||
gain
|
||||
);
|
||||
}
|
||||
|
||||
private List<PlayQueueInfo.Entry> convertMediaFileList(HttpServletRequest request, Player player) {
|
||||
|
||||
String url = NetworkService.getBaseUrl(request);
|
||||
Locale locale = RequestContextUtils.getLocale(request);
|
||||
PlayQueue playQueue = player.getPlayQueue();
|
||||
|
||||
List<PlayQueueInfo.Entry> 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;
|
||||
}
|
||||
|
||||
return new PlayQueueInfo(entries, isStopEnabled, playQueue.isRepeatEnabled(), playQueue.isRadioEnabled(), serverSidePlaylist, gain);
|
||||
private List<PlayQueueInfo.Entry> convertInternetRadio(HttpServletRequest request, Player player) throws Exception {
|
||||
|
||||
PlayQueue playQueue = player.getPlayQueue();
|
||||
InternetRadio radio = playQueue.getInternetRadio();
|
||||
|
||||
final String radioHomepageUrl = radio.getHomepageUrl();
|
||||
final String radioName = radio.getName();
|
||||
|
||||
List<PlayQueueInfo.Entry> 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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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
|
||||
|
||||
@@ -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 <code>null</code> 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.
|
||||
*
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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 <code>null</code> 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 <code>null</code> 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 <code>null</code> 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 <code>null</code> 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.
|
||||
|
||||
@@ -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<Integer, List<InternetRadioSource>> 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<InternetRadioSource> getInternetRadioSources(InternetRadio radio) {
|
||||
List<InternetRadioSource> 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<InternetRadioSource> 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<InternetRadioSource> 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<InternetRadioSource> 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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,9 +123,10 @@
|
||||
|
||||
<c:if test="${not empty model.radios}">
|
||||
<h2 class="bgcolor1" style="padding-left: 2px"><fmt:message key="left.radio"/></h2>
|
||||
<iframe id="radio-playlist-data" style="display:none;"></iframe>
|
||||
<c:forEach items="${model.radios}" var="radio">
|
||||
<p class="dense" style="padding-left: 2px">
|
||||
<a target="hidden" href="${radio.streamUrl}">
|
||||
<a target="hidden" href="${radio.streamUrl}" class="radio-play" data-id="${radio.id}">
|
||||
<img src="<spring:theme code="playImage"/>" alt="<fmt:message key="common.play"/>" title="<fmt:message key="common.play"/>"></a>
|
||||
<span style="vertical-align: middle">
|
||||
<c:choose>
|
||||
@@ -128,7 +134,7 @@
|
||||
${fn:escapeXml(radio.name)}
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<a target="main" href="${radio.homepageUrl}">${fn:escapeXml(radio.name)}</a>
|
||||
<a target="_blank" rel="noopener" href="${radio.homepageUrl}">${fn:escapeXml(radio.name)}</a>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</span>
|
||||
|
||||
@@ -31,13 +31,30 @@
|
||||
<span id="dummy-animation-target" style="max-width: ${model.autoHide ? 50 : 150}px; display: none"></span>
|
||||
|
||||
<script type="text/javascript" language="javascript">
|
||||
|
||||
// These variables store the media player state, received from DWR in the
|
||||
// playQueueCallback function below.
|
||||
|
||||
// List of songs (of type PlayQueueInfo.Entry)
|
||||
var songs = null;
|
||||
|
||||
// Stream URL of the media being played
|
||||
var currentStreamUrl = null;
|
||||
|
||||
// Is autorepeat enabled?
|
||||
var repeatEnabled = false;
|
||||
var radioEnabled = false;
|
||||
|
||||
// Is the "shuffle radio" playing? (More > Shuffle Radio)
|
||||
var shuffleRadioEnabled = false;
|
||||
|
||||
// Is the "internet radio" playing?
|
||||
var internetRadioEnabled = false;
|
||||
|
||||
// Is the play queue visible? (Initially hidden if set to "auto-hide" in the settings)
|
||||
var isVisible = ${model.autoHide ? 'false' : 'true'};
|
||||
|
||||
// Initialize the Cast player (ChromeCast support)
|
||||
var CastPlayer = new CastPlayer();
|
||||
var ignore = false;
|
||||
|
||||
function init() {
|
||||
<c:if test="${model.autoHide}">initAutoHide();</c:if>
|
||||
@@ -82,6 +99,40 @@
|
||||
}
|
||||
});
|
||||
|
||||
/** Toggle between <a> and <span> in order to disable play queue action buttons */
|
||||
$.fn.toggleLink = function(newState) {
|
||||
$(this).each(function(ix, elt) {
|
||||
|
||||
var node, currentState;
|
||||
if (elt.tagName.toLowerCase() === "a") currentState = true;
|
||||
else if (elt.tagName.toLowerCase() === "span") currentState = false;
|
||||
else return true;
|
||||
if (typeof newState === 'undefined') newState = !currentState;
|
||||
if (newState === currentState) return true;
|
||||
|
||||
if (newState) node = document.createElement("a");
|
||||
else node = document.createElement("span");
|
||||
|
||||
node.innerHTML = elt.innerHTML;
|
||||
if (elt.hasAttribute("id")) node.setAttribute("id", elt.getAttribute("id"));
|
||||
if (elt.hasAttribute("style")) node.setAttribute("style", elt.getAttribute("style"));
|
||||
if (elt.hasAttribute("class")) node.setAttribute("class", elt.getAttribute("class"));
|
||||
|
||||
if (newState) {
|
||||
if (elt.hasAttribute("data-href")) node.setAttribute("href", elt.getAttribute("data-href"));
|
||||
node.classList.remove("disabled");
|
||||
node.removeAttribute("aria-disabled");
|
||||
} else {
|
||||
if (elt.hasAttribute("href")) node.setAttribute("data-href", elt.getAttribute("href"));
|
||||
node.classList.add("disabled");
|
||||
node.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
|
||||
elt.parentNode.replaceChild(node, elt);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
getPlayQueue();
|
||||
}
|
||||
|
||||
@@ -280,7 +331,7 @@
|
||||
}
|
||||
function onNext(wrap) {
|
||||
var index = parseInt(getCurrentSongIndex()) + 1;
|
||||
if (radioEnabled && index >= songs.length) {
|
||||
if (shuffleRadioEnabled && index >= songs.length) {
|
||||
playQueueService.reloadSearchCriteria(function(playQueue) {
|
||||
playQueueCallback(playQueue);
|
||||
onSkip(index);
|
||||
@@ -303,6 +354,9 @@
|
||||
function onPlayPlaylist(id, index) {
|
||||
playQueueService.playPlaylist(id, index, playQueueCallback);
|
||||
}
|
||||
function onPlayInternetRadio(id, index) {
|
||||
playQueueService.playInternetRadio(id, index, playQueueCallback);
|
||||
}
|
||||
function onPlayTopSong(id, index) {
|
||||
playQueueService.playTopSong(id, index, playQueueCallback);
|
||||
}
|
||||
@@ -423,14 +477,22 @@
|
||||
function playQueueCallback(playQueue) {
|
||||
songs = playQueue.entries;
|
||||
repeatEnabled = playQueue.repeatEnabled;
|
||||
radioEnabled = playQueue.radioEnabled;
|
||||
shuffleRadioEnabled = playQueue.shuffleRadioEnabled;
|
||||
internetRadioEnabled = playQueue.internetRadioEnabled;
|
||||
|
||||
// If an internet radio has no sources, display a message to the user.
|
||||
if (internetRadioEnabled && songs.length == 0) {
|
||||
top.main.$().toastmessage("showErrorToast", "<fmt:message key="playlist.toast.radioerror"/>");
|
||||
onStop();
|
||||
}
|
||||
|
||||
if ($("#start")) {
|
||||
$("#start").toggle(!playQueue.stopEnabled);
|
||||
$("#stop").toggle(playQueue.stopEnabled);
|
||||
}
|
||||
|
||||
if ($("#toggleRepeat")) {
|
||||
if (radioEnabled) {
|
||||
if (shuffleRadioEnabled) {
|
||||
$("#toggleRepeat").html("<fmt:message key="playlist.repeat_radio"/>");
|
||||
} else if (repeatEnabled) {
|
||||
$("#toggleRepeat").attr('src', '<spring:theme code="repeatOn"/>');
|
||||
@@ -441,6 +503,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Disable some UI items if internet radio is playing
|
||||
$("select#moreActions #loadPlayQueue").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #savePlayQueue").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #savePlaylist").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #downloadPlaylist").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #sharePlaylist").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #sortByTrack").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #sortByAlbum").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #sortByArtist").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #selectAll").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #selectNone").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #removeSelected").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #download").prop("disabled", internetRadioEnabled);
|
||||
$("select#moreActions #appendPlaylist").prop("disabled", internetRadioEnabled);
|
||||
$("#shuffleQueue").toggleLink(!internetRadioEnabled);
|
||||
$("#repeatQueue").toggleLink(!internetRadioEnabled);
|
||||
$("#undoQueue").toggleLink(!internetRadioEnabled);
|
||||
|
||||
if (songs.length == 0) {
|
||||
$("#songCountAndDuration").text("");
|
||||
$("#empty").show();
|
||||
@@ -462,11 +542,26 @@
|
||||
if ($("#trackNumber" + id)) {
|
||||
$("#trackNumber" + id).text(song.trackNumber);
|
||||
}
|
||||
|
||||
if (!internetRadioEnabled) {
|
||||
// Show star/remove buttons in all cases...
|
||||
$("#starSong" + id).show();
|
||||
$("#removeSong" + id).show();
|
||||
$("#songIndex" + id).show();
|
||||
|
||||
// Show star rating
|
||||
if (song.starred) {
|
||||
$("#starSong" + id).attr("src", "<spring:theme code='ratingOnImage'/>");
|
||||
} else {
|
||||
$("#starSong" + id).attr("src", "<spring:theme code='ratingOffImage'/>");
|
||||
}
|
||||
} else {
|
||||
// ...except from when internet radio is playing.
|
||||
$("#starSong" + id).hide();
|
||||
$("#removeSong" + id).hide();
|
||||
$("#songIndex" + id).hide();
|
||||
}
|
||||
|
||||
if ($("#currentImage" + id) && song.streamUrl == currentStreamUrl) {
|
||||
$("#currentImage" + id).show();
|
||||
if (isJavaJukeboxPresent()) {
|
||||
@@ -486,6 +581,13 @@
|
||||
$("#album" + id).text(song.album);
|
||||
$("#album" + id).attr("title", song.album);
|
||||
$("#albumUrl" + id).attr("href", song.albumUrl);
|
||||
// Open external internet radio links in new windows
|
||||
if (internetRadioEnabled) {
|
||||
$("#albumUrl" + id).attr({
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer",
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($("#artist" + id)) {
|
||||
$("#artist" + id).text(song.artist);
|
||||
@@ -495,7 +597,13 @@
|
||||
$("#genre" + id).text(song.genre);
|
||||
}
|
||||
if ($("#year" + id)) {
|
||||
// If song.year is not an int, this will return NaN, which
|
||||
// conveniently returns false in all boolean operations.
|
||||
if (parseInt(song.year) > 0) {
|
||||
$("#year" + id).text(song.year);
|
||||
} else {
|
||||
$("#year" + id).text("");
|
||||
}
|
||||
}
|
||||
if ($("#bitRate" + id)) {
|
||||
$("#bitRate" + id).text(song.bitRate);
|
||||
@@ -805,7 +913,7 @@
|
||||
|
||||
<td style="white-space:nowrap;">
|
||||
<span class="header">
|
||||
<a href="javascript:onShuffle()">
|
||||
<a href="javascript:onShuffle()" id="shuffleQueue">
|
||||
<img src="<spring:theme code="shuffleImage"/>" alt="shuffle" style="cursor:pointer; height:18px">
|
||||
</a>
|
||||
</span> |</td>
|
||||
@@ -813,7 +921,7 @@
|
||||
<c:if test="${model.player.web or model.player.jukebox or model.player.external}">
|
||||
<td style="white-space:nowrap;">
|
||||
<span class="header">
|
||||
<a href="javascript:onToggleRepeat()">
|
||||
<a href="javascript:onToggleRepeat()" id="repeatQueue">
|
||||
<img id="toggleRepeat" src="<spring:theme code="repeatOn"/>" alt="repeatOn" style="cursor:pointer; height:18px">
|
||||
</a>
|
||||
</span> |</td>
|
||||
@@ -821,7 +929,7 @@
|
||||
|
||||
<td style="white-space:nowrap;">
|
||||
<span class="header">
|
||||
<a href="javascript:onUndo()">
|
||||
<a href="javascript:onUndo()" id="undoQueue">
|
||||
<img src="<spring:theme code="undoImage"/>" alt="undo" style="cursor:pointer; height:18px">
|
||||
</a>
|
||||
</span> |</td>
|
||||
|
||||
@@ -62,6 +62,13 @@ a:hover, a:hover * {
|
||||
color: #DD6900
|
||||
}
|
||||
|
||||
/* Disabled links color and behavior */
|
||||
a.disabled, a.disabled span, span.disabled {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
/* Color for warning messages. */
|
||||
.warning {
|
||||
color: red;
|
||||
|
||||
@@ -83,6 +83,13 @@ a:hover, a:hover * {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* Disabled links color and behavior */
|
||||
a.disabled, a.disabled span, span.disabled {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Colour for warning messages. */
|
||||
.warning {
|
||||
color: #990099;
|
||||
|
||||
@@ -51,6 +51,13 @@ a:hover, a:hover * {
|
||||
color: #a3a3a3;
|
||||
}
|
||||
|
||||
/* Disabled links color and behavior */
|
||||
a.disabled, a.disabled span, span.disabled {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Color for warning messages. */
|
||||
.warning {
|
||||
color: red;
|
||||
|
||||
@@ -43,6 +43,13 @@ a:hover, a:hover * {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
/* Disabled links color and behavior */
|
||||
a.disabled, a.disabled span, span.disabled {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Color for warning messages. */
|
||||
.warning {
|
||||
color: orange;
|
||||
|
||||
@@ -51,6 +51,13 @@ a:hover, a:hover * {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/* Disabled links color and behavior */
|
||||
a.disabled, a.disabled span, span.disabled {
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* Colour for warning messages. */
|
||||
.warning {
|
||||
color: #990099;
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package org.airsonic.player.service;
|
||||
|
||||
import org.airsonic.player.domain.InternetRadio;
|
||||
import org.airsonic.player.domain.InternetRadioSource;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.*;
|
||||
import org.mockito.runners.MockitoJUnitRunner;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class InternetRadioServiceTest {
|
||||
|
||||
String TEST_RADIO_NAME = "Test Radio";
|
||||
String TEST_RADIO_HOMEPAGE = "http://example.com";
|
||||
String TEST_PLAYLIST_URL_MOVE = "http://example.com/stream_move.m3u";
|
||||
String TEST_PLAYLIST_URL_MOVE_LOOP = "http://example.com/stream_infinity_move.m3u";
|
||||
String TEST_PLAYLIST_URL_LARGE = "http://example.com/stream_infinity_repeat.m3u";
|
||||
String TEST_PLAYLIST_URL_LARGE_2 = "http://example.com/stream_infinity_big.m3u";
|
||||
String TEST_PLAYLIST_URL_1 = "http://example.com/stream1.m3u";
|
||||
String TEST_PLAYLIST_URL_2 = "http://example.com/stream2.m3u";
|
||||
String TEST_STREAM_URL_1 = "http://example.com/stream1";
|
||||
String TEST_STREAM_URL_2 = "http://example.com/stream2";
|
||||
String TEST_STREAM_URL_3 = "http://example.com/stream3";
|
||||
String TEST_STREAM_URL_4 = "http://example.com/stream4";
|
||||
String TEST_STREAM_PLAYLIST_CONTENTS_1 = (
|
||||
"http://example.com/stream1\n" +
|
||||
"http://example.com/stream2\n"
|
||||
);
|
||||
String TEST_STREAM_PLAYLIST_CONTENTS_2 = (
|
||||
"#EXTM3U\n" +
|
||||
"#EXTINF:123, Sample artist - Sample title\n" +
|
||||
"http://example.com/stream3\n" +
|
||||
"#EXTINF:321,Example Artist - Example title\n" +
|
||||
"http://example.com/stream4\n"
|
||||
);
|
||||
|
||||
InternetRadio radio1;
|
||||
InternetRadio radio2;
|
||||
InternetRadio radioMove;
|
||||
InternetRadio radioMoveLoop;
|
||||
InternetRadio radioLarge;
|
||||
InternetRadio radioLarge2;
|
||||
|
||||
@Spy
|
||||
InternetRadioService internetRadioService;
|
||||
|
||||
@Before
|
||||
public void setup() throws Exception {
|
||||
|
||||
// Prepare a mock InternetRadio object
|
||||
radio1 = new InternetRadio(1, TEST_RADIO_NAME, TEST_PLAYLIST_URL_1, TEST_RADIO_HOMEPAGE, true, new Date());
|
||||
radio2 = new InternetRadio(2, TEST_RADIO_NAME, TEST_PLAYLIST_URL_2, TEST_RADIO_HOMEPAGE, true, new Date());
|
||||
radioMove = new InternetRadio(3, TEST_RADIO_NAME, TEST_PLAYLIST_URL_MOVE, TEST_RADIO_HOMEPAGE, true, new Date());
|
||||
radioMoveLoop = new InternetRadio(3, TEST_RADIO_NAME, TEST_PLAYLIST_URL_MOVE_LOOP, TEST_RADIO_HOMEPAGE, true, new Date());
|
||||
radioLarge = new InternetRadio(4, TEST_RADIO_NAME, TEST_PLAYLIST_URL_LARGE, TEST_RADIO_HOMEPAGE, true, new Date());
|
||||
radioLarge2 = new InternetRadio(5, TEST_RADIO_NAME, TEST_PLAYLIST_URL_LARGE_2, TEST_RADIO_HOMEPAGE, true, new Date());
|
||||
|
||||
// Prepare the mocked URL connection for the simple playlist
|
||||
HttpURLConnection mockURLConnection1 = Mockito.mock(HttpURLConnection.class);
|
||||
InputStream mockURLInputStream1 = new ByteArrayInputStream(TEST_STREAM_PLAYLIST_CONTENTS_1.getBytes());
|
||||
doReturn(mockURLInputStream1).when(mockURLConnection1).getInputStream();
|
||||
doReturn(HttpURLConnection.HTTP_OK).when(mockURLConnection1).getResponseCode();
|
||||
|
||||
// Prepare the mocked URL connection for the second simple playlist
|
||||
HttpURLConnection mockURLConnection2 = Mockito.mock(HttpURLConnection.class);
|
||||
InputStream mockURLInputStream2 = new ByteArrayInputStream(TEST_STREAM_PLAYLIST_CONTENTS_2.getBytes());
|
||||
doReturn(mockURLInputStream2).when(mockURLConnection2).getInputStream();
|
||||
doReturn(HttpURLConnection.HTTP_OK).when(mockURLConnection2).getResponseCode();
|
||||
|
||||
// Prepare the mocked URL connection for the redirection to simple playlist
|
||||
HttpURLConnection mockURLConnectionMove = Mockito.mock(HttpURLConnection.class);
|
||||
InputStream mockURLInputStreamMove = new ByteArrayInputStream("".getBytes());
|
||||
doReturn(mockURLInputStreamMove).when(mockURLConnectionMove).getInputStream();
|
||||
doReturn(HttpURLConnection.HTTP_MOVED_PERM).when(mockURLConnectionMove).getResponseCode();
|
||||
doReturn(TEST_PLAYLIST_URL_2).when(mockURLConnectionMove).getHeaderField(eq("Location"));
|
||||
|
||||
// Prepare the mocked URL connection for the redirection loop
|
||||
HttpURLConnection mockURLConnectionMoveLoop = Mockito.mock(HttpURLConnection.class);
|
||||
InputStream mockURLInputStreamMoveLoop = new ByteArrayInputStream("".getBytes());
|
||||
doReturn(mockURLInputStreamMoveLoop).when(mockURLConnectionMoveLoop).getInputStream();
|
||||
doReturn(HttpURLConnection.HTTP_MOVED_PERM).when(mockURLConnectionMoveLoop).getResponseCode();
|
||||
doReturn(TEST_PLAYLIST_URL_MOVE_LOOP).when(mockURLConnectionMoveLoop).getHeaderField(eq("Location"));
|
||||
|
||||
// Prepare the mocked URL connection for the 'content too large' test
|
||||
HttpURLConnection mockURLConnectionLarge = Mockito.mock(HttpURLConnection.class);
|
||||
InputStream mockURLInputStreamLarge = new InputStream() {
|
||||
private long pos = 0;
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return TEST_STREAM_PLAYLIST_CONTENTS_2.charAt((int)(pos++ % TEST_STREAM_PLAYLIST_CONTENTS_2.length()));
|
||||
}
|
||||
};
|
||||
doReturn(mockURLInputStreamLarge).when(mockURLConnectionLarge).getInputStream();
|
||||
doReturn(HttpURLConnection.HTTP_OK).when(mockURLConnectionLarge).getResponseCode();
|
||||
|
||||
// Prepare the mocked URL connection for the 'content too large' test
|
||||
// (return a single entry with 'aaaa...' running infinitely long).
|
||||
HttpURLConnection mockURLConnectionLarge2 = Mockito.mock(HttpURLConnection.class);
|
||||
InputStream mockURLInputStreamLarge2 = new InputStream() {
|
||||
private long pos = 0;
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return 0x41;
|
||||
}
|
||||
};
|
||||
doReturn(mockURLInputStreamLarge2).when(mockURLConnectionLarge2).getInputStream();
|
||||
doReturn(HttpURLConnection.HTTP_OK).when(mockURLConnectionLarge2).getResponseCode();
|
||||
|
||||
// Prepare the mock 'connectToURL' method
|
||||
doReturn(mockURLConnection1).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_1)));
|
||||
doReturn(mockURLConnection2).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_2)));
|
||||
doReturn(mockURLConnectionMove).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_MOVE)));
|
||||
doReturn(mockURLConnectionMoveLoop).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_MOVE_LOOP)));
|
||||
doReturn(mockURLConnectionLarge).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_LARGE)));
|
||||
doReturn(mockURLConnectionLarge2).when(internetRadioService).connectToURL(eq(new URL(TEST_PLAYLIST_URL_LARGE_2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseSimplePlaylist() throws Exception {
|
||||
List<InternetRadioSource> radioSources = internetRadioService.getInternetRadioSources(radio1);
|
||||
|
||||
Assert.assertEquals(2, radioSources.size());
|
||||
Assert.assertEquals(TEST_STREAM_URL_1, radioSources.get(0).getStreamUrl());
|
||||
Assert.assertEquals(TEST_STREAM_URL_2, radioSources.get(1).getStreamUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirects() throws Exception {
|
||||
List<InternetRadioSource> radioSources = internetRadioService.getInternetRadioSources(radioMove);
|
||||
|
||||
Assert.assertEquals(2, radioSources.size());
|
||||
Assert.assertEquals(TEST_STREAM_URL_3, radioSources.get(0).getStreamUrl());
|
||||
Assert.assertEquals(TEST_STREAM_URL_4, radioSources.get(1).getStreamUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLargeInput() throws Exception {
|
||||
List<InternetRadioSource> radioSources = internetRadioService.getInternetRadioSources(radioLarge);
|
||||
|
||||
// A PlaylistTooLarge exception is thrown internally, and the
|
||||
// `getInternetRadioSources` method logs it and returns a
|
||||
// limited number of sources.
|
||||
Assert.assertEquals(250, radioSources.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLargeInputURL() throws Exception {
|
||||
List<InternetRadioSource> radioSources = internetRadioService.getInternetRadioSources(radioLarge2);
|
||||
|
||||
// A PlaylistTooLarge exception is thrown internally, and the
|
||||
// `getInternetRadioSources` method logs it and returns a
|
||||
// limited number of bytes from the input.
|
||||
Assert.assertEquals(1, radioSources.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRedirectLoop() throws Exception {
|
||||
List<InternetRadioSource> radioSources = internetRadioService.getInternetRadioSources(radioMoveLoop);
|
||||
|
||||
// A PlaylistHasTooManyRedirects exception is thrown internally,
|
||||
// and the `getInternetRadioSources` method logs it and returns 0 sources.
|
||||
Assert.assertEquals(0, radioSources.size());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user