/* This file is part of the Android Clementine Remote.
* Copyright (C) 2013, Andreas Muttscheller <asfa194@gmail.com>
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.qspool.clementineremote.backend.downloader;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.AsyncTask;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.LinkedList;
import de.qspool.clementineremote.App;
import de.qspool.clementineremote.backend.ClementineSimpleConnection;
import de.qspool.clementineremote.backend.elements.DownloaderResult;
import de.qspool.clementineremote.backend.elements.DownloaderResult.DownloadResult;
import de.qspool.clementineremote.backend.pb.ClementineMessage;
import de.qspool.clementineremote.backend.pb.ClementineMessageFactory;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.DownloadItem;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.MsgType;
import de.qspool.clementineremote.backend.pb.ClementineRemoteProtocolBuffer.ResponseSongFileChunk;
import de.qspool.clementineremote.backend.player.MySong;
import de.qspool.clementineremote.utils.DownloadSpeedCalculator;
import de.qspool.clementineremote.utils.IDownloadCalculatorSource;
import de.qspool.clementineremote.utils.Utilities;
public class ClementineSongDownloader extends
AsyncTask<ClementineMessage, DownloadStatus, DownloaderResult> {
public static class DownloadedSong {
public MySong song;
public Uri uri;
public DownloadedSong(MySong song, Uri uri) {
this.song = song;
this.uri = uri;
}
}
private int mId;
private SongDownloaderListener mSongDownloaderListener;
private DownloadStatus mDownloadStatus;
private DownloaderResult mDownloaderResult;
private ClementineSimpleConnection mClient = new ClementineSimpleConnection();
private String mDownloadPath;
private String mPlaylistName;
private boolean mDownloadOnWifiOnly;
private boolean mIsPlaylist = false;
private boolean mCreatePlaylistDir = false;
private boolean mCreateArtistDir = false;
private boolean mCreateAlbumDir = false;
private boolean mOverrideExistingFiles = false;
private DownloadItem mItem;
private LinkedList<DownloadedSong> mDownloadedSongs = new LinkedList<>();
private int mTotalFileSize;
private int mTotalDownloaded;
private DownloadSpeedCalculator mDownloadSpeed;
public ClementineSongDownloader() {
mDownloadStatus = new DownloadStatus(mId).setState(
DownloadStatus.DownloaderState.IDLE);
}
public void startDownload(ClementineMessage message) {
if (mSongDownloaderListener == null) {
throw new IllegalStateException("No listener defined!");
}
mItem = message.getMessage().getRequestDownloadSongs().getDownloadItem();
this.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, message);
}
@Override
protected DownloaderResult doInBackground(ClementineMessage... params) {
publishProgress(new DownloadStatus(mId).setState(
DownloadStatus.DownloaderState.IDLE));
if (mDownloadOnWifiOnly && !Utilities.onWifi()) {
return new DownloaderResult(mId, DownloaderResult.DownloadResult.ONLY_WIFI);
}
// First create a connection
if (!connect()) {
return new DownloaderResult(mId, DownloaderResult.DownloadResult.CONNECTION_ERROR);
}
mDownloadSpeed = new DownloadSpeedCalculator(new IDownloadCalculatorSource() {
@Override
public int getBytesTotalDownloaded() {
return getTotalDownloaded();
}
});
// Start the download
return startDownloading(params[0]);
}
@Override
protected void onProgressUpdate(DownloadStatus... progress) {
mDownloadStatus = progress[0];
mSongDownloaderListener.onProgress(mDownloadStatus);
}
@Override
protected void onCancelled(DownloaderResult result) {
onPostExecute(result);
}
@Override
protected void onPostExecute(DownloaderResult result) {
mDownloaderResult = result;
mDownloadStatus.setState(DownloadStatus.DownloaderState.FINISHED)
.setProgress(100);
mSongDownloaderListener.onDownloadResult(result);
}
/**
* Connect to Clementine
*
* @return true if the connection was established, false if not
*/
private boolean connect() {
ClementineMessage connectMessage = App.ClementineConnection.getRequestConnect();
int authCode = connectMessage.getMessage().getRequestConnect().getAuthCode();
return mClient.createConnection(
ClementineMessageFactory.buildConnectMessage(
connectMessage.getIp(),
connectMessage.getPort(),
authCode,
false,
true)
);
}
/**
* Start the Downlaod
*/
private DownloaderResult startDownloading(ClementineMessage clementineMessage) {
DownloaderResult result = new DownloaderResult(mId, DownloadResult.SUCCESSFUL);
File f = null;
FileOutputStream fo = null;
MySong currentSong = new MySong();
// Do we have a playlist?
checkIsPlaylist(clementineMessage);
// Now request the songs
mClient.sendRequest(clementineMessage);
while (true) {
// Check if the user canceled the process
if (isCancelled()) {
// Close the stream and delete the incomplete file
try {
if (fo != null) {
fo.flush();
fo.close();
}
if (f != null) {
f.delete();
}
} catch (IOException e) {
}
result = new DownloaderResult(mId, DownloadResult.CANCELLED);
break;
}
// Get the raw protocol buffer
ClementineMessage message = mClient.getProtoc(0);
// Check if an error occured
if (message.isErrorMessage()) {
result = new DownloaderResult(mId, DownloadResult.CONNECTION_ERROR);
break;
}
// Is the download forbidden?
if (message.getMessageType() == MsgType.DISCONNECT) {
result = new DownloaderResult(mId, DownloadResult.FOBIDDEN);
break;
}
// Download finished?
if (message.getMessageType() == MsgType.DOWNLOAD_QUEUE_EMPTY) {
break;
}
// Total file size
if (message.getMessageType() == MsgType.DOWNLOAD_TOTAL_SIZE) {
mTotalFileSize = message.getMessage().getResponseDownloadTotalSize().getTotalSize();
continue;
}
// Transcoding files?
if (message.getMessageType() == MsgType.TRANSCODING_FILES) {
parseTranscodingMessage(message);
continue;
}
// Ignore other elements!
if (message.getMessageType() != MsgType.SONG_FILE_CHUNK) {
continue;
}
ResponseSongFileChunk chunk = message.getMessage().getResponseSongFileChunk();
// If we received chunk no 0, then we have to decide wether to
// accept the song offered or not
if (chunk.getChunkNumber() == 0) {
currentSong = MySong.fromProtocolBuffer(chunk.getSongMetadata());
boolean accepted = processSongOffer(currentSong, chunk);
// If we don't accept the file, add the size so the DownloadManager can show it correctly
if (!accepted) {
mTotalDownloaded += chunk.getSize();
updateProgress(chunk, currentSong);
}
continue;
}
try {
// Check if we need to create a new file
if (f == null) {
// Check if we have enougth free space
if (chunk.getSize() > Utilities.getFreeSpaceExternal()) {
result = new DownloaderResult(mId, DownloadResult.INSUFFIANT_SPACE);
break;
}
File dir = new File(BuildDirPath(chunk));
f = new File(BuildFilePath(chunk));
// User wants to override files, so delete it here!
// The check was already done in processSongOffer()
if (f.exists()) {
f.delete();
}
dir.mkdirs();
f.createNewFile();
fo = new FileOutputStream(f);
}
// Write chunk to sdcard
fo.write(chunk.getData().toByteArray());
mTotalDownloaded += chunk.getData().size();
// Have we downloaded all chunks?
if (chunk.getChunkCount() == chunk.getChunkNumber()) {
// Index file
MediaScannerConnection
.scanFile(App.getApp(), new String[]{f.getAbsolutePath()}, null, null);
fo.flush();
fo.close();
f = null;
}
// Update notification
updateProgress(chunk, currentSong);
} catch (IOException e) {
result = new DownloaderResult(mId, DownloaderResult.DownloadResult.NOT_MOUNTED);
break;
}
}
// Disconnect at the end
mClient.disconnect(ClementineMessage.getMessage(MsgType.DISCONNECT));
return result;
}
private void parseTranscodingMessage(ClementineMessage message) {
ClementineRemoteProtocolBuffer.ResponseTranscoderStatus status = message.getMessage()
.getResponseTranscoderStatus();
publishProgress(new DownloadStatus(mId)
.setState(DownloadStatus.DownloaderState.TRANSCODING)
.setTranscodingTotal(status.getTotal())
.setTranscodingFinished(status.getProcessed()));
}
private void checkIsPlaylist(ClementineMessage clementineMessage) {
mIsPlaylist = (clementineMessage.getMessage().getRequestDownloadSongs().getDownloadItem()
== DownloadItem.APlaylist);
if (mIsPlaylist) {
int id = clementineMessage.getMessage().getRequestDownloadSongs().getPlaylistId();
mPlaylistName = App.Clementine.getPlaylistManager().getPlaylist(id).getName();
}
}
/**
* This method checks if the offered file exists and sends a response to Clementine.
* If the file does not exist -> Download file
* otherwise
* The user wants to override existing files -> Download file
* otherwise
* refuse file
*
* @param chunk The chunk with the metadata
* @return a boolean indicating if the song will be sent or not
*/
private boolean processSongOffer(MySong song, ResponseSongFileChunk chunk) {
File f = new File(BuildFilePath(chunk));
boolean accept = true;
if (f.exists() && !mOverrideExistingFiles) {
accept = false;
}
mClient.sendRequest(ClementineMessageFactory.buildSongOfferResponse(accept));
// Save the downloaded files
mDownloadedSongs.add(new DownloadedSong(song, Uri.fromFile(f)));
return accept;
}
/**
* Updates the current notification.
*
* @param chunk The current downloaded chunk
*/
private void updateProgress(ResponseSongFileChunk chunk, MySong song) {
double progress = 0;
if (chunk.getChunkNumber() > 0) {
progress = (((double) (chunk.getFileNumber() - 1) / (double) chunk.getFileCount())
+ (((double) chunk.getChunkNumber() / (double) chunk.getChunkCount())
/ (double) chunk.getFileCount()))
* 100;
}
publishProgress(new DownloadStatus(mId)
.setProgress(progress)
.setSong(song)
.setCurrentFileIndex(chunk.getFileNumber())
.setTotalFiles(chunk.getFileCount())
.setState(DownloadStatus.DownloaderState.DOWNLOADING));
}
/**
* Return the folder where the file will be placed
*
* @param chunk The chunk
*/
private String BuildDirPath(ResponseSongFileChunk chunk) {
StringBuilder sb = new StringBuilder();
sb.append(mDownloadPath);
sb.append(File.separator);
if (mIsPlaylist && mCreatePlaylistDir) {
sb.append(Utilities.removeInvalidFileCharacters(mPlaylistName));
sb.append(File.separator);
}
if (mCreateArtistDir) {
// Append artist name
if (chunk.getSongMetadata().getAlbumartist().length() == 0) {
sb.append(
Utilities.removeInvalidFileCharacters(chunk.getSongMetadata().getArtist()));
} else {
sb.append(Utilities
.removeInvalidFileCharacters(chunk.getSongMetadata().getAlbumartist()));
}
sb.append(File.separator);
if (mCreateAlbumDir) {
sb.append(Utilities.removeInvalidFileCharacters(chunk.getSongMetadata().getAlbum()));
sb.append(File.separator);
}
}
return sb.toString();
}
/**
* Build the filename
*
* @param chunk The SongFileChunk
* @return /sdcard/Music/Artist/Album/file.mp3
*/
private String BuildFilePath(ResponseSongFileChunk chunk) {
StringBuilder sb = new StringBuilder();
sb.append(BuildDirPath(chunk));
sb.append(Utilities.removeInvalidFileCharacters(chunk.getSongMetadata().getFilename()));
return sb.toString();
}
public DownloadItem getItem() {
return mItem;
}
/**
* Get the downloaded songs
*/
public LinkedList<DownloadedSong> getDownloadedSongs() {
return mDownloadedSongs;
}
public int getTotalFileSize() {
return mTotalFileSize;
}
public int getTotalDownloaded() {
return mTotalDownloaded;
}
public int getDownloadSpeedPerSecond() {
return mDownloadSpeed.getDownloadSpeed();
}
public int getId() {
return mId;
}
public void setId(int id) {
mId = id;
}
public void setSongDownloaderListener(SongDownloaderListener songDownloaderListener) {
mSongDownloaderListener = songDownloaderListener;
}
public void setDownloadOnWifiOnly(boolean downloadOnWifiOnly) {
mDownloadOnWifiOnly = downloadOnWifiOnly;
}
public void setDownloadPath(String downloadPath) {
mDownloadPath = downloadPath;
}
public void setCreatePlaylistDir(boolean createPlaylistDir) {
mCreatePlaylistDir = createPlaylistDir;
}
public void setCreateArtistDir(boolean createArtistDir) {
mCreateArtistDir = createArtistDir;
}
public void setCreateAlbumDir(boolean createAlbumDir) {
mCreateAlbumDir = createAlbumDir;
}
public void setOverrideExistingFiles(boolean overrideExistingFiles) {
mOverrideExistingFiles = overrideExistingFiles;
}
public String getPlaylistName() {
return mPlaylistName;
}
public DownloadStatus getDownloadStatus() {
return mDownloadStatus;
}
public DownloaderResult getDownloaderResult() {
return mDownloaderResult;
}
}