Add Last.fm import

Fixes #247
This commit is contained in:
Jonas Kvinge 2020-08-30 18:09:13 +02:00
parent 82d10dd7cb
commit 5aaa5231b8
17 changed files with 1204 additions and 4 deletions

View File

@ -170,6 +170,7 @@ set(SOURCES
dialogs/addstreamdialog.cpp
dialogs/userpassdialog.cpp
dialogs/deleteconfirmationdialog.cpp
dialogs/lastfmimportdialog.cpp
widgets/autoexpandingtreeview.cpp
widgets/busyindicator.cpp
@ -223,6 +224,7 @@ set(SOURCES
scrobbler/lastfmscrobbler.cpp
scrobbler/librefmscrobbler.cpp
scrobbler/listenbrainzscrobbler.cpp
scrobbler/lastfmimport.cpp
organize/organize.cpp
organize/organizeformat.cpp
@ -369,6 +371,7 @@ set(HEADERS
dialogs/addstreamdialog.h
dialogs/userpassdialog.h
dialogs/deleteconfirmationdialog.h
dialogs/lastfmimportdialog.h
widgets/autoexpandingtreeview.h
widgets/busyindicator.h
@ -420,6 +423,7 @@ set(HEADERS
scrobbler/lastfmscrobbler.h
scrobbler/librefmscrobbler.h
scrobbler/listenbrainzscrobbler.h
scrobbler/lastfmimport.h
organize/organize.h
organize/organizedialog.h
@ -472,6 +476,7 @@ set(UI
dialogs/trackselectiondialog.ui
dialogs/addstreamdialog.ui
dialogs/userpassdialog.ui
dialogs/lastfmimportdialog.ui
widgets/trackslider.ui
widgets/fileview.ui

View File

@ -40,6 +40,7 @@
#include "collectionbackend.h"
#include "collectionmodel.h"
#include "playlist/playlistmanager.h"
#include "scrobbler/lastfmimport.h"
const char *SCollection::kSongsTable = "songs";
const char *SCollection::kDirsTable = "directories";
@ -110,6 +111,9 @@ void SCollection::Init() {
connect(app_->playlist_manager(), SIGNAL(CurrentSongChanged(Song)), SLOT(CurrentSongChanged(Song)));
connect(app_->player(), SIGNAL(Stopped()), SLOT(Stopped()));
connect(app_->lastfm_import(), SIGNAL(UpdateLastPlayed(QString, QString, QString, int)), backend_, SLOT(UpdateLastPlayed(QString, QString, QString, int)));
connect(app_->lastfm_import(), SIGNAL(UpdatePlayCount(QString, QString, int)), backend_, SLOT(UpdatePlayCount(QString, QString, int)));
// This will start the watcher checking for updates
backend_->LoadDirectoriesAsync();

View File

@ -42,6 +42,7 @@
#include <QSqlDatabase>
#include <QSqlQuery>
#include "core/logging.h"
#include "core/database.h"
#include "core/scopedtransaction.h"
@ -1297,3 +1298,79 @@ void CollectionBackend::DeleteAll() {
emit DatabaseReset();
}
SongList CollectionBackend::GetSongsBy(const QString &artist, const QString &album, const QString &title) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
SongList songs;
QSqlQuery q(db);
if (album.isEmpty()) {
q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE artist = :artist COLLATE NOCASE AND title = :title COLLATE NOCASE").arg(songs_table_));
}
else {
q.prepare(QString("SELECT ROWID, " + Song::kColumnSpec + " FROM %1 WHERE artist = :artist COLLATE NOCASE AND album = :album COLLATE NOCASE AND title = :title COLLATE NOCASE").arg(songs_table_));
}
q.bindValue(":artist", artist);
if (!album.isEmpty()) q.bindValue(":album", album);
q.bindValue(":title", title);
q.exec();
if (db_->CheckErrors(q)) return SongList();
while (q.next()) {
Song song(source_);
song.InitFromQuery(q, true);
songs << song;
}
return songs;
}
void CollectionBackend::UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const int lastplayed) {
SongList songs = GetSongsBy(artist, album, title);
if (songs.isEmpty()) {
qLog(Debug) << "Could not find a matching song in the database for" << artist << album << title;
return;
}
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
for (const Song &song : songs) {
QSqlQuery q(db);
q.prepare(QString("UPDATE %1 SET lastplayed = :lastplayed WHERE ROWID = :id").arg(songs_table_));
q.bindValue(":lastplayed", lastplayed);
q.bindValue(":id", song.id());
q.exec();
if (db_->CheckErrors(q)) continue;
}
emit SongsStatisticsChanged(SongList() << songs);
}
void CollectionBackend::UpdatePlayCount(const QString &artist, const QString &title, const int playcount) {
SongList songs = GetSongsBy(artist, QString(), title);
if (songs.isEmpty()) {
qLog(Debug) << "Could not find a matching song in the database for" << artist << title;
return;
}
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
for (const Song &song : songs) {
QSqlQuery q(db);
q.prepare(QString("UPDATE %1 SET playcount = :playcount WHERE ROWID = :id").arg(songs_table_));
q.bindValue(":playcount", playcount);
q.bindValue(":id", song.id());
q.exec();
if (db_->CheckErrors(q)) continue;
}
emit SongsStatisticsChanged(SongList() << songs);
}

View File

@ -205,6 +205,10 @@ class CollectionBackend : public CollectionBackendInterface {
void ResetStatistics(const int id);
void SongPathChanged(const Song &song, const QFileInfo &new_file);
SongList GetSongsBy(const QString &artist, const QString &album, const QString &title);
void UpdateLastPlayed(const QString &artist, const QString &album, const QString &title, const int lastplayed);
void UpdatePlayCount(const QString &artist, const QString &title, const int playcount);
signals:
void DirectoryDiscovered(const Directory &dir, const SubdirectoryList &subdirs);
void DirectoryDeleted(const Directory &dir);

View File

@ -69,6 +69,7 @@
#include "lyrics/chartlyricsprovider.h"
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/lastfmimport.h"
#include "internet/internetservices.h"
@ -160,6 +161,7 @@ class ApplicationImpl {
return internet_services;
}),
scrobbler_([=]() { return new AudioScrobbler(app, app); }),
lastfm_import_([=]() { return new LastFMImport(app); }),
#ifdef HAVE_MOODBAR
moodbar_loader_([=]() { return new MoodbarLoader(app, app); }),
@ -187,6 +189,7 @@ class ApplicationImpl {
Lazy<LyricsProviders> lyrics_providers_;
Lazy<InternetServices> internet_services_;
Lazy<AudioScrobbler> scrobbler_;
Lazy<LastFMImport> lastfm_import_;
#ifdef HAVE_MOODBAR
Lazy<MoodbarLoader> moodbar_loader_;
Lazy<MoodbarController> moodbar_controller_;
@ -315,6 +318,7 @@ PlaylistBackend *Application::playlist_backend() const { return p_->playlist_bac
PlaylistManager *Application::playlist_manager() const { return p_->playlist_manager_.get(); }
InternetServices *Application::internet_services() const { return p_->internet_services_.get(); }
AudioScrobbler *Application::scrobbler() const { return p_->scrobbler_.get(); }
LastFMImport *Application::lastfm_import() const { return p_->lastfm_import_.get(); }
#ifdef HAVE_MOODBAR
MoodbarController *Application::moodbar_controller() const { return p_->moodbar_controller_.get(); }
MoodbarLoader *Application::moodbar_loader() const { return p_->moodbar_loader_.get(); }

View File

@ -56,6 +56,7 @@ class CurrentAlbumCoverLoader;
class CoverProviders;
class LyricsProviders;
class AudioScrobbler;
class LastFMImport;
class InternetServices;
#ifdef HAVE_MOODBAR
class MoodbarController;
@ -93,6 +94,7 @@ class Application : public QObject {
LyricsProviders *lyrics_providers() const;
AudioScrobbler *scrobbler() const;
LastFMImport *lastfm_import() const;
InternetServices *internet_services() const;

View File

@ -103,6 +103,7 @@
#include "dialogs/edittagdialog.h"
#include "dialogs/addstreamdialog.h"
#include "dialogs/deleteconfirmationdialog.h"
#include "dialogs/lastfmimportdialog.h"
#include "organize/organizedialog.h"
#include "widgets/fancytabwidget.h"
#include "widgets/playingwidget.h"
@ -169,6 +170,7 @@
#include "internet/internetsearchview.h"
#include "scrobbler/audioscrobbler.h"
#include "scrobbler/lastfmimport.h"
#if defined(HAVE_GSTREAMER) && defined(HAVE_CHROMAPRINT)
# include "musicbrainz/tagfetcher.h"
@ -257,6 +259,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
#ifdef HAVE_TIDAL
tidal_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Tidal), TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, this)),
#endif
lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)),
collection_show_all_(nullptr),
collection_show_duplicates_(nullptr),
collection_show_untagged_(nullptr),
@ -417,6 +420,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
ui_->action_update_collection->setIcon(IconLoader::Load("view-refresh"));
ui_->action_full_collection_scan->setIcon(IconLoader::Load("view-refresh"));
ui_->action_settings->setIcon(IconLoader::Load("configure"));
ui_->action_import_data_from_last_fm->setIcon(IconLoader::Load("scrobble"));
// Scrobble
@ -457,6 +461,7 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
connect(ui_->action_auto_complete_tags, SIGNAL(triggered()), SLOT(AutoCompleteTags()));
#endif
connect(ui_->action_settings, SIGNAL(triggered()), SLOT(OpenSettingsDialog()));
connect(ui_->action_import_data_from_last_fm, SIGNAL(triggered()), lastfm_import_dialog_, SLOT(show()));
connect(ui_->action_toggle_show_sidebar, SIGNAL(toggled(bool)), SLOT(ToggleSidebar(bool)));
connect(ui_->action_about_strawberry, SIGNAL(triggered()), SLOT(ShowAboutDialog()));
connect(ui_->action_about_qt, SIGNAL(triggered()), qApp, SLOT(aboutQt()));
@ -822,6 +827,12 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSDBase *osd
LoveButtonVisibilityChanged(app_->scrobbler()->LoveButton());
ScrobblingEnabledChanged(app_->scrobbler()->IsEnabled());
// Last.fm ImportData
connect(app_->lastfm_import(), SIGNAL(Finished()), lastfm_import_dialog_, SLOT(Finished()));
connect(app_->lastfm_import(), SIGNAL(FinishedWithError(QString)), lastfm_import_dialog_, SLOT(FinishedWithError(QString)));
connect(app_->lastfm_import(), SIGNAL(UpdateTotal(int, int)), lastfm_import_dialog_, SLOT(UpdateTotal(int, int)));
connect(app_->lastfm_import(), SIGNAL(UpdateProgress(int, int)), lastfm_import_dialog_, SLOT(UpdateProgress(int, int)));
// Load settings
qLog(Debug) << "Loading settings";
settings_.beginGroup(kSettingsGroup);

View File

@ -96,6 +96,7 @@ class InternetTabsView;
class Windows7ThumbBar;
#endif
class AddStreamDialog;
class LastFMImportDialog;
class MainWindow : public QMainWindow, public PlatformInterface {
Q_OBJECT
@ -324,6 +325,8 @@ class MainWindow : public QMainWindow, public PlatformInterface {
InternetSongsView *subsonic_view_;
InternetTabsView *tidal_view_;
LastFMImportDialog *lastfm_import_dialog_;
QAction *collection_show_all_;
QAction *collection_show_duplicates_;
QAction *collection_show_untagged_;

View File

@ -511,6 +511,7 @@
<addaction name="action_abort_collection_scan"/>
<addaction name="separator"/>
<addaction name="action_settings"/>
<addaction name="action_import_data_from_last_fm"/>
<addaction name="action_console"/>
<addaction name="separator"/>
<addaction name="action_toggle_show_sidebar"/>
@ -844,6 +845,11 @@
<string>Show sidebar</string>
</property>
</action>
<action name="action_import_data_from_last_fm">
<property name="text">
<string>Import data from last.fm...</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>

View File

@ -0,0 +1,178 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <QDialog>
#include <QStackedWidget>
#include <QPushButton>
#include <QLabel>
#include <QProgressBar>
#include <QShowEvent>
#include <QCloseEvent>
#include "lastfmimportdialog.h"
#include "ui_lastfmimportdialog.h"
#include "core/iconloader.h"
#include "scrobbler/lastfmimport.h"
LastFMImportDialog::LastFMImportDialog(LastFMImport *lastfm_import, QWidget *parent) : QDialog(parent),
ui_(new Ui_LastFMImportDialog),
lastfm_import_(lastfm_import),
finished_(false),
playcount_total_(0),
lastplayed_total_(0)
{
ui_->setupUi(this);
setWindowIcon(IconLoader::Load("scrobble"));
ui_->stackedWidget->setCurrentWidget(ui_->page_start);
Reset();
connect(ui_->button_close, SIGNAL(clicked()), SLOT(hide()));
connect(ui_->button_go, SIGNAL(clicked()), SLOT(Start()));
connect(ui_->button_cancel, SIGNAL(clicked()), SLOT(Cancel()));
connect(ui_->checkbox_last_played, SIGNAL(stateChanged(int)), SLOT(UpdateGoButtonState()));
connect(ui_->checkbox_playcounts, SIGNAL(stateChanged(int)), SLOT(UpdateGoButtonState()));
}
LastFMImportDialog::~LastFMImportDialog() { delete ui_; }
void LastFMImportDialog::showEvent(QShowEvent*) {
if (ui_->stackedWidget->currentWidget() == ui_->page_start) {
Reset();
}
}
void LastFMImportDialog::closeEvent(QCloseEvent*) {
if (ui_->stackedWidget->currentWidget() == ui_->page_progress && finished_) {
finished_ = false;
Reset();
ui_->stackedWidget->setCurrentWidget(ui_->page_start);
}
}
void LastFMImportDialog::Start() {
if (ui_->stackedWidget->currentWidget() == ui_->page_start && (ui_->checkbox_last_played->isChecked() || ui_->checkbox_playcounts->isChecked())) {
ui_->stackedWidget->setCurrentWidget(ui_->page_progress);
ui_->button_go->hide();
ui_->button_cancel->show();
ui_->label_progress_top->setText(tr("Receiving initial data from last.fm..."));
lastfm_import_->ImportData(ui_->checkbox_last_played->isChecked(), ui_->checkbox_playcounts->isChecked());
}
}
void LastFMImportDialog::Cancel() {
if (ui_->stackedWidget->currentWidget() == ui_->page_progress) {
lastfm_import_->AbortAll();
ui_->stackedWidget->setCurrentWidget(ui_->page_start);
Reset();
}
}
void LastFMImportDialog::Reset() {
ui_->button_go->show();
ui_->button_cancel->hide();
playcount_total_ = 0;
lastplayed_total_ = 0;
ui_->progressbar->setValue(0);
ui_->label_progress_top->clear();
ui_->label_progress_bottom->clear();
UpdateGoButtonState();
}
void LastFMImportDialog::UpdateTotal(const int lastplayed_total, const int playcount_total) {
if (ui_->stackedWidget->currentWidget() != ui_->page_progress) return;
playcount_total_ = playcount_total;
lastplayed_total_ = lastplayed_total;
if (lastplayed_total > 0 && playcount_total > 0) {
ui_->label_progress_top->setText(tr("Receiving playcount for %1 songs and last played for %2 songs.").arg(playcount_total).arg(lastplayed_total));
}
else if (lastplayed_total > 0) {
ui_->label_progress_top->setText(tr("Receiving last played for %1 songs.").arg(lastplayed_total));
}
else if (playcount_total > 0) {
ui_->label_progress_top->setText(tr("Receiving playcounts for %1 songs.").arg(playcount_total));
}
else {
ui_->label_progress_top->clear();
}
ui_->label_progress_bottom->clear();
}
void LastFMImportDialog::UpdateProgress(const int lastplayed_received, const int playcount_received) {
if (ui_->stackedWidget->currentWidget() != ui_->page_progress) return;
ui_->progressbar->setValue(static_cast<int>(static_cast<float>(playcount_received + lastplayed_received) / static_cast<float>(playcount_total_ + lastplayed_total_) * 100.0));
if (lastplayed_received > 0 && playcount_received > 0) {
ui_->label_progress_bottom->setText(tr("Playcounts for %1 songs and last played for %2 songs received.").arg(playcount_received).arg(lastplayed_received));
}
else if (lastplayed_received > 0) {
ui_->label_progress_bottom->setText(tr("Last played for %1 songs received.").arg(lastplayed_received));
}
else if (playcount_received > 0) {
ui_->label_progress_bottom->setText(tr("Playcounts for %1 songs received.").arg(playcount_received));
}
else {
ui_->label_progress_bottom->clear();
}
}
void LastFMImportDialog::Finished() {
ui_->button_cancel->hide();
finished_ = true;
}
void LastFMImportDialog::FinishedWithError(const QString &error) {
Finished();
ui_->label_progress_bottom->setText(error);
}
void LastFMImportDialog::UpdateGoButtonState() {
ui_->button_go->setEnabled(ui_->checkbox_last_played->isChecked() || ui_->checkbox_playcounts->isChecked());
}

View File

@ -0,0 +1,68 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef LASTFMIMPORTDIALOG_H
#define LASTFMIMPORTDIALOG_H
#include "config.h"
#include <QObject>
#include <QDialog>
#include <QString>
#include "ui_lastfmimportdialog.h"
class QShowEvent;
class QCloseEvent;
class LastFMImport;
class LastFMImportDialog : public QDialog {
Q_OBJECT
public:
explicit LastFMImportDialog(LastFMImport *lastfm_import, QWidget *parent = nullptr);
~LastFMImportDialog() override;
protected:
void showEvent(QShowEvent*);
void closeEvent(QCloseEvent*);
private:
void Reset();
private slots:
void Start();
void Cancel();
void UpdateGoButtonState();
void UpdateTotal(const int lastplayed_total, const int playcount_total);
void UpdateProgress(const int lastplayed_received, const int playcount_received);
void Finished();
void FinishedWithError(const QString &error);
private:
Ui_LastFMImportDialog *ui_;
LastFMImport *lastfm_import_;
bool finished_;
int playcount_total_;
int lastplayed_total_;
};
#endif // LASTFMIMPORTDIALOG_H

View File

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LastFMImportDialog</class>
<widget class="QDialog" name="LastFMImportDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>520</width>
<height>249</height>
</rect>
</property>
<property name="windowTitle">
<string>Import data from last.fm</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QStackedWidget" name="stackedWidget">
<widget class="QWidget" name="page_start">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_start_top">
<property name="text">
<string>Choose data to import from last.fm</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkbox_last_played">
<property name="text">
<string>Last played</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkbox_playcounts">
<property name="text">
<string>Play counts</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_start_bottom">
<property name="text">
<string>Warning: Play counts and last played from last.fm will completely replace the same data for the matched songs. Play counts will replace the data based on artist and song title for the same albums! Please backup your database before you start.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_progress">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_progress_top">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressbar">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_progress_bottom">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_done"/>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="layout_buttons">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="button_go">
<property name="text">
<string>Go!</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_close">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_cancel">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,588 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <algorithm>
#include <QtGlobal>
#include <QApplication>
#include <QLocale>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QUrl>
#include <QUrlQuery>
#include <QDateTime>
#include <QTimer>
#include <QSettings>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include "core/network.h"
#include "core/timeconstants.h"
#include "core/logging.h"
#include "lastfmimport.h"
#include "scrobblingapi20.h"
#include "lastfmscrobbler.h"
const int LastFMImport::kRequestsDelay = 2000;
LastFMImport::LastFMImport(QObject *parent) :
QObject(parent),
network_(new NetworkAccessManager(this)),
timer_flush_requests_(new QTimer(this)),
lastplayed_(false),
playcount_(false),
playcount_total_(0),
lastplayed_total_(0),
playcount_received_(0),
lastplayed_received_(0) {
timer_flush_requests_->setInterval(kRequestsDelay);
timer_flush_requests_->setSingleShot(false);
connect(timer_flush_requests_, SIGNAL(timeout()), this, SLOT(FlushRequests()));
}
LastFMImport::~LastFMImport() {
AbortAll();
}
void LastFMImport::AbortAll() {
while (!replies_.isEmpty()) {
QNetworkReply *reply = replies_.takeFirst();
disconnect(reply, nullptr, this, nullptr);
reply->abort();
reply->deleteLater();
}
playcount_total_ = 0;
lastplayed_total_ = 0;
playcount_received_ = 0;
lastplayed_received_ = 0;
recent_tracks_requests_.clear();
top_tracks_requests_.clear();
timer_flush_requests_->stop();
}
void LastFMImport::ReloadSettings() {
QSettings s;
s.beginGroup(LastFMScrobbler::kSettingsGroup);
username_ = s.value("username").toString();
s.endGroup();
}
QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
ParamList params = ParamList()
<< Param("api_key", ScrobblingAPI20::kApiKey)
<< Param("user", username_)
<< Param("lang", QLocale().name().left(2).toLower())
<< Param("format", "json")
<< request_params;
std::sort(params.begin(), params.end());
QUrlQuery url_query;
for (const Param &param : params) {
url_query.addQueryItem(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second));
}
QUrl url(LastFMScrobbler::kApiUrl);
url.setQuery(url_query);
QNetworkRequest req(url);
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
#else
req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
#endif
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply *reply = network_->get(req);
replies_ << reply;
//qLog(Debug) << "Sending request" << url_query.toString(QUrl::FullyDecoded);
return reply;
}
QByteArray LastFMImport::GetReplyData(QNetworkReply *reply) {
QByteArray data;
if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) {
data = reply->readAll();
}
else {
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
// This is a network error, there is nothing more to do.
Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
}
else {
QString error;
// See if there is Json data containing "error" and "message" - then use that instead.
data = reply->readAll();
QJsonParseError json_error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error);
int error_code = -1;
if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) {
QJsonObject json_obj = json_doc.object();
if (json_obj.contains("error") && json_obj.contains("message")) {
error_code = json_obj["error"].toInt();
QString error_message = json_obj["message"].toString();
error = QString("%1 (%2)").arg(error_message).arg(error_code);
}
}
if (error.isEmpty()) {
if (reply->error() != QNetworkReply::NoError) {
error = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error());
}
else {
error = QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
}
}
Error(error);
}
return QByteArray();
}
return data;
}
QJsonObject LastFMImport::ExtractJsonObj(const QByteArray &data) {
QJsonParseError error;
QJsonDocument json_doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
Error("Reply from server missing Json data.", data);
return QJsonObject();
}
if (json_doc.isNull() || json_doc.isEmpty()) {
Error("Received empty Json document.", json_doc);
return QJsonObject();
}
if (!json_doc.isObject()) {
Error("Json document is not an object.", json_doc);
return QJsonObject();
}
QJsonObject json_obj = json_doc.object();
if (json_obj.isEmpty()) {
Error("Received empty Json object.", json_doc);
return QJsonObject();
}
return json_obj;
}
void LastFMImport::ImportData(const bool lastplayed, const bool playcount) {
if (!lastplayed && !playcount) return;
ReloadSettings();
if (username_.isEmpty()) {
Error(tr("Missing username, please login to last.fm first!"));
return;
}
AbortAll();
lastplayed_ = lastplayed;
playcount_ = playcount;
if (lastplayed) AddGetRecentTracksRequest(0);
if (playcount) AddGetTopTracksRequest(0);
}
void LastFMImport::FlushRequests() {
if (!recent_tracks_requests_.isEmpty()) {
SendGetRecentTracksRequest(recent_tracks_requests_.dequeue());
return;
}
if (!top_tracks_requests_.isEmpty()) {
SendGetTopTracksRequest(top_tracks_requests_.dequeue());
return;
}
timer_flush_requests_->stop();
}
void LastFMImport::AddGetRecentTracksRequest(const int page) {
recent_tracks_requests_.enqueue(GetRecentTracksRequest(page));
if (!timer_flush_requests_->isActive()) {
timer_flush_requests_->start();
}
}
void LastFMImport::SendGetRecentTracksRequest(GetRecentTracksRequest request) {
ParamList params = ParamList() << Param("method", "user.getRecentTracks");
if (request.page == 0) {
params << Param("page", "1");
params << Param("limit", "1");
}
else {
params << Param("page", QString::number(request.page));
params << Param("limit", "500");
}
QNetworkReply *reply = CreateRequest(params);
connect(reply, &QNetworkReply::finished, [=] { GetRecentTracksRequestFinished(reply, request.page); });
}
void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const int page) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
return;
}
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
return;
}
if (json_obj.contains("error") && json_obj.contains("message")) {
int error_code = json_obj["error"].toInt();
QString error_message = json_obj["message"].toString();
QString error_reason = QString("%1 (%2)").arg(error_message).arg(error_code);
Error(error_reason);
return;
}
if (!json_obj.contains("recenttracks")) {
Error("JSON reply from server is missing recenttracks.", json_obj);
return;
}
if (!json_obj["recenttracks"].isObject()) {
Error("Failed to pase JSON: recenttracks is not an object!", json_obj);
return;
}
json_obj = json_obj["recenttracks"].toObject();
if (!json_obj.contains("@attr")) {
Error("JSON reply from server is missing @attr.", json_obj);
return;
}
if (!json_obj.contains("track")) {
Error("JSON reply from server is missing track.", json_obj);
return;
}
if (!json_obj["@attr"].isObject()) {
Error("Failed to pase JSON: @attr is not an object.", json_obj);
return;
}
if (!json_obj["track"].isArray()) {
Error("Failed to pase JSON: track is not an object.", json_obj);
return;
}
QJsonObject obj_attr = json_obj["@attr"].toObject();
if (!obj_attr.contains("page")) {
Error("Failed to pase JSON: attr object is missing page.", json_obj);
return;
}
if (!obj_attr.contains("totalPages")) {
Error("Failed to pase JSON: attr object is missing totalPages.", json_obj);
return;
}
if (!obj_attr.contains("total")) {
Error("Failed to pase JSON: attr object is missing total.", json_obj);
return;
}
int total = obj_attr["total"].toString().toInt();
int pages = obj_attr["totalPages"].toString().toInt();
if (page == 0) {
lastplayed_total_ = total;
UpdateTotal();
AddGetRecentTracksRequest(1);
}
else {
QJsonArray array_track = json_obj["track"].toArray();
for (const QJsonValue &value_track : array_track) {
++lastplayed_received_;
if (!value_track.isObject()) {
continue;
}
QJsonObject obj_track = value_track.toObject();
if (!obj_track.contains("artist") ||
!obj_track.contains("album") ||
!obj_track.contains("name") ||
!obj_track.contains("date") ||
!obj_track["artist"].isObject() ||
!obj_track["album"].isObject() ||
!obj_track["date"].isObject()
) {
continue;
}
QJsonObject obj_artist = obj_track["artist"].toObject();
QJsonObject obj_album = obj_track["album"].toObject();
QJsonObject obj_date = obj_track["date"].toObject();
if (!obj_artist.contains("#text") || !obj_album.contains("#text") || !obj_date.contains("#text")) {
continue;
}
QString artist = obj_artist["#text"].toString();
QString album = obj_album["#text"].toString();
QString date = obj_date["#text"].toString();
QString title = obj_track["name"].toString();
QDateTime datetime = QDateTime::fromString(date, "dd MMM yyyy, hh:mm");
emit UpdateLastPlayed(artist, album, title, datetime.toSecsSinceEpoch());
UpdateProgress();
}
if (page == 1) {
for (int i = 2 ; i <= pages ; ++i) {
AddGetRecentTracksRequest(i);
}
}
}
FinishCheck();
}
void LastFMImport::AddGetTopTracksRequest(const int page) {
top_tracks_requests_.enqueue(GetTopTracksRequest(page));
if (!timer_flush_requests_->isActive()) {
timer_flush_requests_->start();
}
}
void LastFMImport::SendGetTopTracksRequest(GetTopTracksRequest request) {
ParamList params = ParamList() << Param("method", "user.getTopTracks");
if (request.page == 0) {
params << Param("page", "1");
params << Param("limit", "1");
}
else {
params << Param("page", QString::number(request.page));
params << Param("limit", "500");
}
QNetworkReply *reply = CreateRequest(params);
connect(reply, &QNetworkReply::finished, [=] { GetTopTracksRequestFinished(reply, request.page); });
}
void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int page) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
disconnect(reply, nullptr, this, nullptr);
reply->deleteLater();
QByteArray data = GetReplyData(reply);
if (data.isEmpty()) {
return;
}
QJsonObject json_obj = ExtractJsonObj(data);
if (json_obj.isEmpty()) {
return;
}
if (json_obj.contains("error") && json_obj.contains("message")) {
int error_code = json_obj["error"].toInt();
QString error_message = json_obj["message"].toString();
QString error_reason = QString("%1 (%2)").arg(error_message).arg(error_code);
Error(error_reason);
return;
}
if (!json_obj.contains("toptracks")) {
Error("JSON reply from server is missing toptracks.", json_obj);
return;
}
if (!json_obj["toptracks"].isObject()) {
Error("Failed to pase JSON: toptracks is not an object!", json_obj);
return;
}
json_obj = json_obj["toptracks"].toObject();
if (!json_obj.contains("@attr")) {
Error("JSON reply from server is missing @attr.", json_obj);
return;
}
if (!json_obj.contains("track")) {
Error("JSON reply from server is missing track.", json_obj);
return;
}
if (!json_obj["@attr"].isObject()) {
Error("Failed to pase JSON: @attr is not an object.", json_obj);
return;
}
if (!json_obj["track"].isArray()) {
Error("Failed to pase JSON: track is not an object.", json_obj);
return;
}
QJsonObject obj_attr = json_obj["@attr"].toObject();
if (!obj_attr.contains("page")) {
Error("Failed to pase JSON: attr object is missing page.", json_obj);
return;
}
if (!obj_attr.contains("totalPages")) {
Error("Failed to pase JSON: attr object is missing page.", json_obj);
return;
}
if (!obj_attr.contains("total")) {
Error("Failed to pase JSON: attr object is missing total.", json_obj);
return;
}
int pages = obj_attr["totalPages"].toString().toInt();
int total = obj_attr["total"].toString().toInt();
if (page == 0) {
playcount_total_ = total;
UpdateTotal();
AddGetTopTracksRequest(1);
}
else {
QJsonArray array_track = json_obj["track"].toArray();
for (const QJsonValue &value_track : array_track) {
++playcount_received_;
if (!value_track.isObject()) {
continue;
}
QJsonObject obj_track = value_track.toObject();
if (!obj_track.contains("artist") ||
!obj_track.contains("name") ||
!obj_track.contains("playcount") ||
!obj_track["artist"].isObject()
) {
continue;
}
QJsonObject obj_artist = obj_track["artist"].toObject();
if (!obj_artist.contains("name")) {
continue;
}
QString artist = obj_artist["name"].toString();
QString title = obj_track["name"].toString();
int playcount = obj_track["playcount"].toString().toInt();
if (playcount <= 0) continue;
emit UpdatePlayCount(artist, title, playcount);
UpdateProgress();
}
if (page == 1) {
for (int i = 2 ; i <= pages ; ++i) {
AddGetTopTracksRequest(i);
}
}
}
FinishCheck();
}
void LastFMImport::UpdateTotal() {
if ((!playcount_ || playcount_total_ > 0) && (!lastplayed_ || lastplayed_total_ > 0))
emit UpdateTotal(lastplayed_total_, playcount_total_);
}
void LastFMImport::UpdateProgress() {
emit UpdateProgress(lastplayed_received_, playcount_received_);
}
void LastFMImport::FinishCheck() {
if (replies_.isEmpty() && recent_tracks_requests_.isEmpty() && top_tracks_requests_.isEmpty()) emit Finished();
}
void LastFMImport::Error(const QString &error, const QVariant &debug) {
qLog(Error) << error;
if (debug.isValid()) qLog(Debug) << debug;
emit FinishedWithError(error);
AbortAll();
}

View File

@ -0,0 +1,113 @@
/*
* Strawberry Music Player
* Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef LASTFMIMPORT_H
#define LASTFMIMPORT_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QList>
#include <QVariant>
#include <QByteArray>
#include <QString>
#include <QQueue>
#include <QDateTime>
class QTimer;
class QNetworkReply;
class NetworkAccessManager;
class LastFMImport : public QObject {
Q_OBJECT
public:
explicit LastFMImport(QObject *parent = nullptr);
~LastFMImport() override;
void ReloadSettings();
void ImportData(const bool lastplayed = true, const bool playcount = true);
void AbortAll();
private:
typedef QPair<QString, QString> Param;
typedef QList<Param> ParamList;
struct GetRecentTracksRequest {
explicit GetRecentTracksRequest(const int _page) : page(_page) {}
int page;
};
struct GetTopTracksRequest {
explicit GetTopTracksRequest(const int _page) : page(_page) {}
int page;
};
private:
QNetworkReply *CreateRequest(const ParamList &request_params);
QByteArray GetReplyData(QNetworkReply *reply);
QJsonObject ExtractJsonObj(const QByteArray &data);
void AddGetRecentTracksRequest(const int page = 0);
void AddGetTopTracksRequest(const int page = 0);
void SendGetRecentTracksRequest(GetRecentTracksRequest request);
void SendGetTopTracksRequest(GetTopTracksRequest request);
void Error(const QString &error, const QVariant &debug = QVariant());
void UpdateTotal();
void UpdateProgress();
void FinishCheck();
signals:
void UpdatePlayCount(QString, QString, int);
void UpdateLastPlayed(QString, QString, QString, int);
void UpdateTotal(int, int);
void UpdateProgress(int, int);
void Finished();
void FinishedWithError(QString);
private slots:
void FlushRequests();
void GetRecentTracksRequestFinished(QNetworkReply *reply, const int page);
void GetTopTracksRequestFinished(QNetworkReply *reply, const int page);
private:
static const int kRequestsDelay;
NetworkAccessManager *network_;
QTimer *timer_flush_requests_;
QString username_;
bool lastplayed_;
bool playcount_;
int playcount_total_;
int lastplayed_total_;
int playcount_received_;
int lastplayed_received_;
QQueue<GetRecentTracksRequest> recent_tracks_requests_;
QQueue<GetTopTracksRequest> top_tracks_requests_;
QList<QNetworkReply*> replies_;
};
#endif // LASTFMIMPORT_H

View File

@ -42,13 +42,13 @@ class LastFMScrobbler : public ScrobblingAPI20 {
static const char *kName;
static const char *kSettingsGroup;
static const char *kApiUrl;
NetworkAccessManager *network() const override { return network_; }
ScrobblerCache *cache() const override { return cache_; }
private:
static const char *kAuthUrl;
static const char *kApiUrl;
static const char *kCacheFile;
QString settings_group_;

View File

@ -1013,7 +1013,7 @@ void ScrobblingAPI20::Error(const QString &error, const QVariant &debug) {
}
QString ScrobblingAPI20::ErrorString(const ScrobbleErrorCode error) const {
QString ScrobblingAPI20::ErrorString(const ScrobbleErrorCode error) {
switch (error) {
case ScrobbleErrorCode::NoError:

View File

@ -47,6 +47,7 @@ class ScrobblingAPI20 : public ScrobblerService {
explicit ScrobblingAPI20(const QString &name, const QString &settings_group, const QString &auth_url, const QString &api_url, const bool batch, Application *app, QObject *parent = nullptr);
~ScrobblingAPI20() override;
static const char *kApiKey;
static const char *kRedirectUrl;
void ReloadSettings() override;
@ -118,7 +119,6 @@ class ScrobblingAPI20 : public ScrobblerService {
RateLimitExceeded = 29,
};
static const char *kApiKey;
static const char *kSecret;
static const int kScrobblesPerRequest;
@ -129,7 +129,7 @@ class ScrobblingAPI20 : public ScrobblerService {
void AuthError(const QString &error);
void SendSingleScrobble(ScrobblerCacheItemPtr item);
void Error(const QString &error, const QVariant &debug = QVariant()) override;
QString ErrorString(const ScrobbleErrorCode error) const;
static QString ErrorString(const ScrobbleErrorCode error);
void DoSubmit() override;
void CheckScrobblePrevSong();