/*
Copyright (C) 2012 Sebastian Herbord. All rights reserved.
This file is part of Mod Organizer.
Mod Organizer 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.
Mod Organizer 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 Mod Organizer. If not, see .
*/
#ifndef DOWNLOADMANAGER_H
#define DOWNLOADMANAGER_H
#include "serverinfo.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace MOBase { class IPluginGame; }
class NexusInterface;
class PluginContainer;
class OrganizerCore;
/*!
* \brief manages downloading of files and provides progress information for gui elements
**/
class DownloadManager : public MOBase::IDownloadManager
{
Q_OBJECT
public:
enum DownloadState {
STATE_STARTED = 0,
STATE_DOWNLOADING,
STATE_CANCELING,
STATE_PAUSING,
STATE_CANCELED,
STATE_PAUSED,
STATE_ERROR,
STATE_FETCHINGMODINFO,
STATE_FETCHINGFILEINFO,
STATE_FETCHINGMODINFO_MD5,
STATE_NOFETCH,
STATE_READY,
STATE_INSTALLED,
STATE_UNINSTALLED
};
private:
struct DownloadInfo {
~DownloadInfo() { delete m_FileInfo; }
unsigned int m_DownloadID;
QString m_FileName;
QFile m_Output;
QNetworkReply *m_Reply;
QElapsedTimer m_StartTime;
qint64 m_PreResumeSize;
std::pair m_Progress;
std::tuple m_SpeedDiff;
bool m_HasData;
DownloadState m_State;
int m_CurrentUrl;
QStringList m_Urls;
qint64 m_ResumePos;
qint64 m_TotalSize;
QDateTime m_Created; // used as a cache in DownloadManager::getFileTime, may not be valid elsewhere
QByteArray m_Hash;
QStringList m_GamesToQuery;
QString m_RemoteFileName;
int m_Tries;
bool m_ReQueried;
quint32 m_TaskProgressId;
MOBase::ModRepositoryFileInfo *m_FileInfo { nullptr };
bool m_Hidden;
static DownloadInfo *createNew(const MOBase::ModRepositoryFileInfo *fileInfo, const QStringList &URLs);
static DownloadInfo *createFromMeta(const QString &filePath, bool showHidden, const QString outputDirectory);
/**
* @brief rename the file
* this will change the file name as well as the display name. It will automatically
* append .unfinished to the name if this file is still being downloaded
* @param newName the new name to setName
* @param renameFile if true, the file is assumed to exist and renamed. If the file does not
* yet exist, set this to false
**/
void setName(QString newName, bool renameFile);
unsigned int downloadID() { return m_DownloadID; }
bool isPausedState();
QString currentURL();
private:
static unsigned int s_NextDownloadID;
private:
DownloadInfo() : m_TotalSize(0), m_ReQueried(false), m_Hidden(false), m_SpeedDiff(std::tuple(0,0,0,0,0)), m_HasData(false) {}
};
public:
/**
* @brief constructor
*
* @param nexusInterface interface to use to retrieve information from the relevant nexus page
* @param parent parent object
**/
explicit DownloadManager(NexusInterface *nexusInterface, QObject *parent);
~DownloadManager();
void setParentWidget(QWidget* w);
/**
* @brief determine if a download is currently in progress
*
* @return true if there is currently a download in progress
**/
bool downloadsInProgress();
/**
* @brief determine if a download is currently in progress, does not count paused ones.
*
* @return true if there is currently a download in progress (that is not paused already).
**/
bool downloadsInProgressNoPause();
/**
* @brief set the output directory to write to
*
* @param outputDirectory the new output directory
**/
void setOutputDirectory(const QString &outputDirectory, const bool refresh = true);
/**
* @brief disables feedback from the downlods fileSystemWhatcher untill disableDownloadsWatcherEnd() is called
*
**/
static void startDisableDirWatcher();
/**
* @brief re-enables feedback from the downlods fileSystemWhatcher after disableDownloadsWatcherStart() was called
**/
static void endDisableDirWatcher();
/**
* @return current download directory
**/
QString getOutputDirectory() const { return m_OutputDirectory; }
/**
* @brief set the list of supported extensions
* @param extensions list of supported extensions
*/
void setSupportedExtensions(const QStringList &extensions);
/**
* @brief sets whether hidden files are to be shown after all
*/
void setShowHidden(bool showHidden);
void setPluginContainer(PluginContainer *pluginContainer);
/**
* @brief download from an already open network connection
*
* @param reply the network reply to download from
* @param fileInfo information about the file, like mod id, file id, version, ...
* @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again
**/
bool addDownload(QNetworkReply *reply, const MOBase::ModRepositoryFileInfo *fileInfo);
/**
* @brief download from an already open network connection
*
* @param reply the network reply to download from
* @param fileName the name to use for the file. This may be overridden by the name in the fileInfo-structure or if the http stream specifies a name
* @param fileInfo information previously retrieved from the nexus network
* @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again
**/
bool addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, QString gameName, int modID, int fileID = 0, const MOBase::ModRepositoryFileInfo *fileInfo = new MOBase::ModRepositoryFileInfo());
/**
* @brief start a download using a nxm-link
*
* starts a download using a nxm-link. The download manager will first query the nexus
* page for file information.
* @param url a nxm link looking like this: nxm://skyrim/mods/1234/files/4711
* @todo the game name encoded into the link is currently ignored, all downloads are incorrectly assumed to be for the identified game
**/
void addNXMDownload(const QString &url);
/**
* @brief retrieve the total number of downloads, both finished and unfinished including downloads from previous sessions
*
* @return total number of downloads
**/
int numTotalDownloads() const;
/**
* @brief retrieve number of pending downloads (nexus downloads for which we don't know the name and url yet)
* @return number of pending downloads
*/
int numPendingDownloads() const;
/**
* @brief retrieve the info of a pending download
* @param index index of the pending download (index in the range [0, numPendingDownloads()[)
* @return pair of modid, fileid
*/
std::tuple getPendingDownload(int index);
/**
* @brief retrieve the full path to the download specified by index
*
* @param index the index to look up
* @return absolute path of the file
**/
QString getFilePath(int index) const;
/**
* @brief retrieve a descriptive name of the download specified by index
*
* @param index index of the file to look up
* @return display name of the file
**/
QString getDisplayName(int index) const;
/**
* @brief retrieve the filename of the download specified by index
*
* @param index index of the file to look up
* @return name of the file
**/
QString getFileName(int index) const;
/**
* @brief retrieve the file size of the download specified by index
*
* @param index index of the file to look up
* @return size of the file (total size during download)
*/
qint64 getFileSize(int index) const;
/**
* @brief retrieve the creation time of the download specified by index
* @param index index of the file to look up
* @return size of the file (total size during download)
*/
QDateTime getFileTime(int index) const;
/**
* @brief retrieve the current progress of the download specified by index
*
* @param index index of the file to look up
* @return progress of the download in percent (integer)
**/
std::pair getProgress(int index) const;
/**
* @brief retrieve the current state of the download
*
* retrieve the current state of the download. A download usually goes through
* the following states:
* started -> downloading -> fetching mod info -> fetching file info -> done
* in case of downloads started via nxm-link, file information is fetched first
*
* @param index index of the file to look up
* @return the download state
**/
DownloadState getState(int index) const;
/**
* @param index index of the file to look up
* @return true if the nexus information for this download is not complete
**/
bool isInfoIncomplete(int index) const;
/**
* @brief retrieve the nexus mod id of the download specified by index
*
* @param index index of the file to look up
* @return the nexus mod id
**/
int getModID(int index) const;
/**
* @brief retrieve the displayable game name of the download specified by the index
*
* @param index index of the file to look up
* @return the displayable game name
**/
QString getDisplayGameName(int index) const;
/**
* @brief retrieve the game name of the downlaod specified by the index
*
* @param index index of the file to look up
* @return the game name
**/
QString getGameName(int index) const;
/**
* @brief determine if the specified file is supposed to be hidden
* @param index index of the file to look up
* @return true if the specified file is supposed to be hidden
*/
bool isHidden(int index) const;
/**
* @brief retrieve all nexus info of the download specified by index
*
* @param index index of the file to look up
* @return the nexus mod information
**/
const MOBase::ModRepositoryFileInfo *getFileInfo(int index) const;
/**
* @brief mark a download as installed
*
* @param index index of the file to mark installed
*/
void markInstalled(int index);
void markInstalled(QString download);
/**
* @brief mark a download as uninstalled
*
* @param index index of the file to mark uninstalled
*/
void markUninstalled(int index);
void markUninstalled(QString download);
/**
* @brief refreshes the list of downloads
*/
void refreshList();
virtual int startDownloadURLs(const QStringList &urls);
virtual int startDownloadNexusFile(int modID, int fileID);
virtual QString downloadPath(int id);
/**
* @brief retrieve a download index from the filename
* @param fileName file to look up
* @return index of that download or -1 if it wasn't found
*/
int indexByName(const QString &fileName) const;
int indexByInfo(const DownloadInfo* info) const;
void pauseAll();
signals:
void aboutToUpdate();
/**
* @brief signals that the specified download has changed
*
* @param row the row that changed. This corresponds to the download index
**/
void update(int row);
/**
* @brief signals the ui that a message should be displayed
*
* @param message the message to display
**/
void showMessage(const QString &message);
/**
* @brief emitted whenever the state of a download changes
* @param row the row that changed
* @param state the new state
*/
void stateChanged(int row, DownloadManager::DownloadState state);
/**
* @brief emitted whenever a download completes successfully, reporting the download speed for the server used
*/
void downloadSpeed(const QString &serverName, int bytesPerSecond);
/**
* @brief emitted whenever a new download is added to the list
*/
void downloadAdded();
public slots:
/**
* @brief removes the specified download
*
* @param index index of the download to remove
* @param deleteFile if true, the file will also be deleted from disc, otherwise it is only marked as hidden.
**/
void removeDownload(int index, bool deleteFile);
/**
* @brief restores the specified download to view (which was previously hidden
* @param index index of the download to restore
*/
void restoreDownload(int index);
/**
* @brief cancel the specified download. This will lead to the corresponding file to be deleted
*
* @param index index of the download to cancel
**/
void cancelDownload(int index);
void pauseDownload(int index);
void resumeDownload(int index);
void queryInfo(int index);
void queryInfoMd5(int index);
void visitOnNexus(int index);
void openFile(int index);
void openInDownloadsFolder(int index);
void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID);
void nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
void nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
void nxmFileInfoFromMd5Available(QString gameName, QVariant userData, QVariant resultData, int requestID);
void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString);
void managedGameChanged(MOBase::IPluginGame const *gamePlugin);
private slots:
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void downloadReadyRead();
void downloadFinished(int index = 0);
void downloadError(QNetworkReply::NetworkError error);
void metaDataChanged();
void directoryChanged(const QString &dirctory);
void checkDownloadTimeout();
private:
void createMetaFile(DownloadInfo *info);
DownloadManager::DownloadInfo* getDownloadInfo(QString fileName);
public:
/** Get a unique filename for a download.
*
* This allows you multiple versions of download files, useful if the file
* comes from a web site with no version control
*
* @param basename: Name of the file
*
* @return Unique(ish) name
*/
QString getDownloadFileName(const QString &baseName, bool rename = false) const;
private:
void startDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume);
void resumeDownloadInt(int index);
/**
* @brief start a download from a url
*
* @param url the url to download from
* @param fileInfo information previously retrieved from the mod page
* @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again
**/
bool addDownload(const QStringList &URLs, QString gameName, int modID, int fileID, const MOBase::ModRepositoryFileInfo *fileInfo);
// important: the caller has to lock the list-mutex, otherwise the DownloadInfo-pointer might get invalidated at any time
DownloadInfo *findDownload(QObject *reply, int *index = nullptr) const;
void removeFile(int index, bool deleteFile);
void refreshAlphabeticalTranslation();
bool ByName(int LHS, int RHS);
QString getFileNameFromNetworkReply(QNetworkReply *reply);
void setState(DownloadInfo *info, DownloadManager::DownloadState state);
DownloadInfo *downloadInfoByID(unsigned int id);
void removePending(QString gameName, int modID, int fileID);
static QString getFileTypeString(int fileType);
void writeData(DownloadInfo *info);
private:
static const int AUTOMATIC_RETRIES = 3;
private:
NexusInterface *m_NexusInterface;
OrganizerCore *m_OrganizerCore;
QWidget* m_ParentWidget;
QVector> m_PendingDownloads;
QVector m_ActiveDownloads;
QString m_OutputDirectory;
QStringList m_SupportedExtensions;
std::set m_RequestIDs;
QVector m_AlphabeticalTranslation;
QFileSystemWatcher m_DirWatcher;
//The dirWatcher is actually triggering off normal Mo operations such as deleting downloads or editing .meta files
//so it needs to be disabled during operations that are known to cause the creation or deletion of files in the Downloads folder.
//Notably using QSettings to edit a file creates a temporarily .lock file that causes the Watcher to trigger multiple listRefreshes freezing the ui.
static int m_DirWatcherDisabler;
std::map m_DownloadFails;
bool m_ShowHidden;
MOBase::IPluginGame const *m_ManagedGame;
QTimer m_TimeoutTimer;
};
#endif // DOWNLOADMANAGER_H