From 380f2509acf23484b72797ed9568ad522ae9a71d Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sat, 9 Mar 2019 17:43:20 +0100 Subject: [PATCH] Remove deezer --- .travis.yml | 6 +- CMakeLists.txt | 33 +- README.md | 8 +- data/data.qrc | 1 - data/icons.qrc | 4 - data/icons/128x128/deezer.png | Bin 2793 -> 0 bytes data/icons/22x22/deezer.png | Bin 1115 -> 0 bytes data/icons/32x32/deezer.png | Bin 1469 -> 0 bytes data/icons/48x48/deezer.png | Bin 2187 -> 0 bytes data/icons/64x64/deezer.png | Bin 2015 -> 0 bytes data/icons/full/deezer.png | Bin 4155 -> 0 bytes data/pictures/deezer.png | Bin 6217 -> 0 bytes data/pictures/deezer_black_big.jpg | Bin 29569 -> 0 bytes data/pictures/deezer_white_big.jpg | Bin 28862 -> 0 bytes dist/debian/copyright | 5 - dist/debian/rules | 3 +- dist/pacman/PKGBUILD.in | 1 - dist/unix/org.strawbs.strawberry.appdata.xml | 2 +- dist/windows/strawberry-debug-x64.nsi.in | 2 - dist/windows/strawberry-debug-x86.nsi.in | 2 - dist/windows/strawberry-x64.nsi.in | 2 - dist/windows/strawberry-x86.nsi.in | 2 - src/CMakeLists.txt | 35 - src/config.h.in | 3 - src/core/application.cpp | 15 - src/core/application.h | 3 - src/core/mainwindow.cpp | 25 - src/core/mainwindow.h | 1 - src/core/player.cpp | 22 +- src/core/song.cpp | 4 +- src/core/song.h | 1 - src/deezer/deezerservice.cpp | 827 ------------------- src/deezer/deezerservice.h | 162 ---- src/deezer/deezerurlhandler.cpp | 72 -- src/deezer/deezerurlhandler.h | 56 -- src/engine/deezerengine.cpp | 536 ------------ src/engine/deezerengine.h | 94 --- src/engine/enginetype.cpp | 3 - src/engine/enginetype.h | 3 +- src/playlist/playlistview.h | 2 +- src/settings/backendsettingspage.cpp | 3 - src/settings/deezersettingspage.cpp | 142 ---- src/settings/deezersettingspage.h | 60 -- src/settings/deezersettingspage.ui | 354 -------- src/settings/settingsdialog.cpp | 8 +- src/settings/settingsdialog.h | 1 - 46 files changed, 19 insertions(+), 2484 deletions(-) delete mode 100644 data/icons/128x128/deezer.png delete mode 100644 data/icons/22x22/deezer.png delete mode 100644 data/icons/32x32/deezer.png delete mode 100644 data/icons/48x48/deezer.png delete mode 100644 data/icons/64x64/deezer.png delete mode 100644 data/icons/full/deezer.png delete mode 100644 data/pictures/deezer.png delete mode 100644 data/pictures/deezer_black_big.jpg delete mode 100644 data/pictures/deezer_white_big.jpg delete mode 100644 src/deezer/deezerservice.cpp delete mode 100644 src/deezer/deezerservice.h delete mode 100644 src/deezer/deezerurlhandler.cpp delete mode 100644 src/deezer/deezerurlhandler.h delete mode 100644 src/engine/deezerengine.cpp delete mode 100644 src/engine/deezerengine.h delete mode 100644 src/settings/deezersettingspage.cpp delete mode 100644 src/settings/deezersettingspage.h delete mode 100644 src/settings/deezersettingspage.ui diff --git a/.travis.yml b/.travis.yml index 8b255b26..92b62983 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ before_install: git pull; brew update; brew unlink python; - brew install glib pkgconfig protobuf protobuf-c qt gettext; + brew install glib pkgconfig libffi protobuf protobuf-c qt gettext; brew install sqlite --with-fts; brew install gstreamer gst-plugins-base; brew install gst-plugins-good --with-flac; @@ -35,8 +35,8 @@ before_install: ls /usr/local/lib/gstreamer-1.0; fi before_script: - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker exec build cmake -Hstrawberry -Bbuild -DENABLE_STREAM_DEEZER=ON -DENABLE_TRANSLATIONS=ON ; fi - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mkdir build; cd build; cmake .. -DUSE_BUNDLE=ON -DENABLE_STREAM_DEEZER=ON -DENABLE_TRANSLATIONS=ON ; fi + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker exec build cmake -Hstrawberry -Bbuild -DENABLE_TRANSLATIONS=ON ; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then mkdir build; cd build; cmake .. -DUSE_BUNDLE=ON -DENABLE_TRANSLATIONS=ON ; fi script: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker exec build make -C build -j8 ; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then diff --git a/CMakeLists.txt b/CMakeLists.txt index 471d4a27..35179559 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,8 +112,6 @@ pkg_check_modules(LIBMTP libmtp>=1.0) pkg_check_modules(LIBIMOBILEDEVICE libimobiledevice-1.0) pkg_check_modules(LIBUSBMUXD libusbmuxd) pkg_check_modules(LIBPLIST libplist) -pkg_check_modules(LIBDEEZER libdeezer) -pkg_check_modules(LIBDZMEDIA libdzmedia) find_package(Gettext) if(WIN32) @@ -291,18 +289,6 @@ optional_component(PHONON OFF "Engine: Phonon backend (UNSTABLE)" DEPENDS "phonon4qt5" PHONON_FOUND ) -if (WIN32) - optional_component(DEEZER ON "Engine: Deezer backend" - DEPENDS "libdeezer" LIBDEEZER_FOUND - ) -else () - optional_component(DEEZER ON "Engine: Deezer backend" - DEPENDS "Linux" LINUX - DEPENDS "libdeezer" LIBDEEZER_FOUND - DEPENDS "libpulse" LIBPULSE_FOUND - ) -endif() - optional_component(CHROMAPRINT ON "Chromaprint (Tag fetching from Musicbrainz)" DEPENDS "chromaprint" CHROMAPRINT_FOUND ) @@ -359,21 +345,6 @@ optional_component(TRANSLATIONS OFF "Translations (No languages included yet)" optional_component(STREAM_TIDAL ON "Streaming: Tidal support") -if (LIBDZMEDIA_FOUND OR LIBDEEZER_FOUND) - optional_component(STREAM_DEEZER ON "Streaming: Deezer support") -else() - optional_component(STREAM_DEEZER OFF "Streaming: Deezer support") -endif() - -optional_component(DZMEDIA ON "DZMedia" - DEPENDS "libdzmedia" LIBDZMEDIA_FOUND - DEPENDS "Deezer support" HAVE_STREAM_DEEZER -) - -if (HAVE_STREAM_DEEZER AND NOT HAVE_DZMEDIA AND NOT HAVE_DEEZER) - message(STATUS "Deezer is enabled, but not DZMedia or Deezer engine, only preview streams will be available.") -endif() - if(APPLE) option(USE_BUNDLE "Bundle macOS dependencies" OFF) elseif(WIN32) @@ -418,8 +389,8 @@ add_custom_target(uninstall # Show a summary of what we have enabled summary_show() -if(NOT HAVE_GSTREAMER AND NOT HAVE_XINE AND NOT HAVE_VLC AND NOT HAVE_PHONON AND NOT HAVE_DEEZER) - message(FATAL_ERROR "You need to have either GStreamer, Xine, VLC, Phonon or Deezer to compile!") +if(NOT HAVE_GSTREAMER AND NOT HAVE_XINE AND NOT HAVE_VLC AND NOT HAVE_PHONON) + message(FATAL_ERROR "You need to have either GStreamer, Xine, VLC or Phonon to compile!") elseif(NOT HAVE_GSTREAMER) message(WARNING "GStreamer is the only engine that is fully implemented. Using other engines is possible but not recommended.") endif() diff --git a/README.md b/README.md index b333b4e7..599dbf8e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Strawberry is a audio player and music collection organizer. It is a fork of Cle * Audio analyzer * Audio equalizer * Transfer music to iPod, iPhone, MTP or mass-storage USB player - * Streaming support for Tidal and Deezer [*] + * Streaming support for Tidal * Scrobbler with support for Last.fm, Libre.fm and ListenBrainz It has so far been tested to work on Linux, OpenBSD, macOS and Windows. @@ -48,7 +48,7 @@ To build Strawberry from source you need the following installed on your system * [ALSA library (linux)](https://www.alsa-project.org/) * [DBus (linux)](https://www.freedesktop.org/wiki/Software/dbus/) * [PulseAudio (linux optional)](https://www.freedesktop.org/wiki/Software/PulseAudio/?) -* [GStreamer](https://gstreamer.freedesktop.org/), [Xine](https://www.xine-project.org), [VLC](https://www.videolan.org), [Deezer](https://build-repo.deezer.com/native_sdk/deezer-native-sdk-v1.2.10.zip) or [Phonon](https://techbase.kde.org/Phonon) +* [GStreamer](https://gstreamer.freedesktop.org/), [Xine](https://www.xine-project.org), [VLC](https://www.videolan.org) or [Phonon](https://techbase.kde.org/Phonon) Optional dependencies: @@ -57,11 +57,9 @@ Optional dependencies: * iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/) * iPhone, iPod Touch, iPad and Apple TV devices: [libimobiledevice, libplist and libusbmuxd](https://www.libimobiledevice.org/) -Either GStreamer, Xine, VLC, Deezer or Phonon engine is required, but only GStreamer is fully implemented so far. +Either GStreamer, Xine, VLC or Phonon engine is required, but only GStreamer is fully implemented so far. You should also install the gstreamer plugins base and good, and optionally bad and ugly. -Deezer support require deezer's own engine, and usually only works on Windows. It is not available on Linux unless you specifically compile with the deezer library, which currently only works on Ubuntu Xenial. The Deezer SDK can be found here: https://build-repo.deezer.com/native_sdk/deezer-native-sdk-v1.2.10.zip - ### :wrench: Compiling from source ### Get the code: diff --git a/data/data.qrc b/data/data.qrc index d6ab1e56..717cb626 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -29,7 +29,6 @@ pictures/osd_background.png pictures/osd_shadow_corner.png pictures/osd_shadow_edge.png - pictures/deezer.png pictures/nyancat.png pictures/rainbowdash.png fonts/HumongousofEternitySt.ttf diff --git a/data/icons.qrc b/data/icons.qrc index 346971ff..fd9fee2d 100644 --- a/data/icons.qrc +++ b/data/icons.qrc @@ -84,7 +84,6 @@ icons/128x128/zoom-in.png icons/128x128/zoom-out.png icons/128x128/tidal.png - icons/128x128/deezer.png icons/128x128/scrobble.png icons/128x128/scrobble-disabled.png icons/64x64/albums.png @@ -171,7 +170,6 @@ icons/64x64/zoom-in.png icons/64x64/zoom-out.png icons/64x64/tidal.png - icons/64x64/deezer.png icons/64x64/scrobble.png icons/64x64/scrobble-disabled.png icons/48x48/albums.png @@ -351,7 +349,6 @@ icons/32x32/zoom-in.png icons/32x32/zoom-out.png icons/32x32/tidal.png - icons/32x32/deezer.png icons/32x32/scrobble.png icons/32x32/scrobble-disabled.png icons/22x22/albums.png @@ -442,7 +439,6 @@ icons/22x22/zoom-in.png icons/22x22/zoom-out.png icons/22x22/tidal.png - icons/22x22/deezer.png icons/22x22/scrobble.png icons/22x22/scrobble-disabled.png diff --git a/data/icons/128x128/deezer.png b/data/icons/128x128/deezer.png deleted file mode 100644 index ca22afc366e90c66ce5e30ea6278ae4d2643bbfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2793 zcmZWrc{tSDAN~$8GBU^&5hYO=#!_Y`M432f#tT`n3M0i2&Lua5GFi|q9?u?+wK;rBlWBuzzFnhQdSrsi1agdpz$ zbupb}``@@}1ExlXHl(4&C*8gZ#=p?dl)t0sHPC8*)43^jLckrHBoOfpY(ADOGI3vk zV_2Xdmh<-ZbkarPMu@Ogkh&jFa$^MSti0Hn<_NjHD%alZIVL78&S56fdwU%owsdaN zhqQ9batCe6W5#wYFZ>4n^Ts>&=k1_=&{^{SfAtwTDN4_;>h&hMGte}*60f!geqH&+ zv|aXhO1fU4!qf2GEk{F98cSZjL*81BQs#fj%IUzhWsV%^-CveaqJ3Kg8uity%JWqA zbsk3`mp|R&mG?-1ZUtmVv?h0EOD-n>z&C{5^?Z>Zh00|9shfdHybjdEbe|W$RBrnb z6D#D?T^+GZtR40Cmw6T7GrM?L)mls-R9^nQzgda` zyw!i_!VBvy_)50bTq`ejRkvRL2-@>XReI(&Jhn< zi6i!m@&pA3LlR3k36TVr`wF z0nI$tWq9{>uuv0uV07*fFBkZwxUPxAzSu*v2~`w{IGw#R?OjnRrMo|qw^MB9PE@!* zx)Z{G>VlIn(%S~Ud#?`>SIRRj69A&=+}f`toxa3IV^{JwmYQXp(n$}$Ixd$_{>o3R zbI(&L>dWHo6cpWgg0MTY6*iv#RR^N2Ku^ou+X?ZAWAzO2%gypw-e`P@%@0E4jc=Lr!iHbUYPS^p=B4dz&KMkyV@LWO2!aTP&omFX( zY>-wT4Sg6>VaCXD6-Lf*&j&rD&o9DOH<;d_N3!uY97Z#7YrFZ}^&7 z!YmDBY%-MluBI?%Hd{AtHEog94d%?Jus6UYjPqi4iA3JewiSscX-yx4AwA;Ijej3k zY9b`s@i2~fWTHmLKEc8cmgE?dT=2WJk`i?OhAr&F728E3`z z#2-E^GXiAeBCL?F@d$$P(bya&NzWxf=-*}aEqHr*Ea+5F^pF;!5yko`mT4d$kpO9# ze=OmLDdZFXUO|8aJKKQ-NFiTNAR=$@=KXSGY~sFMh3f9*s&cm_$EEhsjK%UtN10j2 zSOO`wijjL>hRDfu7AX^mNIpyitcqsi6-rhgYREG>GS@b~$h#JA4COcS|3e0!$4iO` zdf%8Q|GZBC9+MU=ej?dgWePCO{d248qy$fcZOMM@&Mo(Q7UKCs(mTHTwO!VF@b(8j z3+H!ct2=HkoR2l=3)I~y$D~m=Pl)AZALsm7SF`;DV^ebGSIu94tLP?2ukj{+-PXtTbsYL~=wV-Zr~)i7$O`PCzH*Z-craZ`U~P zYBGBCsk!FKvLECU{??my%qJqab@>*{D0bcra2+0|#3KjA)(yqS?leIoQ%s~Ut>WW(;)rZC zD0NG;QmJ|Q)fE8;{_~>0%x%>R-2CkxF8m5s8$O#_AeL+lEwK zNG&LeRCZU>uF+`W?f!+dxo0`+GMqibIQ9Pe016-8>kdB8u)IBJmKTQV1Grh-ivamX_iZiVBJsRXaXex!=b$ZZ9`n$||&fsK*>QqmtHs!(&wH8Ok>O$K>=M+4iO> zLI|xsh|fq3q{r6Lad$3F1(zGx<&YH^^I&y#%(tYhYIQ`2^dX&nPp`OQJX4A2@u#;6DX;51R4}^GVi3ty(EHe+ZvRm zBKyH(RGeMp|j0)v``;HN@*NAS;M0bqqZFg<}G?5x8ga#6U)Uwgk yz-a4Wv@}(aniwR~+TUUBe+ajH-3T5b|4-0B{X=LCsq^3x08^}mQQ29i$iD!ip($Yi diff --git a/data/icons/22x22/deezer.png b/data/icons/22x22/deezer.png deleted file mode 100644 index 2fd13abc3f7a83e6b96a11f8edc6b2b7468c914d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1115 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 XGCD9Y#d*DE00000NkvXXu0mjfQb&ST diff --git a/data/icons/48x48/deezer.png b/data/icons/48x48/deezer.png deleted file mode 100644 index ffda072f3e45c8cc7811b4b3f2caafef24c4b4af..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2187 zcmV;62z2*}P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x010qNS#tmY4#WTe4#WYKD-Ig~000McNliru;tB&7FA_JA zTebiI2YN|FK~!ko?U~Py97h$$Kd-ubdS=$MUVDGoaV#9!Kw^R|B!odRRwM|y!RC}3 zKKQ_$zk>r32Tlgb*MLMU1d)P7WN_q=81Ul6VK@GfP0ZTf^?29&W2Sq$s~(4$9Zyg9 z?0S

w}*(8clUq)%(_a-}kMmfhYFFK9>o=$&)8B#sHwIsOon6RqU(?BGUEq%$YOz z>kj~oF?*bIlfb_1c6I=OIT1Ols(04gx&S=SD@RfEizG>Y>YUqdUkl9`Ll6YtFPF~FWf88Zf|H85Vb-C1a?Ks&WF2p=B+s%__gdS3fd_oV%nens63VcoW(8O1)Js~U|ex=c_aWFN?)o1g4w7>h@h*>w0`$%9MXML z0fQ2{w5wN-41fYv6Y{;<5h`AodDgTs@2p&=;o^ZX#1AO%KLkN38-b(+H?Kk3db9vQ zshE%-jC_aDV5GYx5fSRi5>sn4G+abswN{1d5WMtio&)#m@Y=87e*7hqc+09H;Q8%O zF96RUIM_e{gSK-U`y@1M#Gmi{ow5mgMiC+x)36a}@z?@r)Sz|#PiU!<=_{=!rKs`n z955n`1OcY6-2qEUf*YWg0>qkdsrgZkunr}bgrMpZR86jL(r_eEjCXYk?VDL}ki-uc z0R={afS(^c$Y`n5vkp*iwfN=r>(pDV!bm8}h+pH>o=JB3HMG-P5Ej!Fesk+}MqUUx z`0`jcL!g}{H{M*}#s!N%c?xQ~`pW3y)`ZLh#=?+^J^&&@v7Gco4`G)-%vi9qQ%>nb zLogVEAwzYdlmie{Juo^z$Yg6mW>uMQHnG;?x{kFYU^hAJSasA}i&&Rtz%QjMSRAd@ zl-1i&4nS)y1?RRbWCAcbISFt8I0qaMpkysZHRFU6q2+mO1VI%1)I<7SQi0UBINzau z-Wf?fupY7TandJb+7Yt$%(K9Y!0g$xXNx&d^*mmjn4s!q!`85tbM-oF)(+qRRJ@R5 zBj2QA`h?8I{A2ljDu+Bqzdn@NHH*N#={4%Jj`H*0g>oeufv5>r-i5e9e;q7Lp&`%X zCwuoY5r&x*nvY{HE-kS(@X*s1vX@4VwS}yQ#Pwu}|Ezq(=yM^@zOpx$LNkB3P5o91 z$9~x1Ko5ZXb+~pJqDHQc4;Hcx+ohoGbDM1Pdka8&o_rVDDYTlV?X@mc zqqdN}F@K)2^a+{9G+cyzKPHY^Sv3<_z8XO+wBCLLEtRvwwjzS4QMi6N00K0v<*&DI z=e-m`!;osIR0Hkzfkq-UqL}mbe}n7mYXnpFY0M@pe0r~EzuDFg-<2?FKyvXNoJwC; z#Siu2l{j>cqbnEd=aL`r!oem#I=q@A;$J@8!COf>_8Vw?Py%v}i4#LX<43LS!pMF1r^}6%>P@=w?+&I$awgSktp7 z5g`^q1=eD?ecxoV#E7sKwV%g9ignqE(m+uGjRd#kwS}QOwqoM8gC$wRVb^OuicpdW z!R4GEm_o5JyjQEWZvocxP8E#77{j^gkhiDT;Xa66N{9_CHx4Woa=;Ht=#?7wN+~y| zAtdl3z81bq|Gk#K!!3pKrJ=r@{5tzQCT%9wFuhsC< z>}pwGS*|4Lz7+FRoJ%2d%frqMu`K&YP1Y@6QmV{hNH+(d9RSa)uI4z<^nLD>t30>2 z#Vg-h#`UC$G0N*x!(9KQ!sxBAP2wI)LdV)jO7e6A_wD&Si7$?m-W=o8<}*aDN-qb> zromXaKp@HH9LQL2+3G7{8*9-zfwk}VfU$Fo&N(vB#X=G%z)Y!xXKjW9*7Hb>@WKB) zqA1Lj(Q9=d>p5=iUdHrA+xcKUaG!)E&EXu0St#R4!X9s$1dra`T;N$eS`SZRYYYoJ zb`%PkGs4Zq67$O@w{9)xgAZ#HHk%^QP7~k5+^Z1W4{~!F>tjsH-pYKjkV)~4`CuVi zUM?20UAsAPIO65Qt2rHb^S?WI>(Vg$Z|oQ>WJgwz4TNmEIY7w#FIdRNi-fH7M997% zArs-^x{zg((ICd~x0#R&Gx>f=N60?@$|Bk)WYQ~S9~N*x%LHD;SHt%P2-#Ndg^Yor zAqH9EO0>wUsnEHH8ZHg!DFiMgARP3VomIAEKCrH+u6Mh-(HTQzljj>=;%?^BrZ1@K zgY%pZu&}Uzwf5fF*w{aVAULqKHTZHZNs@ovy?ghb*Ez3u;a<3Kfm5eWJwHA^{;Kc$ zd$!xrB1_Zs&cedNYo|}2{^0oW;|~BBW9a#7H3WjsHpL^i1c(cKeqvATv)cawk<-h1 z61_JJ0000bbVXQnWMOn=I%9HWVRU5xGB7bXEio`HFf&v#F*-CiIx{jWFfuwYFj8?{ z=>Px#C3HntbYx+4WjbwdWNBu305UK!I4v`epzC diff --git a/data/icons/64x64/deezer.png b/data/icons/64x64/deezer.png deleted file mode 100644 index 06b9d92bfc49723a5c951cdc6490c31f00ffbdd0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2015 zcmV<52O#)~P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x010qNS#tmY4#WTe4#WYKD-Ig~000McNliru;tB&7FA_JA zTebiI2F^)DK~#9!?VC?*9CsDRKkxUOnf;e_*0xM7rBFzE0f|O%sDwm8>YaugN1|ME z?u{El;=rvO0MP?gZb+P(79`++1Vt)2A&W|IAQ3@TS!5h1c6PlxGr!;CFk{E-otYiS zj{l_lE@SWh_U-R|=e_s&&CG9MZgZR4r`crxKi98cKkfOPh~2t%Yg&N)!HY-;tN`I@ zPxhEB0oGMDIVeJKaHNR5?7jcKs(vjOKu>%2M>GKv5&6&<^8;1==l(bM1P~FdwT;ne z^uzIZ{2k{Us^>lSiP)|*8jY~oY<@RQ)A5u5aU3HeD?2+oucm2w+H*d3bI#$te-&6! z)sLqH7!HR3)_ZSHd$P~U(loV$!C)f3JpsJ;yMLaqyq{I3G66c>7w4n^=hBRUb{9-Z z(s~EOZ8#A0^C5t07W}tvK({xY2_mrl5Z-?$J5FZ8b07d@XTIBmi_66V?2f5&aoRB) z-Q+3X%j|q-fOq&u8(FQFlz{Q>Yzd$*cbh?<=-pFWBlmf$T_~1PlY@ZDZ7!18&f#vYx)OE&gnwrD(RpLnt;MDUVeuVeFqy)BcHS0f%@dPL^OT(Hy8$sAg zAM=xc{j%JtfQu9nsjpw&b15v$?9qdSzyAE+jM5I~jT_Ki zEGdD_qg@La5tgHf&omlSXL&a(%q-AO1Zn66U6x8Hp!pav&|C^>Ek~tme6gV3Al)8Z zTq+r-Gp_~eH^D;$dTds*!kh6z&G%{u;8j^4jqiw%nnhovCO1^H;T6jDq7-4ur5T^ zFcdqR0#8QV2fhj1zkT~QFe3x@x^@KxQs-*lL#s;xBM`M(NXep5J){E%JZlO>5pQ0; zTup&{_t=QzW2eAZZ+`9!Pyl9AfciG4z+r7pfipmXC{2-)AXS7o2+&$<_lyETY$_>` zMjkba3zF;u#b1m~GYae&i!V9T2BqbXKmh;;l^9gO2rRT6revU3*xIppg;X7)#hIP? z%bi%Q_+o`L`+$t4v=0fAMOaY@l>$WG^UCHX4Og5-V{2JkTBhBBn_qvxV#gI`;^x5e z_Mey7ei(A;UYl?%*?qkI;}nvH^2y6XlEpFl`hTF46e~3d;SZOHx4Qge_zEM}oN7_3 z(6l4I)Lo+?W6G=&g90?1qvgu$NXRl^vEx|oxYC|w@)8KgLO3>s0)Wqs0(26z8&fn; z@}nY*T$7P+O`T-jI|_n6$*co*tj`6CKye!IIE{ki^FcRTTyN#9z zAjV*gkPP9i-?b5n$0-J91Rpx?zPOS5o1U^5Llib)U`c+xj4$zLFoa?WWz6N^FL+bv z4r3t1!qW<1M2MQrN(!VRh>%}16l2S)Foz~2XFg#zt41?Ci(Q(#VkIR)kv zm{Xv}YiG&k6sS^XEW({ut9+y3J_`K!_pO`)(^F9lspJ&6w1IjpoUj;qLV@4Rpa8oR zxPvprP0kw0DNt_JcZ7_=T)0sA;6d^%80m$|?rtT!+Z*;8mE*EU*+R>AtH+tlU#}w- z!=&`tLX`j$C&!q2ZNk-YOVT3cKD)BY_pdw&H%K>kaSsMG1Zw6e1DqP6z9_|1@WE?Hk+|9CJM+y`Q7 zEiPZ&O=Li}IZ1b4ufB>nU#4V_ICNi(bDP`T=Ena4WdxRPx#C3HntbYx+4Wjbwd xWNBu305UK!I4v(e)op(>&S9PntTlJl) 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%0A;v zfXkcLK&PURPUvKnMyv1SIU!RVt$3aLhO(bF*0-n8{oRljH#6|W5h(esn@rm8s-?DO zuPbuwyn~IUFXetsRvU8|$PQn(<0=@nu+!<4#(*_12%9X?=>PoAn7$nhGp|V{)ZC9hmFgVm#F<`LxG7|~ zFFP)LK||x(b!|V^>Xbo3LSk&@9%jMo2tK>LS5M{r)^9zSD^jRRym58SQ$Kb!!fwk$ zl_8Hk4PWvu+6qvQD!Y=2wOVAoK1e@<BTXW#??jreH&$~*f zpr+8%D8Kn6d2;5{>p=A27N$dh!;Uj8V(>1M8VqkUEW?oa!$%(<5G6J`k(=i*gl4T& z?ss+8{e;j0#}~!oMmC0ke5-DU02A&)0U&qqOvSyN0o+i|KgoBREcW(ZS;4Te%DBgE z639_5V|H*x00HJJheUL88Z+xAv% zSX`9&Wwut$`$ocIlP3VpGjp2N4|GKR*=OQtmQ*XRsHm_N^p}rTQV1Ro@&SJzE@n3F z6cp38E#@`v;8Z2-CVii4`Cw7W!UO-#ih=8Zr?0~7OaXVfh0N70sYF--t&wx_`aa_n z#uhBzJ_>TpWKjN*&@mrkp*QbYBiWzF?ApEO9X!3gM;{jE4BwQAlr*NfnLRF-d{NdC z7PC`RQ}gq?MPAQC!H5$-DQ(gsD0=nzBZ9Tmvsge7f2O{wclGw88I7(hUFkv&hs&J^ zF5g}`)zi2d4H;Jru=hYpb@Y~~qQk2T2=Z4_S9mD7m3$d(GyylT6L&47wrD)K3#F@+lElml%@Gusj`z$Me)wrV9^-d4 z&1{Lr!ouF8OR9kqZ_I$|i=Fb|^I2n8MAMIo3bkjp4SU^%FAMZv55`g#lB$U{1fe%; z7SpoEwf~5HucA^uUk9WIVojc`NQd`0zP>qZKl;#RXlfcT%#w=zMUDsZZticbjq#87 z-e_st9{nZ{Wjq2va;{pZWldu{7-al5^MzrBda4Ez?Vts5iU-imXykc7vhPYSpS0wm zXX%nVVpcjzj>`D0j@qo@Y)8|&Z(``AKGle(VuyY*jhpS|Mf|c~@yLGoT0wKOD1ChU z>9ZO)1y@&B!Sorqi1i7hwMxtT5BxwcM>KdM(1l67;2k3X(S+u7qZ4_`?OUONi~afJ zKbI=;^qn4CzjvogQ5*CrcA7a-{sXbpG$JlylQWx8J0@p;&gThJ7;L!PKi;{p|I2RV zv7H*Sm3kKQ3S-Jh)mE>(YR$g3NbkW)Syf5RPH(7WNSHI<<@@MyzEvMbs6A&KkGNc7 z4?C94ew^fCcWr}tE(QnN!XfPzJIr6}&IrlNAmO$1aC@}U=IwmQ?E4$a@!!~EGn@I^ zw(U2C8j;YgTF!se?YG`HfatF)1Ug9o+Sm*Fy=BRS*yx^pTtJ8uFr^L;Pft=t;x#`R z1%36;Hj$jSe+8c%&CeyqDRml!4d;X#xW>~r2nP6=h{aWP3Mw^7rwKdHT_bakCN4Bs z^;yB=mF_Jdse@er|G>rJQhV^@!}hj)>D08gZ09D8nh7`N(%IQ8?n|XUvrj~0X*guH zCAeu_>j$O1LHn{Re(wIL7rUM9Bh;v-LTT78IN zNR@d42hl*iiAf(iOfvZ@bOnem5w<2SC?bSX&=t~=)Hp%$sAKa$=xkF9K#Ih-pD^TL zb?<4}vp+ivP9OLQ;iGmSMmnm|>HQYi$O{a-?Z` ziw;UL!%(qF%*EW#nflTQfRH;AjI3HREXFT#dExS#)h1?d-jFMh$^6tw8wv>sFFrg8b;z+4o3y0RV6Ng;J`r-eoM>;*Jz&c%7t;T8q~b6>Hpv#$Xl-*0ESTBm(FE?R}Yx)CN@Z5t$V17K&9SQr!G14$sGn1Cxy{x~uXMq6T4 zs^?gYj2g0);MJ#_O%2v+%!}Q`42&8Tcqg1KP2(4Z^dk(k5YoMdF0=mMOXrfa6AkA0 z`A0@>j<-a79eEojrM#-!uL@rI587ztU^unb(qRS?M_`2T-Q$ei|4=JkqR17C_-88)$Bw8Wpza{`LF{Cmbjovjqr^|^VI;5E!y1a~-gS@VsW9zzazfAFwtVeB z`)D?Zg<)GikA}0$a2iPWL@3UfKXannXu8)^46~)=)Nv2Rj<`;)VhHTsXSDb=7_Er# z=A^eA5yi}G9%2{Ry6<= zQeVl?2F}d~SkKxd=wA_$pR>8rzqNatwj_b29Q1}Pwa^uK?XfhrwiofKREE~b zPoiH9(8!43tIoEyeIz*6N#vJ?|K;?R$X-V7+5tk8f*ri z)HugJ=QaB~HRTZe;FfA`gCD06y>vSM z!Dl5*w02yD{Uzb+1OPuUmZv*ImjVywp~6nDU!5FHi3zIxpt_<`>mGPQYA8ssJ%CxP z=*?r=8T;P&vk-0N`;VTV_W#yqR4@byC3;%E6Uh|T^c>HThTU!Pnb#|dAzSPG#ZXoC ztICj!%*;+jkKipG0p=I7p8|(Nt12tGs(69Kd!A&~>_`Y{sj0`~J3~W5y&^B4Uhh8T z)Oa40qVCc?HZUsqQdik_CB*N)5#rNUr|A* zRIGjCk+zhAoJ~3s9dIyhbf4;`4z2ksQCCHZ4{Qg+uSXS`Pqz4i#gm8AN_MXr7fs7! z%$}uZiX^u~x%>@nt0O(b49VHbplG&|XxR$Pe#pNvOnk3MHIX&W^@sB^3kLvOiPvKX zTVB>c)Grwi;#&sDtWCDglzkUw`3&1s%SU{$XI0?)|Ka1M? z>%b;mW^6;i>H5*yMp}&A!f^d z391n8!IwD#1|{~i8NOGiB{z01vP1>;qiwKn8!`{VuVZS%%3}+lnYub}5A z2}tj~!wtUYotgKY^Ua(&ckcP_A9um;Nm*;Jwx4J3wbx!dm!p?I0JjxnpUVQUumAuo z%pc%#93Tb2{tMt>-r-!sTyd`9Ub_NZJltP^hj;z@jqBI(Zr;Slzj>4Jk8s5V3kL@W z_Zsd^JiMDk1UCtYh;LzlnD`grf4mU>_UVsp|MKbb3xMPX7CFva94rz5HVGCE3D#vB zfDWT1HVzg5^Zz^C#K*gS0~ZVD8a4)RaT{X`E)F))ZCs3Qc-JvRSXatpNJwtpA-zjR zN%cTT+a)fy3Pn!wSU~WpmX)hpd}Z~(;5-9JCw55P#4<3ahLMMl|CxnD|Idd^yryQ( zuM;X>IK6!r-Tp|!G2l(i3`Pzu#$y<-T>0&Hz`Krn10xFSCI+oeg7GFsC4w8ocz9R( z{-y>AF5Vq-QnJVDFElixKg9H3zf18Tt9)RFl7WYp@2QEIgJW!F_UxR%EAxuVuQ!~W zUkAM5mtuTo`tTABAjELNCcz;ANCHZaewX?GUjLs5{wF=4AD`a!&Y^DkTBHE5jZY`9 z%bWKoq4~vN_oA5la(uD@RdN;+u=yv>=xAMC`gUq={w07MtK7jsd`?(dbavLO z!!a3lri0hs-gX#1&#!t^MidqnvPp%o&e(p*{ro__FsYZermZDCe_1Op<_q>z`HA_b3G6`)`;s&(RO_uz6 zTE6kX#4O;oO&bFPRrg*E%}YQS;J;}<4Fv`16mRU1?IQa*EmG*BigK>@( zim>f+TW$EN?J8rD_8g>Ic=J{9&k-$4^`lm4sSbhtsWGV-sT+j%43}8ZaTn?Z$+qlVE(=ddt6iUO81H+rF^h^$5i8Yv$-roAEq>( zx&=!n8pSc}`?k%p8d_oQg=Cp*thPljs-Y&pxIvy`sy&Isw;{piK_z=uu9MiyHlBI@N1Xy&m=fnZY4aWoz=R=X`^j7H?n=UmT5G0^l9Z zzQ;w2edCa2fjYB*mk5UYZh?wy*^?{VgSZ7M>*;+B6-g+UYxfA_nL)V&>$vbyko!2* zhox^8dhNwcdNRhn*I@tLmH+wEPxo;A5$(5JrKP%1KQ zxxf^CFCDk?ahwcDYN!B-p#6EHWYq~GG*PVc*^ul@n7ztDO_@D1?>Zu5{Ps1SDMcpp z*^+IJsDg>F&(INAT;{OyA$}g@@a+AG$}tZbE@&DRiY2j7xv`(NA_XeoFK6{H4WL*) z2-Rvm%Ycu~Qh-;#wWO22c*}9eyQs8_k#x;qGSIjZqcXf|pilwM zQ87@^sD-|P#5S$~+QLekhLnBk8L|7m{3e(s2qHej!G);rlkmo^b+og6R3?F+=%vSZ zYx*~W(0dj7(eo|#4<`m*UIQ4wT-qm^e#jq>0}P#iBLLjUL_Vsg(_h3^?qLEv!9}S$v^L`3RnY zmngQH&`?|xp3;c}Mbo&{wGDO4niT4TTba?VqUICq1-{{Y=Ol1KM)pP*>gB*G zo>nN&cx~cKL>c0mr2lUc|KSzKZWNb`p}4pP$x5AmU2_@zAvXcJ_~M*wHI16;bV0|x zBY3{nj;fu4P!~+G!@68Qh5|!n_-&QoU*n*wWes|0bs>;4?_)`JIYdTJ)OgmW;;Es}yH5eF#+Sp%}4awWs_SRPb*Yl-^DP&CmX&6#813sf2^QOTacs)uGY#Y4P0Z zc}~Hu?0x}B+puP?_2o;zEF*ZMiMYi}8GH+@tvgbG#*D?0@`FzlkW812gxI` zb<<2y(xc}MoO_>ZVV8j52w20G#PK?RDXT+^GF>RwPM{8dtd7kBGLyX`ZpDZ$J0Q6^ zwGq|d-@t3%_aMo+u2Wi?O*n76bap0E)Z#k(BzwuOY$v5zR3~q9OY%axX47b5HHN8vaM)MR~N^5>tvfx(TpKdWpbv>n= z#hd@Mju%jBG9H>)=z(jAza9&%OS+37H(ig{TLV@TZqG7sxf3>FgzoH#x{)l_yu(+J`0_kZ7lNy#qm{Ghv?3HoqM7y z+J)rSew;}Sjfs<VNO|%=Bv#l@WVBMUMHf80n-$l0w9!yPWE5aH2hw1e5rYL&hVohPmzIq zy$YKi7nWdaZKl*Ooo(6i56q%rp)0hz(1nU(2H;G9ssB^+?~5PwtoHLL<8#M=jls9yu^$^WG(PuA-Tm^Mb6?L_9Ur2^ym2|AqgCW2@gxt12|LCOII)}MY zlKcfXVSIEg62Ej&1>A58Z6$)v3U=Mz6AO@1M>46G(l%HM&)L9R+r-?9$`Gal*`m+K zaG(He=AEt@xS)Jo1xg;#R7J(h*{_WYc1~@#S4Gq@j4Th~00(mW2Fo2)4<-Tq`K`M+=% z_>bItsehz<3HUTn8iG}F=S%HYxm)%P>QkQrDtCvhI} zx$L<2uE=iZ$0gLzkp5U+EDm5ehHvDZFrTt}w;hsCD@#t^ZgY?Tw9GT3i+R7Q8D;wO z008Q@hP?{(jN_cry^n}|ufv$rRKR1g4;M00WWT{~6J?K5RW+Y}(@EffnZWGQ_>eZG zi!#_>llUtlK%jM1_Qv_oD#;nDFcws!P?b)#d179|#;8^`kLds-%YMz6IS4s_0_ESM z=>z9#-kbyg@U^A`-?~_g)YKxhXc1Dak3sFqA5EfDhYh8A$4sqf##t``<_QiqS-vkt zH5hVNJWqQxN5(&Jv!DHO9tOR=G8W5WB4s?phBzRXZa4y`_6wF8d){dyN8i5Xq3MgO z#K!MHfd5mw%=vAs-lN$?HS2Ea$?EJpwQLkfhHhWZatW`}Z2mKdrIVh93^z zTm;~PcY4ONK(3W?I-2Q%(y?RSc!i)_I$&PCH0894aTqkqtY((@ajy1xrPVc`KgJEz z*axEVTpSAp6$kfT-VOMrgw81gy+TaG zM`YFLb=Q~#=|Oe7HP3=%DI^5(;?b`1SD~OV_egXiyb&TyD-o!*2ht)Tn`18|;=~Vq zZC?g(=U0jzQdZJ*o{(baoBbHqp=ivrd#~M;18J)#TQ&~IJJ`a0s}PDP>Xp zzcP?;s>gyqHtRdfa!$IGczDT6rthW8LfW^N?{5F}A|^55fd?jm>44RNgaw?;(03q| zfim4HJ;lJTF-t{BM+4NSt!>$TYunqGFV1BR;q^mYYROr!HL2wUVL-KX@_xOz=Ym#T zcwOYcTzrczb5De3M$X5alW@;Rc6i|ua4S9aAkB*>{fJoYgqCP4yQLxl&r>2B;^Y^k zs^X>6oTTb~J;F0>YuHqYUdiCo1&u|z_9A?xFYrz?m2$J$<*(}QWnjV#ho|v9N zq0&*M78=u7EA&#CEqC39U;OBk8-&{0Z7nbNLEdkQ>P~5uMSh|)5>TXPitu-!>|yEl z&YZ|G5^+c6glJK@E>DHLQ7R8D5%O#6G%mIy>$cf`+&ud=<6EuQyKwC(CsNYDcrviv z%r-JU3P<2i(R>zd(V=G5XW*jP@fP(NfNn@oO&JUfl>*6|#^ZS?b(>gJSPgq|n*PY( zIfu8Mt<|)$P}F5w*8w+zvkOD4Es%w*_gcKTEbpz|Q!+_wYgE#x+%vjQr3L`T#GRyD zD0=ILcik(@Da^llcPw`(K1NzwsU^XZp=4nFsl{xHzK*N9Y(!LJ#l$5*%g^=Eefukt-@tQ#>fSIcy$hUANZJ*=v+5Nd73|sKcPZ%@k3V!X;AgK;_dfU=Tt1=Y0A^4!FjVck*X=O4sh~h zS$lKQmAqhF>SB@a%r(VUam6{BrF_rl9fZSEyVWU#6QJ$Q;8_tfFt-T`7|xvt?8<7k|K_mZ{9IcawHwrz+(|wj8DAF+&S@uc9cps@ zf_?(X^zpiOpldMvVPu_q{Mh@zjPSVDx}ddYg>{aI4VV`e$XXqW72Y;J> z2fsr~Kdr7+m_wPft<<1IJ%$d)zao0Wvm9alo5|qAWf5NXI0jJh*?DoL^e|@TKk%fJ7=@(NV0n_gYG_ z5}@$Lm2KE_^@6o`M(J^t#EYq~+xcUE)Kx!p)z4Vz+i3fA>x$wxxC!c_eYt8a_+DQw z<>}I1b@10uGda*WZ98W5;PDXsYQrM3Dahzs1J`8d-eG*){4ZF<9%j&fD}4gknyo6$ z$G+lFvkYO^^&HvJ=;!0E=R3g4n%;;zs6bNM+(h$LQE)I>5DsiZqhWT#$o`@Cx<6+F zfC$Tr7D*Am3)1zv#f|^;mm=&w(ZuzJbspNB3&3mB0PH4V2QTZ!T3k)v7Ncvo{Q8>97t~G@HkyCy zx2wCiyzZ&@gPq0~eSP4$aa)FSw$CAS!@ws)B^_me`^Ikk$;orJqv#x(h4G2HQ;9V3 z+&Tih>6+JjAGEg_6$E=~>m3e!M7)&k$%#Hgah4yJ&dSExrPw*nt(`a@B>$Ch1k1F* z)O_a{MxF!FC1CwUHya!NyIA#uXho2dg>{Ix)QfttS$vTP_sV8jsohI5G2{^707D5S z6N`OSVxlZ>%_YFKw%%$Ct**H&7YYo?i1Jh`aAv!IWjt16$%sIleDOsmjJGTGVX`WF z;iG6?)*`2c2Q!%yjlPJQ?cK!!Bq0w1T2Pf2>{-GuBe18`OA`LL-n6$+F|L@et-FWx ztz4vKTSa_bV~gFoWqJ(4_hG8rdJvVWm}`DdUduLl8h#^@gPZp>x=)-r_Dp_r{1#8T z`?^5ZX`7xiaE-rxa@P+cwwJE4&G^|y2;nhTg$~;!0e>$n8=-LPIWO8B@K9}wJTIGL zU5rFob2Z7po|V8245tMTD*K+#OWqMZfQQ4l!dO-6|r>&w^oIhP>q@e{E*{WuO^RE&(u`e_&L{%*D$t~|5CXHUt@`r8B{2>qv|_co1S zT?1ZyXche4llLD18euFx4KByc;vai|FjNh($uuj7M1bRZP4ZZ!pk@2J1d;gN(i*DM zJax?d(AZ6^Hh}Z_9}-hoAGyrxO?RfO3%N`>u;lMwDS{hS@!*iHmKZoc(f%am2S7WB zlMAZt`lBFf1oe_^%za(;zxdoHfqj*%@p-WE9yh9Flua0j!g{#h zzprCxGNIN~Jy%((=n&L06pnC9JVdlCu8lT-6#Skiv5YUk6?dp!a%OJqQ111_E>q2C z;I54^Yu<=2PifuX5XBBcK2vE=*cg4MS<<9;en&%ru;!l*FWeB|vLf085;r`H2%G1p z-s}J!3b*bhu;mnCcTZ@)upWk}$uy@uLkwf{!-H)5gM;i+w06_hXOr*vtvHlKP?}U6 z*o?heZ9&SoC@o97ZyjyRnG6SBc?m!i_3ExPM~+?`))P57@%X31q~C;O zs_N((I0qD%E0oLZf<|ya!CAh-!R|Zn7Hmg6*G(TMgOlJ(iW}`j>6_*1J_yp&E~Zl5 zt;LNe{43L;uyo~68u4ka^`rDej$TyoYF7>#h3z~NcL-P|g*FamS>)Tw66Ag|fT12&k z72#Hfsq178oja7NF#FtPzBZ*^p&Y8__*I1S2*1YNrEXjAx&xu2_D?VW#i#6VCkwoc zZ}|=BExRE8gP-99k^pQL$1YpB_~S1zPS5?xOn&*@z>Vo(Rv+g_eo%Gxll3jYAF`@n zevw>Vmc24Yy;k8&OI~Y<6xq>l^k8#LM8|kN^g(1FUZ&i?z~?lVebZ`Zo~+ zAf;B~d^B0Eo~+SI+Yq(={A!WCj5Srn9cJQh%Ug zP_M`C4}Q${{Ga>#55p1am`qS(WXd|XWg_~u1E9KWZmr`qtLid$N25A z7rn-ZJo;oZyQyit^CPtTIS*l%kR4yS8DwM9%Wqz3VaB7^JbMQYgz8r z#8R4u#iEr>w$MyITcD7yMh%DkQT$!YDU!gi^B3AHW2)!Zb~5IfClh}1?=FaaM%chE z0z6cAqAShR%D#qoR>t%B6p~dcj=7h1vD-J zPsL!z@?*YI#u8@8vtld{wvE-dm7RPP`kbGfS)2GqKrRr~xtz%sGpX;+Zw0rj3>^B< z@G^&jW)@S&VP|MuAiUh_Xx3q%(}_Mz_8z56*2^oaa3m`V7K`IPeLr}Y?^!!&vv9{Q zpIRawB?6ii`(6bl+OTk?w6x$4`l->d2`SgpFru6hYN}nW^&ZtWkEB+A9JxVx7NRj` zv+Xx;+|`9w?mZp1^0bp(Umz&a}?WUrrMlkFP8rc%PJ%Ol*c% z-r4**3?VGlvXImdC-fEbt9qDp{lU5+e@A+HJUw|NrP)sJ>HBVo#6MoZ12{!vyc{@Q zZ)w8O{U2SA0BrCCikiYkdyw4f{15QiAHyI4`JtU;8=BwG3*ckVQualLwpyC>L+o^F9r-qnUC*h8!HcF3& zw|t)}LsSf#jVPDZaiWw8u-Z=kH6{6EETxBrU0!tOp*KswZ5GW?E*C%EN#k*-J8{t#zMuJup|2&K54bbBwxCPNG~H`eRUP9=dA0 z%J+JY>f>+A)hCfsQA*eBfg|1OsNw8{1`Ww6$< zcEvFxzDs&&lg7`|HyL#hin>PdecpP)K(S7$wA8)Z%ubGS7>%d=ff6%Sf~u=BK*p_F;ZRRPyt z&YRUzigGlZ4F|rBkmH9|OINBQW)dOS^V#jk>p}|8MAxk3yG4!DcCa*mEg7zcCQKH< zb3)K{1u5Rl>hP}R})g>vD4y2M4 zotJ1m25(&)mCjQ{yEhsAhQMtRD zwrchTmVE0kY$L9?F8*Q~SX>Jfc>k_k@nuM0&a16$?md|&2Xqex2}Ow-p&>R>N>xau zve3}b#H`Gfx^Zp6Frj4Fq{jAHS;u@zb*uq&fdxhz`8iaGr-a0;`s_DXR6qRiCNnuk zf!aDenh&L>wt`aR3z-*4M+$vXB2aUN9s z(CE3Y#>e&g1s%t`rH*jgb@MC5KUx`Me-hIt=#QWjMI&FU^3gu|-hwOdUPF;h&Fxm5 zaAomd81cQ_gmB`DeV-xB(yZoXE~AvMml0-*lPT8bbS+o2w>1#!2DiN^6y6v$w>KcQ zTCEBWmT~In2;~$)DM>GP{Pf}4d^pLbI4Ix&QHAYIl(?VJWd)ehuC)@iDXNU=$bih_ zJmCt#g;US2rwLT|LuA9d-C>TtAXPox2DEO+;odaa(>-bJ7O1;MA17rRlzqwQcj{+?0r_wiyx91zqOip7f)estyLs(mMJ? z8F<2(if)s&w@N%jCdM}XV-Lw%ouy1Fe{}NE9zx-T=_<7XUOyg*x7(-P9<@1o+;iP$ z+)Ut6dg z*N1Xat~CBTZP$47Ubz=FzvG%QrgjnwZf0vr7+fZI_tGWJFgBT+Fdi=*9)2FbVKPH~9?%_XW z`Tjg1U6K5KmhaAd^iJ!^j1e{%!t2iOqcYmmsPsf&99S}?$`sxd^w!c2?99w^zUiY2 zrY-fkOCSTlzR%OH4^wj`W-MP;G76HYd5-BRGV^yQO7inWjlsd4xb11wk6?sMqRKo` zxADJXW*pUtzbj_>bHeyfBPgDz*e6zzv^vW;4ldR_{Wr2nZ*48F&2ktry0gi*wwN?c z*eluNv?{Vx41=gGdcazG<_!UnGw?>&9?>)=nDAugDsUrWa9*) zL6n#s;`1+n0j+I!ePE*xDTM(#O<_Ux8l0n&C8kl!NGUg(Fqf4Giy$>Z917APLz}-n zQLaL=6yMjR+aF0Q+SG@TvdI?Eu4op+s}lO1V_>1%ZMiz@Ip84ns=QvW4m8Mpf8l02 zy5t7(2AnX6@5<@_0p$4gc=M}|F9E&HwRGCYPBU?rfZCnb-R{jZP}JGybeJ|}Y$}G9 zI%~=+a9r?xHAN)@u_1hh+)pn<>~WIa4~nRr)eBuM_X_mZZ!Gnn$((gDUjiJ0_HEJ| z+-R=L{uGy5eUCv35wOxNCQ7p9G)=LuR8L4(qJGftMs1w(JUtYuA!fbrksNp7>b0KH zFX1AmXDpvmbj1*u^h&gbl~`o@G+SmU`g}6#%&)RN{sIfS&)i+N};P=CaX@ z>#E{5Bow{$E^_^nV`dA?bu}m8i={xukgfkK?rMtLU7aRi6G5$y^Sd|r-xabQkXIJ= z*EkvRlJL92)p}{nePe*LL_wTrb1$N7NPy`_x0e9c{#dQaJ5yY$#>(^k4URFHQ;%rn zFvTFKnoM)$8suT`kU+BenZ8|m#fVQ}(wV>gcfgBcFIK{CZ(*u+-q_NZPbe`4d5p`KP&#*-#C^Fj|4WtsxVR}t8lv~e zhEh;*pft4XM|G64aNbUOo`sqS`D@B^_~wqg8&uRxc((?PHf(Y*tk%HwK?sVn>1GXt zTZcbRqs$1E5I!dLj(T5Fy|5~iGc_&S>(I)#ih0OQz11mg=QvV|nNAwF94j2YryJIE zof*fdLLL=Txy4>Qf|>jU74aJe{O?DYQ5Bbb^}>{^6~cx%3alf9AAacU?3+K=P&Var zXR&gNGNrZRqGcrJovawJsH)>Kag7`&ggKN9B!eqkEle5F9UBtN$)_px)`-@DM#T<3 z4W%CS3T+`23Gd^#9i-vjFCurOF`G44a8vSi1KA<{*JqM}HMICtyFpNR!%B^PFNBCE z6!9|BUWv3RNE|-~?c~KiL#co4av-TyIlgU&L@f6`6A($Qc4Oa(YAY=p_VVR8_AnG) zlI>J4thbEyedH7PH*^t(NU`i~wl)6r=`dd-Z52N2B+^4 zk=fdgeJp1Rr_>@_f__cKN#7%_MRlt<3s@}{-f!8?H#Bo6nPAFgibXv>%7~grF!-^&kcH+D47v#E98Sm4x#wguu z?K7$Nlu*x>7IR65d)}iVs+b~#nhv8Wm0*4ilLfgP%L7SK6rs$$p-OGG4JkUkY7wW$ zP|+nNqsdoOTwOW43PfhY&jdH;t2Q%fTw>fmGsXPdz9Gq0RNrq}7@61Pi?qYkwDIH) z22(AhcrWsRb3wyVxuY_keOn=so=k3j6^a|t0+rIbN4fU!*~tm#X^x_`B1p-=<6H&{ zGeU3UXVNZl(|x|SI=!BZlGSb-^l>b%&=9h0*9vSd^M9hqmMEe z(owpGXNL>(;fq-`+P*W#FCcVsv)-Xz%I0pn(<$-1>A_IL)1dk1MIQ7aJFM&czcYXE zi#O9hWIe)QBcC3c+Sk6i)+2PZm(P3PPN?j zt#p%7iwB=f>FE*MfCdEyo?s+PI419lWW@yY*T3CM@+ELmY&J|EfQ$-Nz005dSdABC z@YB3HRaxCYs>wBMOve>e_fF-Ma}Zy@pirThuK;yb3D#&`@X`D4V=U^6VYZU$16{Cd zLcVAH@J4Lv;7M5aGE_MKHW%jJh{|mKUS&uiDEYW(!l4IJcJ2P!fAqxf=F1!xwW)Xt04+1;0V2y&q~YU+OK_ZbNPs*U9y(+I9)Bg zghx$M`(E96irqQ(hXQ17L_vSi!CQ}OE48JpX#TaX`0)ghbma$+dZc-l)oHY!-`Pjk zgcm-Kv=uV-H}(9RLOyfn$xphIueNDlkgW@Y#0}PHyB6L@_i$PRtE+GASEHp3p7a2) zw(=qvDh3!a1wwp?^{lMd1_p(0^su#=Ah!<{!Zb-7Yyy#PxZuGmg5q1`cp|LT@*b(8 zHNIgmbz(AFag$hCUH2k3)-y)k%bHdJ*FP#CkA;78zEP}K#W+n6ATk6b|YcmJGy>0ciPhZ z-lv!24MLvIBF8T+{nT<#HBEguVSs7%iXnck?ydDusovU)rGA%Jer_1G=rIeY&;I&8 z&v)6UbP!@!R1Y-RxNHQNzoPo|ugUBNO{(vy9_Sp0Y;d$B`!zfWp7s8BCg=XUdz9E+ z$CO16Si?Arigz6Fij>!Z{Q0y@`*SolUFND}40Mf<%D2+ieDLQNU*oVLPq6frDi4<8 zICA?1ZZd~!&AU3_7FI*zsZTgmm`YHJBJO==B6P%fR|)d_x36Mr5#M>y63+QWY`NZ1 z?A4Ee?-b<||$AA>6+qol5Ov#=|}4=ER~_m1NwIb2J2SX0LJJVb^XSP)-I-% z-V_vK$KNUn)$4}pXz^0@$q+F4WJ5?{;2k3z?xqV9;Yar~kh`163Q+<~mD12VwHlPgnx!L_Q%_#<*I z^O^_J-{W4jTF-urwvNve3G{bA9{}gaYHBo%gqIMU(F{ABok}aR{tRnxo-I^h;7*m0 z{P4hPVZnLAR+~VSMlTl#Q>4G!yRIPtwu|sPbUqn&Yq#@j+JfS)dJ2y zfJWGGWec0v`J5G(et$2dMQH}qA7gwzDN&uatv|g<^VD-H(#deg62Bxo`4X_PykE2# zI&}$fM-vS1IVEq#2!WhmVNSWYsTNy$YaO0;d^f7iP95=1YAwuWzcsk|_2rOxqMkS# zb5bvz@`|;Q12C@aKyQ1TNV?;zg#9u#R2xrSANts6d#T~4VozE%*$~NF5UGUtvb$rt z`>R#2oZ>h#P!5Pp+DmsbG4U-2sSIZK>k3rgma-^MJTU*Tq>a*D5f6OlxV-W(e)!SKI@@-~mg60R7uZ`3VVXL84I2lNMmpir*Y4SslT;h*2KS8 z=|_WcNh57+_fQV$Tu8)WuWBKBbbn%L_1Oosfz1H9NOo##2a+n`OD#E~!R1y7S}WQ7 zQ;)&MHYsupICX4DrrTyhvfYb+Qeg3dxZ<6QWbuj$lht>fx1@VAr@e+(mvYks@Cce1M`f5!x_~g5Qe!)6tK=Jv@Z>`9Ill{M!v( zYQKL^xw3f`8HHGt=T!7Z);yQcJT~E`d+}gn z1DRLDPSRsJJexdnQqMpB95@(?HLopdZ7>ha5R*ptc@D63jI^K>K3e*lxkTtb{J0EE zugzz0juV$x$Y$pK)|{hl|DIFd_xO{HX1jVI#kEf8lWgT8J-&?*C{JV6N=UVCyOU^m zs*H$MT$N)q7YJs*2E^>zA)7{Qm_6dv0V5wVyKVpbrDQncxv>7hVmZ~poz4i6B#juB z=bxHa36fDM1%7zgHPxljdK}@Tb7}SviOkC&+|U| zd;w%m!k*Q=u1a|p@Xjss+1J5J%iF!>T9t;4;hsp=Fol9T2Dd@Nd%wn>Ul$ipif#mz zTE8Q>4XrgH-O8)99MpCD_^sisN2{19g(!=Oup*@DEi>DE%FxulVH%lh4hzr)mmuWr|OYb#i3Y zvefcmfWAyeugL=gna^*)iJiPu&Z0zx-&`lF@*-zxl*sJIJDUP&8tGij2`9S9wNu_X z$mD44_4D$z8x31k6`0{?nYkuzSM5HWELvZ;NS>qY@7#>|3173GB>i$TimpfE;qd2M zR6_{Xm9_J`Vb+|#&X7guXSriNo*RR;Mg5K={hvpv`S^sfWfhrj4H;&P?NzVkCd7SJ z$awlxBG~8dI7jOl(B0a|TXjTR`ZceOX>*?3yfVhju|wl1hk3J&$@aUz+` zWe|Kog>b#AGv31|OY&CW6Mw+in{V!}Lxgz zVlrXLzfzfNu_@FocY##iEIK{;s_Qn$)pANUkb023-Ym8;jeT6)1uLrBi!9SJ+$Y#< zX?M;BmyDx&-6?u(mBr0rpIibeHuH>q4%l*NlwGINoxZd{9Kov`;*3RP2|mwGH_HuK z(2$zF-E`bt{TNkU@G|YoqQ(`9X`j$#S(ITdEOm=3c_P^psro#hBNu5^AQEY6cR#M_ zO_c7NApHHjpgs*LXhP_KMHBSXMW|c2t!CGOpce;_!J(6AI!D({XTv3$=ay{P5Te7X zY77jc6&DUY-j-h}N~= zehXQ48ywkKPMVH&d^Fn{mgAtLt<*iI1eyzh&xJtvU6e#)bU4Puh@$2~f>gA8$#^aS zG^)?iJ86aa114j>j9v&OI?uJ{8qa~3G)BQ)MdWMgc%9pAa2Vb1q;M7Gm$k391SBJw zXoa@&6OPcHcMqr?A-IjquglLIxR*QMDShrheTS8y{zjA-(ELIm{hfGVYNl!Ir-vGf zZt-gUWyMV}1_*Af#2dNd3Uup3GZiO3X-y~vKX>On<*J?nhEuR>?szA8GPNe zrn-n5(uEWae`z8QpA(vjwKh`vp{h`Vg5x2db;N?A zA}yVW_J<}?L>(jTb&afBM#z*?`|r*uX#`@B^}$m?w71)v{x4WYVLcwBDPk;ySm$$8 zINZ#VUkk)h_@?LH!7i4rJlW)kR4P~p!dtV4G~wiW?(4_}{0; z>&~N#PM%Je0IELvxDtDB5!CtTcpA)fcJOTe5@5GqOv4A~c!Hre7uvuc+jK3MLGtS@H?kKN#w`<{S9hNYXGk6q zEFSY;0@z`I(S=ZdjG$|;%&%RzR}voz!-@id8%w^&CVQa8K!h94Vc6nu<*rubK%LF) zf3ho6pRO8Wn`6+iSym^eOrE0CH=}{~8Oboq;93&{IWo>ZF2kw7thU!`qr+>h4;Z=R z-+}9_RU{td8EE5?oVl6`2AS~gktz8N;b8xJsw*kj>u}$4OqVoU4py}i-Cse(A%z>IwCS2k21uFhf7Q|B>i14`D z4lJVb&Us+WQdQI(jrVqZOhs#1V#$r`H?N#WEBVLcrd9-_8#+z3$py$!2mcqp4oYB| ztp)oa*5QbHx5sKsM*y-JWY{=pvJRNNzq57!Edan(|NNj(X-ktL?;I344`bvDl zyuo6I1dj2QjNgE$|M}1Xj14xwb@yiggMh-E%z%T+d}$UHM0gb{_@;vtuMDcET+;b6 zYPv*NVi;XeTo_zp-@%t9!2)mJ^Np zK5ND^>}z?xFk@%tS+A#IVwmQP1_?qGOjAVi6H6@e!&zHXEfu`g8}>^+Tam9??C(u^ zxFL)Lii>9RkAkUEYp|6r+_yvY9n_+XnsuyP^}+G#yD;{}f|sFreS7aa7Z>Ed)xVgN ziPx>pMRag56j&6>S44bZEnvW$W)C^2)Lc4CJX2tpgF?*>dScCl?967j?Lzf}GN(^h;aGe?Ky&BPto%2wLx-jfM!TNDp-^WpL3d#W2P+V|& z@<56Z4ox|7mjKQMAHbH+#A5uIPeEVRIKd@=LY#_!*j#ApMfN2i!AZj3U1GxtY11-f ziH5$r1carBC$I9P+Oeu%0+96?PsHfmgy(CFtt;0BS}y_ZeX`0fNh*S|d6;_RiN23? z#Eq#t<6PCc2Pr0gvy0g*>rIbLAY_wyy#@L<@7p!Bf~bhEZ9rJUR5%g7#ka(CW$j&~aW)aDMBgGG-EMQY;l>piT;Q=wa8hOE?N{O@gnSB&?_V9zM-Y z46fHR31X^R^GS0oz5c`7jil`qHg%4;hdH;Hx_=31pm^}>mKp(W`CjjIxTo46ddOmL znZ1G;?*0$r%&u4AcML67kiUe8_%i7-%fsxbLcG|0ZuEvD_`-R!#H5w0KxnZ9Wc$iM zI0rZpM!P0v73gKD5PB%68})%yB7Ao^q|JYTvX4hfWb5s z%FsvI**TjO?rj~K7c%K=RCo4q9HrWfm+1UZPLr1{cI-SWeT0@;+2QzUZEl7q5%-d8 zo`p7R<*AH#ZUoa`sTqGKM#;t)_^LsdVN-9EL`Vv#;t)0${i@*X=OvOX0(+>F!)r^tcRY+!a-v z13%?9(3DvFW0~!LGn2oz`da7N6y6@Wm!j;~Pv)%3k}D;3<~6*N>=iG@iTNl(Mteeh zQF$(BeSmsasBlB^c1$(wH;8g&N=me#qg6$cGhUF67H=p8AYlPuT? zN&bBQ`H~x&VgeuBa;fuXe68s>!WO$BPvyB7z74 zS5Q$p(nIq~krIeV?-!67LhlIq6bT>#(xiiQ2%QjmQ38aHB(%^&=)EI-;&uFHylc%j zGhg{JYfXN~@&zA@wKK@FrC+!#~i1GN-yie zK3k@zX$8NiGo^APhlO$xUp=6_SH%(}nrLu?uvOX5ML``o-gjVA*TTMq8L)Wtk3+A2 z=EeFGlKu}X-!ATjk>I-}?ml3&eD_qE8S^5E<7OeyPNYY`Ax6L=J3`}zq4GBGcp;=g zN2pH|6XML_PdT?Hk#tLBIU!bp3(_4!*V-8>tgoS}9*W9r6%eFlXl_2C3?JYT-#xBe zSMw#(OlTh)8RCYKZu#t6om3U!ic24o4o?mCbEG%cx%_yGY*vK-B_m3qgTm(ncG4|ib|edlzZ zP`RpV!Be&v8$N0p1JyIlQc!pLOy|?=Q#%K)n{=iNz3Bs7Wo)!kT}_tz`a;VNBzu#*7Xa9OQU{K++6%pF zj&Acu741f?rzts&T>=Ti9y&veiyF{Zfa`kFhc{xGKe?iHwqoz2mX=rkK|!7D;CPj^ zbsmm{#&Eb3mxnRx`dB-0@K&HjPpVBJjWHtQK@XEB5q@!V?oCTfAr`V%Wo;fFLVd%Y zSQS2>73z+s+Luoj)Y33Fl#??T6tkCm8m7TB^r(CPQ+Cw|b(Y8_yR{@JGpCQfPQI5$ z>UgIkPF}2IEhE!oaGH1iTt8AW^K{HM|$16Y;E6 zOLBZiNF~A9a7s2;WaQMBKL%x(c7HN3(R~b90i8%E%uM z4jA^-6)Y{yxnf1d3+C0g@knb#Zm9-CU<6+*-Ka_S=Hr-`KUc5+YLM7}#uLruE<9*| zVfUiKlwylY+W2Hf$dzeMD>f_@Xrt@7TA>4Fw8+%d{AJqfXWorsJ$c@G&wTj?sggm> zdv6Enpj={0u;++dh6uL%*V$EY*kO+mromg<`d!|O6*n)8*u*>*OVv|dlkU-e)$HUq zMQI^gbtJBTiKgY~FT&9i#Fbg=+fQ$<0R}aE!jUl_oo<^{U8FS`@0hSFl4}9m(93H* z+u3f*aRjMZJsR40-+6uaLxTGvi=+juc&2HV+;PXnTbBKIC;EBFgOI_$lyoiCq&!y4 zDrSMv1eW#=J9f5iY94Z1&GSy00_wUi$hWOb2W94x+cMv=Ieesk>0Y$>XBzcp+7E^$ zg{E*Gp?A~R{Q)gaigIdcsDPO|XMA_clNO9bOp|no*98EfDmSOA%G8*vl8QOKVwb2_ zMM6V|sJy;E@=7()q||3}lrnc{FioabEB`LGI=NMKQfvw6o=XT^r!z#C)u>8`#CjFs zkGnoH67}5{PrF6(7wopDK0(y#Wk{4gzLY6187OUra*l*Xiqnw2{^tsTJ7)mPM6DeW zuZ`b@bBt40aJCKbUTF%7=Vc|eP-MKU@P#zpRi`J420>;t!Q--EP?lrJwM|Bwzz3(i z%(%L}2zbmb4BcnguS~o@w-RAJVj@+S45|%w#lf9s9JEWXyO;9pBGoT9)d#%|35k{$ zcO-OHIB6$(Zf06Ut929sG$sCbt4Q{r#2ML;>9)(jJWEZehZ^W1Cj$I&sZdJDaZJci zI?lOSHkk%z9V(czxJ;S-u%c7(qprc<A)v6pn2s%PC-ysZke65<_z1Ps1SjktM9}XH^<=aSS zU+Z%9H{hBkruO?V+K)9f6iEphls7g7h1UvTDea0w!eHtvHnq-&hgCb>_SiE3Ih*tf zMp3w|fmi(GX=(Oq=kgK}C;Y?8uiuTZ{JRa+S+E_;MeyCE?zr5^MtdDH8v4$FK#ORo zA<$;}QGQC5dE%={_a58ALTlvkv=Aa_d@@NiE~)X9mG?mK{)9t>FCwqBth~IBG*|(& ztP{W`37{#A$9s#$$-J3VQl8Q5njeaHrN;&5-ik*z2{PbX(}=&Psdv;um!su0L$pgV zhhnA`<`S9nD6fpDg7s)U`GNrmy|F1c*d%a7MBSC3 zE}uA!lD^}znRRa^qRyGzyM567^N~Nf`qz1=s9_2?AS~NhI zM<((a1@H9iz$sMmI@kC=oAq4}ev3z z*BmwaVQ2u8`Bw|-`J3pYdaKfr)lSF~5ew;5EfrId$;PLO<+6fCZ3X&yKiD(JD*3k> z7_Q7fi&`CKBz2BsZ$hM`xL>BSzv^hk0Fc~ys3pJuH@pH>oy)xrr$r#eV?_7?X*Q;{p|E%mUEx1)TR#Aq`ZsieL zjBmB=LtjCAYr1LuzLXr&PIWAB3|gLU6NPsgE0Gp9B&^NOSkZ$JK5b<}FDatGrA$g2 z)%U}7@7K|+%{>j=o_462=-sG=#5<*4cP8O7!O1UmH}H`5hszv&BC=*%vW*llTHx1G zLfKFxY2nlD;4L$OehjDg-8Rk97R7f-Ng{`*2?c*qv(hdlv+M>X20Gn(&K+RcM(WP{ z@cf>kbq&2UThXAH6|8yKFwm+(cvwSv$fEDS5sOZ}D9UG{)E+NAv43MxHHJFNK=>>%juX_VvbrL)+pq#}^UlnWOnuCxgadmhVG0gssU4 z94RaRW0oX~-sW~+7!Ez#$o?&lhmWGxQ*X7{pWgM9L=xvSm~#R<{hofy{L+1nNom2i zn7b7 zP_8Rio;T!TGhFC*D<+zdlxa4W$&Z49Ix+G15stYP8qiGlp|h&C6J58yx(q#oX<(0spuYUw+z2=AGv&AS*i_78zG^ zm|wV4*V;GYIKuf!DXF6KtM$`3XxWy)GDRg^EF)Vw(z4#-apCawlL!qrDc?B+WVM}v z-tYT8{U<66p4y-B_uoX$Kbwsl3+LH>b$>1j@ar!>Kl8rp_=yk$!21)G*B=r;p$@Oe z{7nK1z@Ml(n`XpUJY%f(D?syG%-xNf=8pvx`OFz4&%z#+fH~ zFZLc=N&0D5f8JOI@15#8tUJlzug0D<4wl>DC{QG+bMFSnv$e*qvUX3fj3sa6^A&?r zXbcZeQaWT7p=W?B4Uu+^8X#)h;n7}mQYb7a6@Ns%D+E&;IGBI$d+JwkKrr0~VM*XG z0HDC{dH0a;ay|nTcVC`+c?Ou`OPv`y zE=wXaPvHFKC{08(?ZI@H%-CE?8AE<;+R%8$VjaWouOy8)#a5(6bQG(lW}Fd(7tkHD1H6ruE*oH)qTJYxG$VO8zk* zD2+++t)KEf1!?Fso4Z*-C&)I!cq8`-SCI81|&S zfbSfncI0Gddu%FVZD_aH9|XP2zTCma_AA4}|CzjXeVd_~#gF3*ebk?QMbAd0Pu-CJ z;u8S5yzzv>fVj6xvoal4%yqgCp;Hp3ZeTd<86360MoWJhc?LMYnT$FE4EX%&^66XD zzrQA;q2;^xR6Fxdvr)1dI}Tg*;%Qb3#fd)0s^{wXd(mYIWpMlb@p|pr`wWEXK8!P) zDA5Zx2lkNCZ>JX*t*bB!0gtZKFzUMv*sQs8mY`Lr-NXptZSA|QX8?^x>oWjc(mQLr z0X(N%A0>l^vF@lkHTJKM$PnOpFV~|E!e8`mOkz%G4IOqR_QJoKB1|M^PR;Rx$D*5O z0J$Ni6Sb3!QwH1Eo&%YZi>prSzr{*9Q!7cJVDw>c${vo;bV zE^tUOSA(+DcgfIyTu`yC4pq4vgu<^P+ywgl3ek_zOH|1NlvkBxB?5bJjORt%{Vd%X z8BN2Od?Rya7854_r&1vMk! z5|f0i=?6g+n8FbxaH^<(V|JAxX!w+r7v^>(`djS)0kxqu1(uUTFw#h7mv%4btL$3I zH|iOeJp~x`w}r-?0n}w|VQ&~eZ~N)w{?k7FT_gfJ?r1akLJDN7U}un9D4OGnITSGf zjdh=!w30vu6B{jz50PK`(3r3_nUQOC$7{8P;y_-b#Zn({2o^7N(giZjDDA5aTjR-c zXBt~}d_Qnqt#Vw?O^IbrLe;h+#~MgmNG&L+)M90h$J{t=%IY<28dQOBsivBJ$u_5? zlypk+)SSw)&AAC)$vpKJ2X95vPO0oqHk~}}euq~&x#*la%l;{Ly)obr-fH(?fG9P<1jx#;4pX_fmJ{#_iPW@s+aaob$pO@5&;#dsAVj{4ww6ZTpbS` zF(<=gwUK*01gHzP4{x0yRDwk-^J6H?Su+^iPusu?A$z250TtFJzO_@68Fga-40g;q zAo2tzXHt2pFUfYgK6@(riF*AE;D4C?L;UF*lkwxmHqUb>XMhcuCjQC^jo;*f^+w21 zW9$JnK&J7OdMWXaOgi7_8Gwg{2IjGK2DmeIY5xrH`9$Ld$Bz6FY&PS&MM(fDlfKFz z=2Si#%$rzunDOOFXVoOS+*p>y9r`HXcz}@Gu65G(_JIeR%^~yjj?j8dHB4F>?HOQH z<+kI&!wEe-|G0#Fc@&d$XIZ!4nh<{<>7Zu7lNjDMZzfXmn4?A`mhAcL^LEP$9#)OC z+f556`g8LO-0ZjTHLt78NoB#MwEqs@Ct00s7>}FRRkFul;Dg1DaJ7x(&6wX>3XZET zntj&!npZ5_eRB2%CA-dSYF+gbxYzIE6!;tCtK#aKM!#mWZyH2BL#AJVapam9S{6D8sA_3P3D z6OM;|wGeiASx;HL!%qEg7o-Lz@`QDmA1vJ&^v~MmrKVVAy}e2cp=u(Q-#%ydA!5*b zhV16rfM*QXz``hikC*KxDG z!=z}WjZlCrC^k6Zd-faH%>$vTg;53uxqV#pRq;nlJjF=Jk!ZlGRxbf`vohUMP@Q5Z zDTSqw!js>~r22UTvVRt;EzYw~KP>O>-G+c|!#?yc8s4ou(2fo0?ZsA~0T$xX5~2P`HU1x92(qk|?>po4^j;->|#go_Nn4~p%Vj8c(6bXOY6l91bp(n#g&Z*sXQ zxs>Xe*k4Ds4A^KDkar1%d@vNaHU?~;c430TX$(&33iMmtOFxtS{^yX~>Dh8`x#D@% zz8DH8k5H!TcA>q>FwWa$3wqAHohy2+Hvy+aeKp;>msw~R_S%YKMP#GrS>MMW<(l0zffItES51s(Q9R{}~SkT}O!QI^<3BlbxfnYN*xC|aNc#y%}-Ce&) z_Sxs2z4!Z`^PPLoegC@6^Q>8{RsE~#TD`ify1Lf=#Qg%`iL}HU2>=2D0Du7h1Kdvo zUIP&SUXb8tNDtsgqzA|keimdD6kr69MVLqlcKNsBq!n zL_Y<=Mn=IQz{P*AsHChC6E`%Bibu#En_af}l#+w%rGb%yWBiw#rDaZTW0UfVZx5ZE z1Kxiap#r`#q;73{eLn?w3b)>&#E{grlzHdMynCw=rLIp&<6DFiBVNVA$$L^y>C9oHK}9amtxTQ(y;! zf+2lquK<|#)}M{$n9O(m)dIIaBlP*i3L`2mQ}&NPLJ|7y`LW)}2qRiznwglITWHY)~?)o2c=b{7O^mLuq9a68`v zhL;~!e(jjyYwOcm^6anL!m+1Qv041wzx!lqB-f~s`?Aj2d~kqTk~PQk*m2GHfyTcU z;@^6Zm>+g5EQ-I*Me~cxPbAlec&km7T3=--kuiD{loQGI_Pr^`ldF3rp~d>eTAfDg z3Y>-R1lT@{=gH^|(&ArMJvSY#$=s?ex++$h1MRYz@(9jU)@Py9jIez#%>u26ckei< zkBc&#{@2~;-_!2DZkRKLk&K)T%}Ky8gKK-ojLcFs?1S0pR`jNJzj$>4*$?NaY;RQZo|21t{KcS@l@U713PqWHFxGhkvrv+ z_{UpyN)`p9)TTNFb;!SbKmNBHMj-ry5wE1dVn_hpffZ-FL1}KB)md4=+BJZCp7tZx`6F*(aAbK15!kHaHz` zN|{|lZMetx#-y7t=tGc*J_~>vKcYf6@+A0xDEO&?>0IT7hfV$1<`nggJUQud9n%e- z{!S87;S~2y%EtCrA?a765dy%#@!OaEfmGHr10TCRz7MRujxAuHmQ`n1i7=LfP$dNv zz2&F=_Xg3okvk_aCzmm|)j1U5UsVyde=wf*s#VFeo4|w(t#(QcT<7!>2Tz}=gc%2A zSyAwBD;&y3Kk@%Xfdrn$XgJ4b={n7NlpipE3xp1xMC|zK97qO8Kl=>>z~5LpJ?Nmc zZ!H3~I_o(q-fKjuhp4wRal{7l*{Rbk@l(Up{7bBn zHTFHBLzLa(UeBT1t!rXInYNP9*QI}x>7V}`LWrOt#Q1uSYBVrTMn(eahbdH5I%A;h zl@%5J>n}0R6?wx`p_YfTZAc8->PF60mD%>7R8vD=-lzz&+NC+&rxy*){kRzUbqB(e z+p1dQ@wM~Zk=BlHmM#; z$G+LaHe17~rff68hs%vmCQTDq0AI)^H$iLLMf<%CkG%mnYLc&Q7#SRuTo`<#+*Vcs zUp?SUA^Ogxng2Qrg;DWfiT!(D0>m;qJK!ec$G8mrLwa)Ih>OTG#+bVDvsnp~%aWTX z&l)G~ogBYo09_4EuMK25)Go^z&x}VU8Ht#6^ZJ#u`TEzgxlKudstP)fMF9Y6^3aRP zw?q3BZ1Z@zG?cIMe{7Y0*5%ky?JdE-v1W4_M*u*XQr}FsYWX#J_+a06=+dJ;kHDspvvJ zP|YxIqFJiTwxvrfEqK%O(lDE!vNg$Q9t3lwglN*3qxgJ3k7|52D)=!3W&X6E@P2DdlTo z2lWLTr;T@fxRv>5TuUV_Q|muk9BC~Sb}Tyd1F8nU_^-$hXHATRHX~VOy+$IgFU2CW zMrX2$z4>N!)Lmn;^!fw4eu;JhzjZusj4l`>GC4IV2!r&TMNZre{TML%8L*AgW<m{=cTQk3oK6{;-7~Zi~ zP5rdki8L$ZE&An7{Ep*PHGY}$Fc!eP`123*o{8kBAf@1S>0Z&`6Why?RM6_jPQLFk zpYuOk`H%pK$Dff>0sxN+&4qkA+B0zMF}IS#D{$?qURZFBFzlL%Cuj4W6daN^@AFgV zB=|jjQes+?VfbwaEPY63kKW?{#~Ih+S3{9`tED{7%8ntG_W-h8z?8}JKC&(0mL~N*X*1lgv&q6QlVxWvWplQ4kZver zeQ#*KHuduB(q8{&X2$g6qX?oo?#SEr@qSuDS3B90y$Q2;^MS)kw>_FB3*Pz)PqTd2 zHo+VTw}_l(2jKCWj@h&kA%SY5RbtKt8?x(=l2m;k{`DHr@gsVfq8^3590JOg0Q?eH zkZ~xK@)(y(geD~@aG7S#_GEKv5+>_jmi%omEBu$$hHx;t*e#JS-&)8=^c?5WH1D~9 zA`WqWwyGP74mot&K-GC1SF*0lY28Slt{l=?XXDe}zG=G%J=m-IbZDTyINaEbxhL$+1I( zFV;NiEKwK$kF>2tXIP5OUF7qMj6)M6=5@bT1+PKIVsue*Kn3aaZ34xy2M6nYDoCi4 z;B}2sFNWa;Ngc$}XMnA(ktO|!>Vi^9RLclqbx$O_h@TK1|~r)qP;d5Wy@Ij3^bs3GL->gtQ<{9NuO`kGHW z8I!I7IRhewv+87rY1j^ykeKI^jaKaI#$T5}e8c6;J$T_DkUh%~(|7x0d0WUkivx~n z^j?Ue(7uHR!DwB-4q=9#e3;aSSP+xPMF0B^SIw0%aE0U5_fQb>s(lyMhK6#Gs~Yk2 z+p{{dD2ccnvZaVAvTcr9H0D`!VL-iNU zQW)~2!h8KOv;dFbQk8;Sfk}$~*4z+-OY#DzQZZ@8zdAC3jJ8|x1Ml1B>Q(! z`E-r8QP=i^0{S>qnVE-C`Q#xoAIMaGp*;57Jt@<>2T+lm;q}&fEe&o;=7M{D538^6 zJ+Ysa;@SVCvXiB_3)8s=5TNcfgso%M<*a3GlpW)qxXud+huFU2%>w+<|9doNz*DG4 z>=mEm{`d(u96)JWev6Tg-(VA)3KPAzT4+^BQNz}fT^07idOItY(q7029OL7CtTXyB z8Sse1nr2juB}yY-YWfS5+5v}YNmN{}3=Dje-=@yTR|lE7q&=e}AN!*vjH+Hu;<3k| z8?`M=sDL=q@>6Vd2kYqLU88kfi#z&Q#3um6`FcXqC3m7Lx$8kSL_hzZG(QU}L$!{> zB3r%0Nn5Dw+K}_I^U}~%xFfZcvnJ5k^Q)LV=`c+ty^CzEHvSCXXM7C+ssT4?DKl%4 zb1i#p*^8RD_O2O)5+T?HMLw>j-=m*_sA|b7d$|S1SqK_`WVApL%>1cPQfD@bqgTF&cPO_Ko98+4G6qpaVK6V+t_=ewa$F5J{$s}2St+UQhfSnBZlTq*!`M)Ud zm0bfTHVc!u$H7oY=&4n#G=FwB)Lr=pk)n!S$s?v>$55)fs;p3vk*qh9iOZu1jmaxg=}TGqu+uESVL-*Y&xU&E|a`AEbHuYoHEi zzPdtY*ynE2FyEo%W0ZKiG&{%Totrw(0jj;d1kh~``x*w_Jonsc*W{)1X>}gf^y!w# zB#vpWSXO>-(MMBij}-ILl%kSP1+!*VxWL|IZ!K9_%F9{5dhMC)Gy!Rb4Bna0Y_kr5 z;dt8j6~7uvjz5UP)KRY$fQY-?8AeM~`nk;#I^bC4C{e|!s>%UT73V&Y9l*}TaMj2L z%VZRDwfFVoew5f5SYkZ{I-E2s?MlsA@a|7za%8%1D|;#WEHMj))f-Y(Qib>vTYn{uh3}#K6SFa4khYVk8 zykSQ>NcbLnb2KhC$norA`}E>``rQwj z7CfqYA)wBzOU9*JK$Jh?d9vJ=`lrlQx;dl=82$hN-qPgr^{K9bx{(`Y>IcsN$dPAH zvR~`4b@57_6{+dsbv5BISW)RxpICyN6!Ep7eue?1XO@>^OyoV4CD`MDqKAK6IU&gW z^*ogSv0Yz@JY zpc=~|gL>kRr#nmJ#0inOW`_ zExzkt{%}Gi z=CAuzpkuP>tvdp)=ZOYl-ZM!z!njU0cOJ&=h<^R!Fc5ss(LyemGo# zoe`Di4LwlWEq{SK4)A!U7p9!MPa{HuDmOndl*txil&```-|A;_4bm4zJc zLEmefdw@q;)m*(sxM`}X{pFWD_k9&lEY|&~BOE!soFKvPD2>_f`_9Oep6$o1zbR)`xVF+CzYtCtGb z<04NbKOfy)+ykD$ws@WTtY9_x3WDiwAR*MG^R7#j62-PmBfa8UkjHANby~UM)`zzt z-aYs1KCAJ5$YdkE}(0 zCyD^u{WRwlnq+h}7&y{W`O6Oa3qs9aM;C7BN;iDOM8Qy zaUU=KCc07KnK2L8DMYKlZ}F>(26TH@k)lSZ@E%YWwMJ=*Oq(MS#!kTGeyC3_=vGH< zK12%fnp^E3a7qHIs_MX0u=>!PK6fHRqEqDz8YB2#)Z18kuj8qY=)jzr*K-+-Is*xr zU?d#35N(lqe7^J_ES~@mzvb2N3&%7@ZD&3_pceyR+yk=RgoGo660GOR-vqe*_$e&1 zy{ny&Dx1hmu4G#@<}-i9wrNzo1m}?LV(1Y*K%fCWv?Pp>dy>7DA)_{GHAZCu}#4--VWhC zO3Qy?i2Ol6JShJ$Qw9-Q&sDpV03L1`+Kb?iim7;UQPB9V&6B|{{RSBH9EHlG*cnaL zTcLCEQvoRufE@4ow3L}SoT-t0t&FA!Y-oHm#4I<u_`nY{*x(^x6&eLHk+-Y1R=* z5(L$<1>q^zA6J>0Pvqe$$S*HWTkd9Kx%#Or00gst9h8T99ba(ah|vB7pyBmydl(WU zNBdFKf-Z;f3K8u-=Mstab1-6BY>O1Gu3er>7GxH;-ll)d~!_Cnc!0^0^qu%lKEQ$8cT?agd<)# z&kwMAprL0^))exg=r4i(dU)U-;h*MgUgi#lP3x_1?U5dh=^PC@lTWj@m(mRu6B}}4 z;o08<9vv+sTxuR-Eur)t3RfMk2;Ku)9sF;KE`kn`OfDk4FR?@p(%*?N3O~OQo%E&B z7k)i>L;Dn~XE50B3AcXm_+a$$Jpio}>8N6fq8%e?3HTdCA5%Ef5_kDl)7^ezWyR{b z@amM`v>K$kdgjiwgp+Cwbn>OJS97RZjf9{hrye&lQJBdW}~F6Zr?e&3Dk)?lT3e4r4PH;?bGGmn*xwi+axPh{TLW6iQW^6fq}&)L3hx$ElH zo9J?K*ocnB=BBJSF|R>X7t{A~o`rwcVhO6oY-HHP9oKeRVF1`-q&B&Pp_Ugjw!5@2 zA_6eut=S9xoAIFn7#*$?Ju_OrwGu~Z4y9UdOT}Dt;2g=nnOuDF{a;9mc)>8S2>vsi5zPf|#vdvWcs_&oWBL9B~ zhR|J{EmF>dbq%Fo4`|%!of>M_woLQTNFo#7t+FCUBq7y+KfeSFksU`zh?APtkro3nZ%B3olrlb=OmMg|HbgN=5_CHgf863|WgfK?(RQ~d?XkcI ztdu;HSiA?gy%0Pg{l~sRo`EIPPYBV1QO?vH?sXNZL(nEYZE#}fH4?(ufjm6erSC*P zV3epjEr~e-3({da{xSalYURM=RJw{G=ml}ER$-44{cv|N``e80Zuci6X=zJhYE>e8 zpB=)Cd5rxY7rI-yzJolNjjicF^pcjy(Qz{&V35pH+#JD}S{ugO5nSv_j=<2xsFz8P zK21LtA|I>zrVRi%UmH3h<+IC3qpKeCP?r@m;U~I$Q%Ui1`dG!g6T&JTkX3YBrOP(o zfh=0}%WU~=97RB7{{&V^Y_|%1P$g}t_lSq)>r;o!&OAqZ_)ZrUv3_+9)Wl+N-novr zGB)(!93rqHG|-S z0!X*w+n(C#DY>Nqn!$wK7gIvkCj2fwMnUpXPPPOhI) zl@=R^a|oNN9d7#iEpaS8_tuzYa;86vzsOk#>Td@6h7Ne$DDWC!$sd<4DEFh>z^)ki z_74RYKa#B*wB)B^T!K&3oaUE2+8zoFv;`<|ksPaU6slbio%dYDy*!7y7?b(+z$ML+&Z)Vstvu4-QzDD|XgW%;6-7Tgr-18EdJrUSD)* zJe*Tvp671hTTkkw*-gJ;;b{`+5m*}!5nq`Ld%nBa{AMBL#^-GCwX5S*s1^yYVZ68faN=jmJQG2`8 z*7s7N8Wn8PbbLM0@~ai=kQ`9)qnNG2iH1jvjc#rRBtu!C@?xFhy|#N4?X&;rr1i7Q z{n?55v!nj2TmHp~K#HknbKmSdm%P%$Rieeg=!n248x-#3!=dIUWB##5GyAPI&Dtd3 z(U>IAkhZ7*SyvQcd^bqv(b6$FV{)5X{4^aekNiELC;jAv6{0$cqo>a_LTkg(A9>mv z2+^=a&hiKR*`NL;h}|tNbo0bw2W)zAB>Si-;Lq`s?*(o?lhz!jWL3_pyKC}{y4*d+ zJ04=8Mg7DjQmm%sc~*(@tva^bD6A}s&mw6_15@HG=d|8C#&{(t*`E3ZyQ-AN{2~3tK_K^OTyQ( z>y=1|Loo3K#}$K9z?mgp?`8adwe#2E2`90T(w@RSK%r#;|4!$8)o=BNt>IYU4rX*$ zvRnv`kd}wjJ-~NwI@q!{zx1ab2}S#<(Kn_*{gMWLv=$O zr&IAKON2%?zbj^^8x4x`Bi3}`t*BJL?5;kAp=s}vs~6CROWp{t1S&>U`^PosliZ(P z09ZXf&gHsW>?uVou!&B<>;Kt;`Gd8Ovhb(C!B3&c^d@Z9YYSaPiY{~oA1V9=;U~=_ z-yI#NkOi|%o_!$=v^)3}`uTFc$gs#MmH7m8?!0G1^ddpXD2k^;;%d)7oy$`WJr|Q~ z&6_^GP)TO7Lu90Rw2L`Z zdKkH(!A3-X%*00Qh{KZQ0Z7tY{a7a@VCeHtHh(K93H1(07zIffuafjX+fFq=V0CM5 z4<1fU9VDOm(VbHz(eU90U1N|P)6MJ@n}$iVgV?&J+HcSj-+bp&J9W(-YaN#uaA@V) z%J1OBg_30QCh~P@qk5{{sxz)Xowkd+Ah+h`FHYj!ZvYo87k_AE^aUpzf0|yoLrp^g zn+yo-!xu2O-|b7!gI2v4t1m?Y*1mxzog%M$>LU3k1cqz2p!&QvO$rMhrIyt?%S}g+ zoXBd-lj19g`T)rJHF%~k$SFQv$3xliIxReUYpl32`oOvV7_17EGUn8b2ufU|9h4+E9aAgi~Pzul0$P}Wz8B7p0HyJUyGL=Lb*TY z!(Jan)OR=Nr@hHV#XC!S?aPeS!@W;)=xak0PpUkni*ZpYLO0!z)^S;x`qn|g!kK63 z$4!pyksaaM@LwSw*Lm7X>&3AP03TUL2Qrlzy&RP^LnBGe10ArU7Rf-$xGTgo_hc7? zzAYaL+PPM{ssYC>o{nI1vMU(w&AL_nbz|Cg=1LgrBa4}9hyt@svye^22NuKi7BYp^r`P87-PhizbY0Wp za|+W7d=kP_EJwcc2tDj)LvC?r7dXwYZ6Ywm(sfcggoSkMdBmFqUd^%4oP(Li3G`TI z>v)EpgmiP5bxVSTElpa<6#lXfxqTLvhL;|jjB8}1PXVF4sGJwM1-4gT?Ah#rn$BH(VB=Q|XZ=sH=0sz5%v3bpp4y?cN@8VF*j zxG5d8_PkwXjk+x+cB82x`x&8>m_#_*~)r&Cl6a8Wyv0dKb zVSC-iSm&(`Nxs8=!wC`dP^C?D^(@$CY;@eVTGNUr71a*<=VyYc7*!qCkX6&-817T4 zyK>tEhxGZ>1iZ-ql$_K})|_s;R|gzyXppkCnQX-UVdf@0-|XO8Tw@6vA9aOzY}F^D z+WG%^NzK$1Lb`;1j_JxAV=$v0&tR3@6Rt(ZVw*kbCN8cng zy2sj2zS>W3^A7j%Z_#Mw%}*7~*R6-Qvwm$X5sBKFzd>TU58MM})9wMpw7c|4m;q(y zcsD;T6~;fqZ}Xr$ymAa1+qehRJU*^Eq~zP&e|qJxk+|AUu^E34IMBTo_shu2t+>n| zE@ZS?qCWl!i2<|slr20hz6Vh2YEsNj+ynBj*viKnn41FiJ}NSqkpCni89{|Fe~*iF z1YEOHUbgKOT;^XfdAHw^)eI_RYez@Y{@W3@f4*K-V$FR#qf$>X=FO6cobUfrgg=}9 z$jz=SeC-FECu!%IMGx+OlE4dsHb%;fn|2nS^$b$Z0M+JGE9$A$NpzA6GA&S>cv2vC zL*MF3LW$))V9*hv;~7Ac)!+l-*i;+iH2F4;c~2hIz&?I1(e8;{&Tux%W*ElItY%2x z1pMhm!;q?3>pl3m1k>PGf zjz7K#aD(hUY3Ol#Jjf5-8&oaCByfY;4#k*mW(VpcIv33htbi8ExsljWk%d`erIBnG z0T+T%)-~VXS>A4Y@=Z5-bl0DH&_4ARy&;#Z)?OiQ_MD4!(sQ)E)qB%P@k9hQW;YQY z*qW=!Ngq#7YgUhS2x`arw&G>d^b&ktys-{kr`dNxSu zYAL}{8}_Ca$AVF=ut!c7Y>GV~WpBj3-_5b86MbH{BG8{vh2JNEt)dX+slg{BSnks} zHr5jYmIg8836-?5v!i{K!+-jl7O+k2ZBN74$*JCTZ?y-n^YDFKV*uqB8CyqJGaa`s z!WSKYwi3sO%dB3vOA6Jv-?=q>wj&kh!QH&QbMWM&xZRIO7>O7T-PiC26M4<=VeXWQoC|{)sWcQCMy7Cwu(6AujF%;u9IiN z(@<308}~{sYzesspl9Ka<6@vVnjw$f16;xc=rfqwrRO)JfBE% zU}xU^dEq+m_^iHX;`z!EMnk>bAbVlW?Uu&?@LYW^xA9W&gB{ECai68lJ-{>5DZ&SK zUTEj5@NBxb`sSGUzeTjZ5v~R$&Y-9Brx{d@*nQj(&_2C*8Yw<_57_+Lb3QH5H^OBE z4K+%=q@1T!v>=%9vIu8O_tvPU&zJred)NT<9p$eFtD-Vo@-OnF9*X{HtIG@t(0iKz z%S1~(c-vp?!Jl2mU!CzU2!C-I;lp5veoZbGCR145JpgKoqc&ebkDCB$upS~bkA1%} zD6yVs>&Qcs4*>=HD^}n+px1}-MtB+O_T%b7y~oTLfhbf7eIGXqAYSjgUH*}T*h#r(6EuG0kKHO%$4Bu!vii7!3&*WPDiHk$P~PSrxE<1eOC8a0_-F^q;iy2bdwb0P^Z)KIa*TEST>a-r2l#p zzYdtMHpO%2cny5{=ee1)^kZUc%)KTXhsz3w%jRZBWaCpq(xt-HSsW;#?&^n68j2H3 zc)Vb)&a&p66c4=Fakk~xbh|73SWHi+ZoSGuU~RcLX~Vk0C@U*nY$$FJG>;f5ZvBm3 zYGLE|*vks-BDVoNr&k^ev_SXJE9t8Ng^U-TmU-utGHab>^Sp}vsQ&4&CB{w0Z9ZY= zgiTP|ObBZlahLa^Bo{fNlX`H;*OH=@%VG6AkewrSleHDuaUkgrceH*z%BI++ivPu^ z>=&Pxzq6UV!?URRia#EzVP&NTk@of!>UB{pR5gE`+w5vwv3q+ZZ zPQ)YF>{V5h9SRDkDF~T~!s~sVq1$n6xfrvb1STO~8t!Qwt1~l(*n>hDKpEW`vlE8` zRBkHk^js6TIP5rh>V4HD>BW+s_(m}07viTw7__c}TRlY3Sf&yl^rbNA;>~PRi_pfR zAq$mGe6g;`ffaLKE_2#Gxj2S;3J7;sj@x2Q(-I_M zEn34Ads?F;sy-OQw|c=fHk;sH~X#7vdf@JdNpoJog$Q zOlDQ*sbO-pLZ!qHE>hKx8%E9FrZpW;aL zfw%nearc5U@FKKM`dX3}GscHo>YPS$O#-ef&OzD$+3_WYO~zf){{=?^Z~UNOCNigf zF!*aprK9~O;fQgRqp`2?Pjv|%x&MvLu%day*G%T60R}m(^F~*#9Ere}X_>|iL_`*5 zHU~K}Ws&0_Fdz$3kqdp}CbC&Z1}3>;w#&`OV3uvN0Y?_Q6;C^LMux)5G%brxD^Q6Q zyUM_-GW)?RJ;T*whbdKb!A2Zg(gu2{dkj_1A_tAF)evsr2GpMP9x%%)a1{BvrZjs~ zMHzpDlXnE)w!QN@6$~;cxH#6I#SNG$^&YM`04LSV=YP*H7>SDl-VX7S zfl>R(@g2{J6k-$<^J&SguCwh0)UZ+o8CLf=vM4ii8^jqS=ocGSP*9vJ0*5@sd58)LdV74+vk0>SRfsoVlb(CQgg3E#Yq(TGZ#e)c@%l zbUI2d6Utfdc}MkO$aU+eXXu+Bl%(I3?Ctn1^ZlJ8O5O^DaiFBqU46h;{oGaLFg@m? zV? zegt=qs*7Zk_Z6WO^25DBIdvXgGhf{WqXj_Y8m?!;Vm?%w$NwABJNOQszvN>W?0zVb zt`j>Qo4G{yC;9N7O6lNTuAqv%5nCv(dGGAe2!V!;T!R`W?79y&oP(p6`5HjO z#0u%!m4WAznv23Gr2mA60$0mF*BYG9uY!KM*){kkl;Vaqgc^OESLoIP?%ZPD?$KU- zfqTMkmt(=H7KxwXq}&6Do^|h<>+7959oKB-&tXX;`6CY@KE5R;6bIGtSWBUi%st>L zRr_XJSpGTj-H-_QjAF_BP8WQ+9_^l;*?lG!TxZq2YQzvi3^D`!F}tZgr^n{@s7*Rj`CUR%5HOr^H4T|x63 zCA(kYg;d_4Eugbka@azb@0#5v#BcZNIq6{5HS+m&>OJ6-&KG`Q>n~AxDh2M$o-{YZ z+qPw0RG;k*UzFK~pKoL?kZ;`s?%)<>xn)95j_5znd;rj=lw&E)>vC4UPaPoMRi}Id z_tv2`_W)FSR_30Ok$rTv!p~*uN55bxi_Iythi6yY-QPb^Tu7h45>BACe72$x2|1JLyp*6dB69`$0HQB;#ReuQkEt#IFVAC+XjE#4OcHG@D$2s7 zKq;VMrX1r?%isuxJW4YO%M6MziU0|nVwVDksm+-YzJ_(focZLUhle*Se`n(tLmCOG_4w+)_FJ3#1x&VpqV-a^j?qW#rKO z1TrZ z;F7}*KW2!ItagxY6my-lx_4W}HS(^96z#|)Y52v;3uA+qdE(X<+~7n~iAG9F5D0Wd zePc41GRgB|&d{}X2;`}kk&$@=8fF8peFMUDWEGvN zhJwRIE?^f;SU37SPstu!<^E_Q>h?aoo`=5vAS`<-1U#;zpsKnm_K6F}eD9;Qq5pWL z$=z-5Ej+_aNTSK@HN11FmXtdL{7?YtK3Ysq8pOQqAtgF8D;OWx@QxhcsydTr<3~G{ z`QptPd%5^GRN~87EO@_DYwGSqeJNb%%=*buUr^wj&fcs$Ge=+G%aMRSXnfL^39EIM z$a;DJsQ<8p*u-iQp$`S$!1L-^kwWriYaYl;nWQWsTAA5_9dRzkXLD>!>^a zVLd{z529>#8tNqxA+Y{rf^md2$z|bkS;dJle)85Zb0|mtvYi|Kr0d13yRbWqv}cvu zPb0t5{J#UPo^{O-&8P&a)U{8)*oB5V<*w)Jla(fSiwA*9GzN6l@=7qv3td-I;X_T2 z?c9{b(2urhqaf&^`#k`R#54;TPm8EZ*eb~D1ko_2B%eT^Vn}k((&{jK&O`%d6R9?Mn-Cp%HH+Z+gBt&xX(K%+R=2diePj~PK$&l zBA2_?AoojT__dBh3C8X0+ALM~!M`wrkV#JB>xeuzF(TcJpN0^qEi~KGPoh>SDm#f6 zbZI;cjj;824U;`uTq}7ApBCvs_+_9bz`^ z1Bs4BS@_~TS2xn(pY{*@Zzdi-U>`Xr8xB6CWiED5A^@&WUbf8d%4N}`X2D{kUBtX9ksbwr1!#L zz7f7FO@&1tZfWrk;mUE0!sm6A$rc55S;BKSTlWE-{_eS}@+;>n1N^}f*iOzJ!ThBv zlZD?sKv~8w!>qfVN`H#>)VJ+Mj-t29Pw)t9bA4klkND#~^J=)mQkaSNIaV7WJyyWq!ME@NzRM!C~aYEBP{OC@iXR=Pp7x*h6xf4qhEM z%;+38<~CFOy7dJHx6bVYAG#Ci_`UP1MuqbeKMdRVn+Ugdc7AbTN5PKzt|dOY@F+~O zgHL`0MX21`PyV=)Pi`e3QBK(2Pxoi~Z-Ili&0D6%ONK6-=D!X(#0 z)Lr0sJW9;v?%GEP#WrQcwV65DV3xPsmK6%!iBWwT>8c47{mM9Z5#=lusouB+@tT3> z{T_foB|sIdBfz6TYctKr@=B&}1=tHkMBA}iUmWBavT5p6ws5gmGrpahBogw#gH#XK z|1)_c=-oTrMiq8C7S(J@5dt-eY>`=Y5R}(`pM6{*o#IZUAuX#(@z!|agWPr05(oBc zq;>%07;Zv)X67BKuZ}kF0rtBs zKOe~dsK14YwV4KwW9knp<1maeqAwRK<(+3aj;_!5Y=^}B*oi*=IV z-`*+V*d3dyUv*IyS0+g-!~GWI=tefcl;Yg4otx0O)fu{M(wSOJmsij^HEi@WB_!O? z(AQXhStQIy#&&N>;ikKApV5~EVzH%}Ive9*GoM?rOKNs#S7X4P6~>}lZ1)0SK7iuoqhyYiL?4@ z9F-zM1f(}5^Z)@OB|t!>gd&6v0Yaqr-uv$|^IUHn*Za+T>o>puUjDc%>)vzsKKI;{ zyUyKve?LJd)l{SwyP<{l?z=@)o`XrPa(wnC?f0F6Mq2mD|_WIRG5=+=^6%D170VCg@meDX}s5o^-k`RwT_P8jDg=PUOG? z%NFLC^{7o|^-cw1-Rzd|_GMjfn&ag7lg~$vW{xJRJG8=R@?(wux(b}#1{2QI^2$1h zE_g}a_jyGW1y!lIw|_9_)`ZHAkFo*i_Fdn4Ipum=@`KQf?Hg9k1@TrYMwwWq-Q!HuYHfF>4uv+yR4qSSzXiEwyImm@9_jykR(_%F+C+7 z9vBprI+Xc6V4@_VU%#`8dd3e@RT_tip_`6Zl8{mp$XE&Z<%M=U=Em zq0}efkdda#K|i5Vx@R3*DBK@q_$k;xUaCCDYytON&{eCfwAzw@fLbeOq!XWvmDz_O zj~Og&HSRMTUW8diA19U!li@$ z$@-B?b#vC0kYlyI=CV@?S)NTM4ds!2I6}rhO>$a04`(c8PD4<_OQ!ZA6}SLYU&t|2}9-*Ba!z?R`s3g(>m{c*V?p>4~4oF2A#(JnR+l9O2kA&q9ojIL`A zD-(>Qmo4(Ub?#UXKdl~|bBh{x?2qvx>1tVJp-@>RwcGGk^?$@K7-=X0ZQy(}gx z9{z$JH~dd)*+$vqn)QT6Gd6z7t%#3xqnE;AnH`zJwkVn7BF0f4O{JdG5%Bh`r$*MqXr0Id4IipR{k{o7b0}JfpAF`_f7@c6_XfJ zcZ%2;9Wb7pwN-ay8XTt|V?m1y4py}(2)5F@|D$1~FrTnyd+K20b?xxSiif2Dy3wC9 zm_PPpSK|+zScyv_E%Gg-^sKFD3E~ABnyhTNQVKrS#DqD^-aO5hjFLM)s#LWyopQBzCKoR=3u*uNy_GQ|ilCVF| zxJ=%_<#InnKk2et4>s~ibCEx` zKPZcS`L~oalBh!({YPswr)OWt)btr`lvp%WC-`s#>pzyc64nymRsB3~kOYHfttAtN zuSqK0te89x_b;yC?{1w-?s+S>HLJ;`yv8`8c4)Yi^NQ+?0*EVP z9sJ=p0*F%~EI9Un0Mh(o#ohxTfE3tx0z{3Y0>|FmOjja6)ATKxG&QQTbl$)6msjQv zE66kFo@C-v$Fl0Zd{b;HLv9Q=fZRR&?MTn{V2+FT*3R9Lg_ewu7AROyti* zejxZ6DyM-)@>>x!k!SFe85r$v>JiD50@D)}7jExGAB9;BryaM1n0M{*46|c>x?@B) zi(5se{o)McZo1oaRa|-ZDmsWw4;Y^^aJba=Yn0BHp+Wm54!m;^N~8aqlDQMF$UILR zhE(zTIh|tzFH=V!ToPW&$5Db=Y5c~_oIvf|fo{0`|2EqWdC{T@aREi0(o6ZGSAF_y zx};~PO03Z|$;%R?VWqp$-R#eETvP7oT8@-6+q`C<{7cToU*JRz7Mus4aU})oK7;q! zMx}YZaHdq$f*rXk6BcObU6wpof5D@s`Lvdb+Elg=d@V|W$bUgy)~3|7^T8RFLBBcq zMKba+~; zr6DIQkiCWTcC0@*UoYrK`U5LkqH!9=d94fyG3t72JcH(ds<^L@GmWK-0Smt}uBdQ= zOkE|^u!jb-#^qOA+d4P7BhqCNe&+{8U-d(yZjZdtDKHl_5^#Or0kav6Gn-ki$Iu}O zZIT1!t@R!7%lSdZSzX{2tDKV(X{Gs;6D8*@{6CCyB|Qr2Olg($W5xDy+e(IT**_yP zU`>~G??+T#OixX$LgQB!#^busRlzMqUQ?Sm+9ij&%ah1Fn_UIDvDJ9TVP}!Vt%`?h zH%*sLcpA@eRO&K&kda)n^zV@sgjmHeqn%$9BfX9i+N%;Bj48}AKhx%cm&|`yfB(N4 zCuVW$bb2~p7{9LZJv&t==bSMcv?0DfeD2(sB~sP6G0b-9nGqTZY0|^7rTPUM=wiQH z9K;5uRM3;>QBo?ye$4lEXI^v(q>tZkKXHFZAjSQqK40gMU{Tkoy@x#<&exHCvAX(J z5^dT!)ql-1uaKBlv6m$gkkm@4;?-Gls>3VZOerIW>=))Uw; z;ERgL@fXG8LTHVA*{gy;Ku$NA2_C{rn^ydX3%hw-MO5d>N0m6*BO@@G#LOgUnO<=! zopELK4(mmtfnLiG>CAnJ9$U5S`=CoTy;NN-?tX>4Z#hd@oJ_%8_4(du%hE*YSFJcH zRi!E6cyt^7NqMMyV4y^{b%S%Zu$J&;5ckT%wBmJmsXDE0Xpkg*ad(l@{P_|p;<3kV#kERp@s z6T`fY39_rF+_Yz4KC=A7oV$0?h~Y|jkDPdxBA=G)_TD{r1>*2{eF(8?w3shFCGmaz zGrh^vrp1iqL+?kEc$KtW5_v^P?1}o2hezdv(bhb^{MO`cJ`KU&MVwkI9!D3DQ=XXfO79;j^mf^sb;&OhnU zoB9u4F^R@t*cN-&(~)oMesMbXWwPyeQ-4fCf1C0rs>VT+FQh`uK-t+B@Rr|=e5s9_ z-}{oY^jn)R)I`kpYzI8E?qNupSpSP$*Fi_VD8Y4e2Z8>yl$}~o^009idCD$yLqjyf zSbC@7y=s>l*v9$T)OZ*&(&L-A*M9E_us!0rGCZDA!?MR#9d-m2&$9>M4aElieb?D` zZ<8=(JJTvUQjNEPhMdT66wSrqbUJBR?vad;*Hq(V`{YxYQYEYayJ3nMHOx=l2kq*J z?71WfoeIW0OzPZbS(L(vcJ3_KDW@mll%j3eUbM9wIHpI!Q#axBHM@oK#1hO@4r9^; zb0tL#ygddyJWtTNF}c&q<7rAzif-#1Vhp1usm90t} znYSvqVSt$Qso4V#2d66c%cA*J{7;;bQ4M0RtGgZjd|a;C zER>b)8Pt$D|Djv{gXgl;+bMTTx^JqLe|6+Y<%09Z$r>jzCOm(@$h=O^rffqBf&Y z=PXYb|m`x$!?d9PC zYA?>P0@k#nUVvP8VC>y&sMy(9C$44uW|#G|*+yIL5c?nI@uI84l!|x_SY{`AcF5}? zos$7!Mq2l|H4COybmMcfW{A{FV?5oqJw0~TZ)xb(=y)l&bRITn6y~jT@BQf-Dr!X< zB|NiIWGBKM7K77bdM;7LnWmdSR44Us%K^6|xO*2d1XO2W5da#h zWM}w}FTeqj^c0cwm|RGa-fbtXFi6z@mYrJe6T5HL&*P^QrzvdD6G|f_vnjY4+uf>cfHl&aSud;Z-2S~elltA%-OwD@(lU;`ww@JVu(RXl0%fPSpA@#%JZSf* zt(EpLD;O-5*Y@HdBs5mbdgJBva?loTqm8ldvvht1YB#!R=T{`;|fD8Pk1GWpjkty5MMU zu{0gXiAfg~^=%Z?y+EEt+m!=pp!Sv)%05U0?3}leIdCWDasNIDD!3@G)JS}Y8GM7w z2QnS=cV#Dc?pQ1glS=E_D4lFsA?P2_TM`Y!IV!U9%>%;hL5 za6DW5gYXeDEK>V*f9So^H&7s}I@KYR!1JJ#wt%zb6YKwUO+W(#QelEP>yNzu&;ZO% zuiAO{Ig)9TZ!y7Vjo!qsc!{(YOuK~7DRU`Y?G`aB^f~*G>)l`L{l3z>9s2VSJf=_I zdR9%w+xE%h-O%!yVQKbs9lU-2%L~VBdsmb2IhlzjjZyU5a{MVF)x`ol(u^JoaBBj2 zLg8cVOCRD!sL(k< zv@RCL#nkH1+)^AANS3t6A?VsrHdO4q(b1wgvMf zltpd@1OJ9|vtYtGtUc25q;cJ{xGBlV5{2rusIe8;gVLtP-zR1rUHa#m>c8LqI|6^# J2=MOH{s;1Y3~c}a diff --git a/dist/debian/copyright b/dist/debian/copyright index cc3300eb..b05a99c9 100644 --- a/dist/debian/copyright +++ b/dist/debian/copyright @@ -25,8 +25,6 @@ Files: src/core/main.h src/engine/enginetype.h src/engine/alsadevicefinder.cpp src/engine/alsadevicefinder.h - src/engine/deezerengine.cpp - src/engine/deezerengine.h src/engine/devicefinder.cpp src/engine/devicefinder.h src/engine/enginedevice.cpp @@ -39,8 +37,6 @@ Files: src/core/main.h src/settings/backendsettingspage.h src/settings/scrobblersettingspage.cpp src/settings/scrobblersettingspage.h - src/settings/deezersettingspage.cpp - src/settings/deezersettingspage.h src/settings/tidalsettingspage.cpp src/settings/tidalsettingspage.h src/covermanager/lastfmcoverprovider.cpp @@ -58,7 +54,6 @@ Files: src/core/main.h src/lyrics/* src/scrobbler/* src/tidal/* - src/deezer/* src/transcoder/transcoderoptionswavpack.cpp src/transcoder/transcoderoptionswavpack.h Copyright: 2012-2014, 2017-2018, Jonas Kvinge diff --git a/dist/debian/rules b/dist/debian/rules index 0eacd860..77dedf36 100755 --- a/dist/debian/rules +++ b/dist/debian/rules @@ -12,8 +12,7 @@ configure-stamp: dh_testdir cmake .. \ -DCMAKE_INSTALL_PREFIX=$(CURDIR)/debian/strawberry/usr \ - -DENABLE_TRANSLATIONS=ON \ - -DENABLE_STREAM_DEEZER=ON + -DENABLE_TRANSLATIONS=ON touch configure-stamp build: build-stamp diff --git a/dist/pacman/PKGBUILD.in b/dist/pacman/PKGBUILD.in index 8e49ba1a..18e58a98 100644 --- a/dist/pacman/PKGBUILD.in +++ b/dist/pacman/PKGBUILD.in @@ -52,7 +52,6 @@ build() { cmake ../${pkgname}-@STRAWBERRY_VERSION_PACKAGE@ \ -DCMAKE_INSTALL_PREFIX=/usr \ -DUSE_SYSTEM_TAGLIB=ON \ - -DENABLE_STREAM_DEEZER=ON \ -DENABLE_PHONON=ON \ -DENABLE_TRANSLATIONS=ON make -j$(nproc) diff --git a/dist/unix/org.strawbs.strawberry.appdata.xml b/dist/unix/org.strawbs.strawberry.appdata.xml index 1e0db46d..92eb17c1 100644 --- a/dist/unix/org.strawbs.strawberry.appdata.xml +++ b/dist/unix/org.strawbs.strawberry.appdata.xml @@ -34,7 +34,7 @@

  • Audio analyzer
  • Audio equalizer
  • Transfer music to iPod, iPhone, MTP or mass-storage USB player
  • -
  • Streaming support for Tidal and Deezer
  • +
  • Streaming support for Tidal
  • Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
  • diff --git a/dist/windows/strawberry-debug-x64.nsi.in b/dist/windows/strawberry-debug-x64.nsi.in index 497dfd04..677ae796 100644 --- a/dist/windows/strawberry-debug-x64.nsi.in +++ b/dist/windows/strawberry-debug-x64.nsi.in @@ -162,7 +162,6 @@ Section "Strawberry" Strawberry File "libxml2-2.dll" File "libsoup-2.4-1.dll" File "liblzma-5.dll" - File "libdeezer.x64.dll" ; Register Strawberry with Default Programs Var /GLOBAL AppIcon @@ -403,7 +402,6 @@ Section "Uninstall" Delete "$INSTDIR\libxml2-2.dll" Delete "$INSTDIR\libsoup-2.4-1.dll" Delete "$INSTDIR\liblzma-5.dll" - Delete "$INSTDIR\libdeezer.x64.dll" Delete "$INSTDIR\platforms\qwindows.dll" Delete "$INSTDIR\sqldrivers\qsqlite.dll" diff --git a/dist/windows/strawberry-debug-x86.nsi.in b/dist/windows/strawberry-debug-x86.nsi.in index 4b260e7f..bb43f8b9 100644 --- a/dist/windows/strawberry-debug-x86.nsi.in +++ b/dist/windows/strawberry-debug-x86.nsi.in @@ -162,7 +162,6 @@ Section "Strawberry" Strawberry File "libxml2-2.dll" File "libsoup-2.4-1.dll" File "liblzma-5.dll" - File "libdeezer.x86.dll" ; Register Strawberry with Default Programs Var /GLOBAL AppIcon @@ -403,7 +402,6 @@ Section "Uninstall" Delete "$INSTDIR\libxml2-2.dll" Delete "$INSTDIR\libsoup-2.4-1.dll" Delete "$INSTDIR\liblzma-5.dll" - Delete "$INSTDIR\libdeezer.x86.dll" Delete "$INSTDIR\platforms\qwindows.dll" Delete "$INSTDIR\sqldrivers\qsqlite.dll" diff --git a/dist/windows/strawberry-x64.nsi.in b/dist/windows/strawberry-x64.nsi.in index 7062f697..1d1dd9bf 100644 --- a/dist/windows/strawberry-x64.nsi.in +++ b/dist/windows/strawberry-x64.nsi.in @@ -161,7 +161,6 @@ Section "Strawberry" Strawberry File "libxml2-2.dll" File "libsoup-2.4-1.dll" File "liblzma-5.dll" - File "libdeezer.x64.dll" ; Register Strawberry with Default Programs Var /GLOBAL AppIcon @@ -370,7 +369,6 @@ Section "Uninstall" Delete "$INSTDIR\libxml2-2.dll" Delete "$INSTDIR\libsoup-2.4-1.dll" Delete "$INSTDIR\liblzma-5.dll" - Delete "$INSTDIR\libdeezer.x64.dll" Delete "$INSTDIR\platforms\qwindows.dll" Delete "$INSTDIR\sqldrivers\qsqlite.dll" diff --git a/dist/windows/strawberry-x86.nsi.in b/dist/windows/strawberry-x86.nsi.in index 66c3c09a..477fa664 100644 --- a/dist/windows/strawberry-x86.nsi.in +++ b/dist/windows/strawberry-x86.nsi.in @@ -161,7 +161,6 @@ Section "Strawberry" Strawberry File "libxml2-2.dll" File "libsoup-2.4-1.dll" File "liblzma-5.dll" - File "libdeezer.x86.dll" ; Register Strawberry with Default Programs Var /GLOBAL AppIcon @@ -370,7 +369,6 @@ Section "Uninstall" Delete "$INSTDIR\libxml2-2.dll" Delete "$INSTDIR\libsoup-2.4-1.dll" Delete "$INSTDIR\liblzma-5.dll" - Delete "$INSTDIR\libdeezer.x86.dll" Delete "$INSTDIR\platforms\qwindows.dll" Delete "$INSTDIR\sqldrivers\qsqlite.dll" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3966def3..a05a838c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -64,14 +64,6 @@ if(HAVE_PHONON) include_directories(${PHONON_INCLUDE_DIRS}) endif() -if(HAVE_LIBDEEZER) - include_directories(${DEEZER_INCLUDE_DIRS}) -endif() - -if(HAVE_LIBDZMEDIA) - include_directories(${DZMEDIA_INCLUDE_DIRS}) -endif() - link_directories(${TAGLIB_LIBRARY_DIRS}) include_directories(${TAGLIB_INCLUDE_DIRS}) @@ -579,12 +571,6 @@ optional_source(HAVE_PHONON HEADERS engine/phononengine.h ) -# Deezer -optional_source(HAVE_DEEZER - SOURCES engine/deezerengine.cpp - HEADERS engine/deezerengine.h -) - # DBUS and MPRIS - Unix specific if(UNIX AND HAVE_DBUS) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/dbus) @@ -888,19 +874,6 @@ optional_source(HAVE_STREAM_TIDAL settings/tidalsettingspage.ui ) -optional_source(HAVE_STREAM_DEEZER - SOURCES - deezer/deezerservice.cpp - deezer/deezerurlhandler.cpp - settings/deezersettingspage.cpp - HEADERS - deezer/deezerservice.h - deezer/deezerurlhandler.h - settings/deezersettingspage.h - UI - settings/deezersettingspage.ui -) - configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) @@ -994,14 +967,6 @@ if(HAVE_PHONON) target_link_libraries(strawberry_lib ${PHONON_LIBRARIES}) endif() -if(HAVE_DEEZER) - target_link_libraries(strawberry_lib ${LIBDEEZER_LIBRARIES}) -endif() - -if(HAVE_DZMEDIA) - target_link_libraries(strawberry_lib ${LIBDZMEDIA_LIBRARIES}) -endif() - if(HAVE_LIBGPOD) target_link_libraries(strawberry_lib ${LIBGPOD_LIBRARIES}) endif(HAVE_LIBGPOD) diff --git a/src/config.h.in b/src/config.h.in index eb7c40e8..7d9fcfb7 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -39,7 +39,6 @@ #cmakedefine HAVE_SPARKLE #cmakedefine HAVE_CHROMAPRINT #cmakedefine HAVE_TAGLIB_DSFFILE -#cmakedefine HAVE_DZMEDIA #cmakedefine HAVE_GLOBALSHORTCUTS #cmakedefine IMOBILEDEVICE_USES_UDIDS #cmakedefine USE_INSTALL_PREFIX @@ -48,10 +47,8 @@ #cmakedefine HAVE_VLC #cmakedefine HAVE_XINE #cmakedefine HAVE_PHONON -#cmakedefine HAVE_DEEZER #cmakedefine HAVE_STREAM_TIDAL -#cmakedefine HAVE_STREAM_DEEZER #cmakedefine HAVE_KEYSYMDEF_H #cmakedefine HAVE_XF86KEYSYM_H diff --git a/src/core/application.cpp b/src/core/application.cpp index 48d708b0..eae7f405 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -66,9 +66,6 @@ #ifdef HAVE_STREAM_TIDAL # include "tidal/tidalservice.h" #endif -#ifdef HAVE_STREAM_DEEZER -# include "deezer/deezerservice.h" -#endif #include "scrobbler/audioscrobbler.h" @@ -127,17 +124,11 @@ class ApplicationImpl { InternetServices *internet_services = new InternetServices(app); #ifdef HAVE_STREAM_TIDAL internet_services->AddService(new TidalService(app, internet_services)); -#endif -#ifdef HAVE_STREAM_DEEZER - internet_services->AddService(new DeezerService(app, internet_services)); #endif return internet_services; }), #ifdef HAVE_STREAM_TIDAL tidal_search_([=]() { return new InternetSearch(app, Song::Source_Tidal, app); }), -#endif -#ifdef HAVE_STREAM_DEEZER - deezer_search_([=]() { return new InternetSearch(app, Song::Source_Deezer, app); }), #endif scrobbler_([=]() { return new AudioScrobbler(app, app); }) {} @@ -161,9 +152,6 @@ class ApplicationImpl { Lazy internet_services_; #ifdef HAVE_STREAM_TIDAL Lazy tidal_search_; -#endif -#ifdef HAVE_STREAM_DEEZER - Lazy deezer_search_; #endif Lazy scrobbler_; @@ -236,7 +224,4 @@ InternetServices *Application::internet_services() const { return p_->internet_s #ifdef HAVE_STREAM_TIDAL InternetSearch *Application::tidal_search() const { return p_->tidal_search_.get(); } #endif -#ifdef HAVE_STREAM_DEEZER -InternetSearch *Application::deezer_search() const { return p_->deezer_search_.get(); } -#endif AudioScrobbler *Application::scrobbler() const { return p_->scrobbler_.get(); } diff --git a/src/core/application.h b/src/core/application.h index 1bfa2626..2173c87c 100644 --- a/src/core/application.h +++ b/src/core/application.h @@ -96,9 +96,6 @@ class Application : public QObject { #ifdef HAVE_STREAM_TIDAL InternetSearch *tidal_search() const; #endif -#ifdef HAVE_STREAM_DEEZER - InternetSearch *deezer_search() const; -#endif AudioScrobbler *scrobbler() const; diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index 10f96660..2d9dc039 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -136,9 +136,6 @@ #ifdef HAVE_STREAM_TIDAL # include "settings/tidalsettingspage.h" #endif -#ifdef HAVE_STREAM_DEEZER -# include "settings/deezersettingspage.h" -#endif #include "internet/internetservices.h" #include "internet/internetservice.h" @@ -206,9 +203,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co }), #ifdef HAVE_STREAM_TIDAL tidal_search_view_(new InternetSearchView(app_, app_->tidal_search(), TidalSettingsPage::kSettingsGroup, SettingsDialog::Page_Tidal, this)), -#endif -#ifdef HAVE_STREAM_DEEZER - deezer_search_view_(new InternetSearchView(app_, app_->deezer_search(), DeezerSettingsPage::kSettingsGroup, SettingsDialog::Page_Deezer, this)), #endif playlist_menu_(new QMenu(this)), playlist_add_to_another_(nullptr), @@ -266,9 +260,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co #ifdef HAVE_STREAM_TIDAL ui_->tabs->addTab(tidal_search_view_, IconLoader::Load("tidal"), tr("Tidal")); #endif -#ifdef HAVE_STREAM_DEEZER - ui_->tabs->addTab(deezer_search_view_, IconLoader::Load("deezer"), tr("Deezer")); -#endif // Add the playing widget to the fancy tab widget ui_->tabs->addBottomWidget(ui_->widget_playing); @@ -547,9 +538,6 @@ MainWindow::MainWindow(Application *app, SystemTrayIcon *tray_icon, OSD *osd, co #ifdef HAVE_STREAM_TIDAL connect(tidal_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); #endif -#ifdef HAVE_STREAM_DEEZER - connect(deezer_search_view_, SIGNAL(AddToPlaylist(QMimeData*)), SLOT(AddToPlaylist(QMimeData*))); -#endif // Playlist menu playlist_play_pause_ = playlist_menu_->addAction(tr("Play"), this, SLOT(PlaylistPlay())); @@ -859,16 +847,6 @@ void MainWindow::ReloadSettings() { ui_->tabs->delTab("Tidal"); #endif -#ifdef HAVE_STREAM_DEEZER - settings.beginGroup(DeezerSettingsPage::kSettingsGroup); - bool enable_deezer = settings.value("enabled", false).toBool(); - settings.endGroup(); - if (enable_deezer) - ui_->tabs->addTab(deezer_search_view_, IconLoader::Load("deezer"), tr("Deezer")); - else - ui_->tabs->delTab("Deezer"); -#endif - } void MainWindow::ReloadAllSettings() { @@ -885,9 +863,6 @@ void MainWindow::ReloadAllSettings() { #ifdef HAVE_STREAM_TIDAL tidal_search_view_->ReloadSettings(); #endif -#ifdef HAVE_STREAM_DEEZER - deezer_search_view_->ReloadSettings(); -#endif } diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index e97d6735..7e7484ea 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -332,7 +332,6 @@ signals: #endif InternetSearchView *tidal_search_view_; - InternetSearchView *deezer_search_view_; QAction *collection_show_all_; QAction *collection_show_duplicates_; diff --git a/src/core/player.cpp b/src/core/player.cpp index 103de00a..95b6e493 100644 --- a/src/core/player.cpp +++ b/src/core/player.cpp @@ -59,9 +59,6 @@ #ifdef HAVE_VLC # include "engine/vlcengine.h" #endif -#ifdef HAVE_DEEZER -# include "engine/deezerengine.h" -#endif #include "collection/collectionbackend.h" #include "playlist/playlist.h" @@ -140,15 +137,6 @@ Engine::EngineType Player::CreateEngine(Engine::EngineType enginetype) { use_enginetype=Engine::Phonon; engine_.reset(new PhononEngine(app_->task_manager())); break; -#endif -#ifdef HAVE_DEEZER - case Engine::Deezer:{ - use_enginetype=Engine::Deezer; - DeezerEngine *deezerengine = new DeezerEngine(app_->task_manager()); - connect(this, SIGNAL(Authenticated()), deezerengine, SLOT(LoadAccessToken())); - engine_.reset(deezerengine); - break; - } #endif default: if (i > 0) { qFatal("No engine available!"); } @@ -321,7 +309,7 @@ void Player::NextInternal(Engine::TrackChangeFlags change) { if (app_->playlist_manager()->active()->current_item()) { const QUrl url = app_->playlist_manager()->active()->current_item()->Url(); - if (url_handlers_.contains(url.scheme()) && !(engine_->type() == Engine::Deezer && url.scheme() == "dzmedia")) { + if (url_handlers_.contains(url.scheme())) { // The next track is already being loaded if (url == loading_async_) return; @@ -531,7 +519,7 @@ void Player::PlayAt(int index, Engine::TrackChangeFlags change, bool reshuffle) if (current_item_ && change == Engine::Manual && engine_->position_nanosec() != engine_->length_nanosec()) { emit TrackSkipped(current_item_); const QUrl &url = current_item_->Url(); - if (url_handlers_.contains(url.scheme()) && !(engine_->type() == Engine::Deezer && url.scheme() == "dzmedia")) { + if (url_handlers_.contains(url.scheme())) { url_handlers_[url.scheme()]->TrackSkipped(); } } @@ -550,7 +538,7 @@ void Player::PlayAt(int index, Engine::TrackChangeFlags change, bool reshuffle) current_item_ = app_->playlist_manager()->active()->current_item(); const QUrl url = current_item_->Url(); - if (url_handlers_.contains(url.scheme()) && !(engine_->type() == Engine::Deezer && url.scheme() == "dzmedia")) { + if (url_handlers_.contains(url.scheme())) { // It's already loading if (url == loading_async_) return; @@ -697,7 +685,7 @@ void Player::TrackAboutToEnd() { // We don't want to preload (and scrobble) the next item in the playlist if it's just going to be stopped again immediately after. if (app_->playlist_manager()->active()->current_item()) { const QUrl url = app_->playlist_manager()->active()->current_item()->Url(); - if (url_handlers_.contains(url.scheme()) && !(engine_->type() == Engine::Deezer && url.scheme() == "dzmedia")) { + if (url_handlers_.contains(url.scheme())) { url_handlers_[url.scheme()]->TrackAboutToEnd(); return; } @@ -730,7 +718,7 @@ void Player::TrackAboutToEnd() { QUrl url = next_item->Url(); // Get the actual track URL rather than the stream URL. - if (url_handlers_.contains(url.scheme()) && !(engine_->type() == Engine::Deezer && url.scheme() == "dzmedia")) { + if (url_handlers_.contains(url.scheme())) { UrlHandler::LoadResult result = url_handlers_[url.scheme()]->LoadNext(url); switch (result.type_) { case UrlHandler::LoadResult::Error: diff --git a/src/core/song.cpp b/src/core/song.cpp index 34813bcc..f001b555 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -304,7 +304,7 @@ uint Song::mtime() const { return d->mtime_; } uint Song::ctime() const { return d->ctime_; } int Song::filesize() const { return d->filesize_; } Song::FileType Song::filetype() const { return d->filetype_; } -bool Song::is_stream() const { return d->source_ == Source_Stream || d->source_ == Source_Tidal || d->source_ == Source_Deezer; } +bool Song::is_stream() const { return d->source_ == Source_Stream || d->source_ == Source_Tidal; } bool Song::is_cdda() const { return d->source_ == Source_CDDA; } bool Song::is_collection_song() const { return !is_cdda() && !is_stream() && id() != -1; @@ -393,7 +393,6 @@ QString Song::TextForSource(Source source) { case Song::Source_Device: return QObject::tr("Device"); case Song::Source_Stream: return QObject::tr("Stream"); case Song::Source_Tidal: return QObject::tr("Tidal"); - case Song::Source_Deezer: return QObject::tr("Deezer"); default: return QObject::tr("Unknown"); } @@ -408,7 +407,6 @@ QIcon Song::IconForSource(Source source) { case Song::Source_Device: return IconLoader::Load("device"); case Song::Source_Stream: return IconLoader::Load("applications-internet"); case Song::Source_Tidal: return IconLoader::Load("tidal"); - case Song::Source_Deezer: return IconLoader::Load("deezer"); default: return IconLoader::Load("edit-delete"); } diff --git a/src/core/song.h b/src/core/song.h index ac6fc4ba..4f7567e2 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -101,7 +101,6 @@ class Song { Source_Device = 4, Source_Stream = 5, Source_Tidal = 6, - Source_Deezer = 7, }; enum FileType { diff --git a/src/deezer/deezerservice.cpp b/src/deezer/deezerservice.cpp deleted file mode 100644 index e6c949ed..00000000 --- a/src/deezer/deezerservice.cpp +++ /dev/null @@ -1,827 +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" - -#ifdef HAVE_DZMEDIA -# include -#endif - -#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/mergedproxymodel.h" -#include "core/network.h" -#include "core/song.h" -#include "core/iconloader.h" -#include "core/taskmanager.h" -#include "core/timeconstants.h" -#include "core/utilities.h" -#include "internet/internetservices.h" -#include "internet/internetsearch.h" -#include "internet/localredirectserver.h" -#include "deezerservice.h" -#include "deezerurlhandler.h" -#include "settings/deezersettingspage.h" - -const Song::Source DeezerService::kSource = Song::Source_Deezer; -const char *DeezerService::kApiUrl = "https://api.deezer.com"; -const char *DeezerService::kOAuthUrl = "https://connect.deezer.com/oauth/auth.php"; -const char *DeezerService::kOAuthAccessTokenUrl = "https://connect.deezer.com/oauth/access_token.php"; -const char *DeezerService::kOAuthRedirectUrl = "https://oauth.strawbs.net"; -const int DeezerService::kAppID = 303684; -const char *DeezerService::kSecretKey = "06911976010b9ddd7256769adf2b2e56"; - -typedef QPair Param; - -DeezerService::DeezerService(Application *app, QObject *parent) - : InternetService(Song::Source_Deezer, "Deezer", "dzmedia", app, parent), - network_(new NetworkAccessManager(this)), - url_handler_(new DeezerUrlHandler(app, this)), -#ifdef HAVE_DZMEDIA - dzmedia_(new DZMedia(this)), -#endif - timer_searchdelay_(new QTimer(this)), - searchdelay_(1500), - albumssearchlimit_(1), - songssearchlimit_(1), - fetchalbums_(false), - preview_(false), - pending_search_id_(0), - next_pending_search_id_(1), - search_id_(0), - albums_requested_(0), - albums_received_(0) - { - - timer_searchdelay_->setSingleShot(true); - connect(timer_searchdelay_, SIGNAL(timeout()), SLOT(StartSearch())); - - connect(this, SIGNAL(Authenticated()), app->player(), SLOT(HandleAuthentication())); - - app->player()->RegisterUrlHandler(url_handler_); - - ReloadSettings(); - LoadAccessToken(); - -#ifdef HAVE_DZMEDIA - connect(dzmedia_, SIGNAL(StreamURLReceived(QUrl, QUrl, DZMedia::FileType)), this, SLOT(GetStreamURLFinished(QUrl, QUrl, DZMedia::FileType))); -#endif - -} - -DeezerService::~DeezerService() {} - -void DeezerService::ShowConfig() { - app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Deezer); -} - -void DeezerService::ReloadSettings() { - - QSettings s; - s.beginGroup(DeezerSettingsPage::kSettingsGroup); - searchdelay_ = s.value("searchdelay", 1500).toInt(); - albumssearchlimit_ = s.value("albumssearchlimit", 100).toInt(); - songssearchlimit_ = s.value("songssearchlimit", 100).toInt(); - fetchalbums_ = s.value("fetchalbums", false).toBool(); - coversize_ = s.value("coversize", "cover_big").toString(); -#if defined(HAVE_DEEZER) || defined(HAVE_DZMEDIA) - bool preview(false); -#else - bool preview(true); -#endif - preview_ = s.value("preview", preview).toBool(); - s.endGroup(); - -} - -void DeezerService::LoadAccessToken() { - - QSettings s; - s.beginGroup(DeezerSettingsPage::kSettingsGroup); - if (s.contains("access_token") && s.contains("expiry_time")) { - access_token_ = s.value("access_token").toString(); - expiry_time_ = s.value("expiry_time").toDateTime(); - } - s.endGroup(); - -} - -void DeezerService::Logout() { - - access_token_.clear(); - QSettings s; - s.beginGroup(DeezerSettingsPage::kSettingsGroup); - s.remove("access_token"); - s.remove("expiry_time"); - s.endGroup(); - -} - -void DeezerService::StartAuthorisation() { - - LocalRedirectServer *server = new LocalRedirectServer(this); - server->Listen(); - - QUrl url = QUrl(kOAuthUrl); - QUrlQuery url_query; - //url_query.addQueryItem("response_type", "token"); - url_query.addQueryItem("response_type", "code"); - url_query.addQueryItem("app_id", QString::number(kAppID)); - QUrl redirect_url; - QUrlQuery redirect_url_query; - - const QString port = QString::number(server->url().port()); - - redirect_url = QUrl(kOAuthRedirectUrl); - redirect_url_query.addQueryItem("port", port); - redirect_url.setQuery(redirect_url_query); - url_query.addQueryItem("redirect_uri", redirect_url.toString()); - url.setQuery(url_query); - - NewClosure(server, SIGNAL(Finished()), this, &DeezerService::RedirectArrived, server, redirect_url); - QDesktopServices::openUrl(url); - -} - -void DeezerService::RedirectArrived(LocalRedirectServer *server, QUrl url) { - - server->deleteLater(); - QUrl request_url = server->request_url(); - RequestAccessToken(QUrlQuery(request_url).queryItemValue("code").toUtf8()); - -} - -void DeezerService::RequestAccessToken(const QByteArray &code) { - - typedef QPair Arg; - typedef QList ArgList; - - typedef QPair EncodedArg; - typedef QList EncodedArgList; - - ArgList args = ArgList() << Arg("app_id", QString::number(kAppID)) - << Arg("secret", kSecretKey) - << Arg("code", code); - - QUrlQuery url_query; - for (const Arg &arg : args) { - EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second)); - url_query.addQueryItem(encoded_arg.first, encoded_arg.second); - } - - QUrl url(kOAuthAccessTokenUrl); - QNetworkRequest request = QNetworkRequest(url); - QNetworkReply *reply = network_->post(request, url_query.toString(QUrl::FullyEncoded).toUtf8()); - NewClosure(reply, SIGNAL(finished()), this, SLOT(FetchAccessTokenFinished(QNetworkReply*)), reply); - -} - -void DeezerService::FetchAccessTokenFinished(QNetworkReply *reply) { - - reply->deleteLater(); - - if (reply->error() != QNetworkReply::NoError) { - Error(QString("%1 (%2)").arg(reply->errorString()).arg(reply->error())); - return; - } - - forever { - QByteArray line = reply->readLine(); - QString str(line); - QStringList args = str.split("&"); - for (QString arg : args) { - QStringList params = arg.split("="); - if (params.count() < 2) continue; - QString param1 = params.first(); - QString param2 = params[1]; - if (param1 == "access_token") access_token_ = param2; - else if (param1 == "expires") SetExpiryTime(param2.toInt()); - } - if (reply->atEnd()) break; - } - - QSettings s; - s.beginGroup(DeezerSettingsPage::kSettingsGroup); - s.setValue("access_token", access_token_); - s.setValue("expiry_time", expiry_time_); - s.endGroup(); - - emit Authenticated(); - emit LoginSuccess(); - -} - -void DeezerService::SetExpiryTime(int expires_in_seconds) { - - // Set the expiry time with two minutes' grace. - expiry_time_ = QDateTime::currentDateTime().addSecs(expires_in_seconds - 120); - qLog(Debug) << "Current oauth access token expires at:" << expiry_time_; - -} - -QNetworkReply *DeezerService::CreateRequest(const QString &ressource_name, const QList ¶ms) { - - typedef QPair Arg; - typedef QList ArgList; - - typedef QPair EncodedArg; - typedef QList EncodedArgList; - - ArgList args = ArgList() << Arg("access_token", access_token_) - << Arg("output", "json") - << params; - - QUrlQuery url_query; - for (const Arg& arg : args) { - EncodedArg encoded_arg(QUrl::toPercentEncoding(arg.first), QUrl::toPercentEncoding(arg.second)); - url_query.addQueryItem(encoded_arg.first, encoded_arg.second); - } - - QUrl url(kApiUrl + QString("/") + ressource_name); - url.setQuery(url_query); - QNetworkRequest req(url); - QNetworkReply *reply = network_->get(req); - - //qLog(Debug) << "Deezer: Sending request" << url; - - return reply; - -} - -QByteArray DeezerService::GetReplyData(QNetworkReply *reply) { - - QByteArray data; - - if (reply->error() == QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { - data = reply->readAll(); - } - else { - if (reply->error() < 200) { - // This is a network error, there is nothing more to do. - QString failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - Error(failure_reason); - } - else { - // See if there is Json data containing "error" - then use that instead. - data = reply->readAll(); - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); - QString failure_reason; - if (error.error == QJsonParseError::NoError && !json_doc.isNull() && !json_doc.isEmpty() && json_doc.isObject()) { - QJsonObject json_obj = json_doc.object(); - if (json_obj.contains("error")) { - QJsonValue json_value_error = json_obj["error"]; - if (json_value_error.isObject()) { - QJsonObject json_error = json_value_error.toObject(); - int code = json_error["code"].toInt(); - if (code == 300) Logout(); - QString message = json_error["message"].toString(); - QString type = json_error["type"].toString(); - failure_reason = QString("%1 (%2)").arg(message).arg(code); - } - else { failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); } - } - else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - } - else { - failure_reason = QString("%1 (%2)").arg(reply->errorString()).arg(reply->error()); - } - if (reply->error() == QNetworkReply::ContentAccessDenied || reply->error() == QNetworkReply::ContentOperationNotPermittedError || reply->error() == QNetworkReply::AuthenticationRequiredError) { - // Session is probably expired - Logout(); - Error(failure_reason); - } - else if (reply->error() == QNetworkReply::ContentNotFoundError) { // Ignore this error - Error(failure_reason); - } - else { // Fail - Error(failure_reason); - } - } - return QByteArray(); - } - - return data; - -} - -QJsonObject DeezerService::ExtractJsonObj(QByteArray &data) { - - QJsonParseError error; - QJsonDocument json_doc = QJsonDocument::fromJson(data, &error); - - if (error.error != QJsonParseError::NoError) { - Error("Reply from server missing Json data.", data); - return QJsonObject(); - } - - if (json_doc.isNull() || json_doc.isEmpty()) { - Error("Received empty Json document.", json_doc); - return QJsonObject(); - } - - if (!json_doc.isObject()) { - Error("Json document is not an object.", json_doc); - return QJsonObject(); - } - - QJsonObject json_obj = json_doc.object(); - if (json_obj.isEmpty()) { - Error("Received empty Json object.", json_doc); - return QJsonObject(); - } - - return json_obj; - -} - -QJsonValue DeezerService::ExtractData(QByteArray &data) { - - QJsonObject json_obj = ExtractJsonObj(data); - if (json_obj.isEmpty()) return QJsonObject(); - - if (json_obj.contains("error")) { - QJsonValue json_value_error = json_obj["error"]; - if (!json_value_error.isObject()) { - Error("Error missing object", json_obj); - return QJsonValue(); - } - QJsonObject json_error = json_value_error.toObject(); - int code = json_error["code"].toInt(); - if (code == 300) Logout(); - QString message = json_error["message"].toString(); - QString type = json_error["type"].toString(); - Error(QString("%1 (%2)").arg(message).arg(code)); - return QJsonValue(); - } - - if (!json_obj.contains("data") && !json_obj.contains("DATA")) { - Error("Json reply is missing data.", json_obj); - return QJsonValue(); - } - - QJsonValue json_data; - if (json_obj.contains("data")) json_data = json_obj["data"]; - else json_data = json_obj["DATA"]; - - return json_data; - -} - -int DeezerService::Search(const QString &text, InternetSearch::SearchType searchby) { - - pending_search_id_ = next_pending_search_id_; - pending_search_text_ = text; - pending_search_type_ = searchby; - - next_pending_search_id_++; - - if (text.isEmpty()) { - timer_searchdelay_->stop(); - return pending_search_id_; - } - timer_searchdelay_->setInterval(searchdelay_); - timer_searchdelay_->start(); - - return pending_search_id_; - -} - -void DeezerService::StartSearch() { - - if (access_token_.isEmpty()) { - emit SearchError(pending_search_id_, "Not authenticated."); - next_pending_search_id_ = 1; - ShowConfig(); - return; - } - ClearSearch(); - search_id_ = pending_search_id_; - search_text_ = pending_search_text_; - - SendSearch(); - -} - -void DeezerService::CancelSearch() { - ClearSearch(); -} - -void DeezerService::ClearSearch() { - search_id_ = 0; - search_text_.clear(); - search_error_.clear(); - albums_requested_ = 0; - albums_received_ = 0; - requests_album_.clear(); - requests_song_.clear(); - songs_.clear(); -} - -void DeezerService::SendSearch() { - - emit UpdateStatus(tr("Searching...")); - - QList parameters; - parameters << Param("q", search_text_); - QString searchparam; - switch (pending_search_type_) { - case InternetSearch::SearchType_Songs: - searchparam = "search/track"; - parameters << Param("limit", QString::number(songssearchlimit_)); - break; - case InternetSearch::SearchType_Albums: - case InternetSearch::SearchType_Artists: - default: - searchparam = "search/album"; - parameters << Param("limit", QString::number(albumssearchlimit_)); - break; - } - - QNetworkReply *reply = CreateRequest(searchparam, parameters); - NewClosure(reply, SIGNAL(finished()), this, SLOT(SearchFinished(QNetworkReply*, int)), reply, search_id_); - -} - -void DeezerService::SearchFinished(QNetworkReply *reply, int id) { - - reply->deleteLater(); - - if (id != search_id_) return; - - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { - CheckFinish(); - return; - } - - QJsonValue json_value = ExtractData(data); - if (!json_value.isArray()) { - CheckFinish(); - return; - } - - QJsonArray json_data = json_value.toArray(); - if (json_data.isEmpty()) { - Error(tr("No match.")); - CheckFinish(); - return; - } - - for (const QJsonValue &value : json_data) { - - if (!value.isObject()) { - Error("Invalid Json reply, data is not an object.", value); - continue; - } - QJsonObject json_obj = value.toObject(); - - if (!json_obj.contains("id") || !json_obj.contains("type")) { - Error("Invalid Json reply, item is missing ID or type.", json_obj); - continue; - } - - QString type = json_obj["type"].toString(); - - if (!json_obj.contains("artist")) { - Error("Invalid Json reply, item missing artist.", 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("name")) { - Error("Invalid Json reply, artist data missing name.", json_artist); - continue; - } - QString artist = json_artist["name"].toString(); - int album_id(0); - QString album; - QString cover; - - if (type == "album") { - album_id = json_obj["id"].toInt(); - album = json_obj["title"].toString(); - cover = json_obj[coversize_].toString(); - } - else if (type == "track") { - - if (!json_obj.contains("album")) { - Error("Invalid Json reply, missing album data.", json_obj); - continue; - } - QJsonValue json_value_album = json_obj["album"]; - if (!json_value_album.isObject()) { - Error("Invalid Json reply, album data is not an 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, album data is missing ID or title.", json_album); - continue; - } - album_id = json_album["id"].toInt(); - album = json_album["title"].toString(); - cover = json_album[coversize_].toString(); - if (!fetchalbums_) { - Song song = ParseSong(album_id, album, artist, cover, value); - songs_ << song; - continue; - } - } - - DeezerAlbumContext *album_ctx; - if (requests_album_.contains(album_id)) { - album_ctx = requests_album_.value(album_id); - album_ctx->search_id = search_id_; - continue; - } - album_ctx = CreateAlbum(album_id, artist, album, cover); - GetAlbum(album_ctx); - albums_requested_++; - if (albums_requested_ >= albumssearchlimit_) break; - - } - - if (albums_requested_ > 0) { - if (albums_requested_ == 1) emit UpdateStatus(tr("Retrieving %1 album...").arg(albums_requested_)); - else emit UpdateStatus(tr("Retrieving %1 albums...").arg(albums_requested_)); - emit ProgressSetMaximum(albums_requested_); - emit UpdateProgress(0); - } - - CheckFinish(); - -} - -DeezerAlbumContext *DeezerService::CreateAlbum(const int album_id, const QString &artist, const QString &album, const QString &cover) { - - DeezerAlbumContext *album_ctx = new DeezerAlbumContext; - album_ctx->id = album_id; - album_ctx->artist = artist; - album_ctx->album = album; - album_ctx->cover = cover; - album_ctx->cover_url.setUrl(cover); - requests_album_.insert(album_id, album_ctx); - - return album_ctx; - - } - -void DeezerService::GetAlbum(const DeezerAlbumContext *album_ctx) { - - QList parameters; - QNetworkReply *reply = CreateRequest(QString("album/%1/tracks").arg(album_ctx->id), parameters); - NewClosure(reply, SIGNAL(finished()), this, SLOT(GetAlbumFinished(QNetworkReply*, int, int)), reply, search_id_, album_ctx->id); - -} - -void DeezerService::GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id) { - - reply->deleteLater(); - - if (!requests_album_.contains(album_id)) { - qLog(Error) << "Deezer: Got reply for cancelled album request: " << album_id; - CheckFinish(); - return; - } - DeezerAlbumContext *album_ctx = requests_album_.value(album_id); - - if (search_id != search_id_) { - if (album_ctx->search_id == search_id) delete requests_album_.take(album_ctx->id); - return; - } - - albums_received_++; - emit UpdateProgress(albums_received_); - - QByteArray data = GetReplyData(reply); - if (data.isEmpty()) { - delete requests_album_.take(album_ctx->id); - CheckFinish(); - return; - } - - QJsonValue json_value = ExtractData(data); - if (!json_value.isArray()) { - delete requests_album_.take(album_ctx->id); - CheckFinish(); - return; - } - - QJsonArray json_data = json_value.toArray(); - if (json_data.isEmpty()) { - delete requests_album_.take(album_ctx->id); - CheckFinish(); - return; - } - - bool compilation = false; - bool multidisc = false; - SongList songs; - for (const QJsonValue &value : json_data) { - Song song = ParseSong(album_ctx->id, album_ctx->album, album_ctx->artist, album_ctx->cover, value); - 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; - } - - delete requests_album_.take(album_ctx->id); - CheckFinish(); - -} - -Song DeezerService::ParseSong(const int album_id, const QString &album, const QString &album_artist, const QString &album_cover, const QJsonValue &value) { - - if (!value.isObject()) { - Error("Invalid Json reply, track is not an object.", value); - return Song(); - } - QJsonObject json_obj = value.toObject(); - - if ( - !json_obj.contains("id") || - !json_obj.contains("title") || - !json_obj.contains("artist") || - !json_obj.contains("duration") || - !json_obj.contains("preview") - ) { - Error("Invalid Json reply, track is missing one or more values.", json_obj); - return Song(); - } - - int song_id = json_obj["id"].toInt(); - QString title = json_obj["title"].toString(); - QJsonValue json_value_artist = json_obj["artist"]; - QVariant q_duration = json_obj["duration"].toVariant(); - int track(0); - if (json_obj.contains("track_position")) track = json_obj["track_position"].toInt(); - int disc(0); - if (json_obj.contains("disk_number")) disc = json_obj["disk_number"].toInt(); - QString preview = json_obj["preview"].toString(); - - if (!json_value_artist.isObject()) { - Error("Invalid Json reply, track artist is not an object.", json_value_artist); - return Song(); - } - QJsonObject json_artist = json_value_artist.toObject(); - if (!json_artist.contains("name")) { - Error("Invalid Json reply, track artist is missing name.", json_artist); - return Song(); - } - QString artist = json_artist["name"].toString(); - - Song song; - song.set_source(Song::Source_Deezer); - song.set_id(song_id); - song.set_album_id(album_id); - if (artist != album_artist) song.set_albumartist(album_artist); - song.set_artist(artist); - song.set_album(album); - song.set_title(title); - song.set_disc(disc); - song.set_track(track); - song.set_art_automatic(album_cover); - - QUrl url; - if (preview_) { - url.setUrl(preview); - quint64 duration = (30 * kNsecPerSec); - song.set_length_nanosec(duration); - } - else { - url.setScheme(url_handler_->scheme()); - url.setPath(QString("track/%1").arg(QString::number(song_id))); - if (q_duration.isValid()) { - quint64 duration = q_duration.toULongLong() * kNsecPerSec; - song.set_length_nanosec(duration); - } - } - song.set_url(url); - song.set_valid(true); - - return song; - -} - -bool DeezerService::GetStreamURL(const QUrl &original_url) { - -#ifdef HAVE_DZMEDIA - stream_request_url_ = original_url; - dzmedia_->GetStreamURL(original_url); - return true; -#else - stream_request_url_ = QUrl(); - return false; -#endif - -} - -#ifdef HAVE_DZMEDIA -void DeezerService::GetStreamURLFinished(const QUrl original_url, const QUrl media_url, const DZMedia::FileType dzmedia_filetype) { - - Song::FileType filetype(Song::FileType_Unknown); - - switch (dzmedia_filetype) { - case DZMedia::FileType_FLAC: - filetype = Song::FileType_FLAC; - break; - case DZMedia::FileType_MPEG: - filetype = Song::FileType_MPEG; - break; - case DZMedia::FileType_Stream: - filetype = Song::FileType_Stream; - break; - default: - filetype = Song::FileType_Unknown; - break; - } - stream_request_url_ = QUrl(); - emit StreamURLReceived(original_url, media_url, filetype); - -} -#endif - -void DeezerService::CheckFinish() { - - if (search_id_ == 0) return; - - if (albums_requested_ <= albums_received_) { - if (songs_.isEmpty()) { - if (search_error_.isEmpty()) emit SearchError(search_id_, "Unknown error"); - else emit SearchError(search_id_, search_error_); - } - else emit SearchResults(search_id_, songs_); - ClearSearch(); - } - -} - -void DeezerService::Error(QString error, QVariant debug) { - qLog(Error) << "Deezer:" << error; - if (debug.isValid()) qLog(Debug) << debug; - if (search_id_ != 0) { - if (!error.isEmpty()) { - search_error_ += error; - search_error_ += "
    "; - } - CheckFinish(); - } - if (!stream_request_url_.isEmpty()) { - emit StreamURLReceived(stream_request_url_, stream_request_url_, Song::FileType_Stream); - stream_request_url_ = QUrl(); - } -} diff --git a/src/deezer/deezerservice.h b/src/deezer/deezerservice.h deleted file mode 100644 index 6aecc99b..00000000 --- a/src/deezer/deezerservice.h +++ /dev/null @@ -1,162 +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 DEEZERSERVICE_H -#define DEEZERSERVICE_H - -#include "config.h" - -#ifdef HAVE_DZMEDIA -# include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "core/song.h" -#include "internet/internetservices.h" -#include "internet/internetservice.h" -#include "internet/internetsearch.h" - -class NetworkAccessManager; -class LocalRedirectServer; -class DeezerUrlHandler; - -struct DeezerAlbumContext { - int id; - int search_id; - QString artist; - QString album; - QString cover; - QUrl cover_url; -}; -Q_DECLARE_METATYPE(DeezerAlbumContext); - -class DeezerService : public InternetService { - Q_OBJECT - - public: - DeezerService(Application *app, QObject *parent); - ~DeezerService(); - - static const Song::Source kSource; - static const int kAppID; - - void ReloadSettings(); - - void Logout(); - int Search(const QString &query, InternetSearch::SearchType searchby); - void CancelSearch(); - - const bool app_id() { return kAppID; } - const bool authenticated() { return !access_token_.isEmpty(); } - - bool GetStreamURL(const QUrl &url); - - signals: - void Login(); - void LoginSuccess(); - void LoginFailure(QString failure_reason); - void Authenticated(); - void SearchResults(int id, SongList songs); - void SearchError(int id, QString message); - void UpdateStatus(QString text); - void ProgressSetMaximum(int max); - void UpdateProgress(int max); - void StreamURLReceived(const QUrl original_url, const QUrl media_url, const Song::FileType filetype); - - public slots: - void ShowConfig(); - - private slots: - void StartAuthorisation(); - void FetchAccessTokenFinished(QNetworkReply *reply); - void StartSearch(); - void SearchFinished(QNetworkReply *reply, int search_id); - void GetAlbumFinished(QNetworkReply *reply, int search_id, int album_id); -#ifdef HAVE_DZMEDIA - void GetStreamURLFinished(const QUrl original_url, const QUrl media_url, const DZMedia::FileType dzmedia_filetype); -#endif - - private: - void LoadAccessToken(); - void RedirectArrived(LocalRedirectServer *server, QUrl url); - void RequestAccessToken(const QByteArray &code); - void SetExpiryTime(int expires_in_seconds); - void ClearSearch(); - QNetworkReply *CreateRequest(const QString &ressource_name, const QList> ¶ms); - QByteArray GetReplyData(QNetworkReply *reply); - QJsonObject ExtractJsonObj(QByteArray &data); - QJsonValue ExtractData(QByteArray &data); - void SendSearch(); - DeezerAlbumContext *CreateAlbum(const int album_id, const QString &artist, const QString &album, const QString &cover); - void GetAlbum(const DeezerAlbumContext *album_ctx); - Song ParseSong(const int album_id, const QString &album, const QString &album_artist, const QString &album_cover, const QJsonValue &value); - void CheckFinish(); - void Error(QString error, QVariant debug = QString()); - - static const char *kApiUrl; - static const char *kOAuthUrl; - static const char *kOAuthAccessTokenUrl; - static const char *kOAuthRedirectUrl; - static const char *kSecretKey; - - NetworkAccessManager *network_; - DeezerUrlHandler *url_handler_; -#ifdef HAVE_DZMEDIA - DZMedia *dzmedia_; -#endif - QTimer *timer_searchdelay_; - - int searchdelay_; - int albumssearchlimit_; - int songssearchlimit_; - bool fetchalbums_; - QString coversize_; - bool preview_; - QString access_token_; - 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_; - QHash requests_album_; - QHash requests_song_; - int albums_requested_; - int albums_received_; - SongList songs_; - QString search_error_; - QUrl stream_request_url_; - -}; - -#endif // DEEZERSERVICE_H diff --git a/src/deezer/deezerurlhandler.cpp b/src/deezer/deezerurlhandler.cpp deleted file mode 100644 index 2e8afa9c..00000000 --- a/src/deezer/deezerurlhandler.cpp +++ /dev/null @@ -1,72 +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/iconloader.h" -#include "core/logging.h" -#include "core/song.h" -#include "deezer/deezerservice.h" -#include "deezerurlhandler.h" - -DeezerUrlHandler::DeezerUrlHandler( - Application *app, DeezerService *service) - : UrlHandler(service), app_(app), service_(service), task_id_(-1) { - - connect(service, SIGNAL(StreamURLReceived(QUrl, QUrl, Song::FileType)), this, SLOT(GetStreamURLFinished(QUrl, QUrl, Song::FileType))); - -} - -UrlHandler::LoadResult DeezerUrlHandler::StartLoading(const QUrl &url) { - - LoadResult ret(url); - if (task_id_ != -1) return ret; - last_original_url_ = url; - task_id_ = app_->task_manager()->StartTask(QString("Loading %1 stream...").arg(url.scheme())); - bool wait_for_url = service_->GetStreamURL(url); - if (wait_for_url) { - ret.type_ = LoadResult::WillLoadAsynchronously; - } - else { - CancelTask(); - ret.type_ = LoadResult::TrackAvailable; - ret.media_url_ = url; - } - return ret; - -} - -void DeezerUrlHandler::GetStreamURLFinished(QUrl original_url, QUrl media_url, Song::FileType filetype) { - - if (task_id_ == -1) return; - CancelTask(); - emit AsyncLoadComplete(LoadResult(original_url, LoadResult::TrackAvailable, media_url, filetype)); - -} - -void DeezerUrlHandler::CancelTask() { - - app_->task_manager()->SetTaskFinished(task_id_); - task_id_ = -1; - -} diff --git a/src/deezer/deezerurlhandler.h b/src/deezer/deezerurlhandler.h deleted file mode 100644 index e17f9dc9..00000000 --- a/src/deezer/deezerurlhandler.h +++ /dev/null @@ -1,56 +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 DEEZERURLHANDLER_H -#define DEEZERURLHANDLER_H - -#include -#include -#include - -#include "core/urlhandler.h" -#include "core/song.h" -#include "deezer/deezerservice.h" - -class Application; -class DeezerService; - -class DeezerUrlHandler : public UrlHandler { - Q_OBJECT - - public: - DeezerUrlHandler(Application *app, DeezerService *service); - - QString scheme() const { return service_->url_scheme(); } - LoadResult StartLoading(const QUrl &url); - - void CancelTask(); - - private slots: - void GetStreamURLFinished(QUrl original_url, QUrl media_url, Song::FileType filetype); - - private: - Application *app_; - DeezerService *service_; - int task_id_; - QUrl last_original_url_; - -}; - -#endif diff --git a/src/engine/deezerengine.cpp b/src/engine/deezerengine.cpp deleted file mode 100644 index 9bd2b607..00000000 --- a/src/engine/deezerengine.cpp +++ /dev/null @@ -1,536 +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 "version.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "core/timeconstants.h" -#include "core/taskmanager.h" -#include "core/logging.h" -#include "core/song.h" -#include "engine_fwd.h" -#include "enginebase.h" -#include "enginetype.h" -#include "deezerengine.h" -#include "deezer/deezerservice.h" -#include "settings/deezersettingspage.h" - -const char *DeezerEngine::kAppID = "303684"; -const char *DeezerEngine::kProductID = "strawberry"; -const char *DeezerEngine::kProductVersion = STRAWBERRY_VERSION_DISPLAY; - -DeezerEngine::DeezerEngine(TaskManager *task_manager) - : EngineBase(), - state_(Engine::Empty), - position_(0), - stopping_(false) { - - type_ = Engine::Deezer; - -} - -DeezerEngine::~DeezerEngine() { - - if (player_) { - dz_object_release((dz_object_handle) player_); - player_ = nullptr; - } - - if (connect_) { - dz_object_release((dz_object_handle) connect_); - connect_ = nullptr; - } - -} - -bool DeezerEngine::Init() { - - qLog(Debug) << "Deezer native SDK Version:" << dz_connect_get_build_id() << QCoreApplication::applicationName().toUtf8(); - - struct dz_connect_configuration config; - memset(&config, 0, sizeof(struct dz_connect_configuration)); - - config.app_id = kAppID; - config.product_id = kProductID; - config.product_build_id = kProductVersion; - config.connect_event_cb = ConnectEventCallback; - - connect_ = dz_connect_new(&config); - if (!connect_) { - qLog(Error) << "Deezer: Failed to create connect."; - return false; - } - - qLog(Debug) << "Device ID:" << dz_connect_get_device_id(connect_); - - dz_error_t dzerr(DZ_ERROR_NO_ERROR); - - dzerr = dz_connect_debug_log_disable(connect_); - if (dzerr != DZ_ERROR_NO_ERROR) { - qLog(Error) << "Deezer: Failed to disable debug log."; - return false; - } - - dzerr = dz_connect_activate(connect_, this); - if (dzerr != DZ_ERROR_NO_ERROR) { - qLog(Error) << "Deezer: Failed to activate connect."; - return false; - } - - dz_connect_cache_path_set(connect_, nullptr, nullptr, QStandardPaths::writableLocation(QStandardPaths::CacheLocation).toUtf8().constData()); - - player_ = dz_player_new(connect_); - if (!player_) { - qLog(Error) << "Deezer: Failed to create player."; - return false; - } - - dzerr = dz_player_activate(player_, this); - if (dzerr != DZ_ERROR_NO_ERROR) { - qLog(Error) << "Deezer: Failed to activate player."; - return false; - } - - dzerr = dz_player_set_event_cb(player_, PlayerEventCallback); - if (dzerr != DZ_ERROR_NO_ERROR) { - qLog(Error) << "Deezer: Failed to set event callback."; - return false; - } - - dzerr = dz_player_set_metadata_cb(player_, PlayerMetaDataCallback); - if (dzerr != DZ_ERROR_NO_ERROR) { - qLog(Error) << "Deezer: Failed to set metadata callback."; - return false; - } - - dzerr = dz_player_set_render_progress_cb(player_, PlayerProgressCallback, 1000); - if (dzerr != DZ_ERROR_NO_ERROR) { - qLog(Error) << "Deezer: Failed to set progress callback."; - return false; - } - - dzerr = dz_player_set_crossfading_duration(player_, nullptr, nullptr, 3000); - if (dzerr != DZ_ERROR_NO_ERROR) { - qLog(Error) << "Deezer: Failed to set crossfade duration."; - return false; - } - - dzerr = dz_connect_offline_mode(connect_, nullptr, nullptr, false); - if (dzerr != DZ_ERROR_NO_ERROR) { - qLog(Error) << "Deezer: Failed to set offline mode."; - return false; - } - - LoadAccessToken(); - ReloadSettings(); - - return true; - -} - -void DeezerEngine::ReloadSettings() { - - QSettings s; - s.beginGroup(DeezerSettingsPage::kSettingsGroup); - QString quality = s.value("quality", "FLAC").toString(); - s.endGroup(); - dz_error_t dzerr; - - if (quality == "MP3_128") - dzerr = dz_player_set_track_quality(player_, nullptr, nullptr, DZ_TRACK_QUALITY_STANDARD); - else if (quality == "MP3_320") - dzerr = dz_player_set_track_quality(player_, nullptr, nullptr, DZ_TRACK_QUALITY_HIGHQUALITY); - else if (quality == "FLAC") - dzerr = dz_player_set_track_quality(player_, nullptr, nullptr, DZ_TRACK_QUALITY_CDQUALITY); - else if (quality == "DATA_EFFICIENT") - dzerr = dz_player_set_track_quality(player_, nullptr, nullptr, DZ_TRACK_QUALITY_DATA_EFFICIENT); - else - dzerr = dz_player_set_track_quality(player_, nullptr, nullptr, DZ_TRACK_QUALITY_CDQUALITY); - - if (dzerr != DZ_ERROR_NO_ERROR) { - qLog(Error) << "Deezer: Failed to set quality."; - } - -} - -bool DeezerEngine::Initialised() const { - - if (connect_ && player_) return true; - return false; - -} - -void DeezerEngine::LoadAccessToken() { - - QSettings s; - s.beginGroup(DeezerSettingsPage::kSettingsGroup); - if (!s.contains("access_token") || !s.contains("expiry_time")) return; - access_token_ = s.value("access_token").toString(); - expiry_time_ = s.value("expiry_time").toDateTime(); - s.endGroup(); - - dz_error_t dzerr = dz_connect_set_access_token(connect_, nullptr, nullptr, access_token_.toUtf8().constData()); - if (dzerr != DZ_ERROR_NO_ERROR) { - qLog(Error) << "Deezer: Failed to set access token."; - } - -} - -bool DeezerEngine::Load(const QUrl &media_url, const QUrl &original_url, Engine::TrackChangeFlags change, bool force_stop_at_end, quint64 beginning_nanosec, qint64 end_nanosec) { - - if (!Initialised()) return false; - stopping_ = false; - - Engine::Base::Load(media_url, original_url, change, force_stop_at_end, beginning_nanosec, end_nanosec); - dz_error_t dzerr = dz_player_load(player_, nullptr, nullptr, media_url.toString().toUtf8().constData()); - if (dzerr != DZ_ERROR_NO_ERROR) return false; - - return true; - -} - -bool DeezerEngine::Play(quint64 offset_nanosec) { - - if (!Initialised()) return false; - stopping_ = false; - - dz_error_t dzerr(DZ_ERROR_NO_ERROR); - if (state() == Engine::Paused) dzerr = dz_player_resume(player_, nullptr, nullptr); - else dzerr = dz_player_play(player_, nullptr, nullptr, DZ_PLAYER_PLAY_CMD_START_TRACKLIST, DZ_INDEX_IN_QUEUELIST_CURRENT); - if (dzerr != DZ_ERROR_NO_ERROR) return false; - - Seek(offset_nanosec); - - return true; - -} - -void DeezerEngine::Stop(bool stop_after) { - - if (!Initialised()) return; - stopping_ = true; - - dz_error_t dzerr = dz_player_stop(player_, nullptr, nullptr); - if (dzerr != DZ_ERROR_NO_ERROR) return; - -} - -void DeezerEngine::Pause() { - - if (!Initialised()) return; - - dz_error_t dzerr = dz_player_pause(player_, nullptr, nullptr); - if (dzerr != DZ_ERROR_NO_ERROR) return; - - state_ = Engine::Paused; - emit StateChanged(state_); - -} - -void DeezerEngine::Unpause() { - - if (!Initialised()) return; - dz_error_t dzerr = dz_player_resume(player_, nullptr, nullptr); - if (dzerr != DZ_ERROR_NO_ERROR) return; - -} - -void DeezerEngine::Seek(quint64 offset_nanosec) { - - if (!Initialised()) return; - - stopping_ = false; - dz_useconds_t offset = (offset_nanosec / kNsecPerUsec); - dz_error_t dzerr = dz_player_seek(player_, nullptr, nullptr, offset); - if (dzerr != DZ_ERROR_NO_ERROR) return; - -} - -void DeezerEngine::SetVolumeSW(uint percent) { - - if (!Initialised()) return; - - dz_error_t dzerr = dz_player_set_output_volume(player_, nullptr, nullptr, percent); - if (dzerr != DZ_ERROR_NO_ERROR) qLog(Error) << "Deezer: Failed to set volume."; - -} - -qint64 DeezerEngine::position_nanosec() const { - - if (state() == Engine::Empty) return 0; - const qint64 result = (position_ * kNsecPerUsec); - return qint64(qMax(0ll, result)); - -} - -qint64 DeezerEngine::length_nanosec() const { - - if (state() == Engine::Empty) return 0; - const qint64 result = (end_nanosec_ - beginning_nanosec_); - return result; - -} - -EngineBase::OutputDetailsList DeezerEngine::GetOutputsList() const { - OutputDetailsList ret; - OutputDetails output; - output.name = "default"; - output.description = "Default"; - output.iconname = "soundcard"; - ret << output; - return ret; -} - -bool DeezerEngine::ValidOutput(const QString &output) { - return(true); -} - -bool DeezerEngine::CustomDeviceSupport(const QString &output) { - return false; -} - -bool DeezerEngine::ALSADeviceSupport(const QString &output) { - return false; -} - -bool DeezerEngine::CanDecode(const QUrl &url) { - if (url.scheme() == "dzmedia") return true; - else return false; -} - -void DeezerEngine::ConnectEventCallback(dz_connect_handle handle, dz_connect_event_handle event, void *delegate) { - - dz_connect_event_t type = dz_connect_event_get_type(event); - //DeezerEngine *engine = reinterpret_cast(delegate); - - switch (type) { - case DZ_CONNECT_EVENT_USER_OFFLINE_AVAILABLE: - qLog(Debug) << "CONNECT_EVENT USER_OFFLINE_AVAILABLE"; - break; - - case DZ_CONNECT_EVENT_USER_ACCESS_TOKEN_OK: { - const char* szAccessToken; - szAccessToken = dz_connect_event_get_access_token(event); - qLog(Debug) << "CONNECT_EVENT USER_ACCESS_TOKEN_OK Access_token :" << szAccessToken; - } - break; - - case DZ_CONNECT_EVENT_USER_ACCESS_TOKEN_FAILED: - qLog(Debug) << "CONNECT_EVENT USER_ACCESS_TOKEN_FAILED"; - break; - - case DZ_CONNECT_EVENT_USER_LOGIN_OK: - qLog(Debug) << "Deezer CONNECT_EVENT USER_LOGIN_OK"; - break; - - case DZ_CONNECT_EVENT_USER_NEW_OPTIONS: - qLog(Debug) << "Deezer: CONNECT_EVENT USER_NEW_OPTIONS"; - break; - - case DZ_CONNECT_EVENT_USER_LOGIN_FAIL_NETWORK_ERROR: - qLog(Debug) << "Deezer: CONNECT_EVENT USER_LOGIN_FAIL_NETWORK_ERROR"; - break; - - case DZ_CONNECT_EVENT_USER_LOGIN_FAIL_BAD_CREDENTIALS: - qLog(Debug) << "Deezer: CONNECT_EVENT USER_LOGIN_FAIL_BAD_CREDENTIALS"; - break; - - case DZ_CONNECT_EVENT_USER_LOGIN_FAIL_USER_INFO: - qLog(Debug) << "Deezer: CONNECT_EVENT USER_LOGIN_FAIL_USER_INFO"; - break; - - case DZ_CONNECT_EVENT_USER_LOGIN_FAIL_OFFLINE_MODE: - qLog(Debug) << "Deezer: CONNECT_EVENT USER_LOGIN_FAIL_OFFLINE_MODE"; - break; - - case DZ_CONNECT_EVENT_ADVERTISEMENT_START: - qLog(Debug) << "Deezer: CONNECT_EVENTADVERTISEMENT_START"; - break; - - case DZ_CONNECT_EVENT_ADVERTISEMENT_STOP: - qLog(Debug) << "Deezer: CONNECT_EVENTADVERTISEMENT_STOP"; - break; - - case DZ_CONNECT_EVENT_UNKNOWN: - default: - qLog(Debug) << "Deezer: CONNECT_EVENTUNKNOWN or default (type =" << type; - break; - } - -} - - -void DeezerEngine::PlayerEventCallback(dz_player_handle handle, dz_player_event_handle event, void *supervisor) { - - DeezerEngine *engine = reinterpret_cast(supervisor); - dz_streaming_mode_t streaming_mode; - dz_index_in_queuelist idx; - dz_player_event_t type = dz_player_event_get_type(event); - - if (!dz_player_event_get_queuelist_context(event, &streaming_mode, &idx)) { - streaming_mode = DZ_STREAMING_MODE_ONDEMAND; - idx = DZ_INDEX_IN_QUEUELIST_INVALID; - } - - switch (type) { - - case DZ_PLAYER_EVENT_LIMITATION_FORCED_PAUSE: - break; - - case DZ_PLAYER_EVENT_QUEUELIST_LOADED: - break; - - case DZ_PLAYER_EVENT_QUEUELIST_NO_RIGHT: - break; - - case DZ_PLAYER_EVENT_QUEUELIST_NEED_NATURAL_NEXT: - break; - - case DZ_PLAYER_EVENT_QUEUELIST_TRACK_NOT_AVAILABLE_OFFLINE: - engine->state_ = Engine::Error; - emit engine->StateChanged(engine->state_); - emit engine->InvalidSongRequested(engine->media_url_); - emit engine->Error("Track not available offline."); - break; - - case DZ_PLAYER_EVENT_QUEUELIST_TRACK_RIGHTS_AFTER_AUDIOADS: - break; - - case DZ_PLAYER_EVENT_QUEUELIST_SKIP_NO_RIGHT: - break; - - case DZ_PLAYER_EVENT_QUEUELIST_TRACK_SELECTED: - break; - - case DZ_PLAYER_EVENT_MEDIASTREAM_DATA_READY: - break; - - case DZ_PLAYER_EVENT_MEDIASTREAM_DATA_READY_AFTER_SEEK: - break; - - case DZ_PLAYER_EVENT_RENDER_TRACK_START_FAILURE: - engine->state_ = Engine::Error; - emit engine->StateChanged(engine->state_); - emit engine->InvalidSongRequested(engine->media_url_); - emit engine->Error("Track start failure."); - break; - - case DZ_PLAYER_EVENT_RENDER_TRACK_START: - engine->state_ = Engine::Playing; - engine->position_ = 0; - emit engine->StateChanged(engine->state_); - break; - - case DZ_PLAYER_EVENT_RENDER_TRACK_END: - engine->state_ = Engine::Idle; - engine->position_ = 0; - emit engine->TrackEnded(); - break; - - case DZ_PLAYER_EVENT_RENDER_TRACK_PAUSED: - engine->state_ = Engine::Paused; - emit engine->StateChanged(engine->state_); - break; - - case DZ_PLAYER_EVENT_RENDER_TRACK_UNDERFLOW: - break; - - case DZ_PLAYER_EVENT_RENDER_TRACK_RESUMED: - engine->state_ = Engine::Playing; - emit engine->StateChanged(engine->state_); - break; - - case DZ_PLAYER_EVENT_RENDER_TRACK_SEEKING: - break; - - case DZ_PLAYER_EVENT_RENDER_TRACK_REMOVED: - if (!engine->stopping_) return; - engine->state_ = Engine::Empty; - engine->position_ = 0; - emit engine->StateChanged(engine->state_); - break; - - case DZ_PLAYER_EVENT_UNKNOWN: - default: - qLog(Error) << "Deezer: Unknown player event" << type; - break; - } - -} - -void DeezerEngine::PlayerProgressCallback(dz_player_handle handle, dz_useconds_t progress, void *userdata) { - DeezerEngine *engine = reinterpret_cast(userdata); - engine->position_ = progress; -} - -void DeezerEngine::PlayerMetaDataCallback(dz_player_handle handle, dz_track_metadata_handle metadata, void *userdata) { - - const dz_media_track_detailed_infos_t *track_metadata = dz_track_metadata_get_format_header(metadata); - DeezerEngine *engine = reinterpret_cast(userdata); - Engine::SimpleMetaBundle bundle; - - switch (track_metadata->format) { - case DZ_MEDIA_FORMAT_AUDIO_MPEG: - bundle.filetype = Song::FileType_MPEG; - break; - case DZ_MEDIA_FORMAT_AUDIO_FLAC: - bundle.filetype = Song::FileType_FLAC; - break; - case DZ_MEDIA_FORMAT_AUDIO_PCM: - bundle.filetype = Song::FileType_PCM; - break; - default: - return; - } - - bundle.url = engine->original_url_; - bundle.title = QString(); - bundle.artist = QString(); - bundle.comment = QString(); - bundle.album = QString(); - bundle.length = 0; - bundle.year = 0; - bundle.tracknr = 0; - bundle.samplerate = track_metadata->audio.samples.sample_rate; - bundle.bitdepth = 0; - bundle.bitrate = track_metadata->average_bitrate / 1000; - bundle.lyrics = QString(); - - emit engine->MetaData(bundle); - -} diff --git a/src/engine/deezerengine.h b/src/engine/deezerengine.h deleted file mode 100644 index 1780ef5d..00000000 --- a/src/engine/deezerengine.h +++ /dev/null @@ -1,94 +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 DEEZERENGINE_H -#define DEEZERENGINE_H - -#include "config.h" - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "engine_fwd.h" -#include "enginebase.h" - -class TaskManager; - -class DeezerEngine : public Engine::Base { - Q_OBJECT - - public: - DeezerEngine(TaskManager *task_manager); - ~DeezerEngine(); - - bool Init(); - void ReloadSettings(); - Engine::State state() const { return state_; } - bool Load(const QUrl &media_url, const QUrl &original_url, Engine::TrackChangeFlags change, bool force_stop_at_end, quint64 beginning_nanosec, qint64 end_nanosec); - bool Play(quint64 offset_nanosec); - void Stop(bool stop_after = false); - void Pause(); - void Unpause(); - void Seek(quint64 offset_nanosec); - protected: - void SetVolumeSW(uint percent); - public: - virtual qint64 position_nanosec() const; - virtual qint64 length_nanosec() const; - - OutputDetailsList GetOutputsList() const; - bool ValidOutput(const QString &output); - QString DefaultOutput() { return ""; } - bool CustomDeviceSupport(const QString &output); - bool ALSADeviceSupport(const QString &output); - - private: - static const char *kAppID; - static const char *kProductVersion; - static const char *kProductID; - static const char *kPath; - Engine::State state_; - dz_connect_handle connect_; - dz_player_handle player_; - QString access_token_; - QDateTime expiry_time_; - qint64 position_; - bool stopping_; - - bool Initialised() const; - bool CanDecode(const QUrl &url); - - static void ConnectEventCallback(dz_connect_handle handle, dz_connect_event_handle event, void *delegate); - static void PlayerEventCallback(dz_player_handle handle, dz_player_event_handle event, void *supervisor); - static void PlayerMetaDataCallback(dz_player_handle handle, dz_track_metadata_handle metadata, void *userdata); - static void PlayerProgressCallback(dz_player_handle handle, dz_useconds_t progress, void *userdata); - - public slots: - void LoadAccessToken(); - -}; - -#endif diff --git a/src/engine/enginetype.cpp b/src/engine/enginetype.cpp index 4096639f..0a4db9fc 100644 --- a/src/engine/enginetype.cpp +++ b/src/engine/enginetype.cpp @@ -32,7 +32,6 @@ Engine::EngineType EngineTypeFromName(QString enginename) { else if (lower == "xine") return Engine::Xine; else if (lower == "vlc") return Engine::VLC; else if (lower == "phonon") return Engine::Phonon; - else if (lower == "deezer") return Engine::Deezer; else return Engine::None; } @@ -42,7 +41,6 @@ QString EngineName(Engine::EngineType enginetype) { case Engine::Xine: return QString("xine"); case Engine::VLC: return QString("vlc"); case Engine::Phonon: return QString("phonon"); - case Engine::Deezer: return QString("deezer"); case Engine::None: default: return QString("None"); } @@ -54,7 +52,6 @@ QString EngineDescription(Engine::EngineType enginetype) { case Engine::Xine: return QString("Xine"); case Engine::VLC: return QString("VLC"); case Engine::Phonon: return QString("Phonon"); - case Engine::Deezer: return QString("Deezer"); case Engine::None: default: return QString("None"); diff --git a/src/engine/enginetype.h b/src/engine/enginetype.h index 4039a4ad..71435eea 100644 --- a/src/engine/enginetype.h +++ b/src/engine/enginetype.h @@ -32,8 +32,7 @@ enum EngineType { GStreamer, VLC, Xine, - Phonon, - Deezer + Phonon }; Engine::EngineType EngineTypeFromName(QString enginename); diff --git a/src/playlist/playlistview.h b/src/playlist/playlistview.h index 4ad38b18..e0802df8 100644 --- a/src/playlist/playlistview.h +++ b/src/playlist/playlistview.h @@ -75,7 +75,7 @@ class PlaylistHeader; // that uses Gtk to paint row backgrounds, ignoring any custom brush or palette the caller set in the QStyleOption. // That breaks our currently playing track animation, which relies on the background painted by Qt to be transparent. // This proxy style uses QCommonStyle to paint the affected elements. -// This class is used by tidal and deezer search view as well. +// This class is used by internet search view as well. class PlaylistProxyStyle : public QProxyStyle { public: PlaylistProxyStyle(QStyle *base); diff --git a/src/settings/backendsettingspage.cpp b/src/settings/backendsettingspage.cpp index 8f80a9ab..b7b262da 100644 --- a/src/settings/backendsettingspage.cpp +++ b/src/settings/backendsettingspage.cpp @@ -94,9 +94,6 @@ void BackendSettingsPage::Load() { #ifdef HAVE_PHONON ui_->combobox_engine->addItem(IconLoader::Load("speaker"), EngineDescription(Engine::Phonon), QVariant::fromValue(Engine::Phonon)); #endif -#ifdef HAVE_DEEZER - ui_->combobox_engine->addItem(IconLoader::Load("deezer"), EngineDescription(Engine::Deezer), QVariant::fromValue(Engine::Deezer)); -#endif enginetype_current_ = enginetype; output_current_ = s_.value("output", QString()).toString(); diff --git a/src/settings/deezersettingspage.cpp b/src/settings/deezersettingspage.cpp deleted file mode 100644 index cae30c45..00000000 --- a/src/settings/deezersettingspage.cpp +++ /dev/null @@ -1,142 +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 "deezersettingspage.h" -#include "ui_deezersettingspage.h" -#include "core/application.h" -#include "core/iconloader.h" -#include "internet/internetservices.h" -#include "deezer/deezerservice.h" - -const char *DeezerSettingsPage::kSettingsGroup = "Deezer"; - -DeezerSettingsPage::DeezerSettingsPage(SettingsDialog *parent) - : SettingsPage(parent), - ui_(new Ui::DeezerSettingsPage), - service_(dialog()->app()->internet_services()->Service()) { - - ui_->setupUi(this); - setWindowIcon(IconLoader::Load("deezer")); - - connect(ui_->button_login, SIGNAL(clicked()), SLOT(LoginClicked())); - connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked())); - - connect(this, SIGNAL(Login()), service_, SLOT(StartAuthorisation())); - - connect(service_, SIGNAL(LoginFailure(QString)), SLOT(LoginFailure(QString))); - connect(service_, SIGNAL(LoginSuccess()), SLOT(LoginSuccess())); - - dialog()->installEventFilter(this); - - ui_->combobox_quality->addItem("MP3 128kbps \"Standard\"", "MP3_128"); - ui_->combobox_quality->addItem("MP3 320kbps \"High Quality\"", "MP3_320"); - ui_->combobox_quality->addItem("FLAC \"CD Quality\"", "FLAC"); - ui_->combobox_quality->addItem("\"Data Efficient\"", "DATA_EFFICIENT"); - - ui_->combobox_coversize->addItem("Small", "cover_small"); - ui_->combobox_coversize->addItem("Medium", "cover_medium"); - ui_->combobox_coversize->addItem("Big", "cover_big"); - ui_->combobox_coversize->addItem("XL", "cover_xl"); - -} - -DeezerSettingsPage::~DeezerSettingsPage() { delete ui_; } - -void DeezerSettingsPage::Load() { - - QSettings s; - - s.beginGroup(kSettingsGroup); - ui_->checkbox_enable->setChecked(s.value("enabled", false).toBool()); - dialog()->ComboBoxLoadFromSettings(s, ui_->combobox_quality, "quality", "FLAC"); - ui_->spinbox_searchdelay->setValue(s.value("searchdelay", 1500).toInt()); - ui_->spinbox_albumssearchlimit->setValue(s.value("albumssearchlimit", 100).toInt()); - ui_->spinbox_songssearchlimit->setValue(s.value("songssearchlimit", 100).toInt()); - ui_->checkbox_fetchalbums->setChecked(s.value("fetchalbums", false).toBool()); - dialog()->ComboBoxLoadFromSettings(s, ui_->combobox_coversize, "coversize", "cover_big"); -#if defined(HAVE_DEEZER) || defined(HAVE_DZMEDIA) - bool preview(false); -#else - bool preview(true); -#endif - ui_->checkbox_preview->setChecked(s.value("preview", preview).toBool()); - s.endGroup(); - - if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); - else ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); - -} - -void DeezerSettingsPage::Save() { - - QSettings s; - s.beginGroup(kSettingsGroup); - s.setValue("enabled", ui_->checkbox_enable->isChecked()); - s.setValue("quality", ui_->combobox_quality->itemData(ui_->combobox_quality->currentIndex())); - s.setValue("searchdelay", ui_->spinbox_searchdelay->value()); - s.setValue("albumssearchlimit", ui_->spinbox_albumssearchlimit->value()); - s.setValue("songssearchlimit", ui_->spinbox_songssearchlimit->value()); - s.setValue("fetchalbums", ui_->checkbox_fetchalbums->isChecked()); - s.setValue("coversize", ui_->combobox_coversize->itemData(ui_->combobox_coversize->currentIndex())); - s.setValue("preview", ui_->checkbox_preview->isChecked()); - s.endGroup(); - - service_->ReloadSettings(); - -} - -void DeezerSettingsPage::LoginClicked() { - emit Login(); - ui_->button_login->setEnabled(false); -} - -bool DeezerSettingsPage::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 DeezerSettingsPage::LogoutClicked() { - service_->Logout(); - ui_->button_login->setEnabled(true); - ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut); -} - -void DeezerSettingsPage::LoginSuccess() { - if (!this->isVisible()) return; - ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn); - ui_->button_login->setEnabled(false); -} - -void DeezerSettingsPage::LoginFailure(QString failure_reason) { - if (!this->isVisible()) return; - QMessageBox::warning(this, tr("Authentication failed"), failure_reason); -} diff --git a/src/settings/deezersettingspage.h b/src/settings/deezersettingspage.h deleted file mode 100644 index 19c8c06b..00000000 --- a/src/settings/deezersettingspage.h +++ /dev/null @@ -1,60 +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 DEEZERSETTINGSPAGE_H -#define DEEZERSETTINGSPAGE_H - -#include -#include -#include - -#include "settings/settingspage.h" - -class DeezerService; -class Ui_DeezerSettingsPage; - -class DeezerSettingsPage : public SettingsPage { - Q_OBJECT - - public: - explicit DeezerSettingsPage(SettingsDialog* parent = nullptr); - ~DeezerSettingsPage(); - - static const char *kSettingsGroup; - - void Load(); - void Save(); - - bool eventFilter(QObject *object, QEvent *event); - -signals: - void Login(); - - private slots: - void LoginClicked(); - void LogoutClicked(); - void LoginSuccess(); - void LoginFailure(QString failure_reason); - - private: - Ui_DeezerSettingsPage* ui_; - DeezerService *service_; -}; - -#endif diff --git a/src/settings/deezersettingspage.ui b/src/settings/deezersettingspage.ui deleted file mode 100644 index d84014da..00000000 --- a/src/settings/deezersettingspage.ui +++ /dev/null @@ -1,354 +0,0 @@ - - - DeezerSettingsPage - - - - 0 - 0 - 715 - 575 - - - - Deezer - - - - - - Enable - - - - - - - - 0 - 0 - - - - Authentication - - - - - - - - Login - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - - - - - - - - Preferences - - - - - - - - - 150 - 0 - - - - Audio quality - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - 150 - 0 - - - - Search delay - - - - - - - ms - - - 0 - - - 10000 - - - 50 - - - 1500 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - 150 - 0 - - - - Albums search limit - - - - - - - 1 - - - 1000 - - - 50 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - 150 - 0 - - - - Songs search limit - - - - - - - 1 - - - 1000 - - - 50 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Fetch entire albums when searching songs - - - - - - - - - - 150 - 0 - - - - Album cover size - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Use 30 seconds preview streams - - - - - - - - - - Qt::Vertical - - - - 20 - 30 - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 200 - 62 - - - - - 200 - 62 - - - - :/pictures/deezer.png - - - - - - - - - - LoginStateWidget - QWidget -
    widgets/loginstatewidget.h
    - 1 -
    -
    - - - - - -
    diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp index 2395f9de..9cdf9747 100644 --- a/src/settings/settingsdialog.cpp +++ b/src/settings/settingsdialog.cpp @@ -65,9 +65,6 @@ #ifdef HAVE_STREAM_TIDAL # include "tidalsettingspage.h" #endif -#ifdef HAVE_STREAM_DEEZER -# include "deezersettingspage.h" -#endif #include "ui_settingsdialog.h" @@ -136,15 +133,12 @@ SettingsDialog::SettingsDialog(Application *app, QWidget *parent) AddPage(Page_GlobalShortcuts, new GlobalShortcutsSettingsPage(this), iface); #endif -#if defined(HAVE_STREAM_TIDAL) || defined(HAVE_STREAM_DEEZER) +#if defined(HAVE_STREAM_TIDAL) QTreeWidgetItem *streaming = AddCategory(tr("Streaming")); #endif #ifdef HAVE_STREAM_TIDAL AddPage(Page_Tidal, new TidalSettingsPage(this), streaming); #endif -#ifdef HAVE_STREAM_DEEZER - AddPage(Page_Deezer, new DeezerSettingsPage(this), streaming); -#endif // List box connect(ui_->list, SIGNAL(currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)), SLOT(CurrentItemChanged(QTreeWidgetItem*))); diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h index 901b930d..024f62cf 100644 --- a/src/settings/settingsdialog.h +++ b/src/settings/settingsdialog.h @@ -83,7 +83,6 @@ public: Page_Proxy, Page_Scrobbler, Page_Tidal, - Page_Deezer, }; enum Role {