#include "LotWUsers.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "qt_helpers.hpp" #include "Logger.hpp" #include "FileDownload.hpp" #include "pimpl_impl.hpp" #include "moc_LotWUsers.cpp" namespace { // Dictionary mapping call sign to date of last upload to LotW using dictionary = QHash; } class LotWUsers::impl final : public QObject { Q_OBJECT public: impl (LotWUsers * self, QNetworkAccessManager * network_manager) : self_ {self} , network_manager_ {network_manager} , url_valid_ {false} , redirect_count_ {0} , age_constraint_ {365} , connected_ {false} { } void load (QString const& url, bool fetch, bool forced_fetch) { abort (); // abort any active download auto csv_file_name = csv_file_.fileName (); auto exists = QFileInfo::exists (csv_file_name); if (fetch && (!exists || forced_fetch)) { current_url_.setUrl(url); if (current_url_.isValid() && !QSslSocket::supportsSsl()) { current_url_.setScheme("http"); } redirect_count_ = 0; Q_EMIT self_->progress (QString("Starting download from %1").arg(url)); lotw_downloader_.configure(network_manager_, url, csv_file_name, "WSJT-X LotW User Downloader"); if (!connected_) { connect(&lotw_downloader_, &FileDownload::complete, [this, csv_file_name] { LOG_INFO(QString{"LotWUsers: Loading LotW file %1"}.arg(csv_file_name)); future_load_ = std::async(std::launch::async, &LotWUsers::impl::load_dictionary, this, csv_file_name); }); connect(&lotw_downloader_, &FileDownload::error, [this] (QString const& msg) { LOG_INFO(QString{"LotWUsers: Error downloading LotW file: %1"}.arg(msg)); Q_EMIT self_->LotW_users_error (msg); }); connect( &lotw_downloader_, &FileDownload::progress, [this] (QString const& msg) { Q_EMIT self_->progress (msg); }); connected_ = true; } lotw_downloader_.start_download(); } else { if (exists) { // load the database asynchronously future_load_ = std::async (std::launch::async, &LotWUsers::impl::load_dictionary, this, csv_file_name); } } } void abort () { lotw_downloader_.abort(); } // Load the database from the given file name // // Expects the file to be in CSV format with no header with one // record per line. Record fields are call sign followed by upload // date in yyyy-MM-dd format followed by upload time (ignored) dictionary load_dictionary (QString const& lotw_csv_file) { dictionary result; QFile f {lotw_csv_file}; if (f.open (QFile::ReadOnly | QFile::Text)) { QTextStream s {&f}; for (auto l = s.readLine (); !l.isNull (); l = s.readLine ()) { auto pos = l.indexOf (','); result[l.left (pos)] = QDate::fromString (l.mid (pos + 1, l.indexOf (',', pos + 1) - pos - 1), "yyyy-MM-dd"); } } else { throw std::runtime_error {QObject::tr ("Failed to open LotW users CSV file: '%1'").arg (f.fileName ()).toStdString ()}; } LOG_INFO(QString{"LotWUsers: Loaded %1 records from %2"}.arg(result.size()).arg(lotw_csv_file)); Q_EMIT self_->progress (QString{"Loaded %1 records from LotW."}.arg(result.size())); Q_EMIT self_->load_finished(); return result; } LotWUsers * self_; QNetworkAccessManager * network_manager_; QSaveFile csv_file_; bool url_valid_; QUrl current_url_; // may be a redirect int redirect_count_; QPointer reply_; std::future future_load_; dictionary last_uploaded_; qint64 age_constraint_; // days FileDownload lotw_downloader_; bool connected_; }; #include "LotWUsers.moc" LotWUsers::LotWUsers (QNetworkAccessManager * network_manager, QObject * parent) : QObject {parent} , m_ {this, network_manager} { } LotWUsers::~LotWUsers () { } void LotWUsers::set_local_file_path (QString const& path) { m_->csv_file_.setFileName (path); } void LotWUsers::load (QString const& url, bool fetch, bool force_download) { m_->load (url, fetch, force_download); } void LotWUsers::set_age_constraint (qint64 uploaded_since_days) { m_->age_constraint_ = uploaded_since_days; } bool LotWUsers::user (QString const& call) const { // check if a pending asynchronous load is ready if (m_->future_load_.valid () && std::future_status::ready == m_->future_load_.wait_for (std::chrono::seconds {0})) { try { // wait for the load to finish if necessary const_cast (m_->last_uploaded_) = const_cast&> (m_->future_load_).get (); } catch (std::exception const& e) { Q_EMIT LotW_users_error (e.what ()); } Q_EMIT load_finished (); } if (m_->last_uploaded_.size ()) { auto p = m_->last_uploaded_.constFind (call); if (p != m_->last_uploaded_.end ()) { return p.value ().daysTo (QDate::currentDate ()) <= m_->age_constraint_; } } return false; }