diff --git a/build_debug.sh b/build_debug.sh deleted file mode 100755 index 2466a19..0000000 --- a/build_debug.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -mkdir -p build/debug && \ -cd build/debug && \ -qmake ../../torrent-client-v2.pro -spec linux-g++ CONFIG+=debug && \ -make diff --git a/build_release.sh b/build_release.sh deleted file mode 100755 index a3c05fd..0000000 --- a/build_release.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -mkdir -p build/release && \ -cd build/release && \ -qmake ../../torrent-client-v2.pro -spec linux-g++ && \ -make diff --git a/scripts/build_debug.sh b/scripts/build_debug.sh new file mode 100755 index 0000000..9482a0e --- /dev/null +++ b/scripts/build_debug.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# This script was designed to be run from Qt Creator through Docker + +mkdir -p build/debug && \ +cd build/debug && \ +qmake ../../torrent-client-v2.pro -spec linux-g++ CONFIG+=debug CONFIG-=qml_debug CONFIG-=qtquickcompiler && \ +make -j 32 diff --git a/scripts/build_release.sh b/scripts/build_release.sh new file mode 100755 index 0000000..a63fc93 --- /dev/null +++ b/scripts/build_release.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# This script was designed to be run from Qt Creator through Docker + +mkdir -p build/release && \ +cd build/release && \ +qmake ../../torrent-client-v2.pro -spec linux-g++ CONFIG-=qml_debug CONFIG-=qtquickcompiler && \ +make -j 32 diff --git a/scripts/run_debug.sh b/scripts/run_debug.sh new file mode 100755 index 0000000..7daed7e --- /dev/null +++ b/scripts/run_debug.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +# This script was designed to be run from Qt Creator through Docker + +cd build/debug && \ +./torrent-client-v2 diff --git a/scripts/run_release.sh b/scripts/run_release.sh new file mode 100644 index 0000000..7daed7e --- /dev/null +++ b/scripts/run_release.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +# This script was designed to be run from Qt Creator through Docker + +cd build/debug && \ +./torrent-client-v2 diff --git a/src/application.cpp b/src/application.cpp index d14ebf7..d912b69 100644 --- a/src/application.cpp +++ b/src/application.cpp @@ -1,7 +1,30 @@ #include "application.h" Application::Application(int argc, char *argv[]) - : QCoreApplication(argc, argv) + : QCoreApplication(argc, argv), m_torrentClient(nullptr) { +} + +bool Application::boot() +{ + // Boot the torrent client +// m_torrentClient = new TorrentClient(); +// if (!m_torrentClient->boot()) { +// return false; +// } + // Boot the IPC socket server interface + m_ipcServer = new IpcServer(m_torrentClient, "/var/autoplex/ipc/torrent_client.sock"); + if (!m_ipcServer->boot()) { + return false; + } + return true; +} + +int Application::exec() +{ + if (!boot()) { + return 1; + } + return QCoreApplication::exec(); } diff --git a/src/application.h b/src/application.h index ffa1de2..2936391 100644 --- a/src/application.h +++ b/src/application.h @@ -3,6 +3,8 @@ #include #include +#include "./interface/ipcserver.h" +#include "./torrent/torrentclient.h" class Application : public QCoreApplication { @@ -10,6 +12,13 @@ class Application : public QCoreApplication public: Application(int argc, char *argv[]); + bool boot(); + virtual int exec(); + +private: + IpcServer *m_ipcServer; + TorrentClient *m_torrentClient; + signals: }; diff --git a/src/interface/ipcserver.cpp b/src/interface/ipcserver.cpp new file mode 100644 index 0000000..62aff51 --- /dev/null +++ b/src/interface/ipcserver.cpp @@ -0,0 +1,85 @@ +#include "ipcserver.h" +#include + +/** + * Create a new IPC server instance + * + * @param client + * @param socketPath + */ +IpcServer::IpcServer(TorrentClient *client, QString socketPath) : QObject() +{ + m_server = nullptr; + m_socketPath = socketPath; + m_torrentClient = client; +} + +/** + * Boot the IPC server + * @return + */ +bool IpcServer::boot() +{ + m_server = new QLocalServer(); + m_server->setSocketOptions(QLocalServer::UserAccessOption); + m_server->setMaxPendingConnections(1); + if (!m_server->listen(m_socketPath)) { + return false; + } + + connect(m_server, &QLocalServer::newConnection, this, &IpcServer::connectIpcSockets); + return true; +} + +/** + * Shutdown the IPC server + */ +void IpcServer::shutdown() +{ + if (m_server == nullptr) { + return; + } + m_server->close(); + disconnectIpcSockets(); + m_server->deleteLater(); + m_server = nullptr; +} + +// ------------------------------------------------------------------------------------------------- + +/** + * Connect all pending IPC socket connections + */ +void IpcServer::connectIpcSockets() +{ + while (m_server->hasPendingConnections()) { + IpcSocketInterface *socket = new IpcSocketInterface(m_server->nextPendingConnection(), m_torrentClient); + connect(socket, &IpcSocketInterface::disconnected, this, &IpcServer::removeIpcSocket); + m_connections.append(socket); + } +} + +/** + * Disconnect all IPC socket clients + */ +void IpcServer::disconnectIpcSockets() +{ + qDebug() << "Disconnecting sockets..."; + foreach (auto socket, m_connections) { + disconnect(socket, &IpcSocketInterface::disconnected, this, &IpcServer::removeIpcSocket); + socket->disconnectSocket(); + socket->deleteLater(); + } + m_connections.clear(); +} + +/** + * Remove an IPC socket when it has been disconnected + */ +void IpcServer::removeIpcSocket() +{ + IpcSocketInterface *socket = qobject_cast(sender()); + qDebug() << "Socket disconnected"; + m_connections.removeOne(socket); + socket->deleteLater(); +} diff --git a/src/interface/ipcserver.h b/src/interface/ipcserver.h new file mode 100644 index 0000000..352e5a1 --- /dev/null +++ b/src/interface/ipcserver.h @@ -0,0 +1,35 @@ +#ifndef IPCSERVER_H +#define IPCSERVER_H + +#include +#include +#include +#include +#include "./ipcsocketinterface.h" +#include "../torrent/torrentclient.h" + +class IpcServer : public QObject +{ +public: + explicit IpcServer(TorrentClient *client, QString socketPath); + + bool boot(); + void shutdown(); + +private: + QString m_socketPath; + QLocalServer *m_server; + TorrentClient *m_torrentClient; + + QList m_connections; + +protected slots: + void connectIpcSockets(); + void disconnectIpcSockets(); + void removeIpcSocket(); + +signals: + +}; + +#endif // IPCSERVER_H diff --git a/src/interface/ipcsocketinterface.cpp b/src/interface/ipcsocketinterface.cpp new file mode 100644 index 0000000..da7e943 --- /dev/null +++ b/src/interface/ipcsocketinterface.cpp @@ -0,0 +1,55 @@ +#include "ipcsocketinterface.h" + +/** + * Create a new IPC socket interface + * + * @param socket + * @param client + */ +IpcSocketInterface::IpcSocketInterface(QLocalSocket *socket, TorrentClient *client) + : TorrentClientInterface(client), m_socket(socket) +{ + connect(m_socket, &QLocalSocket::readyRead, this, &IpcSocketInterface::readBytes); + connect(m_socket, &QLocalSocket::disconnected, this, &IpcSocketInterface::disconnected); + qDebug() << "Connection established"; +} + +/** + * Disconnect the client + */ +void IpcSocketInterface::disconnectSocket() +{ + qDebug() << "Closing socket..."; + m_socket->close(); +} + +/** + * Read the bytes from the socket and execute the request + */ +void IpcSocketInterface::readBytes() +{ + int offset; + while (m_socket->bytesAvailable()) { + offset = m_buffer.size(); + m_buffer.append(m_socket->readAll()); + for (int i = offset; i < m_buffer.size(); i++) { + // If the end of the request is located, execute the request + if (m_buffer[i] == '\f') { + exec(m_buffer.mid(0, i)); + m_buffer = m_buffer.mid(i); + } + } + } +} + +/** + * Write the provided bytes to the socket + * + * @param data + */ +void IpcSocketInterface::writeBytes(const QByteArray data) +{ + m_socket->write(data); + m_socket->write("\f"); + m_socket->flush(); +} diff --git a/src/interface/ipcsocketinterface.h b/src/interface/ipcsocketinterface.h new file mode 100644 index 0000000..fbad27e --- /dev/null +++ b/src/interface/ipcsocketinterface.h @@ -0,0 +1,31 @@ +#ifndef IPCSOCKETINTERFACE_H +#define IPCSOCKETINTERFACE_H + +#include +#include +#include +#include +#include "torrentclientinterface.h" +#include "../torrent/torrentclient.h" + +class IpcSocketInterface : public TorrentClientInterface +{ + Q_OBJECT +public: + explicit IpcSocketInterface(QLocalSocket *socket, TorrentClient *client); + +private: + QLocalSocket *m_socket; + QByteArray m_buffer; + +public slots: + void disconnectSocket(); + void readBytes(); + virtual void writeBytes(const QByteArray data) override; + +signals: + void disconnected(); + +}; + +#endif // IPCSOCKETINTERFACE_H diff --git a/src/interface/torrentclientinterface.cpp b/src/interface/torrentclientinterface.cpp new file mode 100644 index 0000000..1ea1a0e --- /dev/null +++ b/src/interface/torrentclientinterface.cpp @@ -0,0 +1,96 @@ +#include "torrentclientinterface.h" + +/** + * Create a new torrent client interface instance + * + * @param client + */ +TorrentClientInterface::TorrentClientInterface(TorrentClient *client) + : QObject(), + m_torrentClient(client), + m_metaObject(staticMetaObject), + m_metaEnum(m_metaObject.enumerator(m_metaObject.indexOfEnumerator("RequestType"))) +{ + +} + +/** + * Parse and execute the given request and sending any provided responses + * + * @param data + */ +void TorrentClientInterface::exec(QByteArray data) +{ + QJsonDocument json = QJsonDocument::fromJson(data); + if (!json.isObject()) { + qDebug() << "Unable to parse request"; + return; + } + QJsonObject requestPacket = json.object(); + if (requestPacket.count() != 2 || !requestPacket.contains("type") || !requestPacket.contains("data")) { + qDebug() << "Invalid request received"; + return; + } + if (!requestPacket["type"].isString() || !requestPacket["data"].isObject()) { + qDebug() << "Malformed request received"; + return; + } + + // Perform the request + QString requestType = requestPacket["type"].toString(); + QJsonValueRef body = requestPacket["body"]; + QJsonValue *responseData = request(requestType, body); + if (responseData == nullptr) { + return; + } + + // Construct and write the response + QJsonObject response; + response["type"] = requestType; + response["data"] = *responseData; + writeBytes(QJsonDocument(response).toJson(QJsonDocument::Compact)); + delete responseData; +} + +// ------------------------------------------------------------------------------------------------- + +/** + * Match and perform the provided request + * + * @param type + * @param body + * @return + */ +QJsonValue* TorrentClientInterface::request(const QString type, const QJsonValueRef &body) +{ + return request((RequestType)m_metaEnum.keyToValue(("request_" + type).toLatin1()), body); +} + +/** + * Match and perform the provided request + * + * @param type + * @param body + * @return + */ +QJsonValue* TorrentClientInterface::request(const RequestType type, const QJsonValueRef &body) +{ + switch(type) { + case request_list: return requestList(body); + default: + return nullptr; + } +} + +// ------------------------------------------------------------------------------------------------- + +/** + * Request the list of torrents from the client + * + * @param body + * @return + */ +QJsonValue* TorrentClientInterface::requestList(const QJsonValueRef &body) +{ + return nullptr; +} diff --git a/src/interface/torrentclientinterface.h b/src/interface/torrentclientinterface.h new file mode 100644 index 0000000..ba1058d --- /dev/null +++ b/src/interface/torrentclientinterface.h @@ -0,0 +1,45 @@ +#ifndef TORRENTCLIENTINTERFACE_H +#define TORRENTCLIENTINTERFACE_H + +#include +#include +#include +#include +#include +#include +#include +#include "../torrent/torrentclient.h" + +class TorrentClientInterface : public QObject +{ + Q_OBJECT + Q_ENUMS(RequestType) + + QMetaObject m_metaObject; + QMetaEnum m_metaEnum; + +public: + enum RequestType + { + request_list + }; + + explicit TorrentClientInterface(TorrentClient *client); + +protected: + virtual void writeBytes(const QByteArray data) = 0; + + QJsonValue* request(const QString type, const QJsonValueRef &body); + QJsonValue* request(const RequestType type, const QJsonValueRef &body); + + // Interface methods + QJsonValue* requestList(const QJsonValueRef &body); + +private: + const TorrentClient *m_torrentClient; + +public slots: + void exec(QByteArray data); +}; + +#endif // TORRENTCLIENTINTERFACE_H diff --git a/src/torrent/alertmanager.cpp b/src/torrent/alertmanager.cpp new file mode 100644 index 0000000..58243d3 --- /dev/null +++ b/src/torrent/alertmanager.cpp @@ -0,0 +1,40 @@ +#include "alertmanager.h" + +/** + * A macro to generate the switch case statements to handle specific alert types + */ +#define ALERT_EVENT_CASE(name, signal, alert) \ + case name: \ + emit signal(lt::alert_cast(alert)); \ + break + +AlertManager::AlertManager(QObject *parent) : QObject(parent) +{ + connect(this, SIGNAL(alertsReady(lt::session_handle*)), this, SLOT(dispatchAlertsForSession(lt::session_handle*))); +} + +void AlertManager::addSession(lt::session_handle *session) +{ + session->set_alert_notify([this, session]() -> void { + emit alertsReady(session); + }); +} + +void AlertManager::dispatchAlertsForSession(lt::session_handle *session) +{ + std::vector alerts; + session->pop_alerts(&alerts); + + for (auto alert : alerts) { + switch(alert->type()) { + ALERT_EVENT_CASE(add_torrent_alert, addTorrentAlerted, alert); + ALERT_EVENT_CASE(file_completed_alert, fileCompletedAlerted, alert); + ALERT_EVENT_CASE(metadata_received_alert, metadataReceivedAlerted, alert); + ALERT_EVENT_CASE(save_resume_data_alert, saveResumeDataAlerted, alert); + ALERT_EVENT_CASE(save_resume_data_failed_alert, saveResumeDataFailedAlerted, alert); + ALERT_EVENT_CASE(state_changed_alert, stateChangedAlerted, alert); + ALERT_EVENT_CASE(state_update_alert, stateUpdateAlerted, alert); + ALERT_EVENT_CASE(torrent_finished_alert, torrentFinishedAlerted, alert); + } + } +} diff --git a/src/torrent/alertmanager.h b/src/torrent/alertmanager.h new file mode 100644 index 0000000..fba2fd4 --- /dev/null +++ b/src/torrent/alertmanager.h @@ -0,0 +1,36 @@ +#ifndef ALERTMANAGER_H +#define ALERTMANAGER_H + +#include + +#include +#include + +#include "alerts.h" + +class AlertManager : public QObject +{ + Q_OBJECT +public: + explicit AlertManager(QObject *parent = nullptr); + + void addSession(lt::session_handle *session); + +protected slots: + void dispatchAlertsForSession(lt::session_handle *session); + +signals: + void alertsReady(lt::session_handle *session); + + // Alerts + void addTorrentAlerted(const lt::add_torrent_alert*); + void fileCompletedAlerted(const lt::file_completed_alert*); + void metadataReceivedAlerted(const lt::metadata_received_alert*); + void saveResumeDataAlerted(const lt::save_resume_data_alert*); + void saveResumeDataFailedAlerted(const lt::save_resume_data_failed_alert*); + void stateChangedAlerted(const lt::state_changed_alert*); + void stateUpdateAlerted(const lt::state_update_alert*); + void torrentFinishedAlerted(const lt::torrent_finished_alert*); +}; + +#endif // ALERTMANAGER_H diff --git a/src/torrent/alerts.h b/src/torrent/alerts.h new file mode 100644 index 0000000..b7e4631 --- /dev/null +++ b/src/torrent/alerts.h @@ -0,0 +1,102 @@ +#ifndef ALERTS_H +#define ALERTS_H + +/** + * The alert type numbers weren't defined with constants/macros... + * Will have to do that here to make the app readable at all + */ + +enum { + torrent_removed_alert = 4, + read_piece_alert = 5, + file_completed_alert = 6, + file_renamed_alert = 7, + file_rename_failed_alert = 8, + performance_alert = 9, + state_changed_alert = 10, + tracker_error_alert = 11, + tracker_warning_alert = 12, + scrape_reply_alert = 13, + scrape_failed_alert = 14, + tracker_reply_alert = 15, + dht_reply_alert = 16, + tracker_announce_alert = 17, + hash_failed_alert = 18, + peer_ban_alert = 19, + peer_unsnubbed_alert = 20, + peer_snubbed_alert = 21, + peer_error_alert = 22, + peer_connect_alert = 23, + peer_disconnected_alert = 24, + invalid_request_alert = 25, + torrent_finished_alert = 26, + piece_finished_alert = 27, + request_dropped_alert = 28, + block_timeout_alert = 29, + block_finished_alert = 30, + block_downloading_alert = 31, + unwanted_block_alert = 32, + storage_moved_alert = 33, + storage_moved_failed_alert = 34, + torrent_deleted_alert = 35, + torrent_delete_failed_alert = 36, + save_resume_data_alert = 37, + save_resume_data_failed_alert = 38, + torrent_paused_alert = 39, + torrent_resumed_alert = 40, + torrent_checked_alert = 41, + url_seed_alert = 42, + file_error_alert = 43, + metadata_failed_alert = 44, + metadata_received_alert = 45, + udp_error_alert = 46, + external_ip_alert = 47, + listen_failed_alert = 48, + listen_succeeded_alert = 49, + portmap_error_alert = 50, + portmap_alert = 51, + portmap_log_alert = 52, + fastresume_rejected_alert = 53, + peer_blocked_alert = 54, + dht_announce_alert = 55, + dht_get_peers_alert = 56, + stats_alert = 57, + cache_flushed_alert = 58, + anonymous_mode_alert = 59, + lsd_peer_alert = 60, + trackerid_alert = 61, + dht_bootstrap_alert = 62, + torrent_error_alert = 64, + torrent_need_cert_alert = 65, + incoming_connection_alert = 66, + add_torrent_alert = 67, + state_update_alert = 68, + mmap_cache_alert = 69, + session_stats_alert = 70, + dht_error_alert = 73, + dht_immutable_item_alert = 74, + dht_mutable_item_alert = 75, + dht_put_alert = 76, + i2p_alert = 77, + dht_outgoing_get_peers_alert = 78, + log_alert = 79, + torrent_log_alert = 80, + peer_log_alert = 81, + lsd_error_alert = 82, + dht_stats_alert = 83, + incoming_request_alert = 84, + dht_log_alert = 85, + dht_pkt_alert = 86, + dht_get_peers_reply_alert = 87, + dht_direct_response_alert = 88, + picker_log_alert = 89, + session_error_alert = 90, + dht_live_nodes_alert = 91, + session_stats_header_alert = 92, + dht_sample_infohashes_alert = 93, + block_uploaded_alert = 94, + alerts_dropped_alert = 95, + socks5_alert = 96 +}; + +#endif // ALERTS_H diff --git a/src/torrent/torrentclient.cpp b/src/torrent/torrentclient.cpp new file mode 100644 index 0000000..b265cb9 --- /dev/null +++ b/src/torrent/torrentclient.cpp @@ -0,0 +1,221 @@ +#include "torrentclient.h" + +#include +#include + +#define CONNECT_ALERT(manager, alert, signal) \ + connect(manager, SIGNAL(signal ## ed(const lt::alert*)), this, SLOT(signal(const lt::alert*))) + +qint64 totalTime = 0; + +TorrentClient::TorrentClient(QDir dataDir) + : QObject(), m_dataDir(dataDir), m_sessionDataFile(dataDir.absoluteFilePath(".session").toStdString()) +{ + dataDir.mkpath("."); + + m_alertManager = new AlertManager(); + m_numOutstandingResumeData = 0; + + // Handle torrent events + CONNECT_ALERT(m_alertManager, add_torrent_alert, addTorrentAlert); + CONNECT_ALERT(m_alertManager, file_completed_alert, fileCompletedAlert); + CONNECT_ALERT(m_alertManager, metadata_received_alert, metadataReceivedAlert); + CONNECT_ALERT(m_alertManager, save_resume_data_alert, saveResumeDataAlert); + CONNECT_ALERT(m_alertManager, save_resume_data_failed_alert, saveResumeDataFailedAlert); + CONNECT_ALERT(m_alertManager, torrent_finished_alert, torrentFinishedAlert); +} + +// Boot ------------------------------------------------------------------------------------------- + +/** + * Boot the torrent client + */ +int TorrentClient::boot() +{ + qDebug() << "Booting session..."; + createSession(); + loadTorrentsFromDisk(); + + totalTime = QDateTime::currentMSecsSinceEpoch(); + lt::sha1_hash hash; + lt::error_code error; + + return 0; +} + +/** + * Create the torrent client session + */ +void TorrentClient::createSession() +{ + // First, load the saved session parameters (if any) + std::vector loadedParams = load_file(m_sessionDataFile); + lt::session_params sessionParams = loadedParams.empty() ? lt::session_params() : lt::read_session_params(loadedParams); + + // Next, configure the session parameters' settings + sessionParams.settings.set_int(lt::settings_pack::alert_mask, lt::alert_category::status + | lt::alert_category::stats + | lt::alert_category::error + | lt::alert_category::storage + | lt::alert_category::file_progress); + + // Create the session using these parameters and settings + m_session = new lt::session(sessionParams); + + // Finally, listen for when new alerts have been added + m_alertManager->addSession(m_session); +} + +/** + * Load torrents from the disk + */ +void TorrentClient::loadTorrentsFromDisk() +{ + QDirIterator it(m_dataDir.absolutePath(), {"*.resume"}, QDir::Files); + while (it.hasNext()) { + auto buf = load_file(it.next().toStdString()); + if (!buf.empty()) { + lt::add_torrent_params magnet = lt::read_resume_data(buf); + m_session->async_add_torrent(std::move(magnet)); + } + } + qDebug() << "Total torrents:" << m_session->get_torrents().size(); +} + +void TorrentClient::setFilePriorities(const lt::torrent_handle &torrent) +{ + auto files = torrent.torrent_file()->files(); + for (int i = 0; i < files.num_files(); i++) { + if (!should_keep_file(files.file_path(i))) { + torrent.file_priority(i, lt::dont_download); + qDebug() << "Ignoring: " << QString::fromStdString(files.file_path(i)); + } else { + qDebug() << "Downloading: " << QString::fromStdString(files.file_path(i)); + } + } +} + +void TorrentClient::saveTorrent(const lt::torrent_handle &torrent, lt::resume_data_flags_t flags) +{ + torrent.save_resume_data(flags); + incNumOutstandingResumeData(1); + qDebug() << "Saving a torrent file..."; +} + +void TorrentClient::saveTorrents() +{ + for (auto torrent : m_session->get_torrents()) { + saveTorrent(torrent, lt::torrent_handle::save_info_dict); + } +} + +void TorrentClient::saveSession() +{ + std::ofstream of(m_sessionDataFile, std::ios_base::binary); + of.unsetf(std::ios_base::skipws); + auto const buffer = write_session_params_buf(m_session->session_state(), lt::save_state_flags_t::all()); + of.write(buffer.data(), int(buffer.size())); + qDebug() << "Saved"; +} + +void TorrentClient::shutdown() +{ + qDebug() << "Shutting down..."; + QEventLoop loop; + connect(this, SIGNAL(allTorrentsSaved()), &loop, SLOT(quit())); + m_session->pause(); + saveSession(); + saveTorrents(); + loop.exec(); + delete m_session; + qDebug() << "Shutdown complete!"; +} + +// Event Handling --------------------------------------------------------------------------------- + +void TorrentClient::addTorrentAlert(const lt::add_torrent_alert *alert) +{ + if (alert->error) { + qDebug() << "Failed to add torrent. " << QString::fromStdString(alert->error.message()); + return; + } + saveTorrent(alert->handle, lt::torrent_handle::save_info_dict | lt::torrent_handle::only_if_modified); +} + +void TorrentClient::fileCompletedAlert(const lt::file_completed_alert *alert) +{ + qDebug() << "File completed: " << QString::fromStdString(alert->message()); + saveTorrent(alert->handle, lt::torrent_handle::save_info_dict | lt::torrent_handle::only_if_modified); +} + +void TorrentClient::metadataReceivedAlert(const lt::metadata_received_alert *alert) +{ + setFilePriorities(alert->handle); + saveTorrent(alert->handle, lt::torrent_handle::save_info_dict); +} + +void TorrentClient::saveResumeDataAlert(const lt::save_resume_data_alert* alert) +{ + incNumOutstandingResumeData(-1); + auto const buf = write_resume_data_buf(alert->params); + save_file(m_dataDir.absoluteFilePath(resume_file(alert->params.info_hashes)).toStdString(), buf); + qDebug() << "Torrent file written to disk"; + if (m_numOutstandingResumeData == 0) { + qDebug() << "All torrents have been saved successfully!"; + emit allTorrentsSaved(); + } +} + +void TorrentClient::saveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *alert) +{ + qDebug() << "Save resume data failed"; + incNumOutstandingResumeData(-1); + if (alert->error != lt::errors::resume_data_not_modified) { + qDebug() << QString::fromStdString(alert->error.message()); + } +} + +void TorrentClient::torrentFinishedAlert(const lt::torrent_finished_alert *alert) +{ + qDebug() << "The torrent finished!" << (QDateTime::currentMSecsSinceEpoch() - totalTime); +} + +void TorrentClient::incNumOutstandingResumeData(int value) +{ + m_numOutstandingResumeData += value; + qDebug() << "New number of outstanding torrents is: " << m_numOutstandingResumeData; +} + +// ------------------------------------------------------------------------------------------------ + +bool TorrentClient::addTorrent(QString magnetLink, lt::sha1_hash &resultHash, lt::error_code &error) +{ + lt::torrent_handle handle; + lt::add_torrent_params magnet = lt::parse_magnet_uri(magnetLink.toStdString(), error); + magnet.save_path = "/media/david/Storage/TorrentData/TorrentDownloads"; + + if (error) { + std::cout << "Failed to parse magnet link (" << error.value() << "): " << error.message(); + return false; + } + + handle = m_session->find_torrent(magnet.info_hashes.get_best()); + if (handle.is_valid()) { + qDebug() << "This torrent has already been added!"; + return false; + } + + handle = m_session->add_torrent(magnet); + qDebug() << "Torrent added; " << QString::fromStdString(to_hex(handle.info_hashes().get_best())); + qDebug() << "Total torrents:" << m_session->get_torrents().size(); + resultHash = handle.info_hashes().get_best(); + + return true; +} + +// ------------------------------------------------------------------------------------------------ + +AlertManager* TorrentClient::alertManager() +{ + return m_alertManager; +} diff --git a/src/torrent/torrentclient.h b/src/torrent/torrentclient.h new file mode 100644 index 0000000..c1cc134 --- /dev/null +++ b/src/torrent/torrentclient.h @@ -0,0 +1,77 @@ +#ifndef TORRENTCLIENT_H +#define TORRENTCLIENT_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "alerts.h" +#include "alertmanager.h" +#include "./utils.h" + +class TorrentClient : public QObject +{ + Q_OBJECT +public: + explicit TorrentClient(QDir dataDir); + + AlertManager *alertManager(); + +public slots: + int boot(); + void shutdown(); + + bool addTorrent(QString magnetLink, lt::sha1_hash &resultHash, lt::error_code &error); + +protected: + void loadTorrentsFromDisk(); + void setFilePriorities(const lt::torrent_handle &torrent); + void saveTorrent(const lt::torrent_handle &torrent, lt::resume_data_flags_t flags); + +protected slots: + void createSession(); + void saveSession(); + void saveTorrents(); + + void incNumOutstandingResumeData(int value); + + // Alert handlers + void addTorrentAlert(const lt::add_torrent_alert *alert); + void fileCompletedAlert(const lt::file_completed_alert *alert); + void metadataReceivedAlert(const lt::metadata_received_alert *alert); + void saveResumeDataAlert(const lt::save_resume_data_alert *alert); + void saveResumeDataFailedAlert(const lt::save_resume_data_failed_alert *alert); + void torrentFinishedAlert(const lt::torrent_finished_alert *alert); + +private: + const QDir m_dataDir; + const std::string m_sessionDataFile; + + AlertManager *m_alertManager; + int m_numOutstandingResumeData; + lt::session *m_session = nullptr; + +signals: + void allTorrentsSaved(); + void torrentAdded(lt::sha1_hash); +}; + +#endif // TORRENTCLIENT_H diff --git a/src/torrent/utils.cpp b/src/torrent/utils.cpp new file mode 100644 index 0000000..98c88e9 --- /dev/null +++ b/src/torrent/utils.cpp @@ -0,0 +1,38 @@ +#include "utils.h" + +std::vector load_file(const std::string filename) +{ + std::ifstream ifs(filename, std::ios_base::binary); + ifs.unsetf(std::ios_base::skipws); + return { std::istream_iterator(ifs), std::istream_iterator() }; +} + +bool save_file(const std::string filename, const std::vector &bytes) +{ + std::fstream f(filename, std::ios_base::trunc | std::ios_base::out | std::ios_base::binary); + f.write(bytes.data(), int(bytes.size())); + return !f.fail(); +} + +QString resume_file(const lt::info_hash_t hashes) +{ + return QString::fromStdString(to_hex(hashes.get_best())) + ".resume"; +} + +std::string to_hex(const lt::sha1_hash &s) +{ + std::stringstream ret; + ret << s; + return ret.str(); +} + +std::string file_ext(std::string path) +{ + return path.substr(path.find_last_of(".")); +} + +bool should_keep_file(std::string path) +{ + static const std::unordered_set exts({".mp4", ".avi", ".mkv", ".srt"}); + return exts.count(file_ext(path)) > 0; +} diff --git a/src/torrent/utils.h b/src/torrent/utils.h new file mode 100644 index 0000000..79f18d6 --- /dev/null +++ b/src/torrent/utils.h @@ -0,0 +1,32 @@ +#ifndef UTILS_H +#define UTILS_H + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +std::vector load_file(const std::string filename); + +bool save_file(const std::string filename, const std::vector &bytes); + +QString resume_file(const lt::info_hash_t hashes); + +std::string to_hex(const lt::sha1_hash &s); + +std::string file_ext(std::string path); + +bool should_keep_file(std::string path); + +#endif // UTILS_H diff --git a/torrent-client-v2.pro b/torrent-client-v2.pro index 1424cf3..e7755c5 100644 --- a/torrent-client-v2.pro +++ b/torrent-client-v2.pro @@ -1,21 +1,37 @@ QT -= gui QT += network -CONFIG += c++11 console +CONFIG += c++17 console CONFIG -= app_bundle # You can make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 +HEADERS += \ + src/application.h \ + src/interface/ipcserver.h \ + src/interface/ipcsocketinterface.h \ + src/interface/torrentclientinterface.h \ + src/torrent/alertmanager.h \ + src/torrent/alerts.h \ + src/torrent/torrentclient.h \ + src/torrent/utils.h + SOURCES += \ - src/application.cpp \ - src/main.cpp + src/interface/ipcserver.cpp \ + src/interface/ipcsocketinterface.cpp \ + src/interface/torrentclientinterface.cpp \ + src/main.cpp \ + src/application.cpp \ + src/torrent/alertmanager.cpp \ + src/torrent/torrentclient.cpp \ + src/torrent/utils.cpp + +INCLUDEPATH += /usr/local/include +LIBS += -L/usr/local/lib -ltorrent-rasterbar -lpthread # Default rules for deployment. qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target - -HEADERS += \ - src/application.h