1 /*
2  * aTunes
3  * Copyright (C) Alex Aranda, Sylvain Gaudard and contributors
4  *
5  * See http://www.atunes.org/wiki/index.php?title=Contributing for information about contributors
6  *
7  * http://www.atunes.org
8  * http://sourceforge.net/projects/atunes
9  *
10  * This program is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU General Public License
12  * as published by the Free Software Foundation; either version 2
13  * of the License, or (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  */
20 
21 package net.sourceforge.atunes.kernel.modules.podcast;
22 
23 import java.awt.event.ActionEvent;
24 import java.awt.event.ActionListener;
25 import java.io.BufferedInputStream;
26 import java.io.File;
27 import java.io.FileNotFoundException;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.net.URLConnection;
32 import java.nio.channels.Channels;
33 import java.nio.channels.FileChannel;
34 import java.nio.channels.ReadableByteChannel;
35 import java.util.List;
36 
37 import net.sourceforge.atunes.kernel.BackgroundWorker;
38 import net.sourceforge.atunes.model.IBackgroundWorkerCallback;
39 import net.sourceforge.atunes.model.IDialogFactory;
40 import net.sourceforge.atunes.model.IIconFactory;
41 import net.sourceforge.atunes.model.ILookAndFeelManager;
42 import net.sourceforge.atunes.model.INetworkHandler;
43 import net.sourceforge.atunes.model.IPodcastFeedEntry;
44 import net.sourceforge.atunes.model.IProgressDialog;
45 import net.sourceforge.atunes.model.ITable;
46 import net.sourceforge.atunes.utils.ClosingUtils;
47 import net.sourceforge.atunes.utils.I18nUtils;
48 import net.sourceforge.atunes.utils.Logger;
49 
50 /**
51  * The podcast feed entry downloader downloads podcast feed entries from the
52  * internet to a local file.
53  */
54 public class PodcastFeedEntryDownloader extends
55 		BackgroundWorker<Boolean, DownloadPodcastFeedEntryIntermediateResult> {
56 
57 	private IPodcastFeedEntry podcastFeedEntry;
58 	/*
59 	 * Additional Bean properties
60 	 */
61 	private volatile long totalBytes;
62 	private volatile long byteProgress;
63 	private volatile int percentage;
64 	private volatile boolean failed;
65 
66 	private ITable navigationTable;
67 
68 	private PodcastFeedHandler podcastFeedHandler;
69 
70 	private INetworkHandler networkHandler;
71 
72 	private IDialogFactory dialogFactory;
73 
74 	private IIconFactory rssMediumIcon;
75 
76 	private ILookAndFeelManager lookAndFeelManager;
77 
78 	private IProgressDialog dialog;
79 
80 	/**
81 	 * @param lookAndFeelManager
82 	 */
setLookAndFeelManager(ILookAndFeelManager lookAndFeelManager)83 	public void setLookAndFeelManager(ILookAndFeelManager lookAndFeelManager) {
84 		this.lookAndFeelManager = lookAndFeelManager;
85 	}
86 
87 	/**
88 	 * @param rssMediumIcon
89 	 */
setRssMediumIcon(IIconFactory rssMediumIcon)90 	public void setRssMediumIcon(IIconFactory rssMediumIcon) {
91 		this.rssMediumIcon = rssMediumIcon;
92 	}
93 
94 	/**
95 	 * @param dialogFactory
96 	 */
setDialogFactory(IDialogFactory dialogFactory)97 	public void setDialogFactory(IDialogFactory dialogFactory) {
98 		this.dialogFactory = dialogFactory;
99 	}
100 
101 	/**
102 	 * @param navigationTable
103 	 */
setNavigationTable(ITable navigationTable)104 	public void setNavigationTable(ITable navigationTable) {
105 		this.navigationTable = navigationTable;
106 	}
107 
108 	/**
109 	 * @param podcastFeedHandler
110 	 */
setPodcastFeedHandler(PodcastFeedHandler podcastFeedHandler)111 	public void setPodcastFeedHandler(PodcastFeedHandler podcastFeedHandler) {
112 		this.podcastFeedHandler = podcastFeedHandler;
113 	}
114 
115 	/**
116 	 * @param networkHandler
117 	 */
setNetworkHandler(INetworkHandler networkHandler)118 	public void setNetworkHandler(INetworkHandler networkHandler) {
119 		this.networkHandler = networkHandler;
120 	}
121 
122 	/**
123 	 * Downloads entry
124 	 *
125 	 * @param podcastFeedEntry
126 	 * @param callback
127 	 */
download(IPodcastFeedEntry podcastFeedEntry, IBackgroundWorkerCallback<Boolean> callback)128 	public void download(IPodcastFeedEntry podcastFeedEntry,
129 			IBackgroundWorkerCallback<Boolean> callback) {
130 		this.podcastFeedEntry = podcastFeedEntry;
131 		execute(callback);
132 	}
133 
134 	@Override
before()135 	protected void before() {
136 		dialog = this.dialogFactory.newDialog("transferDialog",
137 				IProgressDialog.class);
138 		dialog.setTitle(I18nUtils.getString("PODCAST_FEED_ENTRY_DOWNLOAD"));
139 		dialog.setIcon(this.rssMediumIcon.getIcon(this.lookAndFeelManager
140 				.getCurrentLookAndFeel().getPaintForSpecialControls()));
141 		dialog.addCancelButtonActionListener(new ActionListener() {
142 			@Override
143 			public void actionPerformed(final ActionEvent e) {
144 				podcastFeedHandler.cancelDownloading(podcastFeedEntry, dialog,
145 						PodcastFeedEntryDownloader.this);
146 			}
147 		});
148 		dialog.setInfoText(podcastFeedEntry.getTitle());
149 		dialog.showDialog();
150 	}
151 
152 	@Override
doInBackground()153 	protected Boolean doInBackground() {
154 		Logger.info("Downloading PodcastEntry: ", podcastFeedEntry.getUrl());
155 
156 		InputStream in = null;
157 
158 		String podcastFeedEntryFileName = podcastFeedHandler
159 				.getDownloadPath(podcastFeedEntry);
160 		Logger.info("Downloading to: ", podcastFeedEntryFileName);
161 		File localFile = new File(podcastFeedEntryFileName);
162 
163 		ReadableByteChannel readChannel = null;
164 		FileChannel writeChannel = null;
165 		try {
166 			writeChannel = new FileOutputStream(localFile).getChannel();
167 			URLConnection conn = networkHandler.getConnection(podcastFeedEntry
168 					.getUrl());
169 			in = new BufferedInputStream(conn.getInputStream());
170 			setTotalBytes(conn.getContentLength());
171 
172 			readChannel = Channels.newChannel(in);
173 
174 			int iterations = 0;
175 			long totalTransferred = 0;
176 			while (readChannel.isOpen()) {
177 				long transferred = writeChannel.transferFrom(readChannel,
178 						totalTransferred, 4 * 4096);
179 				totalTransferred += transferred;
180 				if (iterations % 50 == 0) {
181 					setByteProgress(totalTransferred);
182 				}
183 			}
184 
185 			return !isCancelled();
186 		} catch (FileNotFoundException e) {
187 			Logger.info("file not found");
188 			setFailed(true);
189 			return false;
190 		} catch (IOException e) {
191 			Logger.info("Connection to ", podcastFeedEntry.getUrl(), " failed");
192 			setFailed(true);
193 			return false;
194 		} finally {
195 			ClosingUtils.close(readChannel);
196 			ClosingUtils.close(writeChannel);
197 			ClosingUtils.close(in);
198 		}
199 	}
200 
201 	@Override
whileWorking( List<DownloadPodcastFeedEntryIntermediateResult> chunks)202 	protected void whileWorking(
203 			List<DownloadPodcastFeedEntryIntermediateResult> chunks) {
204 		for (DownloadPodcastFeedEntryIntermediateResult chunk : chunks) {
205 			if (chunk.isFailed()) {
206 				podcastFeedHandler.cancelDownloading(podcastFeedEntry, dialog,
207 						this);
208 			} else if (chunk.getPercentage() == 100) {
209 				dialog.hideDialog();
210 			} else {
211 				dialog.setCurrentProgress(chunk.getByteProgress());
212 				dialog.setProgressBarValue(chunk.getPercentage());
213 				dialog.setTotalProgress(chunk.getTotalBytes());
214 			}
215 		}
216 	}
217 
218 	/**
219 	 * Sets the total bytes.
220 	 *
221 	 * @param totalBytes
222 	 *            the new total bytes
223 	 */
224 
setTotalBytes(long totalBytes)225 	private void setTotalBytes(long totalBytes) {
226 		this.totalBytes = totalBytes;
227 		publish();
228 	}
229 
230 	/**
231 	 * Sets the byte progress.
232 	 *
233 	 * @param byteProgress
234 	 *            the new byte progress
235 	 */
236 
setByteProgress(long byteProgress)237 	private void setByteProgress(long byteProgress) {
238 		this.byteProgress = byteProgress;
239 		// we want to update progress on byteProgress change
240 		this.percentage = (int) (((double) byteProgress / (double) totalBytes) * 100);
241 		publish();
242 	}
243 
244 	/**
245 	 * Sets the failed.
246 	 *
247 	 * @param failed
248 	 *            the new failed
249 	 */
250 
setFailed(boolean failed)251 	private void setFailed(boolean failed) {
252 		this.failed = failed;
253 		publish();
254 	}
255 
publish()256 	private void publish() {
257 		publish(new DownloadPodcastFeedEntryIntermediateResult(this.percentage,
258 				this.byteProgress, this.totalBytes, this.failed));
259 	}
260 
261 	/**
262 	 * Gets the total bytes.
263 	 *
264 	 * @return the total bytes
265 	 */
getTotalBytes()266 	public long getTotalBytes() {
267 		return totalBytes;
268 	}
269 
270 	@Override
done(final Boolean result)271 	protected void done(final Boolean result) {
272 		if (!isCancelled() && result) {
273 			Logger.info("Download of " + podcastFeedEntry.getUrl()
274 					+ " finished.");
275 			podcastFeedEntry.setDownloaded(true);
276 			navigationTable.repaint();
277 		}
278 	}
279 
280 	/**
281 	 * Gets the podcast feed entry.
282 	 *
283 	 * @return the podcast feed entry
284 	 */
getPodcastFeedEntry()285 	public IPodcastFeedEntry getPodcastFeedEntry() {
286 		return podcastFeedEntry;
287 	}
288 }
289