aboutsummaryrefslogtreecommitdiff
path: root/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2013-07-27 14:33:25 -0700
committerScott Jackson <daneren2005@gmail.com>2013-07-27 14:33:38 -0700
commit4738428c2c205f42200386ae09b44b9ec07b9144 (patch)
treea6402978fe1b4655f90c3c8a181f4d246fbc5e89 /subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java
parent82ec8315f777c319f2372540098e21111019d629 (diff)
downloaddsub-4738428c2c205f42200386ae09b44b9ec07b9144.tar.gz
dsub-4738428c2c205f42200386ae09b44b9ec07b9144.tar.bz2
dsub-4738428c2c205f42200386ae09b44b9ec07b9144.zip
Move subsonic-android to root
Diffstat (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java')
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java599
1 files changed, 0 insertions, 599 deletions
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java
deleted file mode 100644
index 09184df6..00000000
--- a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java
+++ /dev/null
@@ -1,599 +0,0 @@
-/*
- This file is part of Subsonic.
-
- Subsonic is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Subsonic is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
-
- Copyright 2009 (C) Sindre Mehus
- */
-package net.sourceforge.subsonic.service;
-
-import net.sourceforge.subsonic.Logger;
-import net.sourceforge.subsonic.dao.PodcastDao;
-import net.sourceforge.subsonic.domain.MediaFile;
-import net.sourceforge.subsonic.domain.PodcastChannel;
-import net.sourceforge.subsonic.domain.PodcastEpisode;
-import net.sourceforge.subsonic.domain.PodcastStatus;
-import net.sourceforge.subsonic.util.StringUtil;
-import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang.StringUtils;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.http.params.HttpConnectionParams;
-import org.jdom.Document;
-import org.jdom.Element;
-import org.jdom.Namespace;
-import org.jdom.input.SAXBuilder;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-
-/**
- * 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);
- addMediaFileIdToEpisodes(all);
- 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;
- }
- addMediaFileIdToEpisodes(Arrays.asList(episode));
- return episode;
- }
-
- private void addMediaFileIdToEpisodes(List<PodcastEpisode> episodes) {
- for (PodcastEpisode episode : episodes) {
- if (episode.getPath() != null) {
- MediaFile mediaFile = mediaFileService.getMediaFile(episode.getPath());
- if (mediaFile != null) {
- episode.setMediaFileId(mediaFile.getId());
- }
- }
- }
- }
-
- 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;
- }
-}