content: 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 static 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
       */
      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
       */
      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) {
  
                  }
  
                  @Override
                  public void endVisitPlaylist(Playlist playlist) {
  
                  }
  
                  @Override
                  public void beginVisitParallel(Parallel parallel) {
  
                  }
  
                  @Override
                  public void endVisitParallel(Parallel parallel) {
  
                  }
  
                  @Override
                  public void beginVisitSequence(Sequence sequence) {
  
                  }
  
                  @Override
                  public void endVisitSequence(Sequence sequence) {
  
                  }
  
                  @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) {
  
                  }
              });
          } 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;
      }
  }
  