From e69c93257716f916d020c9549c311e432a315a80 Mon Sep 17 00:00:00 2001 From: Arnaud Bienner Date: Sat, 4 Aug 2012 15:21:44 +0200 Subject: [PATCH 1/5] Update issue 2540 Add basic SoundCloud support. However, only search in internet tab + listen is done for now. We should at least add soundcloud in global search. --- data/data.qrc | 1 + data/providers/soundcloud.png | Bin 0 -> 25751 bytes src/CMakeLists.txt | 2 + src/internet/internetmodel.cpp | 4 +- src/internet/soundcloudservice.cpp | 248 +++++++++++++++++++++++++++++ src/internet/soundcloudservice.h | 90 +++++++++++ 6 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 data/providers/soundcloud.png create mode 100644 src/internet/soundcloudservice.cpp create mode 100644 src/internet/soundcloudservice.h diff --git a/data/data.qrc b/data/data.qrc index 91bbd7ec9..19608fa5b 100644 --- a/data/data.qrc +++ b/data/data.qrc @@ -289,6 +289,7 @@ providers/skyfm.png providers/somafm.png providers/songkick.png + providers/soundcloud.png providers/twitter.png providers/wikipedia.png sample.mood diff --git a/data/providers/soundcloud.png b/data/providers/soundcloud.png new file mode 100644 index 0000000000000000000000000000000000000000..034972f2f7101dab08342cf9d9830b72cbdb9054 GIT binary patch literal 25751 zcmV(`K-0g8P){|ztf^qpg#0H(Hf22nsOIivPXKCp82}Uj015yA1pt5o06+l% zpa1~0vH=^)j{qOaCd}R69By?F@H_W?paA9wynWBGKMy=81Q+|VrO;Lkd=hpOWgt)z zJn=>7#Wb_!%AhS6dJEUd9wzWS_%5#RmI0ptHt5jnBiS_N2qI7lAzBtnIVTW3Fa~i%>r-opbi#_H_tW86{Od}aD z>jwr#Dg?yLqx1UJ&|7scS7jgDQJcJa*ZB7HryIK`n`T==Gzowj)@*24Omhr9muSCV z<|Yt^80UvrkJoe~OUw^5ckSOTV^{a@sva+B5`kau`X~Ai;Ny$#$g3ya3s1k+`JbB~ zoqMJNbXRcoWod$lz}r>;1pXnxw_>Wz-2Q>_voGB`e&CFD{i4Z+xu}ttZIys=6|^ga zomW0?^ld!M+%+9L`gZB_Kxe+|AAY6d9(v}rlMj97q1gwY>R>SjUQD(50x^JGMF`$j z0-(LVfL{T9l70T>_3JM?Z@T%RsfN9{Va=J#0gB7uTcbV=)bMMJzHQWNj;@`SIcF~Q z2VYo7zx~K7Z#;O@q4~oB_Ef;vrbJ+b(Eu^?sB(QH76GrX$(`4%-+$4T#+4h!YL_)F z?a0Ew@jbJGp5H9+dG%9={|A0#9$yMa6s{ElalGU2dF+*iJOASF@gIKUHOvJeNJ~V) zi;YGEh?z$b@|!$wpS8QLoqErKtz%c8(W+fv=uiXnEMgGa01PbxJVJqZ8^B#;ewg_y zTo)@B0aDQbDG{I`45a6v;MV{o zU-`B6o-1~)yJ^#Sa*guzfm6@@1|Qnl10D=sdgjwvP@=w0nRF@C0yj%4$d&4G=LnX@E>pP~g&STt*ek zoN}_Micon!k*q2A%Oip1UC|o)xw=MCF_x-Szh`<5FLRwRUxjQ&{ar*x#puBVOQPa; z$hJJ#yMb5fH+3A)ISFP1H+K@4TZHK{XpvxoAXowj6s~|F3Rf)z^IiYw501U@#a}-- z`(FY2ZZMS|2>ehu;RPWxRgv>({^I()aId-PSaN9A7*;2hZ;Kbm*P$!|v+_ zNox$Xu{P}36q2!ZNSbYstpTSD4467#tb_v0CE*A zMqJe27r_qz-ho{QWGvWq;W{Va7U$vSUPWj2Wuz~?4u5O`_5{Nch3YlI8WDnl7$92q zg@yDVciw&UuK;vPgusg_5qL`rfQG-?>(>F)9=mz-CwFbAe@1xfnSk#BAAonBkrKeI zCK?;Jp|Nfn_0|-qxehQj@j}eOJixN!!b$+by#OOu>C*s8ThV+Cv<6xCk(#f@X~Ce9 z$bbtxatyZyTleRRLYNoYOzf!*K#;!BuSADT#k-6eFa1V*erOl)r9W4cVsW9YkO44u zFiymUL!^+NKsx^lI6y=OYSpgIg!{_Vr=H=OH6)w1qq%WA8k5tY zTC)$kOG6RB2gK!G!JG?h{Ot4L)omJX_(A|RxOxp8Z4}KdY1a{g_Q34(+sKqe0{U6* z10=d#mw{rT(PAR-g5x$37sR3vM-VnzQ(&g}ITi^>b8a65sj7;&c1&efggQ{9?rgLMaT6UI41$mykc->wn?0*3RGk zrS*T?wC0>R-1niP?|dKrG-bGn2_#$2!}#ZKW04=i zQ#96l0X7qc7kK&-=zVdhuRVToWm7|41b>N`KN5G z3A^OB8-y!UE0ec1S}6^aLGI_ zD=rp3Yf+=TRG}cR7l3uX9kr9uA|N??d3s^=w_P!W@jGrS+Aw*2?A_k#DF2JEZm0zP@u5@>#DJ?S$EeCn|9?9*8M{&$hgz8=zoP#cIu z;H4Ak*S7x0(Labppi>YD@W7M+jQDFnimhOOzxEHmJhA)2tRw+e1UhnR|gRhBM@EX}&m^$b%APD*Ex-GA=X-U>+HG^Y?xS{F5ia9!Prh(~=x+ z8cVKy?yAjmArjDDK-zr#B_T8O2qOR;`j>_MpSz^F{gw|*-(?Bw7yvlH-}CxFH}!B+ zo6tDteHfpZ1f?ASE_i$|AbGDnHkZK9GG3sTd1^d-{%(L4i?S4;3TPEkl;rw)9UW;5 zCj`}d{AJ!>o)zFa*B4k>04MFC1u&ZAQ`ziP#{`z$Ir*~ld?2FN?H-Pj+;KOOafv_Fpm3NmhJ_c7s00O_R8_2rK zt81~)-njs1F0JHO&-3~7^c>wjys0`tW&Y3MKK8hrH(k^*6NtNN&^pKW@oz+S{`N$P#ytMDz$G!}JmFfRL9(4*pg?s*Dwda0s=8swpvl|R>0N*nU z{4=}I+PNPMYrt+XK<32bv(w(*_xdx?d+p&xBoy$WUkiYSJX8FG0<$2TuR23ewdGfG zc3?&4UQ0uNF;cezVgxE@57}5nM4=yP|0>x^06M4wW&dUdk-@~?3o%a~-FyENNWBt)^Cs(m_|^AJ zJ$2o|+57YlkH8A}tCI>OYvBFkJ^%Wlo2PHwGEut`0GMQ}FLglIor3>dw0G`D-7wg7 z1WExvf?oFt%mTkaABhNxQU#xZT?;@Mr{QE*Rv;O(po_Ed}*tW1gLT= zQ9yM0d;W|(Pp(r64Z1GQX`#>sMOi~Yrzn*$0d#UA2JFLg5Dx1BpM-5+12zl__pk%8 zwvmsB*6DTGAlwwX4BC74VXoUj=lkCUIaVU@p)Kv()?@aXA&!<<{RF zzcO+Dp6SMC5pVT@;d=0guz&kSs2eW@KIWy0zXsn)TW_oq01YrZl_m&?`9(r404_gN zXI}v_mwDb2!QiUxJiR9Cv3mVd-ZlKZ%-R5%NDxhC4VExGXpai>b@ZT4`zeu$cN}!( zzgS$1m3Oesuj*S1403;VfblZVd$9GrdjbG&0ZRY}Bux+mlf*^am|c*qVPfwu;;l*^_*xKTjPm-rEe2cKP#yuWS75500SNTk^Xs;1ka_6*y?PuEvyOmT zT?fd2CLclQB99-9_g=&RgNxDcFdks2=s`dZ@FLZb1t4wez)mpl0UFG;B09cb#FH_D zx`6FpO${Iy!1g`R8Po?i1z>{^coI&H0eo+c5;3Z`u)l28jkFxmfoQzL-tAN5EM{m%3a?S|PObL6-Wf49*@{mcUxYt+Cl z6^I4k$lE)4j^RgkTx?tx^6O%v_UamR=IU@C%gN)bLDq9i)~XHyt9J^Bv_N^Ue+`56 z0U=p4TbhddVt^4r>$R{rAHB*Z0?3NDXYNDKy?Y7m zQCkvNMlCLhNHpM`6gW8u3RsK?W_&#+-+L)$kI$le>{$5Hkd|oPx1n+CpPkn@bjuTq zM>4-VXd?JYMFDS;0H7fMPyYHtt)1s@YTlA}$}c5?*|s08_BycZ3gqJT<;oHC-etj; zc@ibWke5aR`V0Xoz8yf^Rt>RktA@FH-RgPO!lHp^uAajg>DODH8R&`%p-=!9Z#%lv zi;V1K4C@FYCQ*MY5a1T!lI=m$<0-VK04I?U7>P)2BJZ~SC;-f0%OJ}^K++H`44jP- z=$S)1FV!II`YmXG@N&%m>(`JjEQVBoUK{V;9zK+y?k8{7ww=9H#~{F3kz|MGWLBg~CGMHSA?x zKHod@YHh()gIs;muUas`tRSs2@#sDPsYk%G5H5j29_UJpl$hx>LXW3xQI({hO*9Sx zcAtTt0^k(yG7P?g;grvS+puW0R@nW*0o(14FZe?Q(v zW9D739EJT}Eb{u=%X{h5wE|xOeGI(UFl%og_X%TunLq4Nou3b9yn+F%HZ6i1EV;5V z)%8yV;mAuvsPOo)N0503yz80AOaZwZy+8}Ab+6qAFSAQSb zGw~b7q9Nbt$Y;Hn-h=UG6G%_?p>$~kcPik!G$RH@f)MWzq{_<+%<7wgl4HFEURT4f z=a34S*WA|jWmNJS*0lm3fDq8t)?a|SY)xHzpsF(plpl3)$&P>tev1%=|3Rq8L{}5S ze+QU407P|wz_sPRd4P#POG4+O`8Eh<^iGzq7-?)0t&0y}{^)j?Y;cpqRN z!1f)01JtS};Nz>nowo((eL3)4b&`&UV~xu?X!QW*5ebXHtwCcYpwh3lOnDh%!}1=e z+C@Q1px4xA5GWIom`qv_AZ0HfIGLqirRmoJOf9UzxqgvBppagO2oPWf?+s}OMRfr( zXw0066T9{y{m}z3V3-gE7?8Pg%ew3E=)#l2Tz)Zdq$$7+d)FVRC1!wdmCVK%-K z4NLvp-=zU=AHnZ}h1!6$m8S(j$*DfhfxhIE+Tkng!>hx}Q~A*wYf%H~c5vDJYO&EZ z7^>LeK>iw(D+t*XLJES;0ER^<*hfMT6_=67Tu|XB$8r);3W`!|h|;Qh3*-CtV(!_; z;pg6f2Q5*(p=mD%@aGZ%s3-s`4Om?PsQ!8HcTLx?KK;i%g7+VvLThR|f?qtifS$(p z(x*ir0G;Y@`oSg8$G|J-wW)3{29`$<$&b`nfEQ1l@8K!WQ)Zr3Ql|}){3c`Pi;lJa<6X&fTw;vpU!OY;0`xI z*%^ln+{@WTss00X6IXmULHcj!NDNGj05+s>{F1jAq5BpL5jDo15PZF1~pn}p1&6-pEG;NH&_wCc85`J(uot>USur`D?4D z*g5u|nbYn0m!i(8?wOrF^!H!B|F6G7piK_gM#5Hyp&gE}@+UZn(h0*3l6AkL zG4u*7`U-2!*w*(u*`}0Wghsa5r)9%c1r*}?n|sP-m!S&#RLZU(LhIOZY%KJlSJ$A~ zHVy%B%ktbk0RAN4w{u*o4*{OIzkibgbtUAp-F}h;bDaes7k~?>N8i>q4zXlfClGV958bNDEIrUoaQ2IM%teT9XW|&_3uc*U?wmDi&=8ocmHKB?{B5$3BZsHJbkQp zn+p`pW`NsWWZT2!^|b|ZVL)%MM*{XDH$%YhvHm*3^v8-xcu!pNMmP@UxI&$r_?+B> z6VOXe-#gb!LxfzfGc^_;$N(PNpSSUTUH}+L|A8Vg*StLwtoJ}BK9=f4C+omkgD3%| zZM5Ec4RXT+NM&N0qkKNY4!FT^^P57m3L>g69VzuMKml~5O0(e!T_$ zXt8)-t|7pe*GgKj$wA);d8xp_Ozh*cW`C6TaKi7GcEDPAv-XTzE|y=aTD_gCf(tX8 z5SOjuNIE^Bf52eLRRYWH^lg^_JyqiYghM@iSMN#zm+bv#lfF>yEY0DBozkGmigiF>hP|f~sF8f|j2}qvXjzVU30Y450iqQClsck88b3Iz6pq7>b zr*%FxCuq|?Z~&nfaP!gj$sLQY$SMG-`4{rfcmKuWyR3f*e#C(91HEd%Hxf1oDWL?W zNg0GnKn#@BqIkg9lHoE#6aup-3<3Sl-K2>UB>g@(0hr`nbBa#HfiL9!UTg-!LY6MU z5jUJs1k3vC@o-XpZnq!p-HSUB2mihSy%BO+_^XmdRrMz}<4QnT6q2pu*LJ2t?5U}D zAs|&Y1K@@QsqGk)0y>d80V)8JjGmjFpjKvKAkR8YXgOAOQ4t3ak2N|9U3el`WdI(l zug&mw0*V&&PwTO=|KKMXa{qrG2B=vppepGVXqN6(U~&67bo*WIi|q_}?eHKO2G$G^ zfm$svf-_U+e85-C0mPw@r0B+aVJfhYPp3(XNwJqHj9Whu=GP$8G(GNwAT{$Rh4 zoR{j`Yw(w*QyLM+Eu3kl$WB?)-Ukg@&56O2^fm+9+Xo{GBg}e1ekYN>lpdw3Ljd~U zzZ^jaUwJU1975~QyoWC3gn(x>5_LppwD?J^zJTBBTy428dp!(V+_BF6OqjR5ox!X@ zpcn!v4*Ke|0CMdz|9%;++tFA1U3~MH^^d#$TpQpE_#25ITgkIo0U7VK?6~kSAP&1* z!Q;QodnKJ1a$o{5E(!ChvjFi88$n}$!pobwv62p4ewk|qws;qyMo7M^v-yl7#Al&x z#~&>%#1fk#DGJN*X9Dm-n>8d1)(pyXX{)KM=ioFYWorbvbl-^TIRJ`lNw18O!vW0^ z=HCZbja+_;TDC@b=D{y>oDh^*$UfKg_X>mx@B6RaaAHqFD}_mgK~ds_wO1$+e9#hy z<7DGqN-r6XbLZ)U80BQ)a{ zKU{ON1fDf7Z1$_r-pN+%Z5&!Q0a@UyD}u8fL|5%5D{h8EGZabgzBaM;wRh6S@1gd z%6>WCvWsVnNp1ii3iAvr1ZBZMY=J!v2OPkwWK0r@GANXJT`~qzI9H{p%G`g+gGSnU z*ROw919}kZ$RHD1NQHI z14ptZdtkhtfb2FF1>5&XAHjN)fFU?N!!Gf^4y^xvO9T80fP2}%JH?71tZ)@F&}I8| z?ht+A4R~$Cr-B3UMPeL92s@@kj_($4pt9H+K<^mt~!ctWl#2m>16*O>&IVnx7_o{{WffB-0u z3n1-~fQj7OlZB-(Ky?OCRh$O&4m)t?NULwEU{gus>dzDj$XxpkPafap?1SOl%w9`8 zDG|0a+x3tGn4UDClrb`U1DhBGRM$!NjEO=SC0F1WpYiD5S?uqILb9Xq`NT?8w!~uDk}x{4yxb%!mo=0z4W*uboGJ_6&w+{(<33FJSA1 zXOO@AB1Xg9cob_Z+9-o=PZ@=v6#4G8D1vG72G{|y` z#K9Z4`Zd|9*8rcL#kKPqgEb9g30gPZiq7f#(0Rw*XdSy2l(kV-NF|_l$9M?o@=?rP zf4f=de2Dz@GuU|kkJxzbci8;v(->_Iu`(=xb3lJVq1#uY2psftt&~7$0C)Wdv%Z)Z zhZ_c@fU+C_rh@|r!7Lq{Yfl;_8Vwj zb)A5J4YF<8~$0!VLsFFH5A2m2|M zwa~luUi9AeAq@WV8?5~FyBPiRIb1leG1>yUJwmHZf-54JsgB_Qv7I6DKQW*o#2;Dk zD+XX)ex2CY*oFf-4*2?40=jO0iDQ92+^RIC3 zM~`5Au!(-wMQubW^iF>m-FLnpt55$JD?j@VHdilTa|7tlD-#;HpOuN~qXfvmr_fDc zj{^v5X0$V=p(7AS8YBkmr5@l^5{1m}*gMZt{=;}EkeqrSmLK>wlBFx4I(^LF^(oAM z;;TYR)k_TVxVRk}9C`E?Xy10PSzEiHjYpt0M3^nK>JbivSnwD1#XAMHouTcp!a6+n zm+GhhCa>X-a|1M0z~3|BxYai)yz_5rte(@zhqB(jK6LTtZn#|Z|iow8yG8hd#l-EHCaAcAFM5bFJ?+>y3_2&R|S+JJ~0l0DROT_s`wymQ9$XGVk zHF|yHHbb>K2!*D`yITCn=g~TG3UB=2VQjo|26@itb`?53^8EceW(O#GqOc6td)A_q zX_^Xw*d1j6?%V^rg9{qHp4!>XXT~&1 z=f*p5_**~0>)-pLAqZEt3IGW@T~Pu|0D<;L2@ql>?ot3GYaZy??gOw2fRnm!2Aa_N zvp>%s@EKh3vz3=Owis{u(&Om8`#u0L`TVnUeGGp2Lt`yxCBc$iaU4g!`4nDz{2^>T z`&+D>^OXR=#2`?8HU=^8L<9!Jkz@uE2@fg^hgD@E0Pg}ZG^4xgRBsW7zVdyv?dRQA0v;y-FnI4jFYSgbiamVS+H<@a2-Q9dFk7eY zGM57Vt^lkx42e|~6V{u!20}LElmOBard3pD-Y9|AEgvxV>+4>20Q;Z>*q=E9d=0>B zoEz~LrlSsk%hU$U6!1&C>i=c$YhW$As`I|J_c`bOyuW$#-kTqfnF4kR3XWq4EumJR zxSEKjZw_=Wp%;l4MceeQ860CNi} zEF^-s14j;`LIo)nGKE{^)38#I0S$Mau&%%rem%)~f|bmUYYyc67JIwYHjLd&^+;V(z|wMOKqB5nazM0cql&-R)hE>6I?8 zVdaZk*vjI?$7qFc+M*{mfY874`7ZYZW_Rt71iHSgT z-30q0A72pO+G{c9-ux?Ax#v@8{@_0zF}6dZF!$;o1u5;AbM6Q31~uwpIiP`)zy3L# zc-#Fr{q9d;_Sv*upWI_d>-u{w3z}&HG!Pgq3m*ED;Btv(@|Ts$n>|Up>*a5d^Fb0CKfC1ez%3j|5%6=e3P7p%h5&crA{<<(+8W{%S1$><5sX zy+sr=2$54ygnr*ooZaz5sJ-B}k@lXu>&KnYVC&-o-Pw~^x##zA^S^r-C*Jn}^r@Sj z5Xk#{uub`t!u5p3yWfHI*4v!*6}ke4Yz5Fhg#nLkr4`<%iE9dafWjXXevV%-UmMK7 zvn3gKu6BJvVW#PsDOouyJf{;RC;ukwg>L~I^C4*sHMb02f6CF->>>gvJ$e~z8S{6) z3(ebKGEYKMucQx1*Axb6Ao2tVDTsK!=?BAu={su zHdP?wBp`UG$wB4qjJW!cu0fcH7{I{`cH$xjIdgP8 z z0zUt!d$t+~4*`#?MC1JYW{K92ctIf;P_-s*B+S3&$FTE<_oKD5>nb7@1w44_@D0#t(zD-lDiu`tF^$^2r1=@cHqv}GF~F$)uO+6Y$xlmcZ9RH5Hz zkX9}SJ^=!$!Wx1cl0e{t3jyXaxd#?t;^F6@lTLT;4C>!= zJ33$eW9;qr(WogTDFB_gB9x$_Zrn1c9iPju=9UJ6pC#fiKRy^=nn1$EFNfSR;{vd^ z1-r9W;hSEcn7Q-K=sxsCwA&VQi3&pi94Y_;9mtZWcv$S?D;j(b3xuRIFv1BW* zV;16iEFcECM4$ju1k1A7OI`(I0Ampef#~l_{yU`Ruv~K6Mh!t@Oh7}B>TVVXHvL7j zmsto@>MjV4BcDG?^ilIGVa>c!^3|C|kQVPr&&-1~iK6xlD^MgCl9^Y8nCAA^D~}4kKcD|Q!(EP~)TG-Hdo*b3lDYzV?vC=5`M{u`D~hjbe; z`}nTH6lm)b<`*zIMiq^&23s|A%%oIr4(MD&m*gET(@@I+eZLuE^1SN(vu{+?Ui5N+ zfPT)XGXWF}0jg5e5-X^V7ck%+qe`2Bgx8>uZ`k@X(KQ|eum=q$RgH@_Q3*j6b1;NN z3}%0n*YSriswNCNx9EgGsd$(0JuJs`H6sB-Ya%WV0)MJLix>GB$J}Y7N}z z$j;mXwQx+{As@g%ilm^o=ussSwyjJk61yalxMa7- z!e~-r=9dJWOOU%ffHX9KLbYz5l+8i; zkP@Y0GrziT{=qkKiJ3CVppTydE_`d0%mY$EUpl`&m`ff&NwE*ryt@#idqt>&2J7>dy*5y9BkJNKfW6J|3LP?PD zYLWMG-6J{iHvqsVb)^dW^vy_L_)?^&UI;a}I0~i$46}I|W_u0!r6V`&Fh(N zZ_f{CHVJ7)S0oWsy%`ql%p4yDB6Ax?+es&`k%!s50Ig$jt>%t{5>@mp2{N+(t#uT@ z*os6W9#3af(QgPOizkuY_IJ?y{x_iU-LFJ?@~m6osRH*p=wEmedtdt!+7Epmy+{5G z?LF&`MpL2LP)M`52MCq9x7P7?6a);@gvXS@^EF)ya8TwyP%plbM89w$cjw&5*DM3; z>js^Ubw_K@MvRg7;qF@-q~*qDc6|nHBy*2~f}X@NxdXXn!d)H4y^R&A_ z^EE$;><-^!Z?A1jf3})m76Nm1q&=?KD-++L56@icpi_M=$|5AC17US^%YakVZ@mMj z-t#+H{AV8=b{}#_zaO>%XK>X@S9OsAO~l3^2dT`94`%i>`HtUcW8JuOFeXh=|E`xK zUAfssd94vzZWyZ847&6fp`-rlAH&MqK8#cE`h*JzvCv}kki>^q&AFHf=av8qSX)xD z=l%S0%_M1v<%vF)ufZCCDdvChCvfuK-$!!xR8MY4PvC*JwVfe>IP1OncuECUF* z`$*Ea)zO*#2XDg3v&S)e=Nq3re%KLuA^r_w+NW4l0ZxG_MgCV2b4UDV+GVPe7kI zgLa#-+u|_#U*h~TBQ9xWo``+`<=UUagH46cF8~W2bo}N46D|UY3StAcxeO>J8)3?G zKntfqN=r81{L0|{dTp#{js**IIULIn(~`|T#PKSenALe+&7P%(L3Bcs<4qRRL0Iu9e zf#**T{O}X5aHiL1;l=r-V3*jzJCpT#os1Zd=1<5w*Gen^smSx7csY*J8icM&x7n7g zuMo^e#+KxW8kVs!#x3JmE9gY$fBz6U^W`1kn z5-Qk+bLquXBPP?!X95dJOmF|0AyspEeV{QVQa@VjC#`}&pPuu=z*q*QT6}S-2t*00 z*Y`;|i=qnF;E=#LdQ7qK`kzB~%k3_B-RatZ#xb*$HGZ9tJOJ!h`^OgarB#TVnCK&$aKL@0M3?&IS|a)AvxZ(+3|dG-`DMb~6f-dZ_w0Q^w*M+8 zH)gKG<-92<%`kuWuYtknv_+ePE8;U^MNVW4Af{FCv5(H4MeSv;L3Z0K-F3Gs^kiV# zg9!Tg8F?OuOs*G}fHd{db{4HIbU%;D0je(q?Ys5Nf;aoaj^DxDlx#vikUn9F0vQD()~U6eJmh$1FZC_XUqN?3gF4e0}$}Rzi+j|($D{0tJB6$t?UU2E8v*lHbG07>GWxBnt+DFH4oF=Z#1u12$~ z#sVe`Qz(VG*S!_&GEZ3u7T|GQ%@$$}OmGZqC2sYtN5*!3X%BAr#A5@GpK$uWW(fpn} zp^vYKRVyf8{|8Tq3t$n0_bVv;QXi$6TW(8EzYu1iiLFio-#s~ga^#CssvTnnzV@*b z11%oyg*Ox=j!rX+08u179Ee0XgpE9WkWeQ@{r5}9LMi7Df>1b?hdu`(9t%kTlCjbF zebA2%Q-~s}zwC!#Ezr+PwS=5SAQWi-$%z4o4-*}6jk;Yt3mP;>(5G*OF@qu540LlY z5CD{V|EP~7Y~2+m13}3+@$)r-?Z6r!3Wdk~vRfsv7Jycfl%}i>le_PZOU8-EGK1Yf z8Cw|sXL4gBgk!Ioe)O24_TpCnSq*uQ5$3=MpZHinR3{TNK22C2fS_DPBr9hzM0)xb z*=KKV2~Zls;3ol}3*ZBJIw=$YY@_zvJ|>VPQ5K+#U6FL(Si{TCy!(w25Z&O~cZ%Bc zUxMVOThKS~pbAbVH4()?F}TYAkKm+Y;x^%MaGhF)KE7!#2h5|g+9U1LF1PSR@p(8e5YTuhOy-1O}6d|0!O;4e{u_Mi9|KuoupMsoZ# zq<7odkp-X@RzlO&ZG^GxnAp@8K`JR^w47rBKuSxFe*{-BRFThrthy!l!Qlkc^@%A| zT0?U31&+Xx->(Wb1hM4?CSAjA!39^I8(KG6RUQjH5Il{!m?XkQ z#vTNO>lRZ;zegiXT@5N)37`py0a#oyXz_VrG(SmQ`fsmS(oV@53F?Iy^~g6l$24i( z1=6})01$yl$NpHF$=LFAUKtxf#ztUyZuQu!ryo70pe#d{6|DV}+$+`wld%dQ;C;7i z4g!6o6Stn9B((3$IbZ%;+Mi#7aioiKx8(&ZRsLMkggzPg?HAWb(9zrEzRw(Tcb$F;W^lhNnxBi}GZcL;WPFws)Yl(894T6hMShXQxz&+Fl2seJ<(5#=&)pDLC(=f8kq5H9!#;tI~`> zxjzAU09D}d5Z7mK4gIyJh5vUpVRkmW3&3%4f!*6emJ(EJJ!Zs?XW}$L3}f8bGH|y& zu;K|3tQCP%ICc+$Li&vx1f0TeGj{PDvNU9VCW`%kA_yit4}j$;g>GF&e)$;;(Z6^O z==9+DaQjj?waH<3JmcHeX4HqQjLA zyRq`l<}K? zKm`FnDjR6!AvX2pth^#10!y(_r2QwV7J$X!AM5(s_45!H;0Y)_pzK@%jA;3MV-@Ijy&2!y zhILFSFdG*=jRD&V{e8Q$3FJA_gj_^6_R}sq^FK>t0bzFi*d!rPk``JI4n@Mn8W#i5 zx_B%P8B~!pBQ1a~qY;&N7P1!H*%SJaV~Wl<9|k}DIBJ<_evbM8&`2n-@XrM00aWjC zI2P*iK(^!Hs5Kq?#}eTH%l|#qJkB zg=QmUU?`FculWT;qk}OCslZ%%Ur08nbB=r7ub&6^x-N>JpMM-go|aexvwjgn_+X)r z52)P23e!3PVz*aEjA$bPunxe}cxOE_)6C{YHx?$2_}jgq|5hA~Gp$Xh;S~yC&G9 zFw)lf`UTK(e9el`a{@m8@#*aXv%PGJ?yMY zq`tKouP$s|_B5x(1G*0Y<_4K2um~lA&E>h<>#+~t-t&sVxiEQ@+rXW+8_(@l*%D+` zHQ*s&iq=D4fcol#Xw(QK6@H@q{pavtglAmqEx~x~Jt2?FZluBW!+(O>V_$+&!C0r6 z2XGpmWUynrA@Z$t&uA$X`OMCekl2DtKMvgkW4sT~l29acVQNVHK$a$3o6gTp6GQ<< zySwG$2Oxs#6<7e>P9RJ|xcS`NS}_VRTqO~AC=?*#Ivg3M$k#4l~j{<4Dw_OlF7|fkp?=HmIiLqNE$xC4#Lug$H18-k)&x+y;9Y08rxI{P+E8AADHyQAru~)K))w} zPoV{xIZ1q+BFaIZg{zu5gSI54IVLAv^H-vRZ7k71^v#F8C=LH?fQ zNb3_+aF4-YCUFI1$OPp92q~r)S~umEK+@Vq`+@t>{vRL5;=B%ACTAU3GfWb43^*h! z<0A3_Yb2E0vIJ<1XC%q)3xa?oB&fbv2W*UUA4Ws_UCG`Pzdmpm!rUoY6x~mC#KnHuwBH|mzBm+k;e2dCKv@CQRjQRKx=;K+Z2abrqxl!VKU}`{ zAmIUue0qx0##z`hyfLA90PK!(y5t6w2mu?QW}nd8+XET_Y@JUZmMHHa z(ExcXCT37;9F2^H*<1_h*1u)HNLgpj8( zwCy{SUk^7XVe5mFG|X*Kk!i{OKC(JN#SHEfnsNi(&_HLsxIv0xHuNXqZi-vpS^JNGD>bp@?uKD8zJ1$Ol*G;3-^t8G0Q zn)JE#)&~Kp0_^ssV9mE#m$AvlIXFaf6Y2?r*PoHG8Od+1Z>oH&E- zg{Lrk>CwQwNY>7y{a-!^-14`u_uoE-#aSH)WAnfV(P4J61O4US#f;32?63YO+MoRp z=+?iD&O`qlvlkzcxhe9k&pihIgZnTye70ejPy8af|LE62r(S^0!~YBP;D<(Zefep0 z|MgEo-Stnv^%-=&{2=OI`5dx_0(v>}kG>V=jrTyGeX(~fm~lgb$SuwC_GOrh=fLM5 z1-|j8NYo zH=O*%TaIOK2LR)P>*yJac8{^pRR5p7FORh>xzGB3b?&>bw^{A(85=K(=@7DWF`~i zE#qa{iKjC&-P7G~x%Zy>u->WCr}>pC)wj>wci$VGl1itxQ}_1k`~CL%6%Y(2K-XP; zRuT^ltz&mM=3*n}Z}-b@6_-;nHPjJUho%`A46u~rAzgfH-OkYNX5efW46`9VW43eD zX4uK?qjmdNzKGSeuVFVB=AB_?1D@tT_O&+w*avU`V3F!7)gC}3$!)~}s2imQP-{^4 z0IU%LZLX^6>V}GKX+Og;m#J8;HSR=eUPS|m)8{sI`Y&yhrGO#`XnQKacpE@!^41w8 z>jVXMyG)SV#EgxIs60+dU=lQJodsMElSTlp63oFsIv^KZwqV=t-GJkXfHkiV5RowH z{UucxK@4nL;=ip2{Pz0eK>Q?V*vMsOU@P`tz}J{nP;RH)wvGF~GLZy~cyu zOzX*8CqUe)N;A<0LtW(elN>laQ4TYU|A+vBay@Xf|L3z!CIw~cTTd_;40Xu>4lB)n zE034y^Ef#$PnVoN2?HgQvd9$hBj3N~ZFt0FT4y=WtT$T#>@=wW93eO~=DDs8aJrbI z=l1ZXIVox;mODdq;#^4l(U>ZIT`Etcd-(Nm^kYZ^+arvRN)0Ty{N zH6E#@3;=@j1e|mgfFp>;$YLiM3#7Mjp7JskD;i~=Rsw`N58z;9`I*}O{IBnx_`^VL zUEW;V83IQaJDoy+_XKR&9xxTV4W=gWJ1L+T3&rg~sJDoXsTmr>EKxU?_6ur#0~uWCV(9O9SUdPZg;N#}t({+wjdmQi1DwjK+A%IGd-=;{$`lF18S~FqLtt7Of10mt_yC z6cF+)zy{?FV4i2i1Hn^}6x0vXEusnhgbH8ok^q^j8&f^yWh$j!54t&m0JV-VkPbu^ zU}mQocHh|gam)(8x5D56{gVgxxyB%1Qtf>LjOk$%s+ypkmHp@h7&E(Rn zoavSKz|E1zCIyG%0)f9Bm}?rnIT3+Y==HYC1b(aj=v~+^vt7W2|61y6&Vl~K2LjdE zdQj6~)z@v^le9c(UV&{Afvtu`PMn`5=Bbci=LHC9jAMI>s96Gk)i$S&~AJe-5owy_2Y27l}19)s+ zlc|s`72aR_RaNM7_%9V7P&spOngW3`w4-t(5QM?XHIkyV#x&FZyc0$QY>VLMF`W&V z^nQ1tr@0e{kkTlA}Ps-K@YYsR1?T`tAV@J9I_%JvW$~1iP_R>BQv7jS}#%F(0%S@oyzi$_lK5 zwOEPVv6m@}p{trZF*^R_^S5$dTpNVRs7EL5RzivCt%(!|J=rP%*PA9AfkBq4d%rQ} zM5UK2VK{EiJ{30RsGNV&|B?G4_oervD+mFp$XWLBr~)VJa!{@@TeI*>=(jwU8wTo{ zrTWF9BDW3iz<7{MWh(xs=0VHgod1+jn>0{_0E`BPv=wa_qkjLSe(*?yU=5oNPm~dm zwJBDl0Iy*A+=(o?J2B|0uyBQ$oR>&0eGC3 zVyc>TTL3|-t+Ualssk1(A_9{Id@9H4SRu6z=Jb04%ol)C>VfosOSc6N?L1F$#^fc4 zI&Qn=tHIhiKCJOa0%0_*BZ1&7|517r)g0Eqq%M$!Ud*&&j8|YPQ(?>cFYqgICk+gi zQK5h!Lbm|ZKnsck(6M0cZQ1ItP+-I;%;4l@1fp8EK{+CaK9QfIm`>!U2!rzg=*dlT z{HaV_UokBLzxixG;_|R=cp``>Ag_Uo1{5&J2rVyK`+A(rp7#zpq!pJCg zE_>e8sFHnHV(#@q5dj4QsWKZ*;6x`80j)Q$3fwjkn99WUy$VX!qy1n2rS~hVkB;@~ zcZ@uqh=4W#L5cpcA8Nk>5R-->?$#R)8^N{Qun|!By4lC;4s9;+X+05$Z-bEjl&TLW z_C&zD5xrlQ>bA>NCSAXVA}f>EkcJ59{LB*;HWe23+Y%ChcUwX6^I_@P})ac zb4>a8+<+dxU%j#T#_zjyF7*WbVCs|2tXY~(ky4i3e^zpHGuZ)56ja8NLhaD@hN(;( zzb)>=Di?mFHkrl~_m{Ol2p~x6KrppwAOa`~kVJop9E^h$V6jK!5>D&IVi_+3ps&Ya z1E8LW0QlpNUw_Kqk#0#0>SM)A*z^i`sX^3}%CqLuE=gTrVH8l8slGhRROssZ;(p_n zUy1gqofc8=K8I9^WW7CJ-Y%_xE(Ft_m=Xna1U2@kBsWm_R`oO13`{f$1uQik@$Eb%B;eeshUG z2H;1-0Qz8>13(lmLb<%W`WtfqP!Gl>7$VHs9G=_jzv6HhZ}(3zyB)FL)ws8cUz~U0 z)w1ZPg$fN#QNL@g46*Up6-qSx%^1!7%_seKhpit_gKZlVJ91E#>H2zF1}Qk05-1z zc*?`)4_8lQ!04Z^zQbdBvO&q2)>*|BV?h1_%AS(iS}&1J1n8CwL{_a+IfcoUSheg6 z)TPnSf!`FjFJp87yKVrv;`;&XKnV2m6o|?w-ha^h$sR~TxKZqVpSt=) z#-8ei+<+wg=5#fF@!H`d0AS7tIGY)kk!3_6TlN4tqG4bbvxp*j0<9zh)Ze=FK3Ido zZ8e5yDs0yE_d+S^gQ|`glr{kdfL{PWR4j|-Fmz^!JxjcqD6!bM1DgLM z5u&J$2xP!ZKa)|Arguo6z+qjzXRBr6=p(p&0etu(Vj$of7)%5r7>JOnWf33If(+F` z#y`q6=u&|}M!y)-lK`dv+wuU|S;CS8kad8oi}+Z^Uds?@sl2oaK~!U|Uk8>qmhtfm zJK=$Z??kzPl>u{?93(fGA#A3QU=SwXDy$UyhnFLDJU}@Vz%*wj|Bb?i0Y=u34sbnv z(}*?7RCtX0vUPud>E8z?0odzLMl|vsN1mR6WJ?v4S6tBBpsTriMYgb&~mxWvd4 zk#NBpW}>9Vw5w@Hl9>p=%7{W3OIM{81fiY9*Dv#Jt88Nv(ANNa1Mt>7u39lDYhHGw zl+iZ~l`f%Y1~?Von=Jk^B4k$>1q$8%BWrjT`+*by#N`3{J6AXxEPc#?mgn|YAN$A` zaLqAa>RI9XV+jCBCh%3IfOmf8`Ny8W|LkYa&$~BUmH-0K$;byVb7XhDX8D$;@Gn&@tWq`n>X)c6+fIOWy}icbmGc&8eQ zUI9KJE*}%+7x)vn4RG9@k_hAj4_%S^OZYS}MyPNAe%}LJ0sR7p^L@;n`_BM8`OU+R zCh@=bL_qbpLV;ELT%WqW`skbP?cR+1cY>iaV!mTISP*Br00r`y zPlXOLOoT?br1Xir(x?@r_93CI+hNQ$0wtR}=h>!F*4UR@mK?}6-}RuUTc^HXuS023 z8)R;-_^mXALf15-bbby<7>Y$p9brlv5ej6X3Z@UMX70hae1;Cu(n%n%Yy>3dxJ zz{4**TnB%h`)}3=sM>$#*&C0%@!okfT1XNPx?t$CIOGy32)+U$GpEj@q`(J>_^1#8 zi5F>SeWaw2>{^6W3j_3?5KspT%U~`$xtCeAiOKds-rmF7qJ10miVcxVUiv-cArkNf zeh>lZ6yT$AgVtvUo1{#VB~@btB0&yu|Dj*vpxeVvh?rS`na;vo*;_vNlb>5YCx$CS zpx6Ip`9CHCkiXEoAK&}@_rK<@pSu6-><2Q?f*EE;?Cuy24~gBGW%N|ozko}4y2-~#~+a`{1tKeble3Zzp-Ee|pwD1y+@U#jG6A8U%~HiFx0BR^MvXFNCG zJYAx2+XlRdggCx;C;P-6l%KGepZ$mSUkwL&l0zHT4 zi{3%dXE0_D0T{(61(yu`1f4ik>zC;JeZ)JKm_A~+~XK|=(g{Y+p0 zjU|UAe15^wc*rzM0s?BCAXI>9Bf-@Me>T=;IvtuS%TVz=De$XL zsR(II7JpjMT1f!fN0EKnD-sAX5eLD9k?Qvm*83!PmoPZx7Dd3UM-#!1=-*(6RJy2W#W(0P-fSY~9swZZhLEuzo zG}4OTAc^xHfa(ayjtSBy36;fPb_Q{LDnb!PdSXyATzMGNT*DAI662=9@r9c=8PGkO zKo{2QmuRw{ftqH{=oj1`A_O#wtLyR(fnMMj;efx^+e!ma1b{yE2#pgYfpqH6R!iJG zyN{jS9=qM3`-hO2Ald)7Ctvt?8T{!PD?|Xd0{)je5Xig)kcq%QeDs?i{7-Lv?Vpvy+y>iStyAB6xj!(qVuG?37kTi zjD3+MSV-tg=e`NtT2@?aszzP>r*l5*HEVRa-5O&igMe9^%3b*a?QNLv~5y z=0X4XcfbG2hXj7CD&Xg>0={Yk7$*gIH9)GsAO6(yzwo6$I{){tJ3oI{Rs{e91F#E# zJHXy5;(Qkz=@%g|%0wu@n2l8A^(;8mq0s+TW!M9Q2m*`HLw9FgVRUpyetnJ0Bk=dD z1@%=Y z^Wp;M&l=9m3_Dr(Pd@)&edgf3-~Ik)J_uj|;IJeF3IbZ60F0La{Mmru;{vi2ke>Ea zzjpOUf9Tuq{aJ&}U@2Khk_2oPuy4fvD&lNsC7bh6d=%9u)0jt~RjtZ(E>Gc&Q5}5& zR;~S{%#Q_lM;rn&+iVmOH?+>HE{x+s{#aW=EoA{j)1V3bo~xG;d~ol29p9A1(|^zC zMH%J#$EqLPeQ|-kODkMBW7tjL4KL!9V2`qc!FcOOeab_np&?0o0+TIS(02amV8>lcD18Q8od5^on z)&@Ten@7Ffrd>e5-V`ws(8?eQas1#YWnsNnpidrWDf)ZkVR`eEx^e-Q$H^V&@giS; zSS_*S0*CkYIJY~1e|E(AfsjQ17xw#KxcDzG|INhp7pblQEGq^;kQ`wHXcGYW7TZK1 z)QCV<2OfL-rN8{9dv<@s6M>$@G7dywe?jb;p)P?QpYx#q2_0Ol3Cw#x2ZfOYt= z4sHvkEUEz;D~ys>2yRtnV4DDfHA)x+Oz+#L0PhJv84D1CBlpbnju3=asHQ+qs`e{{ zzQ@6QiTKJMXXgq0!4NEgKLC4+_~5U zKX~b{{r0osP464zIPItCvJTeG=6YW2UsQeu(kkyL*Y*oy6HAkv}A^ zFV26s;#1%CZ@&I#p1O&>4E~k@a6RzXaXq`8mr1e_BFtuBhGY|vkA31sR&h-7_b3!ZBMsGJ=QmocY%$s>)w&C zlWK3Ic-zP31puzQw+A6uo~?%iEU5eTYJ~)))9aEBMe>!@r-?HUe!|Ej@GrV07WXHv zzY{PECh#X?{l56V?A_7_??qApgI;RTEj%)&N zyYv?xc*X8N@{#}nNdu8MSQ3c98D}6It1p|axee=U+SdPZR{zaQ0DfLg8(}aUy)dXr zSm}BxxpS)CO0RM16^*w=Ot;Px28sM3MnvnefAAcrrRpO`Wc2j0r2X8>h34sdht*Hu6uX?K^N>C0l*P~J`P60 z!+vlXh6v1q;g-LejUWna69jAL`dS~?E)|-sVb9}O0>J9>jf)BN_|wyOoI6hdrMLJi zVoig2o#3FQlzoavX(l)%delu0ttdx z&LWm~5#cOr!)P*bZ!2_zrO!ZzV|1ue8h47mB5dc1Ao-BBtvZiP{+2BTpRp; z&OL(*5Ry?4QU|L%0iS|iEF z#&B%Mqy}*Ni9cbr=tngOde>duXlLp-si|xL0yzVpJ9?|A(kfAy8~*<14Non>)ih?R~6X3;R;8U7RcNGwV0^@LG}eL9HZcA<`> zwiwJ}*kI*Y6LV8(+7y%p3M^hCRHz4KnMWD4qqWT}iF!9@qgurQ@T)+?8G&7Z7XX_N z9R5rQm~}vBmJNHzrm8RiUtIs`n?L@&A9(hs0W3VIJ$M_KVmtmy?)$Y>fTq8{4SpE{ z_3T`ohvU-ldOQ~nW#!6A|y4$S_)`iY-8i& z*M@NtL)(M0fwA>0rR_Out@Dz=nx9Q7IgAbMK+ADI`Hnt>F0~G$6xsLC z2Oq!sp&$9=!51qWf8ligm1B@lUr9i*vTaVZE&x- zZag8W_obt&ZI=4dHbQFDK|hqQUR^x&@uzNn?A8RTRK>zpx{ELrVdkDDpf8O`T za}T`jzB_*JYcI^+eCJMhmDA+Z@qIIpC!OnW3Uu4z{38H8T7noqXTC4A7q48tzI^mE zPw#*7=e}_LS3dNG{@0x=uVLWxwGRGq0YT$$2C_BP0XWx1Cx2$Oh$_lf5R+}7|KTV1 z9>bG+zYL)J+ppVw{rA1*+#A3Bj@{pJ-){Fi&+mj+xo7fo`dXLg-}%aL@tv=~^E+O1VRrv3cDqZjIMcnJq}?^|F3rr& zwt@F@mk{ja5(YbYMBw@&{>CE4!-GD)aBZJgzIuK6l`lNIxco2v?~VWW%>@nsFo8Nc zK(AC2w=etAJqdssbIh<&*N6)kO#oW<*%JI^J5;NHZ2~PNnrn~K&y7hRP`uyveaFTc&6k{3^B#BtBsE@um=65Co?5Rp zsy@)CXqRJXNDONefZ3Yhub1`@A-eknLaOFdiK&f{`>L2#2!w>hCgDKOUO|LbdGsJ<5to)SV*&4#0K7uu>bS-RM(mx4&nd zSZ>4(I3gsS6@bNxbkJAE#ob-M!s+YWUKO_p@KBj=0lf|Q`a$5(0AG8*cJvD?5=4Ej zO(c9_V@}Vt=E{8=VDSXNk5g@pyGr-$T|3qYMuWRIrI`lThwpFP3($5=s9gb_#s&Iz zP*I(;YmEo4AFF>Cxb`~S+}3v5>3xoI-qo$fYPX2=+G2mZ(0|tLf!voi5LzL#4I)7G zi*c%7s0u8{wZ5&6c+9ltMjQ(A1Yo@AUll=BxP~V?R6rIgeXVz(ZxA52f14ou|8~w` z2Voe7p`_yf|CojQBIJh5hrxTNQP#tx*-522o%F9^U1XIEj6|;2`*TC8c*V8YBdcw# z+V#KV)BUPr+3cR8CwzYg5WBM>%i3N__NA(7Q{-#&JRZgjUwl>3`-Bkd)ueHda5l`O z^)6~L)}uoad0DIbhVxN$@^8p + + Clementine 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. + + Clementine 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 Clementine. If not, see . + */ + +#include "soundcloudservice.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "internetmodel.h" +#include "searchboxwidget.h" + +#include "core/application.h" +#include "core/closure.h" +#include "core/logging.h" +#include "core/mergedproxymodel.h" +#include "core/network.h" +#include "core/song.h" +#include "core/taskmanager.h" +#include "core/timeconstants.h" +#include "core/utilities.h" +#include "ui/iconloader.h" + +const char* SoundCloudService::kApiClientId = "2add0f709fcfae1fd7a198ec7573d2d4"; + +const char* SoundCloudService::kServiceName = "SoundCloud"; +const char* SoundCloudService::kSettingsGroup = "SoundCloud"; +const char* SoundCloudService::kUrl = "https://api.soundcloud.com/"; +const char* SoundCloudService::kHomepage = "http://soundcloud.com/"; + +const int SoundCloudService::kSearchDelayMsec = 400; +const int SoundCloudService::kSongSearchLimit = 100; +const int SoundCloudService::kSongSimpleSearchLimit = 10; + +typedef QPair Param; + +SoundCloudService::SoundCloudService(Application* app, InternetModel *parent) + : InternetService(kServiceName, app, parent, parent), + root_(NULL), + search_(NULL), + network_(new NetworkAccessManager(this)), + context_menu_(NULL), + search_box_(new SearchBoxWidget(this)), + search_delay_(new QTimer(this)), + next_pending_search_id_(0) { + + search_delay_->setInterval(kSearchDelayMsec); + search_delay_->setSingleShot(true); + connect(search_delay_, SIGNAL(timeout()), SLOT(DoSearch())); + + connect(search_box_, SIGNAL(TextChanged(QString)), SLOT(Search(QString))); +} + + +SoundCloudService::~SoundCloudService() { +} + +QStandardItem* SoundCloudService::CreateRootItem() { + root_ = new QStandardItem(QIcon(":providers/soundcloud.png"), kServiceName); + root_->setData(true, InternetModel::Role_CanLazyLoad); + root_->setData(InternetModel::PlayBehaviour_DoubleClickAction, + InternetModel::Role_PlayBehaviour); + return root_; +} + +void SoundCloudService::LazyPopulate(QStandardItem* item) { + switch (item->data(InternetModel::Role_Type).toInt()) { + case InternetModel::Type_Service: { + EnsureItemsCreated(); + break; + } + default: + break; + } +} + +void SoundCloudService::EnsureItemsCreated() { + search_ = new QStandardItem(IconLoader::Load("edit-find"), + tr("Search results")); + search_->setToolTip(tr("Start typing something on the search box above to " + "fill this search results list")); + search_->setData(InternetModel::PlayBehaviour_MultipleItems, + InternetModel::Role_PlayBehaviour); + root_->appendRow(search_); +} + +QWidget* SoundCloudService::HeaderWidget() const { + return search_box_; +} + +void SoundCloudService::Homepage() { + QDesktopServices::openUrl(QUrl(kHomepage)); +} + +void SoundCloudService::Search(const QString& text, bool now) { + pending_search_ = text; + + // If there is no text (e.g. user cleared search box), we don't need to do a + // real query that will return nothing: we can clear the playlist now + if (text.isEmpty()) { + search_delay_->stop(); + ClearSearchResults(); + return; + } + + if (now) { + search_delay_->stop(); + DoSearch(); + } else { + search_delay_->start(); + } +} + +void SoundCloudService::DoSearch() { + ClearSearchResults(); + + qLog(Debug) << "Search"; + + QList parameters; + parameters << Param("q", pending_search_); + QNetworkReply* reply = CreateRequest("tracks", parameters); + int id = next_pending_search_id_++; + NewClosure(reply, SIGNAL(finished()), + this, SLOT(SearchFinished(QNetworkReply*,int)), + reply, id); +} + +void SoundCloudService::SearchFinished(QNetworkReply* reply, int task_id) { + qLog(Debug) << task_id; + SongList songs = ExtractSongs(ExtractResult(reply)); + // Fill results list + foreach (const Song& song, songs) { + QStandardItem* child = CreateSongItem(song); + search_->appendRow(child); + } + + QModelIndex index = model()->merged_model()->mapFromSource(search_->index()); + ScrollToIndex(index); +} + +void SoundCloudService::ClearSearchResults() { + if (search_) + search_->removeRows(0, search_->rowCount()); +} + +QNetworkReply* SoundCloudService::CreateRequest( + const QString& ressource_name, + const QList& params) { + + QUrl url(kUrl); + + url.setPath(ressource_name); + + url.addQueryItem("client_id", kApiClientId); + foreach(const Param& param, params) { + url.addQueryItem(param.first, param.second); + } + + qLog(Debug) << "Request Url: " << url.toEncoded(); + + QNetworkRequest req(url); + req.setRawHeader("Accept", "application/json"); + QNetworkReply *reply = network_->get(req); + return reply; +} + +QVariant SoundCloudService::ExtractResult(QNetworkReply* reply) { + QJson::Parser parser; + bool ok; + QVariant result = parser.parse(reply, &ok); + if (!ok) { + qLog(Error) << "Error while parsing SoundCloud result"; + } + return result; +} + +SongList SoundCloudService::ExtractSongs(const QVariant& result) { + SongList songs; + + QVariantList q_variant_list = result.toList(); + foreach(const QVariant& q, q_variant_list) { + Song song = ExtractSong(q.toMap()); + if (song.is_valid()) { + songs << song; + } + } + return songs; +} + +Song SoundCloudService::ExtractSong(const QVariantMap& result_song) { + Song song; + if (!result_song.isEmpty() && result_song["streamable"].toBool()) { + QUrl stream_url = result_song["stream_url"].toUrl(); + stream_url.addQueryItem("client_id", kApiClientId); + song.set_url(stream_url); + + QString title = result_song["title"].toString(); + song.set_title(title); + + QString genre = result_song["genre"].toString(); + song.set_genre(genre); + + float bpm = result_song["bpm"].toFloat(); + song.set_bpm(bpm); + + QVariant cover = result_song["artwork_url"]; + if (cover.isValid()) { + // SoundCloud covers URL are https, but our cover loader doesn't seem to + // deal well with https URL. Anyway, we don't need a secure connection to + // get a cover image. + QUrl cover_url = cover.toUrl(); + cover_url.setScheme("http"); + song.set_art_automatic(cover_url.toEncoded()); + } + + int playcount = result_song["playback_count"].toInt(); + song.set_playcount(playcount); + + int year = result_song["release_year"].toInt(); + song.set_year(year); + + QVariant q_duration = result_song["duration"]; + quint64 duration = q_duration.toULongLong() * kNsecPerMsec; + song.set_length_nanosec(duration); + + song.set_valid(true); + } + return song; +} diff --git a/src/internet/soundcloudservice.h b/src/internet/soundcloudservice.h new file mode 100644 index 000000000..10610f63c --- /dev/null +++ b/src/internet/soundcloudservice.h @@ -0,0 +1,90 @@ +/* This file is part of Clementine. + Copyright 2012, David Sansome + + Clementine 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. + + Clementine 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 Clementine. If not, see . + */ + +#ifndef SOUNDCLOUDSERVICE_H +#define SOUNDCLOUDSERVICE_H + +#include "internetmodel.h" +#include "internetservice.h" + +class NetworkAccessManager; +class SearchBoxWidget; + +class QMenu; +class QNetworkReply; + +class SoundCloudService : public InternetService { + Q_OBJECT + public: + SoundCloudService(Application* app, InternetModel *parent); + ~SoundCloudService(); + + // Internet Service methods + QStandardItem* CreateRootItem(); + void LazyPopulate(QStandardItem *parent); + + // TODO + //QList playlistitem_actions(const Song& song); + //void ShowContextMenu(const QPoint& global_pos); + QWidget* HeaderWidget() const; + + static const char* kServiceName; + static const char* kSettingsGroup; + + private slots: + void Search(const QString& text, bool now = false); + void DoSearch(); + void SearchFinished(QNetworkReply* reply, int task); + + void Homepage(); + + private: + void ClearSearchResults(); + void EnsureItemsCreated(); + QNetworkReply* CreateRequest(const QString& ressource_name, + const QList >& params); + // Convenient function for extracting result from reply + QVariant ExtractResult(QNetworkReply* reply); + SongList ExtractSongs(const QVariant& result); + Song ExtractSong(const QVariantMap& result_song); + + QStandardItem* root_; + QStandardItem* search_; + + NetworkAccessManager* network_; + + QMenu* context_menu_; + SearchBoxWidget* search_box_; + QTimer* search_delay_; + QString pending_search_; + int next_pending_search_id_; + + QByteArray api_key_; + + static const char* kUrl; + static const char* kUrlCover; + static const char* kHomepage; + + static const int kSongSearchLimit; + static const int kSongSimpleSearchLimit; + static const int kSearchDelayMsec; + + static const char* kApiClientId; +}; + + +#endif // SOUNDCLOUDSERVICE_H From 2bf84f77d9e4f838d7db96db7739a61255c3c82c Mon Sep 17 00:00:00 2001 From: Arnaud Bienner Date: Wed, 8 Aug 2012 23:23:49 +0200 Subject: [PATCH 2/5] Add soundcloud in the global search --- src/CMakeLists.txt | 2 + src/globalsearch/soundcloudsearchprovider.cpp | 95 +++++++++++++++++++ src/globalsearch/soundcloudsearchprovider.h | 53 +++++++++++ src/internet/soundcloudservice.cpp | 26 ++++- src/internet/soundcloudservice.h | 6 ++ 5 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 src/globalsearch/soundcloudsearchprovider.cpp create mode 100644 src/globalsearch/soundcloudsearchprovider.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 21884edef..6be27725a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -152,6 +152,7 @@ set(SOURCES globalsearch/searchproviderstatuswidget.cpp globalsearch/simplesearchprovider.cpp globalsearch/somafmsearchprovider.cpp + globalsearch/soundcloudsearchprovider.cpp globalsearch/suggestionwidget.cpp globalsearch/urlsearchprovider.cpp @@ -425,6 +426,7 @@ set(HEADERS globalsearch/groovesharksearchprovider.h globalsearch/searchprovider.h globalsearch/simplesearchprovider.h + globalsearch/soundcloudsearchprovider.h globalsearch/suggestionwidget.h internet/digitallyimportedclient.h diff --git a/src/globalsearch/soundcloudsearchprovider.cpp b/src/globalsearch/soundcloudsearchprovider.cpp new file mode 100644 index 000000000..375bf8a92 --- /dev/null +++ b/src/globalsearch/soundcloudsearchprovider.cpp @@ -0,0 +1,95 @@ +/* This file is part of Clementine. + Copyright 2011, David Sansome + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#include "soundcloudsearchprovider.h" + +#include + +#include "core/application.h" +#include "core/logging.h" +#include "covers/albumcoverloader.h" +#include "internet/soundcloudservice.h" + +SoundCloudSearchProvider::SoundCloudSearchProvider(Application* app, QObject* parent) + : SearchProvider(app, parent), + service_(NULL) +{ +} + +void SoundCloudSearchProvider::Init(SoundCloudService* service) { + service_ = service; + SearchProvider::Init("SoundCloud", "soundcloud", + QIcon(":providers/soundcloud.png"), + WantsDelayedQueries | ArtIsProbablyRemote | CanShowConfig); + + connect(service_, SIGNAL(SimpleSearchResults(int, SongList)), + SLOT(SearchDone(int, SongList))); + connect(service_, SIGNAL(AlbumSearchResult(int, QList)), + SLOT(AlbumSearchResult(int, QList))); + connect(service_, SIGNAL(AlbumSongsLoaded(quint64, SongList)), + SLOT(AlbumSongsLoaded(quint64, SongList))); + + cover_loader_options_.desired_height_ = kArtHeight; + cover_loader_options_.pad_output_image_ = true; + cover_loader_options_.scale_output_image_ = true; + + connect(app_->album_cover_loader(), + SIGNAL(ImageLoaded(quint64, QImage)), + SLOT(AlbumArtLoaded(quint64, QImage))); +} + +void SoundCloudSearchProvider::SearchAsync(int id, const QString& query) { + const int service_id = service_->SimpleSearch(query); + pending_searches_[service_id] = PendingState(id, TokenizeQuery(query));; +} + +void SoundCloudSearchProvider::SearchDone(int id, const SongList& songs) { + // Map back to the original id. + const PendingState state = pending_searches_.take(id); + const int global_search_id = state.orig_id_; + + ResultList ret; + foreach (const Song& song, songs) { + Result result(this); + result.metadata_ = song; + + ret << result; + } + + emit ResultsAvailable(global_search_id, ret); + MaybeSearchFinished(global_search_id); +} + +void SoundCloudSearchProvider::MaybeSearchFinished(int id) { + if (pending_searches_.keys(PendingState(id, QStringList())).isEmpty()) { + emit SearchFinished(id); + } +} + +void SoundCloudSearchProvider::LoadArtAsync(int id, const Result& result) { + quint64 loader_id = app_->album_cover_loader()->LoadImageAsync( + cover_loader_options_, result.metadata_); + cover_loader_tasks_[loader_id] = id; +} + +void SoundCloudSearchProvider::AlbumArtLoaded(quint64 id, const QImage& image) { + if (!cover_loader_tasks_.contains(id)) { + return; + } + int original_id = cover_loader_tasks_.take(id); + emit ArtLoaded(original_id, image); +} diff --git a/src/globalsearch/soundcloudsearchprovider.h b/src/globalsearch/soundcloudsearchprovider.h new file mode 100644 index 000000000..7ef38a334 --- /dev/null +++ b/src/globalsearch/soundcloudsearchprovider.h @@ -0,0 +1,53 @@ +/* This file is part of Clementine. + Copyright 2012, David Sansome + + Clementine 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. + + Clementine 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 Clementine. If not, see . +*/ + +#ifndef SOUNDCLOUDSEARCHPROVIDER_H +#define SOUNDCLOUDSEARCHPROVIDER_H + +#include "searchprovider.h" +#include "covers/albumcoverloaderoptions.h" +#include "internet/soundcloudservice.h" + +class AlbumCoverLoader; + +class SoundCloudSearchProvider : public SearchProvider { + Q_OBJECT + + public: + explicit SoundCloudSearchProvider(Application* app, QObject* parent = 0); + void Init(SoundCloudService* service); + + // SearchProvider + void SearchAsync(int id, const QString& query); + void LoadArtAsync(int id, const Result& result); + InternetService* internet_service() { return service_; } + + private slots: + void AlbumArtLoaded(quint64 id, const QImage& image); + void SearchDone(int id, const SongList& songs); + + private: + void MaybeSearchFinished(int id); + + SoundCloudService* service_; + QMap pending_searches_; + + AlbumCoverLoaderOptions cover_loader_options_; + QMap cover_loader_tasks_; +}; + +#endif diff --git a/src/internet/soundcloudservice.cpp b/src/internet/soundcloudservice.cpp index 847162327..45a2aa7ec 100644 --- a/src/internet/soundcloudservice.cpp +++ b/src/internet/soundcloudservice.cpp @@ -38,6 +38,8 @@ #include "core/taskmanager.h" #include "core/timeconstants.h" #include "core/utilities.h" +#include "globalsearch/globalsearch.h" +#include "globalsearch/soundcloudsearchprovider.h" #include "ui/iconloader.h" const char* SoundCloudService::kApiClientId = "2add0f709fcfae1fd7a198ec7573d2d4"; @@ -67,6 +69,10 @@ SoundCloudService::SoundCloudService(Application* app, InternetModel *parent) search_delay_->setSingleShot(true); connect(search_delay_, SIGNAL(timeout()), SLOT(DoSearch())); + SoundCloudSearchProvider* search_provider = new SoundCloudSearchProvider(app_, this); + search_provider->Init(this); + app_->global_search()->AddProvider(search_provider); + connect(search_box_, SIGNAL(TextChanged(QString)), SLOT(Search(QString))); } @@ -133,12 +139,10 @@ void SoundCloudService::Search(const QString& text, bool now) { void SoundCloudService::DoSearch() { ClearSearchResults(); - qLog(Debug) << "Search"; - QList parameters; parameters << Param("q", pending_search_); QNetworkReply* reply = CreateRequest("tracks", parameters); - int id = next_pending_search_id_++; + const int id = next_pending_search_id_++; NewClosure(reply, SIGNAL(finished()), this, SLOT(SearchFinished(QNetworkReply*,int)), reply, id); @@ -162,6 +166,22 @@ void SoundCloudService::ClearSearchResults() { search_->removeRows(0, search_->rowCount()); } +int SoundCloudService::SimpleSearch(const QString& text) { + QList parameters; + parameters << Param("q", pending_search_); + QNetworkReply* reply = CreateRequest("tracks", parameters); + const int id = next_pending_search_id_++; + NewClosure(reply, SIGNAL(finished()), + this, SLOT(SimpleSearchFinished(QNetworkReply*,int)), + reply, id); + return id; +} + +void SoundCloudService::SimpleSearchFinished(QNetworkReply* reply, int id) { + SongList songs = ExtractSongs(ExtractResult(reply)); + emit SimpleSearchResults(id, songs); +} + QNetworkReply* SoundCloudService::CreateRequest( const QString& ressource_name, const QList& params) { diff --git a/src/internet/soundcloudservice.h b/src/internet/soundcloudservice.h index 10610f63c..23476e42e 100644 --- a/src/internet/soundcloudservice.h +++ b/src/internet/soundcloudservice.h @@ -42,13 +42,19 @@ class SoundCloudService : public InternetService { //void ShowContextMenu(const QPoint& global_pos); QWidget* HeaderWidget() const; + int SimpleSearch(const QString& query); + static const char* kServiceName; static const char* kSettingsGroup; + signals: + void SimpleSearchResults(int id, SongList songs); + private slots: void Search(const QString& text, bool now = false); void DoSearch(); void SearchFinished(QNetworkReply* reply, int task); + void SimpleSearchFinished(QNetworkReply* reply, int id); void Homepage(); From 9b8f5892af0b4f3a11c79341fa1a1d3f051d4e1a Mon Sep 17 00:00:00 2001 From: Arnaud Bienner Date: Wed, 8 Aug 2012 23:50:20 +0200 Subject: [PATCH 3/5] Set soundcloud tracks' artist field with username value. Sounds better than "Unknown", but not perfect... --- src/internet/soundcloudservice.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/internet/soundcloudservice.cpp b/src/internet/soundcloudservice.cpp index 45a2aa7ec..2c713e72d 100644 --- a/src/internet/soundcloudservice.cpp +++ b/src/internet/soundcloudservice.cpp @@ -233,6 +233,11 @@ Song SoundCloudService::ExtractSong(const QVariantMap& result_song) { stream_url.addQueryItem("client_id", kApiClientId); song.set_url(stream_url); + QString username = result_song["user"].toMap()["username"].toString(); + // We don't have a real artist name, but username is the most similar thing + // we have + song.set_artist(username); + QString title = result_song["title"].toString(); song.set_title(title); From f13e7d1e6927d45490e3ab17dd9252cbd2b473dd Mon Sep 17 00:00:00 2001 From: Arnaud Bienner Date: Wed, 8 Aug 2012 23:52:59 +0200 Subject: [PATCH 4/5] Ooops... actually search for the good text in soundcloud global search --- src/internet/soundcloudservice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internet/soundcloudservice.cpp b/src/internet/soundcloudservice.cpp index 2c713e72d..5f700ef5a 100644 --- a/src/internet/soundcloudservice.cpp +++ b/src/internet/soundcloudservice.cpp @@ -168,7 +168,7 @@ void SoundCloudService::ClearSearchResults() { int SoundCloudService::SimpleSearch(const QString& text) { QList parameters; - parameters << Param("q", pending_search_); + parameters << Param("q", text); QNetworkReply* reply = CreateRequest("tracks", parameters); const int id = next_pending_search_id_++; NewClosure(reply, SIGNAL(finished()), From 26908ed718a14e9d0a942b93d87a8c966346b3b8 Mon Sep 17 00:00:00 2001 From: Arnaud Bienner Date: Thu, 9 Aug 2012 00:16:16 +0200 Subject: [PATCH 5/5] Add simple context menu to soundcloud items --- src/internet/soundcloudservice.cpp | 22 +++++++++++++++++++++- src/internet/soundcloudservice.h | 3 ++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/internet/soundcloudservice.cpp b/src/internet/soundcloudservice.cpp index 5f700ef5a..ae0cef802 100644 --- a/src/internet/soundcloudservice.cpp +++ b/src/internet/soundcloudservice.cpp @@ -149,7 +149,8 @@ void SoundCloudService::DoSearch() { } void SoundCloudService::SearchFinished(QNetworkReply* reply, int task_id) { - qLog(Debug) << task_id; + reply->deleteLater(); + SongList songs = ExtractSongs(ExtractResult(reply)); // Fill results list foreach (const Song& song, songs) { @@ -178,10 +179,29 @@ int SoundCloudService::SimpleSearch(const QString& text) { } void SoundCloudService::SimpleSearchFinished(QNetworkReply* reply, int id) { + reply->deleteLater(); + SongList songs = ExtractSongs(ExtractResult(reply)); emit SimpleSearchResults(id, songs); } +void SoundCloudService::EnsureMenuCreated() { + if(!context_menu_) { + context_menu_ = new QMenu; + context_menu_->addActions(GetPlaylistActions()); + context_menu_->addSeparator(); + context_menu_->addAction(IconLoader::Load("download"), + tr("Open %1 in browser").arg("soundcloud.com"), + this, SLOT(Homepage())); + } +} + +void SoundCloudService::ShowContextMenu(const QPoint& global_pos) { + EnsureMenuCreated(); + + context_menu_->popup(global_pos); +} + QNetworkReply* SoundCloudService::CreateRequest( const QString& ressource_name, const QList& params) { diff --git a/src/internet/soundcloudservice.h b/src/internet/soundcloudservice.h index 23476e42e..c51f45482 100644 --- a/src/internet/soundcloudservice.h +++ b/src/internet/soundcloudservice.h @@ -39,7 +39,7 @@ class SoundCloudService : public InternetService { // TODO //QList playlistitem_actions(const Song& song); - //void ShowContextMenu(const QPoint& global_pos); + void ShowContextMenu(const QPoint& global_pos); QWidget* HeaderWidget() const; int SimpleSearch(const QString& query); @@ -61,6 +61,7 @@ class SoundCloudService : public InternetService { private: void ClearSearchResults(); void EnsureItemsCreated(); + void EnsureMenuCreated(); QNetworkReply* CreateRequest(const QString& ressource_name, const QList >& params); // Convenient function for extracting result from reply