Limit the amount of data we parse from remote internet radio playlists

master
François-Xavier Thomas 6 years ago
parent afa037611d
commit 760a6e957a
  1. 215
      airsonic-main/src/main/java/org/airsonic/player/service/InternetRadioService.java

@ -3,11 +3,15 @@ package org.airsonic.player.service;
import chameleon.playlist.*; import chameleon.playlist.*;
import org.airsonic.player.domain.InternetRadio; import org.airsonic.player.domain.InternetRadio;
import org.airsonic.player.domain.InternetRadioSource; import org.airsonic.player.domain.InternetRadioSource;
import org.apache.commons.io.input.BoundedInputStream;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.net.URLConnection;
import java.util.*; import java.util.*;
@Service @Service
@ -15,97 +19,208 @@ public class InternetRadioService {
private static final Logger LOG = LoggerFactory.getLogger(InternetRadioService.class); 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
/**
* A list of cached source URLs for remote playlists.
*/
private Map<Integer, List<InternetRadioSource>> cachedSources; private Map<Integer, List<InternetRadioSource>> cachedSources;
/**
* Exception thrown when the remote playlist is too large to be parsed completely.
*/
private class PlaylistTooLarge extends Exception {
public PlaylistTooLarge(String message) {
super(message);
}
}
/**
* Exception thrown when the remote playlist format cannot be determined.
*/
private class PlaylistFormatUnsupported extends Exception {
public PlaylistFormatUnsupported(String message) {
super(message);
}
}
public InternetRadioService() { public InternetRadioService() {
this.cachedSources = new HashMap<>(); this.cachedSources = new HashMap<>();
} }
/**
* Clear the radio source cache.
*/
public void clearInternetRadioSourceCache() { public void clearInternetRadioSourceCache() {
cachedSources.clear(); cachedSources.clear();
} }
public List<InternetRadioSource> getInternetRadioSources(InternetRadio radio) throws Exception { /**
* 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; List<InternetRadioSource> sources;
if (cachedSources.containsKey(radio.getId())) { if (cachedSources.containsKey(radio.getId())) {
LOG.debug("Got cached sources for internet radio {}!", radio.getStreamUrl()); LOG.debug("Got cached sources for internet radio {}!", radio.getStreamUrl());
sources = cachedSources.get(radio.getId()); sources = cachedSources.get(radio.getId());
} else { } else {
LOG.debug("Retrieving sources for internet radio {}...", radio.getStreamUrl()); LOG.debug("Retrieving sources for internet radio {}...", radio.getStreamUrl());
sources = retrieveInternetRadioSources(radio); try {
if (sources.isEmpty()) { sources = retrieveInternetRadioSources(radio);
LOG.warn("No entries found when parsing external playlist."); 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<>();
} }
LOG.info("Retrieved playlist for internet radio {}, got {} sources.", radio.getStreamUrl(), sources.size());
cachedSources.put(radio.getId(), sources); cachedSources.put(radio.getId(), sources);
} }
return 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 { private List<InternetRadioSource> retrieveInternetRadioSources(InternetRadio radio) throws Exception {
// Retrieve radio playlist and parse it return retrieveInternetRadioSources(radio, PLAYLIST_REMOTE_MAX_LENGTH, PLAYLIST_REMOTE_MAX_BYTE_SIZE);
URL playlistUrl = new URL(radio.getStreamUrl()); }
SpecificPlaylist inputPlaylist = null;
try { /**
LOG.debug("Parsing internet radio playlist at {}...", playlistUrl.toString()); * Retrieve a list of sources from the given internet radio.
inputPlaylist = SpecificPlaylistFactory.getInstance().readFrom(playlistUrl); *
} catch (Exception e) { * @param radio an internet radio
LOG.error("Unable to parse internet radio playlist: {}", playlistUrl.toString(), e); * @param maxCount the maximum number of items to read from the remote playlist, or 0 if unlimited
throw e; * @return a list of internet radio sources
} * @throws Exception
if (inputPlaylist == null) { */
LOG.error("Unsupported playlist format: {}", playlistUrl.toString()); private List<InternetRadioSource> retrieveInternetRadioSources(InternetRadio radio, int maxCount, long maxByteSize) throws Exception {
throw new Exception("Unsupported playlist format " + playlistUrl.toString()); // Retrieve the remote playlist
} String playlistUrl = radio.getStreamUrl();
LOG.debug("Parsing internet radio playlist at {}...", playlistUrl);
SpecificPlaylist inputPlaylist = retrievePlaylist(new URL(playlistUrl), maxByteSize);
// Retrieve stream URLs // Retrieve stream URLs
List<InternetRadioSource> entries = new ArrayList<>(); List<InternetRadioSource> entries = new ArrayList<>();
inputPlaylist.toPlaylist().acceptDown(new PlaylistVisitor() { try {
@Override inputPlaylist.toPlaylist().acceptDown(new PlaylistVisitor() {
public void beginVisitPlaylist(Playlist playlist) throws Exception { @Override
public void beginVisitPlaylist(Playlist playlist) throws Exception {
} }
@Override @Override
public void endVisitPlaylist(Playlist playlist) throws Exception { public void endVisitPlaylist(Playlist playlist) throws Exception {
} }
@Override @Override
public void beginVisitParallel(Parallel parallel) throws Exception { public void beginVisitParallel(Parallel parallel) throws Exception {
} }
@Override @Override
public void endVisitParallel(Parallel parallel) throws Exception { public void endVisitParallel(Parallel parallel) throws Exception {
} }
@Override @Override
public void beginVisitSequence(Sequence sequence) throws Exception { public void beginVisitSequence(Sequence sequence) throws Exception {
} }
@Override @Override
public void endVisitSequence(Sequence sequence) throws Exception { public void endVisitSequence(Sequence sequence) throws Exception {
} }
@Override @Override
public void beginVisitMedia(Media media) throws Exception { public void beginVisitMedia(Media media) throws Exception {
String streamUrl = media.getSource().getURI().toString(); // Since we're dealing with remote content, we place a hard
LOG.debug("Got source media at {}", streamUrl); // limit on the maximum number of items to load from the playlist,
entries.add(new InternetRadioSource( // in order to avoid parsing erroneous data.
streamUrl 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 @Override
public void endVisitMedia(Media media) throws Exception { 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; return entries;
} }
/**
* Retrieve playlist data from a given URL.
*
* This throws an ec
*
* @param url URL to the remote playlist
* @param maxByteSize maximum size of the response, in bytes, or 0 if unlimited
* @return the remote playlist data
*/
private SpecificPlaylist retrievePlaylist(URL url, long maxByteSize) throws IOException, PlaylistFormatUnsupported {
URLConnection urlConnection = url.openConnection();
urlConnection.setAllowUserInteraction(false);
urlConnection.setConnectTimeout(10000);
urlConnection.setDoInput(true);
urlConnection.setDoOutput(false);
urlConnection.setReadTimeout(60000);
urlConnection.setUseCaches(true);
urlConnection.connect();
String contentEncoding = urlConnection.getContentEncoding();
SpecificPlaylist playlist = null;
try (InputStream in = urlConnection.getInputStream()) {
if (maxByteSize > 0) {
playlist = SpecificPlaylistFactory.getInstance().readFrom(new BoundedInputStream(in, maxByteSize), contentEncoding);
} else {
playlist = SpecificPlaylistFactory.getInstance().readFrom(in, contentEncoding);
}
}
if (playlist == null) {
throw new PlaylistFormatUnsupported("Unsupported playlist format " + url.toString());
}
return playlist;
}
} }

Loading…
Cancel
Save