From 02d373d9ece82b9092c2ef4aab5741a849f7bd43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20Thomas?= Date: Sun, 7 Apr 2019 18:24:51 +0200 Subject: [PATCH] Play internet radios in MediaElement (fix #408) This allows the user to control playback for the internet radios, which were previously started outside of the main player without any possibility for direct control. --- .../airsonic/player/ajax/PlayQueueInfo.java | 41 +++- .../player/ajax/PlayQueueService.java | 190 +++++++++++++++++- .../controller/RandomPlayQueueController.java | 1 + .../airsonic/player/dao/InternetRadioDao.java | 11 + .../org/airsonic/player/domain/PlayQueue.java | 43 ++-- .../src/main/webapp/WEB-INF/jsp/left.jsp | 15 +- .../src/main/webapp/WEB-INF/jsp/playQueue.jsp | 66 +++++- 7 files changed, 321 insertions(+), 46 deletions(-) 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..71945b70 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 @@ -19,8 +19,11 @@ */ package org.airsonic.player.ajax; +import chameleon.playlist.*; +import chameleon.playlist.Playlist; 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 +31,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; @@ -35,6 +40,7 @@ import org.springframework.web.servlet.support.RequestContextUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.net.URL; import java.util.*; /** @@ -74,8 +80,12 @@ public class PlayQueueService { @Autowired private PlayQueueDao playQueueDao; @Autowired + private InternetRadioDao internetRadioDao; + @Autowired private JWTSecurityService jwtSecurityService; + private static final Logger LOG = LoggerFactory.getLogger(PlayQueueService.class); + /** * Returns the play queue for the player of the current user. * @@ -143,6 +153,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 +243,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 +442,17 @@ 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 { + 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,114 @@ 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 { + + // Retrieve radio playlist and parse it + PlayQueue playQueue = player.getPlayQueue(); + InternetRadio radio = playQueue.getInternetRadio(); + URL playlistUrl = new URL(radio.getStreamUrl()); + SpecificPlaylist inputPlaylist = null; + try { + LOG.info("Parsing playlist at {}...", playlistUrl.toString()); + inputPlaylist = SpecificPlaylistFactory.getInstance().readFrom(playlistUrl); + } catch (Exception e) { + LOG.error("Unable to parse playlist: {}", playlistUrl.toString(), e); + throw e; + } + if (inputPlaylist == null) { + LOG.error("Unsupported playlist format: {}", playlistUrl.toString()); + throw new Exception("Unsupported playlist format " + playlistUrl.toString()); + } + + // Retrieve stream URLs + List entries = new ArrayList<>(); + final String radioHomepageUrl = radio.getHomepageUrl(); + final String radioName = radio.getName(); + 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 { + + // Retrieve stream URL + String streamUrl = media.getSource().getURI().toString(); + // Fake stream title using the URL + String streamTitle = streamUrl; + String streamAlbum = radioName; + String streamGenre = "Internet Radio"; + // Fake entry id so that the source can be selected + Integer streamId = -(1+entries.size()); + Integer streamTrackNumber = entries.size(); + Integer streamYear = 0; + + LOG.info("Got source media at {}...", streamUrl); + + entries.add(new PlayQueueInfo.Entry( + streamId, // Entry id + streamTrackNumber, // Track number + streamTitle, // Use URL as stream title + "", + streamAlbum, // Album name + streamGenre, + streamYear, + "", + 0, + "", + "", + "", + "", + false, + radioHomepageUrl, // Album URL + streamUrl, // Stream URL + streamUrl, // Remote stream URL + null, + null + )); + } - return new PlayQueueInfo(entries, isStopEnabled, playQueue.isRepeatEnabled(), playQueue.isRadioEnabled(), serverSidePlaylist, gain); + @Override + public void endVisitMedia(Media media) throws Exception { + + } + }); + + if (entries.isEmpty()) { + LOG.error("Cannot fetch stream URLs from radio source {}", playlistUrl.toString()); + } + + return entries; } private String formatFileSize(Long fileSize, Locale locale) { @@ -797,4 +961,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/PlayQueue.java b/airsonic-main/src/main/java/org/airsonic/player/domain/PlayQueue.java index f0232cd0..14d18a5d 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. @@ -363,13 +365,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 +414,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/webapp/WEB-INF/jsp/left.jsp b/airsonic-main/src/main/webapp/WEB-INF/jsp/left.jsp index 81da95b7..5ca49ebb 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,10 @@ top.main.location.href = mainLocation; } } + + $('.radio-play').on('click', function() { + top.playQueue.onPlayInternetRadio($(this).data("id"), 0); + }); } function updatePlaylists() { @@ -118,19 +122,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 c9156c42..97762c5e 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 @@