예제 #1
0
  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);
  }
예제 #2
0
  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.");
    }
  }
예제 #3
0
  @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.");
    }
  }
  @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.");
      }
    }
  }
예제 #5
0
  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.");
    }
  }
예제 #6
0
  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();
    }
  }
예제 #7
0
  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.");
    }
  }
예제 #8
0
 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);
     }
   }
 }
  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;
  }
예제 #10
0
  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.");
    }
  }
예제 #11
0
  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();
  }
예제 #12
0
 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);
     }
   }
 }
예제 #13
0
  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());
    }
  }
예제 #14
0
  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();
    }
  }
예제 #15
0
  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);
    }
  }
  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;
  }
예제 #17
0
  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.");
    }
  }