From 2e0f7b367fa28da215d6aa00db36280012fe24da Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Tue, 25 Feb 2020 01:08:03 +0100 Subject: [PATCH] Remove tidal and qobuz Fixes #369 --- CMakeLists.txt | 2 - README.md | 8 +- data/data.qrc | 1 + data/icons.qrc | 10 - data/icons/128x128/qobuz.png | Bin 6410 -> 0 bytes data/icons/128x128/tidal.png | Bin 5892 -> 0 bytes data/icons/22x22/qobuz.png | Bin 964 -> 0 bytes data/icons/22x22/tidal.png | Bin 933 -> 0 bytes data/icons/32x32/qobuz.png | Bin 1405 -> 0 bytes data/icons/32x32/tidal.png | Bin 1433 -> 0 bytes data/icons/48x48/qobuz.png | Bin 2205 -> 0 bytes data/icons/48x48/tidal.png | Bin 2136 -> 0 bytes data/icons/64x64/qobuz.png | Bin 3057 -> 0 bytes data/icons/64x64/tidal.png | Bin 2905 -> 0 bytes data/icons/full/qobuz.png | Bin 60300 -> 0 bytes data/icons/full/tidal.png | Bin 7322 -> 0 bytes data/schema/schema-10.sql | 25 + data/schema/schema.sql | 434 +----- debian/control | 4 +- debian/copyright | 6 - dist/man/strawberry.1 | 4 +- dist/rpm/strawberry.spec.in | 4 +- ...rawberrymusicplayer.strawberry.appdata.xml | 2 +- ...g.strawberrymusicplayer.strawberry.desktop | 2 +- src/CMakeLists.txt | 44 - src/config.h.in | 2 - src/core/application.cpp | 36 - src/core/application.h | 10 +- src/core/database.cpp | 2 +- src/core/iconmapper.h | 2 - src/core/mainwindow.cpp | 69 - src/core/mainwindow.h | 2 - src/covermanager/tidalcoverprovider.cpp | 277 ---- src/covermanager/tidalcoverprovider.h | 73 - src/qobuz/qobuzbaserequest.cpp | 195 --- src/qobuz/qobuzbaserequest.h | 109 -- src/qobuz/qobuzfavoriterequest.cpp | 282 ---- src/qobuz/qobuzfavoriterequest.h | 82 -- src/qobuz/qobuzrequest.cpp | 1265 ----------------- src/qobuz/qobuzrequest.h | 206 --- src/qobuz/qobuzservice.cpp | 765 ---------- src/qobuz/qobuzservice.h | 235 --- src/qobuz/qobuzstreamurlrequest.cpp | 246 ---- src/qobuz/qobuzstreamurlrequest.h | 76 - src/qobuz/qobuzurlhandler.cpp | 66 - src/qobuz/qobuzurlhandler.h | 55 - src/settings/qobuzsettingspage.cpp | 160 --- src/settings/qobuzsettingspage.h | 62 - src/settings/qobuzsettingspage.ui | 293 ---- src/settings/settingsdialog.cpp | 14 +- src/settings/settingsdialog.h | 4 +- src/settings/tidalsettingspage.cpp | 205 --- src/settings/tidalsettingspage.h | 71 - src/settings/tidalsettingspage.ui | 344 ----- src/tidal/tidalbaserequest.cpp | 206 --- src/tidal/tidalbaserequest.h | 113 -- src/tidal/tidalfavoriterequest.cpp | 301 ---- src/tidal/tidalfavoriterequest.h | 88 -- src/tidal/tidalrequest.cpp | 1215 ---------------- src/tidal/tidalrequest.h | 212 --- src/tidal/tidalservice.cpp | 970 ------------- src/tidal/tidalservice.h | 259 ---- src/tidal/tidalstreamurlrequest.cpp | 299 ---- src/tidal/tidalstreamurlrequest.h | 79 - src/tidal/tidalurlhandler.cpp | 68 - src/tidal/tidalurlhandler.h | 57 - 66 files changed, 43 insertions(+), 9578 deletions(-) delete mode 100644 data/icons/128x128/qobuz.png delete mode 100644 data/icons/128x128/tidal.png delete mode 100644 data/icons/22x22/qobuz.png delete mode 100644 data/icons/22x22/tidal.png delete mode 100644 data/icons/32x32/qobuz.png delete mode 100644 data/icons/32x32/tidal.png delete mode 100644 data/icons/48x48/qobuz.png delete mode 100644 data/icons/48x48/tidal.png delete mode 100644 data/icons/64x64/qobuz.png delete mode 100644 data/icons/64x64/tidal.png delete mode 100644 data/icons/full/qobuz.png delete mode 100644 data/icons/full/tidal.png create mode 100644 data/schema/schema-10.sql delete mode 100644 src/covermanager/tidalcoverprovider.cpp delete mode 100644 src/covermanager/tidalcoverprovider.h delete mode 100644 src/qobuz/qobuzbaserequest.cpp delete mode 100644 src/qobuz/qobuzbaserequest.h delete mode 100644 src/qobuz/qobuzfavoriterequest.cpp delete mode 100644 src/qobuz/qobuzfavoriterequest.h delete mode 100644 src/qobuz/qobuzrequest.cpp delete mode 100644 src/qobuz/qobuzrequest.h delete mode 100644 src/qobuz/qobuzservice.cpp delete mode 100644 src/qobuz/qobuzservice.h delete mode 100644 src/qobuz/qobuzstreamurlrequest.cpp delete mode 100644 src/qobuz/qobuzstreamurlrequest.h delete mode 100644 src/qobuz/qobuzurlhandler.cpp delete mode 100644 src/qobuz/qobuzurlhandler.h delete mode 100644 src/settings/qobuzsettingspage.cpp delete mode 100644 src/settings/qobuzsettingspage.h delete mode 100644 src/settings/qobuzsettingspage.ui delete mode 100644 src/settings/tidalsettingspage.cpp delete mode 100644 src/settings/tidalsettingspage.h delete mode 100644 src/settings/tidalsettingspage.ui delete mode 100644 src/tidal/tidalbaserequest.cpp delete mode 100644 src/tidal/tidalbaserequest.h delete mode 100644 src/tidal/tidalfavoriterequest.cpp delete mode 100644 src/tidal/tidalfavoriterequest.h delete mode 100644 src/tidal/tidalrequest.cpp delete mode 100644 src/tidal/tidalrequest.h delete mode 100644 src/tidal/tidalservice.cpp delete mode 100644 src/tidal/tidalservice.h delete mode 100644 src/tidal/tidalstreamurlrequest.cpp delete mode 100644 src/tidal/tidalstreamurlrequest.h delete mode 100644 src/tidal/tidalurlhandler.cpp delete mode 100644 src/tidal/tidalurlhandler.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 89c81630..8714493a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -326,8 +326,6 @@ optional_component(TRANSLATIONS ON "Translations" DEPENDS "Qt5LinguistTools" Qt5LinguistTools_FOUND ) -optional_component(TIDAL ON "Tidal support") -optional_component(QOBUZ ON "Qobuz support") optional_component(SUBSONIC ON "Subsonic support") optional_component(MOODBAR ON "Moodbar" diff --git a/README.md b/README.md index 12eda688..a1dceb89 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,15 @@ Strawberry is a music player and music collection organizer. It is a fork of Cle * Advanced audio output and device configuration for bit-perfect playback on Linux * Edit tags on music files * Fetch tags from MusicBrainz - * Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Deezer](https://www.deezer.com/) and [Tidal](https://tidal.com/) + * Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/) and [Deezer](https://www.deezer.com/) * Song lyrics from [AudD](https://audd.io/), [lyrics.ovh](https://lyrics.ovh/) and [lololyrics.com](https://www.lololyrics.com/) * Support for multiple backends * Audio analyzer * Audio equalizer * Transfer music to iPod, iPhone, MTP or mass-storage USB player - * Subsonic streaming support - * Unofficial streaming support for [Tidal](https://tidal.com/) and [Qobuz](https://www.qobuz.com/) * Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/) - -**Tidal and Qobuz streaming in Strawberry is unofficial. You need an official API token (or App ID/Secret) to use it, we can not provide API tokens, or help getting them. Tidal will not work with Tidal Masters (MQA), because MQA is a proprietary format in lossy quality without an open source decoder, we can't support it.** + * Subsonic streaming support + It has so far been tested to work on Linux, OpenBSD and Windows. diff --git a/data/data.qrc b/data/data.qrc index 001450bd..b8dac081 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -10,6 +10,7 @@ schema/schema-7.sql schema/schema-8.sql schema/schema-9.sql + schema/schema-10.sql schema/device-schema.sql style/strawberry.css html/playing-tooltip-plain.html diff --git a/data/icons.qrc b/data/icons.qrc index 242c2d30..ff491cc7 100644 --- a/data/icons.qrc +++ b/data/icons.qrc @@ -89,8 +89,6 @@ icons/128x128/moodbar.png icons/128x128/love.png icons/128x128/subsonic.png - icons/128x128/tidal.png - icons/128x128/qobuz.png icons/64x64/albums.png icons/64x64/alsa.png icons/64x64/application-exit.png @@ -181,8 +179,6 @@ icons/64x64/moodbar.png icons/64x64/love.png icons/64x64/subsonic.png - icons/64x64/tidal.png - icons/64x64/qobuz.png icons/48x48/albums.png icons/48x48/alsa.png icons/48x48/application-exit.png @@ -276,8 +272,6 @@ icons/48x48/moodbar.png icons/48x48/love.png icons/48x48/subsonic.png - icons/48x48/tidal.png - icons/48x48/qobuz.png icons/32x32/albums.png icons/32x32/alsa.png icons/32x32/application-exit.png @@ -371,8 +365,6 @@ icons/32x32/moodbar.png icons/32x32/love.png icons/32x32/subsonic.png - icons/32x32/tidal.png - icons/32x32/qobuz.png icons/22x22/albums.png icons/22x22/alsa.png icons/22x22/application-exit.png @@ -466,7 +458,5 @@ icons/22x22/moodbar.png icons/22x22/love.png icons/22x22/subsonic.png - icons/22x22/tidal.png - icons/22x22/qobuz.png diff --git a/data/icons/128x128/qobuz.png b/data/icons/128x128/qobuz.png deleted file mode 100644 index 4b6b1775654b6074ccab55e9091379d1760e48d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6410 zcmV+l8TICgP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 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 YQe|Oed2z{QJOBUy07*qoM6N<$g2+560ssI2 diff --git a/data/icons/128x128/tidal.png b/data/icons/128x128/tidal.png deleted file mode 100644 index 5b7e2f11e143be11e9bdada4a687b1b9adfadd5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5892 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 WHuG>8GXHNi4Uh*b%2r7m1^f@tF>E0K diff --git a/data/icons/22x22/qobuz.png b/data/icons/22x22/qobuz.png deleted file mode 100644 index 09834a5f14d275c903069022d533220cd7fa80c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 964 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H0wnYHF4+L2SkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{Xiaj ziKnkC`(rjCZdI9^pHCG6<(GT9IEHAPPn}$wai&n_c>H&Rw%hsq=I3q%e%i5ZuE@1X zHm<^ASxcTIsy1Ph&ElXPTZ^ zzM$#XGAY^q-Fsxze#dXU^3rl~o+oow=-O9LLS2G(_o=)&Kf~5*?)R5FRra6DUTl^z zy)0v{R*A2p=k_W0Pxvr!KRC0$a_fp7E4~Mc^MC53u_eEJrdcYoX;x19-r^HyH%Lx? z`ruz{>Xps2zuyoDOgbWQtfiN&?7}g>ZPPdF@^W)~`OP{Pyy{#Kig~tkU-x&tW(I=99AL7R|6Y zzJq0ofvRV7%ZU`HKGTUF5o=~lu9zXHW-vWR!OLVSv$3*9;yZ4Z!Xgd+3^S2t*`kiKX4VgIU|CWWjC%t>oBDCu26LE=%#QXOueBMU=n{O8w zIw|Q@U}eYF{rBf5?k*1uTK?^XgzEB@4*e>7Cw|smcYkHKo2@VFnHWdQr707SS-d%u zAlPjX+J{qaYP>Y;!0&+d7m z>)g|6H_V+Po{#h7K5j&pUXO@geCxPai%Z; diff --git a/data/icons/22x22/tidal.png b/data/icons/22x22/tidal.png deleted file mode 100644 index 49610554112233a53feca55142bb787d8d0e7e9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 933 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H0wnYHF4+L2SkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{Xiaj ziKnkC`y+N4HeQYf|8d z51g35#8(V-?qxI6hq3#A=FWT0_&{mGVm-%`yC3RV@@`C;+qV13T@J6(lly)$BuwFK z(fFjbOD*i}Yu>$Urhjd_CB8a4Zm9&PaF5(tKL*E}u_CWS_RgNGoale_;=|^{U2Tp0;RUx`@<<2_$BNv?8l;kI?d$(~od+eV-OZau@E2n@X`Sa{bGCRAQmU(ASsnf;sp{FyR}%As9trOejt*w;3|oEk z%kit-LX&NtT@QW#e6RfXbr~yfDA+DFzx9Jt<6_-}iZA6;ZhTs9{VMbKoco43lPzjW zMdOb$Z<-nPeJ0ZaX5saJU;a7Py!>j;8%Bdx|EyTE$Bai_J-tw}i1USN_tLp4YzA8b zm-U-4S}vY`REr^8w(h9v%cZ@>7k%hl^Y2(Q&! zr@!jR_je~wf7BQLm^yP`aDe)bwoP`DdsknXrTgZ~Tbq)9HzKx6edQ9&jQXiv{G&AI zyZfDpa`(e$&*wOjuYI*;<)7mw-}1?=+J4pN|0B~!%Zse z7uG+;@136if5RX5NYVbuS8pe5KF;yYG*9(wdHM4&ZD2B0Epd$~Nl7e8wMs5Z1yT$~ z28I^82Ijg(79j>ER))q_#+KR!Mpgy}rKXjSP&DM`r(~v8Vrnq3fN0Q}aF!FOK@wy` saDG}zd16s2gJVj5QmTSyZen_BP-kdg00004XF*Lt006O% z3;baP0000WV@Og>004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 zVoOIv0RM-N%)bBt00(qQO+^Re1`-P;5Qyd&qj+!)=GhLc%l(K{pq^d)LJ9?pB?pT(8fnI45o;I? zwDT4(|GlN=|XF}Rt{nbB7>jpOu# z4NSW}p7#76GKjE^4=~Bag##z4xQGxWNEc!3T;V8>P(&I?GkJ1vhC#2;h%>yuJ}fLE z^wY~tx(U(+w6l?afLAG*$WpMY#8F;}g9y7~M(e{*63K)(ivSxbbmJpHKLE8HWIZEU zM#EC}!G_{Uhop1L5(6t$52-{O^@a9pt@1TY&N%cjbxNw!FduW^Xr-c(cEk}7dUl}m zQmsl8FsDvP3TSGS4JL)f7(}T{a8&`@89T6>MOA`-L;L}xQ(#bP8dVp&6>5Qb^KQvM01ESJk?qh%9-vNp2B`XUHR|T@%8rhKlUo3zFJZGRO%@ znylqortva_UQ~^i#gN4cNjf5D8^w@AeU&6>yNp2+@nYXg>eLD$mySt_Xr(x0BmsO_ zrglj_&A+bzs*q1or(V+A3QIbztX>up`<-0eEJ-@6!UssaBA=w70+M>QLe!9Qr>Jjc zhEK&@U-kfrO8H{1W49a~%p7Qnk<8vwotGqC(#uM|PaPiCJMu~D*ILnbTi~_L);R;PGU?;=zvz~(&BJ1MqHH<1(*J^oW#m%HX`PHI29oAMYR;COuS1Qyg zsaXZWlVDK0ji))86{^&iYSc|xvH5gShxM+?IbHppXHUdHCk`*^W6e~VOJidOk^}^MDf!?1LtXv zqs(C|n>hym0WFT>IC_fqk^lezC3HntbYx+4WjbSWWnpw>05UK!IV~_YEipJ$F)=zb zHaajjD=;uRFfgpSQY!!e03~!qSaf7zbY(hiZ)9m^c>ppnF*z+THZ3tYR539+Gc`Ij zFe@-HIxsN1lwp1V000?uMObuGZ)S9NVRB^vL1b@YWgtmyVP|DhWnpA_ami&o00000 LNkvXXu0mjfjoVM_ diff --git a/data/icons/32x32/tidal.png b/data/icons/32x32/tidal.png deleted file mode 100644 index c206ac0a340d86497ec7fdf6f4e7091a72b5eb86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1433 zcmV;K1!nq*P)kdg00004XF*Lt006O% z3;baP0000WV@Og>004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 zVoOIv0RM-N%)bBt00(qQO+^Rd2OI_s2!Kzn(*OVjd`Uz>R7l6Am0fI;RT#&A=lyKo zcKujcw{^Ipj%`rviYU>sQ3FZ{Vhln8i5Diz^u~)hOt{e-_0p&pEG8yixR^93Aeu|0v(G(gm#*s*UqHzoK&lWNQXtb=?Cy+u~>1Z5c$UCasg(L<<<5pTo zq(0HIUYm`Mvy_gg>jY9rZz>iJkz&W)y37tzgouT`DIt-r6CKf|#G#i#)i)$0(n`m| z5nwO(!d@UkENrEbLi&aV)n4$?oYXQ%V>}uU0fzzD&XcsWmNhJ6C6Cd>c9^q{a9;p zo$TCPy}E(}b8c%eJtseR*Gs2VQeD-V_P5K$vH4z{MQgZnAEYAUvX0)fX*Zps)Q=e4Acw_KuSgCT}}Y(nlpnWz%42mGMJOy(<%EH zH)##1C8H$4z#>8qxbmE_rl`u97*~fg;45-cB{RQLTPQ$2wjgts>&>>-jCMT(L>JzX#Kvz z(wJ+g!1V_S%j^~XD6~cE7D=0H#5njMX^vT;bG05UK!I4v+YEiyP%Ff=+bGdeRlD=;!TFfesBdEx*703~!qSaf7z zbY(hiZ)9m^c>ppnF*q$SH!U(aR4_C;F*7004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 zVoOIv0RM-N%)bBt00(qQO+^Re1`-P;2*W8J3;+NJfJsC_R9M56n0s(k^%ciIdv~+h zykF!+c!nmB08*4mgtj~sk?;~6rDywZLv(P9klHz(~dI~7!{_rIt~t1 z9*SkG6sDF0BnlV^uK;<6Kwz_*>}Ger{UbkaHpwQ?bN{&K{(k42@A;kI@1Aq+b#Nz2 zXEOJ59}~$Z6*r=r>s;X^l~i((;PC2>Vdk7nWho0NCEaNylS;C&@8uRg2+~9q?{Sc( z{~y4joHZ;aCxfZXWIBaplZXol;-!hRRPkSG355BAH~1S3cZ@|8jpb$9P>G(^QF%2K zwP=r4DO-qYzJup(%`f2unxH@Es_sl7Rp|+(if(o?{?5kTJk9lZtyaw-Qvr3TP1kfw zJp*QXv|slNag47HZ$5z+2;^#ux}(&)RIP1Vr-yW}N;O$!TBh~dr8-5Tq#CqVapDq- zhx7ggLKF0Ulv=y~szn+hk4C6ebF@SinyCp&lSkvVMjtB}<;W%_ih33fb&7t znPVP%*vrdQl1woZ8BGpZq!Q)^?KD$I4Ht3q2v0N3XhirE8|XUEH>v77>wR0hIVX7= z^UY%m8`ww-CEUvdvPi;1Jn@9-CV)sFn;ZiCo4q766(``Jj0lG_HHi=C><=)OZSL_e zveG=(vWB5g@opf&U~25!@a?QX;hTh z(cjOjh6~b3rvV8NB#dz2K!ga=O_#lbFaSP&M;!o3Jc-B6Dm=X*T1Uk?mIKhiQ=GF8 zR+uoB*<#VcDJnU^6?_Eoa+90f#7oGQZzYKb0m$bNS916#4Uokr89HiSmcJ`b^t%#N zps6ZRy4=c9kqVWrIOXXcl_-0V^f;X}mi{Qj27tNTm~ApokiZ*ZOx!~dLeGAOs_PQ?t- z?G<+tf?F2XymXi)jfx>2WRF=Pq_CJ-wuem|j3Mt}wBcF_L9<1jmwGc)8q=e9_$cQ1 z_LwV3rpRy?A;blX&x4#cLo-N

my{aeTm-*umzQJ21`w^l%!yoPn>?^_b~&uIauZ z62Oy=jMu^|` zh4@Lr6?U6`aEPi9>sn?*T+*-XZoFCr;t~`405Ulgr>%Cbe@!WO4Y5@9M(cJf+@){X z<+6_!adW&rwMGA;jJt+dGT=L6tK8xUx%~m0{2t9$vn}?HitZZXQ8n6npQ=Cv`9`1N z+pp#crd%v)VV?OCD&YZca3QKGiA`?uXrq>LQUHi>oLVOd9Ol+Q*P1jhhwD+Q3ONH3qoAshgKs!~GRp*~$hAEORCb5FKi=UM)2I{RTLmu0-fAue-^AQ41_Fx|9M!*147Zknn-8p&>imml}W)^R5U z08$vq7z)V5LxfIRspkr05UK!IV~_YEipJ$F)=zbHaajjD=;uRFfgpSQY!!e03~!qSaf7zbY(hiZ)9m^ zc>ppnF*z+THZ3tYR539+Gc`IjFe@-HIxsN1lwp1V000?uMObuGZ)S9NVRB^vL1b@Y fWgtmyVP|DhWnpA_ami&o00000NkvXXu0mjf;r-P( diff --git a/data/icons/48x48/tidal.png b/data/icons/48x48/tidal.png deleted file mode 100644 index fb014343281ae7c458cd0b4badea3c84e1fbdfcf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2136 zcmV-e2&eanP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 zVoOIv0RM-N%)bBt00(qQO+^Rd2OI_s2!Kzn(*OVmJ4r-AR9M5cm}_iQ*Ad5m_qBVy z55FH^dlwr!7^jA?4Rve>Z~zOtsD?I1B8giG1c)Z7uuA)-3a;EVSXE`IYSJo_BM^!L zQc>*`ssw031PB39;s^*OK!|xc!Nw2l^<%wv?>+r+Z7_HZrowZ-?A`sJIWzy6vomuf zqDnJ=Wg17h!NR1rXM)d%3nO`En&~wp(1a3H+SQAWQ{LklXO4CGSi~(}=S3zh%UOe; zMl~mpROdRO-H|NB_(!s|JFXK+bvc1X)z2E1Pfqz(Ab^naOA>ZCIwZj(9!WSl5_aU5 z2x$&b^Cj#=azl|{V*{%m&?t?4DMR4^ zxfSj8{Q+?op!RBC>ZhG&wX14(PrpW-cR0bw8lD@PZxlr0?%~{@r){~izZ{c;(XFpw z8Cx{prhB%_!hI>V{;A?JFG!>^g9K8TNh%Uh=6$L-wcdS69^;2^(-v(R717j}v5Xo$ zrA`fva;slYuPx06AB)+|W?JuJTiMKR7UKg-v(sz!>!Yz!Ln(DN`ZCJKb6?IDEu~I1 zMr@BHHU;KEAx&(UFk%~Mq7VqoOKg%OBU>0Lbz8LZ`(v?{jeu{{sP$Sft+vvWNh9kX znRV-EB$G-{TCElw^}@FaHnR18U~a`I_l};7^+GPj#}|`I>@V<9jF288w`GApU?06^)*z(h25E%*v|1roQT5^wG(uB;iJzrVO{1Ey!*)!+ z9+;PjZ`{%Jnbt$p+y9XSCXKvJFUl>1U?c43sn+y(fuWA-N> zTd*?usQyvzviM{AzKpCG{xS}`jzUVa-Ir{_lj?wlNv;$2Yihl1?Srx%n7!5!(ZX2= z)sQxnv0n+R!z%JlA;#({KC>f>z(#n_f2RSw|15&fLA&~O08W^T%VSC7f+BMD_G4QtzUUI zFaCJS@fY%1HQ@bRk)XL#PApz*84+dYIV98$6Ic<2%;nKmP-lDEW z-?xsM)44DK`c)1F_0s@C3Ky!8JOCNzS&5TOB!2>`$oyc?7dA?F&f;^fPKZ5;g!8P7 z1w``Ddl3kA$iF0Y%pY=eqAK~jvRz)miihG_NyWcJ9YP@VUfJ{lNJGvZ57`f0xEGa= z&3%?&??Z9f#GrOdg8-&iH6#El)Eg8$NoR zco&H`7~uCL(~gCcT;AX$Z}T%^d7luuM7YeGxT&XWM45_ox7w?kFeH%mc#i|&7FEPy zqKY>55J6?7WUBvEOc^DgC9)hppnvwtSAS+ZH+VtXRiRCbYqokn!tAANQU#K>yY*l5ghzlgw6e?X<6cv`Ndf*|4wx-r+VzmZbwI?Xr5&{%v@! zL9DeWINS%)oeKl9Fdqn=EZjYp|3gbS{i5DUg=gSp{anI7!b^6~IeD+WG8YkpV0f-& zN8qCDI3)>y(EKv*A^Jc9Aq_|AB`o=tFXtyLbe#X;Kr3IBgys(v++Gn*yi;`z*;W@< z_>6OwQ(fu}%?FT#K1=k9`MkPj8Em_FeX`ooLDG5+eh3UHaUfVY9YyTg2 zMh?mStF;ctI`gNle-Him2h$HDqZm^0e-D27!Igw-Pa6dQYM=MGnsMgV1)~TealMrt zC-R7I>0BFk&S4upoLp;}5hKDh;txx&2wCk;YH8{GC2uMTe@lG-(h-^L&i>iXu)uAk zy`^ol@C1wdK3*A#1JEvHoKcT;B^?+l5(oxXmA)5h?Y$bj8X`2p=)hrtf;VXRRWNRIdH6N5+!r;618Mbrsz*rq89zB zYg?YS+m@+pyw=hTQ`0(nP8a#-@T0MiC)@nyRA^%%Q_;R}xPKs=;sFGve1w9@41qIA zCiyXkz$jyEPDT4$1J6bqW1T&Bf)M}zjzw}q3$OYXD?cDS0000bbVXQnWMOn=I%9HWVRU5xGB7bXEigANGB{K) zG&(UeIx{&dFfuwYFm*L~;s5{uC3HntbYx+4WjbwdWNBu305UK!I4v+YEiyP%Ff=+b zGdeIhD=;!TFfc5T%?SVi02y>eSaefwW^{L9a%BKPWN%_+AW3auXJt}lVPtu6$z?nM O0000004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 zVoOIv0RM-N%)bBt00(qQO+^Re1`-P-Ig9KscK`qi)Ja4^RA}C6$J?vmBwKURqEDRk-J+hc8&ZUy^*Q_>QkwPaRzVVK(tBZ<#+vN@d(nD?+)tNY81v!WxeJ`doMDBsqi#^IsMXX|Q1x zzl2Su@GG@WM}`$`r@eYYGldYHJVwc}i_hh5x{yG65da(dQjs`5bK$DY~shT zh8QZbu~d5W{)Cp?@2c#uZwsACtc_zB^_ zO9A7UN(Iw6e$e1u^p50ol#VVB{gbZaWTRwSAHOs4$ zb%Q>Tf6%|eF3)fUtl$xHY|V%H8&A@RlQPPgz-aRD;UgO_Q33=A z&`k&X*u`!-aB()bb50WTueh3eXlFV9vWv9uQG?%dJ&wy+MV_sB12^z2e#)4^$&}*5 zjSClU+_;Dlq>ms$qGV9OXfo+x2k#PK240(Lr?U>fmsxzufz$w&@DQ0*JVUY7B-XH! zt@xP4iR9wKi8C(VLk4a{kPtzF1nHxX7+K^Z4)H1VoJOI=YdpoQB~(Zm?_Iz6n1at`xl zLhb#&a3iw}ZezY}^{cr9KR$Bf>kJVlLWC$$Vh903#E20kN`weuLWBt7CLcE&SvM3gA| zspTANM`baFcj(K*$-hwofRnqKpUdM+x5RH`B?sawbYNg$#7&U!0B;;XuT}cue`X00 z1DZL=B0O=vC(y(un80T=CrmJli(v_8S>ijmlg4;GY^^NW46(QgVnm4$CLDLhehmbz zu=h`%vw8h>lt2MjnX*j)S5TD0HD-vnd0m!fm2wg7z3Ep*tU1u>mZ*u3Fgqm=imb7G0EUkThMrs29imH;*RJcgI2cEN*A$sZw5t_ zkxL)F9N`E@`cpF-y~%Rs#_O{5OwNSxSHbM#8&}Ft{>sd3MN>Lp0$}5zkYxJo) z6_X@|)uiWDDP$7dq@_c$Fv}#prO2Qow@tSyBkeqw zUbRMdsY-~KaKddwefq?9=8sFuW}<3_$bLe)ORltYud|K5PKa+9Wl(`Jit(1McCeqJ z$;W-RV4X~EZs+Huqs_J){bYbr$_yq!2?bWic0y@bEHU@+5R}Djj84GxLdj)4=~ z84x3!20)HY5Nx6zfVD|F$WCjd#Fg1F4`Po&3|r)KB#9VEeMu4i#CWRkAz%CBJcy^+z z;%B=gJv_{DjHYO{T9VGeEm|qxFtdtPXU+IQV}-aITFV%5K<1^W6O>RRO1n-;pLt{GMb{lyrRJ zNz%7EJ*hoQ?^zNeS|&to|Is;_CUm7O%lX1gm_}3{u(;GELAY1}t7p4Pg;>?E4}<5? zXl=3S;HD7+xYygX1L?(4= z#gLbj;mR2)Av|MVT%JgM;M`0EEmOB`LbvXacZ3k;4}wsmF+yzOgwz4f$1BLtok?%v zVZC5JGahrv)RH83jA(_~^BP$Rp>|b-*-VwnHj=|!>zp^3!I^ZjJ8h~pkTNNwm}a)x z-pLHg*+vsgA(xu|q~LRMIL-E|TFhc9=%D4OKx8wCY4Ox;4_ldIK`3HA z-_mF>mz;hG1D#1_ZecYEn9XmjiprL!B4mKE-;Y1jH8l;l;a^n6ajnK!W@d? zBMO<%HX0qwA(t9Lr0~WDb}%n|Tn#PEPI~e6P|gyTGLP}(k%1dGZZgTHhzZPM5ldOb zRI-Q>B}Rl8F<>VfsI=0YLgrJ;9tU&CqlQrGYs@_U!c3^-9?ng9(+T0HlWzKm;lzU* z2O)a!(}kZ-I%%f^KOHo4h$xl(os+EDyZJSrK#bM=E_DLf%sR#}b+6-H4pYu?g9bRs zqJR>{Qc4jyxG~?6VkrJ!MA**{974cezM;y>i3?c3RvI15=ER{g&~`qchl)Pm7yO4H zW0Rh*6Et}D#Q+Ckn)#aT<{jZ<)-!#dVTTanJ$8XHOyP->bq6#UX z4(-<-eWxw@R4=PqbChM`*~^_2;pI^xXs&7{NiDj>?0v=dEyg05UK!IV~_YEipJ$F)=zbHaajjD=;uRFfgpS zQY!!e03~!qSaf7zbY(hiZ)9m^c>ppnF*z+THZ3tYR539+Gc`IjFe@-HIxsN1lwp1V z000?uMObuGZ)S9NVRB^vL1b@YWgtmyVP|DhWnpA_ami&o00000NkvXXu0mjf{m*`> diff --git a/data/icons/64x64/tidal.png b/data/icons/64x64/tidal.png deleted file mode 100644 index cbd33703d9d0895df00702526d09e996a0a3f1d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2905 zcmV-f3#RmmP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 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 R03PPCxL|X>+{7d1e*lR%7w!N6 diff --git a/data/icons/full/tidal.png b/data/icons/full/tidal.png deleted file mode 100644 index 316831c699d9a4bc774690703d17e99f5319818a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7322 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 for multiple backends

  • Audio analyzer and equalizer
  • Transfer music to iPod, iPhone, MTP or mass-storage USB player
  • -
  • Streaming support for Tidal, Qobuz and Subsonic
  • Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
  • +
  • Streaming support for Subsonic
  • diff --git a/dist/unix/org.strawberrymusicplayer.strawberry.desktop b/dist/unix/org.strawberrymusicplayer.strawberry.desktop index c687087a..0e153875 100755 --- a/dist/unix/org.strawberrymusicplayer.strawberry.desktop +++ b/dist/unix/org.strawberrymusicplayer.strawberry.desktop @@ -10,5 +10,5 @@ Icon=strawberry Terminal=false Categories=AudioVideo;Player;Qt;Audio; StartupNotify=false -MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal; +MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf StartupWMClass=strawberry diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f308f332..e9dd0db5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -906,50 +906,6 @@ optional_source(WIN32 core/windows7thumbbar.h ) -optional_source(HAVE_TIDAL - SOURCES - tidal/tidalservice.cpp - tidal/tidalurlhandler.cpp - tidal/tidalbaserequest.cpp - tidal/tidalrequest.cpp - tidal/tidalstreamurlrequest.cpp - tidal/tidalfavoriterequest.cpp - settings/tidalsettingspage.cpp - covermanager/tidalcoverprovider.cpp - HEADERS - tidal/tidalservice.h - tidal/tidalurlhandler.h - tidal/tidalbaserequest.h - tidal/tidalrequest.h - tidal/tidalstreamurlrequest.h - tidal/tidalfavoriterequest.h - settings/tidalsettingspage.h - covermanager/tidalcoverprovider.h - UI - settings/tidalsettingspage.ui -) - -optional_source(HAVE_QOBUZ - SOURCES - qobuz/qobuzservice.cpp - qobuz/qobuzurlhandler.cpp - qobuz/qobuzbaserequest.cpp - qobuz/qobuzrequest.cpp - qobuz/qobuzstreamurlrequest.cpp - qobuz/qobuzfavoriterequest.cpp - settings/qobuzsettingspage.cpp - HEADERS - qobuz/qobuzservice.h - qobuz/qobuzurlhandler.h - qobuz/qobuzbaserequest.h - qobuz/qobuzrequest.h - qobuz/qobuzstreamurlrequest.h - qobuz/qobuzfavoriterequest.h - settings/qobuzsettingspage.h - UI - settings/qobuzsettingspage.ui -) - optional_source(HAVE_SUBSONIC SOURCES subsonic/subsonicservice.cpp diff --git a/src/config.h.in b/src/config.h.in index a474395c..1755932e 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -47,9 +47,7 @@ #cmakedefine HAVE_PHONON #cmakedefine XINE_ANALYZER -#cmakedefine HAVE_TIDAL #cmakedefine HAVE_SUBSONIC -#cmakedefine HAVE_QOBUZ #cmakedefine HAVE_MOODBAR diff --git a/src/core/application.cpp b/src/core/application.cpp index 645e6bb7..df2ec261 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -67,15 +67,6 @@ #include "internet/internetservices.h" #include "internet/internetsearch.h" -#ifdef HAVE_TIDAL -# include "tidal/tidalservice.h" -# include "covermanager/tidalcoverprovider.h" -#endif - -#ifdef HAVE_QOBUZ -# include "qobuz/qobuzservice.h" -#endif - #ifdef HAVE_SUBSONIC # include "subsonic/subsonicservice.h" #endif @@ -121,9 +112,6 @@ class ApplicationImpl { cover_providers->AddProvider(new DiscogsCoverProvider(app, app)); cover_providers->AddProvider(new MusicbrainzCoverProvider(app, app)); cover_providers->AddProvider(new DeezerCoverProvider(app, app)); -#ifdef HAVE_TIDAL - cover_providers->AddProvider(new TidalCoverProvider(app, app)); -#endif return cover_providers; }), album_cover_loader_([=]() { @@ -141,23 +129,11 @@ class ApplicationImpl { }), internet_services_([=]() { InternetServices *internet_services = new InternetServices(app); -#ifdef HAVE_TIDAL - internet_services->AddService(new TidalService(app, internet_services)); -#endif -#ifdef HAVE_QOBUZ - internet_services->AddService(new QobuzService(app, internet_services)); -#endif #ifdef HAVE_SUBSONIC internet_services->AddService(new SubsonicService(app, internet_services)); #endif return internet_services; }), -#ifdef HAVE_TIDAL - tidal_search_([=]() { return new InternetSearch(app, Song::Source_Tidal, app); }), -#endif -#ifdef HAVE_QOBUZ - qobuz_search_([=]() { return new InternetSearch(app, Song::Source_Qobuz, app); }), -#endif scrobbler_([=]() { return new AudioScrobbler(app, app); }), #ifdef HAVE_MOODBAR @@ -185,12 +161,6 @@ class ApplicationImpl { Lazy current_albumcover_loader_; Lazy lyrics_providers_; Lazy internet_services_; -#ifdef HAVE_TIDAL - Lazy tidal_search_; -#endif -#ifdef HAVE_QOBUZ - Lazy qobuz_search_; -#endif Lazy scrobbler_; #ifdef HAVE_MOODBAR Lazy moodbar_loader_; @@ -319,12 +289,6 @@ LyricsProviders *Application::lyrics_providers() const { return p_->lyrics_provi PlaylistBackend *Application::playlist_backend() const { return p_->playlist_backend_.get(); } PlaylistManager *Application::playlist_manager() const { return p_->playlist_manager_.get(); } InternetServices *Application::internet_services() const { return p_->internet_services_.get(); } -#ifdef HAVE_TIDAL -InternetSearch *Application::tidal_search() const { return p_->tidal_search_.get(); } -#endif -#ifdef HAVE_QOBUZ -InternetSearch *Application::qobuz_search() const { return p_->qobuz_search_.get(); } -#endif AudioScrobbler *Application::scrobbler() const { return p_->scrobbler_.get(); } #ifdef HAVE_MOODBAR MoodbarController *Application::moodbar_controller() const { return p_->moodbar_controller_.get(); } diff --git a/src/core/application.h b/src/core/application.h index d6f44ab7..51862f52 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -97,12 +97,6 @@ class Application : public QObject { AudioScrobbler *scrobbler() const; InternetServices *internet_services() const; -#ifdef HAVE_TIDAL - InternetSearch *tidal_search() const; -#endif -#ifdef HAVE_QOBUZ - InternetSearch *qobuz_search() const; -#endif #ifdef HAVE_MOODBAR MoodbarController *moodbar_controller() const; @@ -114,7 +108,7 @@ class Application : public QObject { QThread *MoveToNewThread(QObject *object); void MoveToThread(QObject *object, QThread *thread); -private slots: + private slots: void ExitReceived(); public slots: @@ -122,7 +116,7 @@ private slots: void ReloadSettings(); void OpenSettingsDialogAtPage(SettingsDialog::Page page); -signals: + signals: void ErrorAdded(const QString &message); void SettingsChanged(); void SettingsDialogRequested(SettingsDialog::Page page); diff --git a/src/core/database.cpp b/src/core/database.cpp index 31734b29..307a7803 100644 --- a/src/core/database.cpp +++ b/src/core/database.cpp @@ -54,7 +54,7 @@ #include "scopedtransaction.h" const char *Database::kDatabaseFilename = "strawberry.db"; -const int Database::kSchemaVersion = 9; +const int Database::kSchemaVersion = 10; const char *Database::kMagicAllSongsTables = "%allsongstables"; int Database::sNextConnectionId = 1; diff --git a/src/core/iconmapper.h b/src/core/iconmapper.h index 16273276..31377076 100644 --- a/src/core/iconmapper.h +++ b/src/core/iconmapper.h @@ -105,7 +105,6 @@ static const QMap iconmapper_ = { { "moodbar", { {"preferences-desktop-icons"}, 0, 0 } }, { "nvidia", { {}, 0, 0 } }, { "pulseaudio", { {}, 0, 0 } }, - { "qobuz", { {}, 0, 0 } }, { "realtek", { {}, 0, 0 } }, { "scrobble-disabled", { {}, 0, 0 } }, { "scrobble", { {}, 0, 0 } }, @@ -116,7 +115,6 @@ static const QMap iconmapper_ = { { "star", { {}, 0, 0 } }, { "strawberry", { {}, 0, 0 } }, { "subsonic", { {}, 0, 0 } }, - { "tidal", { {}, 0, 0 } }, { "tools-wizard", { {}, 0, 0 } }, { "view-choose", { {}, 0, 0 } }, { "view-fullscreen", { {}, 0, 0 } }, diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 95b06aa3..b6e117fd 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -144,13 +144,6 @@ #include "settings/behavioursettingspage.h" #include "settings/backendsettingspage.h" #include "settings/playlistsettingspage.h" -#ifdef HAVE_TIDAL -# include "tidal/tidalservice.h" -# include "settings/tidalsettingspage.h" -#endif -#ifdef HAVE_QOBUZ -# include "settings/qobuzsettingspage.h" -#endif #ifdef HAVE_SUBSONIC # include "settings/subsonicsettingspage.h" #endif @@ -230,12 +223,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co dialog->SetDestinationModel(app->collection()->model()->directory_model()); return dialog; }), -#ifdef HAVE_TIDAL - tidal_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Tidal), app_->tidal_search(), TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, this)), -#endif -#ifdef HAVE_QOBUZ - qobuz_view_(new InternetTabsView(app_, app->internet_services()->ServiceBySource(Song::Source_Qobuz), app_->qobuz_search(), QobuzSettingsPage::kSettingsGroup, SettingsDialog::Page_Qobuz, this)), -#endif #ifdef HAVE_SUBSONIC subsonic_view_(new InternetSongsView(app_, app->internet_services()->ServiceBySource(Song::Source_Subsonic), SubsonicSettingsPage::kSettingsGroup, SettingsDialog::Page_Subsonic, this)), #endif @@ -288,12 +275,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co #ifndef Q_OS_WIN ui_->tabs->AddTab(device_view_, "devices", IconLoader::Load("device"), tr("Devices")); #endif -#ifdef HAVE_TIDAL - ui_->tabs->AddTab(tidal_view_, "tidal", IconLoader::Load("tidal"), tr("Tidal")); -#endif -#ifdef HAVE_QOBUZ - ui_->tabs->AddTab(qobuz_view_, "qobuz", IconLoader::Load("qobuz"), tr("Qobuz")); -#endif #ifdef HAVE_SUBSONIC ui_->tabs->AddTab(subsonic_view_, "subsonic", IconLoader::Load("subsonic"), tr("Subsonic")); #endif @@ -578,22 +559,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co collection_view_->filter()->AddMenuAction(separator); collection_view_->filter()->AddMenuAction(collection_config_action); -#ifdef HAVE_TIDAL - connect(tidal_view_->artists_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); - connect(tidal_view_->albums_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); - connect(tidal_view_->songs_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); - connect(tidal_view_->search_view(), SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); - if (TidalService *tidalservice = qobject_cast (app_->internet_services()->ServiceBySource(Song::Source_Tidal))) - connect(this, SIGNAL(AuthorisationUrlReceived(const QUrl&)), tidalservice, SLOT(AuthorisationUrlReceived(const QUrl&))); -#endif - -#ifdef HAVE_QOBUZ - connect(qobuz_view_->artists_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); - connect(qobuz_view_->albums_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); - connect(qobuz_view_->songs_collection_view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); - connect(qobuz_view_->search_view(), SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); -#endif - #ifdef HAVE_SUBSONIC connect(subsonic_view_->view(), SIGNAL(AddToPlaylistSignal(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); #endif @@ -916,26 +881,6 @@ void MainWindow::ReloadSettings() { } } -#ifdef HAVE_TIDAL - settings.beginGroup(TidalSettingsPage::kSettingsGroup); - bool enable_tidal = settings.value("enabled", false).toBool(); - settings.endGroup(); - if (enable_tidal) - ui_->tabs->EnableTab(tidal_view_); - else - ui_->tabs->DisableTab(tidal_view_); -#endif - -#ifdef HAVE_QOBUZ - settings.beginGroup(QobuzSettingsPage::kSettingsGroup); - bool enable_qobuz = settings.value("enabled", false).toBool(); - settings.endGroup(); - if (enable_qobuz) - ui_->tabs->EnableTab(qobuz_view_); - else - ui_->tabs->DisableTab(qobuz_view_); -#endif - #ifdef HAVE_SUBSONIC settings.beginGroup(SubsonicSettingsPage::kSettingsGroup); bool enable_subsonic = settings.value("enabled", false).toBool(); @@ -966,12 +911,6 @@ void MainWindow::ReloadAllSettings() { album_cover_choice_controller_->ReloadSettings(); if (cover_manager_.get()) cover_manager_->ReloadSettings(); context_view_->ReloadSettings(); -#ifdef HAVE_TIDAL - tidal_view_->ReloadSettings(); -#endif -#ifdef HAVE_QOBUZ - qobuz_view_->ReloadSettings(); -#endif #ifdef HAVE_SUBSONIC subsonic_view_->ReloadSettings(); #endif @@ -2043,14 +1982,6 @@ void MainWindow::CommandlineOptionsReceived(const CommandlineOptions &options) { if (!options.urls().empty()) { -#ifdef HAVE_TIDAL - for (const QUrl url : options.urls()) { - if (url.scheme() == "tidal" && url.host() == "login") { - emit AuthorisationUrlReceived(url); - return; - } - } -#endif MimeData *data = new MimeData; data->setUrls(options.urls()); // Behaviour depends on command line options, so set it here diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index 38d6dea6..24d2e871 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -315,8 +315,6 @@ class MainWindow : public QMainWindow, public PlatformInterface { std::unique_ptr track_selection_dialog_; PlaylistItemList autocomplete_tag_items_; - InternetTabsView *tidal_view_; - InternetTabsView *qobuz_view_; InternetSongsView *subsonic_view_; QAction *collection_show_all_; diff --git a/src/covermanager/tidalcoverprovider.cpp b/src/covermanager/tidalcoverprovider.cpp deleted file mode 100644 index 562515bb..00000000 --- a/src/covermanager/tidalcoverprovider.cpp +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/application.h" -#include "core/closure.h" -#include "core/network.h" -#include "core/logging.h" -#include "core/song.h" -#include "internet/internetservices.h" -#include "tidal/tidalservice.h" -#include "albumcoverfetcher.h" -#include "coverprovider.h" -#include "tidalcoverprovider.h" - -const char *TidalCoverProvider::kApiUrl = "https://api.tidalhifi.com/v1"; -const char *TidalCoverProvider::kResourcesUrl = "https://resources.tidal.com"; -const int TidalCoverProvider::kLimit = 10; - -TidalCoverProvider::TidalCoverProvider(Application *app, QObject *parent) : - CoverProvider("Tidal", 2.0, true, app, parent), - service_(app->internet_services()->Service()), - network_(new NetworkAccessManager(this)) { - -} - -bool TidalCoverProvider::StartSearch(const QString &artist, const QString &album, const int id) { - - if (!service_ || !service_->authenticated()) return false; - - ParamList params = ParamList() << Param("query", QString(artist + " " + album)) - << Param("limit", QString::number(kLimit)); - - QNetworkReply *reply = CreateRequest("search/albums", params); - NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleSearchReply(QNetworkReply*, int)), reply, id); - - return true; - -} - -void TidalCoverProvider::CancelSearch(int id) { Q_UNUSED(id); } - -QNetworkReply *TidalCoverProvider::CreateRequest(const QString &ressource_name, const ParamList ¶ms_supplied) { - - const ParamList params = ParamList() << params_supplied - << Param("countryCode", service_->country_code()); - - QUrlQuery url_query; - for (const Param ¶m : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QUrl url(kApiUrl + QString("/") + ressource_name); - url.setQuery(url_query); - QNetworkRequest req(url); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -#endif - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - if (!service_->access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + service_->access_token().toUtf8()); - if (!service_->session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", service_->session_id().toUtf8()); - QNetworkReply *reply = network_->get(req); - - return reply; - -} - -QByteArray TidalCoverProvider::GetReplyData(QNetworkReply *reply, QString &error) { - - 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 = Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - } - else { - // See if there is Json data containing "status" and "userMessage" - then use that instead. - data = reply->readAll(); - QJsonParseError parse_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); - int status = 0; - int sub_status = 0; - if (parse_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { - QJsonObject json_obj = json_doc.object(); - if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) { - status = json_obj["status"].toInt(); - sub_status = json_obj["subStatus"].toInt(); - QString user_message = json_obj["userMessage"].toString(); - error = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); - } - } - 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()); - } - } - if (status == 401 && sub_status == 6001) { // User does not have a valid session - service_->Logout(); - } - error = Error(error); - } - return QByteArray(); - } - - return data; - -} - -QJsonObject TidalCoverProvider::ExtractJsonObj(QByteArray &data, QString &error) { - - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - error = Error("Reply from server missing Json data.", data); - return QJsonObject(); - } - - if (json_doc.isEmpty()) { - error = Error("Received empty Json document.", data); - return QJsonObject(); - } - - if (!json_doc.isObject()) { - error = Error("Json document is not an object.", json_doc); - return QJsonObject(); - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - error = Error("Received empty Json object.", json_doc); - return QJsonObject(); - } - - return json_obj; - -} - -QJsonValue TidalCoverProvider::ExtractItems(QByteArray &data, QString &error) { - - QJsonObject json_obj = ExtractJsonObj(data, error); - if (json_obj.isEmpty()) return QJsonValue(); - return ExtractItems(json_obj, error); - -} - -QJsonValue TidalCoverProvider::ExtractItems(QJsonObject &json_obj, QString &error) { - - if (!json_obj.contains("items")) { - error = Error("Json reply is missing items.", json_obj); - return QJsonArray(); - } - QJsonValue json_items = json_obj["items"]; - return json_items; - -} - -void TidalCoverProvider::HandleSearchReply(QNetworkReply *reply, const int id) { - - reply->deleteLater(); - - CoverSearchResults results; - QString error; - - QByteArray data = GetReplyData(reply, error); - if (data.isEmpty()) { - emit SearchFinished(id, results); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data, error); - if (json_obj.isEmpty()) { - emit SearchFinished(id, results); - return; - } - - QJsonValue json_value = ExtractItems(json_obj, error); - if (!json_value.isArray()) { - emit SearchFinished(id, results); - return; - } - QJsonArray json_items = json_value.toArray(); - if (json_items.isEmpty()) { - emit SearchFinished(id, results); - return; - } - - for (const QJsonValue &value : json_items) { - if (!value.isObject()) { - Error("Invalid Json reply, item not a object.", value); - continue; - } - QJsonObject json_obj = value.toObject(); - - if (!json_obj.contains("artist") || !json_obj.contains("type") || !json_obj.contains("id") || !json_obj.contains("title") || !json_obj.contains("cover")) { - Error("Invalid Json reply, item missing id, type, album or cover.", json_obj); - continue; - } - QString album = json_obj["title"].toString(); - QString cover = json_obj["cover"].toString(); - - QJsonValue json_value_artist = json_obj["artist"]; - if (!json_value_artist.isObject()) { - Error("Invalid Json reply, item artist is not a object.", json_value_artist); - continue; - } - QJsonObject json_artist = json_value_artist.toObject(); - if (!json_artist.contains("name")) { - Error("Invalid Json reply, item artist missing name.", json_artist); - continue; - } - QString artist = json_artist["name"].toString(); - - album.remove(Song::kAlbumRemoveDisc); - album.remove(Song::kAlbumRemoveMisc); - - cover = cover.replace("-", "/"); - QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg("1280x1280")); - - CoverSearchResult cover_result; - cover_result.artist = artist; - cover_result.album = album; - cover_result.image_url = cover_url; - results << cover_result; - - } - emit SearchFinished(id, results); - -} - -QString TidalCoverProvider::Error(QString error, QVariant debug) { - qLog(Error) << "Tidal:" << error; - if (debug.isValid()) qLog(Debug) << debug; - return error; -} diff --git a/src/covermanager/tidalcoverprovider.h b/src/covermanager/tidalcoverprovider.h deleted file mode 100644 index 9eb3ee2e..00000000 --- a/src/covermanager/tidalcoverprovider.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef TIDALCOVERPROVIDER_H -#define TIDALCOVERPROVIDER_H - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "coverprovider.h" - -class QNetworkAccessManager; -class QNetworkReply; -class Application; -class TidalService; - -class TidalCoverProvider : public CoverProvider { - Q_OBJECT - - public: - explicit TidalCoverProvider(Application *app, QObject *parent = nullptr); - bool StartSearch(const QString &artist, const QString &album, const int id); - void CancelSearch(int id); - - private slots: - void HandleSearchReply(QNetworkReply *reply, const int id); - - private: - typedef QPair Param; - typedef QList ParamList; - typedef QPair EncodedParam; - static const char *kApiUrl; - static const char *kResourcesUrl; - static const int kLimit; - - QNetworkReply *CreateRequest(const QString &ressource_name, const ParamList ¶ms_supplied); - QByteArray GetReplyData(QNetworkReply *reply, QString &error); - QJsonObject ExtractJsonObj(QByteArray &data, QString &error); - QJsonValue ExtractItems(QByteArray &data, QString &error); - QJsonValue ExtractItems(QJsonObject &json_obj, QString &error); - QString Error(QString error, QVariant debug = QVariant()); - - TidalService *service_; - QNetworkAccessManager *network_; - -}; - -#endif // TIDALCOVERPROVIDER_H diff --git a/src/qobuz/qobuzbaserequest.cpp b/src/qobuz/qobuzbaserequest.cpp deleted file mode 100644 index dcb74c19..00000000 --- a/src/qobuz/qobuzbaserequest.cpp +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/network.h" -#include "qobuzservice.h" -#include "qobuzbaserequest.h" - -const char *QobuzBaseRequest::kApiUrl = "https://www.qobuz.com/api.json/0.2"; - -QobuzBaseRequest::QobuzBaseRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent) : - QObject(parent), - service_(service), - network_(network) - {} - -QobuzBaseRequest::~QobuzBaseRequest() {} - -QNetworkReply *QobuzBaseRequest::CreateRequest(const QString &ressource_name, const QList ¶ms_provided) { - - ParamList params = ParamList() << params_provided - << Param("app_id", app_id()); - - std::sort(params.begin(), params.end()); - - QUrlQuery url_query; - for (const Param& param : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QUrl url(kApiUrl + QString("/") + ressource_name); - url.setQuery(url_query); - QNetworkRequest req(url); - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - req.setRawHeader("X-App-Id", app_id().toUtf8()); - if (authenticated()) - req.setRawHeader("X-User-Auth-Token", user_auth_token().toUtf8()); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -#endif - - QNetworkReply *reply = network_->get(req); - connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleSSLErrors(QList))); - - //qLog(Debug) << "Qobuz: Sending request" << url; - - return reply; - -} - -void QobuzBaseRequest::HandleSSLErrors(QList ssl_errors) { - - for (QSslError &ssl_error : ssl_errors) { - Error(ssl_error.errorString()); - } - -} - -QByteArray QobuzBaseRequest::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 { - // See if there is Json data containing "status", "code" and "message" - then use that instead. - data = reply->readAll(); - QString error; - QJsonParseError parse_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &parse_error); - if (parse_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) { - QJsonObject json_obj = json_doc.object(); - if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("code") && json_obj.contains("message")) { - QString status = json_obj["status"].toString(); - int code = json_obj["code"].toInt(); - QString message = json_obj["message"].toString(); - error = QString("%1 (%2)").arg(message).arg(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 QobuzBaseRequest::ExtractJsonObj(QByteArray &data) { - - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - Error("Reply from server missing Json data.", data); - return QJsonObject(); - } - - if (json_doc.isEmpty()) { - Error("Received empty Json document.", data); - 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; - -} - -QJsonValue QobuzBaseRequest::ExtractItems(QByteArray &data) { - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) return QJsonValue(); - return ExtractItems(json_obj); - -} - -QJsonValue QobuzBaseRequest::ExtractItems(QJsonObject &json_obj) { - - if (!json_obj.contains("items")) { - Error("Json reply is missing items.", json_obj); - return QJsonArray(); - } - QJsonValue json_items = json_obj["items"]; - return json_items; - -} - -QString QobuzBaseRequest::ErrorsToHTML(const QStringList &errors) { - - QString error_html; - for (const QString &error : errors) { - error_html += error + "
    "; - } - return error_html; - -} diff --git a/src/qobuz/qobuzbaserequest.h b/src/qobuz/qobuzbaserequest.h deleted file mode 100644 index aea92e31..00000000 --- a/src/qobuz/qobuzbaserequest.h +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef QOBUZBASEREQUEST_H -#define QOBUZBASEREQUEST_H - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/song.h" -#include "qobuzservice.h" - -class QNetworkReply; -class NetworkAccessManager; - -class QobuzBaseRequest : public QObject { - Q_OBJECT - - public: - - enum QueryType { - QueryType_None, - QueryType_Artists, - QueryType_Albums, - QueryType_Songs, - QueryType_SearchArtists, - QueryType_SearchAlbums, - QueryType_SearchSongs, - QueryType_StreamURL, - }; - - QobuzBaseRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent); - ~QobuzBaseRequest(); - - typedef QPair Param; - typedef QList ParamList; - - typedef QPair EncodedParam; - typedef QList EncodedParamList; - - QNetworkReply *CreateRequest(const QString &ressource_name, const QList ¶ms_provided); - QByteArray GetReplyData(QNetworkReply *reply); - QJsonObject ExtractJsonObj(QByteArray &data); - QJsonValue ExtractItems(QByteArray &data); - QJsonValue ExtractItems(QJsonObject &json_obj); - - virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; - QString ErrorsToHTML(const QStringList &errors); - - QString api_url() { return QString(kApiUrl); } - QString app_id() { return service_->app_id(); } - QString app_secret() { return service_->app_secret(); } - QString username() { return service_->username(); } - QString password() { return service_->password(); } - int format() { return service_->format(); } - int artistssearchlimit() { return service_->artistssearchlimit(); } - int albumssearchlimit() { return service_->albumssearchlimit(); } - int songssearchlimit() { return service_->songssearchlimit(); } - - qint64 user_id() { return service_->user_id(); } - QString user_auth_token() { return service_->user_auth_token(); } - QString device_id() { return service_->device_id(); } - qint64 credential_id() { return service_->credential_id(); } - - bool authenticated() { return service_->authenticated(); } - bool login_sent() { return service_->login_sent(); } - int max_login_attempts() { return service_->max_login_attempts(); } - int login_attempts() { return service_->login_attempts(); } - - private slots: - void HandleSSLErrors(QList ssl_errors); - - private: - - static const char *kApiUrl; - - QobuzService *service_; - NetworkAccessManager *network_; - -}; - -#endif // QOBUZBASEREQUEST_H diff --git a/src/qobuz/qobuzfavoriterequest.cpp b/src/qobuz/qobuzfavoriterequest.cpp deleted file mode 100644 index 1b479a4b..00000000 --- a/src/qobuz/qobuzfavoriterequest.cpp +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/logging.h" -#include "core/network.h" -#include "core/closure.h" -#include "core/song.h" -#include "qobuzservice.h" -#include "qobuzbaserequest.h" -#include "qobuzfavoriterequest.h" - -QobuzFavoriteRequest::QobuzFavoriteRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent) - : QobuzBaseRequest(service, network, parent), - service_(service), - network_(network) {} - -QobuzFavoriteRequest::~QobuzFavoriteRequest() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - disconnect(reply, 0, this, 0); - reply->abort(); - reply->deleteLater(); - } - -} - -QString QobuzFavoriteRequest::FavoriteText(const FavoriteType type) { - - switch (type) { - case FavoriteType_Artists: - return "artists"; - case FavoriteType_Albums: - return "albums"; - case FavoriteType_Songs: - default: - return "tracks"; - } - -} - -void QobuzFavoriteRequest::AddArtists(const SongList &songs) { - AddFavorites(FavoriteType_Artists, songs); -} - -void QobuzFavoriteRequest::AddAlbums(const SongList &songs) { - AddFavorites(FavoriteType_Albums, songs); -} - -void QobuzFavoriteRequest::AddSongs(const SongList &songs) { - AddFavorites(FavoriteType_Songs, songs); -} - -void QobuzFavoriteRequest::AddFavorites(const FavoriteType type, const SongList &songs) { - - if (songs.isEmpty()) return; - - QString text; - switch (type) { - case FavoriteType_Artists: - text = "artist_ids"; - break; - case FavoriteType_Albums: - text = "album_ids"; - break; - case FavoriteType_Songs: - text = "track_ids"; - break; - } - - QStringList ids_list; - for (const Song &song : songs) { - QString id; - switch (type) { - case FavoriteType_Artists: - if (song.artist_id() <= 0) continue; - id = QString::number(song.artist_id()); - break; - case FavoriteType_Albums: - if (song.album_id().isEmpty()) continue; - id = song.album_id(); - break; - case FavoriteType_Songs: - if (song.song_id() <= 0) continue; - id = QString::number(song.song_id()); - break; - } - if (id.isEmpty()) continue; - if (!ids_list.contains(id)) { - ids_list << id; - } - } - if (ids_list.isEmpty()) return; - - QString ids = ids_list.join(','); - - typedef QPair EncodedParam; - - ParamList params = ParamList() << Param("app_id", app_id()) - << Param("user_auth_token", user_auth_token()) - << Param(text, ids); - - QUrlQuery url_query; - for (const Param& param : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QNetworkReply *reply = CreateRequest("favorite/create", params); - NewClosure(reply, SIGNAL(finished()), this, SLOT(AddFavoritesReply(QNetworkReply*, const FavoriteType, const SongList&)), reply, type, songs); - replies_ << reply; - -} - -void QobuzFavoriteRequest::AddFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs) { - - if (replies_.contains(reply)) { - replies_.removeAll(reply); - reply->deleteLater(); - } - else { - return; - } - - QByteArray data = GetReplyData(reply); - - if (reply->error() != QNetworkReply::NoError) { - return; - } - - qLog(Debug) << "Qobuz:" << songs.count() << "songs added to" << FavoriteText(type) << "favorites."; - - switch (type) { - case FavoriteType_Artists: - emit ArtistsAdded(songs); - break; - case FavoriteType_Albums: - emit AlbumsAdded(songs); - break; - case FavoriteType_Songs: - emit SongsAdded(songs); - break; - } - -} - -void QobuzFavoriteRequest::RemoveArtists(const SongList &songs) { - RemoveFavorites(FavoriteType_Artists, songs); -} - -void QobuzFavoriteRequest::RemoveAlbums(const SongList &songs) { - RemoveFavorites(FavoriteType_Albums, songs); -} - -void QobuzFavoriteRequest::RemoveSongs(const SongList &songs) { - RemoveFavorites(FavoriteType_Songs, songs); -} - -void QobuzFavoriteRequest::RemoveFavorites(const FavoriteType type, const SongList &songs) { - - if (songs.isEmpty()) return; - - QString text; - switch (type) { - case FavoriteType_Artists: - text = "artist_ids"; - break; - case FavoriteType_Albums: - text = "album_ids"; - break; - case FavoriteType_Songs: - text = "track_ids"; - break; - } - - QStringList ids_list; - for (const Song &song : songs) { - QString id; - switch (type) { - case FavoriteType_Artists: - if (song.artist_id() <= 0) continue; - id = QString::number(song.artist_id()); - break; - case FavoriteType_Albums: - if (song.album_id().isEmpty()) continue; - id = song.album_id(); - break; - case FavoriteType_Songs: - if (song.song_id() <= 0) continue; - id = QString::number(song.song_id()); - break; - } - if (id.isEmpty()) continue; - if (!ids_list.contains(id)) { - ids_list << id; - } - } - if (ids_list.isEmpty()) return; - - QString ids = ids_list.join(','); - - ParamList params = ParamList() << Param("app_id", app_id()) - << Param("user_auth_token", user_auth_token()) - << Param(text, ids); - - QUrlQuery url_query; - for (const Param ¶m : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QNetworkReply *reply = CreateRequest("favorite/delete", params); - NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoveFavoritesReply(QNetworkReply*, const FavoriteType, const SongList&)), reply, type, songs); - replies_ << reply; - -} - -void QobuzFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs) { - - if (replies_.contains(reply)) { - replies_.removeAll(reply); - reply->deleteLater(); - } - else { - return; - } - - QByteArray data = GetReplyData(reply); - if (reply->error() != QNetworkReply::NoError) { - return; - } - - qLog(Debug) << "Qobuz:" << songs.count() << "songs removed from" << FavoriteText(type) << "favorites."; - - switch (type) { - case FavoriteType_Artists: - emit ArtistsRemoved(songs); - break; - case FavoriteType_Albums: - emit AlbumsRemoved(songs); - break; - case FavoriteType_Songs: - emit SongsRemoved(songs); - break; - } - -} - -void QobuzFavoriteRequest::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "Qobuz:" << error; - if (debug.isValid()) qLog(Debug) << debug; - -} diff --git a/src/qobuz/qobuzfavoriterequest.h b/src/qobuz/qobuzfavoriterequest.h deleted file mode 100644 index d176c833..00000000 --- a/src/qobuz/qobuzfavoriterequest.h +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef QOBUZFAVORITEREQUEST_H -#define QOBUZFAVORITEREQUEST_H - -#include "config.h" - -#include -#include -#include -#include - -#include "qobuzbaserequest.h" -#include "core/song.h" - -class QNetworkReply; -class QobuzService; -class NetworkAccessManager; - -class QobuzFavoriteRequest : public QobuzBaseRequest { - Q_OBJECT - - public: - QobuzFavoriteRequest(QobuzService *service, NetworkAccessManager *network, QObject *parent); - ~QobuzFavoriteRequest(); - - enum FavoriteType { - FavoriteType_Artists, - FavoriteType_Albums, - FavoriteType_Songs - }; - - signals: - void ArtistsAdded(const SongList &songs); - void AlbumsAdded(const SongList &songs); - void SongsAdded(const SongList &songs); - void ArtistsRemoved(const SongList &songs); - void AlbumsRemoved(const SongList &songs); - void SongsRemoved(const SongList &songs); - - private slots: - void AddArtists(const SongList &songs); - void AddAlbums(const SongList &songs); - void AddSongs(const SongList &songs); - - void RemoveArtists(const SongList &songs); - void RemoveAlbums(const SongList &songs); - void RemoveSongs(const SongList &songs); - - void AddFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs); - void RemoveFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs); - - private: - void Error(const QString &error, const QVariant &debug = QVariant()); - QString FavoriteText(const FavoriteType type); - void AddFavorites(const FavoriteType type, const SongList &songs); - void RemoveFavorites(const FavoriteType type, const SongList &songs); - - QobuzService *service_; - NetworkAccessManager *network_; - QList replies_; - -}; - -#endif // QOBUZFAVORITEREQUEST_H diff --git a/src/qobuz/qobuzrequest.cpp b/src/qobuz/qobuzrequest.cpp deleted file mode 100644 index f4569756..00000000 --- a/src/qobuz/qobuzrequest.cpp +++ /dev/null @@ -1,1265 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/closure.h" -#include "core/logging.h" -#include "core/network.h" -#include "core/song.h" -#include "core/timeconstants.h" -#include "core/application.h" -#include "covermanager/albumcoverloader.h" -#include "qobuzservice.h" -#include "qobuzurlhandler.h" -#include "qobuzbaserequest.h" -#include "qobuzrequest.h" - -const int QobuzRequest::kMaxConcurrentArtistsRequests = 3; -const int QobuzRequest::kMaxConcurrentAlbumsRequests = 3; -const int QobuzRequest::kMaxConcurrentSongsRequests = 3; -const int QobuzRequest::kMaxConcurrentArtistAlbumsRequests = 3; -const int QobuzRequest::kMaxConcurrentAlbumSongsRequests = 3; -const int QobuzRequest::kMaxConcurrentAlbumCoverRequests = 1; - -QobuzRequest::QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent) - : QobuzBaseRequest(service, network, parent), - service_(service), - url_handler_(url_handler), - app_(app), - network_(network), - type_(type), - query_id_(-1), - finished_(false), - artists_requests_active_(0), - artists_total_(0), - artists_received_(0), - albums_requests_active_(0), - songs_requests_active_(0), - artist_albums_requests_active_(0), - artist_albums_requested_(0), - artist_albums_received_(0), - album_songs_requests_active_(0), - album_songs_requested_(0), - album_songs_received_(0), - album_covers_requests_active_(), - album_covers_requested_(0), - album_covers_received_(0), - no_results_(false) {} - -QobuzRequest::~QobuzRequest() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - disconnect(reply, 0, this, 0); - if (reply->isRunning()) reply->abort(); - reply->deleteLater(); - } - - while (!album_cover_replies_.isEmpty()) { - QNetworkReply *reply = album_cover_replies_.takeFirst(); - disconnect(reply, 0, this, 0); - if (reply->isRunning()) reply->abort(); - reply->deleteLater(); - } - -} - -void QobuzRequest::Process() { - - switch (type_) { - case QueryType::QueryType_Artists: - GetArtists(); - break; - case QueryType::QueryType_Albums: - GetAlbums(); - break; - case QueryType::QueryType_Songs: - GetSongs(); - break; - case QueryType::QueryType_SearchArtists: - ArtistsSearch(); - break; - case QueryType::QueryType_SearchAlbums: - AlbumsSearch(); - break; - case QueryType::QueryType_SearchSongs: - SongsSearch(); - break; - default: - Error("Invalid query type."); - break; - } - -} - -void QobuzRequest::Search(const int query_id, const QString &search_text) { - query_id_ = query_id; - search_text_ = search_text; -} - -void QobuzRequest::GetArtists() { - - emit UpdateStatus(query_id_, tr("Retrieving artists...")); - emit UpdateProgress(query_id_, 0); - AddArtistsRequest(); - -} - -void QobuzRequest::AddArtistsRequest(const int offset, const int limit) { - - Request request; - request.limit = limit; - request.offset = offset; - artists_requests_queue_.enqueue(request); - if (artists_requests_active_ < kMaxConcurrentArtistsRequests) FlushArtistsRequests(); - -} - -void QobuzRequest::FlushArtistsRequests() { - - while (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) { - - Request request = artists_requests_queue_.dequeue(); - ++artists_requests_active_; - - ParamList params; - if (type_ == QueryType_Artists) { - params << Param("type", "artists"); - params << Param("user_auth_token", user_auth_token()); - } - else if (type_ == QueryType_SearchArtists) params << Param("query", search_text_); - if (request.limit > 0) params << Param("limit", QString::number(request.limit)); - if (request.offset > 0) params << Param("offset", QString::number(request.offset)); - QNetworkReply *reply = nullptr; - if (type_ == QueryType_Artists) { - reply = CreateRequest(QString("favorite/getUserFavorites"), params); - } - else if (type_ == QueryType_SearchArtists) { - reply = CreateRequest("artist/search", params); - } - if (!reply) continue; - replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistsReplyReceived(QNetworkReply*, const int, const int)), reply, request.limit, request.offset); - - } - -} - -void QobuzRequest::GetAlbums() { - - emit UpdateStatus(query_id_, tr("Retrieving albums...")); - emit UpdateProgress(query_id_, 0); - AddAlbumsRequest(); - -} - -void QobuzRequest::AddAlbumsRequest(const int offset, const int limit) { - - Request request; - request.limit = limit; - request.offset = offset; - albums_requests_queue_.enqueue(request); - if (albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests(); - -} - -void QobuzRequest::FlushAlbumsRequests() { - - while (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) { - - Request request = albums_requests_queue_.dequeue(); - ++albums_requests_active_; - - ParamList params; - if (type_ == QueryType_Albums) { - params << Param("type", "albums"); - params << Param("user_auth_token", user_auth_token()); - } - else if (type_ == QueryType_SearchAlbums) params << Param("query", search_text_); - if (request.limit > 0) params << Param("limit", QString::number(request.limit)); - if (request.offset > 0) params << Param("offset", QString::number(request.offset)); - QNetworkReply *reply = nullptr; - if (type_ == QueryType_Albums) { - reply = CreateRequest(QString("favorite/getUserFavorites"), params); - } - else if (type_ == QueryType_SearchAlbums) { - reply = CreateRequest("album/search", params); - } - if (!reply) continue; - replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReplyReceived(QNetworkReply*, const int, const int)), reply, request.limit, request.offset); - - } - -} - -void QobuzRequest::GetSongs() { - - emit UpdateStatus(query_id_, tr("Retrieving songs...")); - emit UpdateProgress(query_id_, 0); - AddSongsRequest(); - -} - -void QobuzRequest::AddSongsRequest(const int offset, const int limit) { - - Request request; - request.limit = limit; - request.offset = offset; - songs_requests_queue_.enqueue(request); - if (songs_requests_active_ < kMaxConcurrentSongsRequests) FlushSongsRequests(); - -} - -void QobuzRequest::FlushSongsRequests() { - - while (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentSongsRequests) { - - Request request = songs_requests_queue_.dequeue(); - ++songs_requests_active_; - - ParamList params; - if (type_ == QueryType_Songs) { - params << Param("type", "tracks"); - params << Param("user_auth_token", user_auth_token()); - } - else if (type_ == QueryType_SearchSongs) params << Param("query", search_text_); - if (request.limit > 0) params << Param("limit", QString::number(request.limit)); - if (request.offset > 0) params << Param("offset", QString::number(request.offset)); - QNetworkReply *reply = nullptr; - if (type_ == QueryType_Songs) { - reply = CreateRequest(QString("favorite/getUserFavorites"), params); - } - else if (type_ == QueryType_SearchSongs) { - reply = CreateRequest("track/search", params); - } - if (!reply) continue; - replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(SongsReplyReceived(QNetworkReply*, const int, const int)), reply, request.limit, request.offset); - - } - -} - -void QobuzRequest::ArtistsSearch() { - - emit UpdateStatus(query_id_, tr("Searching...")); - emit UpdateProgress(query_id_, 0); - AddArtistsSearchRequest(); - -} - -void QobuzRequest::AddArtistsSearchRequest(const int offset) { - - AddArtistsRequest(offset, service_->artistssearchlimit()); - -} - -void QobuzRequest::AlbumsSearch() { - - emit UpdateStatus(query_id_, tr("Searching...")); - emit UpdateProgress(query_id_, 0); - AddAlbumsSearchRequest(); - -} - -void QobuzRequest::AddAlbumsSearchRequest(const int offset) { - - AddAlbumsRequest(offset, service_->albumssearchlimit()); - -} - -void QobuzRequest::SongsSearch() { - - emit UpdateStatus(query_id_, tr("Searching...")); - emit UpdateProgress(query_id_, 0); - AddSongsSearchRequest(); - -} - -void QobuzRequest::AddSongsSearchRequest(const int offset) { - - AddSongsRequest(offset, service_->songssearchlimit()); - -} - -void QobuzRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { - - if (!replies_.contains(reply)) return; - replies_.removeAll(reply); - reply->deleteLater(); - - QByteArray data = GetReplyData(reply); - - --artists_requests_active_; - - if (finished_) return; - - if (data.isEmpty()) { - ArtistsFinishCheck(); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - ArtistsFinishCheck(); - return; - } - - if (!json_obj.contains("artists")) { - ArtistsFinishCheck(); - Error("Json object is missing artists.", json_obj); - return; - } - QJsonValue json_artists = json_obj["artists"]; - if (!json_artists.isObject()) { - Error("Json artists is not an object.", json_obj); - ArtistsFinishCheck(); - return; - } - QJsonObject json_obj_artists = json_artists.toObject(); - - if (!json_obj_artists.contains("limit") || - !json_obj_artists.contains("offset") || - !json_obj_artists.contains("total") || - !json_obj_artists.contains("items")) { - ArtistsFinishCheck(); - Error("Json artists object is missing values.", json_obj); - return; - } - //int limit = json_obj_artists["limit"].toInt(); - int offset = json_obj_artists["offset"].toInt(); - int artists_total = json_obj_artists["total"].toInt(); - - if (offset_requested == 0) { - artists_total_ = artists_total; - } - else if (artists_total != artists_total_) { - Error(QString("total returned does not match previous total! %1 != %2").arg(artists_total).arg(artists_total_)); - ArtistsFinishCheck(); - return; - } - - if (offset != offset_requested) { - Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - ArtistsFinishCheck(); - return; - } - - if (offset_requested == 0) { - emit ProgressSetMaximum(query_id_, artists_total_); - emit UpdateProgress(query_id_, artists_received_); - } - - QJsonValue json_value = ExtractItems(json_obj_artists); - if (!json_value.isArray()) { - ArtistsFinishCheck(); - return; - } - - QJsonArray json_items = json_value.toArray(); - if (json_items.isEmpty()) { // Empty array means no results - if (offset_requested == 0) no_results_ = true; - ArtistsFinishCheck(); - return; - } - - int artists_received = 0; - for (const QJsonValue &value : json_items) { - - ++artists_received; - - if (!value.isObject()) { - Error("Invalid Json reply, item not a object.", value); - continue; - } - QJsonObject json_obj = value.toObject(); - - if (json_obj.contains("item")) { - QJsonValue json_item = json_obj["item"]; - if (!json_item.isObject()) { - Error("Invalid Json reply, item not a object.", json_item); - continue; - } - json_obj = json_item.toObject(); - } - - if (!json_obj.contains("id") || !json_obj.contains("name")) { - Error("Invalid Json reply, item missing id or album.", json_obj); - continue; - } - - qint64 artist_id = json_obj["id"].toInt(); - if (artist_albums_requests_pending_.contains(artist_id)) continue; - artist_albums_requests_pending_.append(artist_id); - - } - artists_received_ += artists_received; - - if (offset_requested != 0) emit UpdateProgress(query_id_, artists_received_); - - ArtistsFinishCheck(limit_requested, offset, artists_received); - -} - -void QobuzRequest::ArtistsFinishCheck(const int limit, const int offset, const int artists_received) { - - if (finished_) return; - - if ((limit == 0 || limit > artists_received) && artists_received_ < artists_total_) { - int offset_next = offset + artists_received; - if (offset_next > 0 && offset_next < artists_total_) { - if (type_ == QueryType_Artists) AddArtistsRequest(offset_next); - else if (type_ == QueryType_SearchArtists) AddArtistsSearchRequest(offset_next); - } - } - - if (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) FlushArtistsRequests(); - - if (artists_requests_queue_.isEmpty() && artists_requests_active_ <= 0) { // Artist query is finished, get all albums for all artists. - - // Get artist albums - for (qint64 artist_id : artist_albums_requests_pending_) { - AddArtistAlbumsRequest(artist_id); - ++artist_albums_requested_; - } - artist_albums_requests_pending_.clear(); - - if (artist_albums_requested_ > 0) { - if (artist_albums_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving albums for %1 artist...").arg(artist_albums_requested_)); - else emit UpdateStatus(query_id_, tr("Retrieving albums for %1 artists...").arg(artist_albums_requested_)); - emit ProgressSetMaximum(query_id_, artist_albums_requested_); - emit UpdateProgress(query_id_, 0); - } - - } - - FinishCheck(); - -} - -void QobuzRequest::AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { - --albums_requests_active_; - AlbumsReceived(reply, 0, limit_requested, offset_requested); - if (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests(); -} - -void QobuzRequest::AddArtistAlbumsRequest(const qint64 artist_id, const int offset) { - - Request request; - request.artist_id = artist_id; - request.offset = offset; - artist_albums_requests_queue_.enqueue(request); - if (artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests(); - -} - -void QobuzRequest::FlushArtistAlbumsRequests() { - - while (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) { - - Request request = artist_albums_requests_queue_.dequeue(); - ++artist_albums_requests_active_; - - ParamList params = ParamList() << Param("artist_id", QString::number(request.artist_id)) - << Param("extra", "albums"); - - if (request.offset > 0) params << Param("offset", QString::number(request.offset)); - QNetworkReply *reply = CreateRequest(QString("artist/get"), params); - replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistAlbumsReplyReceived(QNetworkReply*, const qint64, const int)), reply, request.artist_id, request.offset); - - } - -} - -void QobuzRequest::ArtistAlbumsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const int offset_requested) { - - --artist_albums_requests_active_; - ++artist_albums_received_; - emit UpdateProgress(query_id_, artist_albums_received_); - AlbumsReceived(reply, artist_id, 0, offset_requested); - if (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests(); - -} - -void QobuzRequest::AlbumsReceived(QNetworkReply *reply, const qint64 artist_id_requested, const int limit_requested, const int offset_requested) { - - if (!replies_.contains(reply)) return; - replies_.removeAll(reply); - reply->deleteLater(); - - QByteArray data = GetReplyData(reply); - - if (finished_) return; - - if (data.isEmpty()) { - AlbumsFinishCheck(artist_id_requested); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - AlbumsFinishCheck(artist_id_requested); - return; - } - - qint64 album_artist_id = artist_id_requested; - if (json_obj.contains("id")) { - album_artist_id = json_obj["id"].toInt(); - } - QString album_artist; - if (json_obj.contains("name")) { - album_artist = json_obj["name"].toString(); - } - - if (album_artist_id != artist_id_requested) { - AlbumsFinishCheck(artist_id_requested); - Error("Artist id returned does not match artist id requested.", json_obj); - return; - } - - if (!json_obj.contains("albums")) { - AlbumsFinishCheck(artist_id_requested); - Error("Json object is missing albums.", json_obj); - return; - } - QJsonValue json_albums = json_obj["albums"]; - if (!json_albums.isObject()) { - Error("Json albums is not an object.", json_obj); - AlbumsFinishCheck(artist_id_requested); - return; - } - QJsonObject json_obj_albums = json_albums.toObject(); - - if (!json_obj_albums.contains("limit") || - !json_obj_albums.contains("offset") || - !json_obj_albums.contains("total") || - !json_obj_albums.contains("items")) { - AlbumsFinishCheck(artist_id_requested); - Error("Json albums object is missing values.", json_obj); - return; - } - - //int limit = json_obj["limit"].toInt(); - int offset = json_obj["offset"].toInt(); - int albums_total = json_obj["total"].toInt(); - - if (offset != offset_requested) { - Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - AlbumsFinishCheck(artist_id_requested); - return; - } - - QJsonValue json_value = ExtractItems(json_obj_albums); - if (!json_value.isArray()) { - AlbumsFinishCheck(artist_id_requested); - return; - } - QJsonArray json_items = json_value.toArray(); - if (json_items.isEmpty()) { - if ((type_ == QueryType_Albums || type_ == QueryType_SearchAlbums) && offset_requested == 0) { - no_results_ = true; - } - AlbumsFinishCheck(artist_id_requested); - return; - } - - int albums_received = 0; - for (const QJsonValue &value : json_items) { - - ++albums_received; - - if (!value.isObject()) { - Error("Invalid Json reply, item not a object.", value); - continue; - } - QJsonObject json_obj = value.toObject(); - - if (!json_obj.contains("artist") || !json_obj.contains("title") || !json_obj.contains("id")) { - Error("Invalid Json reply, item missing artist, title or id.", json_obj); - continue; - } - - QString album_id = json_obj["id"].toString(); - if (album_songs_requests_pending_.contains(album_id)) continue; - - QString album = json_obj["title"].toString(); - - QJsonValue json_value_artist = json_obj["artist"]; - if (!json_value_artist.isObject()) { - Error("Invalid Json reply, item artist is not a object.", json_value_artist); - continue; - } - QJsonObject json_artist = json_value_artist.toObject(); - if (!json_artist.contains("id") || !json_artist.contains("name")) { - Error("Invalid Json reply, item artist missing id or name.", json_artist); - continue; - } - - qint64 artist_id = json_artist["id"].toInt(); - QString artist = json_artist["name"].toString(); - if (artist_id_requested != 0 && artist_id != artist_id_requested) { - qLog(Debug) << "Skipping artist" << "artist" << artist << artist_id << "does not match album artist" << album_artist_id << album_artist; - continue; - } - - Request request; - request.artist_id = artist_id; - request.album_id = album_id; - request.album_artist = artist; - request.album = album; - album_songs_requests_pending_.insert(album_id, request); - - } - - AlbumsFinishCheck(artist_id_requested, limit_requested, offset, albums_total, albums_received); - -} - -void QobuzRequest::AlbumsFinishCheck(const qint64 artist_id, const int limit, const int offset, const int albums_total, const int albums_received) { - - if (finished_) return; - - if (limit == 0 || limit > albums_received) { - int offset_next = offset + albums_received; - if (offset_next > 0 && offset_next < albums_total) { - switch (type_) { - case QueryType_Albums: - AddAlbumsRequest(offset_next); - break; - case QueryType_SearchAlbums: - AddAlbumsSearchRequest(offset_next); - break; - case QueryType_Artists: - case QueryType_SearchArtists: - AddArtistAlbumsRequest(artist_id, offset_next); - break; - default: - break; - } - } - } - - if ( - albums_requests_queue_.isEmpty() && - albums_requests_active_ <= 0 && - artist_albums_requests_queue_.isEmpty() && - artist_albums_requests_active_ <= 0 - ) { // Artist albums query is finished, get all songs for all albums. - - // Get songs for all the albums. - - QHash ::iterator i; - for (i = album_songs_requests_pending_.begin() ; i != album_songs_requests_pending_.end() ; ++i) { - Request request = i.value(); - AddAlbumSongsRequest(request.artist_id, request.album_id, request.album_artist, request.album); - } - album_songs_requests_pending_.clear(); - - if (album_songs_requested_ > 0) { - if (album_songs_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving songs for %1 album...").arg(album_songs_requested_)); - else emit UpdateStatus(query_id_, tr("Retrieving songs for %1 albums...").arg(album_songs_requested_)); - emit ProgressSetMaximum(query_id_, album_songs_requested_); - emit UpdateProgress(query_id_, 0); - } - } - - FinishCheck(); - -} - -void QobuzRequest::SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { - - --songs_requests_active_; - SongsReceived(reply, 0, 0, limit_requested, offset_requested); - -} - -void QobuzRequest::AddAlbumSongsRequest(const qint64 artist_id, const QString &album_id, const QString &album_artist, const QString &album, const int offset) { - - Request request; - request.artist_id = artist_id; - request.album_id = album_id; - request.album_artist = album_artist; - request.album = album; - request.offset = offset; - album_songs_requests_queue_.enqueue(request); - ++album_songs_requested_; - if (album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests(); - -} - -void QobuzRequest::FlushAlbumSongsRequests() { - - while (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) { - - Request request = album_songs_requests_queue_.dequeue(); - ++album_songs_requests_active_; - ParamList params = ParamList() << Param("album_id", request.album_id); - if (request.offset > 0) params << Param("offset", QString::number(request.offset)); - QNetworkReply *reply = CreateRequest(QString("album/get"), params); - replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumSongsReplyReceived(QNetworkReply*, const qint64, const QString&, const int, const QString&, const QString&)), reply, request.artist_id, request.album_id, request.offset, request.album_artist, request.album); - - } - -} - -void QobuzRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const QString &album_id, const int offset_requested, const QString &album_artist, const QString &album) { - - --album_songs_requests_active_; - ++album_songs_received_; - if (offset_requested == 0) { - emit UpdateProgress(query_id_, album_songs_received_); - } - SongsReceived(reply, artist_id, album_id, 0, offset_requested, album_artist, album); - -} - -void QobuzRequest::SongsReceived(QNetworkReply *reply, const qint64 artist_id_requested, const QString &album_id_requested, const int limit_requested, const int offset_requested, const QString &album_artist_requested, const QString &album_requested) { - - if (!replies_.contains(reply)) return; - replies_.removeAll(reply); - reply->deleteLater(); - - QByteArray data = GetReplyData(reply); - - if (finished_) return; - - if (data.isEmpty()) { - SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested, album_requested); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested, album_requested); - return; - } - - if (!json_obj.contains("tracks")) { - Error("Json object is missing tracks.", json_obj); - SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist_requested, album_requested); - return; - } - - qint64 artist_id = artist_id_requested; - QString album_artist = album_artist_requested; - QString album_id = album_id_requested; - QString album = album_requested; - QUrl cover_url; - - if (json_obj.contains("id")) { - album_id = json_obj["id"].toString(); - } - - if (json_obj.contains("title")) { - album = json_obj["title"].toString(); - } - - if (json_obj.contains("artist")) { - QJsonValue json_artist = json_obj["artist"]; - if (!json_artist.isObject()) { - Error("Invalid Json reply, album artist is not a object.", json_artist); - SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); - return; - } - QJsonObject json_obj_artist = json_artist.toObject(); - if (!json_obj_artist.contains("id") || !json_obj_artist.contains("name")) { - Error("Invalid Json reply, album artist is missing id or name.", json_obj_artist); - SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); - return; - } - artist_id = json_obj_artist["id"].toInt(); - album_artist = json_obj_artist["name"].toString(); - } - - if (json_obj.contains("image")) { - QJsonValue json_image = json_obj["image"]; - if (!json_image.isObject()) { - Error("Invalid Json reply, album image is not a object.", json_image); - SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); - return; - } - QJsonObject json_obj_image = json_image.toObject(); - if (!json_obj_image.contains("large")) { - Error("Invalid Json reply, album image is missing large.", json_obj_image); - SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); - return; - } - QString album_image = json_obj_image["large"].toString(); - if (!album_image.isEmpty()) { - cover_url = QUrl(album_image); - } - } - - QJsonValue json_tracks = json_obj["tracks"]; - if (!json_tracks.isObject()) { - Error("Json tracks is not an object.", json_obj); - SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); - return; - } - QJsonObject json_obj_tracks = json_tracks.toObject(); - - if (!json_obj_tracks.contains("limit") || - !json_obj_tracks.contains("offset") || - !json_obj_tracks.contains("total") || - !json_obj_tracks.contains("items")) { - SongsFinishCheck(artist_id_requested, album_id_requested, limit_requested, offset_requested, 0, 0, album_artist, album); - Error("Json songs object is missing values.", json_obj); - return; - } - - //int limit = json_obj_tracks["limit"].toInt(); - int offset = json_obj_tracks["offset"].toInt(); - int songs_total = json_obj_tracks["total"].toInt(); - - if (offset != offset_requested) { - Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist, album); - return; - } - - QJsonValue json_value = ExtractItems(json_obj_tracks); - if (!json_value.isArray()) { - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist, album); - return; - } - - QJsonArray json_items = json_value.toArray(); - if (json_items.isEmpty()) { - if ((type_ == QueryType_Songs || type_ == QueryType_SearchSongs) && offset_requested == 0) { - no_results_ = true; - } - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist, album); - return; - } - - bool compilation = false; - bool multidisc = false; - SongList songs; - int songs_received = 0; - for (const QJsonValue &value : json_items) { - - if (!value.isObject()) { - Error("Invalid Json reply, track is not a object.", value); - continue; - } - QJsonObject json_obj = value.toObject(); - - ++songs_received; - Song song(Song::Source_Qobuz); - ParseSong(song, json_obj, artist_id, album_id, album_artist, album, cover_url); - if (!song.is_valid()) continue; - if (song.disc() >= 2) multidisc = true; - if (song.is_compilation()) compilation = true; - songs << song; - } - - for (Song &song : songs) { - if (compilation) song.set_compilation_detected(true); - if (multidisc) { - QString album_full(QString("%1 - (Disc %2)").arg(song.album()).arg(song.disc())); - song.set_album(album_full); - } - songs_ << song; - } - - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, songs_received, album_artist, album); - -} - -void QobuzRequest::SongsFinishCheck(const qint64 artist_id, const QString &album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist, const QString &album) { - - if (finished_) return; - - if (limit == 0 || limit > songs_received) { - int offset_next = offset + songs_received; - if (offset_next > 0 && offset_next < songs_total) { - switch (type_) { - case QueryType_Songs: - AddSongsRequest(offset_next); - break; - case QueryType_SearchSongs: - AddSongsSearchRequest(offset_next); - break; - case QueryType_Artists: - case QueryType_SearchArtists: - case QueryType_Albums: - case QueryType_SearchAlbums: - AddAlbumSongsRequest(artist_id, album_id, album_artist, album, offset_next); - break; - default: - break; - } - } - } - - if (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests(); - if (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests(); - - if ( - service_->download_album_covers() && - IsQuery() && - songs_requests_queue_.isEmpty() && - songs_requests_active_ <= 0 && - album_songs_requests_queue_.isEmpty() && - album_songs_requests_active_ <= 0 && - album_cover_requests_queue_.isEmpty() && - album_covers_received_ <= 0 && - album_covers_requests_sent_.isEmpty() && - album_songs_received_ >= album_songs_requested_ - ) { - GetAlbumCovers(); - } - - FinishCheck(); - -} - -int QobuzRequest::ParseSong(Song &song, const QJsonObject &json_obj, qint64 artist_id, QString album_id, QString album_artist, QString album, QUrl cover_url) { - - if ( - !json_obj.contains("id") || - !json_obj.contains("title") || - !json_obj.contains("track_number") || - !json_obj.contains("duration") || - !json_obj.contains("copyright") || - !json_obj.contains("streamable") - ) { - Error("Invalid Json reply, track is missing one or more values.", json_obj); - return -1; - } - - qint64 song_id = json_obj["id"].toInt(); - QString title = json_obj["title"].toString(); - int track = json_obj["track_number"].toInt(); - QString copyright = json_obj["copyright"].toString(); - quint64 duration = json_obj["duration"].toInt() * kNsecPerSec; - //bool streamable = json_obj["streamable"].toBool(); - QString composer; - QString performer; - - if (json_obj.contains("album")) { - - QJsonValue json_album = json_obj["album"]; - if (!json_album.isObject()) { - Error("Invalid Json reply, album is not an object.", json_album); - return -1; - } - QJsonObject json_obj_album = json_album.toObject(); - - if (json_obj_album.contains("id")) { - album_id = json_obj_album["id"].toString(); - } - - if (json_obj_album.contains("title")) { - album = json_obj_album["title"].toString(); - } - - if (json_obj_album.contains("artist")) { - QJsonValue json_artist = json_obj_album["artist"]; - if (!json_artist.isObject()) { - Error("Invalid Json reply, album artist is not a object.", json_artist); - return -1; - } - QJsonObject json_obj_artist = json_artist.toObject(); - if (!json_obj_artist.contains("id") || !json_obj_artist.contains("name")) { - Error("Invalid Json reply, album artist is missing id or name.", json_obj_artist); - return -1; - } - artist_id = json_obj_artist["id"].toInt(); - album_artist = json_obj_artist["name"].toString(); - } - - if (json_obj_album.contains("image")) { - QJsonValue json_image = json_obj_album["image"]; - if (!json_image.isObject()) { - Error("Invalid Json reply, album image is not a object.", json_image); - return -1; - } - QJsonObject json_obj_image = json_image.toObject(); - if (!json_obj_image.contains("large")) { - Error("Invalid Json reply, album image is missing large.", json_obj_image); - return -1; - } - QString album_image = json_obj_image["large"].toString(); - if (!album_image.isEmpty()) { - cover_url = QUrl(album_image); - } - } - } - - if (json_obj.contains("composer")) { - QJsonValue json_composer = json_obj["composer"]; - if (!json_composer.isObject()) { - Error("Invalid Json reply, track composer is not a object.", json_composer); - return -1; - } - QJsonObject json_obj_composer = json_composer.toObject(); - if (!json_obj_composer.contains("id") || !json_obj_composer.contains("name")) { - Error("Invalid Json reply, track composer is missing id or name.", json_obj_composer); - return -1; - } - composer = json_obj_composer["name"].toString(); - } - - if (json_obj.contains("performer")) { - QJsonValue json_performer = json_obj["performer"]; - if (!json_performer.isObject()) { - Error("Invalid Json reply, track performer is not a object.", json_performer); - return -1; - } - QJsonObject json_obj_performer = json_performer.toObject(); - if (!json_obj_performer.contains("id") || !json_obj_performer.contains("name")) { - Error("Invalid Json reply, track performer is missing id or name.", json_obj_performer); - return -1; - } - performer = json_obj_performer["name"].toString(); - } - - //if (!streamable) { - //Warn(QString("Song %1 %2 %3 is not streamable").arg(album_artist).arg(album).arg(title)); - //} - - QUrl url; - url.setScheme(url_handler_->scheme()); - url.setPath(QString::number(song_id)); - - title.remove(Song::kTitleRemoveMisc); - - //qLog(Debug) << "id" << song_id << "track" << track << "title" << title << "album" << album << "album artist" << album_artist << cover_url << streamable << url; - - song.set_source(Song::Source_Qobuz); - song.set_song_id(song_id); - song.set_album_id(album_id); - song.set_artist_id(artist_id); - song.set_album(album); - song.set_artist(album_artist); - song.set_title(title); - song.set_track(track); - song.set_url(url); - song.set_length_nanosec(duration); - song.set_art_automatic(cover_url); - song.set_comment(copyright); - song.set_directory_id(0); - song.set_filetype(Song::FileType_Stream); - song.set_filesize(0); - song.set_mtime(0); - song.set_ctime(0); - song.set_valid(true); - - return song_id; - -} - -void QobuzRequest::GetAlbumCovers() { - - for (Song &song : songs_) { - AddAlbumCoverRequest(song); - } - FlushAlbumCoverRequests(); - - if (album_covers_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving album cover for %1 album...").arg(album_covers_requested_)); - else emit UpdateStatus(query_id_, tr("Retrieving album covers for %1 albums...").arg(album_covers_requested_)); - emit ProgressSetMaximum(query_id_, album_covers_requested_); - emit UpdateProgress(query_id_, 0); - -} - -void QobuzRequest::AddAlbumCoverRequest(Song &song) { - - QUrl cover_url(song.art_automatic()); - if (!cover_url.isValid()) return; - - if (album_covers_requests_sent_.contains(cover_url)) { - album_covers_requests_sent_.insertMulti(cover_url, &song); - return; - } - - AlbumCoverRequest request; - request.url = cover_url; - request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), cover_url); - if (request.filename.isEmpty()) return; - - album_covers_requests_sent_.insertMulti(cover_url, &song); - ++album_covers_requested_; - - album_cover_requests_queue_.enqueue(request); - -} - -void QobuzRequest::FlushAlbumCoverRequests() { - - while (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) { - - AlbumCoverRequest request = album_cover_requests_queue_.dequeue(); - ++album_covers_requests_active_; - - QNetworkRequest req(request.url); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -#endif - QNetworkReply *reply = network_->get(req); - album_cover_replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumCoverReceived(QNetworkReply*, const QUrl&, const QString&)), reply, request.url, request.filename); - - } - -} - -void QobuzRequest::AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_url, const QString &filename) { - - if (album_cover_replies_.contains(reply)) { - album_cover_replies_.removeAll(reply); - reply->deleteLater(); - } - else { - AlbumCoverFinishCheck(); - return; - } - - --album_covers_requests_active_; - ++album_covers_received_; - - if (finished_) return; - - emit UpdateProgress(query_id_, album_covers_received_); - - if (!album_covers_requests_sent_.contains(cover_url)) { - AlbumCoverFinishCheck(); - return; - } - - if (reply->error() != QNetworkReply::NoError) { - Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - album_covers_requests_sent_.remove(cover_url); - AlbumCoverFinishCheck(); - return; - } - - QByteArray data = reply->readAll(); - if (data.isEmpty()) { - Error(QString("Received empty image data for %1").arg(cover_url.toString())); - album_covers_requests_sent_.remove(cover_url); - AlbumCoverFinishCheck(); - return; - } - - QImage image; - if (image.loadFromData(data)) { - - if (image.save(filename, "JPG")) { - while (album_covers_requests_sent_.contains(cover_url)) { - Song *song = album_covers_requests_sent_.take(cover_url); - song->set_art_automatic(QUrl::fromLocalFile(filename)); - } - } - - } - else { - album_covers_requests_sent_.remove(cover_url); - Error(QString("Error decoding image data from %1").arg(cover_url.toString())); - } - - AlbumCoverFinishCheck(); - -} - -void QobuzRequest::AlbumCoverFinishCheck() { - - if (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) - FlushAlbumCoverRequests(); - - FinishCheck(); - -} - -void QobuzRequest::FinishCheck() { - - if ( - !finished_ && - albums_requests_queue_.isEmpty() && - artists_requests_queue_.isEmpty() && - songs_requests_queue_.isEmpty() && - artist_albums_requests_queue_.isEmpty() && - album_songs_requests_queue_.isEmpty() && - album_cover_requests_queue_.isEmpty() && - artist_albums_requests_pending_.isEmpty() && - album_songs_requests_pending_.isEmpty() && - album_covers_requests_sent_.isEmpty() && - artists_requests_active_ <= 0 && - albums_requests_active_ <= 0 && - songs_requests_active_ <= 0 && - artist_albums_requests_active_ <= 0 && - artist_albums_received_ >= artist_albums_requested_ && - album_songs_requests_active_ <= 0 && - album_songs_received_ >= album_songs_requested_ && - album_covers_requests_active_ <= 0 && - album_covers_received_ >= album_covers_requested_ - ) { - finished_ = true; - if (no_results_ && songs_.isEmpty()) { - if (IsSearch()) - emit Results(query_id_, SongList(), tr("No match.")); - else - emit Results(query_id_, SongList(), QString()); - } - else { - if (songs_.isEmpty() && errors_.isEmpty()) - emit Results(query_id_, songs_, tr("Unknown error")); - else - emit Results(query_id_, songs_, ErrorsToHTML(errors_)); - } - } - -} - -void QobuzRequest::Error(const QString &error, const QVariant &debug) { - - if (!error.isEmpty()) { - errors_ << error; - qLog(Error) << "Qobuz:" << error; - } - if (debug.isValid()) qLog(Debug) << debug; - FinishCheck(); - -} - -void QobuzRequest::Warn(const QString &error, const QVariant &debug) { - - qLog(Error) << "Qobuz:" << error; - if (debug.isValid()) qLog(Debug) << debug; - -} - diff --git a/src/qobuz/qobuzrequest.h b/src/qobuz/qobuzrequest.h deleted file mode 100644 index ec9a7476..00000000 --- a/src/qobuz/qobuzrequest.h +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef QOBUZREQUEST_H -#define QOBUZREQUEST_H - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/song.h" -#include "qobuzbaserequest.h" - -class QNetworkReply; -class Application; -class NetworkAccessManager; -class QobuzService; -class QobuzUrlHandler; - -class QobuzRequest : public QobuzBaseRequest { - Q_OBJECT - - public: - - QobuzRequest(QobuzService *service, QobuzUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent); - ~QobuzRequest(); - - void ReloadSettings(); - - void Process(); - void Search(const int search_id, const QString &search_text); - - signals: - void Login(); - void Login(const QString &username, const QString &password, const QString &token); - void LoginSuccess(); - void LoginFailure(QString failure_reason); - void Results(const int id, const SongList &songs, const QString &error); - void UpdateStatus(const int id, const QString &text); - void ProgressSetMaximum(const int id, const int max); - void UpdateProgress(const int id, const int max); - void StreamURLFinished(const QUrl original_url, const QUrl url, const Song::FileType, QString error = QString()); - - private slots: - - void ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested); - - void AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested); - void AlbumsReceived(QNetworkReply *reply, const qint64 artist_id_requested, const int limit_requested, const int offset_requested); - - void SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested); - void SongsReceived(QNetworkReply *reply, const qint64 artist_id_requested, const QString &album_id_requested, const int limit_requested, const int offset_requested, const QString &album_artist_requested = QString(), const QString &album_requested = QString()); - - void ArtistAlbumsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const int offset_requested); - void AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const QString &album_id, const int offset_requested, const QString &album_artist, const QString &album); - void AlbumCoverReceived(QNetworkReply *reply, const QUrl &cover_url, const QString &filename); - - private: - typedef QPair Param; - typedef QList ParamList; - - struct Request { - qint64 artist_id = 0; - QString album_id = 0; - qint64 song_id = 0; - int offset = 0; - int limit = 0; - QString album_artist; - QString album; - }; - struct AlbumCoverRequest { - QUrl url; - QString filename; - }; - - bool IsQuery() { return (type_ == QueryType_Artists || type_ == QueryType_Albums || type_ == QueryType_Songs); } - bool IsSearch() { return (type_ == QueryType_SearchArtists || type_ == QueryType_SearchAlbums || type_ == QueryType_SearchSongs); } - - void GetArtists(); - void GetAlbums(); - void GetSongs(); - - void ArtistsSearch(); - void AlbumsSearch(); - void SongsSearch(); - - void AddArtistsRequest(const int offset = 0, const int limit = 0); - void AddArtistsSearchRequest(const int offset = 0); - void FlushArtistsRequests(); - void AddAlbumsRequest(const int offset = 0, const int limit = 0); - void AddAlbumsSearchRequest(const int offset = 0); - void FlushAlbumsRequests(); - void AddSongsRequest(const int offset = 0, const int limit = 0); - void AddSongsSearchRequest(const int offset = 0); - void FlushSongsRequests(); - - void ArtistsFinishCheck(const int limit = 0, const int offset = 0, const int artists_received = 0); - void AlbumsFinishCheck(const qint64 artist_id, const int limit = 0, const int offset = 0, const int albums_total = 0, const int albums_received = 0); - void SongsFinishCheck(const qint64 artist_id, const QString &album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist, const QString &album); - - void AddArtistAlbumsRequest(const qint64 artist_id, const int offset = 0); - void FlushArtistAlbumsRequests(); - - void AddAlbumSongsRequest(const qint64 artist_id, const QString &album_id, const QString &album_artist, const QString &album, const int offset = 0); - void FlushAlbumSongsRequests(); - - int ParseSong(Song &song, const QJsonObject &json_obj, qint64 artist_id, QString album_id, QString album_artist, QString album, QUrl cover_url); - - QString AlbumCoverFileName(const Song &song); - - void GetAlbumCovers(); - void AddAlbumCoverRequest(Song &song); - void FlushAlbumCoverRequests(); - void AlbumCoverFinishCheck(); - - void FinishCheck(); - void Warn(const QString &error, const QVariant &debug = QVariant()); - void Error(const QString &error, const QVariant &debug = QVariant()); - - static const int kMaxConcurrentArtistsRequests; - static const int kMaxConcurrentAlbumsRequests; - static const int kMaxConcurrentSongsRequests; - static const int kMaxConcurrentArtistAlbumsRequests; - static const int kMaxConcurrentAlbumSongsRequests; - static const int kMaxConcurrentAlbumCoverRequests; - - QobuzService *service_; - QobuzUrlHandler *url_handler_; - Application *app_; - NetworkAccessManager *network_; - - QueryType type_; - int query_id_; - QString search_text_; - - bool finished_; - - QQueue artists_requests_queue_; - QQueue albums_requests_queue_; - QQueue songs_requests_queue_; - - QQueue artist_albums_requests_queue_; - QQueue album_songs_requests_queue_; - QQueue album_cover_requests_queue_; - - QList artist_albums_requests_pending_; - QHash album_songs_requests_pending_; - QMultiMap album_covers_requests_sent_; - - int artists_requests_active_; - int artists_total_; - int artists_received_; - - int albums_requests_active_; - int songs_requests_active_; - - int artist_albums_requests_active_; - int artist_albums_requested_; - int artist_albums_received_; - - int album_songs_requests_active_; - int album_songs_requested_; - int album_songs_received_; - - int album_covers_requests_active_; - int album_covers_requested_; - int album_covers_received_; - - SongList songs_; - QStringList errors_; - bool no_results_; - QList replies_; - QList album_cover_replies_; - -}; - -#endif // QOBUZREQUEST_H diff --git a/src/qobuz/qobuzservice.cpp b/src/qobuz/qobuzservice.cpp deleted file mode 100644 index 930ab795..00000000 --- a/src/qobuz/qobuzservice.cpp +++ /dev/null @@ -1,765 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/application.h" -#include "core/player.h" -#include "core/closure.h" -#include "core/logging.h" -#include "core/network.h" -#include "core/database.h" -#include "core/song.h" -#include "core/utilities.h" -#include "internet/internetsearch.h" -#include "collection/collectionbackend.h" -#include "collection/collectionmodel.h" -#include "qobuzservice.h" -#include "qobuzurlhandler.h" -#include "qobuzbaserequest.h" -#include "qobuzrequest.h" -#include "qobuzfavoriterequest.h" -#include "qobuzstreamurlrequest.h" -#include "settings/settingsdialog.h" -#include "settings/qobuzsettingspage.h" - -using std::shared_ptr; - -const Song::Source QobuzService::kSource = Song::Source_Qobuz; -const char *QobuzService::kAuthUrl = "https://www.qobuz.com/api.json/0.2/user/login"; -const int QobuzService::kLoginAttempts = 2; -const int QobuzService::kTimeResetLoginAttempts = 60000; - -const char *QobuzService::kArtistsSongsTable = "qobuz_artists_songs"; -const char *QobuzService::kAlbumsSongsTable = "qobuz_albums_songs"; -const char *QobuzService::kSongsTable = "qobuz_songs"; - -const char *QobuzService::kArtistsSongsFtsTable = "qobuz_artists_songs_fts"; -const char *QobuzService::kAlbumsSongsFtsTable = "qobuz_albums_songs_fts"; -const char *QobuzService::kSongsFtsTable = "qobuz_songs_fts"; - -QobuzService::QobuzService(Application *app, QObject *parent) - : InternetService(Song::Source_Qobuz, "Qobuz", "qobuz", app, parent), - app_(app), - network_(new NetworkAccessManager(this)), - url_handler_(new QobuzUrlHandler(app, this)), - artists_collection_backend_(nullptr), - albums_collection_backend_(nullptr), - songs_collection_backend_(nullptr), - artists_collection_model_(nullptr), - albums_collection_model_(nullptr), - songs_collection_model_(nullptr), - artists_collection_sort_model_(new QSortFilterProxyModel(this)), - albums_collection_sort_model_(new QSortFilterProxyModel(this)), - songs_collection_sort_model_(new QSortFilterProxyModel(this)), - timer_search_delay_(new QTimer(this)), - timer_login_attempt_(new QTimer(this)), - favorite_request_(new QobuzFavoriteRequest(this, network_, this)), - format_(0), - search_delay_(1500), - artistssearchlimit_(1), - albumssearchlimit_(1), - songssearchlimit_(1), - download_album_covers_(true), - user_id_(-1), - credential_id_(-1), - pending_search_id_(0), - next_pending_search_id_(1), - search_id_(0), - login_sent_(false), - login_attempts_(0) - { - - app->player()->RegisterUrlHandler(url_handler_); - -#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)) - network_->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); -#endif - - // Backends - - artists_collection_backend_ = new CollectionBackend(); - artists_collection_backend_->moveToThread(app_->database()->thread()); - artists_collection_backend_->Init(app_->database(), Song::Source_Qobuz, kArtistsSongsTable, QString(), QString(), kArtistsSongsFtsTable); - - albums_collection_backend_ = new CollectionBackend(); - albums_collection_backend_->moveToThread(app_->database()->thread()); - albums_collection_backend_->Init(app_->database(), Song::Source_Qobuz, kAlbumsSongsTable, QString(), QString(), kAlbumsSongsFtsTable); - - songs_collection_backend_ = new CollectionBackend(); - songs_collection_backend_->moveToThread(app_->database()->thread()); - songs_collection_backend_->Init(app_->database(), Song::Source_Qobuz, kSongsTable, QString(), QString(), kSongsFtsTable); - - artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this); - albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this); - songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this); - - artists_collection_sort_model_->setSourceModel(artists_collection_model_); - artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - artists_collection_sort_model_->setDynamicSortFilter(true); - artists_collection_sort_model_->setSortLocaleAware(true); - artists_collection_sort_model_->sort(0); - - albums_collection_sort_model_->setSourceModel(albums_collection_model_); - albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - albums_collection_sort_model_->setDynamicSortFilter(true); - albums_collection_sort_model_->setSortLocaleAware(true); - albums_collection_sort_model_->sort(0); - - songs_collection_sort_model_->setSourceModel(songs_collection_model_); - songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - songs_collection_sort_model_->setDynamicSortFilter(true); - songs_collection_sort_model_->setSortLocaleAware(true); - songs_collection_sort_model_->sort(0); - - // Search - - timer_search_delay_->setSingleShot(true); - connect(timer_search_delay_, SIGNAL(timeout()), SLOT(StartSearch())); - - timer_login_attempt_->setSingleShot(true); - connect(timer_login_attempt_, SIGNAL(timeout()), SLOT(ResetLoginAttempts())); - - connect(this, SIGNAL(Login()), SLOT(SendLogin())); - connect(this, SIGNAL(Login(QString, QString, QString)), SLOT(SendLogin(QString, QString, QString))); - - connect(this, SIGNAL(AddArtists(const SongList&)), favorite_request_, SLOT(AddArtists(const SongList&))); - connect(this, SIGNAL(AddAlbums(const SongList&)), favorite_request_, SLOT(AddAlbums(const SongList&))); - connect(this, SIGNAL(AddSongs(const SongList&)), favorite_request_, SLOT(AddSongs(const SongList&))); - - connect(this, SIGNAL(RemoveArtists(const SongList&)), favorite_request_, SLOT(RemoveArtists(const SongList&))); - connect(this, SIGNAL(RemoveAlbums(const SongList&)), favorite_request_, SLOT(RemoveAlbums(const SongList&))); - connect(this, SIGNAL(RemoveSongs(const SongList&)), favorite_request_, SLOT(RemoveSongs(const SongList&))); - - connect(favorite_request_, SIGNAL(ArtistsAdded(const SongList&)), artists_collection_backend_, SLOT(AddOrUpdateSongs(const SongList&))); - connect(favorite_request_, SIGNAL(AlbumsAdded(const SongList&)), albums_collection_backend_, SLOT(AddOrUpdateSongs(const SongList&))); - connect(favorite_request_, SIGNAL(SongsAdded(const SongList&)), songs_collection_backend_, SLOT(AddOrUpdateSongs(const SongList&))); - - connect(favorite_request_, SIGNAL(ArtistsRemoved(const SongList&)), artists_collection_backend_, SLOT(DeleteSongs(const SongList&))); - connect(favorite_request_, SIGNAL(AlbumsRemoved(const SongList&)), albums_collection_backend_, SLOT(DeleteSongs(const SongList&))); - connect(favorite_request_, SIGNAL(SongsRemoved(const SongList&)), songs_collection_backend_, SLOT(DeleteSongs(const SongList&))); - - ReloadSettings(); - -} - -QobuzService::~QobuzService() { - - while (!stream_url_requests_.isEmpty()) { - QobuzStreamURLRequest *stream_url_req = stream_url_requests_.takeFirst(); - disconnect(stream_url_req, 0, this, 0); - stream_url_req->deleteLater(); - } - - artists_collection_backend_->deleteLater(); - albums_collection_backend_->deleteLater(); - songs_collection_backend_->deleteLater(); - -} - -void QobuzService::Exit() { - - wait_for_exit_ << artists_collection_backend_ << albums_collection_backend_ << songs_collection_backend_; - - connect(artists_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived())); - connect(albums_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived())); - connect(songs_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived())); - - artists_collection_backend_->ExitAsync(); - albums_collection_backend_->ExitAsync(); - songs_collection_backend_->ExitAsync(); - -} - -void QobuzService::ExitReceived() { - - QObject *obj = static_cast(sender()); - disconnect(obj, 0, this, 0); - qLog(Debug) << obj << "successfully exited."; - wait_for_exit_.removeAll(obj); - if (wait_for_exit_.isEmpty()) emit ExitFinished(); - -} - -void QobuzService::ShowConfig() { - app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Qobuz); -} - -void QobuzService::ReloadSettings() { - - QSettings s; - s.beginGroup(QobuzSettingsPage::kSettingsGroup); - - app_id_ = s.value("app_id").toString(); - app_secret_ = s.value("app_secret").toString(); - - username_ = s.value("username").toString(); - QByteArray password = s.value("password").toByteArray(); - if (password.isEmpty()) password_.clear(); - else password_ = QString::fromUtf8(QByteArray::fromBase64(password)); - - format_ = s.value("format", 27).toInt(); - search_delay_ = s.value("searchdelay", 1500).toInt(); - artistssearchlimit_ = s.value("artistssearchlimit", 4).toInt(); - albumssearchlimit_ = s.value("albumssearchlimit", 10).toInt(); - songssearchlimit_ = s.value("songssearchlimit", 10).toInt(); - download_album_covers_ = s.value("downloadalbumcovers", true).toBool(); - - user_id_ = s.value("user_id").toInt(); - device_id_ = s.value("device_id").toString(); - user_auth_token_ = s.value("user_auth_token").toString(); - - s.endGroup(); - -} - -void QobuzService::SendLogin() { - SendLogin(app_id_, username_, password_); -} - -void QobuzService::SendLogin(const QString &app_id, const QString &username, const QString &password) { - - emit UpdateStatus(tr("Authenticating...")); - login_errors_.clear(); - - login_sent_ = true; - ++login_attempts_; - if (timer_login_attempt_->isActive()) timer_login_attempt_->stop(); - timer_login_attempt_->setInterval(kTimeResetLoginAttempts); - timer_login_attempt_->start(); - - const ParamList params = ParamList() << Param("app_id", app_id) - << Param("username", username) - << Param("password", password) - << Param("device_manufacturer_id", Utilities::MacAddress()); - - QUrlQuery url_query; - for (const Param ¶m : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QUrl url(kAuthUrl); - QNetworkRequest req(url); - - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - - QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8(); - QNetworkReply *reply = network_->post(req, query); - - connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList))); - NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*)), reply); - - qLog(Debug) << "Qobuz: Sending request" << url << query; - -} - -void QobuzService::HandleLoginSSLErrors(QList ssl_errors) { - - for (QSslError &ssl_error : ssl_errors) { - login_errors_ += ssl_error.errorString(); - } - -} - -void QobuzService::HandleAuthReply(QNetworkReply *reply) { - - reply->deleteLater(); - - login_sent_ = false; - - if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - return; - } - else { - // See if there is Json data containing "status", "code" and "message" - then use that instead. - QByteArray data(reply->readAll()); - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - if (json_error.error == QJsonParseError::NoError && !json_doc.isEmpty() && json_doc.isObject()) { - QJsonObject json_obj = json_doc.object(); - if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("code") && json_obj.contains("message")) { - QString status = json_obj["status"].toString(); - int code = json_obj["code"].toInt(); - QString message = json_obj["message"].toString(); - login_errors_ << QString("%1 (%2)").arg(message).arg(code); - } - } - if (login_errors_.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - LoginError(); - return; - } - } - - login_errors_.clear(); - - QByteArray data(reply->readAll()); - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - LoginError("Authentication reply from server missing Json data."); - return; - } - - if (json_doc.isEmpty()) { - LoginError("Authentication reply from server has empty Json document."); - return; - } - - if (!json_doc.isObject()) { - LoginError("Authentication reply from server has Json document that is not an object.", json_doc); - return; - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - LoginError("Authentication reply from server has empty Json object.", json_doc); - return; - } - - if (!json_obj.contains("user_auth_token")) { - LoginError("Authentication reply from server is missing user_auth_token", json_obj); - return; - } - user_auth_token_ = json_obj["user_auth_token"].toString(); - - if (!json_obj.contains("user")) { - LoginError("Authentication reply from server is missing user", json_obj); - return; - } - QJsonValue json_user = json_obj["user"]; - if (!json_user.isObject()) { - LoginError("Authentication reply user is not a object", json_obj); - return; - } - QJsonObject json_obj_user = json_user.toObject(); - - if (!json_obj_user.contains("id")) { - LoginError("Authentication reply from server is missing user id", json_obj_user); - return; - } - user_id_ = json_obj_user["id"].toInt(); - - if (!json_obj_user.contains("device")) { - LoginError("Authentication reply from server is missing user device", json_obj_user); - return; - } - QJsonValue json_device = json_obj_user["device"]; - if (!json_device.isObject()) { - LoginError("Authentication reply from server user device is not a object", json_device); - return; - } - QJsonObject json_obj_device = json_device.toObject(); - - if (!json_obj_device.contains("device_manufacturer_id")) { - LoginError("Authentication reply from server device is missing device_manufacturer_id", json_obj_device); - return; - } - device_id_ = json_obj_device["device_manufacturer_id"].toString(); - - if (!json_obj_user.contains("credential")) { - LoginError("Authentication reply from server is missing user credential", json_obj_user); - return; - } - QJsonValue json_credential = json_obj_user["credential"]; - if (!json_credential.isObject()) { - LoginError("Authentication reply from serve userr credential is not a object", json_device); - return; - } - QJsonObject json_obj_credential = json_credential.toObject(); - - if (!json_obj_credential.contains("id")) { - LoginError("Authentication reply user credential from server is missing user credential id", json_obj_device); - return; - } - credential_id_ = json_obj_credential["id"].toInt(); - - QSettings s; - s.beginGroup(QobuzSettingsPage::kSettingsGroup); - s.setValue("user_auth_token", user_auth_token_); - s.setValue("user_id", user_id_); - s.setValue("credential_id", credential_id_); - s.setValue("device_id", device_id_); - s.endGroup(); - - qLog(Debug) << "Qobuz: Login successful" << "user id" << user_id_ << "user auth token" << user_auth_token_ << "device id" << device_id_; - - login_attempts_ = 0; - if (timer_login_attempt_->isActive()) timer_login_attempt_->stop(); - - emit LoginComplete(true); - emit LoginSuccess(); - -} - -void QobuzService::Logout() { - - user_auth_token_.clear(); - device_id_.clear(); - user_id_ = -1; - credential_id_ = -1; - - QSettings s; - s.beginGroup(QobuzSettingsPage::kSettingsGroup); - s.remove("user_id"); - s.remove("credential_id"); - s.remove("device_id"); - s.remove("user_auth_token"); - s.endGroup(); - -} - -void QobuzService::ResetLoginAttempts() { - login_attempts_ = 0; -} - -void QobuzService::TryLogin() { - - if (authenticated() || login_sent_) return; - - if (login_attempts_ >= kLoginAttempts) { - emit LoginComplete(false, tr("Maximum number of login attempts reached.")); - return; - } - if (app_id_.isEmpty()) { - emit LoginComplete(false, tr("Missing Qobuz app ID.")); - return; - } - if (username_.isEmpty()) { - emit LoginComplete(false, tr("Missing Qobuz username.")); - return; - } - if (password_.isEmpty()) { - emit LoginComplete(false, tr("Missing Qobuz password.")); - return; - } - - emit Login(); - -} - -void QobuzService::ResetArtistsRequest() { - - if (artists_request_.get()) { - disconnect(artists_request_.get(), 0, this, 0); - disconnect(this, 0, artists_request_.get(), 0); - artists_request_.reset(); - } - -} - -void QobuzService::GetArtists() { - - if (app_id().isEmpty()) { - emit ArtistsResults(SongList(), tr("Missing Qobuz app ID.")); - return; - } - - if (!authenticated()) { - emit ArtistsResults(SongList(), tr("Not authenticated with Qobuz.")); - return; - } - - ResetArtistsRequest(); - - artists_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Artists, this)); - - connect(artists_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(ArtistsResultsReceived(const int, const SongList&, const QString&))); - connect(artists_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(ArtistsUpdateStatusReceived(const int, const QString&))); - connect(artists_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SLOT(ArtistsProgressSetMaximumReceived(const int, const int))); - connect(artists_request_.get(), SIGNAL(UpdateProgress(const int, const int)), SLOT(ArtistsUpdateProgressReceived(const int, const int))); - - artists_request_->Process(); - -} - -void QobuzService::ArtistsResultsReceived(const int id, const SongList &songs, const QString &error) { - Q_UNUSED(id); - emit ArtistsResults(songs, error); -} - -void QobuzService::ArtistsUpdateStatusReceived(const int id, const QString &text) { - Q_UNUSED(id); - emit ArtistsUpdateStatus(text); -} - -void QobuzService::ArtistsProgressSetMaximumReceived(const int id, const int max) { - Q_UNUSED(id); - emit ArtistsProgressSetMaximum(max); -} - -void QobuzService::ArtistsUpdateProgressReceived(const int id, const int progress) { - Q_UNUSED(id); - emit ArtistsUpdateProgress(progress); -} - -void QobuzService::ResetAlbumsRequest() { - - if (albums_request_.get()) { - disconnect(albums_request_.get(), 0, this, 0); - disconnect(this, 0, albums_request_.get(), 0); - albums_request_.reset(); - } - -} - -void QobuzService::GetAlbums() { - - if (app_id().isEmpty()) { - emit AlbumsResults(SongList(), tr("Missing Qobuz app ID.")); - return; - } - - if (!authenticated()) { - emit AlbumsResults(SongList(), tr("Not authenticated with Qobuz.")); - return; - } - - ResetAlbumsRequest(); - albums_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Albums, this)); - connect(albums_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(AlbumsResultsReceived(const int, const SongList&, const QString&))); - connect(albums_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(AlbumsUpdateStatusReceived(const int, const QString&))); - connect(albums_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SLOT(AlbumsProgressSetMaximumReceived(const int, const int))); - connect(albums_request_.get(), SIGNAL(UpdateProgress(const int, const int)), SLOT(AlbumsUpdateProgressReceived(const int, const int))); - - albums_request_->Process(); - -} - -void QobuzService::AlbumsResultsReceived(const int id, const SongList &songs, const QString &error) { - Q_UNUSED(id); - emit AlbumsResults(songs, error); -} - -void QobuzService::AlbumsUpdateStatusReceived(const int id, const QString &text) { - Q_UNUSED(id); - emit AlbumsUpdateStatus(text); -} - -void QobuzService::AlbumsProgressSetMaximumReceived(const int id, const int max) { - Q_UNUSED(id); - emit AlbumsProgressSetMaximum(max); -} - -void QobuzService::AlbumsUpdateProgressReceived(const int id, const int progress) { - Q_UNUSED(id); - emit AlbumsUpdateProgress(progress); -} - -void QobuzService::ResetSongsRequest() { - - if (songs_request_.get()) { - disconnect(songs_request_.get(), 0, this, 0); - disconnect(this, 0, songs_request_.get(), 0); - songs_request_.reset(); - } - -} - -void QobuzService::GetSongs() { - - if (app_id().isEmpty()) { - emit SongsResults(SongList(), tr("Missing Qobuz app ID.")); - return; - } - - if (!authenticated()) { - emit SongsResults(SongList(), tr("Not authenticated with Qobuz.")); - return; - } - - ResetSongsRequest(); - songs_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, QobuzBaseRequest::QueryType_Songs, this)); - connect(songs_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(SongsResultsReceived(const int, const SongList&, const QString&))); - connect(songs_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(SongsUpdateStatusReceived(const int, const QString&))); - connect(songs_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SLOT(SongsProgressSetMaximumReceived(const int, const int))); - connect(songs_request_.get(), SIGNAL(UpdateProgress(const int, const int)), SLOT(SongsUpdateProgressReceived(const int, const int))); - - songs_request_->Process(); - -} - -void QobuzService::SongsResultsReceived(const int id, const SongList &songs, const QString &error) { - Q_UNUSED(id); - emit SongsResults(songs, error); -} - -void QobuzService::SongsUpdateStatusReceived(const int id, const QString &text) { - Q_UNUSED(id); - emit SongsUpdateStatus(text); -} - -void QobuzService::SongsProgressSetMaximumReceived(const int id, const int max) { - Q_UNUSED(id); - emit SongsProgressSetMaximum(max); -} - -void QobuzService::SongsUpdateProgressReceived(const int id, const int progress) { - Q_UNUSED(id); - emit SongsUpdateProgress(progress); -} - -int QobuzService::Search(const QString &text, InternetSearch::SearchType type) { - - pending_search_id_ = next_pending_search_id_; - pending_search_text_ = text; - pending_search_type_ = type; - - next_pending_search_id_++; - - if (text.isEmpty()) { - timer_search_delay_->stop(); - return pending_search_id_; - } - timer_search_delay_->setInterval(search_delay_); - timer_search_delay_->start(); - - return pending_search_id_; - -} - -void QobuzService::StartSearch() { - - search_id_ = pending_search_id_; - search_text_ = pending_search_text_; - - if (app_id_.isEmpty()) { // App ID is the only thing needed to search. - emit SearchResults(search_id_, SongList(), tr("Missing Qobuz app ID.")); - return; - } - - SendSearch(); - -} - -void QobuzService::CancelSearch() { -} - -void QobuzService::SendSearch() { - - QobuzBaseRequest::QueryType type; - - switch (pending_search_type_) { - case InternetSearch::SearchType_Artists: - type = QobuzBaseRequest::QueryType_SearchArtists; - break; - case InternetSearch::SearchType_Albums: - type = QobuzBaseRequest::QueryType_SearchAlbums; - break; - case InternetSearch::SearchType_Songs: - type = QobuzBaseRequest::QueryType_SearchSongs; - break; - default: - //Error("Invalid search type."); - return; - } - - search_request_.reset(new QobuzRequest(this, url_handler_, app_, network_, type, this)); - - connect(search_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(SearchResultsReceived(const int, const SongList&, const QString&))); - connect(search_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SIGNAL(SearchUpdateStatus(const int, const QString&))); - connect(search_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SIGNAL(SearchProgressSetMaximum(const int, const int))); - connect(search_request_.get(), SIGNAL(UpdateProgress(const int, const int)), SIGNAL(SearchUpdateProgress(const int, const int))); - - search_request_->Search(search_id_, search_text_); - search_request_->Process(); - -} - -void QobuzService::SearchResultsReceived(const int id, const SongList &songs, const QString &error) { - emit SearchResults(id, songs, error); -} - -void QobuzService::GetStreamURL(const QUrl &url) { - - if (app_id().isEmpty() || app_secret().isEmpty()) { // Don't check for login here, because we allow automatic login. - emit StreamURLFinished(url, url, Song::FileType_Stream, -1, -1, -1, tr("Missing Qobuz app ID or secret.")); - return; - } - - QobuzStreamURLRequest *stream_url_req = new QobuzStreamURLRequest(this, network_, url, this); - stream_url_requests_ << stream_url_req; - - connect(stream_url_req, SIGNAL(TryLogin()), this, SLOT(TryLogin())); - connect(stream_url_req, SIGNAL(StreamURLFinished(const QUrl&, const QUrl&, const Song::FileType, const int, const int, const qint64, QString)), this, SLOT(HandleStreamURLFinished(const QUrl&, const QUrl&, const Song::FileType, const int, const int, const qint64, QString))); - connect(this, SIGNAL(LoginComplete(const bool, QString)), stream_url_req, SLOT(LoginComplete(const bool, QString))); - - stream_url_req->Process(); - -} - -void QobuzService::HandleStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error) { - - QobuzStreamURLRequest *stream_url_req = qobject_cast(sender()); - if (!stream_url_req || !stream_url_requests_.contains(stream_url_req)) return; - stream_url_req->deleteLater(); - stream_url_requests_.removeAll(stream_url_req); - - emit StreamURLFinished(original_url, stream_url, filetype, samplerate, bit_depth, duration, error); - -} - -void QobuzService::LoginError(const QString &error, const QVariant &debug) { - - if (!error.isEmpty()) login_errors_ << error; - - QString error_html; - for (const QString &error : login_errors_) { - qLog(Error) << "Qobuz:" << error; - error_html += error + "
    "; - } - if (debug.isValid()) qLog(Debug) << debug; - - emit LoginFailure(error_html); - emit LoginComplete(false, error_html); - - login_errors_.clear(); - -} diff --git a/src/qobuz/qobuzservice.h b/src/qobuz/qobuzservice.h deleted file mode 100644 index d45343df..00000000 --- a/src/qobuz/qobuzservice.h +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef QOBUZSERVICE_H -#define QOBUZSERVICE_H - -#include "config.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/song.h" -#include "internet/internetservice.h" -#include "internet/internetsearch.h" - -class QTimer; -class QNetworkReply; -class QSortFilterProxyModel; -class Application; -class NetworkAccessManager; -class QobuzUrlHandler; -class QobuzRequest; -class QobuzFavoriteRequest; -class QobuzStreamURLRequest; -class CollectionBackend; -class CollectionModel; - -using std::shared_ptr; - -class QobuzService : public InternetService { - Q_OBJECT - - public: - QobuzService(Application *app, QObject *parent); - ~QobuzService(); - - static const Song::Source kSource; - - void Exit(); - void ReloadSettings(); - - void Logout(); - int Search(const QString &query, InternetSearch::SearchType type); - void CancelSearch(); - - int max_login_attempts() { return kLoginAttempts; } - - Application *app() { return app_; } - QString app_id() { return app_id_; } - QString app_secret() { return app_secret_; } - QString username() { return username_; } - QString password() { return password_; } - int format() { return format_; } - int search_delay() { return search_delay_; } - int artistssearchlimit() { return artistssearchlimit_; } - int albumssearchlimit() { return albumssearchlimit_; } - int songssearchlimit() { return songssearchlimit_; } - bool download_album_covers() { return download_album_covers_; } - - QString user_auth_token() { return user_auth_token_; } - qint64 user_id() { return user_id_; } - QString device_id() { return device_id_; } - qint64 credential_id() { return credential_id_; } - - bool authenticated() { return (!app_id_.isEmpty() && !app_secret_.isEmpty() && !user_auth_token_.isEmpty()); } - bool login_sent() { return login_sent_; } - bool login_attempts() { return login_attempts_; } - - void GetStreamURL(const QUrl &url); - - CollectionBackend *artists_collection_backend() { return artists_collection_backend_; } - CollectionBackend *albums_collection_backend() { return albums_collection_backend_; } - CollectionBackend *songs_collection_backend() { return songs_collection_backend_; } - - CollectionModel *artists_collection_model() { return artists_collection_model_; } - CollectionModel *albums_collection_model() { return albums_collection_model_; } - CollectionModel *songs_collection_model() { return songs_collection_model_; } - - QSortFilterProxyModel *artists_collection_sort_model() { return artists_collection_sort_model_; } - QSortFilterProxyModel *albums_collection_sort_model() { return albums_collection_sort_model_; } - QSortFilterProxyModel *songs_collection_sort_model() { return songs_collection_sort_model_; } - - enum QueryType { - QueryType_Artists, - QueryType_Albums, - QueryType_Songs, - QueryType_SearchArtists, - QueryType_SearchAlbums, - QueryType_SearchSongs, - }; - - signals: - - public slots: - void ShowConfig(); - void TryLogin(); - void SendLogin(const QString &app_id, const QString &username, const QString &password); - void GetArtists(); - void GetAlbums(); - void GetSongs(); - void ResetArtistsRequest(); - void ResetAlbumsRequest(); - void ResetSongsRequest(); - - private slots: - void ExitReceived(); - void SendLogin(); - void HandleLoginSSLErrors(QList ssl_errors); - void HandleAuthReply(QNetworkReply *reply); - void ResetLoginAttempts(); - void StartSearch(); - void ArtistsResultsReceived(const int id, const SongList &songs, const QString &error); - void AlbumsResultsReceived(const int id, const SongList &songs, const QString &error); - void SongsResultsReceived(const int id, const SongList &songs, const QString &error); - void SearchResultsReceived(const int id, const SongList &songs, const QString &error); - void ArtistsUpdateStatusReceived(const int id, const QString &text); - void AlbumsUpdateStatusReceived(const int id, const QString &text); - void SongsUpdateStatusReceived(const int id, const QString &text); - void ArtistsProgressSetMaximumReceived(const int id, const int max); - void AlbumsProgressSetMaximumReceived(const int id, const int max); - void SongsProgressSetMaximumReceived(const int id, const int max); - void ArtistsUpdateProgressReceived(const int id, const int progress); - void AlbumsUpdateProgressReceived(const int id, const int progress); - void SongsUpdateProgressReceived(const int id, const int progress); - void HandleStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error); - - private: - typedef QPair Param; - typedef QList ParamList; - - typedef QPair EncodedParam; - typedef QList EncodedParamList; - - void SendSearch(); - void LoginError(const QString &error = QString(), const QVariant &debug = QVariant()); - - static const char *kAuthUrl; - static const int kLoginAttempts; - static const int kTimeResetLoginAttempts; - - static const char *kArtistsSongsTable; - static const char *kAlbumsSongsTable; - static const char *kSongsTable; - - static const char *kArtistsSongsFtsTable; - static const char *kAlbumsSongsFtsTable; - static const char *kSongsFtsTable; - - Application *app_; - NetworkAccessManager *network_; - QobuzUrlHandler *url_handler_; - - CollectionBackend *artists_collection_backend_; - CollectionBackend *albums_collection_backend_; - CollectionBackend *songs_collection_backend_; - - CollectionModel *artists_collection_model_; - CollectionModel *albums_collection_model_; - CollectionModel *songs_collection_model_; - - QSortFilterProxyModel *artists_collection_sort_model_; - QSortFilterProxyModel *albums_collection_sort_model_; - QSortFilterProxyModel *songs_collection_sort_model_; - - QTimer *timer_search_delay_; - QTimer *timer_login_attempt_; - - std::shared_ptr artists_request_; - std::shared_ptr albums_request_; - std::shared_ptr songs_request_; - std::shared_ptr search_request_; - QobuzFavoriteRequest *favorite_request_; - - QString app_id_; - QString app_secret_; - QString username_; - QString password_; - int format_; - int search_delay_; - int artistssearchlimit_; - int albumssearchlimit_; - int songssearchlimit_; - bool download_album_covers_; - - qint64 user_id_; - QString user_auth_token_; - QString device_id_; - qint64 credential_id_; - - int pending_search_id_; - int next_pending_search_id_; - QString pending_search_text_; - InternetSearch::SearchType pending_search_type_; - - int search_id_; - QString search_text_; - bool login_sent_; - int login_attempts_; - - QList stream_url_requests_; - - QStringList login_errors_; - - QList wait_for_exit_; - -}; - -#endif // QOBUZSERVICE_H diff --git a/src/qobuz/qobuzstreamurlrequest.cpp b/src/qobuz/qobuzstreamurlrequest.cpp deleted file mode 100644 index b8b34254..00000000 --- a/src/qobuz/qobuzstreamurlrequest.cpp +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/logging.h" -#include "core/network.h" -#include "core/song.h" -#include "core/timeconstants.h" -#include "qobuzservice.h" -#include "qobuzbaserequest.h" -#include "qobuzstreamurlrequest.h" - -QobuzStreamURLRequest::QobuzStreamURLRequest(QobuzService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent) - : QobuzBaseRequest(service, network, parent), - service_(service), - reply_(nullptr), - original_url_(original_url), - song_id_(original_url.path().toInt()), - tries_(0), - need_login_(false) {} - -QobuzStreamURLRequest::~QobuzStreamURLRequest() { - - if (reply_) { - disconnect(reply_, 0, this, 0); - if (reply_->isRunning()) reply_->abort(); - reply_->deleteLater(); - } - -} - -void QobuzStreamURLRequest::LoginComplete(const bool success, const QString &error) { - - if (!need_login_) return; - need_login_ = false; - - if (!success) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); - return; - } - - Process(); - -} - -void QobuzStreamURLRequest::Process() { - - if (app_id().isEmpty() || app_secret().isEmpty()) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Missing Qobuz app ID or secret.")); - return; - } - - if (!authenticated()) { - need_login_ = true; - emit TryLogin(); - return; - } - GetStreamURL(); - -} - -void QobuzStreamURLRequest::Cancel() { - - if (reply_ && reply_->isRunning()) { - reply_->abort(); - } - else { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Cancelled.")); - } - -} - -void QobuzStreamURLRequest::GetStreamURL() { - - ++tries_; - - if (reply_) { - disconnect(reply_, 0, this, 0); - if (reply_->isRunning()) reply_->abort(); - reply_->deleteLater(); - } - - QByteArray appid = app_id().toUtf8(); - QByteArray secret_decoded = QByteArray::fromBase64(app_secret().toUtf8()); - QString secret; - for (int x = 0, y = 0; x < secret_decoded.length(); ++x , ++y) { - if (y == appid.length()) y = 0; - secret.append(QChar(secret_decoded[x] ^ appid[y])); - } - - quint64 timestamp = QDateTime::currentDateTime().toTime_t(); - - ParamList params_to_sign = ParamList() << Param("format_id", QString::number(format())) - << Param("track_id", QString::number(song_id_)); - - std::sort(params_to_sign.begin(), params_to_sign.end()); - - QString data_to_sign; - data_to_sign += "trackgetFileUrl"; - for (const Param ¶m : params_to_sign) { - data_to_sign += param.first + param.second; - } - data_to_sign += QString::number(timestamp); - data_to_sign += secret.toUtf8(); - - QByteArray const digest = QCryptographicHash::hash(data_to_sign.toUtf8(), QCryptographicHash::Md5); - QString signature = QString::fromLatin1(digest.toHex()).rightJustified(32, '0').toLower(); - - ParamList params = params_to_sign; - params << Param("request_ts", QString::number(timestamp)); - params << Param("request_sig", signature); - params << Param("user_auth_token", user_auth_token()); - - std::sort(params.begin(), params.end()); - - reply_ = CreateRequest(QString("track/getFileUrl"), params); - connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived())); - -} - -void QobuzStreamURLRequest::StreamURLReceived() { - - if (!reply_) return; - - QByteArray data = GetReplyData(reply_); - - disconnect(reply_, 0, this, 0); - reply_->deleteLater(); - reply_ = nullptr; - - if (data.isEmpty()) { - if (!authenticated() && login_sent() && tries_ <= 1) { - need_login_ = true; - return; - } - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - - if (!json_obj.contains("track_id")) { - Error("Invalid Json reply, stream url is missing track_id.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - - int track_id = json_obj["track_id"].toInt(); - if (track_id != song_id_) { - Error("Incorrect track ID returned.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - - if (!json_obj.contains("mime_type") || !json_obj.contains("url")) { - Error("Invalid Json reply, stream url is missing url or mime_type.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - - QUrl url(json_obj["url"].toString()); - QString mimetype = json_obj["mime_type"].toString(); - - Song::FileType filetype(Song::FileType_Unknown); - QMimeDatabase mimedb; - for (QString suffix : mimedb.mimeTypeForName(mimetype.toUtf8()).suffixes()) { - filetype = Song::FiletypeByExtension(suffix); - if (filetype != Song::FileType_Unknown) break; - } - if (filetype == Song::FileType_Unknown) { - qLog(Debug) << "Qobuz: Unknown mimetype" << mimetype; - filetype = Song::FileType_Stream; - } - - if (!url.isValid()) { - Error("Returned stream url is invalid.", json_obj); - emit StreamURLFinished(original_url_, original_url_, filetype, -1, -1, -1, errors_.first()); - return; - } - - qint64 duration = -1; - if (json_obj.contains("duration")) { - duration = json_obj["duration"].toDouble() * kNsecPerSec; - } - int samplerate = -1; - if (json_obj.contains("sampling_rate")) { - samplerate = json_obj["sampling_rate"].toDouble() * 1000; - } - int bit_depth = -1; - if (json_obj.contains("bit_depth")) { - bit_depth = json_obj["bit_depth"].toDouble(); - } - - emit StreamURLFinished(original_url_, url, filetype, samplerate, bit_depth, duration); - -} - -void QobuzStreamURLRequest::Error(const QString &error, const QVariant &debug) { - - if (!error.isEmpty()) { - qLog(Error) << "Qobuz:" << error; - errors_ << error; - } - if (debug.isValid()) qLog(Debug) << debug; - -} - diff --git a/src/qobuz/qobuzstreamurlrequest.h b/src/qobuz/qobuzstreamurlrequest.h deleted file mode 100644 index 568d7f53..00000000 --- a/src/qobuz/qobuzstreamurlrequest.h +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef QOBUZSTREAMURLREQUEST_H -#define QOBUZSTREAMURLREQUEST_H - -#include "config.h" - -#include -#include -#include -#include -#include -#include - -#include "core/song.h" -#include "qobuzbaserequest.h" - -class QNetworkReply; -class NetworkAccessManager; -class QobuzService; - -class QobuzStreamURLRequest : public QobuzBaseRequest { - Q_OBJECT - - public: - QobuzStreamURLRequest(QobuzService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent); - ~QobuzStreamURLRequest(); - - void GetStreamURL(); - void Process(); - void NeedLogin() { need_login_ = true; } - void Cancel(); - - QUrl original_url() { return original_url_; } - int song_id() { return song_id_; } - bool need_login() { return need_login_; } - - signals: - void TryLogin(); - void StreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString()); - - private slots: - void LoginComplete(const bool success, const QString &error = QString()); - void StreamURLReceived(); - - private: - void Error(const QString &error, const QVariant &debug = QVariant()); - - QobuzService *service_; - QNetworkReply *reply_; - QUrl original_url_; - int song_id_; - int tries_; - bool need_login_; - QStringList errors_; - -}; - -#endif // QOBUZSTREAMURLREQUEST_H diff --git a/src/qobuz/qobuzurlhandler.cpp b/src/qobuz/qobuzurlhandler.cpp deleted file mode 100644 index e4a3d27c..00000000 --- a/src/qobuz/qobuzurlhandler.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#include -#include -#include - -#include "core/application.h" -#include "core/taskmanager.h" -#include "core/song.h" -#include "qobuz/qobuzservice.h" -#include "qobuzurlhandler.h" - -QobuzUrlHandler::QobuzUrlHandler(Application *app, QobuzService *service) : - UrlHandler(service), - app_(app), - service_(service), - task_id_(-1) - { - - connect(service, SIGNAL(StreamURLFinished(const QUrl&, const QUrl&, const Song::FileType, const int, const int, const qint64, QString)), this, SLOT(GetStreamURLFinished(const QUrl&, const QUrl&, const Song::FileType, const int, const int, const qint64, QString))); - -} - -UrlHandler::LoadResult QobuzUrlHandler::StartLoading(const QUrl &url) { - - LoadResult ret(url); - if (task_id_ != -1) return ret; - task_id_ = app_->task_manager()->StartTask(QString("Loading %1 stream...").arg(url.scheme())); - service_->GetStreamURL(url); - ret.type_ = LoadResult::WillLoadAsynchronously; - return ret; - -} - -void QobuzUrlHandler::GetStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error) { - - if (task_id_ == -1) return; - CancelTask(); - if (error.isEmpty()) - emit AsyncLoadComplete(LoadResult(original_url, LoadResult::TrackAvailable, stream_url, filetype, samplerate, bit_depth, duration)); - else - emit AsyncLoadComplete(LoadResult(original_url, LoadResult::Error, stream_url, filetype, -1, -1, -1, error)); - -} - -void QobuzUrlHandler::CancelTask() { - app_->task_manager()->SetTaskFinished(task_id_); - task_id_ = -1; -} diff --git a/src/qobuz/qobuzurlhandler.h b/src/qobuz/qobuzurlhandler.h deleted file mode 100644 index 2650f5a2..00000000 --- a/src/qobuz/qobuzurlhandler.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef QOBUZURLHANDLER_H -#define QOBUZURLHANDLER_H - -#include -#include -#include -#include - -#include "core/urlhandler.h" -#include "core/song.h" -#include "qobuz/qobuzservice.h" - -class Application; - -class QobuzUrlHandler : public UrlHandler { - Q_OBJECT - - public: - QobuzUrlHandler(Application *app, QobuzService *service); - - QString scheme() const { return service_->url_scheme(); } - LoadResult StartLoading(const QUrl &url); - - void CancelTask(); - - private slots: - void GetStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString()); - - private: - Application *app_; - QobuzService *service_; - int task_id_; - -}; - -#endif diff --git a/src/settings/qobuzsettingspage.cpp b/src/settings/qobuzsettingspage.cpp deleted file mode 100644 index 846df5d2..00000000 --- a/src/settings/qobuzsettingspage.cpp +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "settingsdialog.h" -#include "qobuzsettingspage.h" -#include "ui_qobuzsettingspage.h" -#include "core/application.h" -#include "core/iconloader.h" -#include "widgets/loginstatewidget.h" -#include "internet/internetservices.h" -#include "qobuz/qobuzservice.h" - -const char *QobuzSettingsPage::kSettingsGroup = "Qobuz"; - -QobuzSettingsPage::QobuzSettingsPage(SettingsDialog *parent) - : SettingsPage(parent), - ui_(new Ui::QobuzSettingsPage), - service_(dialog()->app()->internet_services()->Service()) { - - ui_->setupUi(this); - setWindowIcon(IconLoader::Load("qobuz")); - - connect(ui_->button_login, SIGNAL(clicked()), SLOT(LoginClicked())); - connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked())); - - connect(this, SIGNAL(Login(QString, QString, QString)), service_, SLOT(SendLogin(QString, QString, QString))); - - connect(service_, SIGNAL(LoginFailure(QString)), SLOT(LoginFailure(QString))); - connect(service_, SIGNAL(LoginSuccess()), SLOT(LoginSuccess())); - - dialog()->installEventFilter(this); - - ui_->format->addItem("MP3 320", 5); - ui_->format->addItem("FLAC Lossless", 6); - ui_->format->addItem("FLAC Hi-Res <= 96kHz", 7); - ui_->format->addItem("FLAC Hi-Res > 96kHz", 27); - -} - -QobuzSettingsPage::~QobuzSettingsPage() { delete ui_; } - -void QobuzSettingsPage::Load() { - - QSettings s; - - s.beginGroup(kSettingsGroup); - ui_->enable->setChecked(s.value("enabled", false).toBool()); - ui_->app_id->setText(s.value("app_id").toString()); - ui_->app_secret->setText(s.value("app_secret").toString()); - - ui_->username->setText(s.value("username").toString()); - QByteArray password = s.value("password").toByteArray(); - if (password.isEmpty()) ui_->password->clear(); - else ui_->password->setText(QString::fromUtf8(QByteArray::fromBase64(password))); - - dialog()->ComboBoxLoadFromSettings(s, ui_->format, "format", 27); - ui_->searchdelay->setValue(s.value("searchdelay", 1500).toInt()); - ui_->artistssearchlimit->setValue(s.value("artistssearchlimit", 4).toInt()); - ui_->albumssearchlimit->setValue(s.value("albumssearchlimit", 10).toInt()); - ui_->songssearchlimit->setValue(s.value("songssearchlimit", 10).toInt()); - ui_->checkbox_download_album_covers->setChecked(s.value("downloadalbumcovers", true).toBool()); - - s.endGroup(); - - if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); - -} - -void QobuzSettingsPage::Save() { - - QSettings s; - s.beginGroup(kSettingsGroup); - s.setValue("enabled", ui_->enable->isChecked()); - s.setValue("app_id", ui_->app_id->text()); - s.setValue("app_secret", ui_->app_secret->text()); - - s.setValue("username", ui_->username->text()); - s.setValue("password", QString::fromUtf8(ui_->password->text().toUtf8().toBase64())); - - s.setValue("format", ui_->format->itemData(ui_->format->currentIndex())); - s.setValue("searchdelay", ui_->searchdelay->value()); - s.setValue("artistssearchlimit", ui_->artistssearchlimit->value()); - s.setValue("albumssearchlimit", ui_->albumssearchlimit->value()); - s.setValue("songssearchlimit", ui_->songssearchlimit->value()); - s.setValue("downloadalbumcovers", ui_->checkbox_download_album_covers->isChecked()); - s.endGroup(); - - service_->ReloadSettings(); - -} - -void QobuzSettingsPage::LoginClicked() { - - if (ui_->app_id->text().isEmpty() || ui_->username->text().isEmpty() || ui_->password->text().isEmpty()) { - QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing app id, username or password.")); - return; - } - emit Login(ui_->app_id->text(), ui_->username->text(), ui_->password->text()); - ui_->button_login->setEnabled(false); - -} - -bool QobuzSettingsPage::eventFilter(QObject *object, QEvent *event) { - - if (object == dialog() && event->type() == QEvent::Enter) { - ui_->button_login->setEnabled(true); - return false; - } - - return SettingsPage::eventFilter(object, event); - -} - -void QobuzSettingsPage::LogoutClicked() { - service_->Logout(); - ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); - ui_->button_login->setEnabled(true); -} - -void QobuzSettingsPage::LoginSuccess() { - if (!this->isVisible()) return; - ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); - ui_->button_login->setEnabled(true); -} - -void QobuzSettingsPage::LoginFailure(QString failure_reason) { - if (!this->isVisible()) return; - QMessageBox::warning(this, tr("Authentication failed"), failure_reason); -} diff --git a/src/settings/qobuzsettingspage.h b/src/settings/qobuzsettingspage.h deleted file mode 100644 index 21db0a4f..00000000 --- a/src/settings/qobuzsettingspage.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2019, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef QOBUZSETTINGSPAGE_H -#define QOBUZSETTINGSPAGE_H - -#include -#include - -#include "settings/settingspage.h" - -class QEvent; -class SettingsDialog; -class QobuzService; -class Ui_QobuzSettingsPage; - -class QobuzSettingsPage : public SettingsPage { - Q_OBJECT - - public: - explicit QobuzSettingsPage(SettingsDialog* parent = nullptr); - ~QobuzSettingsPage(); - - static const char *kSettingsGroup; - - void Load(); - void Save(); - - bool eventFilter(QObject *object, QEvent *event); - - signals: - void Login(); - void Login(const QString &username, const QString &password, const QString &token); - - private slots: - void LoginClicked(); - void LogoutClicked(); - void LoginSuccess(); - void LoginFailure(QString failure_reason); - - private: - Ui_QobuzSettingsPage* ui_; - QobuzService *service_; -}; - -#endif diff --git a/src/settings/qobuzsettingspage.ui b/src/settings/qobuzsettingspage.ui deleted file mode 100644 index 6967b321..00000000 --- a/src/settings/qobuzsettingspage.ui +++ /dev/null @@ -1,293 +0,0 @@ - - - QobuzSettingsPage - - - - 0 - 0 - 715 - 836 - - - - Qobuz - - - - - - Enable - - - - - - - - 0 - 0 - - - - Authentication - - - - - - - 150 - 0 - - - - App ID - - - - - - - - - - Username - - - - - - - - - - - - - - Password - - - - - - - QLineEdit::Password - - - - - - - App Secret - - - - - - - - - - - - - Login - - - - - - - - - - Preferences - - - - - - Audio format - - - - - - - - - - Search delay - - - - - - - ms - - - 0 - - - 10000 - - - 50 - - - 1500 - - - - - - - Artists search limit - - - - - - - 1 - - - 100 - - - 50 - - - - - - - Albums search limit - - - - - - - 1 - - - 1000 - - - 50 - - - - - - - Songs search limit - - - - - - - 1 - - - 1000 - - - 50 - - - - - - - Download album covers - - - - - - - - - - Qt::Vertical - - - - 20 - 30 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 64 - 64 - - - - - 64 - 64 - - - - :/icons/64x64/qobuz.png - - - - - - - - - - LoginStateWidget - QWidget -
    widgets/loginstatewidget.h
    - 1 -
    -
    - - enable - app_id - app_secret - username - password - button_login - format - searchdelay - artistssearchlimit - albumssearchlimit - songssearchlimit - checkbox_download_album_covers - - - - - - -
    diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp index c68c9c5d..09eac1f8 100644 --- a/src/settings/settingsdialog.cpp +++ b/src/settings/settingsdialog.cpp @@ -67,12 +67,6 @@ #ifdef HAVE_MOODBAR # include "moodbarsettingspage.h" #endif -#ifdef HAVE_TIDAL -# include "tidalsettingspage.h" -#endif -#ifdef HAVE_QOBUZ -# include "qobuzsettingspage.h" -#endif #ifdef HAVE_SUBSONIC # include "subsonicsettingspage.h" #endif @@ -149,15 +143,9 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent) AddPage(Page_Moodbar, new MoodbarSettingsPage(this), iface); #endif -#if defined(HAVE_TIDAL) || defined(HAVE_SUBSONIC) || defined(HAVE_QOBUZ) +#if defined(HAVE_SUBSONIC) QTreeWidgetItem *streaming = AddCategory(tr("Streaming")); #endif -#ifdef HAVE_TIDAL - AddPage(Page_Tidal, new TidalSettingsPage(this), streaming); -#endif -#ifdef HAVE_QOBUZ - AddPage(Page_Qobuz, new QobuzSettingsPage(this), streaming); -#endif #ifdef HAVE_SUBSONIC AddPage(Page_Subsonic, new SubsonicSettingsPage(this), streaming); #endif diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h index 993bfc30..5de9c55b 100644 --- a/src/settings/settingsdialog.h +++ b/src/settings/settingsdialog.h @@ -84,9 +84,7 @@ class SettingsDialog : public QDialog { Page_Proxy, Page_Scrobbler, Page_Moodbar, - Page_Tidal, - Page_Subsonic, - Page_Qobuz, + Page_Subsonic }; enum Role { diff --git a/src/settings/tidalsettingspage.cpp b/src/settings/tidalsettingspage.cpp deleted file mode 100644 index 841cacea..00000000 --- a/src/settings/tidalsettingspage.cpp +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "settingsdialog.h" -#include "tidalsettingspage.h" -#include "ui_tidalsettingspage.h" -#include "core/application.h" -#include "core/iconloader.h" -#include "internet/internetservices.h" -#include "tidal/tidalservice.h" -#include "widgets/loginstatewidget.h" - -const char *TidalSettingsPage::kSettingsGroup = "Tidal"; - -TidalSettingsPage::TidalSettingsPage(SettingsDialog *parent) - : SettingsPage(parent), - ui_(new Ui::TidalSettingsPage), - service_(dialog()->app()->internet_services()->Service()) { - - ui_->setupUi(this); - setWindowIcon(IconLoader::Load("tidal")); - - connect(ui_->button_login, SIGNAL(clicked()), SLOT(LoginClicked())); - connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked())); - connect(ui_->oauth, SIGNAL(toggled(bool)), SLOT(OAuthClicked(bool))); - - connect(this, SIGNAL(Login()), service_, SLOT(StartAuthorisation())); - connect(this, SIGNAL(Login(const QString&, const QString&, const QString&)), service_, SLOT(SendLogin(const QString&, const QString&, const QString&))); - - connect(service_, SIGNAL(LoginFailure(const QString&)), SLOT(LoginFailure(const QString&))); - connect(service_, SIGNAL(LoginSuccess()), SLOT(LoginSuccess())); - - dialog()->installEventFilter(this); - - ui_->quality->addItem("Low", "LOW"); - ui_->quality->addItem("High", "HIGH"); - ui_->quality->addItem("Lossless", "LOSSLESS"); - ui_->quality->addItem("Hi resolution", "HI_RES"); - - ui_->coversize->addItem("160x160", "160x160"); - ui_->coversize->addItem("320x320", "320x320"); - ui_->coversize->addItem("640x640", "640x640"); - ui_->coversize->addItem("750x750", "750x750"); - ui_->coversize->addItem("1280x1280", "1280x1280"); - - ui_->streamurl->addItem("streamurl", StreamUrlMethod_StreamUrl); - ui_->streamurl->addItem("urlpostpaywall", StreamUrlMethod_UrlPostPaywall); - ui_->streamurl->addItem("playbackinfopostpaywall", StreamUrlMethod_PlaybackInfoPostPaywall); - -} - -TidalSettingsPage::~TidalSettingsPage() { delete ui_; } - -void TidalSettingsPage::Load() { - - QSettings s; - - s.beginGroup(kSettingsGroup); - ui_->enable->setChecked(s.value("enabled", false).toBool()); - ui_->oauth->setChecked(s.value("oauth", false).toBool()); - - ui_->client_id->setText(s.value("client_id").toString()); - ui_->api_token->setText(s.value("api_token").toString()); - - ui_->username->setText(s.value("username").toString()); - QByteArray password = s.value("password").toByteArray(); - if (password.isEmpty()) ui_->password->clear(); - else ui_->password->setText(QString::fromUtf8(QByteArray::fromBase64(password))); - - dialog()->ComboBoxLoadFromSettings(s, ui_->quality, "quality", "HIGH"); - ui_->searchdelay->setValue(s.value("searchdelay", 1500).toInt()); - ui_->artistssearchlimit->setValue(s.value("artistssearchlimit", 4).toInt()); - ui_->albumssearchlimit->setValue(s.value("albumssearchlimit", 10).toInt()); - ui_->songssearchlimit->setValue(s.value("songssearchlimit", 10).toInt()); - ui_->checkbox_fetchalbums->setChecked(s.value("fetchalbums", false).toBool()); - ui_->checkbox_download_album_covers->setChecked(s.value("downloadalbumcovers", true).toBool()); - dialog()->ComboBoxLoadFromSettings(s, ui_->coversize, "coversize", "320x320"); - - StreamUrlMethod stream_url = static_cast(s.value("streamurl").toInt()); - int i = ui_->streamurl->findData(stream_url); - if (i == -1) i = ui_->streamurl->findData(StreamUrlMethod_StreamUrl); - ui_->streamurl->setCurrentIndex(i); - - s.endGroup(); - - OAuthClicked(ui_->oauth->isChecked()); - if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); - -} - -void TidalSettingsPage::Save() { - - QSettings s; - s.beginGroup(kSettingsGroup); - s.setValue("enabled", ui_->enable->isChecked()); - s.setValue("oauth", ui_->oauth->isChecked()); - s.setValue("client_id", ui_->client_id->text()); - s.setValue("api_token", ui_->api_token->text()); - - s.setValue("username", ui_->username->text()); - s.setValue("password", QString::fromUtf8(ui_->password->text().toUtf8().toBase64())); - - s.setValue("quality", ui_->quality->itemData(ui_->quality->currentIndex())); - s.setValue("searchdelay", ui_->searchdelay->value()); - s.setValue("artistssearchlimit", ui_->artistssearchlimit->value()); - s.setValue("albumssearchlimit", ui_->albumssearchlimit->value()); - s.setValue("songssearchlimit", ui_->songssearchlimit->value()); - s.setValue("fetchalbums", ui_->checkbox_fetchalbums->isChecked()); - s.setValue("downloadalbumcovers", ui_->checkbox_download_album_covers->isChecked()); - s.setValue("coversize", ui_->coversize->itemData(ui_->coversize->currentIndex())); - s.setValue("streamurl", ui_->streamurl->itemData(ui_->streamurl->currentIndex())); - s.endGroup(); - - service_->ReloadSettings(); - -} - -void TidalSettingsPage::LoginClicked() { - - if (ui_->oauth->isChecked()) { - if (ui_->client_id->text().isEmpty()) { - QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing Tidal client ID.")); - return; - } - emit Login(); - } - else { - if (ui_->username->text().isEmpty() || ui_->password->text().isEmpty()) { - QMessageBox::critical(this, tr("Configuration incomplete"), tr("Missing username or password.")); - return; - } - emit Login(ui_->api_token->text(), ui_->username->text(), ui_->password->text()); - } - ui_->button_login->setEnabled(false); - -} - -bool TidalSettingsPage::eventFilter(QObject *object, QEvent *event) { - - if (object == dialog() && event->type() == QEvent::Enter) { - ui_->button_login->setEnabled(true); - return false; - } - - return SettingsPage::eventFilter(object, event); - -} - -void TidalSettingsPage::OAuthClicked(const bool enabled) { - - ui_->client_id->setEnabled(enabled); - ui_->api_token->setEnabled(!enabled); - ui_->username->setEnabled(!enabled); - ui_->password->setEnabled(!enabled); - -} - -void TidalSettingsPage::LogoutClicked() { - service_->Logout(); - ui_->button_login->setEnabled(true); - ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); -} - -void TidalSettingsPage::LoginSuccess() { - if (!this->isVisible()) return; - ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); - ui_->button_login->setEnabled(true); -} - -void TidalSettingsPage::LoginFailure(const QString &failure_reason) { - if (!this->isVisible()) return; - QMessageBox::warning(this, tr("Authentication failed"), failure_reason); - ui_->button_login->setEnabled(true); -} diff --git a/src/settings/tidalsettingspage.h b/src/settings/tidalsettingspage.h deleted file mode 100644 index 87cd6a6c..00000000 --- a/src/settings/tidalsettingspage.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef TIDALSETTINGSPAGE_H -#define TIDALSETTINGSPAGE_H - -#include "config.h" - -#include -#include - -#include "settings/settingspage.h" - -class QEvent; -class TidalService; -class SettingsDialog; -class Ui_TidalSettingsPage; - -class TidalSettingsPage : public SettingsPage { - Q_OBJECT - - public: - explicit TidalSettingsPage(SettingsDialog* parent = nullptr); - ~TidalSettingsPage(); - - static const char *kSettingsGroup; - - enum StreamUrlMethod { - StreamUrlMethod_StreamUrl, - StreamUrlMethod_UrlPostPaywall, - StreamUrlMethod_PlaybackInfoPostPaywall, - }; - - void Load(); - void Save(); - - bool eventFilter(QObject *object, QEvent *event); - - signals: - void Login(); - void Login(const QString &api_token, const QString &username, const QString &password); - - private slots: - void OAuthClicked(const bool enabled); - void LoginClicked(); - void LogoutClicked(); - void LoginSuccess(); - void LoginFailure(const QString &failure_reason); - - private: - Ui_TidalSettingsPage* ui_; - TidalService *service_; -}; - -#endif diff --git a/src/settings/tidalsettingspage.ui b/src/settings/tidalsettingspage.ui deleted file mode 100644 index 2541762e..00000000 --- a/src/settings/tidalsettingspage.ui +++ /dev/null @@ -1,344 +0,0 @@ - - - TidalSettingsPage - - - - 0 - 0 - 715 - 836 - - - - Tidal - - - - - - Enable - - - - - - - - 0 - 0 - - - - Authentication - - - - - - Use OAuth - - - - - - - - 150 - 0 - - - - Client ID - - - - - - - - - - - 150 - 0 - - - - API Token - - - - - - - - 200 - 0 - - - - - - - - Username - - - - - - - - - - - - - - Password - - - - - - - QLineEdit::Password - - - - - - - - - - Login - - - - - - - - - - Preferences - - - - - - Audio quality - - - - - - - - - - Search delay - - - - - - - ms - - - 0 - - - 10000 - - - 50 - - - 1500 - - - - - - - Artists search limit - - - - - - - 1 - - - 100 - - - 50 - - - - - - - Albums search limit - - - - - - - 1 - - - 1000 - - - 50 - - - - - - - Songs search limit - - - - - - - Download album covers - - - - - - - Fetch entire albums when searching songs - - - - - - - - - - Album cover size - - - - - - - 1 - - - 1000 - - - 50 - - - - - - - Stream URL method - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 30 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 64 - 64 - - - - - 64 - 64 - - - - :/icons/64x64/tidal.png - - - - - - - - - - LoginStateWidget - QWidget -
    widgets/loginstatewidget.h
    - 1 -
    -
    - - enable - oauth - client_id - api_token - username - password - button_login - quality - searchdelay - artistssearchlimit - albumssearchlimit - songssearchlimit - checkbox_download_album_covers - checkbox_fetchalbums - coversize - streamurl - - - - - - -
    diff --git a/src/tidal/tidalbaserequest.cpp b/src/tidal/tidalbaserequest.cpp deleted file mode 100644 index 57ca9947..00000000 --- a/src/tidal/tidalbaserequest.cpp +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/logging.h" -#include "core/network.h" -#include "tidalservice.h" -#include "tidalbaserequest.h" - -const char *TidalBaseRequest::kApiUrl = "https://api.tidalhifi.com/v1"; - -TidalBaseRequest::TidalBaseRequest(TidalService *service, NetworkAccessManager *network, QObject *parent) : - QObject(parent), - service_(service), - network_(network) - {} - -TidalBaseRequest::~TidalBaseRequest() {} - -QNetworkReply *TidalBaseRequest::CreateRequest(const QString &ressource_name, const QList ¶ms_provided) { - - ParamList params = ParamList() << params_provided - << Param("countryCode", country_code()); - - QUrlQuery url_query; - for (const Param& param : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QUrl url(kApiUrl + QString("/") + ressource_name); - url.setQuery(url_query); - QNetworkRequest req(url); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -#endif - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8()); - if (!session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8()); - - QNetworkReply *reply = network_->get(req); - connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleSSLErrors(QList))); - - //qLog(Debug) << "Tidal: Sending request" << url; - - return reply; - -} - -void TidalBaseRequest::HandleSSLErrors(QList ssl_errors) { - - for (QSslError &ssl_error : ssl_errors) { - Error(ssl_error.errorString()); - } - -} - -QByteArray TidalBaseRequest::GetReplyData(QNetworkReply *reply, const bool send_login) { - - 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 { - // See if there is Json data containing "status" and "userMessage" - then use that instead. - data = reply->readAll(); - QString error; - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - int status = 0; - int sub_status = 0; - if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { - QJsonObject json_obj = json_doc.object(); - if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) { - status = json_obj["status"].toInt(); - sub_status = json_obj["subStatus"].toInt(); - QString user_message = json_obj["userMessage"].toString(); - error = QString("%1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); - } - } - 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()); - } - if (status == 401 && sub_status == 6001) { // User does not have a valid session - emit service_->Logout(); - if (!oauth() && send_login && login_attempts() < max_login_attempts() && !api_token().isEmpty() && !username().isEmpty() && !password().isEmpty()) { - qLog(Error) << "Tidal:" << error; - qLog(Info) << "Tidal:" << "Attempting to login."; - NeedLogin(); - emit service_->Login(); - } - else { - Error(error); - } - } - else { - Error(error); - } - } - return QByteArray(); - } - - return data; - -} - -QJsonObject TidalBaseRequest::ExtractJsonObj(QByteArray &data) { - - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_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.", data); - 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; - -} - -QJsonValue TidalBaseRequest::ExtractItems(QByteArray &data) { - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) return QJsonValue(); - return ExtractItems(json_obj); - -} - -QJsonValue TidalBaseRequest::ExtractItems(QJsonObject &json_obj) { - - if (!json_obj.contains("items")) { - Error("Json reply is missing items.", json_obj); - return QJsonArray(); - } - QJsonValue json_items = json_obj["items"]; - return json_items; - -} - -QString TidalBaseRequest::ErrorsToHTML(const QStringList &errors) { - - QString error_html; - for (const QString &error : errors) { - error_html += error + "
    "; - } - return error_html; - -} diff --git a/src/tidal/tidalbaserequest.h b/src/tidal/tidalbaserequest.h deleted file mode 100644 index c18c8dde..00000000 --- a/src/tidal/tidalbaserequest.h +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef TIDALBASEREQUEST_H -#define TIDALBASEREQUEST_H - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tidalservice.h" - -class QNetworkReply; -class NetworkAccessManager; - -class TidalBaseRequest : public QObject { - Q_OBJECT - - public: - - enum QueryType { - QueryType_None, - QueryType_Artists, - QueryType_Albums, - QueryType_Songs, - QueryType_SearchArtists, - QueryType_SearchAlbums, - QueryType_SearchSongs, - QueryType_StreamURL, - }; - - TidalBaseRequest(TidalService *service, NetworkAccessManager *network, QObject *parent); - ~TidalBaseRequest(); - - typedef QPair Param; - typedef QList ParamList; - - typedef QPair EncodedParam; - typedef QList EncodedParamList; - - QNetworkReply *CreateRequest(const QString &ressource_name, const QList ¶ms_provided); - QByteArray GetReplyData(QNetworkReply *reply, const bool send_login); - QJsonObject ExtractJsonObj(QByteArray &data); - QJsonValue ExtractItems(QByteArray &data); - QJsonValue ExtractItems(QJsonObject &json_obj); - - virtual void Error(const QString &error, const QVariant &debug = QVariant()) = 0; - QString ErrorsToHTML(const QStringList &errors); - - QString api_url() { return QString(kApiUrl); } - bool oauth() { return service_->oauth(); } - QString client_id() { return service_->client_id(); } - QString api_token() { return service_->api_token(); } - quint64 user_id() { return service_->user_id(); } - QString country_code() { return service_->country_code(); } - QString username() { return service_->username(); } - QString password() { return service_->password(); } - QString quality() { return service_->quality(); } - int artistssearchlimit() { return service_->artistssearchlimit(); } - int albumssearchlimit() { return service_->albumssearchlimit(); } - int songssearchlimit() { return service_->songssearchlimit(); } - - QString access_token() { return service_->access_token(); } - QString session_id() { return service_->session_id(); } - - bool authenticated() { return service_->authenticated(); } - bool login_sent() { return service_->login_sent(); } - int max_login_attempts() { return service_->max_login_attempts(); } - int login_attempts() { return service_->login_attempts(); } - - virtual void NeedLogin() = 0; - - private slots: - void HandleSSLErrors(QList ssl_errors); - - private: - - static const char *kApiUrl; - - TidalService *service_; - NetworkAccessManager *network_; - -}; - -#endif // TIDALBASEREQUEST_H diff --git a/src/tidal/tidalfavoriterequest.cpp b/src/tidal/tidalfavoriterequest.cpp deleted file mode 100644 index 7676f74b..00000000 --- a/src/tidal/tidalfavoriterequest.cpp +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/logging.h" -#include "core/network.h" -#include "core/closure.h" -#include "core/song.h" -#include "tidalservice.h" -#include "tidalbaserequest.h" -#include "tidalfavoriterequest.h" - -TidalFavoriteRequest::TidalFavoriteRequest(TidalService *service, NetworkAccessManager *network, QObject *parent) - : TidalBaseRequest(service, network, parent), - service_(service), - network_(network), - need_login_(false) {} - -TidalFavoriteRequest::~TidalFavoriteRequest() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - disconnect(reply, 0, this, 0); - reply->abort(); - reply->deleteLater(); - } - -} - -QString TidalFavoriteRequest::FavoriteText(const FavoriteType type) { - - switch (type) { - case FavoriteType_Artists: - return "artists"; - case FavoriteType_Albums: - return "albums"; - case FavoriteType_Songs: - default: - return "tracks"; - } - -} - -void TidalFavoriteRequest::AddArtists(const SongList &songs) { - AddFavorites(FavoriteType_Artists, songs); -} - -void TidalFavoriteRequest::AddAlbums(const SongList &songs) { - AddFavorites(FavoriteType_Albums, songs); -} - -void TidalFavoriteRequest::AddSongs(const SongList &songs) { - AddFavorites(FavoriteType_Songs, songs); -} - -void TidalFavoriteRequest::AddFavorites(const FavoriteType type, const SongList &songs) { - - if (songs.isEmpty()) return; - - QString text; - switch (type) { - case FavoriteType_Artists: - text = "artistIds"; - break; - case FavoriteType_Albums: - text = "albumIds"; - break; - case FavoriteType_Songs: - text = "trackIds"; - break; - } - - QStringList ids_list; - for (const Song &song : songs) { - QString id; - switch (type) { - case FavoriteType_Artists: - if (song.artist_id() <= 0) continue; - id = QString::number(song.artist_id()); - break; - case FavoriteType_Albums: - if (song.album_id().isEmpty()) continue; - id = song.album_id(); - break; - case FavoriteType_Songs: - if (song.song_id() <= 0) continue; - id = QString::number(song.song_id()); - break; - } - if (id.isEmpty()) continue; - if (!ids_list.contains(id)) { - ids_list << id; - } - } - if (ids_list.isEmpty()) return; - - QString ids = ids_list.join(','); - - typedef QPair EncodedParam; - - ParamList params = ParamList() << Param("countryCode", country_code()) - << Param(text, ids); - - QUrlQuery url_query; - for (const Param& param : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QUrl url(api_url() + QString("/") + "users/" + QString::number(service_->user_id()) + "/favorites/" + FavoriteText(type)); - QNetworkRequest req(url); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -#endif - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8()); - if (!session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8()); - QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8(); - QNetworkReply *reply = network_->post(req, query); - NewClosure(reply, SIGNAL(finished()), this, SLOT(AddFavoritesReply(QNetworkReply*, const FavoriteType, const SongList&)), reply, type, songs); - replies_ << reply; - - qLog(Debug) << "Tidal: Sending request" << url << query; - -} - -void TidalFavoriteRequest::AddFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs) { - - if (replies_.contains(reply)) { - replies_.removeAll(reply); - reply->deleteLater(); - } - else { - return; - } - - QString error; - QByteArray data = GetReplyData(reply, false); - - if (reply->error() != QNetworkReply::NoError) { - return; - } - - qLog(Debug) << "Tidal:" << songs.count() << "songs added to" << FavoriteText(type) << "favorites."; - - switch (type) { - case FavoriteType_Artists: - emit ArtistsAdded(songs); - break; - case FavoriteType_Albums: - emit AlbumsAdded(songs); - break; - case FavoriteType_Songs: - emit SongsAdded(songs); - break; - } - -} - -void TidalFavoriteRequest::RemoveArtists(const SongList &songs) { - RemoveFavorites(FavoriteType_Artists, songs); -} - -void TidalFavoriteRequest::RemoveAlbums(const SongList &songs) { - RemoveFavorites(FavoriteType_Albums, songs); -} - -void TidalFavoriteRequest::RemoveSongs(const SongList &songs) { - RemoveFavorites(FavoriteType_Songs, songs); -} - -void TidalFavoriteRequest::RemoveFavorites(const FavoriteType type, const SongList songs) { - - if (songs.isEmpty()) return; - - QList ids; - QMultiMap songs_map; - for (const Song &song : songs) { - qint64 id = -1; - switch (type) { - case FavoriteType_Artists: - if (song.artist_id() <= 0) continue; - id = song.artist_id(); - break; - case FavoriteType_Albums: - if (song.album_id().isEmpty()) continue; - id = song.album_id().toLongLong(); - break; - case FavoriteType_Songs: - if (song.song_id() <= 0) continue; - id = song.song_id(); - break; - } - if (!ids.contains(id)) ids << id; - songs_map.insertMulti(id, song); - } - - for (int id : ids) { - SongList songs_list = songs_map.values(id); - RemoveFavorites(type, id, songs_list); - } - -} - -void TidalFavoriteRequest::RemoveFavorites(const FavoriteType type, const int id, const SongList &songs) { - - ParamList params = ParamList() << Param("countryCode", country_code()); - - QUrlQuery url_query; - for (const Param& param : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QUrl url(api_url() + QString("/") + "users/" + QString::number(service_->user_id()) + "/favorites/" + FavoriteText(type) + QString("/") + QString::number(id)); - url.setQuery(url_query); - QNetworkRequest req(url); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -#endif - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - if (!access_token().isEmpty()) req.setRawHeader("authorization", "Bearer " + access_token().toUtf8()); - if (!session_id().isEmpty()) req.setRawHeader("X-Tidal-SessionId", session_id().toUtf8()); - QNetworkReply *reply = network_->deleteResource(req); - NewClosure(reply, SIGNAL(finished()), this, SLOT(RemoveFavoritesReply(QNetworkReply*, const FavoriteType, const SongList&)), reply, type, songs); - replies_ << reply; - - qLog(Debug) << "Tidal: Sending request" << url << "with" << songs.count() << "songs"; - -} - -void TidalFavoriteRequest::RemoveFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs) { - - if (replies_.contains(reply)) { - replies_.removeAll(reply); - reply->deleteLater(); - } - else { - return; - } - - QString error; - QByteArray data = GetReplyData(reply, false); - if (reply->error() != QNetworkReply::NoError) { - return; - } - - qLog(Debug) << "Tidal:" << songs.count() << "songs removed from" << FavoriteText(type) << "favorites."; - - switch (type) { - case FavoriteType_Artists: - emit ArtistsRemoved(songs); - break; - case FavoriteType_Albums: - emit AlbumsRemoved(songs); - break; - case FavoriteType_Songs: - emit SongsRemoved(songs); - break; - } - -} - -void TidalFavoriteRequest::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "Tidal:" << error; - if (debug.isValid()) qLog(Debug) << debug; - -} diff --git a/src/tidal/tidalfavoriterequest.h b/src/tidal/tidalfavoriterequest.h deleted file mode 100644 index 34b7994c..00000000 --- a/src/tidal/tidalfavoriterequest.h +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef TIDALFAVORITEREQUEST_H -#define TIDALFAVORITEREQUEST_H - -#include "config.h" - -#include -#include -#include -#include - -#include "tidalbaserequest.h" -#include "core/song.h" - -class QNetworkReply; -class TidalService; -class NetworkAccessManager; - -class TidalFavoriteRequest : public TidalBaseRequest { - Q_OBJECT - - public: - TidalFavoriteRequest(TidalService *service, NetworkAccessManager *network, QObject *parent); - ~TidalFavoriteRequest(); - - enum FavoriteType { - FavoriteType_Artists, - FavoriteType_Albums, - FavoriteType_Songs - }; - - bool need_login() { return need_login_; } - - void NeedLogin() { need_login_ = true; } - - signals: - void ArtistsAdded(const SongList &songs); - void AlbumsAdded(const SongList &songs); - void SongsAdded(const SongList &songs); - void ArtistsRemoved(const SongList &songs); - void AlbumsRemoved(const SongList &songs); - void SongsRemoved(const SongList &songs); - - private slots: - void AddArtists(const SongList &songs); - void AddAlbums(const SongList &songs); - void AddSongs(const SongList &songs); - - void RemoveArtists(const SongList &songs); - void RemoveAlbums(const SongList &songs); - void RemoveSongs(const SongList &songs); - - void AddFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs); - void RemoveFavoritesReply(QNetworkReply *reply, const FavoriteType type, const SongList &songs); - - private: - void Error(const QString &error, const QVariant &debug = QVariant()); - QString FavoriteText(const FavoriteType type); - void AddFavorites(const FavoriteType type, const SongList &songs); - void RemoveFavorites(const FavoriteType type, const SongList songs); - void RemoveFavorites(const FavoriteType type, const int id, const SongList &songs); - - TidalService *service_; - NetworkAccessManager *network_; - QList replies_; - bool need_login_; - -}; - -#endif // TIDALFAVORITEREQUEST_H diff --git a/src/tidal/tidalrequest.cpp b/src/tidal/tidalrequest.cpp deleted file mode 100644 index ede81c35..00000000 --- a/src/tidal/tidalrequest.cpp +++ /dev/null @@ -1,1215 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/closure.h" -#include "core/logging.h" -#include "core/network.h" -#include "core/song.h" -#include "core/timeconstants.h" -#include "core/application.h" -#include "covermanager/albumcoverloader.h" -#include "tidalservice.h" -#include "tidalurlhandler.h" -#include "tidalbaserequest.h" -#include "tidalrequest.h" - -const char *TidalRequest::kResourcesUrl = "https://resources.tidal.com"; -const int TidalRequest::kMaxConcurrentArtistsRequests = 3; -const int TidalRequest::kMaxConcurrentAlbumsRequests = 3; -const int TidalRequest::kMaxConcurrentSongsRequests = 3; -const int TidalRequest::kMaxConcurrentArtistAlbumsRequests = 3; -const int TidalRequest::kMaxConcurrentAlbumSongsRequests = 3; -const int TidalRequest::kMaxConcurrentAlbumCoverRequests = 1; - -TidalRequest::TidalRequest(TidalService *service, TidalUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent) - : TidalBaseRequest(service, network, parent), - service_(service), - url_handler_(url_handler), - app_(app), - network_(network), - type_(type), - fetchalbums_(service->fetchalbums()), - coversize_(service_->coversize()), - query_id_(-1), - finished_(false), - artists_requests_active_(0), - artists_total_(0), - artists_received_(0), - albums_requests_active_(0), - songs_requests_active_(0), - artist_albums_requests_active_(0), - artist_albums_requested_(0), - artist_albums_received_(0), - album_songs_requests_active_(0), - album_songs_requested_(0), - album_songs_received_(0), - album_covers_requests_active_(), - album_covers_requested_(0), - album_covers_received_(0), - need_login_(false), - no_results_(false) {} - -TidalRequest::~TidalRequest() { - - while (!replies_.isEmpty()) { - QNetworkReply *reply = replies_.takeFirst(); - disconnect(reply, 0, this, 0); - if (reply->isRunning()) reply->abort(); - reply->deleteLater(); - } - - while (!album_cover_replies_.isEmpty()) { - QNetworkReply *reply = album_cover_replies_.takeFirst(); - disconnect(reply, 0, this, 0); - if (reply->isRunning()) reply->abort(); - reply->deleteLater(); - } - -} - -void TidalRequest::LoginComplete(const bool success, QString error) { - - if (!need_login_) return; - need_login_ = false; - - if (!success) { - Error(error); - return; - } - - Process(); - -} - -void TidalRequest::Process() { - - if (!service_->authenticated()) { - emit UpdateStatus(query_id_, tr("Authenticating...")); - need_login_ = true; - service_->TryLogin(); - return; - } - - switch (type_) { - case QueryType::QueryType_Artists: - GetArtists(); - break; - case QueryType::QueryType_Albums: - GetAlbums(); - break; - case QueryType::QueryType_Songs: - GetSongs(); - break; - case QueryType::QueryType_SearchArtists: - ArtistsSearch(); - break; - case QueryType::QueryType_SearchAlbums: - AlbumsSearch(); - break; - case QueryType::QueryType_SearchSongs: - SongsSearch(); - break; - default: - Error("Invalid query type."); - break; - } - -} - -void TidalRequest::Search(const int query_id, const QString &search_text) { - query_id_ = query_id; - search_text_ = search_text; -} - -void TidalRequest::GetArtists() { - - emit UpdateStatus(query_id_, tr("Retrieving artists...")); - emit UpdateProgress(query_id_, 0); - AddArtistsRequest(); - -} - -void TidalRequest::AddArtistsRequest(const int offset, const int limit) { - - Request request; - request.limit = limit; - request.offset = offset; - artists_requests_queue_.enqueue(request); - if (artists_requests_active_ < kMaxConcurrentArtistsRequests) FlushArtistsRequests(); - -} - -void TidalRequest::FlushArtistsRequests() { - - while (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) { - - Request request = artists_requests_queue_.dequeue(); - ++artists_requests_active_; - - ParamList parameters; - if (type_ == QueryType_SearchArtists) parameters << Param("query", search_text_); - if (request.limit > 0) parameters << Param("limit", QString::number(request.limit)); - if (request.offset > 0) parameters << Param("offset", QString::number(request.offset)); - QNetworkReply *reply; - if (type_ == QueryType_Artists) { - reply = CreateRequest(QString("users/%1/favorites/artists").arg(service_->user_id()), parameters); - } - if (type_ == QueryType_SearchArtists) { - reply = CreateRequest("search/artists", parameters); - } - if (!reply) continue; - replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistsReplyReceived(QNetworkReply*, const int, const int)), reply, request.limit, request.offset); - - } - -} - -void TidalRequest::GetAlbums() { - - emit UpdateStatus(query_id_, tr("Retrieving albums...")); - emit UpdateProgress(query_id_, 0); - AddAlbumsRequest(); - -} - -void TidalRequest::AddAlbumsRequest(const int offset, const int limit) { - - Request request; - request.limit = limit; - request.offset = offset; - albums_requests_queue_.enqueue(request); - if (albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests(); - -} - -void TidalRequest::FlushAlbumsRequests() { - - while (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) { - - Request request = albums_requests_queue_.dequeue(); - ++albums_requests_active_; - - ParamList parameters; - if (type_ == QueryType_SearchAlbums) parameters << Param("query", search_text_); - if (request.limit > 0) parameters << Param("limit", QString::number(request.limit)); - if (request.offset > 0) parameters << Param("offset", QString::number(request.offset)); - QNetworkReply *reply; - if (type_ == QueryType_Albums) { - reply = CreateRequest(QString("users/%1/favorites/albums").arg(service_->user_id()), parameters); - } - if (type_ == QueryType_SearchAlbums) { - reply = CreateRequest("search/albums", parameters); - } - if (!reply) continue; - replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumsReplyReceived(QNetworkReply*, const int, const int)), reply, request.limit, request.offset); - - } - -} - -void TidalRequest::GetSongs() { - - emit UpdateStatus(query_id_, tr("Retrieving songs...")); - emit UpdateProgress(query_id_, 0); - AddSongsRequest(); - -} - -void TidalRequest::AddSongsRequest(const int offset, const int limit) { - - Request request; - request.limit = limit; - request.offset = offset; - songs_requests_queue_.enqueue(request); - if (songs_requests_active_ < kMaxConcurrentSongsRequests) FlushSongsRequests(); - -} - -void TidalRequest::FlushSongsRequests() { - - while (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentSongsRequests) { - - Request request = songs_requests_queue_.dequeue(); - ++songs_requests_active_; - - ParamList parameters; - if (type_ == QueryType_SearchSongs) parameters << Param("query", search_text_); - if (request.limit > 0) parameters << Param("limit", QString::number(request.limit)); - if (request.offset > 0) parameters << Param("offset", QString::number(request.offset)); - QNetworkReply *reply; - if (type_ == QueryType_Songs) { - reply = CreateRequest(QString("users/%1/favorites/tracks").arg(service_->user_id()), parameters); - } - if (type_ == QueryType_SearchSongs) { - reply = CreateRequest("search/tracks", parameters); - } - if (!reply) continue; - replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(SongsReplyReceived(QNetworkReply*, const int, const int)), reply, request.limit, request.offset); - - } - -} - -void TidalRequest::ArtistsSearch() { - - emit UpdateStatus(query_id_, tr("Searching...")); - emit UpdateProgress(query_id_, 0); - AddArtistsSearchRequest(); - -} - -void TidalRequest::AddArtistsSearchRequest(const int offset) { - - AddArtistsRequest(offset, service_->artistssearchlimit()); - -} - -void TidalRequest::AlbumsSearch() { - - emit UpdateStatus(query_id_, tr("Searching...")); - emit UpdateProgress(query_id_, 0); - AddAlbumsSearchRequest(); - -} - -void TidalRequest::AddAlbumsSearchRequest(const int offset) { - - AddAlbumsRequest(offset, service_->albumssearchlimit()); - -} - -void TidalRequest::SongsSearch() { - - emit UpdateStatus(query_id_, tr("Searching...")); - emit UpdateProgress(query_id_, 0); - AddSongsSearchRequest(); - -} - -void TidalRequest::AddSongsSearchRequest(const int offset) { - - AddSongsRequest(offset, service_->songssearchlimit()); - -} - -void TidalRequest::ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { - - if (!replies_.contains(reply)) return; - replies_.removeAll(reply); - reply->deleteLater(); - - QByteArray data = GetReplyData(reply, (offset_requested == 0)); - - --artists_requests_active_; - - if (finished_) return; - - if (data.isEmpty()) { - ArtistsFinishCheck(); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - ArtistsFinishCheck(); - return; - } - - if (!json_obj.contains("limit") || - !json_obj.contains("offset") || - !json_obj.contains("totalNumberOfItems") || - !json_obj.contains("items")) { - ArtistsFinishCheck(); - Error("Json object missing values.", json_obj); - return; - } - //int limit = json_obj["limit"].toInt(); - int offset = json_obj["offset"].toInt(); - int artists_total = json_obj["totalNumberOfItems"].toInt(); - - if (offset_requested == 0) { - artists_total_ = artists_total; - } - else if (artists_total != artists_total_) { - Error(QString("totalNumberOfItems returned does not match previous totalNumberOfItems! %1 != %2").arg(artists_total).arg(artists_total_)); - ArtistsFinishCheck(); - return; - } - - if (offset != offset_requested) { - Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - ArtistsFinishCheck(); - return; - } - - if (offset_requested == 0) { - emit ProgressSetMaximum(query_id_, artists_total_); - emit UpdateProgress(query_id_, artists_received_); - } - - QJsonValue json_value = ExtractItems(json_obj); - if (!json_value.isArray()) { - ArtistsFinishCheck(); - return; - } - - QJsonArray json_items = json_value.toArray(); - if (json_items.isEmpty()) { // Empty array means no results - if (offset_requested == 0) no_results_ = true; - ArtistsFinishCheck(); - return; - } - - int artists_received = 0; - for (const QJsonValue &value : json_items) { - - ++artists_received; - - if (!value.isObject()) { - Error("Invalid Json reply, item not a object.", value); - continue; - } - QJsonObject json_obj = value.toObject(); - - if (json_obj.contains("item")) { - QJsonValue json_item = json_obj["item"]; - if (!json_item.isObject()) { - Error("Invalid Json reply, item not a object.", json_item); - continue; - } - json_obj = json_item.toObject(); - } - - if (!json_obj.contains("id") || !json_obj.contains("name")) { - Error("Invalid Json reply, item missing id or album.", json_obj); - continue; - } - - qint64 artist_id = json_obj["id"].toInt(); - if (artist_albums_requests_pending_.contains(artist_id)) continue; - artist_albums_requests_pending_.append(artist_id); - - } - artists_received_ += artists_received; - - if (offset_requested != 0) emit UpdateProgress(query_id_, artists_received_); - - ArtistsFinishCheck(limit_requested, offset, artists_received); - -} - -void TidalRequest::ArtistsFinishCheck(const int limit, const int offset, const int artists_received) { - - if (finished_) return; - - if ((limit == 0 || limit > artists_received) && artists_received_ < artists_total_) { - int offset_next = offset + artists_received; - if (offset_next > 0 && offset_next < artists_total_) { - if (type_ == QueryType_Artists) AddArtistsRequest(offset_next); - else if (type_ == QueryType_SearchArtists) AddArtistsSearchRequest(offset_next); - } - } - - if (!artists_requests_queue_.isEmpty() && artists_requests_active_ < kMaxConcurrentArtistsRequests) FlushArtistsRequests(); - - if (artists_requests_queue_.isEmpty() && artists_requests_active_ <= 0) { // Artist query is finished, get all albums for all artists. - - // Get artist albums - for (qint64 artist_id : artist_albums_requests_pending_) { - AddArtistAlbumsRequest(artist_id); - ++artist_albums_requested_; - } - artist_albums_requests_pending_.clear(); - - if (artist_albums_requested_ > 0) { - if (artist_albums_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving albums for %1 artist...").arg(artist_albums_requested_)); - else emit UpdateStatus(query_id_, tr("Retrieving albums for %1 artists...").arg(artist_albums_requested_)); - emit ProgressSetMaximum(query_id_, artist_albums_requested_); - emit UpdateProgress(query_id_, 0); - } - - } - - FinishCheck(); - -} - -void TidalRequest::AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { - --albums_requests_active_; - AlbumsReceived(reply, 0, limit_requested, offset_requested, (offset_requested == 0)); - if (!albums_requests_queue_.isEmpty() && albums_requests_active_ < kMaxConcurrentAlbumsRequests) FlushAlbumsRequests(); -} - -void TidalRequest::AddArtistAlbumsRequest(const qint64 artist_id, const int offset) { - - Request request; - request.artist_id = artist_id; - request.offset = offset; - artist_albums_requests_queue_.enqueue(request); - if (artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests(); - -} - -void TidalRequest::FlushArtistAlbumsRequests() { - - while (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) { - - Request request = artist_albums_requests_queue_.dequeue(); - ++artist_albums_requests_active_; - - ParamList parameters; - if (request.offset > 0) parameters << Param("offset", QString::number(request.offset)); - QNetworkReply *reply = CreateRequest(QString("artists/%1/albums").arg(request.artist_id), parameters); - NewClosure(reply, SIGNAL(finished()), this, SLOT(ArtistAlbumsReplyReceived(QNetworkReply*, const qint64, const int)), reply, request.artist_id, request.offset); - replies_ << reply; - - } - -} - -void TidalRequest::ArtistAlbumsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const int offset_requested) { - - --artist_albums_requests_active_; - ++artist_albums_received_; - emit UpdateProgress(query_id_, artist_albums_received_); - AlbumsReceived(reply, artist_id, 0, offset_requested, false); - if (!artist_albums_requests_queue_.isEmpty() && artist_albums_requests_active_ < kMaxConcurrentArtistAlbumsRequests) FlushArtistAlbumsRequests(); - -} - -void TidalRequest::AlbumsReceived(QNetworkReply *reply, const qint64 artist_id_requested, const int limit_requested, const int offset_requested, const bool auto_login) { - - if (!replies_.contains(reply)) return; - replies_.removeAll(reply); - reply->deleteLater(); - - QByteArray data = GetReplyData(reply, auto_login); - - if (finished_) return; - - if (data.isEmpty()) { - AlbumsFinishCheck(artist_id_requested); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - AlbumsFinishCheck(artist_id_requested); - return; - } - - if (!json_obj.contains("limit") || - !json_obj.contains("offset") || - !json_obj.contains("totalNumberOfItems") || - !json_obj.contains("items")) { - Error("Json object missing values.", json_obj); - AlbumsFinishCheck(artist_id_requested); - return; - } - - //int limit = json_obj["limit"].toInt(); - int offset = json_obj["offset"].toInt(); - int albums_total = json_obj["totalNumberOfItems"].toInt(); - - if (offset != offset_requested) { - Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - AlbumsFinishCheck(artist_id_requested); - return; - } - - QJsonValue json_value = ExtractItems(json_obj); - if (!json_value.isArray()) { - AlbumsFinishCheck(artist_id_requested); - return; - } - QJsonArray json_items = json_value.toArray(); - if (json_items.isEmpty()) { - if ((type_ == QueryType_Albums || type_ == QueryType_SearchAlbums || (type_ == QueryType_SearchSongs && fetchalbums_)) && offset_requested == 0) { - no_results_ = true; - } - AlbumsFinishCheck(artist_id_requested); - return; - } - - int albums_received = 0; - for (const QJsonValue &value : json_items) { - - ++albums_received; - - if (!value.isObject()) { - Error("Invalid Json reply, item not a object.", value); - continue; - } - QJsonObject json_obj = value.toObject(); - - if (json_obj.contains("item")) { - QJsonValue json_item = json_obj["item"]; - if (!json_item.isObject()) { - Error("Invalid Json reply, item not a object.", json_item); - continue; - } - json_obj = json_item.toObject(); - } - - qint64 album_id = 0; - QString album; - if (json_obj.contains("type")) { // This was a albums request or search - if (!json_obj.contains("id") || !json_obj.contains("title")) { - Error("Invalid Json reply, item is missing ID or title.", json_obj); - continue; - } - album_id = json_obj["id"].toInt(); - album = json_obj["title"].toString(); - } - else if (json_obj.contains("album")) { // This was a tracks request or search - QJsonValue json_value_album = json_obj["album"]; - if (!json_value_album.isObject()) { - Error("Invalid Json reply, item album is not a object.", json_value_album); - continue; - } - QJsonObject json_album = json_value_album.toObject(); - if (!json_album.contains("id") || !json_album.contains("title")) { - Error("Invalid Json reply, item album is missing ID or title.", json_album); - continue; - } - album_id = json_album["id"].toInt(); - album = json_album["title"].toString(); - - } - else { - Error("Invalid Json reply, item missing type or album.", json_obj); - continue; - } - - if (album_songs_requests_pending_.contains(album_id)) continue; - - if (!json_obj.contains("artist") || !json_obj.contains("title") || !json_obj.contains("audioQuality")) { - Error("Invalid Json reply, item missing artist, title or audioQuality.", json_obj); - continue; - } - QJsonValue json_value_artist = json_obj["artist"]; - if (!json_value_artist.isObject()) { - Error("Invalid Json reply, item artist is not a object.", json_value_artist); - continue; - } - QJsonObject json_artist = json_value_artist.toObject(); - if (!json_artist.contains("id") || !json_artist.contains("name")) { - Error("Invalid Json reply, item artist missing id or name.", json_artist); - continue; - } - - qint64 artist_id = json_artist["id"].toInt(); - QString artist = json_artist["name"].toString(); - - QString quality = json_obj["audioQuality"].toString(); - QString copyright = json_obj["copyright"].toString(); - - //qLog(Debug) << "Tidal:" << artist << album << quality << copyright; - - Request request; - if (artist_id_requested == 0) { - request.artist_id = artist_id; - } - else { - request.artist_id = artist_id_requested; - } - request.album_id = album_id; - request.album_artist = artist; - album_songs_requests_pending_.insert(album_id, request); - - } - - AlbumsFinishCheck(artist_id_requested, limit_requested, offset, albums_total, albums_received); - -} - -void TidalRequest::AlbumsFinishCheck(const qint64 artist_id, const int limit, const int offset, const int albums_total, const int albums_received) { - - if (finished_) return; - - if (limit == 0 || limit > albums_received) { - int offset_next = offset + albums_received; - if (offset_next > 0 && offset_next < albums_total) { - switch (type_) { - case QueryType_Albums: - AddAlbumsRequest(offset_next); - break; - case QueryType_SearchAlbums: - AddAlbumsSearchRequest(offset_next); - break; - case QueryType_Artists: - case QueryType_SearchArtists: - AddArtistAlbumsRequest(artist_id, offset_next); - break; - default: - break; - } - } - } - - if ( - albums_requests_queue_.isEmpty() && - albums_requests_active_ <= 0 && - artist_albums_requests_queue_.isEmpty() && - artist_albums_requests_active_ <= 0 - ) { // Artist albums query is finished, get all songs for all albums. - - // Get songs for all the albums. - - QHash ::iterator i; - for (i = album_songs_requests_pending_.begin() ; i != album_songs_requests_pending_.end() ; ++i) { - Request request = i.value(); - AddAlbumSongsRequest(request.artist_id, request.album_id, request.album_artist); - } - album_songs_requests_pending_.clear(); - - if (album_songs_requested_ > 0) { - if (album_songs_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving songs for %1 album...").arg(album_songs_requested_)); - else emit UpdateStatus(query_id_, tr("Retrieving songs for %1 albums...").arg(album_songs_requested_)); - emit ProgressSetMaximum(query_id_, album_songs_requested_); - emit UpdateProgress(query_id_, 0); - } - } - - FinishCheck(); - -} - -void TidalRequest::SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested) { - - --songs_requests_active_; - if (type_ == QueryType_SearchSongs && fetchalbums_) { - AlbumsReceived(reply, 0, limit_requested, offset_requested, (offset_requested == 0)); - } - else { - SongsReceived(reply, 0, 0, limit_requested, offset_requested, (offset_requested == 0)); - } - -} - -void TidalRequest::AddAlbumSongsRequest(const qint64 artist_id, const qint64 album_id, const QString &album_artist, const int offset) { - - Request request; - request.artist_id = artist_id; - request.album_id = album_id; - request.album_artist = album_artist; - request.offset = offset; - album_songs_requests_queue_.enqueue(request); - ++album_songs_requested_; - if (album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests(); - -} - -void TidalRequest::FlushAlbumSongsRequests() { - - while (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) { - - Request request = album_songs_requests_queue_.dequeue(); - ++album_songs_requests_active_; - ParamList parameters; - if (request.offset > 0) parameters << Param("offset", QString::number(request.offset)); - QNetworkReply *reply = CreateRequest(QString("albums/%1/tracks").arg(request.album_id), parameters); - replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumSongsReplyReceived(QNetworkReply*, const qint64, const qint64, const int, const QString&)), reply, request.artist_id, request.album_id, request.offset, request.album_artist); - - } - -} - -void TidalRequest::AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const qint64 album_id, const int offset_requested, const QString &album_artist) { - - --album_songs_requests_active_; - ++album_songs_received_; - if (offset_requested == 0) { - emit UpdateProgress(query_id_, album_songs_received_); - } - SongsReceived(reply, artist_id, album_id, 0, offset_requested, false, album_artist); - -} - -void TidalRequest::SongsReceived(QNetworkReply *reply, const qint64 artist_id, const qint64 album_id, const int limit_requested, const int offset_requested, const bool auto_login, const QString &album_artist) { - - if (!replies_.contains(reply)) return; - replies_.removeAll(reply); - reply->deleteLater(); - - QByteArray data = GetReplyData(reply, auto_login); - - if (finished_) return; - - if (data.isEmpty()) { - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, 0, 0, album_artist); - return; - } - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, 0, 0, album_artist); - return; - } - - if (!json_obj.contains("limit") || - !json_obj.contains("offset") || - !json_obj.contains("totalNumberOfItems") || - !json_obj.contains("items")) { - Error("Json object missing values.", json_obj); - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, 0, 0, album_artist); - return; - } - - //int limit = json_obj["limit"].toInt(); - int offset = json_obj["offset"].toInt(); - int songs_total = json_obj["totalNumberOfItems"].toInt(); - - if (offset != offset_requested) { - Error(QString("Offset returned does not match offset requested! %1 != %2").arg(offset).arg(offset_requested)); - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist); - return; - } - - QJsonValue json_value = ExtractItems(json_obj); - if (!json_value.isArray()) { - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist); - return; - } - - QJsonArray json_items = json_value.toArray(); - if (json_items.isEmpty()) { - if ((type_ == QueryType_Songs || type_ == QueryType_SearchSongs) && offset_requested == 0) { - no_results_ = true; - } - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, 0, album_artist); - return; - } - - bool compilation = false; - bool multidisc = false; - SongList songs; - int songs_received = 0; - for (const QJsonValue &value : json_items) { - - if (!value.isObject()) { - Error("Invalid Json reply, track is not a object.", value); - continue; - } - QJsonObject json_obj = value.toObject(); - - if (json_obj.contains("item")) { - QJsonValue json_item = json_obj["item"]; - if (!json_item.isObject()) { - Error("Invalid Json reply, item not a object.", json_item); - continue; - } - json_obj = json_item.toObject(); - } - - ++songs_received; - Song song(Song::Source_Tidal); - ParseSong(song, json_obj, artist_id, album_id, album_artist); - if (!song.is_valid()) continue; - if (song.disc() >= 2) multidisc = true; - if (song.is_compilation()) compilation = true; - songs << song; - } - - for (Song &song : songs) { - if (compilation) song.set_compilation_detected(true); - if (multidisc) { - QString album_full(QString("%1 - (Disc %2)").arg(song.album()).arg(song.disc())); - song.set_album(album_full); - } - songs_ << song; - } - - SongsFinishCheck(artist_id, album_id, limit_requested, offset_requested, songs_total, songs_received, album_artist); - -} - -void TidalRequest::SongsFinishCheck(const qint64 artist_id, const qint64 album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist) { - - if (finished_) return; - - if (limit == 0 || limit > songs_received) { - int offset_next = offset + songs_received; - if (offset_next > 0 && offset_next < songs_total) { - switch (type_) { - case QueryType_Songs: - AddSongsRequest(offset_next); - break; - case QueryType_SearchSongs: - // If artist_id and album_id isn't zero it means that it's a songs search where we fetch all albums too. So fallthrough. - if (artist_id == 0 && album_id == 0) { - AddSongsSearchRequest(offset_next); - break; - } - // fallthrough - case QueryType_Artists: - case QueryType_SearchArtists: - case QueryType_Albums: - case QueryType_SearchAlbums: - AddAlbumSongsRequest(artist_id, album_id, album_artist, offset_next); - break; - default: - break; - } - } - } - - if (!songs_requests_queue_.isEmpty() && songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests(); - if (!album_songs_requests_queue_.isEmpty() && album_songs_requests_active_ < kMaxConcurrentAlbumSongsRequests) FlushAlbumSongsRequests(); - - if ( - service_->download_album_covers() && - IsQuery() && - songs_requests_queue_.isEmpty() && - songs_requests_active_ <= 0 && - album_songs_requests_queue_.isEmpty() && - album_songs_requests_active_ <= 0 && - album_cover_requests_queue_.isEmpty() && - album_covers_received_ <= 0 && - album_covers_requests_sent_.isEmpty() && - album_songs_received_ >= album_songs_requested_ - ) { - GetAlbumCovers(); - } - - FinishCheck(); - -} - -int TidalRequest::ParseSong(Song &song, const QJsonObject &json_obj, const qint64 artist_id_requested, const qint64 album_id_requested, const QString &album_artist) { - - Q_UNUSED(artist_id_requested); - - if ( - !json_obj.contains("album") || - !json_obj.contains("allowStreaming") || - !json_obj.contains("artist") || - !json_obj.contains("artists") || - !json_obj.contains("audioQuality") || - !json_obj.contains("duration") || - !json_obj.contains("id") || - !json_obj.contains("streamReady") || - !json_obj.contains("title") || - !json_obj.contains("trackNumber") || - !json_obj.contains("url") || - !json_obj.contains("volumeNumber") || - !json_obj.contains("copyright") - ) { - Error("Invalid Json reply, track is missing one or more values.", json_obj); - return -1; - } - - QJsonValue json_value_artist = json_obj["artist"]; - QJsonValue json_value_album = json_obj["album"]; - QJsonValue json_duration = json_obj["duration"]; - QJsonArray json_artists = json_obj["artists"].toArray(); - - qint64 song_id = json_obj["id"].toInt(); - - QString title = json_obj["title"].toString(); - QString urlstr = json_obj["url"].toString(); - int track = json_obj["trackNumber"].toInt(); - int disc = json_obj["volumeNumber"].toInt(); - bool allow_streaming = json_obj["allowStreaming"].toBool(); - bool stream_ready = json_obj["streamReady"].toBool(); - QString copyright = json_obj["copyright"].toString(); - - if (!json_value_artist.isObject()) { - Error("Invalid Json reply, track artist is not a object.", json_value_artist); - return -1; - } - QJsonObject json_artist = json_value_artist.toObject(); - if (!json_artist.contains("id") || !json_artist.contains("name")) { - Error("Invalid Json reply, track artist is missing id or name.", json_artist); - return -1; - } - qint64 artist_id = json_artist["id"].toInt(); - QString artist = json_artist["name"].toString(); - - if (!json_value_album.isObject()) { - Error("Invalid Json reply, track album is not a object.", json_value_album); - return -1; - } - QJsonObject json_album = json_value_album.toObject(); - if (!json_album.contains("id") || !json_album.contains("title") || !json_album.contains("cover")) { - Error("Invalid Json reply, track album is missing id, title or cover.", json_album); - return -1; - } - qint64 album_id = json_album["id"].toInt(); - if (album_id_requested != 0 && album_id_requested != album_id) { - Error("Invalid Json reply, track album id is wrong.", json_album); - return -1; - } - QString album = json_album["title"].toString(); - QString cover = json_album["cover"].toString(); - - if (!allow_streaming) { - Warn(QString("Song %1 %2 %3 is not allowStreaming").arg(artist).arg(album).arg(title)); - } - - if (!stream_ready) { - Warn(QString("Song %1 %2 %3 is not streamReady").arg(artist).arg(album).arg(title)); - } - - QUrl url; - url.setScheme(url_handler_->scheme()); - url.setPath(QString::number(song_id)); - - QVariant q_duration = json_duration.toVariant(); - quint64 duration = 0; - if (q_duration.isValid() && (q_duration.type() == QVariant::Int || q_duration.type() == QVariant::Double)) { - duration = q_duration.toLongLong() * kNsecPerSec; - } - else { - Error("Invalid duration for song.", json_duration); - return -1; - } - - cover = cover.replace("-", "/"); - QUrl cover_url (QString("%1/images/%2/%3.jpg").arg(kResourcesUrl).arg(cover).arg(coversize_)); - - title.remove(Song::kTitleRemoveMisc); - - //qLog(Debug) << "id" << song_id << "track" << track << "disc" << disc << "title" << title << "album" << album << "album artist" << album_artist << "artist" << artist << cover << allow_streaming << url; - - song.set_source(Song::Source_Tidal); - song.set_song_id(song_id); - song.set_album_id(album_id); - song.set_artist_id(artist_id); - if (album_artist != artist) song.set_albumartist(album_artist); - song.set_album(album); - song.set_artist(artist); - song.set_title(title); - song.set_track(track); - song.set_disc(disc); - song.set_url(url); - song.set_length_nanosec(duration); - song.set_art_automatic(cover_url); - song.set_comment(copyright); - song.set_directory_id(0); - song.set_filetype(Song::FileType_Stream); - song.set_filesize(0); - song.set_mtime(0); - song.set_ctime(0); - song.set_valid(true); - - return song_id; - -} - -void TidalRequest::GetAlbumCovers() { - - for (Song &song : songs_) { - AddAlbumCoverRequest(song); - } - FlushAlbumCoverRequests(); - - if (album_covers_requested_ == 1) emit UpdateStatus(query_id_, tr("Retrieving album cover for %1 album...").arg(album_covers_requested_)); - else emit UpdateStatus(query_id_, tr("Retrieving album covers for %1 albums...").arg(album_covers_requested_)); - emit ProgressSetMaximum(query_id_, album_covers_requested_); - emit UpdateProgress(query_id_, 0); - -} - -void TidalRequest::AddAlbumCoverRequest(Song &song) { - - if (album_covers_requests_sent_.contains(song.album_id())) { - album_covers_requests_sent_.insertMulti(song.album_id(), &song); - return; - } - - AlbumCoverRequest request; - request.album_id = song.album_id(); - request.url = QUrl(song.art_automatic()); - request.filename = app_->album_cover_loader()->CoverFilePath(song.source(), song.effective_albumartist(), song.effective_album(), song.album_id(), QString(), request.url); - if (request.filename.isEmpty()) return; - - album_covers_requests_sent_.insertMulti(song.album_id(), &song); - ++album_covers_requested_; - - album_cover_requests_queue_.enqueue(request); - -} - -void TidalRequest::FlushAlbumCoverRequests() { - - while (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) { - - AlbumCoverRequest request = album_cover_requests_queue_.dequeue(); - ++album_covers_requests_active_; - - QNetworkRequest req(request.url); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) - req.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); -#endif - QNetworkReply *reply = network_->get(req); - album_cover_replies_ << reply; - NewClosure(reply, SIGNAL(finished()), this, SLOT(AlbumCoverReceived(QNetworkReply*, const QString&, const QUrl&, const QString&)), reply, request.album_id, request.url, request.filename); - - } - -} - -void TidalRequest::AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url, const QString &filename) { - - if (album_cover_replies_.contains(reply)) { - album_cover_replies_.removeAll(reply); - reply->deleteLater(); - } - else { - AlbumCoverFinishCheck(); - return; - } - - --album_covers_requests_active_; - ++album_covers_received_; - - if (finished_) return; - - emit UpdateProgress(query_id_, album_covers_received_); - - if (!album_covers_requests_sent_.contains(album_id)) { - AlbumCoverFinishCheck(); - return; - } - - if (reply->error() != QNetworkReply::NoError) { - Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - album_covers_requests_sent_.remove(album_id); - AlbumCoverFinishCheck(); - return; - } - - QByteArray data = reply->readAll(); - if (data.isEmpty()) { - Error(QString("Received empty image data for %1").arg(url.toString())); - album_covers_requests_sent_.remove(album_id); - AlbumCoverFinishCheck(); - return; - } - - QImage image; - if (image.loadFromData(data)) { - - if (image.save(filename, "JPG")) { - while (album_covers_requests_sent_.contains(album_id)) { - Song *song = album_covers_requests_sent_.take(album_id); - song->set_art_automatic(QUrl::fromLocalFile(filename)); - } - } - - } - else { - album_covers_requests_sent_.remove(album_id); - Error(QString("Error decoding image data from %1").arg(url.toString())); - } - - AlbumCoverFinishCheck(); - -} - -void TidalRequest::AlbumCoverFinishCheck() { - - if (!album_cover_requests_queue_.isEmpty() && album_covers_requests_active_ < kMaxConcurrentAlbumCoverRequests) - FlushAlbumCoverRequests(); - - FinishCheck(); - -} - -void TidalRequest::FinishCheck() { - - if ( - !finished_ && - !need_login_ && - albums_requests_queue_.isEmpty() && - artists_requests_queue_.isEmpty() && - songs_requests_queue_.isEmpty() && - artist_albums_requests_queue_.isEmpty() && - album_songs_requests_queue_.isEmpty() && - album_cover_requests_queue_.isEmpty() && - artist_albums_requests_pending_.isEmpty() && - album_songs_requests_pending_.isEmpty() && - album_covers_requests_sent_.isEmpty() && - artists_requests_active_ <= 0 && - albums_requests_active_ <= 0 && - songs_requests_active_ <= 0 && - artist_albums_requests_active_ <= 0 && - artist_albums_received_ >= artist_albums_requested_ && - album_songs_requests_active_ <= 0 && - album_songs_received_ >= album_songs_requested_ && - album_covers_requested_ <= album_covers_received_ && - album_covers_requests_active_ <= 0 && - album_covers_received_ >= album_covers_requested_ - ) { - finished_ = true; - if (no_results_ && songs_.isEmpty()) { - if (IsSearch()) - emit Results(query_id_, SongList(), tr("No match.")); - else - emit Results(query_id_, SongList(), QString()); - } - else { - if (songs_.isEmpty() && errors_.isEmpty()) - emit Results(query_id_, songs_, tr("Unknown error")); - else - emit Results(query_id_, songs_, ErrorsToHTML(errors_)); - } - } - -} - -void TidalRequest::Error(const QString &error, const QVariant &debug) { - - if (!error.isEmpty()) { - errors_ << error; - qLog(Error) << "Tidal:" << error; - } - - if (debug.isValid()) qLog(Debug) << debug; - - FinishCheck(); - -} - -void TidalRequest::Warn(QString error, QVariant debug) { - - qLog(Error) << "Tidal:" << error; - if (debug.isValid()) qLog(Debug) << debug; - -} - diff --git a/src/tidal/tidalrequest.h b/src/tidal/tidalrequest.h deleted file mode 100644 index 385f4d47..00000000 --- a/src/tidal/tidalrequest.h +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef TIDALREQUEST_H -#define TIDALREQUEST_H - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/song.h" -#include "tidalbaserequest.h" - -class QNetworkReply; -class Application; -class NetworkAccessManager; -class TidalService; -class TidalUrlHandler; - -class TidalRequest : public TidalBaseRequest { - Q_OBJECT - - public: - - TidalRequest(TidalService *service, TidalUrlHandler *url_handler, Application *app, NetworkAccessManager *network, QueryType type, QObject *parent); - ~TidalRequest(); - - void ReloadSettings(); - - void Process(); - void NeedLogin() { need_login_ = true; } - void Search(const int search_id, const QString &search_text); - - signals: - void Login(); - void Login(const QString &username, const QString &password, const QString &token); - void LoginSuccess(); - void LoginFailure(QString failure_reason); - void Results(const int id, const SongList &songs, const QString &error); - void UpdateStatus(const int id, const QString &text); - void ProgressSetMaximum(const int id, const int max); - void UpdateProgress(const int id, const int max); - void StreamURLFinished(const QUrl original_url, const QUrl url, const Song::FileType, QString error = QString()); - - private slots: - void LoginComplete(const bool success, QString error = QString()); - - void ArtistsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested); - - void AlbumsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested); - void AlbumsReceived(QNetworkReply *reply, const qint64 artist_id_requested, const int limit_requested, const int offset_requested, const bool auto_login); - - void SongsReplyReceived(QNetworkReply *reply, const int limit_requested, const int offset_requested); - void SongsReceived(QNetworkReply *reply, const qint64 artist_id, const qint64 album_id, const int limit_requested, const int offset_requested, const bool auto_login = false, const QString &album_artist = QString()); - - void ArtistAlbumsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const int offset_requested); - void AlbumSongsReplyReceived(QNetworkReply *reply, const qint64 artist_id, const qint64 album_id, const int offset_requested, const QString &album_artist); - void AlbumCoverReceived(QNetworkReply *reply, const QString &album_id, const QUrl &url, const QString &filename); - - private: - typedef QPair Param; - typedef QList ParamList; - - struct Request { - qint64 artist_id = 0; - qint64 album_id = 0; - qint64 song_id = 0; - int offset = 0; - int limit = 0; - QString album_artist; - }; - struct AlbumCoverRequest { - qint64 artist_id = 0; - QString album_id = 0; - QUrl url; - QString filename; - }; - - bool IsQuery() { return (type_ == QueryType_Artists || type_ == QueryType_Albums || type_ == QueryType_Songs); } - bool IsSearch() { return (type_ == QueryType_SearchArtists || type_ == QueryType_SearchAlbums || type_ == QueryType_SearchSongs); } - - void GetArtists(); - void GetAlbums(); - void GetSongs(); - - void ArtistsSearch(); - void AlbumsSearch(); - void SongsSearch(); - - void AddArtistsRequest(const int offset = 0, const int limit = 0); - void AddArtistsSearchRequest(const int offset = 0); - void FlushArtistsRequests(); - void AddAlbumsRequest(const int offset = 0, const int limit = 0); - void AddAlbumsSearchRequest(const int offset = 0); - void FlushAlbumsRequests(); - void AddSongsRequest(const int offset = 0, const int limit = 0); - void AddSongsSearchRequest(const int offset = 0); - void FlushSongsRequests(); - - void ArtistsFinishCheck(const int limit = 0, const int offset = 0, const int artists_received = 0); - void AlbumsFinishCheck(const qint64 artist_id, const int limit = 0, const int offset = 0, const int albums_total = 0, const int albums_received = 0); - void SongsFinishCheck(const qint64 artist_id, const qint64 album_id, const int limit, const int offset, const int songs_total, const int songs_received, const QString &album_artist); - - void AddArtistAlbumsRequest(const qint64 artist_id, const int offset = 0); - void FlushArtistAlbumsRequests(); - - void AddAlbumSongsRequest(const qint64 artist_id, const qint64 album_id, const QString &album_artist, const int offset = 0); - void FlushAlbumSongsRequests(); - - int ParseSong(Song &song, const QJsonObject &json_obj, const qint64 artist_id_requested = 0, const qint64 album_id_requested = 0, const QString &album_artist = QString()); - - void GetAlbumCovers(); - void AddAlbumCoverRequest(Song &song); - void FlushAlbumCoverRequests(); - void AlbumCoverFinishCheck(); - - void FinishCheck(); - void Warn(QString error, QVariant debug = QVariant()); - void Error(const QString &error, const QVariant &debug = QVariant()); - - static const char *kResourcesUrl; - static const int kMaxConcurrentArtistsRequests; - static const int kMaxConcurrentAlbumsRequests; - static const int kMaxConcurrentSongsRequests; - static const int kMaxConcurrentArtistAlbumsRequests; - static const int kMaxConcurrentAlbumSongsRequests; - static const int kMaxConcurrentAlbumCoverRequests; - - TidalService *service_; - TidalUrlHandler *url_handler_; - Application *app_; - NetworkAccessManager *network_; - - QueryType type_; - bool fetchalbums_; - QString coversize_; - - int query_id_; - QString search_text_; - - bool finished_; - - QQueue artists_requests_queue_; - QQueue albums_requests_queue_; - QQueue songs_requests_queue_; - - QQueue artist_albums_requests_queue_; - QQueue album_songs_requests_queue_; - QQueue album_cover_requests_queue_; - - QList artist_albums_requests_pending_; - QHash album_songs_requests_pending_; - QMultiMap album_covers_requests_sent_; - - int artists_requests_active_; - int artists_total_; - int artists_received_; - - int albums_requests_active_; - int songs_requests_active_; - - int artist_albums_requests_active_; - int artist_albums_requested_; - int artist_albums_received_; - - int album_songs_requests_active_; - int album_songs_requested_; - int album_songs_received_; - - int album_covers_requests_active_; - int album_covers_requested_; - int album_covers_received_; - - SongList songs_; - QStringList errors_; - bool need_login_; - bool no_results_; - QList replies_; - QList album_cover_replies_; - -}; - -#endif // TIDALREQUEST_H diff --git a/src/tidal/tidalservice.cpp b/src/tidal/tidalservice.cpp deleted file mode 100644 index 1369b62d..00000000 --- a/src/tidal/tidalservice.cpp +++ /dev/null @@ -1,970 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/application.h" -#include "core/player.h" -#include "core/closure.h" -#include "core/logging.h" -#include "core/network.h" -#include "core/database.h" -#include "core/song.h" -#include "core/utilities.h" -#include "internet/internetsearch.h" -#include "collection/collectionbackend.h" -#include "collection/collectionmodel.h" -#include "tidalservice.h" -#include "tidalurlhandler.h" -#include "tidalbaserequest.h" -#include "tidalrequest.h" -#include "tidalfavoriterequest.h" -#include "tidalstreamurlrequest.h" -#include "settings/settingsdialog.h" -#include "settings/tidalsettingspage.h" - -using std::shared_ptr; - -const Song::Source TidalService::kSource = Song::Source_Tidal; -const char *TidalService::kApiTokenB64 = "UDVYYmVvNUxGdkVTZUR5Ng=="; -const char *TidalService::kOAuthUrl = "https://login.tidal.com/authorize"; -const char *TidalService::kOAuthAccessTokenUrl = "https://login.tidal.com/oauth2/token"; -const char *TidalService::kOAuthRedirectUrl = "tidal://login/auth"; -const char *TidalService::kAuthUrl = "https://api.tidalhifi.com/v1/login/username"; -const int TidalService::kLoginAttempts = 2; -const int TidalService::kTimeResetLoginAttempts = 60000; - -const char *TidalService::kArtistsSongsTable = "tidal_artists_songs"; -const char *TidalService::kAlbumsSongsTable = "tidal_albums_songs"; -const char *TidalService::kSongsTable = "tidal_songs"; - -const char *TidalService::kArtistsSongsFtsTable = "tidal_artists_songs_fts"; -const char *TidalService::kAlbumsSongsFtsTable = "tidal_albums_songs_fts"; -const char *TidalService::kSongsFtsTable = "tidal_songs_fts"; - -TidalService::TidalService(Application *app, QObject *parent) - : InternetService(Song::Source_Tidal, "Tidal", "tidal", app, parent), - app_(app), - network_(new NetworkAccessManager(this)), - url_handler_(new TidalUrlHandler(app, this)), - artists_collection_backend_(nullptr), - albums_collection_backend_(nullptr), - songs_collection_backend_(nullptr), - artists_collection_model_(nullptr), - albums_collection_model_(nullptr), - songs_collection_model_(nullptr), - artists_collection_sort_model_(new QSortFilterProxyModel(this)), - albums_collection_sort_model_(new QSortFilterProxyModel(this)), - songs_collection_sort_model_(new QSortFilterProxyModel(this)), - timer_search_delay_(new QTimer(this)), - timer_login_attempt_(new QTimer(this)), - favorite_request_(new TidalFavoriteRequest(this, network_, this)), - user_id_(0), - search_delay_(1500), - artistssearchlimit_(1), - albumssearchlimit_(1), - songssearchlimit_(1), - fetchalbums_(true), - download_album_covers_(true), - pending_search_id_(0), - next_pending_search_id_(1), - search_id_(0), - login_sent_(false), - login_attempts_(0) - { - - app->player()->RegisterUrlHandler(url_handler_); - - // Backends - - artists_collection_backend_ = new CollectionBackend(); - artists_collection_backend_->moveToThread(app_->database()->thread()); - artists_collection_backend_->Init(app_->database(), Song::Source_Tidal, kArtistsSongsTable, QString(), QString(), kArtistsSongsFtsTable); - - albums_collection_backend_ = new CollectionBackend(); - albums_collection_backend_->moveToThread(app_->database()->thread()); - albums_collection_backend_->Init(app_->database(), Song::Source_Tidal, kAlbumsSongsTable, QString(), QString(), kAlbumsSongsFtsTable); - - songs_collection_backend_ = new CollectionBackend(); - songs_collection_backend_->moveToThread(app_->database()->thread()); - songs_collection_backend_->Init(app_->database(), Song::Source_Tidal, kSongsTable, QString(), QString(), kSongsFtsTable); - - artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this); - albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this); - songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this); - - artists_collection_sort_model_->setSourceModel(artists_collection_model_); - artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - artists_collection_sort_model_->setDynamicSortFilter(true); - artists_collection_sort_model_->setSortLocaleAware(true); - artists_collection_sort_model_->sort(0); - - albums_collection_sort_model_->setSourceModel(albums_collection_model_); - albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - albums_collection_sort_model_->setDynamicSortFilter(true); - albums_collection_sort_model_->setSortLocaleAware(true); - albums_collection_sort_model_->sort(0); - - songs_collection_sort_model_->setSourceModel(songs_collection_model_); - songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText); - songs_collection_sort_model_->setDynamicSortFilter(true); - songs_collection_sort_model_->setSortLocaleAware(true); - songs_collection_sort_model_->sort(0); - - // Search - - timer_search_delay_->setSingleShot(true); - connect(timer_search_delay_, SIGNAL(timeout()), SLOT(StartSearch())); - - timer_login_attempt_->setSingleShot(true); - connect(timer_login_attempt_, SIGNAL(timeout()), SLOT(ResetLoginAttempts())); - - connect(this, SIGNAL(Login()), SLOT(SendLogin())); - connect(this, SIGNAL(Login(QString, QString, QString)), SLOT(SendLogin(QString, QString, QString))); - - connect(this, SIGNAL(AddArtists(const SongList&)), favorite_request_, SLOT(AddArtists(const SongList&))); - connect(this, SIGNAL(AddAlbums(const SongList&)), favorite_request_, SLOT(AddAlbums(const SongList&))); - connect(this, SIGNAL(AddSongs(const SongList&)), favorite_request_, SLOT(AddSongs(const SongList&))); - - connect(this, SIGNAL(RemoveArtists(const SongList&)), favorite_request_, SLOT(RemoveArtists(const SongList&))); - connect(this, SIGNAL(RemoveAlbums(const SongList&)), favorite_request_, SLOT(RemoveAlbums(const SongList&))); - connect(this, SIGNAL(RemoveSongs(const SongList&)), favorite_request_, SLOT(RemoveSongs(const SongList&))); - - connect(favorite_request_, SIGNAL(ArtistsAdded(const SongList&)), artists_collection_backend_, SLOT(AddOrUpdateSongs(const SongList&))); - connect(favorite_request_, SIGNAL(AlbumsAdded(const SongList&)), albums_collection_backend_, SLOT(AddOrUpdateSongs(const SongList&))); - connect(favorite_request_, SIGNAL(SongsAdded(const SongList&)), songs_collection_backend_, SLOT(AddOrUpdateSongs(const SongList&))); - - connect(favorite_request_, SIGNAL(ArtistsRemoved(const SongList&)), artists_collection_backend_, SLOT(DeleteSongs(const SongList&))); - connect(favorite_request_, SIGNAL(AlbumsRemoved(const SongList&)), albums_collection_backend_, SLOT(DeleteSongs(const SongList&))); - connect(favorite_request_, SIGNAL(SongsRemoved(const SongList&)), songs_collection_backend_, SLOT(DeleteSongs(const SongList&))); - - ReloadSettings(); - -} - -TidalService::~TidalService() { - - while (!stream_url_requests_.isEmpty()) { - TidalStreamURLRequest *stream_url_req = stream_url_requests_.takeFirst(); - disconnect(stream_url_req, 0, this, 0); - stream_url_req->deleteLater(); - } - - artists_collection_backend_->deleteLater(); - albums_collection_backend_->deleteLater(); - songs_collection_backend_->deleteLater(); - -} - -void TidalService::Exit() { - - wait_for_exit_ << artists_collection_backend_ << albums_collection_backend_ << songs_collection_backend_; - - connect(artists_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived())); - connect(albums_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived())); - connect(songs_collection_backend_, SIGNAL(ExitFinished()), this, SLOT(ExitReceived())); - - artists_collection_backend_->ExitAsync(); - albums_collection_backend_->ExitAsync(); - songs_collection_backend_->ExitAsync(); - -} - -void TidalService::ExitReceived() { - - QObject *obj = static_cast(sender()); - disconnect(obj, 0, this, 0); - qLog(Debug) << obj << "successfully exited."; - wait_for_exit_.removeAll(obj); - if (wait_for_exit_.isEmpty()) emit ExitFinished(); - -} - -void TidalService::ShowConfig() { - app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Tidal); -} - -void TidalService::ReloadSettings() { - - QSettings s; - s.beginGroup(TidalSettingsPage::kSettingsGroup); - - oauth_ = s.value("oauth", false).toBool(); - client_id_ = s.value("client_id").toString(); - api_token_ = s.value("api_token").toString(); - if (api_token_.isEmpty()) api_token_ = QString::fromUtf8(QByteArray::fromBase64(kApiTokenB64)); - - username_ = s.value("username").toString(); - QByteArray password = s.value("password").toByteArray(); - if (password.isEmpty()) password_.clear(); - else password_ = QString::fromUtf8(QByteArray::fromBase64(password)); - - quality_ = s.value("quality", "LOSSLESS").toString(); - search_delay_ = s.value("searchdelay", 1500).toInt(); - artistssearchlimit_ = s.value("artistssearchlimit", 4).toInt(); - albumssearchlimit_ = s.value("albumssearchlimit", 10).toInt(); - songssearchlimit_ = s.value("songssearchlimit", 10).toInt(); - fetchalbums_ = s.value("fetchalbums", false).toBool(); - coversize_ = s.value("coversize", "320x320").toString(); - download_album_covers_ = s.value("downloadalbumcovers", true).toBool(); - stream_url_method_ = static_cast(s.value("streamurl").toInt()); - - user_id_ = s.value("user_id").toInt(); - country_code_ = s.value("country_code", "US").toString(); - access_token_ = s.value("access_token").toString(); - refresh_token_ = s.value("refresh_token").toString(); - session_id_ = s.value("session_id").toString(); - expiry_time_ = s.value("expiry_time").toDateTime(); - - s.endGroup(); - -} - -void TidalService::StartAuthorisation() { - - login_sent_ = true; - ++login_attempts_; - if (timer_login_attempt_->isActive()) timer_login_attempt_->stop(); - timer_login_attempt_->setInterval(kTimeResetLoginAttempts); - timer_login_attempt_->start(); - - code_verifier_ = Utilities::CryptographicRandomString(44); - code_challenge_ = QString(QCryptographicHash::hash(code_verifier_.toUtf8(), QCryptographicHash::Sha256).toBase64(QByteArray::Base64UrlEncoding)); - - if (code_challenge_.lastIndexOf(QChar('=')) == code_challenge_.length() - 1) { - code_challenge_.chop(1); - } - - const ParamList params = ParamList() << Param("response_type", "code") - << Param("code_challenge", code_challenge_) - << Param("code_challenge_method", "S256") - << Param("redirect_uri", kOAuthRedirectUrl) - << Param("client_id", client_id_) - << Param("scope", "r_usr w_usr"); - - QUrlQuery url_query; - for (const Param ¶m : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QUrl url = QUrl(kOAuthUrl); - url.setQuery(url_query); - QDesktopServices::openUrl(url); - -} - -void TidalService::AuthorisationUrlReceived(const QUrl &url) { - - qLog(Debug) << "Tidal: Authorisation URL Received" << url; - - QUrlQuery url_query(url); - - if (url_query.hasQueryItem("token_type") && url_query.hasQueryItem("expires_in") && url_query.hasQueryItem("access_token")) { - - access_token_ = url_query.queryItemValue("access_token").toUtf8(); - int expires_in = url_query.queryItemValue("expires_in").toInt(); - expiry_time_ = QDateTime::currentDateTime().addSecs(expires_in - 120); - session_id_.clear(); - - QSettings s; - s.beginGroup(TidalSettingsPage::kSettingsGroup); - s.setValue("access_token", access_token_); - s.setValue("expiry_time", expiry_time_); - s.remove("refresh_token"); - s.remove("session_id"); - s.endGroup(); - - login_attempts_ = 0; - if (timer_login_attempt_->isActive()) timer_login_attempt_->stop(); - - emit LoginComplete(true); - emit LoginSuccess(); - } - - else if (url_query.hasQueryItem("code") && url_query.hasQueryItem("state")) { - - QString code = url_query.queryItemValue("code"); - QString state = url_query.queryItemValue("state"); - - const ParamList params = ParamList() << Param("code", code) - << Param("client_id", client_id_) - << Param("grant_type", "authorization_code") - << Param("redirect_uri", kOAuthRedirectUrl) - << Param("scope", "r_usr w_usr") - << Param("code_verifier", code_verifier_); - - QUrlQuery url_query; - for (const Param ¶m : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QUrl url(kOAuthAccessTokenUrl); - QNetworkRequest request = QNetworkRequest(url); - QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8(); - - login_errors_.clear(); - QNetworkReply *reply = network_->post(request, query); - connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList))); - NewClosure(reply, SIGNAL(finished()), this, SLOT(AccessTokenRequestFinished(QNetworkReply*)), reply); - - } - - else { - - LoginError(tr("Reply from Tidal is missing query items.")); - return; - } - -} - -void TidalService::HandleLoginSSLErrors(QList ssl_errors) { - - for (QSslError &ssl_error : ssl_errors) { - login_errors_ += ssl_error.errorString(); - } - -} - -void TidalService::AccessTokenRequestFinished(QNetworkReply *reply) { - - reply->deleteLater(); - - login_sent_ = false; - - if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - return; - } - else { - // See if there is Json data containing "status" and "userMessage" then use that instead. - QByteArray data(reply->readAll()); - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { - QJsonObject json_obj = json_doc.object(); - if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) { - int status = json_obj["status"].toInt(); - int sub_status = json_obj["subStatus"].toInt(); - QString user_message = json_obj["userMessage"].toString(); - login_errors_ << QString("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); - } - } - if (login_errors_.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - LoginError(); - return; - } - } - - QByteArray data(reply->readAll()); - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - LoginError("Authentication reply from server missing Json data."); - return; - } - - if (json_doc.isNull() || json_doc.isEmpty()) { - LoginError("Authentication reply from server has empty Json document."); - return; - } - - if (!json_doc.isObject()) { - LoginError("Authentication reply from server has Json document that is not an object.", json_doc); - return; - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - LoginError("Authentication reply from server has empty Json object.", json_doc); - return; - } - - if (!json_obj.contains("access_token") || - !json_obj.contains("refresh_token") || - !json_obj.contains("expires_in") || - !json_obj.contains("user") - ) { - LoginError("Authentication reply from server is missing access_token, refresh_token, expires_in or user", json_obj); - return; - } - - access_token_ = json_obj["access_token"].toString(); - refresh_token_ = json_obj["refresh_token"].toString(); - int expires_in = json_obj["expires_in"].toInt(); - expiry_time_ = QDateTime::currentDateTime().addSecs(expires_in - 120); - - QJsonValue json_user = json_obj["user"]; - if (!json_user.isObject()) { - LoginError("Authentication reply from server has Json user that is not an object.", json_doc); - return; - } - QJsonObject json_obj_user = json_user.toObject(); - if (json_obj_user.isEmpty()) { - LoginError("Authentication reply from server has empty Json user object.", json_doc); - return; - } - - country_code_ = json_obj_user["countryCode"].toString(); - user_id_ = json_obj_user["userId"].toInt(); - session_id_.clear(); - - QSettings s; - s.beginGroup(TidalSettingsPage::kSettingsGroup); - s.setValue("access_token", access_token_); - s.setValue("refresh_token", refresh_token_); - s.setValue("expiry_time", expiry_time_); - s.setValue("country_code", country_code_); - s.setValue("user_id", user_id_); - s.remove("session_id"); - s.endGroup(); - - qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "access token" << access_token_; - - login_attempts_ = 0; - if (timer_login_attempt_->isActive()) timer_login_attempt_->stop(); - - emit LoginComplete(true); - emit LoginSuccess(); - -} - -void TidalService::SendLogin() { - SendLogin(api_token_, username_, password_); -} - -void TidalService::SendLogin(const QString &api_token, const QString &username, const QString &password) { - - login_sent_ = true; - ++login_attempts_; - if (timer_login_attempt_->isActive()) timer_login_attempt_->stop(); - timer_login_attempt_->setInterval(kTimeResetLoginAttempts); - timer_login_attempt_->start(); - - const ParamList params = ParamList() << Param("token", (api_token.isEmpty() ? api_token_ : api_token)) - << Param("username", username) - << Param("password", password) - << Param("clientVersion", "2.2.1--7"); - - QUrlQuery url_query; - for (const Param ¶m : params) { - EncodedParam encoded_param(QUrl::toPercentEncoding(param.first), QUrl::toPercentEncoding(param.second)); - url_query.addQueryItem(encoded_param.first, encoded_param.second); - } - - QUrl url(kAuthUrl); - QNetworkRequest req(url); - - req.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - req.setRawHeader("X-Tidal-Token", (api_token.isEmpty() ? api_token_.toUtf8() : api_token.toUtf8())); - - QByteArray query = url_query.toString(QUrl::FullyEncoded).toUtf8(); - QNetworkReply *reply = network_->post(req, query); - connect(reply, SIGNAL(sslErrors(QList)), this, SLOT(HandleLoginSSLErrors(QList))); - NewClosure(reply, SIGNAL(finished()), this, SLOT(HandleAuthReply(QNetworkReply*)), reply); - - //qLog(Debug) << "Tidal: Sending request" << url << query; - -} - -void TidalService::HandleAuthReply(QNetworkReply *reply) { - - reply->deleteLater(); - - login_sent_ = false; - - if (reply->error() != QNetworkReply::NoError || reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 200) { - if (reply->error() != QNetworkReply::NoError && reply->error() < 200) { - // This is a network error, there is nothing more to do. - LoginError(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - login_errors_.clear(); - return; - } - else { - // See if there is Json data containing "status" and "userMessage" - then use that instead. - QByteArray data(reply->readAll()); - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - if (json_error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { - QJsonObject json_obj = json_doc.object(); - if (!json_obj.isEmpty() && json_obj.contains("status") && json_obj.contains("userMessage")) { - int status = json_obj["status"].toInt(); - int sub_status = json_obj["subStatus"].toInt(); - QString user_message = json_obj["userMessage"].toString(); - login_errors_ << QString("Authentication failure: %1 (%2) (%3)").arg(user_message).arg(status).arg(sub_status); - } - } - if (login_errors_.isEmpty()) { - if (reply->error() != QNetworkReply::NoError) { - login_errors_ << QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - else { - login_errors_ << QString("Received HTTP code %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); - } - } - LoginError(); - login_errors_.clear(); - return; - } - } - - login_errors_.clear(); - - QByteArray data(reply->readAll()); - QJsonParseError json_error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &json_error); - - if (json_error.error != QJsonParseError::NoError) { - LoginError("Authentication reply from server missing Json data."); - return; - } - - if (json_doc.isNull() || json_doc.isEmpty()) { - LoginError("Authentication reply from server has empty Json document."); - return; - } - - if (!json_doc.isObject()) { - LoginError("Authentication reply from server has Json document that is not an object.", json_doc); - return; - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - LoginError("Authentication reply from server has empty Json object.", json_doc); - return; - } - - if (!json_obj.contains("userId") || !json_obj.contains("sessionId") || !json_obj.contains("countryCode") ) { - LoginError("Authentication reply from server is missing userId, sessionId or countryCode", json_obj); - return; - } - - country_code_ = json_obj["countryCode"].toString(); - session_id_ = json_obj["sessionId"].toString(); - user_id_ = json_obj["userId"].toInt(); - access_token_.clear(); - refresh_token_.clear(); - expiry_time_ = QDateTime(); - - QSettings s; - s.beginGroup(TidalSettingsPage::kSettingsGroup); - s.remove("access_token"); - s.remove("refresh_token"); - s.remove("expiry_time"); - s.setValue("user_id", user_id_); - s.setValue("session_id", session_id_); - s.setValue("country_code", country_code_); - s.endGroup(); - - qLog(Debug) << "Tidal: Login successful" << "user id" << user_id_ << "session id" << session_id_ << "country code" << country_code_; - - login_attempts_ = 0; - if (timer_login_attempt_->isActive()) timer_login_attempt_->stop(); - - emit LoginComplete(true); - emit LoginSuccess(); - -} - -void TidalService::Logout() { - - access_token_.clear(); - session_id_.clear(); - expiry_time_ = QDateTime(); - - QSettings s; - s.beginGroup(TidalSettingsPage::kSettingsGroup); - s.remove("user_id"); - s.remove("country_code"); - s.remove("access_token"); - s.remove("session_id"); - s.remove("expiry_time"); - s.endGroup(); - -} - -void TidalService::ResetLoginAttempts() { - login_attempts_ = 0; -} - -void TidalService::TryLogin() { - - if (authenticated() || login_sent_) return; - - if (api_token_.isEmpty()) { - emit LoginComplete(false, tr("Missing Tidal API token.")); - return; - } - if (username_.isEmpty()) { - emit LoginComplete(false, tr("Missing Tidal username.")); - return; - } - if (password_.isEmpty()) { - emit LoginComplete(false, tr("Missing Tidal password.")); - return; - } - if (login_attempts_ >= kLoginAttempts) { - emit LoginComplete(false, tr("Not authenticated with Tidal and reached maximum number of login attempts.")); - return; - } - - emit Login(); - -} - -void TidalService::ResetArtistsRequest() { - - if (artists_request_.get()) { - disconnect(artists_request_.get(), 0, this, 0); - disconnect(this, 0, artists_request_.get(), 0); - artists_request_.reset(); - } - -} - -void TidalService::GetArtists() { - - if (!authenticated()) { - if (oauth_) { - emit ArtistsResults(SongList(), tr("Not authenticated with Tidal.")); - ShowConfig(); - return; - } - else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) { - emit ArtistsResults(SongList(), tr("Missing Tidal API token, username or passord.")); - ShowConfig(); - return; - } - } - - ResetArtistsRequest(); - - artists_request_.reset(new TidalRequest(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Artists, this)); - - connect(artists_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(ArtistsResultsReceived(const int, const SongList&, const QString&))); - connect(artists_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(ArtistsUpdateStatusReceived(const int, const QString&))); - connect(artists_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SLOT(ArtistsProgressSetMaximumReceived(const int, const int))); - connect(artists_request_.get(), SIGNAL(UpdateProgress(const int, const int)), SLOT(ArtistsUpdateProgressReceived(const int, const int))); - connect(this, SIGNAL(LoginComplete(const bool, QString)), artists_request_.get(), SLOT(LoginComplete(const bool, QString))); - - artists_request_->Process(); - -} - -void TidalService::ArtistsResultsReceived(const int id, const SongList &songs, const QString &error) { - Q_UNUSED(id); - emit ArtistsResults(songs, error); -} - -void TidalService::ArtistsUpdateStatusReceived(const int id, const QString &text) { - Q_UNUSED(id); - emit ArtistsUpdateStatus(text); -} - -void TidalService::ArtistsProgressSetMaximumReceived(const int id, const int max) { - Q_UNUSED(id); - emit ArtistsProgressSetMaximum(max); -} - -void TidalService::ArtistsUpdateProgressReceived(const int id, const int progress) { - Q_UNUSED(id); - emit ArtistsUpdateProgress(progress); -} - -void TidalService::ResetAlbumsRequest() { - - if (albums_request_.get()) { - disconnect(albums_request_.get(), 0, this, 0); - disconnect(this, 0, albums_request_.get(), 0); - albums_request_.reset(); - } - -} - -void TidalService::GetAlbums() { - - if (!authenticated()) { - if (oauth_) { - emit AlbumsResults(SongList(), tr("Not authenticated with Tidal.")); - ShowConfig(); - return; - } - else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) { - emit AlbumsResults(SongList(), tr("Missing Tidal API token, username or passord.")); - ShowConfig(); - return; - } - } - - ResetAlbumsRequest(); - albums_request_.reset(new TidalRequest(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Albums, this)); - connect(albums_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(AlbumsResultsReceived(const int, const SongList&, const QString&))); - connect(albums_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(AlbumsUpdateStatusReceived(const int, const QString&))); - connect(albums_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SLOT(AlbumsProgressSetMaximumReceived(const int, const int))); - connect(albums_request_.get(), SIGNAL(UpdateProgress(const int, const int)), SLOT(AlbumsUpdateProgressReceived(const int, const int))); - connect(this, SIGNAL(LoginComplete(const bool, const QString&)), albums_request_.get(), SLOT(LoginComplete(const bool, const QString&))); - - albums_request_->Process(); - -} - -void TidalService::AlbumsResultsReceived(const int id, const SongList &songs, const QString &error) { - Q_UNUSED(id); - emit AlbumsResults(songs, error); -} - -void TidalService::AlbumsUpdateStatusReceived(const int id, const QString &text) { - Q_UNUSED(id); - emit AlbumsUpdateStatus(text); -} - -void TidalService::AlbumsProgressSetMaximumReceived(const int id, const int max) { - Q_UNUSED(id); - emit AlbumsProgressSetMaximum(max); -} - -void TidalService::AlbumsUpdateProgressReceived(const int id, const int progress) { - Q_UNUSED(id); - emit AlbumsUpdateProgress(progress); -} - -void TidalService::ResetSongsRequest() { - - if (songs_request_.get()) { - disconnect(songs_request_.get(), 0, this, 0); - disconnect(this, 0, songs_request_.get(), 0); - songs_request_.reset(); - } - -} - -void TidalService::GetSongs() { - - if (!authenticated()) { - if (oauth_) { - emit SongsResults(SongList(), tr("Not authenticated with Tidal.")); - ShowConfig(); - return; - } - else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) { - emit SongsResults(SongList(), tr("Missing Tidal API token, username or passord.")); - ShowConfig(); - return; - } - } - - ResetSongsRequest(); - songs_request_.reset(new TidalRequest(this, url_handler_, app_, network_, TidalBaseRequest::QueryType_Songs, this)); - connect(songs_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(SongsResultsReceived(const int, const SongList&, const QString&))); - connect(songs_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SLOT(SongsUpdateStatusReceived(const int, const QString&))); - connect(songs_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SLOT(SongsProgressSetMaximumReceived(const int, const int))); - connect(songs_request_.get(), SIGNAL(UpdateProgress(const int, const int)), SLOT(SongsUpdateProgressReceived(const int, const int))); - connect(this, SIGNAL(LoginComplete(const bool, const QString&)), songs_request_.get(), SLOT(LoginComplete(const bool, const QString&))); - - songs_request_->Process(); - -} - -void TidalService::SongsResultsReceived(const int id, const SongList &songs, const QString &error) { - Q_UNUSED(id); - emit SongsResults(songs, error); -} - -void TidalService::SongsUpdateStatusReceived(const int id, const QString &text) { - Q_UNUSED(id); - emit SongsUpdateStatus(text); -} - -void TidalService::SongsProgressSetMaximumReceived(const int id, const int max) { - Q_UNUSED(id); - emit SongsProgressSetMaximum(max); -} - -void TidalService::SongsUpdateProgressReceived(const int id, const int progress) { - Q_UNUSED(id); - emit SongsUpdateProgress(progress); -} - -int TidalService::Search(const QString &text, InternetSearch::SearchType type) { - - pending_search_id_ = next_pending_search_id_; - pending_search_text_ = text; - pending_search_type_ = type; - - next_pending_search_id_++; - - if (text.isEmpty()) { - timer_search_delay_->stop(); - return pending_search_id_; - } - timer_search_delay_->setInterval(search_delay_); - timer_search_delay_->start(); - - return pending_search_id_; - -} - -void TidalService::StartSearch() { - - if (!authenticated()) { - if (oauth_) { - emit SearchResults(pending_search_id_, SongList(), tr("Not authenticated with Tidal.")); - ShowConfig(); - return; - } - else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) { - emit SearchResults(pending_search_id_, SongList(), tr("Missing Tidal API token, username or passord.")); - ShowConfig(); - return; - } - } - - search_id_ = pending_search_id_; - search_text_ = pending_search_text_; - - SendSearch(); - -} - -void TidalService::CancelSearch() { -} - -void TidalService::SendSearch() { - - TidalBaseRequest::QueryType type; - - switch (pending_search_type_) { - case InternetSearch::SearchType_Artists: - type = TidalBaseRequest::QueryType_SearchArtists; - break; - case InternetSearch::SearchType_Albums: - type = TidalBaseRequest::QueryType_SearchAlbums; - break; - case InternetSearch::SearchType_Songs: - type = TidalBaseRequest::QueryType_SearchSongs; - break; - default: - //Error("Invalid search type."); - return; - } - - search_request_.reset(new TidalRequest(this, url_handler_, app_, network_, type, this)); - - connect(search_request_.get(), SIGNAL(Results(const int, const SongList&, const QString&)), SLOT(SearchResultsReceived(const int, const SongList&, const QString&))); - connect(search_request_.get(), SIGNAL(UpdateStatus(const int, const QString&)), SIGNAL(SearchUpdateStatus(const int, const QString&))); - connect(search_request_.get(), SIGNAL(ProgressSetMaximum(const int, const int)), SIGNAL(SearchProgressSetMaximum(const int, const int))); - connect(search_request_.get(), SIGNAL(UpdateProgress(const int, const int)), SIGNAL(SearchUpdateProgress(const int, const int))); - connect(this, SIGNAL(LoginComplete(const bool, const QString&)), search_request_.get(), SLOT(LoginComplete(const bool, const QString&))); - - search_request_->Search(search_id_, search_text_); - search_request_->Process(); - -} - -void TidalService::SearchResultsReceived(const int id, const SongList &songs, const QString &error) { - emit SearchResults(id, songs, error); -} - -void TidalService::GetStreamURL(const QUrl &url) { - - if (!authenticated()) { - if (oauth_) { - emit StreamURLFinished(url, url, Song::FileType_Stream, -1, -1, -1, tr("Not authenticated with Tidal.")); - return; - } - else if (api_token_.isEmpty() || username_.isEmpty() || password_.isEmpty()) { - emit StreamURLFinished(url, url, Song::FileType_Stream, -1, -1, -1, tr("Missing Tidal API token, username or passord.")); - return; - } - } - - TidalStreamURLRequest *stream_url_req = new TidalStreamURLRequest(this, network_, url, this); - stream_url_requests_ << stream_url_req; - - connect(stream_url_req, SIGNAL(TryLogin()), this, SLOT(TryLogin())); - connect(stream_url_req, SIGNAL(StreamURLFinished(const QUrl&, const QUrl&, const Song::FileType, const int, const int, const qint64, QString)), this, SLOT(HandleStreamURLFinished(const QUrl&, const QUrl&, const Song::FileType, const int, const int, const qint64, QString))); - connect(this, SIGNAL(LoginComplete(const bool, const QString&)), stream_url_req, SLOT(LoginComplete(const bool, QString))); - - stream_url_req->Process(); - -} - -void TidalService::HandleStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error) { - - TidalStreamURLRequest *stream_url_req = qobject_cast(sender()); - if (!stream_url_req || !stream_url_requests_.contains(stream_url_req)) return; - stream_url_req->deleteLater(); - stream_url_requests_.removeAll(stream_url_req); - - emit StreamURLFinished(original_url, stream_url, filetype, samplerate, bit_depth, duration, error); - -} - -void TidalService::LoginError(const QString &error, const QVariant &debug) { - - if (!error.isEmpty()) login_errors_ << error; - - QString error_html; - for (const QString &error : login_errors_) { - qLog(Error) << "Tidal:" << error; - error_html += error + "
    "; - } - if (debug.isValid()) qLog(Debug) << debug; - - emit LoginFailure(error_html); - emit LoginComplete(false, error_html); - - login_errors_.clear(); - -} diff --git a/src/tidal/tidalservice.h b/src/tidal/tidalservice.h deleted file mode 100644 index a2a47e47..00000000 --- a/src/tidal/tidalservice.h +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef TIDALSERVICE_H -#define TIDALSERVICE_H - -#include "config.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/song.h" -#include "internet/internetservice.h" -#include "internet/internetsearch.h" -#include "settings/tidalsettingspage.h" - -class QSortFilterProxyModel; -class QNetworkReply; -class QTimer; - -class Application; -class NetworkAccessManager; -class TidalUrlHandler; -class TidalRequest; -class TidalFavoriteRequest; -class TidalStreamURLRequest; -class CollectionBackend; -class CollectionModel; - -using std::shared_ptr; - -class TidalService : public InternetService { - Q_OBJECT - - public: - TidalService(Application *app, QObject *parent); - ~TidalService(); - - static const Song::Source kSource; - - void Exit(); - void ReloadSettings(); - - void Logout(); - int Search(const QString &query, InternetSearch::SearchType type); - void CancelSearch(); - - int max_login_attempts() { return kLoginAttempts; } - - Application *app() { return app_; } - - bool oauth() { return oauth_; } - QString client_id() { return client_id_; } - QString api_token() { return api_token_; } - quint64 user_id() { return user_id_; } - QString country_code() { return country_code_; } - QString username() { return username_; } - QString password() { return password_; } - QString quality() { return quality_; } - int search_delay() { return search_delay_; } - int artistssearchlimit() { return artistssearchlimit_; } - int albumssearchlimit() { return albumssearchlimit_; } - int songssearchlimit() { return songssearchlimit_; } - bool fetchalbums() { return fetchalbums_; } - QString coversize() { return coversize_; } - bool download_album_covers() { return download_album_covers_; } - TidalSettingsPage::StreamUrlMethod stream_url_method() { return stream_url_method_; } - - QString access_token() { return access_token_; } - QString session_id() { return session_id_; } - - bool authenticated() { return (!access_token_.isEmpty() || !session_id_.isEmpty()); } - bool login_sent() { return login_sent_; } - bool login_attempts() { return login_attempts_; } - - void GetStreamURL(const QUrl &url); - - CollectionBackend *artists_collection_backend() { return artists_collection_backend_; } - CollectionBackend *albums_collection_backend() { return albums_collection_backend_; } - CollectionBackend *songs_collection_backend() { return songs_collection_backend_; } - - CollectionModel *artists_collection_model() { return artists_collection_model_; } - CollectionModel *albums_collection_model() { return albums_collection_model_; } - CollectionModel *songs_collection_model() { return songs_collection_model_; } - - QSortFilterProxyModel *artists_collection_sort_model() { return artists_collection_sort_model_; } - QSortFilterProxyModel *albums_collection_sort_model() { return albums_collection_sort_model_; } - QSortFilterProxyModel *songs_collection_sort_model() { return songs_collection_sort_model_; } - - enum QueryType { - QueryType_Artists, - QueryType_Albums, - QueryType_Songs, - QueryType_SearchArtists, - QueryType_SearchAlbums, - QueryType_SearchSongs, - }; - - signals: - - public slots: - void ShowConfig(); - void TryLogin(); - void SendLogin(const QString &api_token, const QString &username, const QString &password); - void GetArtists(); - void GetAlbums(); - void GetSongs(); - void ResetArtistsRequest(); - void ResetAlbumsRequest(); - void ResetSongsRequest(); - - private slots: - void ExitReceived(); - void StartAuthorisation(); - void AuthorisationUrlReceived(const QUrl &url); - void HandleLoginSSLErrors(QList ssl_errors); - void AccessTokenRequestFinished(QNetworkReply *reply); - void SendLogin(); - void HandleAuthReply(QNetworkReply *reply); - void ResetLoginAttempts(); - void StartSearch(); - void ArtistsResultsReceived(const int id, const SongList &songs, const QString &error); - void AlbumsResultsReceived(const int id, const SongList &songs, const QString &error); - void SongsResultsReceived(const int id, const SongList &songs, const QString &error); - void SearchResultsReceived(const int id, const SongList &songs, const QString &error); - void ArtistsUpdateStatusReceived(const int id, const QString &text); - void AlbumsUpdateStatusReceived(const int id, const QString &text); - void SongsUpdateStatusReceived(const int id, const QString &text); - void ArtistsProgressSetMaximumReceived(const int id, const int max); - void AlbumsProgressSetMaximumReceived(const int id, const int max); - void SongsProgressSetMaximumReceived(const int id, const int max); - void ArtistsUpdateProgressReceived(const int id, const int progress); - void AlbumsUpdateProgressReceived(const int id, const int progress); - void SongsUpdateProgressReceived(const int id, const int progress); - void HandleStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString()); - - private: - typedef QPair Param; - typedef QList ParamList; - - typedef QPair EncodedParam; - typedef QList EncodedParamList; - - void SendSearch(); - void LoginError(const QString &error = QString(), const QVariant &debug = QVariant()); - - static const char *kApiTokenB64; - static const char *kOAuthUrl; - static const char *kOAuthAccessTokenUrl; - static const char *kOAuthRedirectUrl; - static const char *kAuthUrl; - static const int kLoginAttempts; - static const int kTimeResetLoginAttempts; - - static const char *kArtistsSongsTable; - static const char *kAlbumsSongsTable; - static const char *kSongsTable; - - static const char *kArtistsSongsFtsTable; - static const char *kAlbumsSongsFtsTable; - static const char *kSongsFtsTable; - - Application *app_; - NetworkAccessManager *network_; - TidalUrlHandler *url_handler_; - - CollectionBackend *artists_collection_backend_; - CollectionBackend *albums_collection_backend_; - CollectionBackend *songs_collection_backend_; - - CollectionModel *artists_collection_model_; - CollectionModel *albums_collection_model_; - CollectionModel *songs_collection_model_; - - QSortFilterProxyModel *artists_collection_sort_model_; - QSortFilterProxyModel *albums_collection_sort_model_; - QSortFilterProxyModel *songs_collection_sort_model_; - - QTimer *timer_search_delay_; - QTimer *timer_login_attempt_; - - std::shared_ptr artists_request_; - std::shared_ptr albums_request_; - std::shared_ptr songs_request_; - std::shared_ptr search_request_; - TidalFavoriteRequest *favorite_request_; - - bool oauth_; - QString client_id_; - QString api_token_; - quint64 user_id_; - QString country_code_; - QString username_; - QString password_; - QString quality_; - int search_delay_; - int artistssearchlimit_; - int albumssearchlimit_; - int songssearchlimit_; - bool fetchalbums_; - QString coversize_; - bool download_album_covers_; - TidalSettingsPage::StreamUrlMethod stream_url_method_; - - QString access_token_; - QString refresh_token_; - QString session_id_; - QDateTime expiry_time_; - - int pending_search_id_; - int next_pending_search_id_; - QString pending_search_text_; - InternetSearch::SearchType pending_search_type_; - - int search_id_; - QString search_text_; - bool login_sent_; - int login_attempts_; - - QString code_verifier_; - QString code_challenge_; - - QList stream_url_requests_; - - QStringList login_errors_; - - QList wait_for_exit_; - -}; - -#endif // TIDALSERVICE_H diff --git a/src/tidal/tidalstreamurlrequest.cpp b/src/tidal/tidalstreamurlrequest.cpp deleted file mode 100644 index cbbf70ab..00000000 --- a/src/tidal/tidalstreamurlrequest.cpp +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/logging.h" -#include "core/network.h" -#include "core/song.h" -#include "settings/tidalsettingspage.h" -#include "tidalservice.h" -#include "tidalbaserequest.h" -#include "tidalstreamurlrequest.h" - -TidalStreamURLRequest::TidalStreamURLRequest(TidalService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent) - : TidalBaseRequest(service, network, parent), - service_(service), - reply_(nullptr), - original_url_(original_url), - song_id_(original_url.path().toInt()), - tries_(0), - need_login_(false) {} - -TidalStreamURLRequest::~TidalStreamURLRequest() { - - if (reply_) { - disconnect(reply_, 0, this, 0); - if (reply_->isRunning()) reply_->abort(); - reply_->deleteLater(); - } - -} - -void TidalStreamURLRequest::LoginComplete(const bool success, QString error) { - - if (!need_login_) return; - need_login_ = false; - - if (!success) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, error); - return; - } - - Process(); - -} - -void TidalStreamURLRequest::Process() { - - if (!authenticated()) { - if (oauth()) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Not authenticated with Tidal.")); - return; - } - else if (api_token().isEmpty() || username().isEmpty() || password().isEmpty()) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Missing Tidal API token, username or passord.")); - return; - } - need_login_ = true; - emit TryLogin(); - return; - } - - GetStreamURL(); - -} - -void TidalStreamURLRequest::Cancel() { - - if (reply_ && reply_->isRunning()) { - reply_->abort(); - } - else { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, tr("Cancelled.")); - } - -} - -void TidalStreamURLRequest::GetStreamURL() { - - ++tries_; - - if (reply_) { - disconnect(reply_, 0, this, 0); - if (reply_->isRunning()) reply_->abort(); - reply_->deleteLater(); - } - - ParamList params; - - switch (stream_url_method()) { - case TidalSettingsPage::StreamUrlMethod_StreamUrl: - params << Param("soundQuality", quality()); - reply_ = CreateRequest(QString("tracks/%1/streamUrl").arg(song_id_), params); - connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived())); - break; - case TidalSettingsPage::StreamUrlMethod_UrlPostPaywall: - params << Param("audioquality", quality()); - params << Param("playbackmode", "STREAM"); - params << Param("assetpresentation", "FULL"); - params << Param("urlusagemode", "STREAM"); - reply_ = CreateRequest(QString("tracks/%1/urlpostpaywall").arg(song_id_), params); - connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived())); - break; - case TidalSettingsPage::StreamUrlMethod_PlaybackInfoPostPaywall: - params << Param("audioquality", quality()); - params << Param("playbackmode", "STREAM"); - params << Param("assetpresentation", "FULL"); - reply_ = CreateRequest(QString("tracks/%1/playbackinfopostpaywall").arg(song_id_), params); - connect(reply_, SIGNAL(finished()), this, SLOT(StreamURLReceived())); - break; - } - -} - -void TidalStreamURLRequest::StreamURLReceived() { - - if (!reply_) return; - disconnect(reply_, 0, this, 0); - reply_->deleteLater(); - - QByteArray data = GetReplyData(reply_, true); - if (data.isEmpty()) { - reply_ = nullptr; - if (!authenticated() && login_sent() && tries_ <= 1) { - need_login_ = true; - return; - } - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - reply_ = nullptr; - - //qLog(Debug) << "Tidal:" << data; - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - - if (!json_obj.contains("trackId")) { - Error("Invalid Json reply, stream missing trackId.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - int track_id(json_obj["trackId"].toInt()); - if (track_id != song_id_) { - Error("Incorrect track ID returned.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - - Song::FileType filetype(Song::FileType_Unknown); - - if (json_obj.contains("codec") || json_obj.contains("codecs")) { - QString codec; - if (json_obj.contains("codec")) codec = json_obj["codec"].toString().toLower(); - if (json_obj.contains("codecs")) codec = json_obj["codecs"].toString().toLower(); - filetype = Song::FiletypeByExtension(codec); - if (filetype == Song::FileType_Unknown) { - qLog(Debug) << "Tidal: Unknown codec" << codec; - filetype = Song::FileType_Stream; - } - } - - QList urls; - - if (json_obj.contains("manifest")) { - - QString manifest(json_obj["manifest"].toString()); - QByteArray data_manifest = QByteArray::fromBase64(manifest.toUtf8()); - - //qLog(Debug) << "Tidal:" << data_manifest; - - QXmlStreamReader xml_reader(data_manifest); - if (xml_reader.readNextStartElement()) { - - QString filepath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/tidalstreams"; - QString filename = "tidal-" + QString::number(song_id_) + ".xml"; - if (!QDir().mkpath(filepath)) { - Error(QString("Failed to create directory %1.").arg(filepath), json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - QUrl url("file://" + filepath + "/" + filename); - QFile file(url.toLocalFile()); - if (file.exists()) - file.remove(); - if (!file.open(QIODevice::WriteOnly)) { - Error(QString("Failed to open file %1 for writing.").arg(url.toLocalFile()), json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - file.write(data_manifest); - file.close(); - - urls << url; - - } - - else { - - json_obj = ExtractJsonObj(data_manifest); - if (json_obj.isEmpty()) { - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - - if (!json_obj.contains("mimeType")) { - Error("Invalid Json reply, stream url reply manifest is missing mimeType.", json_obj); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - - QString mimetype = json_obj["mimeType"].toString(); - QMimeDatabase mimedb; - for (QString suffix : mimedb.mimeTypeForName(mimetype.toUtf8()).suffixes()) { - filetype = Song::FiletypeByExtension(suffix); - if (filetype != Song::FileType_Unknown) break; - } - if (filetype == Song::FileType_Unknown) { - qLog(Debug) << "Tidal: Unknown mimetype" << mimetype; - filetype = Song::FileType_Stream; - } - } - - } - - if (json_obj.contains("urls")) { - QJsonValue json_urls = json_obj["urls"]; - if (!json_urls.isArray()) { - Error("Invalid Json reply, urls is not an array.", json_urls); - emit StreamURLFinished(original_url_, original_url_, Song::FileType_Stream, -1, -1, -1, errors_.first()); - return; - } - QJsonArray json_array_urls = json_urls.toArray(); - for (const QJsonValue &value : json_array_urls) { - urls << QUrl(value.toString()); - } - } - else if (json_obj.contains("url")) { - QUrl new_url(json_obj["url"].toString()); - urls << new_url; - } - - if (urls.isEmpty()) { - Error("Missing stream urls.", json_obj); - emit StreamURLFinished(original_url_, original_url_, filetype, -1, -1, -1, errors_.first()); - return; - } - - emit StreamURLFinished(original_url_, urls.first(), filetype, -1, -1, -1); - -} - -void TidalStreamURLRequest::Error(const QString &error, const QVariant &debug) { - - qLog(Error) << "Tidal:" << error; - if (debug.isValid()) qLog(Debug) << debug; - - if (!error.isEmpty()) { - errors_ << error; - } - -} diff --git a/src/tidal/tidalstreamurlrequest.h b/src/tidal/tidalstreamurlrequest.h deleted file mode 100644 index aacb86f0..00000000 --- a/src/tidal/tidalstreamurlrequest.h +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef TIDALSTREAMURLREQUEST_H -#define TIDALSTREAMURLREQUEST_H - -#include "config.h" - -#include -#include -#include -#include -#include -#include - -#include "core/song.h" -#include "tidalservice.h" -#include "tidalbaserequest.h" -#include "settings/tidalsettingspage.h" - -class QNetworkReply; -class NetworkAccessManager; - -class TidalStreamURLRequest : public TidalBaseRequest { - Q_OBJECT - - public: - TidalStreamURLRequest(TidalService *service, NetworkAccessManager *network, const QUrl &original_url, QObject *parent); - ~TidalStreamURLRequest(); - - void GetStreamURL(); - void Process(); - void NeedLogin() { need_login_ = true; } - void Cancel(); - - bool oauth() { return service_->oauth(); } - TidalSettingsPage::StreamUrlMethod stream_url_method() { return service_->stream_url_method(); } - QUrl original_url() { return original_url_; } - int song_id() { return song_id_; } - bool need_login() { return need_login_; } - - signals: - void TryLogin(); - void StreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString()); - - private slots: - void LoginComplete(const bool success, QString error = QString()); - void StreamURLReceived(); - - private: - void Error(const QString &error, const QVariant &debug = QVariant()); - - TidalService *service_; - QNetworkReply *reply_; - QUrl original_url_; - int song_id_; - int tries_; - bool need_login_; - QStringList errors_; - -}; - -#endif // TIDALSTREAMURLREQUEST_H diff --git a/src/tidal/tidalurlhandler.cpp b/src/tidal/tidalurlhandler.cpp deleted file mode 100644 index c49d672e..00000000 --- a/src/tidal/tidalurlhandler.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#include "config.h" - -#include -#include -#include - -#include "core/application.h" -#include "core/taskmanager.h" -#include "core/song.h" -#include "tidal/tidalservice.h" -#include "tidalurlhandler.h" - -TidalUrlHandler::TidalUrlHandler(Application *app, TidalService *service) : - UrlHandler(service), - app_(app), - service_(service), - task_id_(-1) - { - - connect(service, SIGNAL(StreamURLFinished(const QUrl&, const QUrl&, const Song::FileType, const int, const int, const qint64, QString)), this, SLOT(GetStreamURLFinished(const QUrl&, const QUrl&, const Song::FileType, const int, const int, const qint64, QString))); - -} - -UrlHandler::LoadResult TidalUrlHandler::StartLoading(const QUrl &url) { - - LoadResult ret(url); - if (task_id_ != -1) return ret; - task_id_ = app_->task_manager()->StartTask(QString("Loading %1 stream...").arg(url.scheme())); - service_->GetStreamURL(url); - ret.type_ = LoadResult::WillLoadAsynchronously; - return ret; - -} - -void TidalUrlHandler::GetStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error) { - - if (task_id_ == -1) return; - CancelTask(); - if (error.isEmpty()) - emit AsyncLoadComplete(LoadResult(original_url, LoadResult::TrackAvailable, stream_url, filetype, samplerate, bit_depth, duration)); - else - emit AsyncLoadComplete(LoadResult(original_url, LoadResult::Error, stream_url, filetype, -1, -1, -1, error)); - -} - -void TidalUrlHandler::CancelTask() { - app_->task_manager()->SetTaskFinished(task_id_); - task_id_ = -1; -} diff --git a/src/tidal/tidalurlhandler.h b/src/tidal/tidalurlhandler.h deleted file mode 100644 index 519510a6..00000000 --- a/src/tidal/tidalurlhandler.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Strawberry Music Player - * Copyright 2018, Jonas Kvinge - * - * 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 . - * - */ - -#ifndef TIDALURLHANDLER_H -#define TIDALURLHANDLER_H - -#include "config.h" - -#include -#include -#include -#include - -#include "core/urlhandler.h" -#include "core/song.h" -#include "tidal/tidalservice.h" - -class Application; - -class TidalUrlHandler : public UrlHandler { - Q_OBJECT - - public: - TidalUrlHandler(Application *app, TidalService *service); - - QString scheme() const { return service_->url_scheme(); } - LoadResult StartLoading(const QUrl &url); - - void CancelTask(); - - private slots: - void GetStreamURLFinished(const QUrl &original_url, const QUrl &stream_url, const Song::FileType filetype, const int samplerate, const int bit_depth, const qint64 duration, QString error = QString()); - - private: - Application *app_; - TidalService *service_; - int task_id_; - -}; - -#endif