From 55feb3f3e1576f8ea2fd3d431d17f7e3a1eb397d Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Wed, 11 Mar 2015 05:14:36 +0100 Subject: [PATCH] Added DownloadManager and DownloadJob. --- src/libtomahawk/CMakeLists.txt | 2 + src/libtomahawk/DownloadJob.cpp | 446 ++++++++++++++++++++++++++++ src/libtomahawk/DownloadJob.h | 117 ++++++++ src/libtomahawk/DownloadManager.cpp | 239 +++++++++++++++ src/libtomahawk/DownloadManager.h | 70 +++++ 5 files changed, 874 insertions(+) create mode 100644 src/libtomahawk/DownloadJob.cpp create mode 100644 src/libtomahawk/DownloadJob.h create mode 100644 src/libtomahawk/DownloadManager.cpp create mode 100644 src/libtomahawk/DownloadManager.h diff --git a/src/libtomahawk/CMakeLists.txt b/src/libtomahawk/CMakeLists.txt index 17d849f14..8d878c791 100644 --- a/src/libtomahawk/CMakeLists.txt +++ b/src/libtomahawk/CMakeLists.txt @@ -15,6 +15,8 @@ set( libGuiSources ActionCollection.cpp ContextMenu.cpp + DownloadManager.cpp + DownloadJob.cpp DropJob.cpp GlobalActionManager.cpp ViewPage.cpp diff --git a/src/libtomahawk/DownloadJob.cpp b/src/libtomahawk/DownloadJob.cpp new file mode 100644 index 000000000..854d38519 --- /dev/null +++ b/src/libtomahawk/DownloadJob.cpp @@ -0,0 +1,446 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2015, Christian Muehlhaeuser + * + * Tomahawk 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. + * + * Tomahawk 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 Tomahawk. If not, see . + */ + +#include "DownloadJob.h" + +#include "Track.h" +#include "TomahawkSettings.h" +#include "utils/NetworkAccessManager.h" +#include "utils/Logger.h" + + +DownloadJob::DownloadJob( const Tomahawk::track_ptr& track, DownloadFormat format, bool tryResuming, DownloadJob::TrackState state ) + : m_state( state ) + , m_retries( 0 ) + , m_tryResuming( tryResuming ) + , m_reply( 0 ) + , m_file( 0 ) + , m_rcvdStamp( 0 ) + , m_rcvdEmit( 0 ) + , m_rcvdSize( 0 ) + , m_fileSize( 0 ) + , m_format( format ) + , m_track( track ) +{ + m_finished = ( state == Finished ); +} + + +DownloadJob::~DownloadJob() +{ +} + + +int +DownloadJob::progressPercentage() const +{ + if ( m_fileSize == 0 ) + return 0; + + return ( (double)m_rcvdSize / (double)m_fileSize ) * 100.0; +} + + +void +DownloadJob::setState( TrackState state ) +{ + TrackState oldState = m_state; + m_state = state; + + emit stateChanged( state, oldState ); + + if ( m_state == Finished ) + { + m_rcvdSize = m_fileSize; + emit finished(); + } +} + + +QString +DownloadJob::localFile() const +{ + return m_localFile; +} + + +QString +DownloadJob::localPath() const +{ + QDir dir = TomahawkSettings::instance()->downloadsPath(); + + if ( !dir.exists() ) + { + dir.mkpath( "." ); + } + + QString path = QString( "%1/%2" ).arg( safeEncode( m_track->artist(), true ) ).arg( safeEncode( m_track->album(), true ) ); + dir.mkpath( path ); + + return QString( dir.path() + "/" + path ).replace( "//", "/" ); +} + + +QUrl +DownloadJob::prepareFilename() +{ + QString filename = QString( "%1. %2.%3" ).arg( m_track->albumpos() ).arg( safeEncode( m_track->track() ) ).arg( m_format.extension ); + QString path = localPath(); + QString localFile = QString( path + "/" + filename ); + + if ( !m_tryResuming ) + { + QFileInfo fileInfo( localFile ); + unsigned int dupe = 1; + while ( dupe < 100 ) + { + QFileInfo fi( localFile ); + if ( fi.exists() ) + { + QString dupeToken = QString( " (%1)" ).arg( dupe++ ); + localFile = path + "/" + fileInfo.completeBaseName() + dupeToken + "." + fileInfo.suffix(); + } + else + break; + } + } + else + { + QFileInfo fileInfo( localFile ); + unsigned int dupe = 1; + QString lastFound = localFile; + while ( dupe < 100 ) + { + QFileInfo fi( localFile ); + if ( fi.exists() ) + { + lastFound = localFile; + QString dupeToken = QString( " (%1)" ).arg( dupe++ ); + localFile = path + "/" + fileInfo.completeBaseName() + dupeToken + "." + fileInfo.suffix(); + } + else + break; + } + + localFile = lastFound; + } + + tLog() << "Storing file as" << localFile << m_tryResuming; + return QUrl( localFile ); +} + + +void +DownloadJob::retry() +{ + tLog() << Q_FUNC_INFO; + + m_retries = 0; + m_reply = 0; + m_file = 0; + m_rcvdSize = 0; + m_fileSize = 0; + m_finished = false; + m_tryResuming = true; + + setState( Waiting ); + emit updated(); +} + + +bool +DownloadJob::download() +{ + QUrl localFile = prepareFilename(); + + if ( m_file ) + { + tLog() << "Recovering from failed download for track:" << toString() << "-" << m_retries << "retries so far."; + m_finished = false; + delete m_file; + m_file = 0; + } + + tLog() << "Saving download to file:" << localFile << localFile.toLocalFile(); + + m_file = new QFile( localFile.toString() ); + m_localFile = localFile.toString(); + + if ( m_tryResuming && checkForResumedFile() ) + return true; + + m_reply = Tomahawk::Utils::nam()->get( QNetworkRequest( m_format.url ) ); + + connect( m_reply, SIGNAL( error( QNetworkReply::NetworkError ) ), SLOT( onDownloadError( QNetworkReply::NetworkError ) ) ); + connect( m_reply, SIGNAL( downloadProgress( qint64, qint64 ) ), SLOT( onDownloadProgress( qint64, qint64 ) ) ); + connect( m_reply, SIGNAL( finished() ), SLOT( onDownloadNetworkFinished() ) ); + + setState( Running ); + return true; +} + + +void +DownloadJob::pause() +{ + if ( !m_reply ) + return; + + setState( Paused ); +// m_reply->setReadBufferSize( 0 ); +} + + +void +DownloadJob::resume() +{ + tLog() << Q_FUNC_INFO << m_finished << m_rcvdSize << m_fileSize; + if ( !m_reply ) + { + tLog() << "Initiating paused download from previous session:" << toString(); + download(); + return; + } + + setState( Running ); +// m_reply->setReadBufferSize( 65536 ); + + onDownloadProgress( m_fileSize, m_fileSize ); +} + + +void +DownloadJob::abort() +{ + tLog() << Q_FUNC_INFO << toString(); + setState( Aborted ); + + if ( m_reply ) + { + m_reply->abort(); + m_reply->deleteLater(); + m_reply = 0; + } +} + + +void +DownloadJob::onDownloadError( QNetworkReply::NetworkError code ) +{ + if ( code == QNetworkReply::NoError ) + return; + if ( m_state == Aborted ) + return; + + tLog() << "Download error for track:" << toString() << "-" << code; + + if ( ++m_retries > 3 ) + { + setState( Failed ); + } + else + { + m_tryResuming = true; + download(); + } +} + + +void +DownloadJob::onDownloadProgress( qint64 rcvd, qint64 total ) +{ + if ( m_reply == 0 ) + return; + + if ( rcvd >= m_fileSize && m_fileSize > 0 ) + { + m_finished = true; + } + + if ( state() == Paused ) + return; + + m_rcvdSize = rcvd; + m_fileSize = total; + + qint64 now = QDateTime::currentDateTime().toMSecsSinceEpoch(); + if ( ( now - 50 > m_rcvdStamp ) || ( rcvd == total ) ) + { + m_rcvdStamp = now; + if ( ( m_rcvdSize - 16384 > m_rcvdEmit ) || ( rcvd == total ) ) + { + m_rcvdEmit = m_rcvdSize; + emit progress( progressPercentage() ); + } + } + + if ( !m_file ) + return; + + if ( !m_file->isOpen() ) + { + if ( m_tryResuming && checkForResumedFile() ) + return; + + if ( !m_file->open( QIODevice::WriteOnly ) ) + { + tLog() << Q_FUNC_INFO << "Failed opening file:" << m_file->fileName(); + setState( Failed ); + return; + } + } + + QByteArray data = m_reply->readAll(); + if ( data.length() == 0 ) + return; + + if ( m_file->write( data ) < 0 ) + { + tLog() << Q_FUNC_INFO << "Failed writing to file:" << data.length(); + onDownloadError( QNetworkReply::UnknownContentError ); + return; + } + + if ( m_rcvdSize >= m_fileSize && m_fileSize > 0 ) + { + onDownloadFinished(); + } + else if ( m_reply->isFinished() && m_reply->bytesAvailable() == 0 ) + { + if ( ( m_fileSize > 0 && m_rcvdSize < m_fileSize ) || m_rcvdSize == 0 ) + { + onDownloadError( QNetworkReply::UnknownContentError ); + return; + } + } +} + + +void +DownloadJob::onDownloadNetworkFinished() +{ + tLog() << Q_FUNC_INFO << m_rcvdSize << m_fileSize; + if ( m_reply && m_reply->bytesAvailable() > 0 ) + { + tLog() << "Expecting more data!"; + return; + } + + if ( ( m_fileSize > 0 && m_rcvdSize < m_fileSize ) || m_rcvdSize == 0 ) + { + if ( m_reply ) + onDownloadError( QNetworkReply::UnknownContentError ); + return; + } +} + + +void +DownloadJob::onDownloadFinished() +{ + tLog() << Q_FUNC_INFO << m_rcvdSize << m_fileSize; + if ( state() == Paused ) + { + m_finished = true; + return; + } + + if ( ( m_fileSize > 0 && m_rcvdSize < m_fileSize ) || m_rcvdSize == 0 ) + { + onDownloadError( QNetworkReply::UnknownContentError ); + return; + } + + disconnect( m_reply, SIGNAL( error( QNetworkReply::NetworkError ) ), this, SLOT( onDownloadError( QNetworkReply::NetworkError ) ) ); + disconnect( m_reply, SIGNAL( downloadProgress( qint64, qint64 ) ), this, SLOT( onDownloadProgress( qint64, qint64 ) ) ); + disconnect( m_reply, SIGNAL( finished() ), this, SLOT( onDownloadNetworkFinished() ) ); + m_reply->abort(); + + if ( m_file && m_file->isOpen() ) + { + m_file->flush(); + m_file->close(); + } + + delete m_file; + m_file = 0; + + m_finishedTimestamp = QDateTime::currentDateTimeUtc(); + setState( Finished ); + tLog() << Q_FUNC_INFO << "Finished downloading:" << toString(); + tLog() << Q_FUNC_INFO << m_finished; +} + + +bool +DownloadJob::checkForResumedFile() +{ + tLog() << Q_FUNC_INFO; + if ( !m_tryResuming ) + return false; + + QUrl localFile = prepareFilename(); + QFileInfo fi( localFile.toString() ); + + tLog() << Q_FUNC_INFO << m_fileSize << fi.size(); + + if ( fi.size() > 0 && fi.exists() && fi.size() == m_fileSize ) + { + tLog() << Q_FUNC_INFO << "Detected previously finished download."; + m_rcvdSize = fi.size(); + m_finished = true; + onDownloadFinished(); + return true; + } + + return false; +} + + +QString +DownloadJob::safeEncode( const QString& filename, bool removeTrailingDots ) const +{ + //FIXME: make it a regexp + QString res = QString( filename ).toLatin1().replace( "/", "_" ).replace( "\\", "_" ) + .replace( "*", "_" ).replace( "?", "_" ).replace( "%", "_" ) + .replace( "'", "_" ).replace( "\"", "_" ) + .replace( ":", "_" ).replace( "#", "_" ) + .replace( "<", "_" ).replace( ">", "_" ); + + if ( removeTrailingDots ) + { + while ( res.endsWith( "." ) ) + res = res.left( res.count() - 1 ); + } + + return res.left( 127 ); +} + + +QString +DownloadJob::toString() const +{ + return m_track->toString(); +} + + +DownloadFormat +DownloadJob::format() const +{ + return m_format; +} diff --git a/src/libtomahawk/DownloadJob.h b/src/libtomahawk/DownloadJob.h new file mode 100644 index 000000000..32f76e92c --- /dev/null +++ b/src/libtomahawk/DownloadJob.h @@ -0,0 +1,117 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2015, Christian Muehlhaeuser + * + * Tomahawk 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. + * + * Tomahawk 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 Tomahawk. If not, see . + */ + +#ifndef DOWNLOADJOB_H +#define DOWNLOADJOB_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Track.h" + +struct DownloadFormat +{ + QUrl url; + QString extension; + QString mimetype; +}; + +class DownloadJob : public QObject +{ +Q_OBJECT + +public: + enum TrackState + { Waiting = 0, Running, Paused, Failed, Finished, Aborted, Any }; + + DownloadJob( const Tomahawk::track_ptr& track, DownloadFormat format, bool tryResuming = false, DownloadJob::TrackState state = Waiting ); + ~DownloadJob(); + + QString toString() const; + + TrackState state() const { return m_state; } + unsigned int retries() const { return m_retries; } + + int progressPercentage() const; + long receivedSize() const { return m_rcvdSize; } + long fileSize() const { return m_fileSize; } + + QString localPath() const; + QString localFile() const; + DownloadFormat format() const; + + QDateTime finishedTimestamp() const { return m_finishedTimestamp; } + void setFinishedTimestamp( const QDateTime& timestamp ) { m_finishedTimestamp = timestamp; } + + void setState( TrackState state ); + +public slots: + bool download(); + void pause(); + void resume(); + void retry(); + void abort(); + +signals: + void updated(); + void progress( int percentage ); + void stateChanged( DownloadJob::TrackState newState, DownloadJob::TrackState oldState ); + void finished(); + +private slots: + void onDownloadNetworkFinished(); + void onDownloadError( QNetworkReply::NetworkError code ); + void onDownloadProgress( qint64, qint64 ); + void onDownloadFinished(); + +private: + void storeState(); + QString safeEncode( const QString& filename, bool removeTrailingDots = false ) const; + bool checkForResumedFile(); + QUrl prepareFilename(); + + TrackState m_state; + unsigned int m_retries; + bool m_tryResuming; + + QNetworkReply* m_reply; + QFile* m_file; + + qint64 m_rcvdStamp; + long m_rcvdEmit; + long m_rcvdSize; + long m_fileSize; + bool m_finished; + + QString m_localFile; + + QDateTime m_finishedTimestamp; + + DownloadFormat m_format; + Tomahawk::track_ptr m_track; +}; + +typedef QSharedPointer downloadjob_ptr; + +#endif // DOWNLOADJOB_H diff --git a/src/libtomahawk/DownloadManager.cpp b/src/libtomahawk/DownloadManager.cpp new file mode 100644 index 000000000..ef6b715b0 --- /dev/null +++ b/src/libtomahawk/DownloadManager.cpp @@ -0,0 +1,239 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2015, Christian Muehlhaeuser + * + * Tomahawk 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. + * + * Tomahawk 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 Tomahawk. If not, see . + */ + +#include "DownloadManager.h" + +#include + +#include "utils/Logger.h" + +DownloadManager* DownloadManager::s_instance = 0; + + +DownloadManager* +DownloadManager::instance() +{ + if ( !s_instance ) + { + s_instance = new DownloadManager(); + } + + return s_instance; +} + + +DownloadManager::DownloadManager() + : m_globalState( true ) +{ + tLog() << Q_FUNC_INFO << "Initializing DownloadManager."; + + QTimer::singleShot( 0, this, SLOT( resumeJobs() ) ); +} + + +DownloadManager::~DownloadManager() +{ + tLog() << Q_FUNC_INFO << "Shutting down DownloadManager."; + + QList< downloadjob_ptr > jl; + foreach ( const downloadjob_ptr& job, jobs() ) + { + jl << job; + } + +// TomahawkSettings::instance()->storeJobs( jl ); +} + + +void +DownloadManager::resumeJobs() +{ + tLog() << Q_FUNC_INFO; + +/* QList jobs = TomahawkSettings::instance()->storedJobs(); + for ( int i = jobs.count() - 1; i >= 0; i-- ) + { + downloadjob_ptr job = jobs.at( i ); + addJob( job ); + } + + tLog() << Q_FUNC_INFO << "Restored" << jobs.count() << "existing jobs.";*/ +} + + +QList +DownloadManager::jobs( DownloadJob::TrackState state ) const +{ + if ( state < 0 ) + return m_jobs; + + QList jobs; + foreach ( const downloadjob_ptr& job, m_jobs ) + { + if ( job.isNull() ) + continue; + + if ( state == DownloadJob::TrackState::Any || job->state() == state ) + jobs << job; + } + + return jobs; +} + + +bool +DownloadManager::addJob( const downloadjob_ptr& job ) +{ + if ( job.isNull() ) + { + tLog() << "Found invalid download job - ignoring!"; + return false; + } + + if ( containsJob( job ) ) + { + tLog() << "Found duplicate download job - ignoring:" << job->toString(); + return false; + } + + m_jobs << job; + emit jobAdded( job ); + + connect( job.data(), SIGNAL( finished() ), SLOT( checkJobs() ) ); + connect( job.data(), SIGNAL( stateChanged( DownloadJob::TrackState, DownloadJob::TrackState ) ), SLOT( checkJobs() ) ) ; +// connect( job.data(), SIGNAL( stateChanged( DownloadJob::TrackState, DownloadJob::TrackState ) ), SIGNAL( stateChanged( DownloadJob::TrackState, DownloadJob::TrackState ) ) ); + + checkJobs(); + return true; +} + + +bool +DownloadManager::removeJob( const downloadjob_ptr& job ) +{ + tLog() << "Removing job:" << job->toString(); + job->abort(); + m_jobs.removeAll( job ); + emit jobRemoved( job ); + + checkJobs(); + + return true; +} + + +bool +DownloadManager::containsJob( const downloadjob_ptr& job ) const +{ + return m_jobs.contains( job ); +} + + +downloadjob_ptr +DownloadManager::currentJob() const +{ + QList j = jobs( DownloadJob::TrackState::Running ); + if ( j.count() ) + return j.first(); + + j = jobs( DownloadJob::TrackState::Paused ); + if ( j.count() ) + return j.first(); + + j = jobs( DownloadJob::TrackState::Waiting ); + if ( j.count() ) + return j.first(); + + return downloadjob_ptr(); +} + + + +DownloadManager::DownloadManagerState +DownloadManager::state() const +{ + if ( !currentJob().isNull() ) + { + switch ( currentJob()->state() ) + { + case DownloadJob::TrackState::Waiting: + return DownloadManager::DownloadManagerState::Waiting; + + case DownloadJob::TrackState::Paused: + return DownloadManager::DownloadManagerState::Paused; + + case DownloadJob::TrackState::Running: + return DownloadManager::DownloadManagerState::Running; + } + } + + return DownloadManager::DownloadManagerState::Waiting; +} + + +void +DownloadManager::checkJobs() +{ + if ( !m_globalState ) + return; + + if ( state() == DownloadManager::DownloadManagerState::Waiting && !currentJob().isNull() ) + { + downloadjob_ptr job = currentJob(); +/* connect( job.data(), SIGNAL( finished() ), SLOT( checkJobs() ) ); + connect( job.data(), SIGNAL( stateChanged( DownloadJob::TrackState, DownloadJob::TrackState ) ), SLOT( checkJobs() ) ) ; + connect( job.data(), SIGNAL( stateChanged( DownloadJob::TrackState, DownloadJob::TrackState ) ), SIGNAL( stateChanged( Track::TrackState, Track::TrackState ) ) );*/ + + job->download(); + } +} + + +void +DownloadManager::pause() +{ + tLog() << Q_FUNC_INFO; + + m_globalState = false; + foreach ( const downloadjob_ptr& job, jobs( DownloadJob::TrackState::Running ) ) + { + job->pause(); + } +} + + +void +DownloadManager::resume() +{ + tLog() << Q_FUNC_INFO; + + m_globalState = true; + + if ( jobs( DownloadJob::TrackState::Paused ).count() ) + { + foreach ( const downloadjob_ptr& job, jobs( DownloadJob::TrackState::Paused ) ) + { + tLog() << "Resuming job:" << job->toString(); + job->resume(); + } + + return; + } + + checkJobs(); +} diff --git a/src/libtomahawk/DownloadManager.h b/src/libtomahawk/DownloadManager.h new file mode 100644 index 000000000..2714ecebf --- /dev/null +++ b/src/libtomahawk/DownloadManager.h @@ -0,0 +1,70 @@ +/* === This file is part of Tomahawk Player - === + * + * Copyright 2015, Christian Muehlhaeuser + * + * Tomahawk 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. + * + * Tomahawk 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 Tomahawk. If not, see . + */ + +#ifndef DOWNLOADMANAGER_H +#define DOWNLOADMANAGER_H + +#include +#include +#include + +#include "DownloadJob.h" + +class DownloadManager : public QObject +{ +Q_OBJECT + +public: + enum DownloadManagerState { None, Running, Paused, Waiting }; + static DownloadManager* instance(); + + DownloadManager(); + ~DownloadManager(); + + DownloadManagerState state() const; + + QList jobs( DownloadJob::TrackState state = DownloadJob::TrackState::Any ) const; + bool containsJob( const downloadjob_ptr& job ) const; + downloadjob_ptr currentJob() const; + +public slots: + bool addJob( const downloadjob_ptr& job ); + bool removeJob( const downloadjob_ptr& job ); + void checkJobs(); + + void pause(); + void resume(); + + void resumeJobs(); + +signals: + void jobAdded( const downloadjob_ptr& job ); + void jobRemoved( const downloadjob_ptr& job ); + + void stateChanged( DownloadManagerState newState, DownloadManagerState oldState ); + +private slots: + +private: + QList< downloadjob_ptr > m_jobs; + bool m_globalState; + + static DownloadManager* s_instance; +}; + +#endif // DOWNLOADMANAGER_H