2018-12-23 18:54:27 +01:00
/*
* Strawberry Music Player
2023-03-25 14:25:21 +01:00
* Copyright 2018 - 2023 , Jonas Kvinge < jonas @ jkvinge . net >
2018-12-23 18:54:27 +01:00
*
* 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"
2022-02-16 17:46:40 +01:00
# include <algorithm>
2022-07-15 14:34:43 +02:00
# include <QCoreApplication>
2018-12-23 18:54:27 +01:00
# include <QtGlobal>
# include <QDesktopServices>
# include <QVariant>
# include <QByteArray>
# include <QString>
# include <QUrl>
# include <QUrlQuery>
# include <QDateTime>
2021-01-30 21:50:28 +01:00
# include <QTimer>
2018-12-23 18:54:27 +01:00
# include <QMessageBox>
# include <QSettings>
# include <QNetworkRequest>
# include <QNetworkReply>
# include <QJsonDocument>
# include <QJsonObject>
# include <QJsonArray>
# include <QJsonValue>
# include "core/application.h"
2021-01-11 16:48:46 +01:00
# include "core/networkaccessmanager.h"
2018-12-23 18:54:27 +01:00
# include "core/song.h"
# include "core/logging.h"
2022-12-28 03:12:00 +01:00
# include "utilities/timeconstants.h"
2018-12-23 18:54:27 +01:00
# include "internet/localredirectserver.h"
2022-06-29 22:27:44 +02:00
# include "settings/scrobblersettingspage.h"
2018-12-23 18:54:27 +01:00
# include "audioscrobbler.h"
# include "scrobblerservice.h"
# include "scrobblercache.h"
# include "scrobblercacheitem.h"
2023-03-25 14:25:21 +01:00
# include "scrobblemetadata.h"
2018-12-23 18:54:27 +01:00
# include "listenbrainzscrobbler.h"
const char * ListenBrainzScrobbler : : kName = " ListenBrainz " ;
const char * ListenBrainzScrobbler : : kSettingsGroup = " ListenBrainz " ;
2020-05-11 00:51:18 +02:00
const char * ListenBrainzScrobbler : : kOAuthAuthorizeUrl = " https://musicbrainz.org/oauth2/authorize " ;
const char * ListenBrainzScrobbler : : kOAuthAccessTokenUrl = " https://musicbrainz.org/oauth2/token " ;
const char * ListenBrainzScrobbler : : kOAuthRedirectUrl = " http://localhost " ;
2018-12-23 18:54:27 +01:00
const char * ListenBrainzScrobbler : : kApiUrl = " https://api.listenbrainz.org " ;
2020-05-11 00:51:18 +02:00
const char * ListenBrainzScrobbler : : kClientIDB64 = " b2VBVU53cVNRZXIwZXIwOUZpcWkwUQ== " ;
const char * ListenBrainzScrobbler : : kClientSecretB64 = " Uk9GZ2hrZVEzRjNvUHlFaHFpeVdQQQ== " ;
2018-12-23 18:54:27 +01:00
const char * ListenBrainzScrobbler : : kCacheFile = " listenbrainzscrobbler.cache " ;
const int ListenBrainzScrobbler : : kScrobblesPerRequest = 10 ;
2021-07-11 07:40:57 +02:00
ListenBrainzScrobbler : : ListenBrainzScrobbler ( Application * app , QObject * parent )
: ScrobblerService ( kName , app , parent ) ,
app_ ( app ) ,
network_ ( new NetworkAccessManager ( this ) ) ,
cache_ ( new ScrobblerCache ( kCacheFile , this ) ) ,
server_ ( nullptr ) ,
enabled_ ( false ) ,
expires_in_ ( - 1 ) ,
login_time_ ( 0 ) ,
submitted_ ( false ) ,
scrobbled_ ( false ) ,
2022-02-16 17:46:40 +01:00
timestamp_ ( 0 ) ,
2022-06-29 22:27:44 +02:00
submit_error_ ( false ) ,
prefer_albumartist_ ( false ) {
2018-12-23 18:54:27 +01:00
2020-05-11 00:51:18 +02:00
refresh_login_timer_ . setSingleShot ( true ) ;
2021-01-26 16:48:04 +01:00
QObject : : connect ( & refresh_login_timer_ , & QTimer : : timeout , this , & ListenBrainzScrobbler : : RequestNewAccessToken ) ;
2020-05-11 00:51:18 +02:00
2021-01-30 21:50:28 +01:00
timer_submit_ . setSingleShot ( true ) ;
QObject : : connect ( & timer_submit_ , & QTimer : : timeout , this , & ListenBrainzScrobbler : : Submit ) ;
2021-03-21 04:43:55 +01:00
ListenBrainzScrobbler : : ReloadSettings ( ) ;
2018-12-23 18:54:27 +01:00
LoadSession ( ) ;
}
2020-05-12 21:28:42 +02:00
ListenBrainzScrobbler : : ~ ListenBrainzScrobbler ( ) {
while ( ! replies_ . isEmpty ( ) ) {
QNetworkReply * reply = replies_ . takeFirst ( ) ;
2021-01-26 16:48:04 +01:00
QObject : : disconnect ( reply , nullptr , this , nullptr ) ;
2020-05-12 21:28:42 +02:00
reply - > abort ( ) ;
reply - > deleteLater ( ) ;
}
if ( server_ ) {
2021-01-26 16:48:04 +01:00
QObject : : disconnect ( server_ , nullptr , this , nullptr ) ;
2020-05-12 21:28:42 +02:00
if ( server_ - > isListening ( ) ) server_ - > close ( ) ;
server_ - > deleteLater ( ) ;
}
}
2018-12-23 18:54:27 +01:00
void ListenBrainzScrobbler : : ReloadSettings ( ) {
QSettings s ;
s . beginGroup ( kSettingsGroup ) ;
enabled_ = s . value ( " enabled " , false ) . toBool ( ) ;
user_token_ = s . value ( " user_token " ) . toString ( ) ;
s . endGroup ( ) ;
2022-06-29 22:27:44 +02:00
s . beginGroup ( ScrobblerSettingsPage : : kSettingsGroup ) ;
prefer_albumartist_ = s . value ( " albumartist " , false ) . toBool ( ) ;
s . endGroup ( ) ;
2018-12-23 18:54:27 +01:00
}
void ListenBrainzScrobbler : : LoadSession ( ) {
QSettings s ;
s . beginGroup ( kSettingsGroup ) ;
access_token_ = s . value ( " access_token " ) . toString ( ) ;
expires_in_ = s . value ( " expires_in " , - 1 ) . toInt ( ) ;
token_type_ = s . value ( " token_type " ) . toString ( ) ;
refresh_token_ = s . value ( " refresh_token " ) . toString ( ) ;
2020-05-11 00:51:18 +02:00
login_time_ = s . value ( " login_time " ) . toLongLong ( ) ;
2018-12-23 18:54:27 +01:00
s . endGroup ( ) ;
2020-05-11 00:51:18 +02:00
if ( ! refresh_token_ . isEmpty ( ) ) {
2021-10-30 02:21:29 +02:00
qint64 time = expires_in_ - ( QDateTime : : currentDateTime ( ) . toSecsSinceEpoch ( ) - static_cast < qint64 > ( login_time_ ) ) ;
2020-05-11 00:51:18 +02:00
if ( time < 6 ) time = 6 ;
2021-03-21 18:53:02 +01:00
refresh_login_timer_ . setInterval ( static_cast < int > ( time * kMsecPerSec ) ) ;
2020-05-11 00:51:18 +02:00
refresh_login_timer_ . start ( ) ;
}
2018-12-23 18:54:27 +01:00
}
void ListenBrainzScrobbler : : Logout ( ) {
access_token_ . clear ( ) ;
token_type_ . clear ( ) ;
refresh_token_ . clear ( ) ;
2020-05-11 00:51:18 +02:00
expires_in_ = - 1 ;
login_time_ = 0 ;
2018-12-23 18:54:27 +01:00
QSettings settings ;
settings . beginGroup ( kSettingsGroup ) ;
settings . remove ( " access_token " ) ;
settings . remove ( " expires_in " ) ;
settings . remove ( " token_type " ) ;
settings . remove ( " refresh_token " ) ;
settings . endGroup ( ) ;
}
2019-04-15 22:17:40 +02:00
void ListenBrainzScrobbler : : Authenticate ( const bool https ) {
2018-12-23 18:54:27 +01:00
2019-11-14 21:07:30 +01:00
if ( ! server_ ) {
2020-05-08 18:35:36 +02:00
server_ = new LocalRedirectServer ( this ) ;
server_ - > set_https ( https ) ;
2019-11-14 21:07:30 +01:00
if ( ! server_ - > Listen ( ) ) {
AuthError ( server_ - > error ( ) ) ;
delete server_ ;
server_ = nullptr ;
return ;
}
2021-01-26 16:48:04 +01:00
QObject : : connect ( server_ , & LocalRedirectServer : : Finished , this , & ListenBrainzScrobbler : : RedirectArrived ) ;
2019-04-15 22:17:40 +02:00
}
2018-12-23 18:54:27 +01:00
2020-05-11 00:51:18 +02:00
QUrl redirect_url ( kOAuthRedirectUrl ) ;
2019-11-14 21:07:30 +01:00
redirect_url . setPort ( server_ - > url ( ) . port ( ) ) ;
2018-12-23 18:54:27 +01:00
QUrlQuery url_query ;
url_query . addQueryItem ( " response_type " , " code " ) ;
2020-05-11 00:51:18 +02:00
url_query . addQueryItem ( " client_id " , QByteArray : : fromBase64 ( kClientIDB64 ) ) ;
2018-12-23 18:54:27 +01:00
url_query . addQueryItem ( " redirect_uri " , redirect_url . toString ( ) ) ;
url_query . addQueryItem ( " scope " , " profile;email;tag;rating;collection;submit_isrc;submit_barcode " ) ;
2020-05-11 00:51:18 +02:00
QUrl url ( kOAuthAuthorizeUrl ) ;
2018-12-23 18:54:27 +01:00
url . setQuery ( url_query ) ;
bool result = QDesktopServices : : openUrl ( url ) ;
if ( ! result ) {
2019-12-29 23:37:48 +01:00
QMessageBox messagebox ( QMessageBox : : Information , tr ( " ListenBrainz Authentication " ) , tr ( " Please open this URL in your browser " ) + QString ( " :<br /><a href= \" %1 \" >%1</a> " ) . arg ( url . toString ( ) ) , QMessageBox : : Ok ) ;
2018-12-24 00:15:53 +01:00
messagebox . setTextFormat ( Qt : : RichText ) ;
messagebox . exec ( ) ;
2018-12-23 18:54:27 +01:00
}
}
2019-11-14 21:07:30 +01:00
void ListenBrainzScrobbler : : RedirectArrived ( ) {
2018-12-23 18:54:27 +01:00
2019-11-14 21:07:30 +01:00
if ( ! server_ ) return ;
2018-12-23 18:54:27 +01:00
2019-11-14 21:07:30 +01:00
if ( server_ - > error ( ) . isEmpty ( ) ) {
QUrl url = server_ - > request_url ( ) ;
if ( url . isValid ( ) ) {
QUrlQuery url_query ( url ) ;
if ( url_query . hasQueryItem ( " error " ) ) {
AuthError ( QUrlQuery ( url ) . queryItemValue ( " error " ) ) ;
}
else if ( url_query . hasQueryItem ( " code " ) ) {
2020-05-11 00:51:18 +02:00
RequestAccessToken ( url , url_query . queryItemValue ( " code " ) ) ;
2019-11-14 21:07:30 +01:00
}
else {
AuthError ( tr ( " Redirect missing token code! " ) ) ;
}
}
else {
AuthError ( tr ( " Received invalid reply from web browser. " ) ) ;
}
2018-12-23 18:54:27 +01:00
}
2019-11-14 21:07:30 +01:00
else {
AuthError ( server_ - > error ( ) ) ;
2018-12-23 18:54:27 +01:00
}
2019-11-14 21:07:30 +01:00
server_ - > close ( ) ;
server_ - > deleteLater ( ) ;
server_ = nullptr ;
2018-12-23 18:54:27 +01:00
}
2020-05-11 00:51:18 +02:00
void ListenBrainzScrobbler : : RequestAccessToken ( const QUrl & redirect_url , const QString & code ) {
refresh_login_timer_ . stop ( ) ;
ParamList params = ParamList ( ) < < Param ( " client_id " , QByteArray : : fromBase64 ( kClientIDB64 ) )
< < Param ( " client_secret " , QByteArray : : fromBase64 ( kClientSecretB64 ) ) ;
if ( ! code . isEmpty ( ) & & ! redirect_url . isEmpty ( ) ) {
params < < Param ( " grant_type " , " authorization_code " ) ;
params < < Param ( " code " , code ) ;
params < < Param ( " redirect_uri " , redirect_url . toString ( ) ) ;
}
else if ( ! refresh_token_ . isEmpty ( ) & & enabled_ ) {
params < < Param ( " grant_type " , " refresh_token " ) ;
params < < Param ( " refresh_token " , refresh_token_ ) ;
}
else {
return ;
}
2018-12-23 18:54:27 +01:00
QUrlQuery url_query ;
2020-05-11 00:51:18 +02:00
for ( const Param & param : params ) {
url_query . addQueryItem ( QUrl : : toPercentEncoding ( param . first ) , QUrl : : toPercentEncoding ( param . second ) ) ;
}
QUrl session_url ( kOAuthAccessTokenUrl ) ;
2018-12-23 18:54:27 +01:00
QNetworkRequest req ( session_url ) ;
2020-08-14 20:20:41 +02:00
req . setAttribute ( QNetworkRequest : : RedirectPolicyAttribute , QNetworkRequest : : NoLessSafeRedirectPolicy ) ;
2018-12-23 18:54:27 +01:00
req . setHeader ( QNetworkRequest : : ContentTypeHeader , " application/x-www-form-urlencoded " ) ;
QByteArray query = url_query . toString ( QUrl : : FullyEncoded ) . toUtf8 ( ) ;
QNetworkReply * reply = network_ - > post ( req , query ) ;
2020-05-12 21:28:42 +02:00
replies_ < < reply ;
2021-03-21 00:37:17 +01:00
QObject : : connect ( reply , & QNetworkReply : : finished , this , [ this , reply ] ( ) { AuthenticateReplyFinished ( reply ) ; } ) ;
2018-12-23 18:54:27 +01:00
}
void ListenBrainzScrobbler : : AuthenticateReplyFinished ( QNetworkReply * reply ) {
2020-05-12 21:28:42 +02:00
if ( ! replies_ . contains ( reply ) ) return ;
replies_ . removeAll ( reply ) ;
2021-01-26 16:48:04 +01:00
QObject : : disconnect ( reply , nullptr , this , nullptr ) ;
2018-12-23 18:54:27 +01:00
reply - > deleteLater ( ) ;
QByteArray data ;
if ( reply - > error ( ) = = QNetworkReply : : NoError & & reply - > attribute ( QNetworkRequest : : HttpStatusCodeAttribute ) . toInt ( ) = = 200 ) {
data = reply - > readAll ( ) ;
}
else {
2019-07-07 21:14:24 +02:00
if ( reply - > error ( ) ! = QNetworkReply : : NoError & & reply - > error ( ) < 200 ) {
2018-12-23 18:54:27 +01:00
// This is a network error, there is nothing more to do.
2019-07-07 21:14:24 +02:00
AuthError ( QString ( " %1 (%2) " ) . arg ( reply - > errorString ( ) ) . arg ( reply - > error ( ) ) ) ;
2018-12-23 18:54:27 +01:00
}
else {
// See if there is Json data containing "error" and "error_description" - then use that instead.
data = reply - > readAll ( ) ;
2019-07-07 21:14:24 +02:00
QString error ;
QJsonParseError json_error ;
QJsonDocument json_doc = QJsonDocument : : fromJson ( data , & json_error ) ;
2020-10-02 19:27:47 +02:00
if ( json_error . error = = QJsonParseError : : NoError & & ! json_doc . isEmpty ( ) & & json_doc . isObject ( ) ) {
2018-12-23 18:54:27 +01:00
QJsonObject json_obj = json_doc . object ( ) ;
if ( json_obj . contains ( " error " ) & & json_obj . contains ( " error_description " ) ) {
2019-07-07 21:14:24 +02:00
error = json_obj [ " error_description " ] . toString ( ) ;
}
}
if ( error . isEmpty ( ) ) {
if ( reply - > error ( ) ! = QNetworkReply : : NoError ) {
error = QString ( " %1 (%2) " ) . arg ( reply - > errorString ( ) ) . arg ( reply - > error ( ) ) ;
2018-12-23 18:54:27 +01:00
}
else {
2019-07-07 21:14:24 +02:00
error = QString ( " Received HTTP code %1 " ) . arg ( reply - > attribute ( QNetworkRequest : : HttpStatusCodeAttribute ) . toInt ( ) ) ;
2018-12-23 18:54:27 +01:00
}
}
2019-07-07 21:14:24 +02:00
AuthError ( error ) ;
2018-12-23 18:54:27 +01:00
}
return ;
}
QJsonObject json_obj = ExtractJsonObj ( data ) ;
if ( json_obj . isEmpty ( ) ) {
AuthError ( " Json document from server was empty. " ) ;
return ;
}
2021-07-11 07:40:57 +02:00
2018-12-23 18:54:27 +01:00
if ( json_obj . contains ( " error " ) & & json_obj . contains ( " error_description " ) ) {
QString failure_reason = json_obj [ " error_description " ] . toString ( ) ;
AuthError ( failure_reason ) ;
return ;
}
2020-05-11 00:51:18 +02:00
if ( ! json_obj . contains ( " access_token " ) | | ! json_obj . contains ( " expires_in " ) | | ! json_obj . contains ( " token_type " ) ) {
2018-12-24 00:15:53 +01:00
AuthError ( " Json access_token, expires_in or token_type is missing. " ) ;
2018-12-23 18:54:27 +01:00
return ;
}
access_token_ = json_obj [ " access_token " ] . toString ( ) ;
expires_in_ = json_obj [ " expires_in " ] . toInt ( ) ;
token_type_ = json_obj [ " token_type " ] . toString ( ) ;
2020-05-11 00:51:18 +02:00
if ( json_obj . contains ( " refresh_token " ) ) {
refresh_token_ = json_obj [ " refresh_token " ] . toString ( ) ;
}
2020-07-18 04:24:16 +02:00
login_time_ = QDateTime : : currentDateTime ( ) . toSecsSinceEpoch ( ) ;
2018-12-23 18:54:27 +01:00
QSettings s ;
s . beginGroup ( kSettingsGroup ) ;
s . setValue ( " access_token " , access_token_ ) ;
s . setValue ( " expires_in " , expires_in_ ) ;
s . setValue ( " token_type " , token_type_ ) ;
s . setValue ( " refresh_token " , refresh_token_ ) ;
2020-05-11 00:51:18 +02:00
s . setValue ( " login_time " , login_time_ ) ;
2018-12-23 18:54:27 +01:00
s . endGroup ( ) ;
2020-05-11 00:51:18 +02:00
if ( expires_in_ > 0 ) {
2021-03-21 18:53:02 +01:00
refresh_login_timer_ . setInterval ( static_cast < int > ( expires_in_ * kMsecPerSec ) ) ;
2020-05-11 00:51:18 +02:00
refresh_login_timer_ . start ( ) ;
}
2018-12-23 18:54:27 +01:00
emit AuthenticationComplete ( true ) ;
2021-06-04 00:15:35 +02:00
qLog ( Debug ) < < " ListenBrainz: Authentication was successful, login expires in " < < expires_in_ ;
2020-05-11 00:51:18 +02:00
2022-02-16 17:46:40 +01:00
StartSubmit ( ) ;
2018-12-26 01:45:28 +01:00
2018-12-23 18:54:27 +01:00
}
QNetworkReply * ListenBrainzScrobbler : : CreateRequest ( const QUrl & url , const QJsonDocument & json_doc ) {
QNetworkRequest req ( url ) ;
2020-08-14 20:20:41 +02:00
req . setAttribute ( QNetworkRequest : : RedirectPolicyAttribute , QNetworkRequest : : NoLessSafeRedirectPolicy ) ;
2018-12-23 18:54:27 +01:00
req . setHeader ( QNetworkRequest : : ContentTypeHeader , " application/json " ) ;
req . setRawHeader ( " Authorization " , QString ( " Token %1 " ) . arg ( user_token_ ) . toUtf8 ( ) ) ;
QNetworkReply * reply = network_ - > post ( req , json_doc . toJson ( ) ) ;
2020-05-12 21:28:42 +02:00
replies_ < < reply ;
2018-12-23 18:54:27 +01:00
//qLog(Debug) << "ListenBrainz: Sending request" << json_doc.toJson();
return reply ;
}
QByteArray ListenBrainzScrobbler : : GetReplyData ( QNetworkReply * reply ) {
QByteArray data ;
if ( reply - > error ( ) = = QNetworkReply : : NoError & & reply - > attribute ( QNetworkRequest : : HttpStatusCodeAttribute ) . toInt ( ) = = 200 ) {
data = reply - > readAll ( ) ;
}
else {
if ( reply - > error ( ) < 200 ) {
// This is a network error, there is nothing more to do.
2019-07-07 21:14:24 +02:00
Error ( QString ( " %1 (%2) " ) . arg ( reply - > errorString ( ) ) . arg ( reply - > error ( ) ) ) ;
2018-12-23 18:54:27 +01:00
}
else {
2018-12-24 00:15:53 +01:00
// See if there is Json data containing "code" and "error" - then use that instead.
2018-12-23 18:54:27 +01:00
data = reply - > readAll ( ) ;
2019-07-07 21:14:24 +02:00
QString error ;
QJsonParseError json_error ;
QJsonDocument json_doc = QJsonDocument : : fromJson ( data , & json_error ) ;
2020-10-02 19:27:47 +02:00
if ( json_error . error = = QJsonParseError : : NoError & & ! json_doc . isEmpty ( ) & & json_doc . isObject ( ) ) {
2018-12-23 18:54:27 +01:00
QJsonObject json_obj = json_doc . object ( ) ;
if ( json_obj . contains ( " code " ) & & json_obj . contains ( " error " ) ) {
2019-04-08 18:46:11 +02:00
int error_code = json_obj [ " code " ] . toInt ( ) ;
2018-12-24 00:15:53 +01:00
QString error_message = json_obj [ " error " ] . toString ( ) ;
2019-07-07 21:14:24 +02:00
error = QString ( " %1 (%2) " ) . arg ( error_message ) . arg ( error_code ) ;
2018-12-23 18:54:27 +01:00
}
else {
2019-07-07 21:14:24 +02:00
error = QString ( " %1 (%2) " ) . arg ( reply - > errorString ( ) ) . arg ( reply - > error ( ) ) ;
2018-12-23 18:54:27 +01:00
}
}
2019-07-07 21:14:24 +02:00
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 ( ) ) ;
}
2018-12-23 18:54:27 +01:00
}
if ( reply - > error ( ) = = QNetworkReply : : ContentAccessDenied | | reply - > error ( ) = = QNetworkReply : : ContentOperationNotPermittedError | | reply - > error ( ) = = QNetworkReply : : AuthenticationRequiredError ) {
// Session is probably expired
Logout ( ) ;
}
2019-07-07 21:14:24 +02:00
Error ( error ) ;
2018-12-23 18:54:27 +01:00
}
return QByteArray ( ) ;
}
return data ;
2021-07-11 07:40:57 +02:00
2018-12-23 18:54:27 +01:00
}
2023-03-25 14:25:21 +01:00
QJsonObject ListenBrainzScrobbler : : JsonTrackMetadata ( const ScrobbleMetadata & metadata ) const {
2018-12-26 13:33:56 +01:00
2018-12-23 18:54:27 +01:00
QJsonObject object_track_metadata ;
2023-03-25 14:25:21 +01:00
if ( prefer_albumartist_ ) {
object_track_metadata . insert ( " artist_name " , QJsonValue : : fromVariant ( metadata . effective_albumartist ( ) ) ) ;
2019-09-29 13:31:46 +02:00
}
else {
2023-03-25 14:25:21 +01:00
object_track_metadata . insert ( " artist_name " , QJsonValue : : fromVariant ( metadata . artist ) ) ;
2019-09-29 13:31:46 +02:00
}
2019-11-20 21:30:41 +01:00
2023-03-25 14:25:21 +01:00
if ( ! metadata . album . isEmpty ( ) ) {
object_track_metadata . insert ( " release_name " , QJsonValue : : fromVariant ( StripAlbum ( metadata . album ) ) ) ;
2022-12-27 21:15:20 +01:00
}
2019-11-20 21:30:41 +01:00
2023-03-25 14:25:21 +01:00
object_track_metadata . insert ( " track_name " , QJsonValue : : fromVariant ( StripTitle ( metadata . title ) ) ) ;
2018-12-25 23:28:58 +01:00
2022-07-15 14:34:43 +02:00
QJsonObject object_additional_info ;
2023-03-25 14:25:21 +01:00
object_additional_info . insert ( " duration_ms " , metadata . length_nanosec / kNsecPerMsec ) ;
2022-07-15 14:34:43 +02:00
2023-03-25 14:25:21 +01:00
if ( metadata . track > 0 ) {
object_additional_info . insert ( " tracknumber " , metadata . track ) ;
2022-07-15 14:34:43 +02:00
}
2022-07-15 15:32:56 +02:00
object_additional_info . insert ( " media_player " , QCoreApplication : : applicationName ( ) ) ;
object_additional_info . insert ( " media_player_version " , QCoreApplication : : applicationVersion ( ) ) ;
2022-07-15 14:34:43 +02:00
object_additional_info . insert ( " submission_client " , QCoreApplication : : applicationName ( ) ) ;
object_additional_info . insert ( " submission_client_version " , QCoreApplication : : applicationVersion ( ) ) ;
2023-03-25 14:25:21 +01:00
QJsonArray artist_mbids ;
if ( ! metadata . musicbrainz_album_artist_id . isEmpty ( ) ) {
artist_mbids . append ( metadata . musicbrainz_album_artist_id ) ;
}
if ( ! metadata . musicbrainz_artist_id . isEmpty ( ) ) {
artist_mbids . append ( metadata . musicbrainz_artist_id ) ;
}
if ( ! metadata . musicbrainz_original_artist_id . isEmpty ( ) ) {
artist_mbids . append ( metadata . musicbrainz_original_artist_id ) ;
}
if ( ! artist_mbids . isEmpty ( ) ) {
object_additional_info . insert ( " artist_mbids " , artist_mbids ) ;
}
if ( ! metadata . musicbrainz_album_id . isEmpty ( ) ) {
object_additional_info . insert ( " release_mbid " , metadata . musicbrainz_album_id ) ;
}
else if ( ! metadata . musicbrainz_original_album_id . isEmpty ( ) ) {
object_additional_info . insert ( " release_mbid " , metadata . musicbrainz_original_album_id ) ;
}
if ( ! metadata . musicbrainz_recording_id . isEmpty ( ) ) {
object_additional_info . insert ( " recording_mbid " , metadata . musicbrainz_recording_id ) ;
}
if ( ! metadata . musicbrainz_track_id . isEmpty ( ) ) {
object_additional_info . insert ( " track_mbid " , metadata . musicbrainz_track_id ) ;
}
if ( ! metadata . musicbrainz_work_id . isEmpty ( ) ) {
object_additional_info . insert ( " work_mbids " , QJsonArray ( ) < < metadata . musicbrainz_work_id ) ;
}
2022-07-15 14:34:43 +02:00
object_track_metadata . insert ( " additional_info " , object_additional_info ) ;
2023-03-25 14:25:21 +01:00
return object_track_metadata ;
}
void ListenBrainzScrobbler : : UpdateNowPlaying ( const Song & song ) {
CheckScrobblePrevSong ( ) ;
song_playing_ = song ;
scrobbled_ = false ;
timestamp_ = QDateTime : : currentDateTime ( ) . toSecsSinceEpoch ( ) ;
if ( ! song . is_metadata_good ( ) | | ! IsAuthenticated ( ) | | app_ - > scrobbler ( ) - > IsOffline ( ) ) return ;
2018-12-23 18:54:27 +01:00
2023-03-25 14:25:21 +01:00
QJsonObject object_listen ;
object_listen . insert ( " track_metadata " , JsonTrackMetadata ( ScrobbleMetadata ( song ) ) ) ;
2018-12-23 18:54:27 +01:00
QJsonArray array_payload ;
array_payload . append ( object_listen ) ;
QJsonObject object ;
object . insert ( " listen_type " , " playing_now " ) ;
object . insert ( " payload " , array_payload ) ;
QJsonDocument doc ( object ) ;
QUrl url ( QString ( " %1/1/submit-listens " ) . arg ( kApiUrl ) ) ;
QNetworkReply * reply = CreateRequest ( url , doc ) ;
2021-03-21 00:37:17 +01:00
QObject : : connect ( reply , & QNetworkReply : : finished , this , [ this , reply ] ( ) { UpdateNowPlayingRequestFinished ( reply ) ; } ) ;
2018-12-23 18:54:27 +01:00
}
void ListenBrainzScrobbler : : UpdateNowPlayingRequestFinished ( QNetworkReply * reply ) {
2020-05-12 21:28:42 +02:00
if ( ! replies_ . contains ( reply ) ) return ;
replies_ . removeAll ( reply ) ;
2021-01-26 16:48:04 +01:00
QObject : : disconnect ( reply , nullptr , this , nullptr ) ;
2018-12-23 18:54:27 +01:00
reply - > deleteLater ( ) ;
QByteArray data = GetReplyData ( reply ) ;
if ( data . isEmpty ( ) ) {
return ;
}
2018-12-25 23:28:58 +01:00
QJsonObject json_obj = ExtractJsonObj ( data ) ;
if ( json_obj . isEmpty ( ) ) {
return ;
}
2018-12-26 02:23:48 +01:00
if ( json_obj . contains ( " code " ) & & json_obj . contains ( " error_description " ) ) {
2018-12-25 23:28:58 +01:00
QString error_desc = json_obj [ " error_description " ] . toString ( ) ;
Error ( error_desc ) ;
return ;
}
if ( ! json_obj . contains ( " status " ) ) {
Error ( " Missing status from server. " , json_obj ) ;
return ;
}
QString status = json_obj [ " status " ] . toString ( ) ;
2021-07-13 23:18:12 +02:00
if ( status . compare ( " ok " , Qt : : CaseInsensitive ) ! = 0 ) {
2018-12-25 23:28:58 +01:00
Error ( status ) ;
}
2018-12-23 18:54:27 +01:00
}
2019-06-12 00:38:52 +02:00
void ListenBrainzScrobbler : : ClearPlaying ( ) {
2020-04-25 00:07:42 +02:00
CheckScrobblePrevSong ( ) ;
2019-06-12 00:38:52 +02:00
song_playing_ = Song ( ) ;
2020-04-25 00:07:42 +02:00
scrobbled_ = false ;
timestamp_ = 0 ;
2019-06-12 00:38:52 +02:00
}
2018-12-23 18:54:27 +01:00
void ListenBrainzScrobbler : : Scrobble ( const Song & song ) {
if ( song . id ( ) ! = song_playing_ . id ( ) | | song . url ( ) ! = song_playing_ . url ( ) | | ! song . is_metadata_good ( ) ) return ;
2020-04-25 00:07:42 +02:00
scrobbled_ = true ;
2018-12-23 18:54:27 +01:00
cache_ - > Add ( song , timestamp_ ) ;
2020-05-11 00:51:18 +02:00
if ( app_ - > scrobbler ( ) - > IsOffline ( ) | | ! IsAuthenticated ( ) ) return ;
2018-12-23 18:54:27 +01:00
2022-02-16 17:46:40 +01:00
StartSubmit ( ) ;
2018-12-23 18:54:27 +01:00
}
2022-02-16 17:46:40 +01:00
void ListenBrainzScrobbler : : StartSubmit ( const bool initial ) {
2018-12-26 01:17:17 +01:00
if ( ! submitted_ & & cache_ - > Count ( ) > 0 ) {
2022-02-16 17:46:40 +01:00
if ( initial & & app_ - > scrobbler ( ) - > SubmitDelay ( ) < = 0 & & ! submit_error_ ) {
if ( timer_submit_ . isActive ( ) ) {
timer_submit_ . stop ( ) ;
}
Submit ( ) ;
}
else if ( ! timer_submit_ . isActive ( ) ) {
int submit_delay = static_cast < int > ( std : : max ( app_ - > scrobbler ( ) - > SubmitDelay ( ) , submit_error_ ? 30 : 5 ) * kMsecPerSec ) ;
timer_submit_ . setInterval ( submit_delay ) ;
2021-01-30 21:50:28 +01:00
timer_submit_ . start ( ) ;
}
2018-12-26 01:17:17 +01:00
}
}
2018-12-23 18:54:27 +01:00
void ListenBrainzScrobbler : : Submit ( ) {
2020-05-11 00:51:18 +02:00
qLog ( Debug ) < < " ListenBrainz: Submitting scrobbles. " ;
2018-12-23 18:54:27 +01:00
if ( ! IsEnabled ( ) | | ! IsAuthenticated ( ) | | app_ - > scrobbler ( ) - > IsOffline ( ) ) return ;
QJsonArray array ;
2023-03-25 14:25:21 +01:00
int i = 0 ;
2018-12-23 18:54:27 +01:00
QList < quint64 > list ;
2023-03-25 14:25:21 +01:00
ScrobblerCacheItemPtrList cache_items = cache_ - > List ( ) ;
for ( ScrobblerCacheItemPtr cache_item : cache_items ) {
if ( cache_item - > sent ) continue ;
cache_item - > sent = true ;
2019-11-20 21:30:41 +01:00
+ + i ;
2023-03-25 14:25:21 +01:00
list < < cache_item - > timestamp ;
2018-12-23 18:54:27 +01:00
QJsonObject object_listen ;
2023-03-25 14:25:21 +01:00
object_listen . insert ( " listened_at " , QJsonValue : : fromVariant ( cache_item - > timestamp ) ) ;
object_listen . insert ( " track_metadata " , JsonTrackMetadata ( cache_item - > metadata ) ) ;
2018-12-23 18:54:27 +01:00
array . append ( QJsonValue : : fromVariant ( object_listen ) ) ;
if ( i > = kScrobblesPerRequest ) break ;
}
if ( i < = 0 ) return ;
2022-02-16 17:46:40 +01:00
submitted_ = true ;
2018-12-23 18:54:27 +01:00
QJsonObject object ;
object . insert ( " listen_type " , " import " ) ;
object . insert ( " payload " , array ) ;
QJsonDocument doc ( object ) ;
QUrl url ( QString ( " %1/1/submit-listens " ) . arg ( kApiUrl ) ) ;
QNetworkReply * reply = CreateRequest ( url , doc ) ;
2021-03-21 00:37:17 +01:00
QObject : : connect ( reply , & QNetworkReply : : finished , this , [ this , reply , list ] ( ) { ScrobbleRequestFinished ( reply , list ) ; } ) ;
2018-12-23 18:54:27 +01:00
}
2021-06-20 19:04:08 +02:00
void ListenBrainzScrobbler : : ScrobbleRequestFinished ( QNetworkReply * reply , const QList < quint64 > & list ) {
2018-12-23 18:54:27 +01:00
2020-05-12 21:28:42 +02:00
if ( ! replies_ . contains ( reply ) ) return ;
replies_ . removeAll ( reply ) ;
2021-01-26 16:48:04 +01:00
QObject : : disconnect ( reply , nullptr , this , nullptr ) ;
2018-12-23 18:54:27 +01:00
reply - > deleteLater ( ) ;
2022-02-16 17:46:40 +01:00
submitted_ = false ;
2018-12-23 18:54:27 +01:00
QByteArray data = GetReplyData ( reply ) ;
if ( data . isEmpty ( ) ) {
cache_ - > ClearSent ( list ) ;
2022-02-16 17:46:40 +01:00
submit_error_ = true ;
StartSubmit ( ) ;
2018-12-23 18:54:27 +01:00
return ;
}
QJsonObject json_obj = ExtractJsonObj ( data ) ;
if ( json_obj . isEmpty ( ) ) {
cache_ - > ClearSent ( list ) ;
2022-02-16 17:46:40 +01:00
submit_error_ = true ;
StartSubmit ( ) ;
2018-12-23 18:54:27 +01:00
return ;
}
2018-12-26 02:23:48 +01:00
if ( json_obj . contains ( " code " ) & & json_obj . contains ( " error_description " ) ) {
2018-12-23 18:54:27 +01:00
QString error_desc = json_obj [ " error_description " ] . toString ( ) ;
Error ( error_desc ) ;
cache_ - > ClearSent ( list ) ;
2022-02-16 17:46:40 +01:00
submit_error_ = true ;
StartSubmit ( ) ;
2018-12-23 18:54:27 +01:00
return ;
}
if ( json_obj . contains ( " status " ) ) {
QString status = json_obj [ " status " ] . toString ( ) ;
2018-12-24 00:15:53 +01:00
qLog ( Debug ) < < " ListenBrainz: Received scrobble status: " < < status ;
2018-12-23 18:54:27 +01:00
}
cache_ - > Flush ( list ) ;
2022-02-16 17:46:40 +01:00
submit_error_ = false ;
StartSubmit ( ) ;
2018-12-23 18:54:27 +01:00
}
2023-03-25 14:25:21 +01:00
void ListenBrainzScrobbler : : Love ( ) {
if ( ! song_playing_ . is_valid ( ) | | ! song_playing_ . is_metadata_good ( ) ) return ;
if ( ! IsAuthenticated ( ) ) app_ - > scrobbler ( ) - > ShowConfig ( ) ;
if ( song_playing_ . musicbrainz_recording_id ( ) . isEmpty ( ) ) {
qLog ( Error ) < < " ListenBrainz: Missing MusicBrainz recording ID for " < < song_playing_ . artist ( ) < < song_playing_ . album ( ) < < song_playing_ . title ( ) ;
return ;
}
qLog ( Debug ) < < " ListenBrainz: Sending love for song " < < song_playing_ . artist ( ) < < song_playing_ . album ( ) < < song_playing_ . title ( ) ;
QJsonObject object ;
object . insert ( " recording_mbid " , song_playing_ . musicbrainz_recording_id ( ) ) ;
object . insert ( " score " , 1 ) ;
QUrl url ( QString ( " %1/1/feedback/recording-feedback " ) . arg ( kApiUrl ) ) ;
QNetworkReply * reply = CreateRequest ( url , QJsonDocument ( object ) ) ;
QObject : : connect ( reply , & QNetworkReply : : finished , this , [ this , reply ] ( ) { LoveRequestFinished ( reply ) ; } ) ;
}
void ListenBrainzScrobbler : : LoveRequestFinished ( QNetworkReply * reply ) {
if ( ! replies_ . contains ( reply ) ) return ;
replies_ . removeAll ( reply ) ;
QObject : : 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 ( " code " ) & & json_obj . contains ( " error_description " ) ) {
Error ( json_obj [ " error_description " ] . toString ( ) ) ;
return ;
}
if ( json_obj . contains ( " status " ) ) {
QString status = json_obj [ " status " ] . toString ( ) ;
qLog ( Debug ) < < " ListenBrainz: Received recording-feedback status: " < < status ;
}
}
2019-11-14 21:07:30 +01:00
void ListenBrainzScrobbler : : AuthError ( const QString & error ) {
2018-12-23 18:54:27 +01:00
emit AuthenticationComplete ( false , error ) ;
}
2019-11-14 21:07:30 +01:00
void ListenBrainzScrobbler : : Error ( const QString & error , const QVariant & debug ) {
2018-12-23 18:54:27 +01:00
qLog ( Error ) < < " ListenBrainz: " < < error ;
if ( debug . isValid ( ) ) qLog ( Debug ) < < debug ;
}
2020-04-25 00:07:42 +02:00
void ListenBrainzScrobbler : : CheckScrobblePrevSong ( ) {
2021-10-30 02:21:29 +02:00
qint64 duration = QDateTime : : currentDateTime ( ) . toSecsSinceEpoch ( ) - static_cast < qint64 > ( timestamp_ ) ;
if ( duration < 0 ) duration = 0 ;
2020-04-25 00:07:42 +02:00
2021-07-11 05:18:39 +02:00
if ( ! scrobbled_ & & song_playing_ . is_metadata_good ( ) & & song_playing_ . is_radio ( ) & & duration > 30 ) {
2020-04-25 01:15:23 +02:00
Song song ( song_playing_ ) ;
song . set_length_nanosec ( duration * kNsecPerSec ) ;
Scrobble ( song ) ;
2020-04-25 00:07:42 +02:00
}
}