Exemple #1
0
/**
 * Used for creating and evolving the database schema. This class implementes the database schema
 * for Subsonic version 3.0.
 *
 * @author Sindre Mehus
 */
public class Schema30 extends Schema {
  private static final Logger LOG = Logger.getLogger(Schema30.class);

  public void execute(JdbcTemplate template) {

    if (template.queryForInt("select count(*) from version where version = 6") == 0) {
      LOG.info("Updating database schema to version 6.");
      template.execute("insert into version values (6)");
    }

    if (!columnExists(template, "last_fm_enabled", "user_settings")) {
      LOG.info("Database columns 'user_settings.last_fm_*' not found.  Creating them.");
      template.execute(
          "alter table user_settings add last_fm_enabled boolean default false not null");
      template.execute("alter table user_settings add last_fm_username varchar null");
      template.execute("alter table user_settings add last_fm_password varchar null");
      LOG.info("Database columns 'user_settings.last_fm_*' were added successfully.");
    }

    if (!columnExists(template, "transcode_scheme", "user_settings")) {
      LOG.info("Database column 'user_settings.transcode_scheme' not found.  Creating it.");
      template.execute(
          "alter table user_settings add transcode_scheme varchar default '"
              + TranscodeScheme.OFF.name()
              + "' not null");
      LOG.info("Database column 'user_settings.transcode_scheme' was added successfully.");
    }
  }
}
Exemple #2
0
/**
 * Used for creating and evolving the database schema. This class implementes the database schema
 * for Subsonic version 2.7.
 *
 * @author Sindre Mehus
 */
public class Schema27 extends Schema {
  private static final Logger LOG = Logger.getLogger(Schema27.class);

  public void execute(JdbcTemplate template) {

    if (template.queryForInt("select count(*) from version where version = 3") == 0) {
      LOG.info("Updating database schema to version 3.");
      template.execute("insert into version values (3)");

      LOG.info("Converting database column 'music_file_info.path' to varchar_ignorecase.");
      template.execute("drop index idx_music_file_info_path");
      template.execute("alter table music_file_info alter column path varchar_ignorecase not null");
      template.execute("create index idx_music_file_info_path on music_file_info(path)");
      LOG.info("Database column 'music_file_info.path' was converted successfully.");
    }

    if (!columnExists(template, "bytes_streamed", "user")) {
      LOG.info(
          "Database columns 'user.bytes_streamed/downloaded/uploaded' not found.  Creating them.");
      template.execute("alter table user add bytes_streamed bigint default 0 not null");
      template.execute("alter table user add bytes_downloaded bigint default 0 not null");
      template.execute("alter table user add bytes_uploaded bigint default 0 not null");
      LOG.info(
          "Database columns 'user.bytes_streamed/downloaded/uploaded' were added successfully.");
    }
  }
}
Exemple #3
0
/**
 * Used for creating and evolving the database schema. This class implementes the database schema
 * for Subsonic version 3.4.
 *
 * @author Sindre Mehus
 */
public class Schema34 extends Schema {

  private static final Logger LOG = Logger.getLogger(Schema34.class);

  public void execute(JdbcTemplate template) {

    if (template.queryForInt("select count(*) from version where version = 10") == 0) {
      LOG.info("Updating database schema to version 10.");
      template.execute("insert into version values (10)");
    }

    if (!columnExists(template, "ldap_authenticated", "users")) {
      LOG.info("Database column 'users.ldap_authenticated' not found.  Creating it.");
      template.execute("alter table users add ldap_authenticated boolean default false not null");
      LOG.info("Database column 'users.ldap_authenticated' was added successfully.");
    }

    if (!columnExists(template, "party_mode_enabled", "users_settings")) {
      LOG.info("Database column 'users_settings.party_mode_enabled' not found.  Creating it.");
      template.execute(
          "alter table users_settings add party_mode_enabled boolean default false not null");
      LOG.info("Database column 'users_settings.party_mode_enabled' was added successfully.");
    }
  }
}
public class UserService {

  private static final Logger LOG = Logger.getLogger(UserService.class);

  private SecurityService securityService;

  public void init() {
    CheckUser();
  }

  public void CheckUser() {
    Runnable runnable =
        new Runnable() {
          public void run() {
            try {
              LOG.info("Checking User accounts ...");

              securityService.checkAccounts();
              LOG.info("Checking Done");

            } catch (Throwable x) {
              LOG.error("Failed to check User service: " + x, x);
            }
          }
        };
    new Thread(runnable).start();
  }

  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }
}
Exemple #5
0
/**
 * Used for creating and evolving the database schema. This class implementes the database schema
 * for Subsonic version 2.5.
 *
 * @author Sindre Mehus
 */
public class Schema25 extends Schema {
  private static final Logger LOG = Logger.getLogger(Schema25.class);

  public void execute(JdbcTemplate template) {
    if (!tableExists(template, "version")) {
      LOG.info("Database table 'version' not found.  Creating it.");
      template.execute("create table version (version int not null)");
      template.execute("insert into version values (1)");
      LOG.info("Database table 'version' was created successfully.");
    }

    if (!tableExists(template, "role")) {
      LOG.info("Database table 'role' not found.  Creating it.");
      template.execute(
          "create table role ("
              + "id int not null,"
              + "name varchar not null,"
              + "primary key (id))");
      template.execute("insert into role values (1, 'admin')");
      template.execute("insert into role values (2, 'download')");
      template.execute("insert into role values (3, 'upload')");
      template.execute("insert into role values (4, 'playlist')");
      template.execute("insert into role values (5, 'coverart')");
      LOG.info("Database table 'role' was created successfully.");
    }

    if (!tableExists(template, "user")) {
      LOG.info("Database table 'user' not found.  Creating it.");
      template.execute(
          "create table user ("
              + "username varchar not null,"
              + "password varchar not null,"
              + "primary key (username))");
      template.execute("insert into user values ('admin', 'admin')");
      LOG.info("Database table 'user' was created successfully.");
    }

    if (!tableExists(template, "user_role")) {
      LOG.info("Database table 'user_role' not found.  Creating it.");
      template.execute(
          "create table user_role ("
              + "username varchar not null,"
              + "role_id int not null,"
              + "primary key (username, role_id),"
              + "foreign key (username) references user(username),"
              + "foreign key (role_id) references role(id))");
      template.execute("insert into user_role values ('admin', 1)");
      template.execute("insert into user_role values ('admin', 2)");
      template.execute("insert into user_role values ('admin', 3)");
      template.execute("insert into user_role values ('admin', 4)");
      template.execute("insert into user_role values ('admin', 5)");
      LOG.info("Database table 'user_role' was created successfully.");
    }
  }
}
Exemple #6
0
/**
 * Used for creating and evolving the database schema. This class implements the database schema for
 * Subsonic version 4.9.
 *
 * @author Sindre Mehus
 */
public class Schema49 extends Schema {

  private static final Logger LOG = Logger.getLogger(Schema49.class);

  @Override
  public void execute(JdbcTemplate template) {

    if (template.queryForInt("select count(*) from version where version = 21") == 0) {
      LOG.info("Updating database schema to version 21.");
      template.execute("insert into version values (21)");
    }

    if (!columnExists(template, "year", "album")) {
      LOG.info("Database column 'album.year' not found.  Creating it.");
      template.execute("alter table album add year int");
      LOG.info("Database column 'album.year' was added successfully.");
    }

    if (!columnExists(template, "genre", "album")) {
      LOG.info("Database column 'album.genre' not found.  Creating it.");
      template.execute("alter table album add genre varchar");
      LOG.info("Database column 'album.genre' was added successfully.");
    }

    if (!tableExists(template, "genre")) {
      LOG.info("Database table 'genre' not found.  Creating it.");
      template.execute(
          "create table genre (" + "name varchar not null," + "song_count int not null)");

      LOG.info("Database table 'genre' was created successfully.");
    }

    if (!columnExists(template, "album_count", "genre")) {
      LOG.info("Database column 'genre.album_count' not found.  Creating it.");
      template.execute("alter table genre add album_count int default 0 not null");
      LOG.info("Database column 'genre.album_count' was added successfully.");
    }
  }
}
/**
 * Used for creating and evolving the database schema. This class implements the database schema for
 * Subsonic version 4.6, with the additions for MusicCabinet 0.7.4.
 */
public class Schema46MusicCabinet0_7_04 extends Schema {

  private static final Logger LOG = Logger.getLogger(Schema46MusicCabinet0_7_04.class);

  @Override
  public void execute(JdbcTemplate template) {

    if (template.queryForInt("select count(*) from version where version = 28") == 0) {
      LOG.info("Updating database schema to version 28.");
      template.execute("insert into version values (28)");

      if (!columnExists(template, "only_album_artist_recommendations", "user_settings")) {
        LOG.info(
            "Database column 'user_settings.only_album_artist_recommendations' not found. Creating it.");
        template.execute(
            "alter table user_settings add only_album_artist_recommendations boolean default true not null");
        LOG.info(
            "Database column 'user_settings.only_album_artist_recommendations' was added successfully.");
      }
    }
  }
}
/**
 * Controller which produces cover art images.
 *
 * @author Sindre Mehus
 */
public class CoverArtControllerEx implements Controller, LastModified {

  public static final String ALBUM_COVERART_PREFIX = "al-";
  public static final String ARTIST_COVERART_PREFIX = "ar-";

  private static final Logger LOG = Logger.getLogger(CoverArtController.class);

  private SecurityService securityService;
  private MediaFileService mediaFileService;
  private ArtistDao artistDao;
  private AlbumDao albumDao;

  public long getLastModified(HttpServletRequest request) {
    try {
      File file = getImageFile(request);
      if (file == null) {
        return 0; // Request for the default image.
      }
      if (!FileUtil.exists(file)) {
        return -1;
      }

      return FileUtil.lastModified(file);
    } catch (Exception e) {
      return -1;
    }
  }

  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
      throws Exception {
    File file = getImageFile(request);

    if (file != null && !FileUtil.exists(file)) {
      response.sendError(HttpServletResponse.SC_NOT_FOUND);
      return null;
    }

    // Check access.
    if (file != null && !securityService.isReadAllowed(file)) {
      response.sendError(HttpServletResponse.SC_FORBIDDEN);
      return null;
    }

    // Send default image if no path is given. (No need to cache it, since it will be cached in
    // browser.)
    Integer size = ServletRequestUtils.getIntParameter(request, "size");
    boolean typArtist = ServletRequestUtils.getBooleanParameter(request, "typArtist", false);

    if (typArtist == true) {
      if (file == null) {
        sendDefaultArtist(size, response);
        return null;
      }
    } else {
      if (file == null) {
        sendDefault(size, request, response);
        return null;
      }
    }

    // Optimize if no scaling is required.
    if (size == null) {
      sendUnscaled(file, response);
      return null;
    }

    // Send cached image, creating it if necessary.
    try {
      File cachedImage = getCachedImage(file, size);
      sendImage(cachedImage, response);
    } catch (IOException e) {
      sendDefault(size, request, response);
    }

    return null;
  }

  private File getImageFile(HttpServletRequest request) {
    String id = request.getParameter("id");
    if (id != null) {
      if (id.startsWith(ALBUM_COVERART_PREFIX)) {
        return getAlbumImage(Integer.valueOf(id.replace(ALBUM_COVERART_PREFIX, "")));
      }
      if (id.startsWith(ARTIST_COVERART_PREFIX)) {
        return getArtistImage(Integer.valueOf(id.replace(ARTIST_COVERART_PREFIX, "")));
      }

      return getMediaFileImage(Integer.valueOf(id));
    }

    String path = StringUtils.trimToNull(request.getParameter("path"));
    return path != null ? new File(path) : null;
  }

  private File getArtistImage(int id) {
    Artist artist = artistDao.getArtist(id);
    return artist == null || artist.getCoverArtPath() == null
        ? null
        : new File(artist.getCoverArtPath());
  }

  private File getAlbumImage(int id) {
    Album album = albumDao.getAlbum(id);
    return album == null || album.getCoverArtPath() == null
        ? null
        : new File(album.getCoverArtPath());
  }

  private File getMediaFileImage(int id) {
    MediaFile mediaFile = mediaFileService.getMediaFile(id);
    return mediaFile == null ? null : mediaFileService.getCoverArt(mediaFile);
  }

  private void sendImage(File file, HttpServletResponse response) throws IOException {
    response.setContentType(StringUtil.getMimeType(FilenameUtils.getExtension(file.getName())));
    InputStream in = new FileInputStream(file);
    try {
      IOUtils.copy(in, response.getOutputStream());
    } finally {
      IOUtils.closeQuietly(in);
    }
  }

  private void sendDefault(Integer size, HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    try {
      int id = Integer.parseInt(request.getParameter("id"));
      MediaFile mediaFile = mediaFileService.getMediaFile(id);
      sendAutoGenerated(size, mediaFile, response);
    } catch (Throwable x) {
      sendFallback(size, response);
    }
  }

  private void sendAutoGenerated(Integer size, MediaFile mediaFile, HttpServletResponse response)
      throws IOException {
    if (mediaFile.isFile()) {
      mediaFile = mediaFileService.getParentOf(mediaFile);
    }
    if (size == null) {
      size = CoverArtScheme.MEDIUM.getSize() * 2;
    }
    BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
    Graphics2D graphics = image.createGraphics();
    AutoCover autoCover = new AutoCover(graphics, mediaFile, size);
    autoCover.paintCover();
    graphics.dispose();

    response.setContentType(StringUtil.getMimeType("png"));
    ImageIO.write(image, "png", response.getOutputStream());
  }

  private void sendFallback(Integer size, HttpServletResponse response) throws IOException {
    if (response.getContentType() == null) {
      response.setContentType(StringUtil.getMimeType("png"));
    }
    InputStream in = null;
    try {
      in = getClass().getResourceAsStream("default_cover.png");
      BufferedImage image = ImageIO.read(in);
      if (size != null) {
        image = scale(image, size, size);
      }
      ImageIO.write(image, "png", response.getOutputStream());
    } finally {
      IOUtils.closeQuietly(in);
    }
  }

  private void sendDefaultArtist(Integer size, HttpServletResponse response) throws IOException {
    InputStream in = null;
    try {
      in = getClass().getResourceAsStream("default_artist.png");
      BufferedImage imageArtist = ImageIO.read(in);
      if (size != null) {
        imageArtist = scale(imageArtist, size, size);
      }
      ImageIO.write(imageArtist, "png", response.getOutputStream());
    } finally {
      IOUtils.closeQuietly(in);
    }
  }

  private void sendUnscaled(File file, HttpServletResponse response) throws IOException {
    JaudiotaggerParser parser = new JaudiotaggerParser();
    if (!parser.isApplicable(file)) {
      response.setContentType(StringUtil.getMimeType(FilenameUtils.getExtension(file.getName())));
    }
    InputStream in = null;
    try {
      in = getImageInputStream(file);
      IOUtils.copy(in, response.getOutputStream());
    } finally {
      IOUtils.closeQuietly(in);
    }
  }

  private File getCachedImage(File file, int size) throws IOException {
    String md5 = DigestUtils.md5Hex(file.getPath());
    File cachedImage = new File(getImageCacheDirectory(size), md5 + ".jpeg");

    // Is cache missing or obsolete?
    if (!cachedImage.exists() || FileUtil.lastModified(file) > cachedImage.lastModified()) {
      InputStream in = null;
      OutputStream out = null;
      try {
        in = getImageInputStream(file);
        out = new FileOutputStream(cachedImage);
        BufferedImage image = ImageIO.read(in);
        if (image == null) {
          throw new Exception("Unable to decode image.");
        }

        image = scale(image, size, size);
        ImageIO.write(image, "jpeg", out);

      } catch (Throwable x) {
        // Delete corrupt (probably empty) thumbnail cache.
        LOG.warn("Failed to create thumbnail for " + file, x);
        IOUtils.closeQuietly(out);
        cachedImage.delete();
        throw new IOException("Failed to create thumbnail for " + file + ". " + x.getMessage());

      } finally {
        IOUtils.closeQuietly(in);
        IOUtils.closeQuietly(out);
      }
    }
    return cachedImage;
  }

  /**
   * Returns an input stream to the image in the given file. If the file is an audio file, the
   * embedded album art is returned.
   */
  private InputStream getImageInputStream(File file) throws IOException {
    JaudiotaggerParser parser = new JaudiotaggerParser();
    if (parser.isApplicable(file)) {
      MediaFile mediaFile = mediaFileService.getMediaFile(file);
      return new ByteArrayInputStream(parser.getImageData(mediaFile));
    } else {
      return new FileInputStream(file);
    }
  }

  private synchronized File getImageCacheDirectory(int size) {
    File dir = new File(SettingsService.getSubsonicHome(), "thumbs");
    dir = new File(dir, String.valueOf(size));
    if (!dir.exists()) {
      if (dir.mkdirs()) {
        LOG.info("Created thumbnail cache " + dir);
      } else {
        LOG.error("Failed to create thumbnail cache " + dir);
      }
    }

    return dir;
  }

  public static BufferedImage scale(BufferedImage image, int width, int height) {
    int w = image.getWidth();
    int h = image.getHeight();
    BufferedImage thumb = image;

    // For optimal results, use step by step bilinear resampling - halfing the size at each step.
    do {
      w /= 2;
      h /= 2;
      if (w < width) {
        w = width;
      }
      if (h < height) {
        h = height;
      }

      double thumbRatio = (double) width / (double) height;
      double aspectRatio = (double) w / (double) h;

      //            LOG.debug("## thumbsRatio: " + thumbRatio);
      //            LOG.debug("## aspectRatio: " + aspectRatio);

      if (thumbRatio < aspectRatio) {
        h = (int) (w / aspectRatio);
      } else {
        w = (int) (h * aspectRatio);
      }

      BufferedImage temp = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
      Graphics2D g2 = temp.createGraphics();
      g2.setRenderingHint(
          RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
      g2.drawImage(thumb, 0, 0, w, h, null);
      g2.dispose();

      thumb = temp;
    } while (w != width);

    // FIXME: check
    if (thumb.getHeight() > thumb.getWidth()) {
      thumb = thumb.getSubimage(0, 0, thumb.getWidth(), thumb.getWidth());
    }
    return thumb;
  }

  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  public void setMediaFileService(MediaFileService mediaFileService) {
    this.mediaFileService = mediaFileService;
  }

  public void setArtistDao(ArtistDao artistDao) {
    this.artistDao = artistDao;
  }

  public void setAlbumDao(AlbumDao albumDao) {
    this.albumDao = albumDao;
  }

  static class AutoCover {

    private static final int[] COLORS = {
      0xCF8E25
    }; // 0x33B5E5, 0xAA66CC, 0x99CC00, 0xFFBB33, 0xFF4444};
    private final Graphics2D graphics;
    private final MediaFile mediaFile;
    private final int size;
    private final Color color;

    public AutoCover(Graphics2D graphics, MediaFile mediaFile, int size) {
      this.graphics = graphics;
      this.mediaFile = mediaFile;
      this.size = size;

      int rgb = COLORS[Math.abs(mediaFile.getId()) % COLORS.length];
      this.color = new Color(rgb);
    }

    public void paintCover() {
      graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
      graphics.setRenderingHint(
          RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

      graphics.setPaint(color);
      graphics.fillRect(0, 0, size, size);

      int y = size * 1 / 3;
      graphics.setPaint(new GradientPaint(0, y, new Color(82, 82, 82), 0, size, Color.BLACK));
      graphics.fillRect(0, y, size, size / 1);

      graphics.setPaint(Color.WHITE);
      float fontSize = 3.0f + size * 0.06f;
      Font font = new Font(Font.SANS_SERIF, Font.BOLD, (int) fontSize);
      graphics.setFont(font);

      String album = mediaFile.getAlbumName();
      if (album != null) {
        graphics.drawString(album, size * 0.05f, size * 0.25f);
      }
      String artist = mediaFile.getAlbumArtist();
      if (artist == null) {
        artist = mediaFile.getArtist();
      }
      if (artist != null) {
        graphics.drawString(artist, size * 0.05f, size * 0.45f);
      }

      int borderWidth = size / 50;
      graphics.fillRect(0, 0, borderWidth, size);
      graphics.fillRect(size - borderWidth, 0, size - borderWidth, size);
      graphics.fillRect(0, 0, size, borderWidth);
      graphics.fillRect(0, size - borderWidth, size, size);

      graphics.setColor(Color.BLACK);
      graphics.drawRect(0, 0, size - 1, size - 1);
    }
  }
}
/**
 * Used for creating and evolving the database schema. This class implementes the database schema
 * for Subsonic version 3.2.
 *
 * @author Sindre Mehus
 */
public class Schema32 extends Schema {
  private static final Logger LOG = Logger.getLogger(Schema32.class);

  public void execute(JdbcTemplate template) {

    if (template.queryForInt("select count(*) from version where version = 8") == 0) {
      LOG.info("Updating database schema to version 8.");
      template.execute("insert into version values (8)");
    }

    if (!columnExists(template, "show_now_playing", "user_settings")) {
      LOG.info("Database column 'user_settings.show_now_playing' not found.  Creating it.");
      template.execute(
          "alter table user_settings add show_now_playing boolean default false not null");
      LOG.info("Database column 'user_settings.show_now_playing' was added successfully.");
    }

    if (!columnExists(template, "selected_music_folder_id", "user_settings")) {
      LOG.info("Database column 'user_settings.selected_music_folder_id' not found.  Creating it.");
      template.execute(
          "alter table user_settings add selected_music_folder_id int default -1 not null");
      LOG.info("Database column 'user_settings.selected_music_folder_id' was added successfully.");
    }

    if (!tableExists(template, "podcast_channel")) {
      LOG.info("Database table 'podcast_channel' not found.  Creating it.");
      template.execute(
          "create table podcast_channel ("
              + "id identity,"
              + "url varchar not null,"
              + "title varchar,"
              + "description varchar,"
              + "status varchar not null,"
              + "error_message varchar)");
      LOG.info("Database table 'podcast_channel' was created successfully.");
    }

    if (!tableExists(template, "podcast_episode")) {
      LOG.info("Database table 'podcast_episode' not found.  Creating it.");
      template.execute(
          "create table podcast_episode ("
              + "id identity,"
              + "channel_id int not null,"
              + "url varchar not null,"
              + "path varchar,"
              + "title varchar,"
              + "description varchar,"
              + "publish_date datetime,"
              + "duration varchar,"
              + "bytes_total bigint,"
              + "bytes_downloaded bigint,"
              + "status varchar not null,"
              + "error_message varchar,"
              + "foreign key (channel_id) references podcast_channel(id) on delete cascade)");
      LOG.info("Database table 'podcast_episode' was created successfully.");
    }

    if (template.queryForInt("select count(*) from role where id = 7") == 0) {
      LOG.info("Role 'podcast' not found in database. Creating it.");
      template.execute("insert into role values (7, 'podcast')");
      template.execute(
          "insert into user_role "
              + "select distinct u.username, 7 from user u, user_role ur "
              + "where u.username = ur.username and ur.role_id = 1");
      LOG.info("Role 'podcast' was created successfully.");
    }
  }
}
Exemple #10
0
/**
 * Provides database services for caching.
 *
 * @author Sindre Mehus
 */
public class CacheDao {

  private static final Logger LOG = Logger.getLogger(CacheDao.class);
  private static final int BATCH_SIZE = 100;

  private EmbeddedObjectContainer db;
  private final ReadWriteLock dbLock = new ReentrantReadWriteLock();
  private int writeCount = 0;
  private final File dbFile;

  public CacheDao() {

    File subsonicHome = SettingsService.getSubsonicHome();
    File dbDir = new File(subsonicHome, "cache");
    dbFile = new File(dbDir, "cache.dat");

    if (!dbDir.exists()) {
      dbDir.mkdirs();
    }

    //        if (dbFile.exists()) {
    //            try {
    //                Defragment.defrag(dbFile.getPath());
    //            }catch (IOException e) {
    //                e.printStackTrace();
    //            }
    //        }

    try {
      openDatabase(dbFile);
    } catch (Throwable x) {
      LOG.error("Failed to open " + dbFile + ", deleting it: " + x);

      dbFile.delete();
      openDatabase(dbFile);
    }
  }

  private void openDatabase(File dbFile) {
    EmbeddedConfiguration config = Db4oEmbedded.newConfiguration();
    config.common().objectClass(CacheElement.class).objectField("id").indexed(true);
    config.common().objectClass(CacheElement.class).cascadeOnUpdate(true);
    config.common().objectClass(CacheElement.class).cascadeOnDelete(true);
    config.common().objectClass(CacheElement.class).cascadeOnActivate(true);

    db = Db4oEmbedded.openFile(config, dbFile.getPath());
  }

  /** Recreates the database. */
  public void clearDatabase() {
    dbLock.writeLock().lock();
    try {
      db.close();
      dbFile.delete();
      openDatabase(dbFile);
    } finally {
      dbLock.writeLock().unlock();
    }
  }

  /**
   * Creates a new cache element.
   *
   * @param element The cache element to create (or update).
   */
  public void createCacheElement(CacheElement element) {
    dbLock.writeLock().lock();
    try {
      deleteCacheElement(element);
      db.store(element);

      if (writeCount++ == BATCH_SIZE) {
        db.commit();
        writeCount = 0;
      }

    } finally {
      dbLock.writeLock().unlock();
    }
  }

  public CacheElement getCacheElement(int type, String key) {
    dbLock.readLock().lock();
    try {

      ObjectSet<CacheElement> result = db.query(new CacheElementPredicate(type, key));
      if (result.size() > 1) {
        LOG.error(
            "Programming error. Got "
                + result.size()
                + " cache elements of type "
                + type
                + " and key "
                + key);
      }
      return result.isEmpty() ? null : result.get(0);

    } finally {
      dbLock.readLock().unlock();
    }
  }

  /** Deletes the cache element with the given type and key. */
  private void deleteCacheElement(CacheElement element) {
    // Retrieve it from the database first.
    element = getCacheElement(element.getType(), element.getKey());
    if (element != null) {
      db.delete(element);
    }
  }

  private static class CacheElementPredicate extends Predicate<CacheElement> {
    private static final long serialVersionUID = 54911003002373726L;

    private final long id;

    public CacheElementPredicate(int type, String key) {
      id = CacheElement.createId(type, key);
    }

    @Override
    public boolean match(CacheElement candidate) {
      return candidate.getId() == id;
    }
  }
}
/**
 * Controller which produces cover art images.
 *
 * @author Sindre Mehus
 */
public class CoverArtController implements Controller, LastModified {

  private static final Logger LOG = Logger.getLogger(CoverArtController.class);

  private SecurityService securityService;
  private MediaFileService mediaFileService;

  public long getLastModified(HttpServletRequest request) {
    String path = request.getParameter("path");
    if (StringUtils.trimToNull(path) == null) {
      return 0;
    }

    File file = new File(path);
    if (!FileUtil.exists(file)) {
      return -1;
    }

    return FileUtil.lastModified(file);
  }

  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
      throws Exception {
    String path = request.getParameter("path");
    File file = (path == null || path.length() == 0) ? null : new File(path);
    Integer size = ServletRequestUtils.getIntParameter(request, "size");

    // Check access.
    if (file != null && !securityService.isReadAllowed(file)) {
      response.sendError(HttpServletResponse.SC_FORBIDDEN);
      return null;
    }

    // Optimize if no scaling is required.
    if (size == null) {
      sendUnscaled(file, response);
      return null;
    }

    // Send default image if no path is given. (No need to cache it, since it will be cached in
    // browser.)
    if (file == null) {
      sendDefault(size, response);
      return null;
    }

    // Send cached image, creating it if necessary.
    try {
      File cachedImage = getCachedImage(file, size);
      sendImage(cachedImage, response);
    } catch (IOException e) {
      sendDefault(size, response);
    }

    return null;
  }

  private void sendImage(File file, HttpServletResponse response) throws IOException {
    InputStream in = new FileInputStream(file);
    try {
      IOUtils.copy(in, response.getOutputStream());
    } finally {
      IOUtils.closeQuietly(in);
    }
  }

  private void sendDefault(Integer size, HttpServletResponse response) throws IOException {
    InputStream in = null;
    try {
      in = getClass().getResourceAsStream("default_cover.jpg");
      BufferedImage image = ImageIO.read(in);
      image = scale(image, size, size);
      ImageIO.write(image, "jpeg", response.getOutputStream());
    } finally {
      IOUtils.closeQuietly(in);
    }
  }

  private void sendUnscaled(File file, HttpServletResponse response) throws IOException {
    InputStream in = null;
    try {
      in = getImageInputStream(file);
      IOUtils.copy(in, response.getOutputStream());
    } finally {
      IOUtils.closeQuietly(in);
    }
  }

  private File getCachedImage(File file, int size) throws IOException {
    String md5 = DigestUtils.md5Hex(file.getPath());
    File cachedImage = new File(getImageCacheDirectory(size), md5 + ".jpeg");

    // Is cache missing or obsolete?
    if (!cachedImage.exists() || FileUtil.lastModified(file) > cachedImage.lastModified()) {
      InputStream in = null;
      OutputStream out = null;
      try {
        in = getImageInputStream(file);
        out = new FileOutputStream(cachedImage);
        BufferedImage image = ImageIO.read(in);
        if (image == null) {
          throw new Exception("Unable to decode image.");
        }

        image = scale(image, size, size);
        ImageIO.write(image, "jpeg", out);

      } catch (Throwable x) {
        // Delete corrupt (probably empty) thumbnail cache.
        LOG.warn("Failed to create thumbnail for " + file, x);
        IOUtils.closeQuietly(out);
        cachedImage.delete();
        throw new IOException("Failed to create thumbnail for " + file + ". " + x.getMessage());

      } finally {
        IOUtils.closeQuietly(in);
        IOUtils.closeQuietly(out);
      }
    }
    return cachedImage;
  }

  /**
   * Returns an input stream to the image in the given file. If the file is an audio file, the
   * embedded album art is returned.
   */
  private InputStream getImageInputStream(File file) throws IOException {
    JaudiotaggerParser parser = new JaudiotaggerParser();
    if (parser.isApplicable(file)) {
      MediaFile mediaFile = mediaFileService.getMediaFile(file);
      return new ByteArrayInputStream(parser.getImageData(mediaFile));
    } else {
      return new FileInputStream(file);
    }
  }

  private synchronized File getImageCacheDirectory(int size) {
    File dir = new File(SettingsService.getSubsonicHome(), "thumbs");
    dir = new File(dir, String.valueOf(size));
    if (!dir.exists()) {
      if (dir.mkdirs()) {
        LOG.info("Created thumbnail cache " + dir);
      } else {
        LOG.error("Failed to create thumbnail cache " + dir);
      }
    }

    return dir;
  }

  public static BufferedImage scale(BufferedImage image, int width, int height) {
    int w = image.getWidth();
    int h = image.getHeight();
    BufferedImage thumb = image;

    // For optimal results, use step by step bilinear resampling - halfing the size at each step.
    do {
      w /= 2;
      h /= 2;
      if (w < width) {
        w = width;
      }
      if (h < height) {
        h = height;
      }

      BufferedImage temp = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
      Graphics2D g2 = temp.createGraphics();
      g2.setRenderingHint(
          RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
      g2.drawImage(thumb, 0, 0, temp.getWidth(), temp.getHeight(), null);
      g2.dispose();

      thumb = temp;
    } while (w != width);

    return thumb;
  }

  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  public void setMediaFileService(MediaFileService mediaFileService) {
    this.mediaFileService = mediaFileService;
  }
}
/**
 * Provides database services for artists.
 *
 * @author Sindre Mehus
 */
public class LastFMArtistSimilarDao extends AbstractDao {

  private static final Logger LOG = Logger.getLogger(LastFMArtistSimilarDao.class);

  private static final String COLUMNS = "id, artist_name, artist_mbid, similar_name, similar_mbid";

  private final RowMapper rowMapper = new LastFMArtistSimilarMapper();

  //    public List<LastFMArtistSimilar> getAllSimilar() {
  //        return query("select " + COLUMNS + " from lastfm_artist_similar order by name",
  // rowMapper);
  //    }

  public LastFMArtistSimilar getSimilar(String artistName) {
    return queryOne(
        "select " + COLUMNS + " from lastfm_artist_similar where lower(artist_name)=?",
        rowMapper,
        artistName.toLowerCase());
  }

  public List<String> getSimilarArtist(String ArtistName) {
    return queryForStrings(
        "select distinct SIMILAR_NAME from lastfm_artist_similar where lower(artist_name)=?",
        ArtistName.toLowerCase());
  }

  public LastFMArtistSimilar getSimilar(int mbid) {
    return queryOne(
        "select " + COLUMNS + " from lastfm_artist_similar where mbid=?", rowMapper, mbid);
  }

  public synchronized void createOrUpdateLastFMArtistSimilar(
      LastFMArtistSimilar lastFMArtistSimilar) {
    String sql =
        "update lastfm_artist_similar set "
            + "artist_name=?,"
            + "artist_mbid=?,"
            + "similar_name=?, "
            + "similar_mbid=? "
            + "where artist_name=? and similar_name=?";

    int n =
        update(
            sql,
            lastFMArtistSimilar.getArtistName(),
            lastFMArtistSimilar.getArtistMbid(),
            lastFMArtistSimilar.getSimilarName(),
            lastFMArtistSimilar.getSimilarMbid(),
            lastFMArtistSimilar.getArtistName(),
            lastFMArtistSimilar.getSimilarName());

    if (n == 0) {

      update(
          "insert into lastfm_artist_similar ("
              + COLUMNS
              + ") values ("
              + questionMarks(COLUMNS)
              + ")",
          null,
          lastFMArtistSimilar.getArtistName(),
          lastFMArtistSimilar.getArtistMbid(),
          lastFMArtistSimilar.getSimilarName(),
          lastFMArtistSimilar.getSimilarMbid());
    }

    //  int id = queryForInt("select id from lastfm_artist where artistname=?", null,
    // lastFMArtist.getArtistname());
    //   lastFMArtist.setId(id);
  }

  private static class LastFMArtistSimilarMapper
      implements ParameterizedRowMapper<LastFMArtistSimilar> {
    public LastFMArtistSimilar mapRow(ResultSet rs, int rowNum) throws SQLException {
      return new LastFMArtistSimilar(
          rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5));
    }
  }
}
/** Controller for the page used to administrate last.fm group subscriptions. */
public class GroupSettingsController extends ParameterizableViewController {

  private LastFmService lastFmService;
  private GroupWeeklyArtistChartService artistChartService;

  private static final Logger LOG = Logger.getLogger(GroupSettingsController.class);

  @Override
  protected ModelAndView handleRequestInternal(
      HttpServletRequest request, HttpServletResponse response) throws Exception {

    Map<String, Object> map = new HashMap<String, Object>();

    String[] groups = request.getParameterValues("group");
    if (groups != null) {
      updateGroups(groups);
    }

    ModelAndView result = super.handleRequestInternal(request, response);
    map.put("lastFmGroups", lastFmService.getLastFmGroups());

    result.addObject("model", map);
    return result;
  }

  /*
   * Store selected groups, and asynchronously fetch group artists from last.fm.
   */
  private void updateGroups(String[] groups) {
    List<LastFmGroup> lastFmGroups = new ArrayList<>();
    for (String group : groups) {
      if (StringUtils.trimToNull(group) != null) {
        lastFmGroups.add(new LastFmGroup(group));
      }
    }
    lastFmService.setLastFmGroups(lastFmGroups);

    Executors.newSingleThreadExecutor()
        .execute(
            new Runnable() {
              @Override
              public void run() {
                try {
                  LOG.debug("Update artist chart.");
                  artistChartService.updateSearchIndex();
                  LOG.debug("Artist chart updated.");
                } catch (Throwable t) {
                  LOG.warn("Couldn't update group artists!", t);
                }
              }
            });
  }

  public void setLastFmService(LastFmService lastFmService) {
    this.lastFmService = lastFmService;
  }

  public void setGroupWeeklyArtistChartService(GroupWeeklyArtistChartService artistChartService) {
    this.artistChartService = artistChartService;
  }
}
Exemple #14
0
/**
 * Represents a file or directory containing music. Media files can be put in a {@link Playlist},
 * and may be streamed to remote players. All media files are located in a configurable root music
 * folder.
 *
 * @author Sindre Mehus
 */
public class MediaFile implements Serializable, Comparable<MediaFile> {

  private static final long serialVersionUID = -3826007043440542822L;

  private static final Logger LOG = Logger.getLogger(MediaFile.class);

  private int id;
  private File file;
  private boolean isFile;
  private boolean isDirectory;
  private boolean isVideo;
  private long lastModified;
  private MetaData metaData;

  /** Preferred usage: {@link MediaFileService#getmediaFile}. */
  public MediaFile(int id, File file) {
    this.id = id;
    this.file = file;

    // Cache these values for performance.
    isFile = file.isFile();
    isDirectory = file.isDirectory();
    lastModified = file.lastModified();
    isVideo = isFile && isVideoFile(file);

    if (isFile) {
      getMetaData();
      LOG.debug("created file with id " + id + " from file " + file + ", metadata = " + metaData);
    }
  }

  /** Empty constructor. Used for testing purposes only. */
  protected MediaFile() {
    isFile = true;
  }

  public MediaFile(int id) {
    this.id = id;
    this.isFile = true;
    this.metaData = new MetaData();
  }

  public int getId() {
    return id;
  }

  public File getFile() {
    return file;
  }

  public void setFile(File file) {
    this.file = file;
  }

  public boolean isFile() {
    return isFile;
  }

  /**
   * Returns whether this music file is a directory.
   *
   * @return Whether this music file is a directory.
   */
  public boolean isDirectory() {
    return isDirectory;
  }

  /**
   * Returns whether this media file is a video.
   *
   * @return Whether this media file is a video.
   */
  public boolean isVideo() {
    return isVideo;
  }

  /**
   * Returns whether this music file is one of the root music folders.
   *
   * @return Whether this music file is one of the root music folders.
   */
  public boolean isRoot() {
    MediaFolderService mediaFolderSettings = ServiceLocator.getMediaFolderService();
    List<MediaFolder> folders = mediaFolderSettings.getAllMediaFolders();
    for (MediaFolder folder : folders) {
      if (file.equals(folder.getPath())) {
        return true;
      }
    }
    return false;
  }

  /**
   * Returns the time this music file was last modified.
   *
   * @return The time since this music file was last modified, in milliseconds since the epoch.
   */
  public long lastModified() {
    return lastModified;
  }

  /**
   * Returns the length of the music file. The return value is unspecified if this music file is a
   * directory.
   *
   * @return The length, in bytes, of the music file, or or <code>0L</code> if the file does not
   *     exist
   */
  public long length() {
    return file.length();
  }

  /**
   * Returns whether this music file exists.
   *
   * @return Whether this music file exists.
   */
  public boolean exists() {
    return file.exists();
  }

  /**
   * Returns the name of the music file. This is normally just the last name in the pathname's name
   * sequence.
   *
   * @return The name of the music file.
   */
  public String getName() {
    return file.getName();
  }

  /**
   * Same as {@link #getName}, but without file suffix (unless this music file represents a
   * directory).
   *
   * @return The name of the file without the suffix
   */
  public String getNameWithoutSuffix() {
    String name = getName();
    if (isDirectory()) {
      return name;
    }
    int i = name.lastIndexOf('.');
    return i == -1 ? name : name.substring(0, i);
  }

  /**
   * Returns the file suffix, e.g., "mp3".
   *
   * @return The file suffix.
   */
  public String getSuffix() {
    return FilenameUtils.getExtension(getName());
  }

  /**
   * Returns the full pathname as a string.
   *
   * @return The full pathname as a string.
   */
  public String getPath() {
    return file.getPath();
  }

  /**
   * Returns meta data for this music file.
   *
   * @return Meta data (artist, album, title etc) for this music file.
   */
  public MetaData getMetaData() {
    if (metaData == null) {
      MetaDataParser parser = ServiceLocator.getMetaDataParserFactory().getParser(this);
      metaData = (parser == null) ? null : parser.getMetaData(this);
    }
    return metaData;
  }

  /**
   * Returns the title of the music file, by attempting to parse relevant meta-data embedded in the
   * file, for instance ID3 tags in MP3 files.
   *
   * <p>If this music file is a directory, or if no tags are found, this method is equivalent to
   * {@link #getNameWithoutSuffix}.
   *
   * @return The song title of this music file.
   */
  public String getTitle() {
    return getMetaData() == null ? getNameWithoutSuffix() : getMetaData().getTitle();
  }

  /**
   * Returns the parent music file.
   *
   * @return The parent music file, or <code>null</code> if no parent exists.
   * @throws IOException If an I/O error occurs.
   */
  public MediaFile getParent() throws IOException {
    File parent = file.getParentFile();
    return parent == null ? null : createMediaFile(parent);
  }

  /**
   * Returns all music files that are children of this music file.
   *
   * @param includeFiles Whether files should be included in the result.
   * @param includeDirectories Whether directories should be included in the result.
   * @throws IOException If an I/O error occurs.
   */
  public List<MediaFile> getChildren(FileFilter filter) throws IOException {
    File[] children = FileUtil.listFiles(file, filter);
    List<MediaFile> result = new ArrayList<MediaFile>(children.length);

    for (File child : children) {
      try {
        if (acceptMedia(child)) {
          result.add(createMediaFile(child));
        }
      } catch (SecurityException x) {
        LOG.warn("Failed to create mediaFile for " + child, x);
      }
    }

    Collections.sort(result);

    return result;
  }

  private MediaFile createMediaFile(File file) {
    return ServiceLocator.getMediaFileService().getNonIndexedMediaFile(file);
  }

  private boolean acceptMedia(File file) throws IOException {

    if (isExcluded(file)) {
      return false;
    }

    if (file.isDirectory()) {
      return true;
    }

    return isMusicFile(file) || isVideoFile(file);
  }

  private static boolean isMusicFile(File file) {
    return FilenameUtils.isExtension(
        file.getName(), ServiceLocator.getSettingsService().getMusicFileTypesAsArray());
  }

  private static boolean isVideoFile(File file) {
    return FilenameUtils.isExtension(
        file.getName(), ServiceLocator.getSettingsService().getVideoFileTypesAsArray());
  }

  /**
   * @param file The child file in question.
   * @return Whether the child file is excluded.
   */
  private boolean isExcluded(File file) throws IOException {

    // Exclude all hidden files starting with a "." or "@eaDir" (thumbnail
    // dir created on Synology devices).
    return (file.getName().startsWith(".") || file.getName().startsWith("@eaDir"));
  }

  public boolean equals(Object o) {
    if (o == null) return false;
    if (o == this) return true;
    if (o.getClass() != getClass()) return false;

    return id == ((MediaFile) o).id;
  }

  /**
   * Returns the hash code of this music file.
   *
   * @return The hash code of this music file.
   */
  @Override
  public int hashCode() {
    return id;
  }

  /**
   * Equivalent to {@link #getPath}.
   *
   * @return This music file as a string.
   */
  @Override
  public String toString() {
    return getPath();
  }

  @Override
  public int compareTo(MediaFile mf) {

    if (!isDirectory && mf.isDirectory) {
      return 1;
    } else if (isDirectory && !mf.isDirectory) {
      return -1;
    } else if (isDirectory && mf.isDirectory) {
      return getName().compareTo(mf.getName());
    }

    MetaData md1 = getMetaData();
    MetaData md2 = mf.getMetaData();

    CompareToBuilder ctb =
        new CompareToBuilder()
            .append(nvl(md1.getDiscNumber(), 1), nvl(md2.getDiscNumber(), 1))
            .append(nvl(md1.getTrackNumber(), -1), nvl(md2.getTrackNumber(), -1))
            .append(md1.getTitle(), md2.getTitle());

    return ctb.toComparison();
  }

  private int nvl(Integer value, int defaultValue) {
    return value == null ? defaultValue : value;
  }
}
/**
 * Provides AJAX-enabled services for changing cover art images.
 *
 * <p>This class is used by the DWR framework (http://getahead.ltd.uk/dwr/).
 *
 * @author Sindre Mehus
 */
public class CoverArtService {

  private static final Logger LOG = Logger.getLogger(CoverArtService.class);

  private SecurityService securityService;
  private MediaFileService mediaFileService;

  /**
   * Downloads and saves the cover art at the given URL.
   *
   * @param path The directory in which to save the image.
   * @param url The image URL.
   * @return The error string if something goes wrong, <code>null</code> otherwise.
   */
  public String setCoverArtImage(String path, String url) {
    try {
      saveCoverArt(path, url);
      return null;
    } catch (Exception x) {
      LOG.warn("Failed to save cover art for " + path, x);
      return x.toString();
    }
  }

  private void saveCoverArt(String path, String url) throws Exception {
    InputStream input = null;
    HttpClient client = new DefaultHttpClient();

    try {
      HttpConnectionParams.setConnectionTimeout(client.getParams(), 20 * 1000); // 20 seconds
      HttpConnectionParams.setSoTimeout(client.getParams(), 20 * 1000); // 20 seconds
      HttpGet method = new HttpGet(url);

      HttpResponse response = client.execute(method);
      input = response.getEntity().getContent();

      // Attempt to resolve proper suffix.
      String suffix = "jpg";
      if (url.toLowerCase().endsWith(".gif")) {
        suffix = "gif";
      } else if (url.toLowerCase().endsWith(".png")) {
        suffix = "png";
      }

      // Check permissions.
      File newCoverFile = new File(path, "cover." + suffix);
      if (!securityService.isWriteAllowed(newCoverFile)) {
        throw new Exception("Permission denied: " + StringUtil.toHtml(newCoverFile.getPath()));
      }

      // If file exists, create a backup.
      backup(newCoverFile, new File(path, "cover.backup." + suffix));

      // Write file.
      IOUtils.copy(input, new FileOutputStream(newCoverFile));

      MediaFile mediaFile = mediaFileService.getMediaFile(path);

      // Rename existing cover file if new cover file is not the preferred.
      try {
        File coverFile = mediaFileService.getCoverArt(mediaFile);
        if (coverFile != null) {
          if (!newCoverFile.equals(coverFile)) {
            coverFile.renameTo(new File(coverFile.getCanonicalPath() + ".old"));
            LOG.info("Renamed old image file " + coverFile);
          }
        }
      } catch (Exception x) {
        LOG.warn("Failed to rename existing cover file.", x);
      }

      mediaFileService.refreshMediaFile(mediaFile);

    } finally {
      IOUtils.closeQuietly(input);
      client.getConnectionManager().shutdown();
    }
  }

  private void backup(File newCoverFile, File backup) {
    if (newCoverFile.exists()) {
      if (backup.exists()) {
        backup.delete();
      }
      if (newCoverFile.renameTo(backup)) {
        LOG.info("Backed up old image file to " + backup);
      } else {
        LOG.warn("Failed to create image file backup " + backup);
      }
    }
  }

  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  public void setMediaFileService(MediaFileService mediaFileService) {
    this.mediaFileService = mediaFileService;
  }
}
/**
 * Subclass of {@link InputStream} which provides on-the-fly transcoding. Instances of <code>
 * TranscodeInputStream</code> can be chained together, for instance to convert from OGG to WAV to
 * MP3.
 *
 * @author Sindre Mehus
 */
public class TranscodeInputStream extends InputStream {

  private static final Logger LOG = Logger.getLogger(TranscodeInputStream.class);

  private InputStream processInputStream;
  private OutputStream processOutputStream;
  private Process process;

  /**
   * Creates a transcoded input stream by executing the given command. If <code>in</code> is not
   * null, data from it is copied to the command.
   *
   * @param command The command to execute.
   * @param in Data to feed to the command. May be <code>null</code>.
   * @throws IOException If an I/O error occurs.
   */
  public TranscodeInputStream(String[] command, final InputStream in) throws IOException {

    StringBuffer buf = new StringBuffer("Starting transcoder: ");
    for (String s : command) {
      buf.append('[').append(s).append("] ");
    }
    LOG.debug(buf);

    process = Runtime.getRuntime().exec(command);
    processOutputStream = process.getOutputStream();
    processInputStream = process.getInputStream();

    // Must read stderr from the process, otherwise it may block.
    final String name = command[0];
    new InputStreamReaderThread(process.getErrorStream(), name, true).start();

    // Copy data in a separate thread
    if (in != null) {
      new Thread(name + " TranscodedInputStream copy thread") {
        public void run() {
          try {
            IOUtils.copy(in, processOutputStream);
          } catch (IOException x) {
            // Intentionally ignored. Will happen if the remote player closes the stream.
          } finally {
            IOUtils.closeQuietly(in);
            IOUtils.closeQuietly(processOutputStream);
          }
        }
      }.start();
    }
  }

  /** @see InputStream#read() */
  public int read() throws IOException {
    return processInputStream.read();
  }

  /** @see InputStream#read(byte[]) */
  public int read(byte[] b) throws IOException {
    return processInputStream.read(b);
  }

  /** @see InputStream#read(byte[], int, int) */
  public int read(byte[] b, int off, int len) throws IOException {
    return processInputStream.read(b, off, len);
  }

  /** @see InputStream#close() */
  public void close() throws IOException {
    IOUtils.closeQuietly(processInputStream);
    IOUtils.closeQuietly(processOutputStream);

    if (process != null) {
      process.destroy();
    }
  }
}
/**
 * Provides services for instantiating and caching music files and cover art.
 *
 * @author Sindre Mehus
 */
public class MediaFileService {

  private Ehcache mediaFileCache;
  private Ehcache coverArtCache;

  private SettingsService settingsService;
  private SecurityService securityService;
  private LibraryBrowserService libraryBrowserService;

  private static final Logger LOG = Logger.getLogger(MediaFileService.class);

  public MediaFile getMediaFile(int trackId) {
    if (!mediaFileCache.isElementInMemory(trackId)) {
      LOG.debug("media file not in memory, load meta data from db!");
      loadMediaFiles(Arrays.asList(trackId));
      if (!mediaFileCache.isElementInMemory(trackId)) {
        // trackId might refer to a playing track/video that since have been removed
        return null;
      }
    }
    return (MediaFile) mediaFileCache.get(trackId).getValue();
  }

  public List<MediaFile> getMediaFiles(List<Integer> trackIds) {
    List<MediaFile> mediaFiles = new ArrayList<>();
    for (Integer trackId : trackIds) {
      MediaFile mediaFile = getMediaFile(trackId);
      if (mediaFile != null) {
        mediaFiles.add(mediaFile);
      }
    }
    return mediaFiles;
  }

  public void loadMediaFiles(List<Integer> mediaFileIds) {
    List<Integer> missingMediaFileIds = new ArrayList<>(mediaFileIds);
    for (Iterator<Integer> it = missingMediaFileIds.iterator(); it.hasNext(); ) {
      if (mediaFileCache.isElementInMemory(it.next())) {
        it.remove();
      }
    }
    if (missingMediaFileIds.size() > 0) {
      List<Track> tracks = libraryBrowserService.getTracks(missingMediaFileIds);
      for (Track track : tracks) {
        mediaFileCache.put(new Element(track.getId(), getMediaFile(track)));
      }
    }
  }

  public static MediaFile getMediaFile(Track track) {
    MediaFile mediaFile = new MediaFile(track.getId());
    mediaFile.setFile(new File(track.getMetaData().getPath()));
    mediaFile.getMetaData().setAlbum(track.getMetaData().getAlbum());
    mediaFile.getMetaData().setAlbumId(track.getMetaData().getAlbumId());
    mediaFile.getMetaData().setArtist(track.getMetaData().getArtist());
    mediaFile.getMetaData().setArtistId(track.getMetaData().getArtistId());
    mediaFile.getMetaData().setComposer(track.getMetaData().getComposer());
    mediaFile.getMetaData().setBitRate((int) track.getMetaData().getBitrate());
    mediaFile.getMetaData().setDiscNumber((int) track.getMetaData().getDiscNr());
    mediaFile.getMetaData().setDuration((int) track.getMetaData().getDuration());
    mediaFile.getMetaData().setFileSize((long) track.getMetaData().getSize());
    mediaFile
        .getMetaData()
        .setFormat(track.getMetaData().getMediaType().getFilesuffix().toLowerCase());
    mediaFile.getMetaData().setGenre(track.getMetaData().getGenre());
    mediaFile.getMetaData().setTitle(track.getName());
    mediaFile.getMetaData().setTrackNumber((int) track.getMetaData().getTrackNr());
    mediaFile.getMetaData().setVariableBitRate(track.getMetaData().isVbr());
    mediaFile.getMetaData().setYear(toYear(track.getMetaData().getYear()));
    mediaFile.getMetaData().setHasLyrics(track.getMetaData().hasLyrics());
    return mediaFile;
  }

  private static String toYear(short year) {
    return year == 0 ? null : "" + year;
  }

  public File getCoverArt(MediaFile mediaFile) throws IOException {
    String coverArtFile = null;
    Element element = coverArtCache.get(mediaFile.getId());
    if (element == null) {
      coverArtFile = libraryBrowserService.getCoverArtFileForTrack(mediaFile.getId());
      coverArtCache.put(new Element(mediaFile.getId(), coverArtFile));
    } else {
      coverArtFile = (String) element.getObjectValue();
    }
    return coverArtFile == null ? null : new File(coverArtFile);
  }

  public List<Album> getAlbums(List<com.github.hakko.musiccabinet.domain.model.music.Album> alb) {
    return getAlbums(alb, false);
  }

  public List<Album> getAlbums(
      List<com.github.hakko.musiccabinet.domain.model.music.Album> alb, boolean onlyLocalArtwork) {
    List<Album> albums = new ArrayList<>();
    List<Integer> trackIds = new ArrayList<>();

    boolean preferLastFmArtwork = settingsService.isPreferLastFmArtwork();
    for (com.github.hakko.musiccabinet.domain.model.music.Album a : alb) {
      Album album = new Album();
      album.setArtistId(a.getArtist().getId());
      album.setArtistName(a.getArtist().getName());
      album.setId(a.getId());
      album.setTitle(a.getName());
      album.setYear(a.getYear());
      album.setCoverArtPath(a.getCoverArtPath());
      album.setCoverArtUrl(a.getCoverArtURL());
      album.setTrackIds(a.getTrackIds());
      if (album.getCoverArtPath() != null && album.getCoverArtUrl() != null) {
        if (preferLastFmArtwork && !onlyLocalArtwork) {
          album.setCoverArtPath(null);
        } else {
          album.setCoverArtUrl(null);
        }
      }
      trackIds.addAll(a.getTrackIds());
      albums.add(album);
    }
    loadMediaFiles(trackIds);
    return albums;
  }

  /*
   * Inefficient MediaFile instantiation. Only to be used when we don't have an id,
   * like when loading files from a saved playlist.
   */
  public MediaFile getMediaFile(String absolutePath) {
    int trackId = libraryBrowserService.getTrackId(absolutePath);
    return trackId == -1 ? null : getMediaFile(trackId);
  }

  /*
   * Instantiate MediaFile by path name. Only to be used by file based browser.
   */
  public MediaFile getNonIndexedMediaFile(String pathName) {
    return getNonIndexedMediaFile(new File(pathName));
  }

  /*
   * Instantiate MediaFile by path name. Only to be used by file based browser.
   */
  public MediaFile getNonIndexedMediaFile(File file) {

    int fileId = -Math.abs(file.getAbsolutePath().hashCode());

    LOG.debug(
        "request for non indexed media file " + file.getAbsolutePath() + ", cache as " + fileId);

    Element element = mediaFileCache.get(fileId);
    if (element != null) {

      // Check if cache is up-to-date.
      MediaFile cachedMediaFile = (MediaFile) element.getObjectValue();
      if (cachedMediaFile.lastModified() >= file.lastModified()) {
        return cachedMediaFile;
      }
    }

    if (!securityService.isReadAllowed(file)) {
      throw new SecurityException("Access denied to file " + file);
    }

    MediaFile mediaFile = new MediaFile(fileId, file);
    mediaFileCache.put(new Element(fileId, mediaFile));

    return mediaFile;
  }

  /**
   * Register in service locator so that non-Spring objects can access me. This method is invoked
   * automatically by Spring.
   */
  public void init() {
    ServiceLocator.setMediaFileService(this);
  }

  public void setSettingsService(SettingsService settingsService) {
    this.settingsService = settingsService;
  }

  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  public void setLibraryBrowserService(LibraryBrowserService libraryBrowserService) {
    this.libraryBrowserService = libraryBrowserService;
  }

  public void setMediaFileCache(Ehcache mediaFileCache) {
    this.mediaFileCache = mediaFileCache;
    mediaFileCache.removeAll(); // TODO : remove in version 0.8 or so, just clearing traces from 4.6
  }

  public void setChildDirCache(Ehcache childDirCache) {
    childDirCache.removeAll(); // TODO : remove in version 0.8 or so, just clearing traces from 4.6
  }

  public void setCoverArtCache(Ehcache coverArtCache) {
    this.coverArtCache = coverArtCache;
  }
}
/**
 * A controller which streams the content of a {@link net.sourceforge.subsonic.domain.PlayQueue} to
 * a remote {@link Player}.
 *
 * @author Sindre Mehus
 */
public class StreamController implements Controller {

  private static final Logger LOG = Logger.getLogger(StreamController.class);

  private StatusService statusService;
  private PlayerService playerService;
  private PlaylistService playlistService;
  private SecurityService securityService;
  private SettingsService settingsService;
  private TranscodingService transcodingService;
  private AudioScrobblerService audioScrobblerService;
  private MediaFileService mediaFileService;
  private SearchService searchService;

  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
      throws Exception {

    TransferStatus status = null;
    PlayQueueInputStream in = null;
    Player player = playerService.getPlayer(request, response, false, true);
    User user = securityService.getUserByName(player.getUsername());

    try {

      if (!user.isStreamRole()) {
        response.sendError(
            HttpServletResponse.SC_FORBIDDEN,
            "Streaming is forbidden for user " + user.getUsername());
        return null;
      }

      // If "playlist" request parameter is set, this is a Podcast request. In that case, create a
      // separate
      // play queue (in order to support multiple parallel Podcast streams).
      Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist");
      boolean isPodcast = playlistId != null;
      if (isPodcast) {
        PlayQueue playQueue = new PlayQueue();
        playQueue.addFiles(false, playlistService.getFilesInPlaylist(playlistId));
        player.setPlayQueue(playQueue);
        Util.setContentLength(response, playQueue.length());
        LOG.info("Incoming Podcast request for playlist " + playlistId);
      }

      String contentType = StringUtil.getMimeType(request.getParameter("suffix"));
      response.setContentType(contentType);

      String preferredTargetFormat = request.getParameter("format");
      Integer maxBitRate = ServletRequestUtils.getIntParameter(request, "maxBitRate");
      if (Integer.valueOf(0).equals(maxBitRate)) {
        maxBitRate = null;
      }

      VideoTranscodingSettings videoTranscodingSettings = null;

      // Is this a request for a single file (typically from the embedded Flash player)?
      // In that case, create a separate playlist (in order to support multiple parallel streams).
      // Also, enable partial download (HTTP byte range).
      MediaFile file = getSingleFile(request);
      boolean isSingleFile = file != null;
      LongRange range = null;

      if (isSingleFile) {
        PlayQueue playQueue = new PlayQueue();
        playQueue.addFiles(true, file);
        player.setPlayQueue(playQueue);

        if (!file.isVideo()) {
          response.setIntHeader("ETag", file.getId());
          //          response.setHeader("Accept-Ranges", "bytes");
        }

        TranscodingService.Parameters parameters =
            transcodingService.getParameters(
                file, player, maxBitRate, preferredTargetFormat, null, false);
        long fileLength = getFileLength(parameters);
        boolean isConversion = parameters.isDownsample() || parameters.isTranscode();
        boolean estimateContentLength =
            ServletRequestUtils.getBooleanParameter(request, "estimateContentLength", false);
        boolean isHls = ServletRequestUtils.getBooleanParameter(request, "hls", false);

        range = getRange(request, file);
        if (range != null) {
          LOG.info("Got range: " + range);

          //                    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
          //                    Util.setContentLength(response, fileLength -
          // range.getMinimumLong());
          //                    long firstBytePos = range.getMinimumLong();
          //                    long lastBytePos = fileLength - 1;
          //                    response.setHeader("Content-Range", "bytes " + firstBytePos + "-" +
          // lastBytePos + "/" + fileLength);

          ///
          if (isConversion) {
            response.setHeader("Accept-Ranges", "none");
          } else {
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            long maxLength = fileLength;
            if (maxLength > range.getMaximumLong()) maxLength = range.getMaximumLong() + 1;
            Util.setContentLength(response, Math.max(maxLength - range.getMinimumLong(), 0));
            long firstBytePos = range.getMinimumLong();
            long lastBytePos = maxLength - 1;
            response.setHeader(
                "Content-Range", "bytes " + firstBytePos + "-" + lastBytePos + "/" + fileLength);
          }
          ///
        } else if (!isHls && (!isConversion || estimateContentLength)) {
          Util.setContentLength(response, fileLength);
        }

        if (isHls) {
          response.setContentType(StringUtil.getMimeType("ts")); // HLS is always MPEG TS.
        } else {
          String transcodedSuffix =
              transcodingService.getSuffix(player, file, preferredTargetFormat);
          response.setContentType(StringUtil.getMimeType(transcodedSuffix));
        }

        if (file.isVideo() || isHls) {
          videoTranscodingSettings = createVideoTranscodingSettings(file, request);
        }
      }

      if (request.getMethod().equals("HEAD")) {
        return null;
      }

      // Terminate any other streams to this player.
      if (!isPodcast && !isSingleFile) {
        for (TransferStatus streamStatus : statusService.getStreamStatusesForPlayer(player)) {
          if (streamStatus.isActive()) {
            streamStatus.terminate();
          }
        }
      }

      status = statusService.createStreamStatus(player);

      in =
          new PlayQueueInputStream(
              player,
              status,
              maxBitRate,
              preferredTargetFormat,
              videoTranscodingSettings,
              transcodingService,
              audioScrobblerService,
              mediaFileService,
              searchService);
      OutputStream out = RangeOutputStream.wrap(response.getOutputStream(), range);

      // Enabled SHOUTcast, if requested.
      boolean isShoutCastRequested = "1".equals(request.getHeader("icy-metadata"));
      if (isShoutCastRequested && !isSingleFile) {
        response.setHeader("icy-metaint", "" + ShoutCastOutputStream.META_DATA_INTERVAL);
        response.setHeader("icy-notice1", "This stream is served using FutureSonic");
        response.setHeader("icy-notice2", "FutureSonic - Free media streamer - sonic.lt");
        response.setHeader("icy-name", "FutureSonic");
        response.setHeader("icy-genre", "Mixed");
        response.setHeader("icy-url", "http://sonic.lt/");
        out = new ShoutCastOutputStream(out, player.getPlayQueue(), settingsService);
      }

      final int BUFFER_SIZE = 2048;
      byte[] buf = new byte[BUFFER_SIZE];

      while (true) {

        // Check if stream has been terminated.
        if (status.terminated()) {
          return null;
        }

        if (player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) {
          if (isPodcast || isSingleFile) {
            break;
          } else {
            sendDummy(buf, out);
          }
        } else {

          int n = in.read(buf);
          if (n == -1) {
            if (isPodcast || isSingleFile) {
              break;
            } else {
              sendDummy(buf, out);
            }
          } else {
            out.write(buf, 0, n);
          }
        }
      }

    } finally {
      if (status != null) {
        securityService.updateUserByteCounts(user, status.getBytesTransfered(), 0L, 0L);
        statusService.removeStreamStatus(status);
      }
      IOUtils.closeQuietly(in);
    }
    return null;
  }

  private MediaFile getSingleFile(HttpServletRequest request)
      throws ServletRequestBindingException {
    String path = request.getParameter("path");
    if (path != null) {
      return mediaFileService.getMediaFile(path);
    }
    Integer id = ServletRequestUtils.getIntParameter(request, "id");
    if (id != null) {
      return mediaFileService.getMediaFile(id);
    }
    return null;
  }

  private long getFileLength(TranscodingService.Parameters parameters) {
    MediaFile file = parameters.getMediaFile();

    if (!parameters.isDownsample() && !parameters.isTranscode()) {
      return file.getFileSize();
    }
    Integer duration = file.getDurationSeconds();
    Integer maxBitRate = parameters.getMaxBitRate();

    if (duration == null) {
      LOG.warn("Unknown duration for " + file + ". Unable to estimate transcoded size.");
      return file.getFileSize();
    }

    if (maxBitRate == null) {
      LOG.error("Unknown bit rate for " + file + ". Unable to estimate transcoded size.");
      return file.getFileSize();
    }

    return duration * maxBitRate * 1000L / 8L;
  }

  private LongRange getRange(HttpServletRequest request, MediaFile file) {

    // First, look for "Range" HTTP header.
    LongRange range = StringUtil.parseRange(request.getHeader("Range"));
    if (range != null) {
      return range;
    }

    // Second, look for "offsetSeconds" request parameter.
    String offsetSeconds = request.getParameter("offsetSeconds");
    range = parseAndConvertOffsetSeconds(offsetSeconds, file);
    if (range != null) {
      return range;
    }

    return null;
  }

  private LongRange parseAndConvertOffsetSeconds(String offsetSeconds, MediaFile file) {
    if (offsetSeconds == null) {
      return null;
    }

    try {
      Integer duration = file.getDurationSeconds();
      Long fileSize = file.getFileSize();
      if (duration == null || fileSize == null) {
        return null;
      }
      float offset = Float.parseFloat(offsetSeconds);

      // Convert from time offset to byte offset.
      long byteOffset = (long) (fileSize * (offset / duration));
      return new LongRange(byteOffset, Long.MAX_VALUE);

    } catch (Exception x) {
      LOG.error("Failed to parse and convert time offset: " + offsetSeconds, x);
      return null;
    }
  }

  private VideoTranscodingSettings createVideoTranscodingSettings(
      MediaFile file, HttpServletRequest request) throws ServletRequestBindingException {
    Integer existingWidth = file.getWidth();
    Integer existingHeight = file.getHeight();
    Integer maxBitRate = ServletRequestUtils.getIntParameter(request, "maxBitRate");
    int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0);
    int defaultDuration =
        file.getDurationSeconds() == null
            ? Integer.MAX_VALUE
            : file.getDurationSeconds() - timeOffset;
    int duration = ServletRequestUtils.getIntParameter(request, "duration", defaultDuration);
    boolean hls = ServletRequestUtils.getBooleanParameter(request, "hls", false);

    Dimension dim = getRequestedVideoSize(request.getParameter("size"));
    if (dim == null) {
      dim = getSuitableVideoSize(existingWidth, existingHeight, maxBitRate);
    }

    return new VideoTranscodingSettings(dim.width, dim.height, timeOffset, duration, hls);
  }

  protected Dimension getRequestedVideoSize(String sizeSpec) {
    if (sizeSpec == null) {
      return null;
    }

    Pattern pattern = Pattern.compile("^(\\d+)x(\\d+)$");
    Matcher matcher = pattern.matcher(sizeSpec);
    if (matcher.find()) {
      int w = Integer.parseInt(matcher.group(1));
      int h = Integer.parseInt(matcher.group(2));
      if (w >= 0 && h >= 0 && w <= 2000 && h <= 2000) {
        return new Dimension(w, h);
      }
    }
    return null;
  }

  protected Dimension getSuitableVideoSize(
      Integer existingWidth, Integer existingHeight, Integer maxBitRate) {
    if (maxBitRate == null) {
      return new Dimension(400, 300);
    }

    int w, h;
    if (maxBitRate < 400) {
      w = 400;
      h = 300;
    } else if (maxBitRate < 600) {
      w = 480;
      h = 360;
    } else if (maxBitRate < 1800) {
      w = 640;
      h = 480;
    } else {
      w = 960;
      h = 720;
    }

    if (existingWidth == null || existingHeight == null) {
      return new Dimension(w, h);
    }

    if (existingWidth < w || existingHeight < h) {
      return new Dimension(even(existingWidth), even(existingHeight));
    }

    double aspectRate = existingWidth.doubleValue() / existingHeight.doubleValue();
    h = (int) Math.round(w / aspectRate);

    return new Dimension(even(w), even(h));
  }

  // Make sure width and height are multiples of two, as some versions of ffmpeg require it.
  private int even(int size) {
    return size + (size % 2);
  }

  /** Feed the other end with some dummy data to keep it from reconnecting. */
  private void sendDummy(byte[] buf, OutputStream out) throws IOException {
    try {
      Thread.sleep(2000);
    } catch (InterruptedException x) {
      LOG.warn("Interrupted in sleep.", x);
    }
    Arrays.fill(buf, (byte) 0xFF);
    out.write(buf);
    out.flush();
  }

  public void setStatusService(StatusService statusService) {
    this.statusService = statusService;
  }

  public void setPlayerService(PlayerService playerService) {
    this.playerService = playerService;
  }

  public void setPlaylistService(PlaylistService playlistService) {
    this.playlistService = playlistService;
  }

  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  public void setSettingsService(SettingsService settingsService) {
    this.settingsService = settingsService;
  }

  public void setTranscodingService(TranscodingService transcodingService) {
    this.transcodingService = transcodingService;
  }

  public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) {
    this.audioScrobblerService = audioScrobblerService;
  }

  public void setMediaFileService(MediaFileService mediaFileService) {
    this.mediaFileService = mediaFileService;
  }

  public void setSearchService(SearchService searchService) {
    this.searchService = searchService;
  }
}
/**
 * Performs Lucene-based searching and indexing.
 *
 * @author Sindre Mehus
 * @version $Id$
 * @see MediaScannerService
 */
public class SearchService {

  private static final Logger LOG = Logger.getLogger(SearchService.class);

  private static final String FIELD_ID = "id";
  private static final String FIELD_TITLE = "title";
  private static final String FIELD_ALBUM = "album";
  private static final String FIELD_ARTIST = "artist";
  private static final String FIELD_GENRE = "genre";
  private static final String FIELD_YEAR = "year";
  private static final String FIELD_MEDIA_TYPE = "mediaType";
  private static final String FIELD_FOLDER = "folder";
  private static final String FIELD_FOLDER_ID = "folderId";

  private static final Version LUCENE_VERSION = Version.LUCENE_30;
  private static final String LUCENE_DIR = "lucene2";

  private MediaFileService mediaFileService;
  private ArtistDao artistDao;
  private AlbumDao albumDao;

  private IndexWriter artistWriter;
  private IndexWriter artistId3Writer;
  private IndexWriter albumWriter;
  private IndexWriter albumId3Writer;
  private IndexWriter songWriter;

  public SearchService() {
    removeLocks();
  }

  public void startIndexing() {
    try {
      artistWriter = createIndexWriter(ARTIST);
      artistId3Writer = createIndexWriter(ARTIST_ID3);
      albumWriter = createIndexWriter(ALBUM);
      albumId3Writer = createIndexWriter(ALBUM_ID3);
      songWriter = createIndexWriter(SONG);
    } catch (Exception x) {
      LOG.error("Failed to create search index.", x);
    }
  }

  public void index(MediaFile mediaFile) {
    try {
      if (mediaFile.isFile()) {
        songWriter.addDocument(SONG.createDocument(mediaFile));
      } else if (mediaFile.isAlbum()) {
        albumWriter.addDocument(ALBUM.createDocument(mediaFile));
      } else {
        artistWriter.addDocument(ARTIST.createDocument(mediaFile));
      }
    } catch (Exception x) {
      LOG.error("Failed to create search index for " + mediaFile, x);
    }
  }

  public void index(Artist artist, MusicFolder musicFolder) {
    try {
      artistId3Writer.addDocument(ARTIST_ID3.createDocument(artist, musicFolder));
    } catch (Exception x) {
      LOG.error("Failed to create search index for " + artist, x);
    }
  }

  public void index(Album album) {
    try {
      albumId3Writer.addDocument(ALBUM_ID3.createDocument(album));
    } catch (Exception x) {
      LOG.error("Failed to create search index for " + album, x);
    }
  }

  public void stopIndexing() {
    try {
      artistWriter.optimize();
      artistId3Writer.optimize();
      albumWriter.optimize();
      albumId3Writer.optimize();
      songWriter.optimize();
    } catch (Exception x) {
      LOG.error("Failed to create search index.", x);
    } finally {
      FileUtil.closeQuietly(artistId3Writer);
      FileUtil.closeQuietly(artistWriter);
      FileUtil.closeQuietly(albumWriter);
      FileUtil.closeQuietly(albumId3Writer);
      FileUtil.closeQuietly(songWriter);
    }
  }

  public SearchResult search(
      SearchCriteria criteria, List<MusicFolder> musicFolders, IndexType indexType) {
    SearchResult result = new SearchResult();
    int offset = criteria.getOffset();
    int count = criteria.getCount();
    result.setOffset(offset);

    IndexReader reader = null;
    try {
      reader = createIndexReader(indexType);
      Searcher searcher = new IndexSearcher(reader);
      Analyzer analyzer = new SubsonicAnalyzer();

      MultiFieldQueryParser queryParser =
          new MultiFieldQueryParser(
              LUCENE_VERSION, indexType.getFields(), analyzer, indexType.getBoosts());

      BooleanQuery query = new BooleanQuery();
      query.add(queryParser.parse(analyzeQuery(criteria.getQuery())), BooleanClause.Occur.MUST);

      List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
      for (MusicFolder musicFolder : musicFolders) {
        if (indexType == ALBUM_ID3 || indexType == ARTIST_ID3) {
          musicFolderQueries.add(
              new SpanTermQuery(
                  new Term(FIELD_FOLDER_ID, NumericUtils.intToPrefixCoded(musicFolder.getId()))));
        } else {
          musicFolderQueries.add(
              new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath())));
        }
      }
      query.add(
          new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])),
          BooleanClause.Occur.MUST);

      TopDocs topDocs = searcher.search(query, null, offset + count);
      result.setTotalHits(topDocs.totalHits);

      int start = Math.min(offset, topDocs.totalHits);
      int end = Math.min(start + count, topDocs.totalHits);
      for (int i = start; i < end; i++) {
        Document doc = searcher.doc(topDocs.scoreDocs[i].doc);
        switch (indexType) {
          case SONG:
          case ARTIST:
          case ALBUM:
            MediaFile mediaFile = mediaFileService.getMediaFile(Integer.valueOf(doc.get(FIELD_ID)));
            addIfNotNull(mediaFile, result.getMediaFiles());
            break;
          case ARTIST_ID3:
            Artist artist = artistDao.getArtist(Integer.valueOf(doc.get(FIELD_ID)));
            addIfNotNull(artist, result.getArtists());
            break;
          case ALBUM_ID3:
            Album album = albumDao.getAlbum(Integer.valueOf(doc.get(FIELD_ID)));
            addIfNotNull(album, result.getAlbums());
            break;
          default:
            break;
        }
      }

    } catch (Throwable x) {
      LOG.error("Failed to execute Lucene search.", x);
    } finally {
      FileUtil.closeQuietly(reader);
    }
    return result;
  }

  private String analyzeQuery(String query) throws IOException {
    StringBuilder result = new StringBuilder();
    ASCIIFoldingFilter filter =
        new ASCIIFoldingFilter(new StandardTokenizer(LUCENE_VERSION, new StringReader(query)));
    TermAttribute termAttribute = filter.getAttribute(TermAttribute.class);
    while (filter.incrementToken()) {
      result.append(termAttribute.term()).append("* ");
    }
    return result.toString();
  }

  /**
   * Returns a number of random songs.
   *
   * @param criteria Search criteria.
   * @return List of random songs.
   */
  public List<MediaFile> getRandomSongs(RandomSearchCriteria criteria) {
    List<MediaFile> result = new ArrayList<MediaFile>();

    IndexReader reader = null;
    try {
      reader = createIndexReader(SONG);
      Searcher searcher = new IndexSearcher(reader);

      BooleanQuery query = new BooleanQuery();
      query.add(
          new TermQuery(new Term(FIELD_MEDIA_TYPE, MediaFile.MediaType.MUSIC.name().toLowerCase())),
          BooleanClause.Occur.MUST);
      if (criteria.getGenre() != null) {
        String genre = normalizeGenre(criteria.getGenre());
        query.add(new TermQuery(new Term(FIELD_GENRE, genre)), BooleanClause.Occur.MUST);
      }
      if (criteria.getFromYear() != null || criteria.getToYear() != null) {
        NumericRangeQuery<Integer> rangeQuery =
            NumericRangeQuery.newIntRange(
                FIELD_YEAR, criteria.getFromYear(), criteria.getToYear(), true, true);
        query.add(rangeQuery, BooleanClause.Occur.MUST);
      }

      List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
      for (MusicFolder musicFolder : criteria.getMusicFolders()) {
        musicFolderQueries.add(
            new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath())));
      }
      query.add(
          new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])),
          BooleanClause.Occur.MUST);

      TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE);
      List<ScoreDoc> scoreDocs = Lists.newArrayList(topDocs.scoreDocs);
      Random random = new Random(System.currentTimeMillis());

      while (!scoreDocs.isEmpty() && result.size() < criteria.getCount()) {
        int index = random.nextInt(scoreDocs.size());
        Document doc = searcher.doc(scoreDocs.remove(index).doc);
        int id = Integer.valueOf(doc.get(FIELD_ID));
        try {
          addIfNotNull(mediaFileService.getMediaFile(id), result);
        } catch (Exception x) {
          LOG.warn("Failed to get media file " + id);
        }
      }

    } catch (Throwable x) {
      LOG.error("Failed to search or random songs.", x);
    } finally {
      FileUtil.closeQuietly(reader);
    }
    return result;
  }

  private static String normalizeGenre(String genre) {
    return genre.toLowerCase().replace(" ", "").replace("-", "");
  }

  /**
   * Returns a number of random albums.
   *
   * @param count Number of albums to return.
   * @param musicFolders Only return albums from these folders.
   * @return List of random albums.
   */
  public List<MediaFile> getRandomAlbums(int count, List<MusicFolder> musicFolders) {
    List<MediaFile> result = new ArrayList<MediaFile>();

    IndexReader reader = null;
    try {
      reader = createIndexReader(ALBUM);
      Searcher searcher = new IndexSearcher(reader);

      List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
      for (MusicFolder musicFolder : musicFolders) {
        musicFolderQueries.add(
            new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath())));
      }
      Query query =
          new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()]));

      TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE);
      List<ScoreDoc> scoreDocs = Lists.newArrayList(topDocs.scoreDocs);
      Random random = new Random(System.currentTimeMillis());

      while (!scoreDocs.isEmpty() && result.size() < count) {
        int index = random.nextInt(scoreDocs.size());
        Document doc = searcher.doc(scoreDocs.remove(index).doc);
        int id = Integer.valueOf(doc.get(FIELD_ID));
        try {
          addIfNotNull(mediaFileService.getMediaFile(id), result);
        } catch (Exception x) {
          LOG.warn("Failed to get media file " + id, x);
        }
      }

    } catch (Throwable x) {
      LOG.error("Failed to search for random albums.", x);
    } finally {
      FileUtil.closeQuietly(reader);
    }
    return result;
  }

  /**
   * Returns a number of random albums, using ID3 tag.
   *
   * @param count Number of albums to return.
   * @param musicFolders Only return albums from these folders.
   * @return List of random albums.
   */
  public List<Album> getRandomAlbumsId3(int count, List<MusicFolder> musicFolders) {
    List<Album> result = new ArrayList<Album>();

    IndexReader reader = null;
    try {
      reader = createIndexReader(ALBUM_ID3);
      Searcher searcher = new IndexSearcher(reader);

      List<SpanTermQuery> musicFolderQueries = new ArrayList<SpanTermQuery>();
      for (MusicFolder musicFolder : musicFolders) {
        musicFolderQueries.add(
            new SpanTermQuery(
                new Term(FIELD_FOLDER_ID, NumericUtils.intToPrefixCoded(musicFolder.getId()))));
      }
      Query query =
          new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()]));
      TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE);
      List<ScoreDoc> scoreDocs = Lists.newArrayList(topDocs.scoreDocs);
      Random random = new Random(System.currentTimeMillis());

      while (!scoreDocs.isEmpty() && result.size() < count) {
        int index = random.nextInt(scoreDocs.size());
        Document doc = searcher.doc(scoreDocs.remove(index).doc);
        int id = Integer.valueOf(doc.get(FIELD_ID));
        try {
          addIfNotNull(albumDao.getAlbum(id), result);
        } catch (Exception x) {
          LOG.warn("Failed to get album file " + id, x);
        }
      }

    } catch (Throwable x) {
      LOG.error("Failed to search for random albums.", x);
    } finally {
      FileUtil.closeQuietly(reader);
    }
    return result;
  }

  private <T> void addIfNotNull(T value, List<T> list) {
    if (value != null) {
      list.add(value);
    }
  }

  private IndexWriter createIndexWriter(IndexType indexType) throws IOException {
    File dir = getIndexDirectory(indexType);
    return new IndexWriter(
        FSDirectory.open(dir), new SubsonicAnalyzer(), true, new IndexWriter.MaxFieldLength(10));
  }

  private IndexReader createIndexReader(IndexType indexType) throws IOException {
    File dir = getIndexDirectory(indexType);
    return IndexReader.open(FSDirectory.open(dir), true);
  }

  private File getIndexRootDirectory() {
    return new File(SettingsService.getSubsonicHome(), LUCENE_DIR);
  }

  private File getIndexDirectory(IndexType indexType) {
    return new File(getIndexRootDirectory(), indexType.toString().toLowerCase());
  }

  private void removeLocks() {
    for (IndexType indexType : IndexType.values()) {
      Directory dir = null;
      try {
        dir = FSDirectory.open(getIndexDirectory(indexType));
        if (IndexWriter.isLocked(dir)) {
          IndexWriter.unlock(dir);
          LOG.info("Removed Lucene lock file in " + dir);
        }
      } catch (Exception x) {
        LOG.warn("Failed to remove Lucene lock file in " + dir, x);
      } finally {
        FileUtil.closeQuietly(dir);
      }
    }
  }

  public void setMediaFileService(MediaFileService mediaFileService) {
    this.mediaFileService = mediaFileService;
  }

  public void setArtistDao(ArtistDao artistDao) {
    this.artistDao = artistDao;
  }

  public void setAlbumDao(AlbumDao albumDao) {
    this.albumDao = albumDao;
  }

  public static enum IndexType {
    SONG(new String[] {FIELD_TITLE, FIELD_ARTIST}, FIELD_TITLE) {
      @Override
      public Document createDocument(MediaFile mediaFile) {
        Document doc = new Document();
        doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId()));
        doc.add(
            new Field(
                FIELD_MEDIA_TYPE,
                mediaFile.getMediaType().name(),
                Field.Store.NO,
                Field.Index.ANALYZED_NO_NORMS));

        if (mediaFile.getTitle() != null) {
          doc.add(
              new Field(FIELD_TITLE, mediaFile.getTitle(), Field.Store.YES, Field.Index.ANALYZED));
        }
        if (mediaFile.getArtist() != null) {
          doc.add(
              new Field(
                  FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
        }
        if (mediaFile.getGenre() != null) {
          doc.add(
              new Field(
                  FIELD_GENRE,
                  normalizeGenre(mediaFile.getGenre()),
                  Field.Store.NO,
                  Field.Index.ANALYZED));
        }
        if (mediaFile.getYear() != null) {
          doc.add(
              new NumericField(FIELD_YEAR, Field.Store.NO, true).setIntValue(mediaFile.getYear()));
        }
        if (mediaFile.getFolder() != null) {
          doc.add(
              new Field(
                  FIELD_FOLDER,
                  mediaFile.getFolder(),
                  Field.Store.NO,
                  Field.Index.NOT_ANALYZED_NO_NORMS));
        }

        return doc;
      }
    },

    ALBUM(new String[] {FIELD_ALBUM, FIELD_ARTIST, FIELD_FOLDER}, FIELD_ALBUM) {
      @Override
      public Document createDocument(MediaFile mediaFile) {
        Document doc = new Document();
        doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId()));

        if (mediaFile.getArtist() != null) {
          doc.add(
              new Field(
                  FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
        }
        if (mediaFile.getAlbumName() != null) {
          doc.add(
              new Field(
                  FIELD_ALBUM, mediaFile.getAlbumName(), Field.Store.YES, Field.Index.ANALYZED));
        }
        if (mediaFile.getFolder() != null) {
          doc.add(
              new Field(
                  FIELD_FOLDER,
                  mediaFile.getFolder(),
                  Field.Store.NO,
                  Field.Index.NOT_ANALYZED_NO_NORMS));
        }

        return doc;
      }
    },

    ALBUM_ID3(new String[] {FIELD_ALBUM, FIELD_ARTIST, FIELD_FOLDER_ID}, FIELD_ALBUM) {
      @Override
      public Document createDocument(Album album) {
        Document doc = new Document();
        doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(album.getId()));

        if (album.getArtist() != null) {
          doc.add(
              new Field(FIELD_ARTIST, album.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
        }
        if (album.getName() != null) {
          doc.add(new Field(FIELD_ALBUM, album.getName(), Field.Store.YES, Field.Index.ANALYZED));
        }
        if (album.getFolderId() != null) {
          doc.add(
              new NumericField(FIELD_FOLDER_ID, Field.Store.NO, true)
                  .setIntValue(album.getFolderId()));
        }

        return doc;
      }
    },

    ARTIST(new String[] {FIELD_ARTIST, FIELD_FOLDER}, null) {
      @Override
      public Document createDocument(MediaFile mediaFile) {
        Document doc = new Document();
        doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId()));

        if (mediaFile.getArtist() != null) {
          doc.add(
              new Field(
                  FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED));
        }
        if (mediaFile.getFolder() != null) {
          doc.add(
              new Field(
                  FIELD_FOLDER,
                  mediaFile.getFolder(),
                  Field.Store.NO,
                  Field.Index.NOT_ANALYZED_NO_NORMS));
        }

        return doc;
      }
    },

    ARTIST_ID3(new String[] {FIELD_ARTIST}, null) {
      @Override
      public Document createDocument(Artist artist, MusicFolder musicFolder) {
        Document doc = new Document();
        doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(artist.getId()));
        doc.add(new Field(FIELD_ARTIST, artist.getName(), Field.Store.YES, Field.Index.ANALYZED));
        doc.add(
            new NumericField(FIELD_FOLDER_ID, Field.Store.NO, true)
                .setIntValue(musicFolder.getId()));

        return doc;
      }
    };

    private final String[] fields;
    private final Map<String, Float> boosts;

    private IndexType(String[] fields, String boostedField) {
      this.fields = fields;
      boosts = new HashMap<String, Float>();
      if (boostedField != null) {
        boosts.put(boostedField, 2.0F);
      }
    }

    public String[] getFields() {
      return fields;
    }

    protected Document createDocument(MediaFile mediaFile) {
      throw new UnsupportedOperationException();
    }

    protected Document createDocument(Artist artist, MusicFolder musicFolder) {
      throw new UnsupportedOperationException();
    }

    protected Document createDocument(Album album) {
      throw new UnsupportedOperationException();
    }

    public Map<String, Float> getBoosts() {
      return boosts;
    }
  }

  private class SubsonicAnalyzer extends StandardAnalyzer {
    private SubsonicAnalyzer() {
      super(LUCENE_VERSION);
    }

    @Override
    public TokenStream tokenStream(String fieldName, Reader reader) {
      TokenStream result = super.tokenStream(fieldName, reader);
      return new ASCIIFoldingFilter(result);
    }

    @Override
    public TokenStream reusableTokenStream(String fieldName, Reader reader) throws IOException {
      class SavedStreams {
        StandardTokenizer tokenStream;
        TokenStream filteredTokenStream;
      }

      SavedStreams streams = (SavedStreams) getPreviousTokenStream();
      if (streams == null) {
        streams = new SavedStreams();
        setPreviousTokenStream(streams);
        streams.tokenStream = new StandardTokenizer(LUCENE_VERSION, reader);
        streams.filteredTokenStream = new StandardFilter(streams.tokenStream);
        streams.filteredTokenStream = new LowerCaseFilter(streams.filteredTokenStream);
        streams.filteredTokenStream =
            new StopFilter(true, streams.filteredTokenStream, STOP_WORDS_SET);
        streams.filteredTokenStream = new ASCIIFoldingFilter(streams.filteredTokenStream);
      } else {
        streams.tokenStream.reset(reader);
      }
      streams.tokenStream.setMaxTokenLength(DEFAULT_MAX_TOKEN_LENGTH);

      return streams.filteredTokenStream;
    }
  }
}
/**
 * Provides services for Podcast reception.
 *
 * @author Sindre Mehus
 */
public class PodcastService {

  private static final Logger LOG = Logger.getLogger(PodcastService.class);
  private static final DateFormat[] RSS_DATE_FORMATS = {
    new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US),
    new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z", Locale.US)
  };

  private static final Namespace[] ITUNES_NAMESPACES = {
    Namespace.getNamespace("http://www.itunes.com/DTDs/Podcast-1.0.dtd"),
    Namespace.getNamespace("http://www.itunes.com/dtds/podcast-1.0.dtd")
  };

  private final ExecutorService refreshExecutor;
  private final ExecutorService downloadExecutor;
  private final ScheduledExecutorService scheduledExecutor;
  private ScheduledFuture<?> scheduledRefresh;
  private PodcastDao podcastDao;
  private SettingsService settingsService;
  private SecurityService securityService;
  private MediaFileService mediaFileService;

  public PodcastService() {
    ThreadFactory threadFactory =
        new ThreadFactory() {
          public Thread newThread(Runnable r) {
            Thread t = Executors.defaultThreadFactory().newThread(r);
            t.setDaemon(true);
            return t;
          }
        };
    refreshExecutor = Executors.newFixedThreadPool(5, threadFactory);
    downloadExecutor = Executors.newFixedThreadPool(3, threadFactory);
    scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory);
  }

  public synchronized void init() {
    // Clean up partial downloads.
    for (PodcastChannel channel : getAllChannels()) {
      for (PodcastEpisode episode : getEpisodes(channel.getId(), false)) {
        if (episode.getStatus() == PodcastStatus.DOWNLOADING) {
          deleteEpisode(episode.getId(), false);
          LOG.info(
              "Deleted Podcast episode '"
                  + episode.getTitle()
                  + "' since download was interrupted.");
        }
      }
    }

    schedule();
  }

  public synchronized void schedule() {
    Runnable task =
        new Runnable() {
          public void run() {
            LOG.info("Starting scheduled Podcast refresh.");
            refreshAllChannels(true);
            LOG.info("Completed scheduled Podcast refresh.");
          }
        };

    if (scheduledRefresh != null) {
      scheduledRefresh.cancel(true);
    }

    int hoursBetween = settingsService.getPodcastUpdateInterval();

    if (hoursBetween == -1) {
      LOG.info("Automatic Podcast update disabled.");
      return;
    }

    long periodMillis = hoursBetween * 60L * 60L * 1000L;
    long initialDelayMillis = 5L * 60L * 1000L;

    scheduledRefresh =
        scheduledExecutor.scheduleAtFixedRate(
            task, initialDelayMillis, periodMillis, TimeUnit.MILLISECONDS);
    Date firstTime = new Date(System.currentTimeMillis() + initialDelayMillis);
    LOG.info(
        "Automatic Podcast update scheduled to run every "
            + hoursBetween
            + " hour(s), starting at "
            + firstTime);
  }

  /**
   * Creates a new Podcast channel.
   *
   * @param url The URL of the Podcast channel.
   */
  public void createChannel(String url) {
    url = sanitizeUrl(url);
    PodcastChannel channel = new PodcastChannel(url);
    int channelId = podcastDao.createChannel(channel);

    refreshChannels(Arrays.asList(getChannel(channelId)), true);
  }

  private String sanitizeUrl(String url) {
    return url.replace(" ", "%20");
  }

  private PodcastChannel getChannel(int channelId) {
    for (PodcastChannel channel : getAllChannels()) {
      if (channelId == channel.getId()) {
        return channel;
      }
    }
    return null;
  }

  /**
   * Returns all Podcast channels.
   *
   * @return Possibly empty list of all Podcast channels.
   */
  public List<PodcastChannel> getAllChannels() {
    return podcastDao.getAllChannels();
  }

  /**
   * Returns all Podcast episodes for a given channel.
   *
   * @param channelId The Podcast channel ID.
   * @param includeDeleted Whether to include logically deleted episodes in the result.
   * @return Possibly empty list of all Podcast episodes for the given channel, sorted in reverse
   *     chronological order (newest episode first).
   */
  public List<PodcastEpisode> getEpisodes(int channelId, boolean includeDeleted) {
    List<PodcastEpisode> all = podcastDao.getEpisodes(channelId);
    if (includeDeleted) {
      return all;
    }

    List<PodcastEpisode> filtered = new ArrayList<PodcastEpisode>();
    for (PodcastEpisode episode : all) {
      if (episode.getStatus() != PodcastStatus.DELETED) {
        filtered.add(episode);
      }
    }
    return filtered;
  }

  public PodcastEpisode getEpisode(int episodeId, boolean includeDeleted) {
    PodcastEpisode episode = podcastDao.getEpisode(episodeId);
    if (episode == null) {
      return null;
    }
    if (episode.getStatus() == PodcastStatus.DELETED && !includeDeleted) {
      return null;
    }
    return episode;
  }

  private PodcastEpisode getEpisode(int channelId, String url) {
    if (url == null) {
      return null;
    }

    for (PodcastEpisode episode : getEpisodes(channelId, true)) {
      if (url.equals(episode.getUrl())) {
        return episode;
      }
    }
    return null;
  }

  public void refreshAllChannels(boolean downloadEpisodes) {
    refreshChannels(getAllChannels(), downloadEpisodes);
  }

  private void refreshChannels(
      final List<PodcastChannel> channels, final boolean downloadEpisodes) {
    for (final PodcastChannel channel : channels) {
      Runnable task =
          new Runnable() {
            public void run() {
              doRefreshChannel(channel, downloadEpisodes);
            }
          };
      refreshExecutor.submit(task);
    }
  }

  @SuppressWarnings({"unchecked"})
  private void doRefreshChannel(PodcastChannel channel, boolean downloadEpisodes) {
    InputStream in = null;
    HttpClient client = new DefaultHttpClient();

    try {
      channel.setStatus(PodcastStatus.DOWNLOADING);
      channel.setErrorMessage(null);
      podcastDao.updateChannel(channel);

      HttpConnectionParams.setConnectionTimeout(client.getParams(), 2 * 60 * 1000); // 2 minutes
      HttpConnectionParams.setSoTimeout(client.getParams(), 10 * 60 * 1000); // 10 minutes
      HttpGet method = new HttpGet(channel.getUrl());

      HttpResponse response = client.execute(method);
      in = response.getEntity().getContent();

      Document document = new SAXBuilder().build(in);
      Element channelElement = document.getRootElement().getChild("channel");

      channel.setTitle(channelElement.getChildTextTrim("title"));
      channel.setDescription(channelElement.getChildTextTrim("description"));
      channel.setStatus(PodcastStatus.COMPLETED);
      channel.setErrorMessage(null);
      podcastDao.updateChannel(channel);

      refreshEpisodes(channel, channelElement.getChildren("item"));

    } catch (Exception x) {
      LOG.warn("Failed to get/parse RSS file for Podcast channel " + channel.getUrl(), x);
      channel.setStatus(PodcastStatus.ERROR);
      channel.setErrorMessage(x.toString());
      podcastDao.updateChannel(channel);
    } finally {
      IOUtils.closeQuietly(in);
      client.getConnectionManager().shutdown();
    }

    if (downloadEpisodes) {
      for (final PodcastEpisode episode : getEpisodes(channel.getId(), false)) {
        if (episode.getStatus() == PodcastStatus.NEW && episode.getUrl() != null) {
          downloadEpisode(episode);
        }
      }
    }
  }

  public void downloadEpisode(final PodcastEpisode episode) {
    Runnable task =
        new Runnable() {
          public void run() {
            doDownloadEpisode(episode);
          }
        };
    downloadExecutor.submit(task);
  }

  private void refreshEpisodes(PodcastChannel channel, List<Element> episodeElements) {

    List<PodcastEpisode> episodes = new ArrayList<PodcastEpisode>();

    for (Element episodeElement : episodeElements) {

      String title = episodeElement.getChildTextTrim("title");
      String duration = getITunesElement(episodeElement, "duration");
      String description = episodeElement.getChildTextTrim("description");
      if (StringUtils.isBlank(description)) {
        description = getITunesElement(episodeElement, "summary");
      }

      Element enclosure = episodeElement.getChild("enclosure");
      if (enclosure == null) {
        LOG.debug("No enclosure found for episode " + title);
        continue;
      }

      String url = enclosure.getAttributeValue("url");
      url = sanitizeUrl(url);
      if (url == null) {
        LOG.debug("No enclosure URL found for episode " + title);
        continue;
      }

      if (getEpisode(channel.getId(), url) == null) {
        Long length = null;
        try {
          length = new Long(enclosure.getAttributeValue("length"));
        } catch (Exception x) {
          LOG.warn("Failed to parse enclosure length.", x);
        }

        Date date = parseDate(episodeElement.getChildTextTrim("pubDate"));
        PodcastEpisode episode =
            new PodcastEpisode(
                null,
                channel.getId(),
                url,
                null,
                title,
                description,
                date,
                duration,
                length,
                0L,
                PodcastStatus.NEW,
                null);
        episodes.add(episode);
        LOG.info("Created Podcast episode " + title);
      }
    }

    // Sort episode in reverse chronological order (newest first)
    Collections.sort(
        episodes,
        new Comparator<PodcastEpisode>() {
          public int compare(PodcastEpisode a, PodcastEpisode b) {
            long timeA = a.getPublishDate() == null ? 0L : a.getPublishDate().getTime();
            long timeB = b.getPublishDate() == null ? 0L : b.getPublishDate().getTime();

            if (timeA < timeB) {
              return 1;
            }
            if (timeA > timeB) {
              return -1;
            }
            return 0;
          }
        });

    // Create episodes in database, skipping the proper number of episodes.
    int downloadCount = settingsService.getPodcastEpisodeDownloadCount();
    if (downloadCount == -1) {
      downloadCount = Integer.MAX_VALUE;
    }

    for (int i = 0; i < episodes.size(); i++) {
      PodcastEpisode episode = episodes.get(i);
      if (i >= downloadCount) {
        episode.setStatus(PodcastStatus.SKIPPED);
      }
      podcastDao.createEpisode(episode);
    }
  }

  private Date parseDate(String s) {
    for (DateFormat dateFormat : RSS_DATE_FORMATS) {
      try {
        return dateFormat.parse(s);
      } catch (Exception x) {
        // Ignored.
      }
    }
    LOG.warn("Failed to parse publish date: '" + s + "'.");
    return null;
  }

  private String getITunesElement(Element element, String childName) {
    for (Namespace ns : ITUNES_NAMESPACES) {
      String value = element.getChildTextTrim(childName, ns);
      if (value != null) {
        return value;
      }
    }
    return null;
  }

  private void doDownloadEpisode(PodcastEpisode episode) {
    InputStream in = null;
    OutputStream out = null;

    if (getEpisode(episode.getId(), false) == null) {
      LOG.info("Podcast " + episode.getUrl() + " was deleted. Aborting download.");
      return;
    }

    LOG.info("Starting to download Podcast from " + episode.getUrl());

    HttpClient client = new DefaultHttpClient();
    try {
      PodcastChannel channel = getChannel(episode.getChannelId());

      HttpConnectionParams.setConnectionTimeout(client.getParams(), 2 * 60 * 1000); // 2 minutes
      HttpConnectionParams.setSoTimeout(client.getParams(), 10 * 60 * 1000); // 10 minutes
      HttpGet method = new HttpGet(episode.getUrl());

      HttpResponse response = client.execute(method);
      in = response.getEntity().getContent();

      File file = getFile(channel, episode);
      out = new FileOutputStream(file);

      episode.setStatus(PodcastStatus.DOWNLOADING);
      episode.setBytesDownloaded(0L);
      episode.setErrorMessage(null);
      episode.setPath(file.getPath());
      podcastDao.updateEpisode(episode);

      byte[] buffer = new byte[4096];
      long bytesDownloaded = 0;
      int n;
      long nextLogCount = 30000L;

      while ((n = in.read(buffer)) != -1) {
        out.write(buffer, 0, n);
        bytesDownloaded += n;

        if (bytesDownloaded > nextLogCount) {
          episode.setBytesDownloaded(bytesDownloaded);
          nextLogCount += 30000L;
          if (getEpisode(episode.getId(), false) == null) {
            break;
          }
          podcastDao.updateEpisode(episode);
        }
      }

      if (getEpisode(episode.getId(), false) == null) {
        LOG.info("Podcast " + episode.getUrl() + " was deleted. Aborting download.");
        IOUtils.closeQuietly(out);
        file.delete();
      } else {
        episode.setBytesDownloaded(bytesDownloaded);
        podcastDao.updateEpisode(episode);
        LOG.info("Downloaded " + bytesDownloaded + " bytes from Podcast " + episode.getUrl());
        IOUtils.closeQuietly(out);
        episode.setStatus(PodcastStatus.COMPLETED);
        podcastDao.updateEpisode(episode);
        deleteObsoleteEpisodes(channel);
      }

    } catch (Exception x) {
      LOG.warn("Failed to download Podcast from " + episode.getUrl(), x);
      episode.setStatus(PodcastStatus.ERROR);
      episode.setErrorMessage(x.toString());
      podcastDao.updateEpisode(episode);
    } finally {
      IOUtils.closeQuietly(in);
      IOUtils.closeQuietly(out);
      client.getConnectionManager().shutdown();
    }
  }

  private synchronized void deleteObsoleteEpisodes(PodcastChannel channel) {
    int episodeCount = settingsService.getPodcastEpisodeRetentionCount();
    if (episodeCount == -1) {
      return;
    }

    List<PodcastEpisode> episodes = getEpisodes(channel.getId(), false);

    // Don't do anything if other episodes of the same channel is currently downloading.
    for (PodcastEpisode episode : episodes) {
      if (episode.getStatus() == PodcastStatus.DOWNLOADING) {
        return;
      }
    }

    // Reverse array to get chronological order (oldest episodes first).
    Collections.reverse(episodes);

    int episodesToDelete = Math.max(0, episodes.size() - episodeCount);
    for (int i = 0; i < episodesToDelete; i++) {
      deleteEpisode(episodes.get(i).getId(), true);
      LOG.info("Deleted old Podcast episode " + episodes.get(i).getUrl());
    }
  }

  private synchronized File getFile(PodcastChannel channel, PodcastEpisode episode) {

    File podcastDir = new File(settingsService.getPodcastFolder());
    File channelDir = new File(podcastDir, StringUtil.fileSystemSafe(channel.getTitle()));

    if (!channelDir.exists()) {
      boolean ok = channelDir.mkdirs();
      if (!ok) {
        throw new RuntimeException("Failed to create directory " + channelDir);
      }

      MediaFile mediaFile = mediaFileService.getMediaFile(channelDir);
      mediaFile.setComment(channel.getDescription());
      mediaFileService.updateMediaFile(mediaFile);
    }

    String filename = StringUtil.getUrlFile(episode.getUrl());
    if (filename == null) {
      filename = episode.getTitle();
    }
    filename = StringUtil.fileSystemSafe(filename);
    String extension = FilenameUtils.getExtension(filename);
    filename = FilenameUtils.removeExtension(filename);
    if (StringUtils.isBlank(extension)) {
      extension = "mp3";
    }

    File file = new File(channelDir, filename + "." + extension);
    for (int i = 0; file.exists(); i++) {
      file = new File(channelDir, filename + i + "." + extension);
    }

    if (!securityService.isWriteAllowed(file)) {
      throw new SecurityException("Access denied to file " + file);
    }
    return file;
  }

  /**
   * Deletes the Podcast channel with the given ID.
   *
   * @param channelId The Podcast channel ID.
   */
  public void deleteChannel(int channelId) {
    // Delete all associated episodes (in case they have files that need to be deleted).
    List<PodcastEpisode> episodes = getEpisodes(channelId, false);
    for (PodcastEpisode episode : episodes) {
      deleteEpisode(episode.getId(), false);
    }
    podcastDao.deleteChannel(channelId);
  }

  /**
   * Deletes the Podcast episode with the given ID.
   *
   * @param episodeId The Podcast episode ID.
   * @param logicalDelete Whether to perform a logical delete by setting the episode status to
   *     {@link PodcastStatus#DELETED}.
   */
  public void deleteEpisode(int episodeId, boolean logicalDelete) {
    PodcastEpisode episode = podcastDao.getEpisode(episodeId);
    if (episode == null) {
      return;
    }

    // Delete file.
    if (episode.getPath() != null) {
      File file = new File(episode.getPath());
      if (file.exists()) {
        file.delete();
        // TODO: Delete directory if empty?
      }
    }

    if (logicalDelete) {
      episode.setStatus(PodcastStatus.DELETED);
      episode.setErrorMessage(null);
      podcastDao.updateEpisode(episode);
    } else {
      podcastDao.deleteEpisode(episodeId);
    }
  }

  public void setPodcastDao(PodcastDao podcastDao) {
    this.podcastDao = podcastDao;
  }

  public void setSettingsService(SettingsService settingsService) {
    this.settingsService = settingsService;
  }

  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  public void setMediaFileService(MediaFileService mediaFileService) {
    this.mediaFileService = mediaFileService;
  }
}
Exemple #21
0
/**
 * Provides database services for playlists.
 *
 * @author Sindre Mehus
 */
public class PlaylistDao extends AbstractDao {

  private static final Logger LOG = Logger.getLogger(PlaylistDao.class);
  private static final String COLUMNS =
      "id, username, is_public, name, comment, file_count, duration_seconds, "
          + "created, changed, imported_from";
  private final RowMapper rowMapper = new PlaylistMapper();

  public List<Playlist> getReadablePlaylistsForUser(String username) {

    List<Playlist> result1 = getWritablePlaylistsForUser(username);
    List<Playlist> result2 =
        query("select " + COLUMNS + " from playlist where is_public", rowMapper);
    List<Playlist> result3 =
        query(
            "select "
                + prefix(COLUMNS, "playlist")
                + " from playlist, playlist_user where "
                + "playlist.id = playlist_user.playlist_id and "
                + "playlist.username != ? and "
                + "playlist_user.username = ?",
            rowMapper,
            username,
            username);

    // Put in sorted map to avoid duplicates.
    SortedMap<Integer, Playlist> map = new TreeMap<Integer, Playlist>();
    for (Playlist playlist : result1) {
      map.put(playlist.getId(), playlist);
    }
    for (Playlist playlist : result2) {
      map.put(playlist.getId(), playlist);
    }
    for (Playlist playlist : result3) {
      map.put(playlist.getId(), playlist);
    }
    return new ArrayList<Playlist>(map.values());
  }

  public List<Playlist> getWritablePlaylistsForUser(String username) {
    return query("select " + COLUMNS + " from playlist where username=?", rowMapper, username);
  }

  public Playlist getPlaylist(int id) {
    return queryOne("select " + COLUMNS + " from playlist where id=?", rowMapper, id);
  }

  public List<Playlist> getAllPlaylists() {
    return query("select " + COLUMNS + " from playlist", rowMapper);
  }

  public synchronized void createPlaylist(Playlist playlist) {
    update(
        "insert into playlist(" + COLUMNS + ") values(" + questionMarks(COLUMNS) + ")",
        null,
        playlist.getUsername(),
        playlist.isShared(),
        playlist.getName(),
        playlist.getComment(),
        0,
        0,
        playlist.getCreated(),
        playlist.getChanged(),
        playlist.getImportedFrom());

    int id = queryForInt("select max(id) from playlist", 0);
    playlist.setId(id);
  }

  public void setFilesInPlaylist(int id, List<MediaFile> files) {
    update("delete from playlist_file where playlist_id=?", id);
    int duration = 0;
    for (MediaFile file : files) {
      update(
          "insert into playlist_file (playlist_id, media_file_id) values (?, ?)", id, file.getId());
      if (file.getDurationSeconds() != null) {
        duration += file.getDurationSeconds();
      }
    }
    update(
        "update playlist set file_count=?, duration_seconds=?, changed=? where id=?",
        files.size(),
        duration,
        new Date(),
        id);
  }

  public List<String> getPlaylistUsers(int playlistId) {
    return queryForStrings("select username from playlist_user where playlist_id=?", playlistId);
  }

  public void addPlaylistUser(int playlistId, String username) {
    if (!getPlaylistUsers(playlistId).contains(username)) {
      update("insert into playlist_user(playlist_id,username) values (?,?)", playlistId, username);
    }
  }

  public void deletePlaylistUser(int playlistId, String username) {
    update("delete from playlist_user where playlist_id=? and username=?", playlistId, username);
  }

  public synchronized void deletePlaylist(int id) {
    update("delete from playlist where id=?", id);
  }

  public void updatePlaylist(Playlist playlist) {
    update(
        "update playlist set username=?, is_public=?, name=?, comment=?, changed=?, imported_from=? where id=?",
        playlist.getUsername(),
        playlist.isShared(),
        playlist.getName(),
        playlist.getComment(),
        new Date(),
        playlist.getImportedFrom(),
        playlist.getId());
  }

  private static class PlaylistMapper implements ParameterizedRowMapper<Playlist> {
    public Playlist mapRow(ResultSet rs, int rowNum) throws SQLException {
      return new Playlist(
          rs.getInt(1),
          rs.getString(2),
          rs.getBoolean(3),
          rs.getString(4),
          rs.getString(5),
          rs.getInt(6),
          rs.getInt(7),
          rs.getTimestamp(8),
          rs.getTimestamp(9),
          rs.getString(10));
    }
  }
}