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.
master
François-Xavier Thomas 5 years ago
parent 767b39ed5b
commit 02d373d9ec
  1. 41
      airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueInfo.java
  2. 190
      airsonic-main/src/main/java/org/airsonic/player/ajax/PlayQueueService.java
  3. 1
      airsonic-main/src/main/java/org/airsonic/player/controller/RandomPlayQueueController.java
  4. 11
      airsonic-main/src/main/java/org/airsonic/player/dao/InternetRadioDao.java
  5. 43
      airsonic-main/src/main/java/org/airsonic/player/domain/PlayQueue.java
  6. 15
      airsonic-main/src/main/webapp/WEB-INF/jsp/left.jsp
  7. 66
      airsonic-main/src/main/webapp/WEB-INF/jsp/playQueue.jsp

@ -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;
}
}
}

@ -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<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);
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<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;
Locale locale = RequestContextUtils.getLocale(request);
List<PlayQueueInfo.Entry> entries = new ArrayList<PlayQueueInfo.Entry>();
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<PlayQueueInfo.Entry>();
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<PlayQueueInfo.Entry> 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<PlayQueueInfo.Entry> 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;
}
}

@ -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.
*

@ -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 <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.

@ -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 @@
<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}">
<img src="<spring:theme code="playImage"/>" alt="<fmt:message key="common.play"/>" title="<fmt:message key="common.play"/>"></a>
<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>
<c:when test="${empty radio.homepageUrl}">
<c:when test="${empty radio.homepageUrl}">
${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>
</c:choose>
</span>
</p>
</c:forEach>

@ -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>
@ -267,7 +284,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);
@ -290,6 +307,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);
}
@ -410,14 +430,16 @@
function playQueueCallback(playQueue) {
songs = playQueue.entries;
repeatEnabled = playQueue.repeatEnabled;
radioEnabled = playQueue.radioEnabled;
shuffleRadioEnabled = playQueue.shuffleRadioEnabled;
internetRadioEnabled = playQueue.internetRadioEnabled;
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"/>');
@ -449,11 +471,24 @@
if ($("#trackNumber" + id)) {
$("#trackNumber" + id).text(song.trackNumber);
}
if (song.starred) {
$("#starSong" + id).attr("src", "<spring:theme code='ratingOnImage'/>");
if (!internetRadioEnabled) {
// Show star/remove buttons in all cases...
$("#starSong" + id).show();
$("#removeSong" + 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 {
$("#starSong" + id).attr("src", "<spring:theme code='ratingOffImage'/>");
// ...except from when internet radio is playing.
$("#starSong" + id).hide();
$("#removeSong" + id).hide();
}
if ($("#currentImage" + id) && song.streamUrl == currentStreamUrl) {
$("#currentImage" + id).show();
if (isJavaJukeboxPresent()) {
@ -473,6 +508,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);
@ -482,7 +524,13 @@
$("#genre" + id).text(song.genre);
}
if ($("#year" + id)) {
$("#year" + id).text(song.year);
// 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);

Loading…
Cancel
Save