2018-12-23 18:54:27 +01:00
/*
* Strawberry Music Player
2021-03-20 21:14:47 +01:00
* Copyright 2018 - 2021 , 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"
# 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>
2020-02-09 02:29:35 +01:00
# include <QtDebug>
2018-12-23 18:54:27 +01:00
# 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/timeconstants.h"
# include "core/logging.h"
# include "internet/localredirectserver.h"
# include "audioscrobbler.h"
# include "scrobblerservice.h"
# include "scrobblercache.h"
# include "scrobblercacheitem.h"
# 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 ;
ListenBrainzScrobbler : : ListenBrainzScrobbler ( Application * app , QObject * parent ) : ScrobblerService ( kName , app , parent ) ,
app_ ( app ) ,
network_ ( new NetworkAccessManager ( this ) ) ,
cache_ ( new ScrobblerCache ( kCacheFile , this ) ) ,
2019-11-14 21:07:30 +01:00
server_ ( nullptr ) ,
2018-12-23 18:54:27 +01:00
enabled_ ( false ) ,
expires_in_ ( - 1 ) ,
2020-05-11 00:51:18 +02:00
login_time_ ( 0 ) ,
2019-11-14 21:07:30 +01:00
submitted_ ( false ) ,
2020-04-25 00:07:42 +02:00
scrobbled_ ( false ) ,
2019-11-14 21:07:30 +01:00
timestamp_ ( 0 ) {
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 ( ) ;
}
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 ( ) ) {
2020-07-18 04:24:16 +02:00
qint64 time = expires_in_ - ( QDateTime : : currentDateTime ( ) . toSecsSinceEpoch ( ) - 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
# if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
req . setAttribute ( QNetworkRequest : : RedirectPolicyAttribute , QNetworkRequest : : NoLessSafeRedirectPolicy ) ;
# else
2019-08-22 19:28:54 +02:00
req . setAttribute ( QNetworkRequest : : FollowRedirectsAttribute , true ) ;
2020-08-14 20:20:41 +02:00
# endif
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 ;
}
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
2018-12-26 01:45:28 +01:00
DoSubmit ( ) ;
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
# if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
req . setAttribute ( QNetworkRequest : : RedirectPolicyAttribute , QNetworkRequest : : NoLessSafeRedirectPolicy ) ;
# else
2019-08-22 19:28:54 +02:00
req . setAttribute ( QNetworkRequest : : FollowRedirectsAttribute , true ) ;
2020-08-14 20:20:41 +02:00
# endif
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 ;
}
void ListenBrainzScrobbler : : UpdateNowPlaying ( const Song & song ) {
2020-04-25 00:07:42 +02:00
CheckScrobblePrevSong ( ) ;
2018-12-23 18:54:27 +01:00
song_playing_ = song ;
2020-04-25 00:07:42 +02:00
scrobbled_ = false ;
2020-07-18 04:24:16 +02:00
timestamp_ = QDateTime : : currentDateTime ( ) . toSecsSinceEpoch ( ) ;
2018-12-23 18:54:27 +01:00
2019-04-04 20:17:28 +02:00
if ( ! song . is_metadata_good ( ) | | ! IsAuthenticated ( ) | | app_ - > scrobbler ( ) - > IsOffline ( ) ) return ;
2018-12-23 18:54:27 +01:00
2018-12-26 13:33:56 +01:00
QString album = song . album ( ) ;
2021-05-01 18:19:08 +02:00
QString title = song . title ( ) ;
2019-01-22 22:49:48 +01:00
album = album . remove ( Song : : kAlbumRemoveDisc ) ;
album = album . remove ( Song : : kAlbumRemoveMisc ) ;
2021-05-01 18:19:08 +02:00
title = title . remove ( Song : : kTitleRemoveMisc ) ;
2018-12-26 13:33:56 +01:00
2018-12-23 18:54:27 +01:00
QJsonObject object_track_metadata ;
2019-11-20 21:30:41 +01:00
if ( song . albumartist ( ) . isEmpty ( ) | | song . albumartist ( ) . toLower ( ) = = Song : : kVariousArtists ) {
2019-09-29 13:31:46 +02:00
object_track_metadata . insert ( " artist_name " , QJsonValue : : fromVariant ( song . artist ( ) ) ) ;
}
else {
object_track_metadata . insert ( " artist_name " , QJsonValue : : fromVariant ( song . albumartist ( ) ) ) ;
}
2019-11-20 21:30:41 +01:00
if ( ! album . isEmpty ( ) )
object_track_metadata . insert ( " release_name " , QJsonValue : : fromVariant ( album ) ) ;
2021-05-01 18:19:08 +02:00
object_track_metadata . insert ( " track_name " , QJsonValue : : fromVariant ( title ) ) ;
2018-12-25 23:28:58 +01:00
QJsonObject object_listen ;
2018-12-23 18:54:27 +01:00
object_listen . insert ( " track_metadata " , object_track_metadata ) ;
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 ( ) ;
if ( status . toLower ( ) ! = " ok " ) {
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
if ( ! submitted_ ) {
submitted_ = true ;
2018-12-25 22:58:34 +01:00
if ( app_ - > scrobbler ( ) - > SubmitDelay ( ) < = 0 ) {
Submit ( ) ;
}
2021-01-30 21:50:28 +01:00
else if ( ! timer_submit_ . isActive ( ) ) {
2021-03-21 18:53:02 +01:00
timer_submit_ . setInterval ( static_cast < int > ( app_ - > scrobbler ( ) - > SubmitDelay ( ) * 60 * kMsecPerSec ) ) ;
2021-01-30 21:50:28 +01:00
timer_submit_ . start ( ) ;
2018-12-25 22:58:34 +01:00
}
2018-12-23 18:54:27 +01:00
}
}
2018-12-26 01:17:17 +01:00
void ListenBrainzScrobbler : : DoSubmit ( ) {
if ( ! submitted_ & & cache_ - > Count ( ) > 0 ) {
submitted_ = true ;
2021-01-30 21:50:28 +01:00
if ( ! timer_submit_ . isActive ( ) ) {
2021-03-21 18:53:02 +01:00
timer_submit_ . setInterval ( static_cast < int > ( app_ - > scrobbler ( ) - > SubmitDelay ( ) * 60 * kMsecPerSec ) ) ;
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
submitted_ = false ;
if ( ! IsEnabled ( ) | | ! IsAuthenticated ( ) | | app_ - > scrobbler ( ) - > IsOffline ( ) ) return ;
QJsonArray array ;
int i ( 0 ) ;
QList < quint64 > list ;
2021-06-20 19:04:08 +02:00
QList < ScrobblerCacheItemPtr > items = cache_ - > List ( ) ;
for ( ScrobblerCacheItemPtr item : items ) { // clazy:exclude=range-loop
2018-12-23 18:54:27 +01:00
if ( item - > sent_ ) continue ;
item - > sent_ = true ;
2019-11-20 21:30:41 +01:00
+ + i ;
2018-12-23 18:54:27 +01:00
list < < item - > timestamp_ ;
QJsonObject object_listen ;
object_listen . insert ( " listened_at " , QJsonValue : : fromVariant ( item - > timestamp_ ) ) ;
QJsonObject object_track_metadata ;
2019-11-20 21:30:41 +01:00
if ( item - > albumartist_ . isEmpty ( ) | | item - > albumartist_ . toLower ( ) = = Song : : kVariousArtists )
2019-09-29 13:50:24 +02:00
object_track_metadata . insert ( " artist_name " , QJsonValue : : fromVariant ( item - > artist_ ) ) ;
else
object_track_metadata . insert ( " artist_name " , QJsonValue : : fromVariant ( item - > albumartist_ ) ) ;
2019-11-20 21:30:41 +01:00
if ( ! item - > album_ . isEmpty ( ) )
object_track_metadata . insert ( " release_name " , QJsonValue : : fromVariant ( item - > album_ ) ) ;
2018-12-23 18:54:27 +01:00
object_track_metadata . insert ( " track_name " , QJsonValue : : fromVariant ( item - > song_ ) ) ;
object_listen . insert ( " track_metadata " , object_track_metadata ) ;
array . append ( QJsonValue : : fromVariant ( object_listen ) ) ;
if ( i > = kScrobblesPerRequest ) break ;
}
if ( i < = 0 ) return ;
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 ( ) ;
QByteArray data = GetReplyData ( reply ) ;
if ( data . isEmpty ( ) ) {
cache_ - > ClearSent ( list ) ;
2018-12-26 01:17:17 +01:00
DoSubmit ( ) ;
2018-12-23 18:54:27 +01:00
return ;
}
QJsonObject json_obj = ExtractJsonObj ( data ) ;
if ( json_obj . isEmpty ( ) ) {
cache_ - > ClearSent ( list ) ;
2018-12-26 01:17:17 +01:00
DoSubmit ( ) ;
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 ) ;
2018-12-26 01:17:17 +01:00
DoSubmit ( ) ;
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 ) ;
2018-12-26 01:17:17 +01:00
DoSubmit ( ) ;
2018-12-23 18:54:27 +01:00
}
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 ( ) {
2020-07-18 04:24:16 +02:00
quint64 duration = QDateTime : : currentDateTime ( ) . toSecsSinceEpoch ( ) - timestamp_ ;
2020-04-25 00:07:42 +02:00
if ( ! scrobbled_ & & song_playing_ . is_metadata_good ( ) & & song_playing_ . source ( ) = = Song : : Source_Stream & & 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
}
}