From 8af508d491952304725fa1bba925e5a3ce41f013 Mon Sep 17 00:00:00 2001 From: CharlesPikachu <1159254961@qq.com> Date: Wed, 1 Apr 2020 15:48:03 +0800 Subject: [PATCH] update to v2.1.0 --- .github/FUNDING.yml | 3 + .github/pictures/alipay.JPG | Bin 0 -> 85180 bytes MusicDownloader/__init__.py | 1 - MusicDownloader/cmd.py | 140 ----------------- MusicDownloader/platforms/__init__.py | 11 -- MusicDownloader/platforms/baiduFlac.py | 124 --------------- MusicDownloader/platforms/kugou.py | 126 --------------- MusicDownloader/platforms/kuwo.py | 121 --------------- MusicDownloader/platforms/migu.py | 111 -------------- MusicDownloader/platforms/qianqian.py | 124 --------------- MusicDownloader/platforms/qq.py | 154 ------------------- MusicDownloader/platforms/wangyiyun.py | 202 ------------------------- MusicDownloader/platforms/xiami.py | 162 -------------------- README.md | 77 ++++------ Screenshot/cmd.png | Bin 71019 -> 0 bytes musicdl/__init__.py | 1 + musicdl/config.json | 6 + musicdl/modules/__init__.py | 3 + musicdl/modules/sources/__init__.py | 8 + musicdl/modules/sources/baiduFlac.py | 79 ++++++++++ musicdl/modules/sources/kugou.py | 80 ++++++++++ musicdl/modules/sources/kuwo.py | 88 +++++++++++ musicdl/modules/sources/migu.py | 82 ++++++++++ musicdl/modules/sources/netease.py | 134 ++++++++++++++++ musicdl/modules/sources/qianqian.py | 80 ++++++++++ musicdl/modules/sources/qq.py | 114 ++++++++++++++ musicdl/modules/utils/__init__.py | 4 + musicdl/modules/utils/downloader.py | 82 ++++++++++ musicdl/modules/utils/logger.py | 44 ++++++ musicdl/modules/utils/misc.py | 40 +++++ musicdl/musicdl.py | 141 +++++++++++++++++ Screenshot/pikachu.jpg => pikachu.jpg | Bin Log/LOG.md => record/README.md | 7 +- requirements.txt | 3 +- setup.py | 20 ++- 35 files changed, 1040 insertions(+), 1332 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 .github/pictures/alipay.JPG delete mode 100644 MusicDownloader/__init__.py delete mode 100644 MusicDownloader/cmd.py delete mode 100644 MusicDownloader/platforms/__init__.py delete mode 100644 MusicDownloader/platforms/baiduFlac.py delete mode 100644 MusicDownloader/platforms/kugou.py delete mode 100644 MusicDownloader/platforms/kuwo.py delete mode 100644 MusicDownloader/platforms/migu.py delete mode 100644 MusicDownloader/platforms/qianqian.py delete mode 100644 MusicDownloader/platforms/qq.py delete mode 100644 MusicDownloader/platforms/wangyiyun.py delete mode 100644 MusicDownloader/platforms/xiami.py delete mode 100644 Screenshot/cmd.png create mode 100644 musicdl/__init__.py create mode 100644 musicdl/config.json create mode 100644 musicdl/modules/__init__.py create mode 100644 musicdl/modules/sources/__init__.py create mode 100644 musicdl/modules/sources/baiduFlac.py create mode 100644 musicdl/modules/sources/kugou.py create mode 100644 musicdl/modules/sources/kuwo.py create mode 100644 musicdl/modules/sources/migu.py create mode 100644 musicdl/modules/sources/netease.py create mode 100644 musicdl/modules/sources/qianqian.py create mode 100644 musicdl/modules/sources/qq.py create mode 100644 musicdl/modules/utils/__init__.py create mode 100644 musicdl/modules/utils/downloader.py create mode 100644 musicdl/modules/utils/logger.py create mode 100644 musicdl/modules/utils/misc.py create mode 100644 musicdl/musicdl.py rename Screenshot/pikachu.jpg => pikachu.jpg (100%) rename Log/LOG.md => record/README.md (88%) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..151345e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +patreon: CharlesPikachu +ko_fi: charlespikachu +custom: https://github.com/CharlesPikachu/DecryptLogin/tree/master/.github/pictures/alipay.JPG \ No newline at end of file diff --git a/.github/pictures/alipay.JPG b/.github/pictures/alipay.JPG new file mode 100644 index 0000000000000000000000000000000000000000..0863c884101a79bc04126b93fd55bd85da523cb5 GIT binary patch literal 85180 zcmeFZ2{@E*`#*e(>_P}xrXoZrWyv;Q2_dPJHKvkvLPB;^WZy!_IwAX(Y*{DSNwV+8 z*mpB%472_3zQ1SrJ(b)DCFe$LN%-l*f$IeCy+9zxVjz*k5}*X8M8lujjOHKGOf?8nCm!9P~7_fBB65zmJ@T{c%PH1_mZ3MrKxSHdYoER{oP*?A$`9 zA;LnZ1qIKD%7~vicTq%8P(ty-#Y>m3UcCwtzow=nrz&&hs@(5RXqcFoSXo&4*x2~w z&I+ED`#(OYUja@=zzcXrOLG=D#z{lVNkeS|AfV94!Ik~JxPLjoiKRPE&%nsU%mQ9e z%>f*vp`|@WM|=D@9k@(1{=X-lj`R4*vzImKx$Zt@IOobO_c|_%QRI3>3(vg)tmu^| zF9MmEPx12cpFS@pE^$Hfs=R`t(luqx8(P{rH+A)l?msX#F*P%{erjWDXYb(X=I-&* z)63f@=uL1)Xjphee8RiLq~!N0so6OnbMx{G3X48hR#n&3eyOYf*4ozI(b?7AGdMIn zGCDRsF^QR9SX^3OSzTMl?e6XW!XFS0kABMqYQukt^*71>NiI%Mu48m`v~&!=<)S&} z2{u|zy5nar)1TD1%kbEh>zv$cM(*ozSrsizB3JHVd7iu&U_K>!6>}cWvys*Ht z))@`y?)>IVFoT&Eg}P!OgYryglZf{#lb9wlCuijuS6t7;o7#y8COTgpe)OPN z&AGqr^wLL9Wrc~LZ1bj%Z}{%sexQeZ_j-Enk!^F#{jv=Et5rm@VQjwT1xc>Rc$cAM z5>%a-QInH5ISc=!;hp1iN_^@Uy3Re?RJr<7Rntv(~?rP-C1d@pCZFN0T$54X0T zH0v8jx3{GEJzFx@BClQ4UgQ&3r3s2SY~5sC`DsK{ygelQ=u7;p_WPOY#si5cQD@EL zaVe2k0zH!erW1dZ8nT)CNAxVdOMIHN6cu80!S3lXxx={Wx3gADE8B^J3!=1+e-rvo z$^ZIxxZz6${87q^sDr7!@|wk1>pg^n`sSZM=65A?B%Um(*txUO$XRE2T`jg$|Nc;A zh3*JEMe&Iy+(#@GBIYJ4X?~ws_YdWSRR*7os_4hUSDn-k&2McQu^b!cS6cXK?3{bN z0rt8mH^Nt(5EH94YHNU7J1D6WIVZM$f|0~{E{D=P;>h+b>$lfzO!sX>!cOdmbkA=4@PQ8|=-8i8?l6-KL86k^N0fW#x=iq$` ze3R4RqJbKmGJTcInorpZ1jeK@<)lKY`OjyMbt7OS{rc=-QMn~md;YZ>8w5@V^NjQW zNCZohHhK&8w8>17FZX=Zt6j4?@dB&Z;mqR~C+lUJiVWbswuQ^0Z~M=$?S^y8%ng*h z`ZUKH?EK=|CON?^aek{@pPV~TeAd^i=y_G*n5>0=a)W_l85JnJy?q3!h80`sEXkzF zX-N#ThM2twl~nDJI6=>Q?Tp~c2iK9gFM}Ab^2J_9?VCAY8tF|Jc5hx&cYI6hOndQ4 z=fkx3> zay3Q#%#*noz^E#uU&!Z#WWy)vFWtSM)>?9|*xrq@;Snk&dpy?i6n=E~kJHLl!G&-u z7rcLM#^M6vM{(2A?Q9EuGjBv4+X4uzo<$vVk}f_rIimTVhS&^prX#({;m6%G3k z?-Acv<_(YNslcKa6(AHrF~@P@WLm_oDuVPt2cN`yh+?28kN zLeYt(0uebB4%kV=Pem#q(n$r{?~-$aDK0Hk;4Q*>0g-@%EkpkM2x18ue!5iP4>Tzj zLIrsK+WB=#X9pD!pGSTFhkLtKHwhX@9^jqv*D92h32kYZQrdpd%5M}ZBEIhQ@Wf$QlnpU&mIc*f>EaVSga+5tM^h1gI|1hL1u_Kqi z`?5z;=!Sm@z**Ehzt`6%J`PszR+`@Y<8J(Weo>Fo8J+e?;e@QdQY*B#x!;whsswMD zsi$VF&n|Ii3f=s1xx>eVX)SRdB}9Omk0$F3{gPECm&UJZE#5@cyPFty-??UD6t#jZ zH6J0FWZNrersb`hG;7V~izhlj-fb-_EIqW*piQ#+=<$KI7Zsk#QBmDkUpIo+wDZO@ zO>GWlBw%8`AMYkccP+$JCsP5ITFeTW6?%Byl?rsM(*BfMi8zpRsofq@1Dx-z_NPRABR$&iosbJ;Knp=W8|{Ip_bktp zn(l5!_x@713qviT`}diprq!QQPK?N?m5Ur^|VmuWZKu{J8P%QGUW@arHz-p%iN`{e#}2elG0C$M7{&2<-WvdzsTM7`PB}* z)chtax96dD`6`Uw@M9?^Y0}{K#F9e-QZ`uC+kW;=7vBTGweN2Ikdq7Pu`9-(p|zRL zz|gQdM!&0kzwiWJGOAzTOh^B$M61w5zq38<)crstP|L5(hVn}`c`>%Gn+i;4?U5!r$2;dyf`ofVTV=UT)wmm% zwM+A-1-zXqoduSv8kDpL#zx=kE8z70{IU~H>a z{KM7J)|O2QJFEtFxfUUu4hJS2EUy z3CU?_%)|+6Yw;o87aUGKEov+CJ(z#r{}KK#ZFq)uvp1fTbozzn&DO0pF7x-^5oJlraKvK&wp^?w68^QsmkX}w+pr1IXt**+0A)*`XQPw zgIW9j`>$~n%eF+m?k+J%U)ug#>4Hopv-rv1DCbd2fRJd>~M9q z;YJn2iI_bHp9drD+X>crRS=sDw*;2v)sbXh8BD`PPudu%sH9m4lb?JQ?s)X1qq5E7 zy@_#6L22%!v1fyFGglx@S1nFoS}%G|&nRTtpS$rdO|46Z6{LBj(GA0PeRg(YYaipY zYw&_+kl$Q>*EH)i=N~Y%iwjM3i{U_!`&LS~L`gKYRMl-17XVVbz@7 z5{vZrJUSvOLdORIqt4*@o)SxkqTq~}-n=#P;nEJqE# zzX_4lC4K{^cB4^V7QaxsFi= zB5T?p=jM}<&5jg!4$O4M(aE+gf5sxu>mW4VBwq3P!t0Q(7cB*}QP@34H$wKp9c)N^ zv&2xz(7S5i;`L4FBv?l^M5~%jWSuGpjDX_vlRV^wYLqUAFYlYc6`w#y6 zbF+*$d>+Nzdp@|73dGqI|46F6b6C-}_Do1q+_mn1V+y(nigyh;Ck#FP=g15<>pqR_ z$5t9x=lN&!r@YK+exkej!8Im-w2bi0H%+Bav#ow5V*5i)rf=89{Rh<18c0+N&VDzGG zWzEQ$MmUVq{9NIyPSO77eNs$7CRBQ!{?cu2`WGJo_N;$RZ%5(kyo1%mUV#Qenfi$J zvVSVvx9*YG!7B)xz=cHsO7{34Dr@t2u2iHiL|1eqf7e!bKt|rT&dBGI&3W_d`K&aH z!in&^*dGZcOhfl-=j9|B!S2?yQy{AkU9ZSw}6Xx z>|NYl-0WSRpSdD)2~fJOYe0AYPfmwpSSV^D7WhoJ_KNBKCgFO(pRCj|8k*lrH6 z)BZl_K}O^_13gGpfsK)gg^7`o86>Wl*_fGGSiysV=>$6)>xtjzf3i~GU;q9q@Qam^ zf${eh|7XXatkkj8W2Uq;=l;V=0oDJXXQjxb>IJUyt#<8ozrVc@seNqr3QuBn)=ah3$3P^AG?N=<@0YopK=KoR-N{jCsxC{)Wk{h(02Bt z3&Ltt;9JNJ6^Jo8t`*Qn4oac|{b7hX35q8bFuUS%#8gNHUPJd(&;-ZW2tFzhAGfeW zl5hrjiX|4Jk>4HY-aSA@+SW}~S~*l72u+X+nlvhGXrF)+b4T51{i>^cS>9gtYFyRHM>$$RfPz%Id zCJKAk7cu#nzfomg%GY19WAw5&A@m&DC^E~dMYLE};B5jK({Y__+$4o9#^1rUkF}yE zt-nq-rI)fM9gS4E)Dp7eHMk?Gz_I9Q#Mk13d*U-EGdJbnhoNmhcv}|_qXG&_Z)ewA zrq0Mpr;zjKIt8A536e~A{qf7bDs4Np-lgB2I7y5jc;Ml$rzW6R*YN&w%&1rjZj4#Z z6_$a(akFn?4N0%mrKrGMWFpgzArG}fJW6N^}e5z47Rpf8l7U*6h3sWQ(K+m4zP z=**NFI=QZ<*fx7^SRQ`=%bkRaio9}qufJZTeLQyNlw5yn)2q4}~G4F8JY zmPcr4sUIm1Q&lYXMp>h3`V(E3dT{EBfLEN5p^$NX5eXy?0v}O<7EcNrr33M)|25?_ zp;(9T+M}G{wuOc#8w@$+RU@Ex>b_W(97uF7rnn^vu54J5sQ{-2r61JrIK(`P!US%{ zLz&2IZ?hw{hq3M8{;*;vBO67i024zrNjJyKL~Ab|y;Yw$IMupb^YVjj=lAF1!X1q( z_!H!2RbDD^oR#pn$gANi-s)V%;J*Les_-$>>WEpplqV5S?(2xYDw{}74@$?u5)oLp z{w-gy-<3{7Hj>cbcI{d-!yCuR@Al}6snIQI*5{P5K??)$JKeAG@#e z-c_3peyYozX!Ga=qtdwq>nLZ${Zxf&yLstzLwASDwmjINcY>Xzol6%_YPxbocb`*U zl>3}`CMfV4k5`RU3t|qm2fQQ^oGf00;F3>pX%`m49X|;5gJe%Rj!rt~vQ1Au&Fhip z{83SHS;5*h1Xxzsmp?=h9T6=p5Mgy7V3Z1+RAtXHC$R0qms_H7%d}E4`-n7Jj$<{`n^& zu}|Qbo!8J>b`rM*K8awMLs)2mvQkdNt@y^`bt)Rsb}lQ7OI{X>No1ktxKB$uxAx25 zHG@XEMF8;(+$jr+;i?|{Or;;_faH7adePoI=mE=N>rSKzKv`|kVfeHaM z;95fIbhE~%`#c03mGe?uU7L-kCv1i}tksmtuaQ4M=bE6^W1CGlPh+fXj2{$>*1~Cr zE|P1yO#Sn__a5sGt%R)oT)xJE?G*Ur-Icw1&831^s^teV(R4}VYLA_%F}&g+$zwiR z<<^h~4&jzJ%XZCDI?vwpiI$12RH9ASv^T3Z4YPWB-p#vO+`zw~(O~UJye1Ug&ew=d zaVRCCyw8yg`lrBkB%xOLI2HH=YRr)&O1}7L=v(Ze_ywO{DxhbFg~cFuO-%MfDQ{zl zbL^W_^QcLSwy{)`Hzjk8%@wu8`^EA--;O!6q*nmDIy@T??3zoop7&4pe$r_TkcKyT zMNpX+Zo;k~!OPh<>PLe-V1F`flf0Qcaik|?iPqDfRE2>IP`p#rtM*FT>QoidB=pH%OMq%0>L zHug=`+NH0^6UxI)zx(@epSr@Sm)%fX*)$sIZ${>y`vFVB;7mgUTTMX6J!4$*Yt6pY zFbXl31J1Ew8*)+{ADhq?z&p6iIWC+Og%hOJyMWVz2M!L)O9!r;EDhT$O@5%=W*pnnjXf+y6L|g< z1uu9x)pmHdo_lp3i;I=~oVeZOzf|5uI*9u07oX=Hk>Phj@`anHYps;$jBmMNiqSlT z#AQ9-|E-^P-RjJ!yT3Y{tZuDuak9bn3;cJrb(sakvE|2GkM}$7mq(yDD6#~zB`g() zR@K5W56@18q#2HQsC=qyYfoN|(QYvcS-_DSbIY(En&OU zV2Gp%ci|a++k(0BA|gJz$MfTj7gC!MhlUhp*N=Wsf)v)*n9x)f(63HU==0C<)-P__ zPJTKW+!PMFNeVE8Az{wp*YtSk6w=ZA@{wVpL#XlVnbc9y4ymsc!at9?5-*HWYKAuZ@JCQems#f8kLbiHWM`F>w<^%5wV7m22m^cD@e zM1uAjnq-@_G~2n1B%Pe1FeejbC_j*&l3)7?;J82+j!m&gIa7YlV9aD=cjQ@8vdP%+ zpn&z8MBGrdKOJJO7FKbY3Lp(h=bnOIPW~IHbT6F{c2r;-g*T}CNrGaNG3P_uT*7Fo z$Y)DcFXVT6RVFor{w$#|i4D z)u@v_z4fO;6u%pH*4Vvu>Ts*Tp(t7&Td=#=v=sFN;U2epj~B{4v2-+&v<0RN&#) zU*%w$kZ=)H{wtc`tU7q@wD@R;&pTPP4G^-w&F`8t)yAB9(|JyU*6!g-(@AE#bq3N& zH#01n#Jxaa((#>RKSj{)*TOmn6^IAjM&{1y3s`E{9giNoJ^1ue;!=WJ-0;fq7!r=g z-Uja@&K~Ti>z}y8XddP|5$JCZWpmZjp5J1N=T?Bti` zA1ubnH!8qBz0TV$X9FGDC2|f5lY^Q|Yom~Kq2yTQPD+xl(V6J&fFn>lL{U#%fPfx! zB{Z~Acdk!ixAQ(B1(U&kf9zxCk?!ccN0nS)(PCJ~Mv50~$CsG^!S`ViBrg0OzJd@g z9@}V!Gau_1gdaqgX*^~PnN(@Lu=mEa*5#f0=8J&pqe3DZqC1fU=5#B>wW+`$qf|H{ z7()dnv{yQgDhWlMc%Ou}SdX(K0uMrU6#HC}v)+_P=w-Vd=M^=_a6_YI$V3o@MU5yN z&;^3FMksdgTjoVv5CPHZ+AEaRG$3R9r7fZRinHtKpuL;pbFNmeLAcBp(2Lx^N&v$U zrVSM3T$2ceGj_Zavdd0>kHSJgonj_|Ksc~^ig101!VJO5Kxy(rMX>RflT^S?Ch(I? zW-(qu(fb@^X5l+Jqv^o2Ppl_8h4=>=>x$T{(IG`!odfab%MB_JULFl~jEzKL4Ixu3 z3qP800`0Sx2ql^4aJ+H&tjE@>3RIx3HfBa4tM&=hlX)fK$NB+`AmSWMl$%AgC{hHw zam@X5w=yreY(Z;l!yXT&-Gkz;HtaSIDeWOfo=r6nUM@tL47IjGDq^6E5aMM7I+~qy zA>mU$K~y^h>#0kAn-w`{n>Ej7^c6XI;3POX80P`{vpznFQ2RsIc7L6RI$z<2p*8BX zrWAgixp6<)mAVJm`YGwqj1CeDVrFM8lTtnPQ;hqr)-h&~L->ETec1oN9*CH$m#W45 zhB@aIf5V(OSS|!>d4&p`LVrc!Pa>&+mdQ70W$h-pJYbLtaBY&LFjF4z;*wrhS24e0 z30{HR7b>jfbr*rfv0o-3UOh_Ayum)ZOHmKgT(Xz#u(eU|PmlkQFAQ$g>UpuKD?R0^ z!v_0f647!mKxuqJlU4ny=^!^_#MMZVDw@}$r;j!?$Z05B*tl~CAIZDe{se8NW zvFwzXbxg4x-=ktO`YjS4?QV`Baf{<0GuJ#ZP@f(4`+#KaTZ-E^HG~9vd*|~%-c!ie zm&z1Nf0+=1#BVJ~Q>;QO;t9UtJzUX#>wJ2js~1iu`8>;Y(1AB^RH{o*fzU#~msB8Y z0)f424JPazbVxiV3m}?EK~sSX3Q&@5GZ}l(ik?Wum&DxM&rrlrD%|9D;x5Lk=+CK{ zighQiyFTP?Hzb@3ZAI~jCU}{cjy6g^N4xR||0+U!1#2x9^>&|6TYhs4=)BaM$nQNA zNtPuDveOMA*xQ%_|;d3?1mPvRG4 ztz>qQUkQ+|A%GaTR^&(Ha!9_OSr(WC5o&}-W&kSZJs~I zv4?3o_}np`>=p?WzT@ldQ1_aGZ<%dYjFPmetAU4pUF2TiYm(*5^_0Qu-z@d3hH7B( zF|Kh>y-LO+9Dm$1d6}MG%2$@`WMwd6;VZJofL|hXpaiLaBZ3b;y6ZC3jvn9Ct694A zl}@jfoIJ6Nn9|#QG|n)aP|ZWuNSe!f^Udg+5#dq1Rk$fdwy#q1J;~{r4J_hgIohAzEOE7c;p)?AV8-#Ojf;kz_qYy%ZKEKm}%> zqcF)UTt{iEW0Z|(D!>gDGkD;kCWkg3D!aHNJy%~{AC5`o&pD|hcd1qC!d#&shtEjS z9y%6XZPJRIfVV~$k*_}!8gmgslbCX>qCI1WQ9~?z@7zu0d!Nzylt0ez4YhU zLu?yU!E7*mUdZ`7^u5Ir70|8#^C(_d{8`1C&B#d9+-pdc&X(d_sZo1!>g|L%n7TrJ z#b+Fg086RC_=@9H$D66b!;0l1_IhBd3#n<9nrSVZj4P(FJRsUzTDCaJ)XrNvbf#{+ z-$${HU-%Z-c}~i!Wuf@ycXWWx$`RvlMD|#g5mZ+wg=PW_9a!+ib(h^r9d|oUEtoZk zut|A*iwR5lR$h3qkB~sT6flOaVkdFb&dXDQ;KsXikjho%_OVIXeJ88$*HaVDOf65M zI|N=b03k_d_x$RXQ5H~ZLxObdSLD=b0Sf^a&ip`?*I;^hf!B;{)|`sl9EwP&{! z)nooQr~!2BbC}q1sdh*W@7Mn08&=1#0A1Q6} z_krf3r8J)c3N*h%lu$$+IS;}2KST|&Odu0cSZDMm5m$-V$HCeRnf|u>pFh~yb zmSR&&t0M2qv)3s`9SRkPA7ixClr1QTL?ree`tYV_Cp%HtBbIzI9#wSbGOVYQ#GUsP zG)IANNG|Rk?k<9G$R#H*=7c2N?<#1hJTfAO@a^aPg>pg*2FGQ^g*gE$402ZNfh#%~Z_&p{YlJzOyTCuS_PY?Hb8KintrH zctKjR%|uuf$AFKW5*{;+XSC=Zb}k*_eEpjEB|&{beqZCih68^4+i=QSDU7V+;GF1@ zKvVFglalFDysJoIh7+|(0c6l1fOK=X6>BwN0QaI_|MSRVQKCM#nsS*_*si_%A2D@HK~$Z3G5@MZ2hRV*5!-FB)qI3qx|bzaOKVjE3Dc5bMFbgT4*w)pADE z5B7{No-B+TZ8D;;X?j#dUZY2b6 zP|BC@qI;7>pPXPaV_b`e`t(Q(v$PHulFAuL0=qkbZiRHU+^;s)z8sE9xe{h@6^H}37qxrl|jMAi;P)$+M()-M>CflZh ztqfLyg~91I43!p;zPco{*w%R)9f8|-3u+BiD56ORa^o{PYK^h16`$PQpOk%cKNF)- zUIm>?N7g_(c24=-A0!DqcAu3SFl(L7uuRn+a;o(k^#92k8%E6jA!TB`3CJEl?fLH+nAu|)N@FGamfkSmvZi>??Enl0LhDM~ z-g&k7p4YRh9?qmP2lvVuPbZ6-{9*$Sp(^30-z&0X^c8+WPqz?)v6Za|-YQuzP(5j1 zZ`EYoZMq{pXmj1S46}=L4T2?u0qk8EL87m7>#V2Z8$S_Id^1CJ!j;=@+^Iu_bDxx? z7*&fhbtn6N0COOuMi3+p+Sw=OG#ub1LcUs&z}jS)T{Oqb=v|>pO@I?>GnF97{^s!e zr^}s2@xhkELPPp_O_1c-ful@^g>v@R%b8~8X@#10^0sLk+f55AW7+mZsRb7jr`^}g z%U3MDpbv%{&$=%r*7~8%R)*klXZp)_8FKwnaIq97Gn_BxTD-6SDRjHCpSKHCJFU;} zC3)z0f+w*^r^&yR2t~A5H4Bg|E=>@^bFu2BV!Td=XxlzY-&f%H!P_(Cg;hD0Jq-vk=+NXE zPNZd0C)`ibLo6rH6egc;s46x;W3&75?%Rzof|o?jM6Bub?w2z}f9b~2<`~~U6y!3r zn8okvjCi?Vc8-cFxUDTFwR0lP&5B|EW|25v?O|0dEb-w`RT}Ek`q+_2dsE`rdmK5_ zE;>9UHX3{db0Xp}^1oGLC#EtldVsdh)0&Fv0GN*>-aWT1`f_Vo9zWLp|W>4=QQ}K*2 zsuNIo?+VLN2?n+w`Dqg#WBQ|nxxf4*J@~Ln5gjO_0;A*WNv<;%-K%;^A|e$XT7H~F zeMCz&l$LTy-_?Jv7|r2#c|@%DqVWray+cF48(pW@k!V5Ar}BXp&!y&(g!4NjZnq9p z%~M1cx+w9BQE1n}EkZXv< za}r(?j}ds=Gf#nJ2@h^L($$-;S-5SCZ|Lle=eRJNi=RTC}wd$6j$GHRgeD!iL zs7F^eSI)tAxF!ij{uXU+8`-nThS>dZBUvhy7ZigciN}(-tvis_ zk?W|(pMxBMr_pFLQwuj^sSZL5yFw#%4<|S*Tv@=HiG)+66Da zSmJUQ-J-+A8w7-z#05;Fs|f6L#g_V;^Mvajc>zBv!8Litm@}` zCb;1G!5qoXPLMCgT%ZD}aX1wU1>{a~5syJ^VcN`5`IK~n5N}8vg^`{#Q2`1gh_sJO z45Q|9(7>-g>$Vzmv4jP7*7c6gyOo1ENh&VqZm_$^mBfn3?968MLQ+quo{-u+M+^EYN`!BFC^vSPOAOJ*aQAQRNwnu~;TXoIn{Pe6TWN>}>${*vBN!+oWNB~4w zC8@G4&@dn9o2>u=0%cn9P4$!tJj_KjRU<4XW7adQHa-52>)G*x*Qe>*Rq(6edcF19 z!{l#uV^MSLElncdQdgo>Eh95t`a90fuL}u}#5};asNUd?xVHRU_mJn3y3euAUuc4O z`R4H~P??kixEyEMu;_4Ajd?cl`yj`2x0ugt%f>z@I#IO>miXr8b6+xi^)!Q?gy_Gg zaw2H(l{zaZ#_A0VO!Dieojvs*yA12X5WxX2y%alq@>`Lo#y3)hv)uC5>z`)B;RP0K zEbPmSb3rWvujf%P=v*$k^1;s5d7}Kx#M-69qvfFI^|^0YMwjButj=x^G(7TrR83fr zZ%nF{y^ayt@pFMHA2G`d}#)o zB04F|i9~e^=f+rLxNjjtUo`WaF5y!wq%=T#KbKa1A_|K+S*dO(Wb18`2J2`ZXZ@el?A+;mLlkVSq?U&_V0#SVJC_*Z!t z%AN{LK|9ga>e&KqlNG0*88!>a^xaZ$74EdvwG~R~92_WBP|w})T8v#~x<)aN>`)EsRYApVl!Qv0K# zB7$B_dQzVPayE7#c7jM@5dxou;NK$2F2LC2Yf4My*07GJjlAFLUPPVS3IDOkFLK_D z64*>JyJn@1?H@!zpmXn`mBLxLDSXVJM`(=)79HfAJ?!Fq^YB)erzYbs>odk8p9}k+ zQNC$^5zv_tq=oeah}n1>!`^qYC3Q?%=^f}cHC-K@wWlm%t(E()Jj^!zKL4->ALJl5 zII{*{-wIfZH-@w!gw{|jsx)O@Y39hydY#Z`;=|?nBINrV9fK+X%z3p^i{*riRNxgw zl4L^;`{iuO)U1BcME()<}aDUq=i@y(m0aRM7r zYzEPy0ey^^tX=2~%YNMmUog05Y1C@Jd0OA~(ekbrrvE`4Y_k~T(_LZtNURQWGL3Q? z3@wOr20qIxTguCkPGEux|5@J|Q9Vf`d;pz|mz4xzzGZPcg&omS2R`MnodVkFmy}D` z<>mil*S>J%QGq5>~bQqVax^iZ{YiiOf)fgsC_ zz<(JuZv{XpJqQLUynmb>A5WN>XJ_;Ss&RQ~I4P_6b$(}(md&$MeA{KS;&)`O>_w)( z{mNPMu7>-i-6rF;>}FSTX!A9aAEBwlD+C$^^5^^LV0_J`uaggJ>Zg^1U=6%`_rqrJ zTQuxSKh}?w1cyXP;3Nf#BYN(mp7j&?T@d>_zObUiLmU`BR+FRE&j<9Quy-JnZXE%a zu-{W9re{_aOTAs>);ZO4Jmfx^&Sba=h9SIY4x8Zn!mqnX*5uF4SG|Jp0ifBB z5W$&*s4|q0^hTfgK~HO9$mb(0tr!L~_zLg8B}JI^_jp;&z>CNQ>VlM?Xsm@{7?}Pp zYr>kC5YA#dTb09y<()Ji*6+zI&s0CmZgjo;SQr~tpkdpqB&1gU;P(Kh5iN+TsHjMc zw_&S=KkH<3HNMxs_`={~kW1pSH+n;_e7Z9reQZ^U?}LmEtCS0KHe1mL$=E-n_Gl?A zYYP@&!OH^lD|#YGvyrIQA5;Cs!~cbr6Uw%FkCmgrCp)&f2Xrv~^@74uzfPbo?js4J zD`8-k3`)45)4mgitkMbcP*cGW&wR!v7K~c!G{jt^Q`lv%)a&Epxbf8Hna#G!+)qv^ zl3Xpw?t*9$Yn6;`j<2bAX+^|NvT~ZXu&`H}&wiNVSfH2-yg&a3bnWnW;s6H15pdoK zXOa@IotVxL(gUIl0cIRK4-Hk-g!?Wwf3f`dTJcjH+xOSBof6@pA|jsVvQD{k$qbPfJPQM%EV)e5# z7VFLOgx9`earw$09#qywK6+oY_vvKZji91Vm(^bfI$OnamKO8*o8CCL7H_$(U$Z zKOjF9lu|7usX>=O>?$dVLQG11IodDJQciWz*dUDA!cv}mdf;DkGUrW0z6-&vx9{`A z6Ly;Ggd@F=zVe9FP0|1stb)^IXE>$e=P>_xtzOAAmgIg9(Hf-+=7==&!=XFhp(F$U zq1e6DONj3Be6Xf5xDB??BU^`v!y#~t1bxb@g@ayFe=wNdZJz}(W!6v|;$R&@5sM|b zdcHxdy5k_^BM`cVm)EPCtD7Tm9E3heIIKKR)0|@E5`bqE+M0fCUF)CT%hPy|U0X7(V*e3*OC!oi&o$yl zEd+P`=B_@MY)in+!PsEc3%4CzTRLwaAc9_ALx-SPz+6}4x0gqdko`1Eh<+6nFgafD z#J}D4%T{eX-q6jGqx-`NHM0GK6*WrruDqn)$Mogx*{yB=t<&7AL-z}E`AScki#Eoc zS<|9@t)=_v-%!EwFIUApiTM3+`QICDF8AF(j{pCxK{8NSiUVpjsU75{`N2nO&7m05 z5Mn?&63jKjLEr}K{6q-s6cIZ={sj z8#S^diGC_@Zh#7~FQ-w$pTP)skrb`+esc+M$C$Lj_D_KN4F(~*8el@SG77N@rmaQ6 zU2!fT7}0tN)?s8@@KU(#z^XzJ>HTA$V=JZH|1gX*V3!je(0yKz-v;+Z9P~QsABSG` zPhG{vpuAIO;hu> zNeKk6zKPfsUo{~?Q(cbES0FIjh{IE{|I}ZA&wvi;dh=;;uX}=J#)hEa?1boaYbv0D zI245*a_vh|AbpDDljA6C4wMwB^AEkD)a-L8XJKrJpDI+~D`>*H8A#ecK;!-aMT&=z z!jE|Van*AEp)Xiw6=+yE%0p1AAh4YSG1+3!f3)B~TJRq&_>V34k1hC*NBECN_}6g( z4~VJ$0Z71b0$zVZ0cW+iO;&Z5{vGMle`lYxX))s;JC8L$Kx!ESCCNggQW@T85rzb; z>Q8;!Y*{&kNGZH@en_583-&KTVKwUBBf9U!g2iyHAQF+^JzU|pxu#|RvvXuA!Ngp5 zt@cKL4F<*THhuJ_;@SgYul9*%`%))F)zEr??*h++J+u^ zt*ag=vx%@#8yR5~nLbT41<79eWe`*5LMU7y$c%??Atx5*umpCjMJt6#V4MTmgZuW7VQV&^lEsWr7hQUx>#yGHU9HXk;nW*AYAx3`3gF5}e#vh0kFrbsr4fgR^4qeM%b0z4KfzoJSWp;0e<{!4}X&nSo$$ z*N`h4&9WRL=etOXe#hdF*gHbcp+X<`QDF?6pG&P0pC7EdrXE*a-(@@M_T}&O_u-==-+W(N+ zf=zN!827}#x&&FB!MPHGg$wH0avqZ3+K14pGcF(|1%!OIg2+AvxdB7kBn+_!_uZ;N-v+06eyy&)tZmOJBGBfh!*1@VHLCVX@?Wq+f;`9PvIap6NmjL$j-$AovYFY+R@87@m$grT*mNGv0&vxAbNW zwEJbe^NOA57~a`qqgf5)jC&oU`uwACB*EYrY;)E6QD2qVZ3ZPC%p&E2b@9)9$>Bo1 zdtg;iQ?v@)CB&%~{ryPh4Aw-Z^@-!RXrGFxleOD+CTaS${dCz479LK9z34YkdQ}75 zRENXgr*-A`v~K@7ttPRhKA@QoH1qT6SEmIjQTpG{t3k>_LAwL~V=N>Xkv?ibEmj|) z77W3y^6dh~!07&N^UtdrW-@-+Q+|CB7W^$8u4DAI3$Yw~sySCp!GDoCoQ-VE*q*W! zZQ%E^!>Ah|7c0M4GABgkT#)r>ml8#q){fN?NNB7+G*+t)E0_WnJJx_9bm!{-V(-1f zn(DT_Q4|po6h-MsR8Ry&l#Z0x5CH*21f@kqr3r{g4~c?+)VzSeOOdEF5h2o%7U=>4 zQX@TuB0ZsmK$7J*@jYjM=j^@Dz4x5^-TU0wdNe2hR_3TDJa>1i~|aKn{n?Cw3D5l3vdPX7pvOGNS>HtK8nv(!gvsR6@B^_ zhmW^>eJAn~`CAq!zSYst92>5wkyD=0@Ts@s>XOoY;9FU2zR*!rN@wI2jPf^+GrW>o zGMN8SAN25iWpaAf0rxnDq^n)35BhXO7O53;b(<71jq4FOUoj6ZzlHD4Juyz&uQOp z<;Mp!{8X1zUKjMtJY1c}d}H0KG~LNM0!jc=opPwtAuK)eF4qz1o|(!=1g8V`htEXc z+OBVp$OO5j9cLL2(Nhm-1c+-07)#ZFr1?~)D8rKu|L)}qZ&=^#e7hI-xLm0$L)z?Y zQNefN_UN6bwSp1qslKgAEqs$nO>NnUW)^n-BbzMDPiAUgQ?q8~D_IvGq10hx9n)sO z-p1NPD}T0{-KV<5&A{pPcGs77K=C>yaxKr)D^Ik}E8F0%BPy{aE%U?gpgugGG_~Nf zSMIGJ9ojxSt+}I*kAxk5&>gCSvfB6taK+wQ5zI!iwjW5tcwc?pGp3wO1>560OcD}{oKJ0zT9>s8K@Q=D=YuboB;np#%~$aDY2 z!G}ULL5HSB)OUL+iyt$i7uOEpr$Bz5H=lK`zqa9x<7W0}|9xNYd@6~Xwav&K7O{Um z7coNPp5LG>?V1r53(Y6Gi#VkORxYJX&0VetI$_EDw)hPA;-5aO{W39T<#Qx%FxXE(ruH~H3J#5RBQxbox|5OiQLFHbwGSSIad?cbR5+`+5U4X;*gj1A=0KIYclwrw5S zy*Rwclv+-S_?=MX>i{MAhH#b)KS+~x|KivynhHg{LNiQYkO9sUI-0Y`nhlH-5hN)m zOkaas;4zR31VSATEI^ztg38Z2K%fXW!FM{19sZ6KKmIn;IhH5i?Q+%wkz{_+p=bTMxc~%X}byiPt)@kY(z__aPl$oBK~()s$d z&*Oa6jNw;$$jT!lhfLZ`kDqDw&ivMbyW_IPB(2wBI9VqvD=Mfhp-5%Y#?n5Z@@4ym zs@wLSyBER3L07FJyB@egOAPkRb{;4@o0#gGZrfDaBmLKfiy%xxHfFoJdU3o?Nd;(Ja-VmD;q zKwzi_PGG0xM%TO(^%V}b2g%c07UBY~SXP0&b2&eOSCPeoj$M)eNr^`9Rf2iK_uR5i z$R^~WhSLxAvS3V0#@yAOkB?SI)w-^q86FyHEY#aQZzp(=gZq=<UQZOVZ z!m!N=USk6VbUaxPYQ9<5I@%i_%^dt;c;a0Zzsi55dgKR{8{_330x9>h&c7K847TW* zIACq#=OhzHiCTEvhTi8gM1Xdu(te`9ZOA~oLA4rm#s)L554yzbVWsHjr3zM?UBKMsAc>PrKm7-oYnOVA8q7 zfae#%^_l({ZrN;UavzMZgANmLsL?#E$~?DH73aobBHJI>bw>E(}W9I2mEO%`#npKnnO92K(sweFo-vxC4RuV zCi>bINHyL^EhO~oo6PoaDm7cSP-CP7Uy(9Gr)p!%3K0hw7PE6MGoLqj+kZYl`h?aw zRD8BS==$+^lEG{EHp{1Vy?9`1wP1-b_D!Y*p3M~wXr#h>3Fp#&MAlLG8D zuBXNMir~Is2SJnRZ3<|t!;l(LBHw|u@{ZohZ%opC_}0}!dcH?(s`d6O``!q)9QdYC zbZa8(0)Tba=rah=XHGR7Rj-HSwyLv9Fq%d1JC`#5rpOK6+}nCj3$I{L4;`}JvHBI4 zNYqy2qK+xh(27By8B?Vyy(@xZiZ|o5b6QOQjFJ&&Wz+72^V3S_u^Wc>s5KJid2^ z6VVH%Wr4x103e*9O}h`4`wW(`7nRcj2)$TtQ3Q!Qw67?7=;o)nUmRBxYtI6JeiHmI zuLC52Vcg!o01ZHWOm+IUUmWRU?nzY(@KBGX2*ybI?FUv23<%T>y&0PBc?7h#oh=66 zcV=$Z)a0lpBqXF%XH1W5a+0z0vDD(L3@4ne6;hLWRb6QF>2lLiNAC|` z>(veiPg2kSboq)W#vw}WkroUu*faDet0wIO*^O9trh78I=N4uyK-d8{rn)BFeDo85 zsYfTy*vd@;?dm;_3jxR?WYcF;Kc=9J9;7gy|8&yaiR|lh$a25mS+~9F?w1#<=niUL zcG>)q%R?ga7gX)98%viO$P8CnI|rIF?NI$<07-I(^PvJkjZe5tU2 z%rukbk*wzPCv;#%(Zr9E3g3}}ehy^#^Yo+Cj@r^4Wga%pmc`b2%15}}lbzqbDKjKKNn7hf!FBgO z&6>P(ZBH>+l6Oo&+$YgE52ameFWiXX7a@r$=v|E4%wX~st z%0n9U7pTC({3q8k(~bLIzgVtkUVP*gm7t-?y~780vFQb2|M`okTc}4X+B~mdJU6|( zrvYfAieOW9!3JuO3i1ZEE9R-|Wr_v$qV>%B_mTFZ&6(r`X^X=TIh3EbsBS)=d`M^q zCEc#53oK1=M=HyMP?wtFC@<_ z*yu`K>zwHW!0=J$0dHh=(sIqJoqh&suT#(Of zMEO(EVYAEUG`>_ZQB^$ zvR2Hw+-kj{WAEm&>Dk%kdY(MC#Kxf_i**fv({5Fj&G3HILzFScz16_Ta7dHv`EzDy&70=xgH}^A9ScKruEZp zU3nFXb{Bu>H%Wn>csGRt7sG*`>q~)y)R$n*a*!ZQ$Kb$kui>CF*m49$HRK;MY924a zJVV~}qZoUXR^_W-ixBrw`((Z0*gX+-DS}`3Gwy*qaLpu6Mb<0O6n=Ix2VfY}NP06v zUqlOa%y*CFcbBpZ(9wbGSXn&W@K8kdF7>myW9#sEME~uL^X_HSb7=|x(K$VaHft6;{YY+S?vjDj6Df2 z-_f6bLwtx`lcrROfKd*mPxNJ4@-hjZGgNRvq!Ca)K}D}c^|)- zwIHK|s!vZN)NVsQte_1zK@a)yDjPd{B5Ji21WL>-Ss5(D)GGGvH11s{6eOI=N{abr5F5? z$vLC8Rt&W1psRxer_sqVtGhR^Z<8)O6oS(Bb)YxV!WtBZ0<0LZdupP_q^lBrCxXpG z#xyuzXYJ0**#6doK*bGWcFe{5W{~I!xwa==bl=(we*2L8LzKdIu2n`<+Y%!Kl{o$aCngurFu^mlrT}Diom0MU0X+=tk_n zUQ?w{H!A+9BB2{$!Drw7ggB{Q(PLTJT7#ojj`Z%UG2N4*w%7LgKiPv)aUOa!q?YUH z@K|lc%J91(e)NF`{5>w4#K+C&(4E&BbS*)xAQh0ZVWKGC+M_k@nd=nXo4xSCRVBZ) zq~9-NU6Fp1k(-NjXJfS$$Te2-SG*LQTix8GN-b_(l!!X<@E-S}R$M5W2y#)Su$R^? zN#nezb^x2|wI+s~C4F82!?)u1$ZfpX9X8Tc6Bq{Y;qoICuzdXRc_C~^-b&ZM=7X@C zzfD0TQKEd0?#Xq|S8ty4!n`WWymxe=X z#Zeuoz1?tBMcX566 zy^3!l*Qt+^PPBZZ&I0wiq|h;KWeTpj6CnCH`45^X*QDv2%6!KWvPS6^ZVOuF`PE&g z{L1uGAFoN^V5yh)PlS-;j5V^dSSfHFaC-s-`0CVYOVELJG|LUccz_Py08erUynQl@ zAV?TRWuiI|)En>}s6PC`Bh>V;4BzyYYbj$!35n3qjxcAwbTbRh)eV;+T@|ExFT{;J zHBfy!cX-t1hokq@l;gs6D~IdPL-^Wwraue*yvH)>{8aIhbVlSvb4F=AIHy6Xqn#y6 zns?iKi*?*y(=2-Fn2WqT{xd8bc*l0w0np-@%2)xoi;0KS5m{q9*tG@Pvcwu0V=S?y z#X!1uo>b;z%JVuac6*%iagW#;8&DP-L`0qeC5R3!a0WLBds`1;fcWAW_uBMXQo0(` zP}%}|<7fpV!VmJ!Z4phxd($T?$03nLk_HvO*D!OLMY8k8E19|sujq2J3s?_PN1&ec z(*rTdd5$gDYL&$m%!cRJ6E|*d+{eg(Gb*Zym{*oQOkmbunDw5~j?X(O}@(n7=PbSON+q8C?uouVh zI`t7JaMx%T{!MM(zpc~zKl~kk9!^Zb?yu!%xpyC?xAykoD~ajHU$}lQzA|b8A#uG`LZfGoXEj#=F+2mh$+vy-S1`OSAyQ|J0$+6jRDt} zUWf8s4H-P5=G%2kX{-kcM2{bedE`7fvoR&N0knVRBBqwQuK7*rThqPAfBM*o8){b* z5T3>JpmuZ-GaL``Xyy3Qn?O9)+}xl)a0IhHq;jQbzhYf+pt=2RJ-F`qIvt1J;4JG%!}k7_tO-rU8wgfB)Z_@c%}h8l$kW-dR{XNh}* zZc$DuLj&j64$bxHNzAGWHVnk(>71hA{sV?wc=Q)#%}$y49Amf@@1b=zG{_sd$gK4EF}A>m@{u z{N~=pMYK|&eD&vYSK;$NX^8Zr#oI9= z>&llMy-%Tw=br|=Tv8w-Y(F%OV!R)4f8R}k`f{5^<6?A6PTZy7HC8J5jx{?9T07abhC$bP!>t-1L$DuhpDmGk2>slRO`Oy6V+Qh2(>vL08 zc!mtIYaxlc4%Pque>MeX%42#s-)Ki~j+mL@u#*s4anh@>zaobpaoTjK?W1^QaaA1r z1_0S_bD-13WMOtt>P7@BhMib@SiOONNUC0pT-H?FM-o6+v^aH@iQ#R~#J3r+LWf2e zldF71knxr;|Iyr0HG-MwZS;!>)^xpuiihTLd48R&UL0+~CA1y_Fo!JW?9wiS&yZL)-xK6@`G zalj_+k^r)*Gl4XnQXZ*&@}0dx3_2E}diG}c(3?AafAVPir>x+38F(|$W_a%B<^b;O z$ZSgDXBXARcgJ2{nY!OP^xPc9VVPftluEv@;&H91Q@7K3wqtCT-7_jF{L;qn70pD3 z%aYT?BhNT<426Mhg+o! znqL2C%#1UYj_)J9gnBl`7-)zmA%76pRtAZLgPZ}!MY}<2)HHEDED&Af`io=LkT9(| zB};zXS&g4w?deuf5K2tXnPn+Hi{TYq^Y=kiWRK;K*)6+UtaI;$@ri#>^j%<&O?q=B?;wBMtCM=sJDq(9rZGVvn;DEcdA( z1wpkvr7OJEDj$#j8E+R)UNBmKQ*UB;hNmH&Y6U;A(WaEL;?6h)JXNGE5y9MT3~DWm zJs)T3Uj4;!(sTn3ckH?t{efvB+=*aomygbOue7lC&q1cTEl`~gqSa+BvdN!MSC+a! zPk!8UQY@ru`0LkmUY2H=Q9;VVErJ`sJ?{X+#CH{fq>%0SvK9Lpqu56cOKoVh6k0am zqt2A`OT28d6}jBF&p(L&q7|bgm3-p8j-UNTG4_k!W$j~TH!N*{^X$2YEGOX^N7oxn z`5196X5}>D=*(1;&oQ&o0aa#MvB4my+Y4GZafQ{T%hhG=xe^*_FYPMf|O$s<+4YF0;)bZ(3`Y(KcPn`MjnSEv`;3?xE4_ z@10FAMayMGWh4j#wL;(TQp>|mK5h4oU07uZ45>ZgEqWKSgSahiuP?3%9)T!XZ_I;* zk4cAujeL#rh;j4ED;dV91tHbjHXI!B9NWHI&J*g5a!dI`B_%5=;%?`pnw0xVd!Jlv zRt3@>=buum(Yk^ZjucvpGv!M7nT$h~P^3)yoWux}&h z)9j0C)tl^}41aF^STeRbn=+fsJkJE%Dz*FB#V8?t7vH-pDR8(^ziV05aG@xdDeVYF zss?oh>OmCYBk>RLqH0NGYawet5q2DOOu+@2QCW`rAgLGl7Kl_=3HxoWaH~tEb2@)@ z>KCOn*MU=9ubW?|)8tVr?o2L^pL&6<6kOPlOe2opG?Ni*SlC}O{I=}YsdoZWC~kbk z6#huNZ|Z@1jlnyCnI0m6t8w0|V_lX0@J9BE8gWDh_Mq@vP1>hFGe2&;>p_(pC40Nv z>|qml;~iOJ`l?g!bPg#M2(w?@NS|2E?WoAx-}L8KqB$k( zEg?AztzJ6rh+`a2hH;WK4k#wN$USMgm;az$Xkp?bR&Gfqzib}K1TgYE`*Sb-1?>(M z6Y3_)l6u#9wOG;kGRb!YK9d3h3Y^u(L2$@mWD{%`>n1UL_nfXMZD(A2pWV!IT{*OQ z-G(}yk3RnxvVPva{DbsqFU`m+f82sK$jWKD-TWYBs;Ffu)*~&wL-j$o^rqB0o&>Ul zk(#7&=8j=4k6zZyW8ume2@ zc#37twAA`|rckn-6&rm5ivu)!ijisFRV=JiaX-q z{#fj){o!`;{fv8sC{^ftts?uKSG(J4@xueW*U<4afGKu2ry!ZZ{Q zy>h6CdisDrudg=Ar+?JR#i@)5^21|CCI{E|+q4Ke7AN zYwa_6d6z}vr!Gxv>b%5dnzHtDlYH-j{dM&iJXHxyT>j9pgTUaJ_i+<`1nDu$+LA929WqNwthKu8nSO{V+$?Lwzv^Vsa5&G4!Xy zJ?j3Bm^NRBC=Gq?#GZyJQHRnGiABA(2&^lAA7&5>@gx(`g8}3;UHR{8xPu7ieZ#Gf z3K1h;Xs#%p`U^R_X@p{yBjd(hS_27HO46|i$a95Tm#eL`JGT`dO_RudaEJd;9j;T6 zaZJC(RKjBQ8yM&swv@1K5n>bE%-UD>zK~$V#7ooCYD5PGiVr<-@6i-hj?n-2}pyAkM% zWRF`YiJUmJJKA1GFLN=aCbev=3M=kQxrAxZRry_)u&~M;%)jT*pe1+3{J^uFk2T3f zg-u32e#hTxmkSFRfZDmq$%G%EcJ7IROq{{2^QnHe>;8+ElhY5Y{0(g zk1zj~&Cy|o`-k%x$ct3=XkYlA((D{}?SfG$y#xbo|J`i2el+m|di$z8I;kn&PwB*l zLP>j0S9f&{J|CrZprR-4+%Q78NrFG9>qBpD5asi0i)e7=oreQHZYo6!1KC3WbmJSZrNKvOL- zkz-nY39FF{@Otmq+KHh0!3W-)%%#&vh=X$mq0nu!fe8J)muK|)jljtz6u;@0na*ki zIjZ$Zm%vO*iOzUaz7#BT`6PH@qT~g10e|AG*+i+wl%(K~2H4 zk<$(eYTmqQ20g`+KaO``-=V2yro%2IpM`DAQ=^X1ewvDZV&3t$?kCEbhnlJqERJE8 zq^>t1gP8)p7*i^G!JcSqWR5THeah6iWZ>cDCD(LNzVNfdJr2nU!Yhi(=hms3k2%5A z4L3_mxwmSR8>yBL*^_SJFdeK1q^#EYbJ{n><3?6{rd#}^k9FDF8r%j@RKbUWN%$!A z{VoY7e>i5Vq`@-9zJ;r_tkCsjubTeu=dCh7cC#W6IT?(2UNJE?-AR3=#HT`~zMslo z`cQref1dHY%t599f{|??!OHxGo1yZzP=wGR7a!-eqv(IlBgAR$Am~@zxb3{kpniSKVBpI!`hW zY(etQGBW$K3~DjeqtpLh#ismMlH|6mKPo$4JHQ>|`V{~Rs%{uY+YHwT+9&CKg7Evp z-Q@wp__v$g%zID|b`R@3Q-o1`u}9mF$Tqqjf0?OMlU0CoJ=s&VAYyJQoql$kT?n;{ zUd6D^L7w{V>ZU8@dt5v|7sJvI-5on> zWzfdT`(T8RU3-N@HupBQ3w`RYdU4mK=H0{+z}c`|#3#Z6FLk8>&Vkg+azUw4vy;36 z3ucQ>s{7|xpXxSHc76Pjm~iI_|HYJiFwMOT8g-qdUT169Vnr)|v6`3bH8&_AUSl;T ze8%sE6qf_HlSJ@$I<9?$W}-0&_4|;kPi5H&k`^WsoMd%`67ksNDI(Ij>FzAa`J=Y= zTha5*Yi4D*MV4ug%lMFTojQVgQydNo+1(z;FW@$H54;E-m)yA=_T)nVpi2*?CtyQa*OnuL*U{dp+}~GoCE` zeRDwkfY`E0M1cq(o#k=F?sQn%G^tG zv}%=w!^7xj9DZqW0hZ)+-zFKuIkNj+$7#_Qp1Tv$tPE}+>n`6+(-tRB$%ifHPCk8q z+0eMEZ0Up1s(P*Pm|hOjfI8XK7;8N|Je$~cvY<{t_4c(m10u)S6!u5pL0cFmCqZEiir;C8FrGC4JCS3LKpPCmkUEi543B*IIl z>)2|Y@&Ra&bLS@pjqdPhP8of7(s?JxHP}|XL8>4>@9?wHBL!*?3$7)Tj~x;yO-P8d z-TblcYt$TdS?|SWzJ&C#JU8Q#(P97L(pB9U0JO3h00aP%#9L#|>9GEiQ5WNQ)_^GUbS za$-!@BW2CgZrfW-+oug%tNhE+r5061g~W;46Ze+CEeXEv&JZ=MS$sWT&^zPso$bT` z@Q3Rq15W%g_%t=&p|MwydfRD9V0turnXxX4ONLa@btyQ% zf-giJ+2O*-3v?lqo#gp&_0?fJ`=+_C|Kji)r&=KVwUoQWeX*w_bHqMGA+E@yPAwyj zV+v~bpzG|R<4E26VU3?=YE_DF#5tUIddgiA8;wmlfG|dCfcfZUouQd~QaM`XlspYj4xBiPX~gWev2Hy$ur8L>-Xy0s z5i4eSI7s7gOhWL`b89LNoV_ZR@E|N#HsDfXSr1`8$ulWN`)ad?&e{D<-BCL6?xVHk z<=#wi&M+U|1rt(Usj9c*77doVN1$poPd4((}NH$$VlB2)-aHn z%M>5#s2V*+_-z^q({8G|G&1{}ZHPk31^lZ$qC5}2V=KTeI^ zQkttcJ$y9JV->ekMnQj6bvaF$)QO0@H>S?3`S~`^FT1qdv7lwd0~48y+fNj-jmp|p zKWjG|=VQ5aJRfJjDh}~9qPBywN_8u+bpyh@5L=M<7an#~N4v4Z7El(zMC<}i% zaQV`_a?9-L`IaDLAX5dYHb5|=E*4VVFZIRl=FQ`MeRk|l&ik=U2}z1-pKmJtEkl;F zM_}5xzuJ#FZq{26-(FE#IdF0+^ycb)ha+Kyf(&#|6d@MAQ?}F4vk9EOorV6kJ`;Gv zr6+sF?8}Yofd`xd2jDKcFqS79TYK=fsXGZPh?J+YYZ}6jV~qPwx>{H?Z@UwK^uYz7^&|rcXQ$K#PPI}z)$+Rw@AZngxTAorCHsZbD`(g2WkZWzL zd`-+ep3_=9JSVt8zBA*xksgq$s$SHlWPsjQzkVo)kN?(N1-1#o_&@^iZza0~Ai$00 zfHhzjngKGgpgA4K2ttBn?K$j*Hhe=0G~@qO=lgGO>ITt}$p5VK&DbjQMSs5t`qdpk zr%4#=yzUv$uWrgphp}Qo6W#y$TYv}dzX5vIkAU!38PsH5gbrgx0CRy1WC~hvKtnzr zum&_7KzIMG6;0(IKkQ{LU@q7P_TxZQy$qhT4;<9-69jHDU?Wu;H02`za{wtE|1(z~ z6hx80C@Jv2f9AiP3Gk$sGN4$Q=Uga5V%TbA+vYjR+-V$wOa zY5DUUz@mG;@v{<6bg|ok^2X(X=Fxbf0XwBu+hvT~^_mO($HQ&(ZdW>5>syd!?bKAW z_P0RF>(6wY{V|FSR=t2)vAeq0kUkmLGvj4csf!NR*6Lc8P->cbrFnQ<4%zKrGMX={ zUWC$5@75Ds({Bj*&$90Xa)BuH6R@LeRAKg0Kx#kCp% zCf_ZIjlw|j%F{z9-r?V4$j|<>Fa`GpPfUa@qiO-fEC5{a=RFa+gzpHd5&s4pu?TJlQNFMZATwT&)tZQa|C4JR&^QqO|GQ6y>Ot9!ojiyXfIhA=U@caPaf4H-)10?-vkmU_P^8B`^%$ z3C57gi7dr|T~JyWrZf*BVSBp?O;&KG-25CWSX_KptW~Q@Zu|U&;HYBB)oq(^^#nH% zfP>Km`i{~Cr>+pw)AtY59%4|MCASR}RC~BMD1a&R%8QnpabPKY=|K1JP6W$0P<<~O-=V9P5^#{a6p3v7ZFrraG*%e!VyXrSIK89$zRq60xP6z@Q9K6C2!JgB1g)>YJclOL z_XBrw^clRoO`Z`9zM&B68()1gUcwDYD1a}HlV|P%v+U%Hv!G404)hXHp9`F=LW@7n zj_vAERP^FyhJF5AonoXhIFYD-?DdbdMCmvaV~&8oh|w%pvVKq6Fjfrizz@Ff#ZyEW zRv0D2;J#c*)ytnKla!zt(-T}(I{bXIFURm)H~J%jcYm5(Oy4%PP&c9x#?rxTowb&c z2#!jmlAM2WuwoIp09HHc|CRvEZwaWtP9WQ5|M?MB;+*vno>b+hA zD-`2plhsS^p5qH+XUiu$Y80(-i1r`1d^&KIh z;tDgM)#4oN@7|@nHX9o16j_osk2%rv_rwKOF4`PF(A+0Uv>)A{7c;JSKVA+8u0-KVkhF=_46acv| z@Z=MI@J*26U40LG9(WW#kqYDukpcYmw+%Y*(6dsEH~-m9Bl(|TZ!e66#XZB%WvxSh z*bur(SA|&u4Uj3}3u3K@2?jsqGVNOhUGl|muEW{Ku-0D?%Vu}ra|>{`EUei4x*l2p zJEO7nVU{c49l7(+Z8x-mxZTeKv(;N$i0EBFpGHvq%UM~4?=v_|B$+{gTG``J;-KX`f+evZ_((wF!4k-!>rz1}PBmw? zue-&7s{6t7@7g8H`HHPSD!OZDK9_juGbOkb8Tm*_WJPsLV{KJ(u{NF(&H%EW4MRo}g1H|_czr|Vi50`39 z*%5H!0w5ltN3lPlM0~lesF?byK&j^R6TPcOgRUyf#&iw0Qv%1&B!0dl!A&_FMaTWD zd0pD1Je)o%G z=AijD1~*vU40PL776@nWL%qK^jNDz8mKaJ)(DfvWDHFB`L^GnT7Tgoujh{S@+P!tN z6L@5K=}H*ufE2WM7XwK1j`s}VTn6_ST^Lm|KVx>NF=eD^&cf$_NwN0zMstzrDAz`# zVa+a}E+K>tJoN^eaaj3LYUp0biZ<5-aqFRt2=yEKiX(~N*>fmd$QPFn_Vxha+JibTNfZ_n4 zEp>HpFtfxDpwJu)iR7e!jR0SMqwZDQmQbGxcktL#mwZZr%#kePQ7lmYXj+0WJH5b3 zwZ6*EsNLy{|8lmEkhlqtR*B3PqFY<=Xg?mg)O{xE)8W&5n$L?aoP&LXFKlU<3+nrL ziTjuFe-W1UKv;8%&Ol^LeBwTv#`s&p&%{uNwbJQfdEJHa9>c*F@H^9O+!K)4F zM{8ftaZD;2sxY90l1WEBMem%uBeEruZ}{bL{rrRL(B1*|TfBqf^ICrD-bfNGBuNY& zv>IS5)Mj0yOUsE9_f0(JAphjeXP!K?@#CbMx$qIqFPu7g6EKD>o^^1%1ylwq##kX5`Cp15 zLRV|7jk+n__agN|Z@CT~OuT*msDZ@dsJu_}nI;ajDV*vK?jf{*-97go%tlFF8a?1hVz#OWNgCbBOf+1JF5KQLhU@&jc zj>l)r=n7-4B8&wb-&Q!qk#{{ier4oQW5=-Oa0mJ#f_fDLZX<#v7s#Mw=TN{31h=G= z73Mh!IL0;Q+gW#PuU~GFy(1N*Ty;hfX|HX$vIiKCufX1>GNvIOYg)TG<2f+xM%1y~ zJm(bcl#_)n*VzA8k59=R2H2ltv(d>&G#cjRaUY;!#=dxtuSQm*f7$e||za1va z*Dlam_NkH9OET+gn8GwIqC!AEk0tE0V zu(+IC7B?K2$4>PP)dTjoJDK>x*E3!+RI@-&TJ zN?wXc+{8y~E9T7kbXz$KIqmSYvCjM`wdYaeVZ~+xjU_|`5K-)gK~BpO6SjtDE2Q9Y zU4QQ|-ta&BCoUNNk6yV0$YT(kNW=n1O*O=T!h(jeZB*{CYVHq?o463-Kvt4bmg$eSDs8245zXd8WINV2tPk^c3JR6-oRMuRGxQdImrDPoDozF0p=b{1Ypmx486Q zS$|!rzdm5x{RQa3!wdrBpiAX`iS+y6B8H1N-2EWZy1G1NQ%Y<4jGJbL71WJOO%a&@wCq+@b;5gMSw|NEULgZ&C{xI=+kG*aC3Q{}BkJ-$kvH5K&-8%|RRkM@XXaz>;MO^=}e_0AAF_vIEeY=KNo3 zV-d^7JzJbpAV2aD$pnOXYx`N({KY(ORxan%z)_2KofVT|p$4|;0|mj9Sx0YLKR}?-bw9B{AA$1&Vh&J6re!JM zFcI|xx};3QvE!LQWQ%ZkJaN@@<1zw_G>-;3nI8Ee4(QI-%x-f2bHxK;mq03H<^&Y8 zg$=NS(Oula@-z5LI76($`U_g1wdTaqdIP~Pp!55-rTageRTKL_-3Dn#}}gPQ#e?^AugCc9Kg6+;HV9N zqga<|5R~Hq9H)hWL+q%ES83~*>hnY$YyJMxm#bIab-SNB{`#SB=BqOT=S7BJrq+7K z$Zz_AVeg~sj9I`abD1&3KOoOB&{>Rlqn@WV_l$*ZmA=0hO3J?S?$sYsvCX4JIOMdp zWj&hezXd?+&~*3^dPZ=81EqF&{pTM| zs`cO2_ZjjCTnT=7#BHNqr-eCMX;GF_0O{cy+eL2j~716#;N%N5a_F4FhZNY*VabAN}28|}y+ z@1;+iRNfk4WsaHw?ZGf+fR~zLq`pC-XxMN_eAaFsL-BnB#rPq?ptdp&B6pd3cq*O4 zxkWsX7w6(@Q|^mk0;XuaEgWjJ<)ng!6(PglI8=fJaOiI3EgZ^8?BYXS=|v)FSd-K` zf7O9}dzV8YHB)awhlF3pk$zkf(>rhxO1YZ|9s=<@ItT26%OA-6X9%q@^~Eu6X|SHAh22EeHq*uUJ_ zMN~JQ6Zw}r<5gHii#gK&a%bc&3HALax7^vY$FMX3<*~}8e-v~yp7?4@>z~2trmG0{ z(TM8*lH5ag@kEeV0gyuie2zRy(+SNF9d=F^?^kubp^veOuoZH8SPr!19!Ij&#j2;q z!y+xJbH8r@&V$0w5=0Q~{(>l9{7o>KbALe;n~PdkWA(Qn%K37E-x5BGFAJMTkkO2@ zRb)VdwgjlN6hpL+kc>5v!d@;E1IZr9=&SAG?S9vR2WJE;&+*)OlHY_%1TVFv1y^yP zvtAA~jPCR0ZW+RcuDRM<&@OHEnK{NA0bE0;UfQN~ISh5z?ML&NBj?5>kd@TGZWTcj z6JXm{yI{TmE^_vl1X|z1T#8T-S|&RUI7Ap++%LU9x+1)4T7$d=y1-GG0IMA{-NI_% zj;pleAPyT7#_C1r2#91?+)gL+06@C$QKQ1R=5W&%R(q_^Iw1cqc(ei9M%|n9FL-ok z>B*Flp5J(swvW%_^LTPn*_o;o$PfK{!rk%BFaWT>)F`)%>4Fe{4XE{iRQJ(Hz3Pe- zuVixEwMK&-_HIXkT5G)FAIAR`J*1-@$7AgNiXI}oH(R1rBmasXI_|`{gj;My4*;}u zZqrymWME79n_nx_pD;wppn}Kc+L(S_n+C zp*z4hUtmnwswrC`6a>XvF?25TGV~SxJBGeJ>{iAR1t0#RErb|IMB&@x1btQ8z_+6vdP`Qv&n@9q9FK%b&ii-IO(e!s3> z-?pt(YMljo@;DZTpGOaX2)oP~!o&YgEAW5(U+`AfxQQ^9Wb~H5q*?-h2|m9D*Rr{F zQtv;+>kLqaz=isPu1kV1$HT$q^dP(nxVB+A78>^hV3&XAo6o`isdMILiQWK%0dK;N z%hv~FGMhTUY6GA34tU0`qulM;IZ&(};R}rVz!W5hTE^*yr7*VE&(&>g-ENRi+R7>Y z|2|R1fu}wIIHao?PdD94u#~mo?*Ye6#4wIq#(=EIc;R{&i<5ZBw87t`Fq83akMnKS z&sm|PZ;kxOhear!cxA;$_CBf%5NK?PABvluh9|yF2R+z<6#hL7#VZw5o&HSA%7F~Q z=<0aY$<$p9SI(WhaHiQa#2Jc&ljhLCmk2L`FoR)n8%=BD*vbjLJoWb&;&-0x?=QNn zGa<=xrV?mXL^0r?Uh6^9FAfQ?C-V>2-tafUegN-zVk>h1ALb^3eF~f~QoiDM)DO3= zjUZgB_L~SDBOSXd;_`s&>;E9{&BLMY+y7w_iX_?BX(6&EYmrg1<&qY=m`a5vw23l| zRI)3DE0rlrl4X)*WE;Cm$X;ZfiY%Ea%fy)bIX}aF|BgF7$8%lR^L_sLK7Vv{G)J?1 zKA-pde4pp*e7#=h{_Td%SC|o*!|=PV^ds#$N=EiO<;}N&lFgqtO1|)b@j(?Z}j%%?c3L|Y3HZy+jguxYm7ez)d7-kLO_w9 zQ)AvXHk6TukI+GKJp+E&yNU)oJ(AdI@Q^NNVkHW1%aDYMRUVeUQ&v;H&AG*#qL(Ll zsZPBSuTMsTtWZ}48H)ZM?rI<CE`Pt}lejnD^-r2`3~6!wQHu!SoFWwjnazgV`s`S~T?{MmGPc zm>XH@Ua@UWy7bwqAhZ=^qoSMJrk|#@C{KJLtt%@l z1srz7_X{hJoYR|mw~()|4tv=Sr~ZP@LFH<2q^3tOwmv0Bspgb=NI2S?yYcMG+f-2U zN8O<=Gebo#;g`fpOcXi~S_F7El%P>EeZG>pnd*}30olskN)4h`u!DJfYAIyCP3*wkaI z+j1oSlxq-}U9dCefV(A%&+l4TO8~FRlm0-yDtL!efT!-t+=;K;QNCpag(`I$!iUTy zTsPny-9~S7C}{Q0NhO1V=HrpKx0HI4@9nFrG}<0Hy@{Mk6s8mZpvs5%@L?U>m8TwV zHYde?`~Lmgu{9#kAAS68HWBpf(TO`1!Zer^%FX2AplVRa;~F&o%A_^)GHQRViz?0T z+O(y-NlQLqU3YK)Xu=)G*HYTlJ<*Ql&L3=ScFPuWvLQ}z0?Ev`3I)(zX%Xat-1cR{tY_6Y)g z2O%F)wQm-5!d+o>VpSsHDu~D3i#rLIQ%C14YvL2)eb4^sR&Z}^SM=U9!Y4Pcl-Ui< z*A&)>Z-oW~{x|`<7ETp~%pYXHhT-M~l&irHgX|NFz^3yRG8?Hf2`OJ)M6Vggt9X4M zlDxM4$dP?!0u_`~axOoc{D>|5zakVlc@*?SHuc)fd7A!CwHp4gJ`Px97raOZ9k5qE zmqmWx<8aq(z3CzAxMNB84jZD>7YmTTm3l4ZU6m+qh{SOyY0YLpysDY{9R*QNW-(5X zO*Zwpi-^XX1>cdSJ^T<>;wtu~yo?SlNI%J$XzJVUt{@yVZXp#WadkTN3LXfb>)mo7 z%T-1EsF+el-GvZgMi1PFk$yOPIq@IRNFCeL&9(rABT{oMelI}G(pqRXI)n=SR-Y(& zvpY`l&ZnFD#3Y2`b;GYoeV6_a=kF^1C5EoM8NfLT5x}|efI-Mu=-h{?eRr}v3>2<8 z##?%M93#0O>hlZb()~F3wxLI?w=ocw@UI6yGuA+p`A&vw;Q>x#Z2@Ic%v(!?VEB*m;cm&lfzm>G4kAlK}$A3aCv|;4!fN|-R~V%%w0bzPA3eOrw#H4&qab( z%Jbs=u?)3u%2TtV8nQeyBTo+HK0oj_WtSPT2tA~UfhNy&GF&fRu0iIvgnY2Tz#33_!&l)J z(ZFFq!uLW!0BE4d_u##^HhfqF8K#VWlPdf^eF(xY!+q!+zd_@O@LK?w^~6I$lkdh5 zY_8g=78I!9$6uFNMv7L+NqfQNchLiH2tW6^#gND2 zPNV01TShqM0+t9y?3ur(DSf!{`zG(03ojfO-|^EEfYlyAub4B&zt*u}l1=}`W`6I- zgwR1uyD81^-<{&W`nTOOrV;_Wthw7n^JRq6@YCP$a~1}qAo8cw3Vc^Le(n(Z)YIf} zrX0HUEOJrAVN>Q>%0jrowm>*tsfSQ2#|ZpPF~7;WVl85|lcf0MMd!6cbA94n)*UC@ z?@?^ot6oK#T)26-kDNTZh)Lf(90>4%VIaiwK{+@c>qdZX@#|}2^SERd(&nQxp#|g7 zxFK<2uD2s2Hbcu^B9q?yk9;)wIi$`B*Qc#k;4Af+t44}DANr?_Nnn79V=<=m)KN0R zJhn;By;qXc*{qWL7PeR65&swdWtmn-KSIkOQt>kvASz#`ml)G#Yx&vfYIZMawTfN_QB7-U3rgn^%pwu+wgaKx@H? zF4NiS)GK;0e6DXt{C8U9$rM>G_IApSY8ajd-_H&D-UcR*_|OElzHb|~KX@u4hNIWf z*gyNUx_!@U4Ht*!k($?6)NGb=)3qhOnN#4HgVg<>?0^~!+_eZzGTb#IgH>aWohc^) zw!}nDMU;&7WW-DQn;rVT#Y4J>{UF$m1u0dHJmMe6{=W&@4D+`-A+E z3^(e{A=4YU&4U;*k1K@;D@Ud{|9R{9U~#m0j{LoM%I7vIX=totJCaE2r_1R8cnh8G zm^pc7d^G~z3VQ>1O9Nz{2>~GW&^xGO_?-~cjf`NZRWBOgu=?8tfSe%&jIj#qDXJ2 zkX#96c{6kM@X&;4eZP=C_duD;aJ{GwlIZl7Mb+H8ucx)1^ENbd?cJVOch#T~CK)W4 zZXH-KgVNXv16VN7XvTANcU$-;kE&yw>bLi~s7L0X2&&lmz)klgYr)Jw7=6jAGXBG_ z`0B=xCe9o9QDy*5fVFRGOy$4EFsE3$^w*1!&c7cizDVj4s47H4sMv8K(Sr|Ml?#5R zLj`&SI#qu#un2!(5}@`onRB>D{FDUd>IDEzne<`m=9w>aBMMrav5|gz5VMMIS0~|; zjS%H-=42~tmgst=#A3s~T;!E93xE5zhD~$}IVFa!zz=EhfLlHOz1z_2HGOfK{~$TM z)Om==J({>bD#gQKV?hbILv2eztga^vImwSLdGYL7u=yFD88y0MKLFAI^50p2UuBkt zFd~-q=iYI@WIf?TOui2CgddYv`eb7VVrnhbOj{Y}Qk`A;di_p)7gO074SPWLlA*Bm zn%Mnp#Bs%N2wBr`j}U|pNkF7v_XO%-9n~sP;fYUTp7~MiieHo!P#ZtJbFP;Al=Z@7 z^!oPaJ61{wxWJ-Vo@mefG-kuE!&67SyRpnr{#xjs$`bK(7@za&D|qj_i^0~>{{Djl)A5xlgj~}){twkm9wUn9g(2>0erdXrNiiK8;L7=};PFTMo<_aXiPjP8 zOT!<%wC@dSIveS4H|6|)2Ha}>-?XjfxBZ*=iw6o)47&e0B)Yt(7qFH1pE6d=GV+2V z_W;I>_;brwd>0%bUo{Ni&{diXWh(q_D)R68RujLaNB2R5ye*@~ENg}X*{zc*%DDR{ z|J=M4DCA7XJK%V(NuS&Q?_S#fVM^*Ez)Dh}KxZijg@3)Dnq3jCHanG6teT<%d$Hgl zMXbSOx8lC7b?4$7S!^HvdQ@HlD+58wfgvl-(ODVYg8cYdL0Cv8WG}oRU4t@1&ceA%$x(9(Lamqcd6e6SK9m327V z_pLWQFmPhEnC~P0CKUZjGjhTj&m$+SFp12qDA!3N5uf@FpO z-HvrSYWZg>^zMmw0%AX{8kRzhuj z(AN8Q;$`aX535g|+9CVk4(3hsV)0FD9x$C~Tq?e4o2$wNsKiLErQ*BS(ca_I*KQFvws1Gy;?PTUsF;zPgu1!p1 zvn4BiJ(KN^xa{g^_dNTq=kDRHW-S)CV>ch{4nq5q2ipmN1+GIOSfKd_Jgu0KyO>KH zkAau+2uo`@P?XMJd8Mp5T7`uX=N**OT2(CIkp6@Vkoy3 zM5zwx!H%+G?Wk8h@n*dTgijtrHk1{Uhx9KtKuF+B1cby9V&YcpPan7~9{5TN{Nf>E z0Euf?-Q?mS(r;E!_M(`kwRni^Yk3v$8wd!087XQ7+g zBGH+Rj;;=bzKcFT)>kDuBHh^4TNa@?@5#Nv8EPzEcQzA@Q(ZY7^JHX2=GS6 zA;23#Ym7elo7Zg6NO?0wmWHPUnL1M6xB=%T9Wr&KzS-M3d$#P;zLsKbBPEVt{*`t! z0+0`+B6)BS1IdH;Hvpms-v?{rFMS`2F1qXkT7LVXkgu?+7D@~xp-AG2R4p)8K+7L5 z<;<#Dw)Y<~<@xXdsakG5Dv@#%KSk`|8DjDVHZxh!lXk=aikr@Yo+^vu8}_yhA<&cJ zJ)Sxt5yfzg$U$R7A&xB1LNeqm&8$>cv;r*zULj?hhAU>e4t;P>?5%}fhh#(TcHyYI zq8qKu1ttRx!R{Ow_8wZn$ z4r1cb)M)~wViB`QDyHO(r+*Hd#8>8m2@(=PI3*y0SW(b1-CQxWl-h3ZNL9`I&Te(_ z5!wAcAF{KvBE0gBa35QDSvr^_=!9z@1i@uNmJPv#md6zq7u^)?=^cs6$1{X(B2;w# zFs2?YCk9LHoSWm7#vUmuCx!TQXr?;Y$ew}{eRQqH&0~FJ?DFpzGvH;`qTz|3f_aA@ zu>}6b7~av5Y2WOhJaTD9XMIIebc<+|b)1UCdX32Bq;~evy9XCDy#KY?9VQx0JsZQ2 z;-g&y(0bmD?l#z84ZFgR-=@Wf$l2`|?cr0R7nZZ~dq1Y<#Wg8f)P2tbS0g`>41y$3 zzc5|!+GI|$JHHgO$zaQUM=F1$zUe^O@zIan4gSfe*T4U$uWSRgz2d%jB-=|aFN0`* zAJ%at3Of6M^g?T&1u=89UJQ`8Uxb*4#Jf*~BM|d$0}z|!~&oc16z8-Cy} z0N7SQ-9V2J=A8KO-f=%MECy{FIDNplzYyKXIF#RpjpBn z&alg%Pu6sY6l9+kO)Mm49x?a5eA!n$(LdwCH*k7x~JRA;a<*f@MBMf4^CIpzj!%#+X!B&N=ATJ?gOG_Y% zu^)*n{`W(W7}lk#d^Gm>vnP9Q7YElUMow3S4>J+0eaAvdspTTp9#fZCn_a}h+U3fN z%&hYOYq!6#X8hcPlTm>=i!BUqQWZa4kvx8N2!-;4{*-}!>vWV-t>8$mak7h3wbwBV z|I52KnMqB)&F&Yg7yiM`5=pQG7$B0Uq=Nb!#{miq+m>l>-pXu?EhNpuN0gh->F?!` za@RgCJn`V$mmq_+Ic)==nWGRs)Yr6O5u7TNA)4jvbl<`IqU9bZ$(uLAb0hOlnjd?rrSRQrn%iF9|p?A0d5n_gPv zIaAh8TY8gq;42v!+H8ug2ks!WLmhiRmv#<`l8S&{c6BYIri>OzGerG~7YAq; zxy||BAsbr)H{jqlM``+FZy-m0m#W&M=%ZQ5hOv5V6&h=%t0hJdQJ!Ec1Ck8RHT2EN zV~##a87ZSlI;|>dSvijdoqDTgZ!4LMd{~pUc9p(>%RHn@@TYQnDKEUkcHud0H*kqh z2g)M5vg*g!LGxrwAsxQ$M|K01STgF*dnImqmx(M^3*Xj-B)pp%uv%y{5x~V|ai=1j zPrpHk_m5)TI22wHJ!0mc^&ytAPux=&@0;e&Up_7Tfgbwhdlw|ffZPN zXqef($OCH{1_eOKA`c89arKk}4-CrG$boCo({u}jMhs_Sz7Ft+k21DV*A@Vu|I!>w z1TX?30FT))gghquhpWo@3SIt1i9$jDva}j6gW$;htG@=Wgk=zd^J=;1yt5f(5oF2H znRsp6=b@ex8!cYjxb8i|X~V&`mW}vr6IlteQrMT7Gk(avq_g%V^VfZu9+!5~u=DMn z;EyNnL{G7g^&=9)EeYd4pc0i&sFcP!(l=9y)}2~bD?LQ*NF*2WyhQ((sd-_E=F+#~ zPsz*PiN(@HnQOaXb^!l8&=HOFHz(7%E18m1m)qwC{GDE&fuE`*d3g&K$rmCOl=mn! zY20vRCZXLXfeSuPp=wmgu!BOOsD&Y#>Nrw(sCOH}RFDh%1CidioMmf1&GZD+XbOl& zgkbVG0jQ6jKz;OJh%FS9t1+8g#O@o@@ie`9q(|)~G3uluc%&#d*gX8%Jsy)$-gVD3 zx_Ff4KurV9Wg)sNlSgZQjUS;7kILsmQA{K9`^AoUXcsp}sfap}NqJ6A4kEp$iq=+b z_^!8-o9$J78Dc8JTDRr>tI)`4@&bs=0Tk$)(|J;;-h3}_s7!ob8Mfg9 zc{>T`DrNVr3sLQ?zCGYgm-DXZJv<03?uElOiBIl4!>&g}5|%<;yc)GatN<|)=I^!x zi1XXvQYY~5QwD&6JB$?KEMW5njDK(;UZ6x^kvOrwuWrc9k_Cx8+qeGNxi&?m&!}Hm zMZabI(&gb3Um=I%-;e-6*ZJ8C#$VhaKVa1fBqPlWImSlV>z;s9%Go^)tVJR4Dc`(F z;Cu8MeU5BD(?RF*+Y~Dp4=p!dm>4i*C8Z;}T2QRv$n&)vv|A?r2e>D?uEV`Z!~iUe zvr1=yem4?rbJISW`keIl7`(6RmVZz3{AZEi%giI!yBFAfC+;vu!Zi0@IZ~zgnaMuYt;H7t^&S?g+z+g8jtSY zaoeYdfRRQrKVl*lD?#oGx| z*XU!HPXQl91B_{qU=8D-5spCcNk38M>cjI#>G%o*_I8GOru!TT(G%QpO5BwmFz0Vt4W$l(1d45m80YI6&yg)MH)`Jlf% zh^j@r0sht>%x-+8G8&|(oZyG2MxF}(QXcRd0|dR={_mTiwCRElz*ptDKB%o?@34dH z6bSfV%@z^?jiuiOn6wJHxN<&tJaAQ&WTcdUQW|wJCX}#Fe>bI3-mxWr6>i5$)?q*4 zEnM4J^>Y30js4}LhWpRmaM>039U6i6&`T|AIt2u42 zMEeWu1q;5CJ@G$k4Atv~15dfUult7mVKJk&5WxUwjO!_5e;Iv&HaBZb!(7+5OVNWcv{2uX-Tqz~O(r!&Qpz}sQ*~Gry&$~AqT7AgcZKaeiu@OU$5@m@()etb2_YC=`B3P<-!NMv4JcZD$ zF=Id{uqX$@D${rgKg@v^WrSDSitJ*)yRZJkISA6n&nPUu&D|T`R!KiLAdRg;^Gr;| zNcg2#wWyuUZ<}22H&|3;_Gg{rXW&_baeO!QXqk|VEz8dDvNJ}I0(ddb?O?t~3$lpy zgU)rg2gpLFCFBt117%TscaP`eQ`J2eg1G$cC~6ZNwDiNQGn!oA-LB1I>_P`}M#*L? zsEqctK95wFRZwAxjPfvrWQN4Sd7y<83i)gP#fEY(SEbu|r~0+kN`#PHg*zYn-KpoR zt$veiSooGBh=K6Y=?yTaNu(X_4k|iO4U!P8hjqEo?HV&Ki|>f21X)!6AFj&miu-7{ z*6JPL?jj@FGTqI<+g_gnyzK!L8w86hg)y0MYMpP$cKB$y^DWI(;DBqAI!Qq7%&>mf zSMPDmyeOQlYjfzX{ANIo-GynU?clz1SK> z8m5^KZ?z26Vhi}N6m(Abfn%K$RmYl+hveFtwk0EVG_fYJ{l(Z}W6(6vj0M>0CHP8? zyL@Q6Ohk@~UzIBhfy7rR`Yu7}TMm%tQ4{E?@R=YqCwkx>ntIrKiON1oSZ9Fx8n{Sh zzXLp})8Yt~-9Alr)hD2`Tl9-H3(u3`Cqd{6&SmzX3haWW2oW#~REH`!h^ZhK)cnA@ zY>YYZytHegno0F|o^?n6dbEg%;q`^KEuD^x!|*-;hAMR=nLIx8m)vFeN{jMhs`^md z=+|oAHOSt69q`21XN8sb>fN_xgDqda>K4Dh;|OYmcV=uJT^Dn6GX1M|i^tsB80?It zyj20|Am77M*W@ot%@9E0Smnm&QMI_p04E$8~#yuh5s5X`ts7oPlmcNAOnSQB|`trU$I>%LdLU;k! zJTlYI>IhT^{xt+)7t2&;C!S|2d&|&Z-K*O0`H8Q@l+FTx!Qemjzf>FHLRh@yH=h^lT1 zN7D}&$o6g)yc$DKw5x-?ec0+fdC&?J_mrf2G>-cI!kC+yWlsBA+CVKJIuM_?hL6WW z_R(#*`G@mQyy$g(N}po*8KQ*h*#k{ZR)zK_qY8J+be`EZifw<#oLm@{yB>Tg;^pX} z8f$EytpURB4&KSYEZjp6L=*B{NYoaxduZT>BV#ff5}cfdsoI$-S*Of4hVHRespojz zw9sGM7VU!`$N`gRa<|ir6Fnf60fPR4$q>8%6V4Df;>;Hk?~&695^{s_yP3Bo^tT(Y zj(PJlQC~@0Jj4#o#SmUdI!b>?;h(UyjI}iA9@UNSVft_TgV9&rRu&k_Y~1+(z1Anx z^JHE@G4)#-|BM$gt$9^!*MS$e&cts$z9Dn#p^xiK2NQpObUC>{%KN1E;r5i94>Eq_ zQK8%E4UphU+%dSOOa||gzKS3G2_7C_)K`VNdBko-^i{94hgi~c7L|63-Bo~W`V09( zL6J3KRg(yM5jvtX)ih5AMU+M8CeWCi%M;W&GwiMPo(@OAD(N9&6q@P~6OGREu8KNb zeMLWz^RoW#bZwH)PNTI6x`jogYgbG+YlaDr^O#HhJaYl#f}C-=cp!BTbfChz$f$C( z%SgEr^=A0g7pikMG5C{z34LCcO&x3+@x2Ps}LEq1jr^c~d$7>1DBU_6=1U6~|Czx{+xrWicb37*1c zJJxmT@%lIJ=F3;pldFCnsu^zoEVtee`=fp)7>=`j(Bk;eFyI}j9{}^v22a+-43x=? zIhKXD9VqpH!fH!g9EX#F>YhcBmh*4Y^@G28&F(XCd;yU!r6AW(u~WHo@t8t{B~Mt# zRPI(p%ca7Sm1JOOoc&10)F{0Q#SnJ^b)^bhKqZxc2)sn0_!p<2ToQQ6dX%fto70UJ z1zrWwRee7W>}CnP#&fxD2Zx-}9ks;aX^_&Nr4K_G25A_P!A^AxF<6p@H4cDTAo;kcN=L^>qp`*{cy@IC5dW}UVp9c7C7&)TC7g| z^3?nrvJ`#*q@+5-6mDn<&NNe-YpiZEfnOb$JW|e^PyN(_^-Wm!QGBSJTy$!MuB@bX z?*{H2ue^2{YW{|gO9z4=6l2$d1J~gZzV-=zen0LCfa$(K8_NaZ6+3%1^+{gfar(H| zP`zn5@FXPJm$UkpDg4UM!=)fdcR6%k{<4TI$Y2jR=JKA#=lj;9SNU-pCpPul)1D9! z7lw3H?9GfL>4puxQOFtHP)04KgFN z7AySIJ0UIK&#LhKZoRTzFC-$4T=UP4-!XaJ5zCWbrKZH2Ou#&lo|QCuAO$Vx0}HBe z4Gmo#pi_mC&=N28NO2tP>N*G=tI@Sy$ILradiOp)aiG#6Ky&D1-vOX;fl#7sYopimvQhysetF#gr&FOiDG{dm!pU~l zv=VynFt5Z?qr!mY{G$ab%GS-DLkY%tq(sgepJO+q!q&KS98{D$h!4oi$>wao%e%|R zVWIwA!SgMItpB&_Pts%9$(@Ov&Yu&{*pZBNV=u}p8yuM(%JoqT z<7ze!kx#UaaknpIG(2#9JfhV2cI%I{A6n^>1OnXdLzBFtF2eNNGS4DNR;3Z+^C>@* zv|oD#z8osggfBb`3rmEdN4r0Ou0@G6_Rvf+lD`(td6so#J$I^Cs!z|yxF_x5=iRpL zsr1b^mxV`UL>Hn7Gf`bRL|j~X8&2=jX!l(^fyw#Zs(Vac4y-p4yfbT)>CfbOJg@CX z86b>*ptjZbNhcg^!^zL=$dAH(a5g{Fb*NveDnzUP{^}_GQXQFkzw8~SI}W6YVyBs9 zYq$CSf@ zlxjx&4W?&_Gk!|l&h?gOJx#?YNt_PbY;{6)fOk#o=_pT`JI{8f2gGgs*e1Rq_Uz{{ zij0eTOkL5uZbyROeB%)@oQ-1agRN!9Vcltrr&*@nAN8LcPm$?gRe~DoXl8~BPm`pX&vG$V)xI!O6(I z5>_($-{|qpQjP%#_>182?raNHk{b&={dp3g2fW9o>Yaqa8*7h0rv!Hm?D_Bn=z`+Bm49EI?)-8Zy{s z1dpLiU~Bt%zG=pA(o?KDCzA`>)nqPjBo&$4n@D`veO{1txFnAi5Fe;=XmwWX?7AMf z^&?gWxHS1rh|`!hq?DiC@7*eR6MH>foQ+bj*y^nFR0DG*#Ei+X>Lt>i`1E+4B<6#o=?(X-1uxk0304PxvjbO=#0 z_7vJoJ{S(4+ehAjA7~)|Gy#d=IKBiZPfCh&knSv&U`V?2mn|+J&Mm{WsWFR<3<`m+ zm{6~>)X1p5;*y;<@}1SlP}ZK{u|OIbUcO!|*;E;R5pGVTi!yn#5xBX&kG)ObJauHw z3i0ZQaSBoJk~mS6bjUE;R8oEWY?JRfj46b>Uxia_-pfxnKYV3bKh?Xr4w0`-5Qe-F znAB)smz^i2Ati88Pv^@*X?!ORQdL`f|lpZ>8H}2CHp(P9Y zRT_uI60&qf`JnrA#4Uqnp&)C%36Ld3R!>Ck-cfvIML##>-2lq^;BVsj8R$h&c6F(# zesS-^Qf&m3EhyQb-i&~<+qu+lcm)_5eE(l7aY+vUPL4uB3m-W21A+dzMzQi^*^_b>CIeEgiq!>d7f}*;cYcn*CIj)Cjo! z(lvfK1oWl1dse*BXj4{t4$BwjwfQZz`CKarY(5K1Z9b>bE=Qf2}Kp`sFYmo&#=2Ron`%ZV4XP=W! zd-yPQi^sPo7Ya5n&JoDzjk&9>9$p;L92ZM=l@ z1D%7fwQ7Fs`Z>a{_ghhF^&5E@Q5t?t5bQ(4KrWo`LP(&RpGGA? zv|N&&S=*;b4a`U#>DIn8ByW9NOu!Y7%72A8=SDi9sad$827sJfDV6}_Yw*ib5f69dj*yA=Nb}n&er|B%p=T`Zh{n?<^>~oHyuhg-&y}%;K1jKb;-%|B@1z2BJpQ1s-Q zp>jn!YDfvi!`MtJd1;mVVPtle?A~1x-=`sT^1(bV+?;rs1oxv3C(vHLavESb${2?HT#2Ct`*Egk#JoWdKk-L8YEdhPX>&oyR20-uNpFQfHx zcE}2JIBkoxD-l^(tE}om3=sUGS&H_4gs)1Wt>`z(u*?A7sVNL%vi9}%mRN`#j{h(p zsx>b|sY;zNiQn8ttwnze_>X@Izz1Y z^)c{2*$8r($Bs|7aq?V!#wAcUCV_LReH&~XNLj^F0}`E8}U zRSAk7q&C%u$x5bDpDv%Ne0+D^+6$6bBt9*dt}g7OH?zdz=cqLjT`{E~7Uvw9wOaw2 z0PKPAy528G-)`pHc)$jkZq?oY<5ChWIOugvR?`^gUwArjRlVjvPdCLqeDgD)0^7RJ5P z6YULrWvN>)3u;DB1U+S$5G)+VR~q669-%o5^qAFX(Ngh&=7@eihRx8VupipG^`fVw zIgKy7mzi-{E9-W(_ezD&sd2q=HrDGcj@o^1_?`ObAIYnpKv-D`#s$G1f$#btw+Rkm z2(ZN6!kT3$3tHW6AAf(J!TYa{=7;FLx(7lIHk$+6&2?9hpFaahCrD40JKr`>&w{Np z387vY`B9iWlC%;QU*8&hK@ZnUly$^S5JSAT1b-7(lRFu!W4$r%>Z=U{;q;refkN$! z)kB^}#+~`|jq-M_Ub6Y6c_r7YcV)f0RC<2%(af!48@_{&xe*2-K>tnID2*S{+wG>K zSb94!foBm&btsmb`YUD%WBW#mBkekAZ%GLtvJl^}ZEd=ElQ{N!dD-EC`tidw{7e+J zrW+YI1c8qBB*yb>=Zn6PMCsZofw#i;buOAm#kdYf7acR?{(IgbYjvT#{L+YGIuv>k_bt%%Uti3w!qYM<-{D<4*Cz;77#nMAJ zp+F8TiG`jk$e}A8wYsXE#>CWoveypP^?CGIx2D?1eX`+vQ#a0a;ks*g5kOJe=s_!_ zPxF9GLWeQjX!C}h2E26BL)3t}JJ>9GYfHWbZ$w>j!EMh@Y*6^gt&-0T&+k100y<$T z2I^PzyqVtgWV*_rL>IOne-dIW?);hYyu?xvopQSewN7l zFT6~Cpxl79Cb%!!K18fFkCW58nMcV3)7~J=55kYfdGycub}Fz#A^G2Y5WH0!Ly{q~ z@JRo{ye>V_6H&g>P{Gp?nHR7s-@=|+xSplv+N#G#W+*xcQFPxxQny?$7lbA@FB6Ln ztu1S-B>=IpTe98-d;t;JZ|yES(+-tSpvWm|4KW1|?}e=+-p4|A=~dBC3Pk)>eF_)HRo{hMdBTE0BU&Aj*>^1uH=^-&quXEK9-a zo!R}t!I7T0s9*&fLF2QKVY6|u5rNTjGYc^S#30TrgHHZ1p+Nxwk$*KF>R-l#xBw-{ z8GIFC4ZhNZd@M$g-b|Ufn!<3WUAj-K`1OA0vy8;m=@E zQ%ZW~A9@{3%+Cw0ufJSp#ql}k%--x%t_NOI(KpT1|qwn@y7Lh*6OiJaIeXt^TV$xq!WfFIY$CjNySv$ zPWdp^O$FB`9Iq4Huap0L_BKNNXCQST=~CCVzz^|`@iFrbxIYTovzXO*?2xDN$*6OY z1#hF;J_>7nL+62H#di-??FLS7&|)&=l0^H?fN>p5ebOH7n#*p+dQ$6#Z^u#6_xZiC z`Mi7Qs%qXtvDx#T$8d+Jlc5G{=$SP#BITW`S@;*zX24GN^!$Q48S8gG_>ULE-%8eg z)H>lFsGI-J$IN>}aqFkV4@x8D|?^Y@+p$7q}-+SM|b%ol|OuAmu7z3MN4gGwW??HLrzcl12VRKm8)c1 zgTJJHL%LGGYj}1%2D^|(4IJ7kad7zbTVjc1`KQ3RDSS2hrb7JLpF8iEd|c@0VMGP` zCzc$4Qri5yGL=dVG;XHv;%6R=8z3&Rfmmkyzbs;#*;$4RJ>-dDyf=-+RgI;Jdr(U@Qc(PbTNZ?WNY4ta`#dBW&yki~v$?;$p=s z36bmIQpH==8#R&D$f|giltag$;zcBN3j|{rH~{lmvewJ~Sh!R5;i9!30`mxoY!q@6ugb=8|1HMsBmP!+^pfC+43AzO zo~e_+=`!!*26PlUpF0}wJ}GDfQsO$9&9o=ESTB|1Wu4Qjy>r$JD??2yve8HnWOc5; zvsW_!BJ~^IZE^vz+F%asrrY#b8vg;m;rz< zYzs44h6p|VM~qjL6xYZY9~aV$Hf-8H?%(yy?gz3?&Vl{{1cve2iAC}G z>B?GnEb%TV7Uc=oeTZxf5buH~FG0%55rk##@gUsWb7m3@M6?bah&vp8?;zsxzD%(g zvy_Zm0lF*!y70whJeQVIwbT3Bx5Z@KGyA<$pIiqk85cdMU@mQR^_J|hz96(1b_7uD zWo|1w@33$^4g}X*t*6G+anXZMhZG#3!ZiRDu3@;)2-ghOF?6WJh*G72g~PVUT%zjE z!h|{JPuefc`Dz3=aYbAE*xDXirF7d6bqlsW_1Z0)r2d%Tu1B6$boNMB9~l%||6ZEWOs%>&G@5mT<%d+4AjhIC6+lB|zT#1>n0`h1fkVlpOma^>0avwMb z{%;Au4sg$S&U^VKHdNvBZ;ogB1Z?lA)~UH)B7AB|@EgQ-`-|^F&da1Tl zlICjroJ9SBeb=4UDOig?9eerw=3Bf8Z|bZwSFkhH4u*rZk5(R~KN>tR=nC3-j~ORu z3E>9v9tPsn(((;0**J@5mKmn0qX+sNL%ls~vifXlj`g3&2yw3GBA-CRM8>N)iy_Ho z;Spa)mpeW?JPIWJJM1UCW##W3GWqH1rV?+vS*0lWN|Kasf%T-hbK=4D)o(haW9re} z8Wgu0!UvZhrt@nvyr8NaX~4Nu`lTOL z#rS(G8dS#I2N`iAt!!_N%q~qSnmB2VV@Rfq^-7e<`l5>t`Z`pak5z8N7ma_hd3`;b zqBQS%4p@A)ThkZ z3rPi9;sty$w+vKqhMj?$em|UOI^o}Pv@q+&fbn*)RQot8e_zE4y5YJLoP&n7M;SXv zSbV66yXfqh7x))1-*-%tLeT@EgaVmI+SSwoSKIl|ILkCupbY<*cDSO(5RjjFPAw#R zKQ*G>@QQ`e8dgFhBNIig!Cm%~V=ldlv>9A>(yr6nXEL7V-Js1#Ru(o_tM8^C8nFha2i8VPSqs5 z8+Mn(oZh^r$T-{kmvO5xyVz~lMVow*4cbj?roG=;kQ;06-Z$JcKsy++o^R4KCeA>B z9^E_xER1XSLD4pOqa6J(P!=aoC5EV|=978G(&sIVTMNX_Oog6`m-O&8mP)pJ$r*g@ z9;&^I0KXOZnz;z6h2`~x_%@+=8H-*|A}k{6EDmjdC40A77qflOcfPmw50b33(!ekkPt}=_ z9{lqn2IYsYPSR|d@Zv;_%m~8M#eY1$YbIh?vCZz-1GhEUQGw5>S`4dy@mFhErFkJL zID{bHGHPK{V5|$Wyu5v#nkAu#<$*rS@?f)kkv4 z4}^F_$>br;p+KZLbOVuwNr4^VuP-4kr&UQpZThPRAEJ$0F3#_gH*58zZ6IngD<7{vb`W?_gb>HnwIdCMf-M;>up-pdVnE_3}Q48w~Yg}(;f zR3IgPXs8x+C-XDq{op{`)mY7PPDns{DmI5Vn)U8JE^KmT&=_yAXkYhtj36HI=cAXo zBQPLDLcbJ|MXqiDS)^+np6wzZYJeP3peE+@RmZV8ctUh9xb9+|G9-GRkXF@gqNNDQ zudGWi2yC|d#LticY)+wpidm|Z3{g!miMGcH+Dl-e9C{HGiV`ecbWrvJo8svP#6ej} z;g)PPI4C2oA>@4!CjQ$Sa_svGGG^JU|GOO{|2jU$0e9yp`d5q&2hZ^zXsbuCAWuc! ziCC8)9FcZLzyQn;Ssa66x!%4YvjG`{V*gor(>Y@7Z8Rx1EUw zgWXQ@RJh~Zir>A;#?*p-AyBX}6O!{79E*Z|*^?z#Vo|WKvnKNa1$!kIdo(ili5X5_ zJ`Rb&4&V1XZ&+A?yM2&A^X@B>(sJ2i7!jt+U&6@$6J^%7E`a~79?Wh*c*f>C`2N?v}7e`tjGGqOrk8C%VOy z1`P4bcqZLb#_2&(LH@%l&Mn4PB3{ji1=s(whO$usL7X4?qGx^Tm zL>B!gc*E*g9Z7YKOHG%4AZpVkcFr)9Cymyy$j+hmC1iSB(Py!98bEg#+W_pG%@2Fh zKwqPq!dkzaI<#iSdJ-i*gT^t)Oc*RXY}=uES<~To=i_*+&yCOFKr-5^N7}n+bI{g| zu$lN~k&_9ADSpkM&sP|1pnb{CI^DfDQ8pKA7yrZXd1>w&`&9#x-`=tmVAGR-eaIl7 zMGTWLgw&gWka{`6E-udH@jtNwkg1%)Ed4l&`pn z_w@A@sh{sBHnYSVtU;g`U=Sz_4mL&xfkq)iirO_{=+M-BeUh2yHyI$ezdUf{`Y_v? z8RCJ32nbMM^pCIX=wIU|d`K#0_3%O)U7fu}DL%7MWP^ zP5U@YnKkzxHs2T`9!L2tviWpiBm)VV<_2s&hwM&K62j)&tM`Lvwod092CU0}8NE^d z8-UE61CFj6DC&iq_23)TT+<8O*hq#twcnC@vo6X6#W|+qXc^TGosKKlB0Q3~{BIsq za1hHlc`WHsS1Y)>mJ zmB5jOp<{5WK$QYeTvgvT>Yo2E7WQ@U;wARil-hVgg7o*&<~m1 zhm{jKcX7#s-X0tUwN|n`=)X0r^AJ-w`0tND9YI^aT$ylrXb+F>dT-as$TTnp2Bd9pL>8!~e(CG@F! zGt69L4M!Y-Zt{O)z>X+HB_TOMy~gAr&TsI$S>K1I@yBSEx$c9xQ9uVxh^O}ad8n`N zfLScJ-jBukqJe;;BXS!Ux(4H>5PkgO%(W$bJiKk>7IIF-Q<0f#ppR$iK)c{xBE;-J zVmR=X*5zDZ@%+dHE!ozohidk$30kZvpIea$TIWb-vU_&aYr<{^t+EUkJabzHUxxvT zf$q>Zf9A3CN$!>c<>K}h%d;@^7X(6e*Sy}dF7XwNj0Ea15hmNjp^?cp0Rz$+)mLCD zs2^egFkN&qvG27;#kh5=bz}(9c$$PfqWs%~0p*`aA1ME9kf{k=vj~M& z44DdAhz~1AgjcgcQ`w@%ROkNC!GyheFuAW;c=|?h#&)X>9}2&|7Hit`W0ZYfddZoA z!|wO~4tc!4caH}mFxb$3`WM?+4{Yy8ft*v};Jx6Lun-u-yrVG;Iydsun4zGhjz8O9 z{rfi_4+dS!tm7XJP5u;sZ8G)|CJx01c(dbQn**nkm=FI<4QxDUEX>%4PaQ-q4f9r% zAp>D&>mO8pnDYc6@BX=3yj20C5??vm|NU+=8`Ok~ZRDxRtOF}x;r&XFUKoYd0l3KG zBthh0NMjh%ufilj*1@n#L?Pe)IC3zgfj`u3I2d$r!%}u|F#J0C@jV%FkNA6oF4%CN z5vk~}Gf~k0S9#|e4&~bS|4|DSt%*n|Q;|ZbkT#ZSPf1cCrc5QFDN>2Tj8uw}v{6|W zvPp#|Mapi-uBe2J5C$omWTtEeGt=`suhD(~Z{t4xOV9DVc<$$g7tKL)%{A9~p13V;3Js;|dfeqm%}^QV2M?Ca%9td~quyykRI4)?1#nHdNLoP*+YSMuB9gRH*-k(^Y|oj(Ex-G^#ayY{Iw#|j zprLHb(W8?#-*fGlD>Wxz(ks2U;bWDwrg1I)apgyS7QZ#dam(@vvIe>eyy=y<+4he; z;@;Q3+~a;CA^OQJmebSqi>3t!g;_1_5I;uXDCwd~#cDECCV10oXy2@GREA9p?WeyO z8SX}+pjsgG7i0Yae%@IKw*>xT1%?v)o?`xDMXxqye~;iGLn7GE?g-F+cFza)vspgG z3f_kJ_T}~|>hL%35TZ%V68JPPT3l-|cxSsN$X#j8QVejCIcEV*GK*pJ{b&dD+7Ykt zgNK(v9zhbmv%d?K0rjNFSABQa-x8IMm4T|;=>pF;dCdv;SRjvOFzI{KwtRm%Im=vYLjDw~DOvkYMfMI*Mq85Y zHURYjO(9BU0~_%@wn@TN))Kr?GLE2BHnSjLb@b)vhnRkQ6iMMqLi8w)12vvn)(DqY zYshjsW8@o?f5I&mT!Yh?Nii8)z=)*440P}yZTWzFNO(|+6~R97<*9{42@j#Cngh&W z+l32F)%3e$`$#sJsao7-%$_7--}p;_^n(vL66wcBfu+t598iKm_LqXp@lBHwVfP0s zC=80`TvjL9ObbaR0~9SCQ{Sv1r37_u$-*e?t(LP$`^9pphfT{)^8bMU$`_uYTlUN0 znc6NWZkWZ!holiFchnXoBc@kexjM=!yIAMP?F;^$_q;wOI13HOjh$LXb)nW0gf;{n z-GNLpAqUscmLtbLcUHfZy{n2ItmbVtpgY_1nUhtz?X?2ycHG=kCF$}#p1-pG>>Y*_ z1hp#D;p1(mPH*6earutPUi3nN);m&BSh~Gbu+A6N$G2o_EW?{_@0m}rQ4?2{&Uz~u zC4j|0cCUm_VEzdK+SH%1m3OW&j@>^TTY-sC=keoMY}E{YQ=TinYxWiCz4o0s^YT;VWb)&F!~+fHDmLK>&JP<%Z36uYEbqUDSZ1=2NyYi)8X~0{O@{U){qGCg zs9i!6xMAlEp$Xiub4I(+1P*h~Xh$Y+V0G?}nGwNCM75_JG{Se?bz^&ZY;R1u3SygJb3RYl1Sj-K3QGwx2R|O_g{R;M^_3%{jc)Q`Rw^C~a z>fAui_yXhcRWR0+=SiIrpe_5YnR3b~ovAPhv1<3%+5Gl-87P-ODqzm0;s$J89`aA+ zNJ0Vgj+f9rwJfGkB~Ti(PyH=ARPFqdz02pjn7niPZMdFrU|;~Z zWaX8A791^2uBpp+WpZ|sNRP$?!fohl>@0`r*5Du4t@$79Y{7GfP5lfF^0kWuwiTm} zm`{{3CUAjPW_=-st5cbObzO49&NYXff(8XmHz%+zAFMm(H{3=8!HrDux2DIMu1^G)GuOm9J}8(dVqco&3Jgv@@Q11AUK=KVJfUIl?cbqRx9kq*T>cBXEP781T>h$=!wf?%7PJ0jIK8E9X zy`!Wvi4VrEjpwy+9}7i13i(Px9cx0o^Wd(m8{hy;RYVmF}sGgaZ7zL2{se_B=DDTDju|J4~e>q_JV#vKfc!H2dEiEktqUuY@F41kkcz z!kY-j!{R`mB4-GMH&Wcpt(>z($&^^Himqd3Ll+Fw(t=&5Df?gCtE+tX@WM?8SJ<@m zkG4hrC5&*CyErt#M9U#ES6IFD>oq4>v9qx1<&CL3Cq_1->g8|Bi>7+MaWC}c3-gq* zDBvFqv5GMyY%s^DRyQBye+?kQPC4X?u^?KrdpOS(x4+nGxL0nPW#p|r4FEQ2Wi!Vo*2y2o$03M%nJI2<`GodC<=C!*kN8|E8GGUq#Bu9TsjQ!ro z7NY+O{_QNOreG60W)Ao|RHXXqG?;-9n}G?j=Vu}dYjG<}&|%AoeFks&{=7pLeV4eO zoA9`{$F?XZuj)cM`Au{ongw@Iew>ana%wzHB#&i-Tc(KC#E*DW6zCZuvY{}d=`c%e zMstc9$BnWCI6`ovlzy^<+0!e`zLM?&=HW7z#Y>o%);fvX?%3_Wc(S+T-#r2Zn!;dp zQ^*Wh9a_|7F2cIA(C!keJG&seOIuWT2D?l1y6MFRo$zFV1BGEDIDpEN!2uM|r`*%I zC&7uYQ`Y%PVY2yGUovVB^!wtDinho71rv80Y$-58Qi3-&+u4D#fyk`dRqmPHjpdRhMHQ(*UA0 z_D&biJu@R-gq2*Zo-8$Sjoq_Z8q*y=h19LV{$}Pbc2Ow{pR24*c^;|Dz7vz9pOL|d zb8*=-H)4L@s~~2Vo}$^V*|@vYtv}@~f(wLvVr<;VVs)eX>F@Z>3f{|UeN zvw2n{WRQCYiG!QUqMQML8bb^khc^AO{N%vZ`|qH=9OOy~GRWX2 z_u6M@6mMnE3yhB@5X;aZH>t4g%@Y5Co2x!vkW&f&3>`992U-ga!PT?qW6>6p*(cL^ z@p`$$zKyBf{1u(@eBuqbhc6HK8A${W=G#3+m= zE#`ZfAL@Zur<$hKrn3&xs{=eBba$yH>fKJUAGbi-#jONTFw;e8~b_Z)`3ULid;mCI>bcY;ycq*B2CQ} z2aYrC*mxyLuTqmTx*K|`*wDZz!LiL}Ja0x5hm=oXThiY<A8M=(iH zIT0#}QmB#$Pnap4GH?oTtXdF_!Rd)O_mFP^A+J8BjiH<)KOB=WOG>H>m>_%64?A+d zSV`;3s{$5ou{lkLF=8AehenSkjey zs5Sa0!SI#2F%ju#pnKGQuxbcSc8OhRB#YF zmi=X5c#N@WCYt^q=2_X*mpa6>S71D=293)}d(Lm!wch>Wqw0>ca$-Cv+~*M)Fx@&s z@Ei3*;{+e47UCJAtdUj|VG=jrtSap1e#j_X)5b9u#`=G#IVwQ8?NcS~=(klmv~x%rhmwOCO8hiNROEmwtyt zSme9=ZZRfRJ2^gT#1n)Kap;Vwwci~-kjAaV97)_c`pb1gvcQout8EqLBVMR?% zdrQ=$W^LX6XyKbh*9``j3DaZhuPndRGp9~OO8#o1&qV}+uxKY`Q({$R+c;Q+83 zNBW+=mF&re5BY2E4ZkW&&ng}%kG1I;Sozc9^#A&y3frbAr?}@aDj!7i+z}$ih^LzD zC0V4R21pb4mJ6f}ZpU`5CjsV?vI3^?FZyr4h9HFhhIkBn&qzmi98Hw(MihN%Kmenw zrI6QRoA~W4*u;g{BC>Lh=7DhIdo8|mAl~jQsJ^<&Q~E}z76$IhW%(~$N84ld@0RoF z(Kcf~m6oRgX9x|JepDM{aGgADmdMTyg7~@f0O$ym?W=s)kWFKv@r48~K&=tk0~W|I zO9!Dn6ykPrRcevD59w)2`V|KZ4f)9bBZj-;Il|Ig>4ljAEysjSm>EzrLZEKJ%z%*H z$zD0Vlp^;qgTY&)>k`p*;uxv?}?So6@c)>3iI~Zdj)z zKhHnBYwc|{kKZL$hMqggKZy2qxOe&trznW^@{DqXyy&whq0r7%9p*(t4Z(b71mZ>e zC2Amv=~$E&BZ_HWZEs0 zi3qUX=071%4hJ~wta^%dUiWbVhR+mgdCt6ZcHlTrjO;$NlbL7{tVz#_T)IYYLjH@3 ztNmZOS1zDLWA<6feeUd6n0=PeAV>nU&%z9XkbM>y1R++HoY+7yEE$U6Ep;rAY5_PY zif%$!r+Q>~`P~F{sS!YHJ$%tZpbrqVICnzbSl)?GlRT1_Yjt{kB9#J={_;ZU@ekkV z1suR}L$EU0-}TP0S0LAY=85P^O+;T>Nhn}If#Du^YC$ckF)M^`rpJ9SK{e(@0p7Nk zze0^U&wA_P2_KdEMzQ)}z{Vz`a0wB8$&YxMC~6cEBKp#({~;3PXt)8WbsQj&j18SL zbpCQ01{#_vYwcLOr0;V4$NMQUItXVdiQ6I>#dE)Kyj)y~v3EV`I_Anvwi!py}${1Kx{Q4!}b!pD3`8jCR9 zDM1ZDmzrG*{IEU+~uMvtKI=1*)q5z@j@y(JjGCPx7ZIT8o zdSCHNX%BXiN%_k=!nZECe?<@1w9i9QG5=w=J#3@U$`~sh+Kd1>Hz+@#utp1VH7GmB zz;t&9I;xVKvdfRHfYQ%yCllujMfW#`1y58gva~pW>)rSt+@%vW+tC{I9o3n~PeCNW z1cR89C|Jm&^$`JpC*Ot2(4uixe%l)7|@n zlml+f3Bz<3$4lxTM;wLao-7a63o;x^p&J6=OD zn#B2ZHOXc9+LLOQ&go(7+_!O$Hck(mb@{6hQaS*EgRoZt-qY6!k0gg@C%yyFku0|g zo1JhoczU+$E1I2H!&s#2<6wjuykpYvW#Gq#%syM~GDP%oc?Uuh-AmLdAZnQ`1X5Mo zfg`dR0#YU2$1s}Tfu8bKvKJ~GSibH;Ei;Ck*8hZ$D;5P!>dAI6gM1x$fE0`e7`|ee zEoR*Whto0%Mu;7M3qP8{>T&~~M=)2p1&`~rm*Lu?88wrPayP{Hnuh6L_OGs90JN-q zGLcnm09w=#$OdPe}670jO=sSwQaSy{m|jPIzm4^aV-?t;bnuCRVe(A56%c2drwOJLQd zhlX^MLDM(Cx~r_2CgDS^?hOf6k*V|{xF_1RHRxv^a(&*w(Mfh7oef}R1gi z_4nRBm{VzDCqS8VGZ~dR^|<4sp(HEL+jY@t9{TZQ21 zx0k9L{E^5m8?&m-o&o>0Aw(zs#tY$tS_#D4BgHr%0FVU5-R2aPbh!-ezY6qHU&@7w zt8e(I?5=hEjr(gHxW0=xg4nmB&Kb(01aBKqbu7R~25U*l8<-Qeb8RTvQSbbL!}%ySg#--VIAbi;q35N$KBFqhH4YTn zl^^O>e}A-d&aI6_@j>@5_Kfo*dWxdy5rh>p#EwD&@1tCyA+`gHamGVevQB7-UAOk( z(=6{q%n*D2ovooUH&oTu$EHEAMO4dyXa?v(!w9l}Q=|!$M+!=CQ27TXI7Yl(n7@)5 zSb4j(Tu3yfdnB(Xb)Ih`t$^b2Jc3>XF+Sy}ASUjd7ZgV(}bb zQwo$mGF~v93+>0k*O{2T&+$IJmlGWw6urtfY_;0F+;QE>c%PZV1Jm3GHeb-XhGY?; z`Pm++T$L6~DxBi{s5m))0kc!8t_&EOb7l^l^=?sqO25fqHFR^ADi5=PIEML*^C~LC zY@kfejKytXo){bGVY(GseM{<>sVcs(_gzVJxEm9Mn-4z+_}$MLim*T31H}wA$LI%+ z3IT?xc4FTiYJU^?BE)3df8Yj)WEC1?55zjtss^i}clOOFitkMu+>X!92_=6shh2!!Bh`S)ss~dHZoX)5s;Uo????cMtJ8tgoNh6F7U7 z>*5W?Cj}KFL+l**f<$ixn(&7ue=Mpdo#l2~Kxp$+I@=Ho43FxyDdFIacW+O6;NJDu zLoLlN$Esg74PtDFJ|*uv57H5C_y+0m({R?Neg&B+ z(nq6fgbKaB)!1VZf$$EHJhBp_X6l!nC&(CCaEK<%)W1|9HuqU}Z$}+TUt_@)3CGAc z`x`U%zm%3(REX0Z9w+5g1D&zYczB%D4MO@T1n7ow(uZ}a8g6Ku^pfYN{oNN2H6wVI zh+o@R1{UT|h0fVVi&NnV`HkmJ7GLO1CRkT}WVZeZ2>E_PEd=e>TJ@XOV;LQ>ou7>c z32cBqtIzYJfxK46Dnd*qdlv!%P6N$%eGtNdOsqW1wJO zrxG(c_GOG2{(P^N@}G`pTQ99Hd1(8*u7CB@x#wX$WZlK&*CWEv2bq1a#(^W~6jf8& za^tiRafn6edr^emz2&hprdqXn{tv<-H05x=ECIiOU}P>)k3(|HFc{gl788KSV=%Hj zK#6&UfnFOoZT5-W@qZ+y!k#h;0nZsm#NknMOMi~%YBIi@Y9tgIl`L27+yicr}V+s(y}_%baWS1<=<7FPN4#f>gl< zoi2sR4W!quv%S3oH;h|4rCaDdz-=VgS)kl+VjxtSG#hhY>o#>4m^%~>>V(8~a5~-1 zn@SVCDPbt9S4{ACtO+E|(m8s`mnt{fddAU~Fwl&@1%zEN=>#+WHHcqR)rGYE*Ro{I z-89jRzib>GDf`i+lXFyRHGd*)0*#IQYhPLo!agCo`-tv(D+zese7L#7U}im4y^VnL zaYC_-+Zkj10}Hvjwiy)%U1Nd9@oolb6I<`Y^1#(E3eB{ndnGUto(==Xd1o5y!ZIl- z1vMuHMj08b^0J!DzQf#h2pP{S5rT6J>nX-kOl^3+-YdIORvuz$r@m|%YLV`K&k>0T-`MKw#H2s4R?(z&|QjiN~2AK2WdnT zKD`7GL(w$hdxL1N1%F=YGrVMH(EH)avRtHtek=Jc zVI44zMNgAh!L0lIFk%~(i%x{ouT_Ddbq(4ABi%6v`Q#;-Z0;sHJ^@-NU1hAoTfsCU zCp!;5p{u7aXi&{afA-8aa(>Kgy9u**-3&ZD>GQJBmF>%M1LHpvtNQCPe3N$1Xp#PK(zo@Wp@zY~ z30j60GQ8Q~5X9B|SFj`{k2auUA;@O%H6>YOQ~H2k-sEV#2y9`+DPE57}@7hiL;E)9jI+=W_ qk^|?&gu%^YiK52xyZ;+*75+cqRuTW7r)uiYr%L1BJyFCTHU9+;@l=ig literal 0 HcmV?d00001 diff --git a/MusicDownloader/__init__.py b/MusicDownloader/__init__.py deleted file mode 100644 index 78bb595..0000000 --- a/MusicDownloader/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '2.0.7' \ No newline at end of file diff --git a/MusicDownloader/cmd.py b/MusicDownloader/cmd.py deleted file mode 100644 index 2447d80..0000000 --- a/MusicDownloader/cmd.py +++ /dev/null @@ -1,140 +0,0 @@ -''' -Function: - 音乐下载器-Cmd版,目前支持的平台: - --网易云: wangyiyun.wangyiyun() - --QQ: qq.qq() - --酷狗: kugou.kugou() - --千千: qianqian.qianqian() - --酷我: kuwo.kuwo() - --虾米: xiami.xiami() - --百度无损: baiduFlac.baiduFlac() - --咪咕音乐: migu.migu() -Author: - Charles -微信公众号: - Charles的皮卡丘 -声明: - 代码仅供学习交流, 不得用于商业/非法使用. -''' -import sys -import warnings -warnings.filterwarnings('ignore') -try: - from platforms import * -except: - from .platforms import * - - -'''音乐下载器类''' -class MusicDownloader(): - def __init__(self, **kwargs): - self.INFO = '''************************************************************ -Author: Charles -微信公众号: Charles的皮卡丘 -Function: 音乐下载器 V2.0.7 -操作帮助: - 输入r: 返回主菜单(即重新选择平台号) - 输入q: 退出程序 - 其他: 选择想要下载的歌曲时,输入{1,2,5}可同时下载第1,2,5首歌 -歌曲保存路径: - 当前路径下的results文件夹内 -************************************************************ -''' - self.RESOURCES = ['网易云音乐', 'QQ音乐', '酷狗音乐', '虾米音乐', '酷我音乐', '千千音乐', '百度无损音乐', '咪咕音乐'] - self.platform_now = None - self.platform_now_name = None - self.is_select_platform = False - '''外部调用''' - def run(self): - self.platform_now, self.platform_now_name = self.__selectPlatform() - self.is_select_platform = True - while True: - print(self.INFO) - try: - num_downed, num_need_down = self.__userSearch() - print('[%s-INFO]: 下载成功%s/%s首歌曲, 歌曲保存在results文件夹下...' % (self.platform_now_name, num_downed, num_need_down)) - except: - pass - '''选择平台''' - def __selectPlatform(self): - while True: - print(self.INFO) - print('目前支持的平台:') - for idx, resource in enumerate(self.RESOURCES): - print('--%d. %s' % ((idx+1), resource)) - platform_idx = self.__input('请选择平台号(1-%d):' % len(self.RESOURCES)) - if platform_idx == '1': - return wangyiyun.wangyiyun(), 'wangyiyun' - elif platform_idx == '2': - return qq.qq(), 'qq' - elif platform_idx == '3': - return kugou.kugou(), 'kugou' - elif platform_idx == '4': - return xiami.xiami(), 'xiami' - elif platform_idx == '5': - return kuwo.kuwo(), 'kuwo' - elif platform_idx == '6': - return qianqian.qianqian(), 'qianqian' - elif platform_idx == '7': - return baiduFlac.baiduFlac(), 'baiduFlac' - elif platform_idx == '8': - return migu.migu(), 'migu' - else: - print('--平台号输入有误, 请重新输入--') - '''用户搜索操作''' - def __userSearch(self): - songname = self.__input('[%s-INFO]: 请输入歌曲名 --> ' % self.platform_now_name) - if songname is None: - return - results = self.platform_now.get(mode='search', songname=songname) - if len(results) == 0: - print('--未检索到歌曲%s的相关信息, 请重新输入--' % songname) - return - while True: - print('[%s-INFO]: 搜索结果如下 -->' % self.platform_now_name) - for idx, result in enumerate(sorted(results.keys())): - print('[%d]. %s' % (idx+1, result)) - need_down_numbers = self.__input('[%s-INFO]: 请输入需要下载的歌曲编号(1-%d) --> ' % (self.platform_now_name, len(results.keys()))) - if need_down_numbers is None: - return - need_down_numbers = need_down_numbers.split(',') - numbers_legal = [str(i) for i in range(1, len(results.keys())+1)] - error_flag = False - for number in need_down_numbers: - if number not in numbers_legal: - print('--歌曲号输入有误, 请重新输入--') - error_flag = True - break - if error_flag: - continue - need_down_list = [] - for number in need_down_numbers: - need_down_list.append(sorted(results.keys())[int(number)-1]) - break - downed_list = self.__download(need_down_list) - return len(downed_list), len(need_down_list) - '''下载用户选择的歌曲''' - def __download(self, need_down_list): - return self.platform_now.get(mode='download', need_down_list=need_down_list) - '''处理用户输入''' - def __input(self, tip=None): - if tip is None: - user_input = input() - else: - user_input = input(tip) - if user_input.lower() == 'q': - print('Bye...') - sys.exit(-1) - elif user_input.lower() == 'r': - self.is_select_platform = False - if not self.is_select_platform: - self.platform_now, self.platform_now_name = self.__selectPlatform() - self.is_select_platform = True - return None - else: - return user_input - - -'''run''' -if __name__ == '__main__': - MusicDownloader().run() \ No newline at end of file diff --git a/MusicDownloader/platforms/__init__.py b/MusicDownloader/platforms/__init__.py deleted file mode 100644 index d13a626..0000000 --- a/MusicDownloader/platforms/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -''' -Function: - __init__ -Author: - Charles -微信公众号: - Charles的皮卡丘 -声明: - 代码仅供学习交流, 不得用于商业/非法使用. -''' -__all__ = ['wangyiyun', 'kugou', 'qq', 'qianqian', 'kuwo', 'xiami', 'baiduFlac', 'migu'] \ No newline at end of file diff --git a/MusicDownloader/platforms/baiduFlac.py b/MusicDownloader/platforms/baiduFlac.py deleted file mode 100644 index 6af5f35..0000000 --- a/MusicDownloader/platforms/baiduFlac.py +++ /dev/null @@ -1,124 +0,0 @@ -''' -Function: - 百度无损音乐下载: http://music.baidu.com/ -Author: - Charles -微信公众号: - Charles的皮卡丘 -声明: - 代码仅供学习交流, 不得用于商业/非法使用. -''' -import os -import click -import requests -from contextlib import closing - - -''' -Input: - -mode: search(搜索模式)/download(下载模式) - --search模式: - ----songname: 搜索的歌名 - --download模式: - ----need_down_list: 需要下载的歌曲名列表 - ----savepath: 下载歌曲保存路径 -Return: - -search模式: - --search_results: 搜索结果 - -download模式: - --downed_list: 成功下载的歌曲名列表 -''' -class baiduFlac(): - def __init__(self): - self.headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', - 'referer': 'http://music.baidu.com/' - } - self.search_url = "http://musicapi.qianqian.com/v1/restserver/ting" - self.flac_url = 'http://music.baidu.com/data/music/fmlink' - self.search_results = {} - '''外部调用''' - def get(self, mode='search', **kwargs): - if mode == 'search': - songname = kwargs.get('songname') - self.search_results = self.__searchBySongname(songname) - return self.search_results - elif mode == 'download': - need_down_list = kwargs.get('need_down_list') - downed_list = [] - savepath = kwargs.get('savepath') if kwargs.get('savepath') is not None else './results' - if need_down_list is not None: - for download_name in need_down_list: - songid = self.search_results.get(download_name) - params = {"songIds": songid, 'type': 'flac'} - res = requests.get(self.flac_url, params=params, headers=self.headers) - if res.json().get('errorCode') != 22000: - continue - download_url = res.json().get('data').get('songList')[0].get('songLink') - if not download_url: - continue - res = self.__download(download_name, download_url, savepath, '.flac') - if res: - downed_list.append(download_name) - return downed_list - else: - raise ValueError('mode in baiduFlac().get must be or ...') - '''下载''' - def __download(self, download_name, download_url, savepath, extension='.flac'): - if not os.path.exists(savepath): - os.mkdir(savepath) - download_name = download_name.replace('<', '').replace('>', '').replace('\\', '').replace('/', '') \ - .replace('?', '').replace(':', '').replace('"', '').replace(':', '') \ - .replace('|', '').replace('?', '').replace('*', '') - savename = 'baiduFlac_{}'.format(download_name) - count = 0 - while os.path.isfile(os.path.join(savepath, savename+extension)): - count += 1 - savename = 'baiduFlac_{}_{}'.format(download_name, count) - savename += extension - try: - print('[baiduFlac-INFO]: 正在下载 --> %s' % savename.split('.')[0]) - with closing(requests.get(download_url, headers=self.headers, stream=True, verify=False)) as res: - total_size = int(res.headers['content-length']) - if res.status_code == 200: - label = '[FileSize]:%0.2f MB' % (total_size/(1024*1024)) - with click.progressbar(length=total_size, label=label) as progressbar: - with open(os.path.join(savepath, savename), "wb") as f: - for chunk in res.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) - progressbar.update(1024) - else: - raise RuntimeError('Connect error...') - return True - except: - return False - '''根据歌名搜索''' - def __searchBySongname(self, songname): - params = { - "query": songname, - "method": "baidu.ting.search.common", - "format": "json", - "page_no": 1, - "page_size": 15 - } - res = requests.get(self.search_url, params=params, headers=self.headers) - results = {} - for song in res.json()['song_list']: - songid = song.get('song_id') - singers = song.get('author').replace("", "").replace("", "") - album = song.get('album_title').replace("", "").replace("", "") - download_name = '%s--%s--%s' % (song.get('title').replace("", "").replace("", ""), singers, album) - count = 0 - while download_name in results: - count += 1 - download_name = '%s(%d)--%s--%s' % (song.get('title'), count, singers, album) - results[download_name] = songid - return results - - -'''测试用''' -if __name__ == '__main__': - baiduflac_downloader = baiduFlac() - res = baiduflac_downloader.get(mode='search', songname='最好的我们') - baiduflac_downloader.get(mode='download', need_down_list=list(res.keys())[:2]) \ No newline at end of file diff --git a/MusicDownloader/platforms/kugou.py b/MusicDownloader/platforms/kugou.py deleted file mode 100644 index c1a5485..0000000 --- a/MusicDownloader/platforms/kugou.py +++ /dev/null @@ -1,126 +0,0 @@ -''' -Function: - 酷狗音乐下载: http://www.kugou.com/ -Author: - Charles -微信公众号: - Charles的皮卡丘 -声明: - 代码仅供学习交流, 不得用于商业/非法使用. -''' -import re -import os -import time -import click -import requests -from contextlib import closing - - -''' -Input: - -mode: search(搜索模式)/download(下载模式) - --search模式: - ----songname: 搜索的歌名 - --download模式: - ----need_down_list: 需要下载的歌曲名列表 - ----savepath: 下载歌曲保存路径 -Return: - -search模式: - --search_results: 搜索结果 - -download模式: - --downed_list: 成功下载的歌曲名列表 -''' -class kugou(): - def __init__(self): - self.headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36' - } - self.down_headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36', - 'Host': 'webfs.yun.kugou.com', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', - 'Cache-Control': 'max-age=0', - 'Connection': 'keep-alive' - } - self.search_url = 'http://songsearch.kugou.com/song_search_v2?keyword={}&page=1&pagesize=30' - self.hash_url = 'https://wwwapi.kugou.com/yy/index.php?r=play/getdata&hash={}&album_id={}&dfid=&mid=ccbb9592c3177be2f3977ff292e0f145&platid=4' - self.search_results = {} - '''外部调用''' - def get(self, mode='search', **kwargs): - if mode == 'search': - songname = kwargs.get('songname') - self.search_results = self.__searchBySongname(songname) - return self.search_results - elif mode == 'download': - need_down_list = kwargs.get('need_down_list') - downed_list = [] - savepath = kwargs.get('savepath') if kwargs.get('savepath') is not None else './results' - if need_down_list is not None: - for download_name in need_down_list: - filehash, album_id = self.search_results.get(download_name) - res = requests.get(self.hash_url.format(filehash, album_id)) - play_url = re.findall('"play_url":"(.*?)"', res.text)[0] - download_url = play_url.replace("\\", "") - if not download_url: - continue - res = self.__download(download_name, download_url, savepath, '.mp3') - if res: - downed_list.append(download_name) - return downed_list - else: - raise ValueError('mode in kugou().get must be or ...') - '''下载''' - def __download(self, download_name, download_url, savepath, extension='.mp3'): - if not os.path.exists(savepath): - os.mkdir(savepath) - download_name = download_name.replace('<', '').replace('>', '').replace('\\', '').replace('/', '') \ - .replace('?', '').replace(':', '').replace('"', '').replace(':', '') \ - .replace('|', '').replace('?', '').replace('*', '') - savename = 'kugou_{}'.format(download_name) - count = 0 - while os.path.isfile(os.path.join(savepath, savename+extension)): - count += 1 - savename = 'kugou_{}_{}'.format(download_name, count) - savename += extension - try: - print('[kugou-INFO]: 正在下载 --> %s' % savename.split('.')[0]) - with closing(requests.get(download_url, headers=self.down_headers, stream=True, verify=False)) as res: - total_size = int(res.headers['content-length']) - if res.status_code == 200: - label = '[FileSize]:%0.2f MB' % (total_size/(1024*1024)) - with click.progressbar(length=total_size, label=label) as progressbar: - with open(os.path.join(savepath, savename), "wb") as f: - for chunk in res.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) - progressbar.update(1024) - else: - raise RuntimeError('Connect error...') - return True - except: - return False - '''根据歌名搜索''' - def __searchBySongname(self, songname): - res = requests.get(self.search_url.format(songname), headers=self.headers) - results = {} - for song in res.json()['data']['lists']: - filehash = song.get('FileHash') - singers = song.get('SingerName') - album = song.get('AlbumName') - album_id = song.get('AlbumID') - download_name = '%s--%s--%s' % (song.get('SongName'), singers, album) - count = 0 - while download_name in results: - count += 1 - download_name = '%s(%d)--%s--%s' % (song.get('SongName'), count, singers, album) - results[download_name] = [filehash, album_id] - return results - - -'''测试用''' -if __name__ == '__main__': - kg = kugou() - res = kg.get(mode='search', songname='那些年') - kg.get(mode='download', need_down_list=list(res.keys())[:5]) \ No newline at end of file diff --git a/MusicDownloader/platforms/kuwo.py b/MusicDownloader/platforms/kuwo.py deleted file mode 100644 index 576bd4c..0000000 --- a/MusicDownloader/platforms/kuwo.py +++ /dev/null @@ -1,121 +0,0 @@ -''' -Function: - 酷我音乐下载: http://yinyue.kuwo.cn/ -Author: - Charles -微信公众号: - Charles的皮卡丘 -声明: - 代码仅供学习交流, 不得用于商业/非法使用. -''' -import re -import os -import click -import requests -from contextlib import closing - - -''' -Input: - -mode: search(搜索模式)/download(下载模式) - --search模式: - ----songname: 搜索的歌名 - --download模式: - ----need_down_list: 需要下载的歌曲名列表 - ----savepath: 下载歌曲保存路径 -Return: - -search模式: - --search_results: 搜索结果 - -download模式: - --downed_list: 成功下载的歌曲名列表 -''' -class kuwo(): - def __init__(self): - self.headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' - } - self.search_url = 'http://sou.kuwo.cn/ws/NSearch?type=all&catalog=yueku2016&key={}' - self.player_url = 'http://player.kuwo.cn/webmusic/st/getNewMuiseByRid?rid=MUSIC_{}' - self.search_results = {} - '''外部调用''' - def get(self, mode='search', **kwargs): - if mode == 'search': - songname = kwargs.get('songname') - self.search_results = self.__searchBySongname(songname) - return self.search_results - elif mode == 'download': - need_down_list = kwargs.get('need_down_list') - downed_list = [] - savepath = kwargs.get('savepath') if kwargs.get('savepath') is not None else './results' - if need_down_list is not None: - for download_name in need_down_list: - songid = self.search_results.get(download_name) - res = requests.get(self.player_url.format(songid), headers=self.headers) - mp3dl = re.findall(r'(.*?)', res.text)[0] - mp3path = re.findall(r'(.*?)', res.text)[0] - download_url = 'http://' + mp3dl + '/resource/' + mp3path - res = self.__download(download_name, download_url, savepath, '.mp3') - if res: - downed_list.append(download_name) - return downed_list - else: - raise ValueError('mode in kuwo().get must be or ...') - '''下载''' - def __download(self, download_name, download_url, savepath, extension='.mp3'): - if not os.path.exists(savepath): - os.mkdir(savepath) - download_name = download_name.replace('<', '').replace('>', '').replace('\\', '').replace('/', '') \ - .replace('?', '').replace(':', '').replace('"', '').replace(':', '') \ - .replace('|', '').replace('?', '').replace('*', '') - savename = 'kuwo_{}'.format(download_name) - count = 0 - while os.path.isfile(os.path.join(savepath, savename+extension)): - count += 1 - savename = 'kuwo_{}_{}'.format(download_name, count) - savename += extension - try: - print('[kuwo-INFO]: 正在下载 --> %s' % savename.split('.')[0]) - with closing(requests.get(download_url, headers=self.headers, stream=True, verify=False)) as res: - total_size = int(res.headers['content-length']) - if res.status_code == 200: - label = '[FileSize]:%0.2f MB' % (total_size/(1024*1024)) - with click.progressbar(length=total_size, label=label) as progressbar: - with open(os.path.join(savepath, savename), "wb") as f: - for chunk in res.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) - progressbar.update(1024) - else: - raise RuntimeError('Connect error...') - return True - except: - return False - '''根据歌名搜索''' - def __searchBySongname(self, songname): - res = requests.get(self.search_url.format(songname), headers=self.headers) - infos = re.findall(r'', res.text) - albums = re.findall(r'\

(.*?)\', res.text) - all_singers = re.findall(r'\

(.*?)\', res.text) - results = {} - for i in range(len(infos)): - songid = infos[i][0] - singers = re.findall(r'title="(.*?)"', all_singers[i]) - singers = ','.join(singers) - try: - album = re.findall(r'title="(.*?)"', albums[i])[0] - except: - album = '无专辑' - download_name = '%s--%s--%s' % (infos[i][1], singers, album) - count = 0 - while download_name in results: - count += 1 - download_name = '%s(%d)--%s--%s' % (infos[i][1], count, singers, album) - results[download_name] = songid - return results - - -'''测试用''' -if __name__ == '__main__': - kw = kuwo() - res = kw.get(mode='search', songname='尾戒') - kw.get(mode='download', need_down_list=list(res.keys())[:2]) \ No newline at end of file diff --git a/MusicDownloader/platforms/migu.py b/MusicDownloader/platforms/migu.py deleted file mode 100644 index bbe1b0e..0000000 --- a/MusicDownloader/platforms/migu.py +++ /dev/null @@ -1,111 +0,0 @@ -''' -Function: - 咪咕音乐下载: http://www.migu.cn/ -Author: - Charles -微信公众号: - Charles的皮卡丘 -声明: - 代码仅供学习交流, 不得用于商业/非法使用. -''' -import re -import os -import click -import requests -from contextlib import closing - - -''' -Input: - -mode: search(搜索模式)/download(下载模式) - --search模式: - ----songname: 搜索的歌名 - --download模式: - ----need_down_list: 需要下载的歌曲名列表 - ----savepath: 下载歌曲保存路径 -Return: - -search模式: - --search_results: 搜索结果 - -download模式: - --downed_list: 成功下载的歌曲名列表 -''' -class migu(): - def __init__(self): - self.headers = { - 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Mobile Safari/537.36' - } - self.search_url = 'http://m.music.migu.cn/migu/remoting/scr_search_tag?rows=20&type=2&keyword={}&pgc=1' - self.search_results = {} - '''外部调用''' - def get(self, mode='search', **kwargs): - if mode == 'search': - songname = kwargs.get('songname') - self.search_results = self.__searchBySongname(songname) - return self.search_results - elif mode == 'download': - need_down_list = kwargs.get('need_down_list') - downed_list = [] - savepath = kwargs.get('savepath') if kwargs.get('savepath') is not None else './results' - if need_down_list is not None: - for download_name in need_down_list: - download_url = self.search_results.get(download_name)[-1] - res = self.__download(download_name, download_url, savepath, extension='.mp3') - if res: - downed_list.append(download_name) - return downed_list - else: - raise ValueError('mode in migu().get must be or ...') - '''下载''' - def __download(self, download_name, download_url, savepath, extension='.mp3'): - if not os.path.exists(savepath): - os.mkdir(savepath) - download_name = download_name.replace('<', '').replace('>', '').replace('\\', '').replace('/', '') \ - .replace('?', '').replace(':', '').replace('"', '').replace(':', '') \ - .replace('|', '').replace('?', '').replace('*', '') - savename = 'migu_{}'.format(download_name) - count = 0 - while os.path.isfile(os.path.join(savepath, savename+extension)): - count += 1 - savename = 'migu_{}_{}'.format(download_name, count) - savename += extension - try: - print('[migu-INFO]: 正在下载 --> %s' % savename.split('.')[0]) - with closing(requests.get(download_url, headers=self.headers, stream=True, verify=False)) as res: - total_size = int(res.headers['content-length']) - if res.status_code == 200: - label = '[FileSize]:%0.2f MB' % (total_size/(1024*1024)) - with click.progressbar(length=total_size, label=label) as progressbar: - with open(os.path.join(savepath, savename), "wb") as f: - for chunk in res.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) - progressbar.update(1024) - else: - raise RuntimeError('Connect error...') - return True - except: - return False - '''根据歌名搜索''' - def __searchBySongname(self, songname): - res = requests.get(self.search_url.format(songname), headers=self.headers) - results = {} - for item in res.json().get('musics'): - songid = item.get('id', '') - songname = item.get('songName', '') - singers = item.get('singerName', '') - album = item.get('albumName', '') - download_name = '%s--%s--%s' % (songname, singers, album) - count = 0 - while download_name in results: - count += 1 - download_name = '%s(%d)--%s--%s' % (songname, count, singers, album) - download_url = item.get('mp3') - results[download_name] = [songid, download_url] - return results - - -'''测试用''' -if __name__ == '__main__': - mg = migu() - res = mg.get(mode='search', songname='那些年') - mg.get(mode='download', need_down_list=list(res.keys())[:2]) \ No newline at end of file diff --git a/MusicDownloader/platforms/qianqian.py b/MusicDownloader/platforms/qianqian.py deleted file mode 100644 index 9807265..0000000 --- a/MusicDownloader/platforms/qianqian.py +++ /dev/null @@ -1,124 +0,0 @@ -''' -Function: - 千千音乐下载: http://music.taihe.com/ -Author: - Charles -微信公众号: - Charles的皮卡丘 -声明: - 代码仅供学习交流, 不得用于商业/非法使用. -''' -import os -import click -import requests -from contextlib import closing - - -''' -Input: - -mode: search(搜索模式)/download(下载模式) - --search模式: - ----songname: 搜索的歌名 - --download模式: - ----need_down_list: 需要下载的歌曲名列表 - ----savepath: 下载歌曲保存路径 -Return: - -search模式: - --search_results: 搜索结果 - -download模式: - --downed_list: 成功下载的歌曲名列表 -''' -class qianqian(): - def __init__(self): - self.headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', - 'referer': 'http://music.baidu.com/' - } - self.search_url = "http://musicapi.qianqian.com/v1/restserver/ting" - self.player_url = 'http://music.baidu.com/data/music/links' - self.search_results = {} - '''外部调用''' - def get(self, mode='search', **kwargs): - if mode == 'search': - songname = kwargs.get('songname') - self.search_results = self.__searchBySongname(songname) - return self.search_results - elif mode == 'download': - need_down_list = kwargs.get('need_down_list') - downed_list = [] - savepath = kwargs.get('savepath') if kwargs.get('savepath') is not None else './results' - if need_down_list is not None: - for download_name in need_down_list: - songid = self.search_results.get(download_name) - params = {"songIds": songid} - res = requests.get(self.player_url, params=params, headers=self.headers) - if not res.json().get('data').get('songList'): - continue - download_url = res.json().get('data').get('songList')[0].get('songLink') - if not download_url: - continue - res = self.__download(download_name, download_url, savepath, '.mp3') - if res: - downed_list.append(download_name) - return downed_list - else: - raise ValueError('mode in qianqian().get must be or ...') - '''下载''' - def __download(self, download_name, download_url, savepath, extension): - if not os.path.exists(savepath): - os.mkdir(savepath) - download_name = download_name.replace('<', '').replace('>', '').replace('\\', '').replace('/', '') \ - .replace('?', '').replace(':', '').replace('"', '').replace(':', '') \ - .replace('|', '').replace('?', '').replace('*', '') - savename = 'qianqian_{}'.format(download_name) - count = 0 - while os.path.isfile(os.path.join(savepath, savename+extension)): - count += 1 - savename = 'qianqian_{}_{}'.format(download_name, count) - savename += extension - try: - print('[qianqian-INFO]: 正在下载 --> %s' % savename.split('.')[0]) - with closing(requests.get(download_url, headers=self.headers, stream=True, verify=False)) as res: - total_size = int(res.headers['content-length']) - if res.status_code == 200: - label = '[FileSize]:%0.2f MB' % (total_size/(1024*1024)) - with click.progressbar(length=total_size, label=label) as progressbar: - with open(os.path.join(savepath, savename), "wb") as f: - for chunk in res.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) - progressbar.update(1024) - else: - raise RuntimeError('Connect error...') - return True - except: - return False - '''根据歌名搜索''' - def __searchBySongname(self, songname): - params = { - "query": songname, - "method": "baidu.ting.search.common", - "format": "json", - "page_no": 1, - "page_size": 15 - } - res = requests.get(self.search_url, params=params, headers=self.headers) - results = {} - for song in res.json()['song_list']: - songid = song.get('song_id') - singers = song.get('author').replace("", "").replace("", "") - album = song.get('album_title').replace("", "").replace("", "") - download_name = '%s--%s--%s' % (song.get('title').replace("", "").replace("", ""), singers, album) - count = 0 - while download_name in results: - count += 1 - download_name = '%s(%d)--%s--%s' % (song.get('title'), count, singers, album) - results[download_name] = songid - return results - - -'''测试用''' -if __name__ == '__main__': - qianqian_downloader = qianqian() - res = qianqian_downloader.get(mode='search', songname='尾戒') - qianqian_downloader.get(mode='download', need_down_list=list(res.keys())[:2]) \ No newline at end of file diff --git a/MusicDownloader/platforms/qq.py b/MusicDownloader/platforms/qq.py deleted file mode 100644 index 1622eb4..0000000 --- a/MusicDownloader/platforms/qq.py +++ /dev/null @@ -1,154 +0,0 @@ -''' -Function: - qq音乐下载: https://y.qq.com/ -Author: - Charles -微信公众号: - Charles的皮卡丘 -声明: - 代码仅供学习交流, 不得用于商业/非法使用. -''' -import os -import click -import random -import requests -from contextlib import closing - - -''' -Input: - -mode: search(搜索模式)/download(下载模式) - --search模式: - ----songname: 搜索的歌名 - --download模式: - ----need_down_list: 需要下载的歌曲名列表 - ----savepath: 下载歌曲保存路径 -Return: - -search模式: - --search_results: 搜索结果 - -download模式: - --downed_list: 成功下载的歌曲名列表 -''' -class qq(): - def __init__(self): - self.headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36', - 'referer': 'http://y.qq.com' - } - self.ios_headers = { - 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', - 'referer': 'http://y.qq.com' - } - self.search_url = 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp' - self.mobile_fcg_url = 'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg' - self.download_url_format = 'http://dl.stream.qqmusic.qq.com/{}?vkey={}&guid={}&uin=3051522991&fromtag=64' - self.fcg_url = 'https://u.y.qq.com/cgi-bin/musicu.fcg?data=%7B%22req%22%3A%7B%22module%22%3A%22CDN.SrfCdnDispatchServer%22%2C%22method%22%3A%22GetCdnDispatch%22%2C%22param%22%3A%7B%7D%7D%2C%22req_0%22%3A%7B%22module%22%3A%22vkey.GetVkeyServer%22%2C%22method%22%3A%22CgiGetVkey%22%2C%22param%22%3A%7B%22guid%22%3A%2200%22%2C%22songmid%22%3A%5B%22{}%22%5D%2C%22songtype%22%3A%5B0%5D%2C%22uin%22%3A%2200%22%7D%7D%7D' - self.search_results = {} - '''外部调用''' - def get(self, mode='search', **kwargs): - if mode == 'search': - songname = kwargs.get('songname') - self.search_results = self.__searchBySongname(songname) - return self.search_results - elif mode == 'download': - need_down_list = kwargs.get('need_down_list') - downed_list = [] - savepath = kwargs.get('savepath') if kwargs.get('savepath') is not None else './results' - if need_down_list is not None: - for download_name in need_down_list: - songmid, media_mid = self.search_results.get(download_name) - guid = str(random.randrange(1000000000, 10000000000)) - params = {"guid": guid, - "loginUin": "3051522991", - "format": "json", - "platform": "yqq", - "cid": "205361747", - "uin": "3051522991", - "songmid": songmid, - "needNewCode": 0} - qualities = [("A000", "ape", 800), ("F000", "flac", 800), ("M800", "mp3", 320), ("C400", "m4a", 128), ("M500", "mp3", 128)] - for quality in qualities: - params['filename'] = '%s%s.%s' % (quality[0], songmid, quality[1]) - res = requests.get(self.mobile_fcg_url, params=params, headers=self.ios_headers) - try: - vkey = res.json().get('data', {}).get('items', [{}])[0].get('vkey', '') - except: - vkey = '' - if vkey: - download_url = self.download_url_format.format('%s%s.%s' % (quality[0], songmid, quality[1]), vkey, guid) - res = self.__download(download_name, download_url, savepath, '.'+quality[1]) - else: - res = False - if res: - break - print('[qq-INFO]: %s-%s下载失败, 将尝试降低歌曲音质重新下载...' % (download_name, quality[0]+quality[1]+str(quality[2]))) - if not res: - fcg_res = requests.get(self.fcg_url.format(songmid), headers=self.headers) - fcg_res_json = fcg_res.json() - download_url = str(fcg_res_json["req"]["data"]["freeflowsip"][0]) + str(fcg_res_json["req_0"]["data"]["midurlinfo"][0]["purl"]) - res = self.__download(download_name, download_url, savepath, '.m4a') - if res: - downed_list.append(download_name) - return downed_list - else: - raise ValueError('mode in qq().get must be or ...') - '''下载''' - def __download(self, download_name, download_url, savepath, extension='.m4a'): - if not os.path.exists(savepath): - os.mkdir(savepath) - download_name = download_name.replace('<', '').replace('>', '').replace('\\', '').replace('/', '') \ - .replace('?', '').replace(':', '').replace('"', '').replace(':', '') \ - .replace('|', '').replace('?', '').replace('*', '') - savename = 'qq_{}'.format(download_name) - count = 0 - while os.path.isfile(os.path.join(savepath, savename+extension)): - count += 1 - savename = 'qq_{}_{}'.format(download_name, count) - savename += extension - try: - print('[qq-INFO]: 正在下载 --> %s' % savename.split('.')[0]) - with closing(requests.get(download_url, headers=self.headers, stream=True, verify=False)) as res: - total_size = int(res.headers['content-length']) - if res.status_code == 200: - label = '[FileSize]:%0.2f MB' % (total_size/(1024*1024)) - with click.progressbar(length=total_size, label=label) as progressbar: - with open(os.path.join(savepath, savename), "wb") as f: - for chunk in res.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) - progressbar.update(1024) - else: - raise RuntimeError('Connect error...') - return True - except: - return False - '''根据歌名搜索''' - def __searchBySongname(self, songname): - params = { - 'w': songname, - 'format': 'json', - 'p': 1, - 'n': 15 - } - res = requests.get(self.search_url, params=params, headers=self.headers) - results = {} - for song in res.json()['data']['song']['list']: - media_mid = song.get('media_mid') - songmid = song.get('songmid') - singers = [s.get('name') for s in song.get('singer')] - singers = ','.join(singers) - album = song.get('albumname') - download_name = '%s--%s--%s' % (song.get('songname'), singers, album) - count = 0 - while download_name in results: - count += 1 - download_name = '%s(%d)--%s--%s' % (song.get('songname'), count, singers, album) - results[download_name] = [songmid, media_mid] - return results - - -'''测试用''' -if __name__ == '__main__': - qq_downloader = qq() - res = qq_downloader.get(mode='search', songname='那些年') - qq_downloader.get(mode='download', need_down_list=list(res.keys())[:9]) \ No newline at end of file diff --git a/MusicDownloader/platforms/wangyiyun.py b/MusicDownloader/platforms/wangyiyun.py deleted file mode 100644 index 18c0352..0000000 --- a/MusicDownloader/platforms/wangyiyun.py +++ /dev/null @@ -1,202 +0,0 @@ -''' -Function: - 网易云音乐下载: https://music.163.com/ -Author: - Charles -微信公众号: - Charles的皮卡丘 -声明: - 代码仅供学习交流, 不得用于商业/非法使用. -''' -import os -import json -import time -import click -import random -import base64 -import codecs -import requests -from Crypto.Cipher import AES -from contextlib import closing - - -''' -Function: - 用于算post的两个参数, 具体原理详见知乎: - https://www.zhihu.com/question/36081767 -''' -class Cracker(): - def __init__(self): - self.modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' - self.nonce = '0CoJUm6Qyw8W8jud' - self.pubKey = '010001' - def get(self, text): - text = json.dumps(text) - secKey = self._createSecretKey(16) - encText = self._aesEncrypt(self._aesEncrypt(text, self.nonce), secKey) - encSecKey = self._rsaEncrypt(secKey, self.pubKey, self.modulus) - post_data = { - 'params': encText, - 'encSecKey': encSecKey - } - return post_data - def _aesEncrypt(self, text, secKey): - pad = 16 - len(text) % 16 - if isinstance(text, bytes): - text = text.decode('utf-8') - text = text + str(pad * chr(pad)) - secKey = secKey.encode('utf-8') - encryptor = AES.new(secKey, 2, b'0102030405060708') - text = text.encode('utf-8') - ciphertext = encryptor.encrypt(text) - ciphertext = base64.b64encode(ciphertext) - return ciphertext - def _rsaEncrypt(self, text, pubKey, modulus): - text = text[::-1] - rs = int(codecs.encode(text.encode('utf-8'), 'hex_codec'), 16) ** int(pubKey, 16) % int(modulus, 16) - return format(rs, 'x').zfill(256) - def _createSecretKey(self, size): - return (''.join(map(lambda xx: (hex(ord(xx))[2:]), str(os.urandom(size)))))[0:16] - - -''' -Input: - -mode: search(搜索模式)/download(下载模式) - --search模式: - ----songname: 搜索的歌名 - --download模式: - ----need_down_list: 需要下载的歌曲名列表 - ----savepath: 下载歌曲保存路径 -Return: - -search模式: - --search_results: 搜索结果 - -download模式: - --downed_list: 成功下载的歌曲名列表 -''' -class wangyiyun(): - def __init__(self): - self.headers = { - 'Accept': '*/*', - 'Accept-Encoding': 'gzip,deflate,sdch', - 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', - 'Connection': 'keep-alive', - 'Content-Type': 'application/x-www-form-urlencoded', - 'Host': 'music.163.com', - 'cookie': '_iuqxldmzr_=32; _ntes_nnid=0e6e1606eb78758c48c3fc823c6c57dd,1527314455632; ' - '_ntes_nuid=0e6e1606eb78758c48c3fc823c6c57dd; __utmc=94650624; __utmz=94650624.1527314456.1.1.' - 'utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); WM_TID=blBrSVohtue8%2B6VgDkxOkJ2G0VyAgyOY;' - ' JSESSIONID-WYYY=Du06y%5Csx0ddxxx8n6G6Dwk97Dhy2vuMzYDhQY8D%2BmW3vlbshKsMRxS%2BJYEnvCCh%5CKY' - 'x2hJ5xhmAy8W%5CT%2BKqwjWnTDaOzhlQj19AuJwMttOIh5T%5C05uByqO%2FWM%2F1ZS9sqjslE2AC8YD7h7Tt0Shufi' - '2d077U9tlBepCx048eEImRkXDkr%3A1527321477141; __utma=94650624.1687343966.1527314456.1527314456' - '.1527319890.2; __utmb=94650624.3.10.1527319890', - 'Origin': 'https://music.163.com', - 'Referer': 'https://music.163.com/', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.32 Safari/537.36' - } - self.search_url = 'http://music.163.com/weapi/cloudsearch/get/web?csrf_token=' - self.player_url = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token=' - self.cracker = Cracker() - self.session = requests.Session() - self.session.headers.update(self.headers) - self.search_results = {} - '''外部调用''' - def get(self, mode='search', bit_rate=320000, csrf='', timeout=600, **kwargs): - if mode == 'search': - songname = kwargs.get('songname') - self.search_results = self.__searchBySongname(songname) - return self.search_results - elif mode == 'download': - need_down_list = kwargs.get('need_down_list') - downed_list = [] - savepath = kwargs.get('savepath') if kwargs.get('savepath') is not None else './results' - if need_down_list is not None: - for download_name in need_down_list: - songid = self.search_results.get(download_name) - params2 = { - 'ids': [songid], - 'br': bit_rate, - 'csrf_token': csrf - } - res = self.__postRequests(self.player_url, params2, timeout) - try: - download_url = res['data'][0]['url'] - except: - download_url = self.song_url.format(songid) - res = self.__download(download_name, download_url, savepath, '.mp3') - if res: - downed_list.append(download_name) - time.sleep(random.random()) - return downed_list - else: - raise ValueError('mode in wangyiyun().get must be or ...') - '''下载''' - def __download(self, download_name, download_url, savepath, extension='.mp3'): - if not os.path.exists(savepath): - os.mkdir(savepath) - download_name = download_name.replace('<', '').replace('>', '').replace('\\', '').replace('/', '') \ - .replace('?', '').replace(':', '').replace('"', '').replace(':', '') \ - .replace('|', '').replace('?', '').replace('*', '') - savename = 'wangyiyun_{}'.format(download_name) - count = 0 - while os.path.isfile(os.path.join(savepath, savename+extension)): - count += 1 - savename = 'wangyiyun_{}_{}'.format(download_name, count) - savename += extension - try: - print('[wangyiyun-INFO]: 正在下载 --> %s' % savename.split('.')[0]) - with closing(requests.get(download_url, stream=True, verify=False)) as res: - total_size = int(res.headers['content-length']) - if res.status_code == 200: - label = '[FileSize]:%0.2f MB' % (total_size/(1024*1024)) - with click.progressbar(length=total_size, label=label) as progressbar: - with open(os.path.join(savepath, savename), "wb") as f: - for chunk in res.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) - progressbar.update(1024) - else: - raise RuntimeError('Connect error...') - return True - except: - return False - '''根据歌名搜索''' - def __searchBySongname(self, songname, search_type=1, limit=9, timeout=600): - params1 = { - 's': songname, - 'type': search_type, - 'offset': 0, - 'sub': 'false', - 'limit': limit - } - res = self.__postRequests(self.search_url, params1, timeout) - results = {} - if res is not None: - if res['result']['songCount'] >= 1: - songs = res['result']['songs'] - for song in songs: - songid = song.get('id') - singers = [each.get('name') for each in song.get('ar')] - singers = ','.join(singers) - album = song.get('al').get('name') - download_name = '%s--%s--%s' % (song.get('name'), singers, album) - count = 0 - while download_name in results: - count += 1 - download_name = '%s(%d)--%s--%s' % (song.get('name'), count, singers, album) - results[download_name] = songid - return results - '''post请求函数''' - def __postRequests(self, url, params, timeout): - post_data = self.cracker.get(params) - res = self.session.post(url, data=post_data, timeout=timeout, headers=self.headers) - if res.json()['code'] != 200: - return None - else: - return res.json() - - -'''测试用''' -if __name__ == '__main__': - wangyiyun_downloader = wangyiyun() - res = wangyiyun_downloader.get(mode='search', songname='尾戒') - wangyiyun_downloader.get(mode='download', need_down_list=list(res.keys())[:2]) \ No newline at end of file diff --git a/MusicDownloader/platforms/xiami.py b/MusicDownloader/platforms/xiami.py deleted file mode 100644 index 2e04984..0000000 --- a/MusicDownloader/platforms/xiami.py +++ /dev/null @@ -1,162 +0,0 @@ -''' -Function: - 虾米音乐下载: https://www.xiami.com/ -Author: - Charles -微信公众号: - Charles的皮卡丘 -声明: - 代码仅供学习交流, 不得用于商业/非法使用. -''' -import os -import json -import click -import requests -from contextlib import closing -try: - from urllib.parse import unquote -except ImportError: - from urllib import unquote - - -''' -Function: - 破解虾米URL加密 -''' -class ParseURL(): - def __init__(self): - self.info = 'parse xiami url' - def parse(self, location): - rows, encryptUrl = int(location[:1]), location[1:] - encryptUrlLen = len(encryptUrl) - cols_base = encryptUrlLen // rows - rows_ex = encryptUrlLen % rows - matrix = [] - for row in range(rows): - length = cols_base + 1 if row < rows_ex else cols_base - matrix.append(encryptUrl[:length]) - encryptUrl = encryptUrl[length:] - decryptUrl = '' - for i in range(encryptUrlLen): - decryptUrl += matrix[i % rows][i // rows] - decryptUrl = unquote(decryptUrl).replace('^', '0') - return 'https:' + decryptUrl - - -''' -Input: - -mode: search(搜索模式)/download(下载模式) - --search模式: - ----songname: 搜索的歌名 - --download模式: - ----need_down_list: 需要下载的歌曲名列表 - ----savepath: 下载歌曲保存路径 -Return: - -search模式: - --search_results: 搜索结果 - -download模式: - --downed_list: 成功下载的歌曲名列表 -''' -class xiami(): - def __init__(self): - self.headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', - 'referer': 'http://m.xiami.com/' - } - self.search_url = 'http://api.xiami.com/web' - self.playlist_url = 'http://www.xiami.com/song/playlist/id/{}/object_name/default/object_id/0/cat/json' - self.parser = ParseURL() - self.session = requests.Session() - self.session.headers.update(self.headers) - self.session.head('http://m.xiami.com') - self.search_results = {} - '''外部调用''' - def get(self, mode='search', **kwargs): - if mode == 'search': - songname = kwargs.get('songname') - self.search_results = self.__searchBySongname(songname) - return self.search_results - elif mode == 'download': - need_down_list = kwargs.get('need_down_list') - downed_list = [] - savepath = kwargs.get('savepath') if kwargs.get('savepath') is not None else './results' - if need_down_list is not None: - for download_name in need_down_list: - songid = self.search_results.get(download_name) - try: - res = requests.get(self.playlist_url.format(songid), headers=self.headers) - songinfos = json.loads(res.text) - except: - continue - location = songinfos['data']['trackList'][0]['location'] - if not location: - continue - download_url = self.parser.parse(location) - res = self.__download(download_name, download_url, savepath, '.mp3') - if res: - downed_list.append(download_name) - return downed_list - else: - raise ValueError('mode in xiami().get must be or ...') - '''下载''' - def __download(self, download_name, download_url, savepath, extension): - if not os.path.exists(savepath): - os.mkdir(savepath) - download_name = download_name.replace('<', '').replace('>', '').replace('\\', '').replace('/', '') \ - .replace('?', '').replace(':', '').replace('"', '').replace(':', '') \ - .replace('|', '').replace('?', '').replace('*', '') - savename = 'xiami_{}'.format(download_name) - count = 0 - while os.path.isfile(os.path.join(savepath, savename+extension)): - count += 1 - savename = 'xiami_{}_{}'.format(download_name, count) - savename += extension - try: - print('[xiami-INFO]: 正在下载 --> %s' % savename.split('.')[0]) - with closing(requests.get(download_url, headers=self.headers, stream=True, verify=False)) as res: - total_size = int(res.headers['content-length']) - if res.status_code == 200: - label = '[FileSize]:%0.2f MB' % (total_size/(1024*1024)) - with click.progressbar(length=total_size, label=label) as progressbar: - with open(os.path.join(savepath, savename), "wb") as f: - for chunk in res.iter_content(chunk_size=1024): - if chunk: - f.write(chunk) - progressbar.update(1024) - else: - raise RuntimeError('Connect error...') - return True - except: - return False - '''根据歌名搜索''' - def __searchBySongname(self, songname): - params = { - "key": songname, - "v": "2.0", - "app_key": "1", - "r": "search/songs", - "page": 1, - "limit": 20, - } - res = self.session.get(self.search_url, params=params) - results = {} - for song in res.json()['data']['songs']: - if not song.get('listen_file'): - continue - songid = song.get('song_id') - singers = song.get('artist_name') - album = song.get('album_name') - download_name = '%s--%s--%s' % (song.get('song_name'), singers, album) - count = 0 - while download_name in results: - count += 1 - download_name = '%s(%d)--%s--%s' % (song.get('song_name'), count, singers, album) - results[download_name] = songid - return results - - -'''测试用''' -if __name__ == '__main__': - xiami_downloader = xiami() - res = xiami_downloader.get(mode='search', songname='尾戒') - xiami_downloader.get(mode='download', need_down_list=list(res.keys())[:9]) \ No newline at end of file diff --git a/README.md b/README.md index 1a342f7..e67988c 100644 --- a/README.md +++ b/README.md @@ -4,69 +4,50 @@ Music Downloader You can star this repository to keep track of the project if it's helpful for you, thank you for your support. ``` +# Documents +#### In Chinese +still on the way + # Statement ``` This repository is created just for learning python(Commercial prohibition). ``` # Support List -| Websites | Support? | in Chinese | -| :----: | :----: | :----: | -| [QQ](https://y.qq.com/) | ✔ | QQ音乐 | -| [Kuwo](http://yinyue.kuwo.cn/) | ✔ | 酷我音乐 | -| [Kugou](http://www.kugou.com/) | ✔ | 酷狗音乐 | -| [Xiami](https://www.xiami.com/) | ✔ | 虾米音乐 | -| [Qianqian](http://music.taihe.com/) | ✔ | 千千音乐 | -| [Migu](http://www.migu.cn/) | ✔ | 咪咕音乐 | -| [Wangyiyun](https://music.163.com/) | ✔ | 网易云音乐 | -| [baiduFlac](http://music.baidu.com/) | ✔ | 百度无损音乐 | +| Websites | Support Search? | Support Download? | in Chinese | +| :----: | :----: | :----: | :----: | +| [QQ](https://y.qq.com/) | ✓ | ✓ | QQ音乐 | +| [Kuwo](http://yinyue.kuwo.cn/) | ✓ | ✓ | 酷我音乐 | +| [Kugou](http://www.kugou.com/) | ✓ | ✓ | 酷狗音乐 | +| [Xiami](https://www.xiami.com/) | ✓ | ✓ | 虾米音乐 | +| [Qianqian](http://music.taihe.com/) | ✓ | ✓ | 千千音乐 | +| [Migu](http://www.migu.cn/) | ✓ | ✓ | 咪咕音乐 | +| [Wangyiyun](https://music.163.com/) | ✓ | ✓ | 网易云音乐 | +| [baiduFlac](http://music.baidu.com/) | ✓ | ✓ | 百度无损音乐 | -# Usage -### Take it as a software -#### Step1 -```sh -Download this repository: -Clone or download. +# Quick Start ``` -#### Step2 -```sh -Install some dependencies: -"pip install -r requirements.txt" or -"py -3 -m pip install -r requirements.txt" +run "python musicdl.py" ``` -#### Step3 -```sh -cd MusicDownloader, run cmd.py: -"python cmd.py" or "python3 cmd.py" + +# Install +#### Pip install ``` -### Take it as a library -#### Step1 -```sh -pip install git+https://github.com/CharlesPikachu/Music-Downloader.git@master +run "pip install musicdl" ``` -#### Step2 +#### Source code install ```sh -Write a python script like the following: -from MusicDownloader import cmd -md = cmd.MusicDownloader() -md.run() -Then, just run the python script~ -``` - -# Environment +(1) Offline +Step1: git clone https://github.com/CharlesPikachu/Music-Downloader.git +Step2: cd Music-Downloader -> run "python setup.py install" +(2) Online +run "pip install git+https://github.com/CharlesPikachu/Music-Downloader.git@master" ``` -OS: Windows/Linux -Python: Python3.5+(have installed necessary dependencies.) -``` - -# Running Screenshot -#### Cmd.py -![img](./Screenshot/cmd.png) -# Log -see Log dir → [click](./Log) +# Develop Log +see → [click](./record) # More #### WeChat Official Accounts *Charles_pikachu* -![img](./Screenshot/pikachu.jpg) \ No newline at end of file +![img](pikachu.jpg) \ No newline at end of file diff --git a/Screenshot/cmd.png b/Screenshot/cmd.png deleted file mode 100644 index 4ca95c810f6ca0e2d8982aa96d88bda070e04bb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71019 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 diff --git a/musicdl/__init__.py b/musicdl/__init__.py new file mode 100644 index 0000000..b15121b --- /dev/null +++ b/musicdl/__init__.py @@ -0,0 +1 @@ +__version__ = '2.1.0' \ No newline at end of file diff --git a/musicdl/config.json b/musicdl/config.json new file mode 100644 index 0000000..98e8157 --- /dev/null +++ b/musicdl/config.json @@ -0,0 +1,6 @@ +{ + "logfilepath": "musicdl.log", + "proxies": {"https": "127.0.0.1:1080"}, + "search_size_per_source": 5, + "savedir": "downloaded" +} \ No newline at end of file diff --git a/musicdl/modules/__init__.py b/musicdl/modules/__init__.py new file mode 100644 index 0000000..148fad7 --- /dev/null +++ b/musicdl/modules/__init__.py @@ -0,0 +1,3 @@ +'''import all''' +from .utils import * +from .sources import * \ No newline at end of file diff --git a/musicdl/modules/sources/__init__.py b/musicdl/modules/sources/__init__.py new file mode 100644 index 0000000..720668f --- /dev/null +++ b/musicdl/modules/sources/__init__.py @@ -0,0 +1,8 @@ +'''import all''' +from .qq import qq +from .kuwo import kuwo +from .migu import migu +from .kugou import kugou +from .netease import netease +from .qianqian import qianqian +from .baiduFlac import baiduFlac \ No newline at end of file diff --git a/musicdl/modules/sources/baiduFlac.py b/musicdl/modules/sources/baiduFlac.py new file mode 100644 index 0000000..d9195fe --- /dev/null +++ b/musicdl/modules/sources/baiduFlac.py @@ -0,0 +1,79 @@ +''' +Function: + 百度无损音乐下载: http://music.baidu.com/ +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import requests +from ..utils.misc import * +from ..utils.downloader import Downloader + + +'''百度无损音乐下载类''' +class baiduFlac(): + def __init__(self, config, logger_handle, **kwargs): + self.source = 'baiduFlac' + self.session = requests.Session() + self.session.proxies.update(config['proxies']) + self.config = config + self.logger_handle = logger_handle + self.__initialize() + '''歌曲搜索''' + def search(self, keyword): + self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword)) + cfg = self.config.copy() + params = { + 'query': keyword, + 'method': 'baidu.ting.search.common', + 'format': 'json', + 'page_no': '1', + 'page_size': cfg['search_size_per_source'] + } + response = self.session.get(self.search_url, headers=self.headers, params=params) + all_items = response.json()['song_list'] + songinfos = [] + for item in all_items: + params = { + 'songIds': str(item['song_id']), + 'type': 'flac' + } + response = self.session.get(self.fmlink_url, headers=self.headers, params=params) + response_json = response.json() + if response_json.get('errorCode') != 22000: continue + download_url = response_json['data']['songList'][0]['songLink'] + if not download_url: continue + filesize = str(round(int(response_json['data']['songList'][0]['size'])/1024/1024, 2)) + 'MB' + ext = response_json['data']['songList'][0]['format'] + duration = int(response_json['data']['songList'][0]['time']) + songinfo = { + 'source': self.source, + 'songid': str(item['song_id']), + 'singers': filterBadCharacter(item.get('author', '-')), + 'album': filterBadCharacter(item.get('album_title', '-')), + 'songname': filterBadCharacter(item.get('title', '-')).split('–')[0].strip(), + 'savedir': cfg['savedir'], + 'savename': '_'.join([self.source, filterBadCharacter(item.get('title', '-')).split('–')[0].strip()]), + 'download_url': download_url, + 'filesize': filesize, + 'ext': ext, + 'duration': seconds2hms(duration) + } + songinfos.append(songinfo) + return songinfos + '''歌曲下载''' + def download(self, songinfos): + for songinfo in songinfos: + self.logger_handle.info('正在从%s下载 ——> %s...' % (self.source, songinfo['savename'])) + task = Downloader(songinfo, self.session) + if task.start(): + self.logger_handle.info('成功从%s下载到了 ——> %s...' % (self.source, songinfo['savename'])) + '''初始化''' + def __initialize(self): + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', + 'Referer': 'http://music.baidu.com/' + } + self.search_url = 'http://musicapi.qianqian.com/v1/restserver/ting' + self.fmlink_url = 'http://music.baidu.com/data/music/fmlink' \ No newline at end of file diff --git a/musicdl/modules/sources/kugou.py b/musicdl/modules/sources/kugou.py new file mode 100644 index 0000000..480ec6c --- /dev/null +++ b/musicdl/modules/sources/kugou.py @@ -0,0 +1,80 @@ +''' +Function: + 酷狗音乐下载: http://www.kugou.com/ +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import requests +from ..utils.misc import * +from ..utils.downloader import Downloader + + +'''酷狗音乐下载类''' +class kugou(): + def __init__(self, config, logger_handle, **kwargs): + self.source = 'kugou' + self.session = requests.Session() + self.session.proxies.update(config['proxies']) + self.config = config + self.logger_handle = logger_handle + self.__initialize() + '''歌曲搜索''' + def search(self, keyword): + self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword)) + cfg = self.config.copy() + params = { + 'keyword': keyword, + 'page': '1', + 'pagesize': cfg['search_size_per_source'] + } + response = self.session.get(self.search_url, headers=self.headers, params=params) + all_items = response.json()['data']['lists'] + songinfos = [] + for item in all_items: + params = { + 'r': 'play/getdata', + 'hash': str(item['FileHash']), + 'album_id': str(item['AlbumID']), + 'dfid': '', + 'mid': 'ccbb9592c3177be2f3977ff292e0f145', + 'platid': '4' + } + response = self.session.get(self.hash_url, headers=self.headers, params=params) + response_json = response.json() + if response_json.get('err_code') != 0: continue + download_url = response_json['data']['play_url'].replace('\\', '') + if not download_url: continue + filesize = str(round(int(response_json['data']['filesize'])/1024/1024, 2)) + 'MB' + ext = download_url.split('.')[-1] + duration = int(item.get('Duration', 0)) + songinfo = { + 'source': self.source, + 'songid': str(item['ID']), + 'singers': filterBadCharacter(item.get('SingerName', '-')), + 'album': filterBadCharacter(item.get('AlbumName', '-')), + 'songname': filterBadCharacter(item.get('SongName', '-')), + 'savedir': cfg['savedir'], + 'savename': '_'.join([self.source, filterBadCharacter(item.get('SongName', '-'))]), + 'download_url': download_url, + 'filesize': filesize, + 'ext': ext, + 'duration': seconds2hms(duration) + } + songinfos.append(songinfo) + return songinfos + '''歌曲下载''' + def download(self, songinfos): + for songinfo in songinfos: + self.logger_handle.info('正在从%s下载 ——> %s...' % (self.source, songinfo['savename'])) + task = Downloader(songinfo, self.session) + if task.start(): + self.logger_handle.info('成功从%s下载到了 ——> %s...' % (self.source, songinfo['savename'])) + '''初始化''' + def __initialize(self): + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' + } + self.search_url = 'http://songsearch.kugou.com/song_search_v2' + self.hash_url = 'https://wwwapi.kugou.com/yy/index.php' \ No newline at end of file diff --git a/musicdl/modules/sources/kuwo.py b/musicdl/modules/sources/kuwo.py new file mode 100644 index 0000000..b131749 --- /dev/null +++ b/musicdl/modules/sources/kuwo.py @@ -0,0 +1,88 @@ +''' +Function: + 酷我音乐下载: http://www.kuwo.cn/ +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import time +import requests +from ..utils.misc import * +from ..utils.downloader import Downloader + + +'''酷我音乐下载类''' +class kuwo(): + def __init__(self, config, logger_handle, **kwargs): + self.source = 'kuwo' + self.session = requests.Session() + self.session.proxies.update(config['proxies']) + self.config = config + self.logger_handle = logger_handle + self.__initialize() + '''歌曲搜索''' + def search(self, keyword): + self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword)) + cfg = self.config.copy() + params = { + 'key': keyword, + 'pn': '1', + 'rn': cfg['search_size_per_source'], + 'reqId': 'ffa3dc80-73c2-11ea-a715-7de8a8cc7b68' + } + response = self.session.get(self.search_url, headers=self.headers, params=params) + all_items = response.json()['data']['list'] + songinfos = [] + for item in all_items: + params = { + 'format': 'mp3', + 'rid': str(item['rid']), + 'response': 'url', + 'type': 'convert_url3', + 'br': '128kmp3', + 'from': 'web', + 't': str(int(time.time()*1000)), + 'reqId': 'de97aac1-73c3-11ea-a715-7de8a8cc7b68' + } + response = self.session.get(self.player_url, headers=self.headers, params=params) + response_json = response.json() + if response_json.get('code') != 200: continue + download_url = response_json['url'] + if not download_url: continue + filesize = '-MB' + ext = download_url.split('.')[-1] + duration = int(item.get('duration', 0)) + songinfo = { + 'source': self.source, + 'songid': str(item['rid']), + 'singers': filterBadCharacter(item.get('artist', '-')), + 'album': filterBadCharacter(item.get('album', '-')), + 'songname': filterBadCharacter(item.get('name', '-')), + 'savedir': cfg['savedir'], + 'savename': '_'.join([self.source, filterBadCharacter(item.get('name', '-'))]), + 'download_url': download_url, + 'filesize': filesize, + 'ext': ext, + 'duration': seconds2hms(duration) + } + songinfos.append(songinfo) + return songinfos + '''歌曲下载''' + def download(self, songinfos): + for songinfo in songinfos: + self.logger_handle.info('正在从%s下载 ——> %s...' % (self.source, songinfo['savename'])) + task = Downloader(songinfo, self.session) + if task.start(): + self.logger_handle.info('成功从%s下载到了 ——> %s...' % (self.source, songinfo['savename'])) + '''初始化''' + def __initialize(self): + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', + 'csrf': '0HQ0UGKNAKR', + 'Host': 'www.kuwo.cn', + 'Referer': 'http://www.kuwo.cn/search/list', + 'Cookie': 'kw_token=0HQ0UGKNAKR;' + } + self.search_url = 'http://www.kuwo.cn/api/www/search/searchMusicBykeyWord' + self.player_url = 'http://www.kuwo.cn/url' \ No newline at end of file diff --git a/musicdl/modules/sources/migu.py b/musicdl/modules/sources/migu.py new file mode 100644 index 0000000..dd9596c --- /dev/null +++ b/musicdl/modules/sources/migu.py @@ -0,0 +1,82 @@ +''' +Function: + 咪咕音乐下载: http://www.migu.cn/ +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import requests +from ..utils.misc import * +from ..utils.downloader import Downloader + + +'''咪咕音乐下载类''' +class migu(): + def __init__(self, config, logger_handle, **kwargs): + self.source = 'migu' + self.session = requests.Session() + self.session.proxies.update(config['proxies']) + self.config = config + self.logger_handle = logger_handle + self.__initialize() + '''歌曲搜索''' + def search(self, keyword): + self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword)) + cfg = self.config.copy() + params = { + 'ua': 'Android_migu', + 'version': '5.0.1', + 'text': keyword, + 'pageNo': '1', + 'pageSize': cfg['search_size_per_source'], + 'searchSwitch': '{"song":1,"album":0,"singer":0,"tagSong":0,"mvSong":0,"songlist":0,"bestShow":1}', + } + response = self.session.get(self.search_url, headers=self.headers, params=params) + all_items = response.json()['songResultData']['result'] + songinfos = [] + for item in all_items: + ext = '' + download_url = '' + filesize = '-MB' + for rate in sorted(item.get('rateFormats', []), key=lambda x: int(x['size']), reverse=True): + if (int(rate['size']) == 0) or (not rate.get('formatType', '')) or (not rate.get('resourceType', '')): continue + ext = 'flac' if rate.get('formatType') == 'SQ' else 'mp3' + download_url = self.player_url.format(copyrightId=item['copyrightId'], + contentId=item['contentId'], + toneFlag=rate['formatType'], + resourceType=rate['resourceType']) + filesize = str(round(int(rate['size'])/1024/1024, 2)) + 'MB' + break + if not download_url: continue + duration = '-:-:-' + songinfo = { + 'source': self.source, + 'songid': str(item['id']), + 'singers': filterBadCharacter(','.join([s.get('name', '') for s in item.get('singers', [])])), + 'album': filterBadCharacter(item.get('albums', [{'name': '-'}])[0].get('name', '-')), + 'songname': filterBadCharacter(item.get('name', '-')), + 'savedir': cfg['savedir'], + 'savename': '_'.join([self.source, filterBadCharacter(item.get('name', '-'))]), + 'download_url': download_url, + 'filesize': filesize, + 'ext': ext, + 'duration': duration + } + songinfos.append(songinfo) + return songinfos + '''歌曲下载''' + def download(self, songinfos): + for songinfo in songinfos: + self.logger_handle.info('正在从%s下载 ——> %s...' % (self.source, songinfo['savename'])) + task = Downloader(songinfo, self.session) + if task.start(): + self.logger_handle.info('成功从%s下载到了 ——> %s...' % (self.source, songinfo['savename'])) + '''初始化''' + def __initialize(self): + self.headers = { + 'Referer': 'https://m.music.migu.cn/', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Mobile Safari/537.36' + } + self.search_url = 'http://pd.musicapp.migu.cn/MIGUM3.0/v1.0/content/search_all.do' + self.player_url = 'https://app.pd.nf.migu.cn/MIGUM3.0/v1.0/content/sub/listenSong.do?channel=mx©rightId={copyrightId}&contentId={contentId}&toneFlag={toneFlag}&resourceType={resourceType}&userId=15548614588710179085069&netType=00' \ No newline at end of file diff --git a/musicdl/modules/sources/netease.py b/musicdl/modules/sources/netease.py new file mode 100644 index 0000000..2b51352 --- /dev/null +++ b/musicdl/modules/sources/netease.py @@ -0,0 +1,134 @@ +''' +Function: + 网易云音乐下载: https://music.163.com/ +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import os +import base64 +import codecs +import requests +from ..utils.misc import * +from Crypto.Cipher import AES +from ..utils.downloader import Downloader + + +''' +Function: + 用于算post的两个参数, 具体原理详见知乎: + https://www.zhihu.com/question/36081767 +''' +class Cracker(): + def __init__(self): + self.modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' + self.nonce = '0CoJUm6Qyw8W8jud' + self.pubKey = '010001' + def get(self, text): + text = json.dumps(text) + secKey = self._createSecretKey(16) + encText = self._aesEncrypt(self._aesEncrypt(text, self.nonce), secKey) + encSecKey = self._rsaEncrypt(secKey, self.pubKey, self.modulus) + post_data = { + 'params': encText, + 'encSecKey': encSecKey + } + return post_data + def _aesEncrypt(self, text, secKey): + pad = 16 - len(text) % 16 + if isinstance(text, bytes): + text = text.decode('utf-8') + text = text + str(pad * chr(pad)) + secKey = secKey.encode('utf-8') + encryptor = AES.new(secKey, 2, b'0102030405060708') + text = text.encode('utf-8') + ciphertext = encryptor.encrypt(text) + ciphertext = base64.b64encode(ciphertext) + return ciphertext + def _rsaEncrypt(self, text, pubKey, modulus): + text = text[::-1] + rs = int(codecs.encode(text.encode('utf-8'), 'hex_codec'), 16) ** int(pubKey, 16) % int(modulus, 16) + return format(rs, 'x').zfill(256) + def _createSecretKey(self, size): + return (''.join(map(lambda xx: (hex(ord(xx))[2:]), str(os.urandom(size)))))[0:16] + + +'''网易云音乐下载类''' +class netease(): + def __init__(self, config, logger_handle, **kwargs): + self.source = 'netease' + self.session = requests.Session() + self.session.proxies.update(config['proxies']) + self.cracker = Cracker() + self.config = config + self.logger_handle = logger_handle + self.__initialize() + '''歌曲搜索''' + def search(self, keyword): + self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword)) + cfg = self.config.copy() + params = { + 's': keyword, + 'type': '1', + 'offset': '0', + 'sub': 'false', + 'limit': cfg['search_size_per_source'] + } + response = self.session.post(self.search_url, headers=self.headers, params=params, data=self.cracker.get(params)) + all_items = response.json()['result']['songs'] + songinfos = [] + for item in all_items: + if item['privilege']['fl'] == 0: continue + for q in ['h', 'm', 'l']: + params = { + 'ids': [item['id']], + 'br': item[q]['br'], + 'csrf_token': '' + } + response = self.session.post(self.player_url, headers=self.headers, data=self.cracker.get(params)) + response_json = response.json() + if response_json.get('code') == 200: break + if response_json.get('code') != 200: continue + download_url = response_json['data'][0]['url'] + if not download_url: continue + filesize = str(round(int(item[q]['size'])/1024/1024, 2)) + 'MB' + ext = download_url.split('.')[-1] + duration = int(item.get('dt', 0) / 1000) + songinfo = { + 'source': self.source, + 'songid': str(item['id']), + 'singers': filterBadCharacter(','.join([s.get('name', '') for s in item.get('ar')])), + 'album': filterBadCharacter(item.get('al', {}).get('name', '-')), + 'songname': filterBadCharacter(item.get('name', '-')), + 'savedir': cfg['savedir'], + 'savename': '_'.join([self.source, filterBadCharacter(item.get('name', '-'))]), + 'download_url': download_url, + 'filesize': filesize, + 'ext': ext, + 'duration': seconds2hms(duration) + } + songinfos.append(songinfo) + return songinfos + '''歌曲下载''' + def download(self, songinfos): + for songinfo in songinfos: + self.logger_handle.info('正在从%s下载 ——> %s...' % (self.source, songinfo['savename'])) + task = Downloader(songinfo, self.session) + if task.start(): + self.logger_handle.info('成功从%s下载到了 ——> %s...' % (self.source, songinfo['savename'])) + '''初始化''' + def __initialize(self): + self.headers = { + 'Accept': '*/*', + 'Accept-Encoding': 'gzip,deflate,sdch', + 'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4', + 'Connection': 'keep-alive', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Host': 'music.163.com', + 'Origin': 'https://music.163.com', + 'Referer': 'https://music.163.com/', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.32 Safari/537.36' + } + self.search_url = 'http://music.163.com/weapi/cloudsearch/get/web?csrf_token=' + self.player_url = 'http://music.163.com/weapi/song/enhance/player/url?csrf_token=' \ No newline at end of file diff --git a/musicdl/modules/sources/qianqian.py b/musicdl/modules/sources/qianqian.py new file mode 100644 index 0000000..19a0bbb --- /dev/null +++ b/musicdl/modules/sources/qianqian.py @@ -0,0 +1,80 @@ +''' +Function: + 千千音乐下载: http://music.taihe.com/ +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import requests +from ..utils.misc import * +from ..utils.downloader import Downloader + + +'''千千音乐下载类''' +class qianqian(): + def __init__(self, config, logger_handle, **kwargs): + self.source = 'qianqian' + self.session = requests.Session() + self.session.proxies.update(config['proxies']) + self.config = config + self.logger_handle = logger_handle + self.__initialize() + '''歌曲搜索''' + def search(self, keyword): + self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword)) + cfg = self.config.copy() + params = { + 'query': keyword, + 'method': 'baidu.ting.search.common', + 'format': 'json', + 'page_no': '1', + 'page_size': cfg['search_size_per_source'] + } + response = self.session.get(self.search_url, headers=self.headers, params=params) + all_items = response.json()['song_list'] + songinfos = [] + for item in all_items: + params = { + 'songIds': item['song_id'] + } + response = self.session.get(self.player_url, headers=self.headers, params=params) + response_json = response.json() + if response_json.get('errorCode') != 22000: continue + song_list = response_json['data']['songList'] + if not song_list: continue + download_url = song_list[0]['songLink'] + if not download_url: continue + filesize = str(round(int(song_list[0]['size'])/1024/1024, 2)) + 'MB' + ext = song_list[0]['format'] + duration = int(song_list[0].get('time', 0)) + songinfo = { + 'source': self.source, + 'songid': str(item['song_id']), + 'singers': filterBadCharacter(item.get('author', '-')), + 'album': filterBadCharacter(item.get('album_title', '-')), + 'songname': filterBadCharacter(item.get('title', '-')).split('–')[0].strip(), + 'savedir': cfg['savedir'], + 'savename': '_'.join([self.source, filterBadCharacter(item.get('title', '-')).split('–')[0].strip()]), + 'download_url': download_url, + 'filesize': filesize, + 'ext': ext, + 'duration': seconds2hms(duration) + } + songinfos.append(songinfo) + return songinfos + '''歌曲下载''' + def download(self, songinfos): + for songinfo in songinfos: + self.logger_handle.info('正在从%s下载 ——> %s...' % (self.source, songinfo['savename'])) + task = Downloader(songinfo, self.session) + if task.start(): + self.logger_handle.info('成功从%s下载到了 ——> %s...' % (self.source, songinfo['savename'])) + '''初始化''' + def __initialize(self): + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', + 'Referer': 'http://music.baidu.com/' + } + self.search_url = 'http://musicapi.qianqian.com/v1/restserver/ting' + self.player_url = 'http://music.baidu.com/data/music/links' \ No newline at end of file diff --git a/musicdl/modules/sources/qq.py b/musicdl/modules/sources/qq.py new file mode 100644 index 0000000..389a2e0 --- /dev/null +++ b/musicdl/modules/sources/qq.py @@ -0,0 +1,114 @@ +''' +Function: + qq音乐下载: https://y.qq.com/ +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import random +import requests +from ..utils.misc import * +from ..utils.downloader import Downloader + + +'''QQ音乐下载类''' +class qq(): + def __init__(self, config, logger_handle, **kwargs): + self.source = 'qq' + self.session = requests.Session() + self.session.proxies.update(config['proxies']) + self.config = config + self.logger_handle = logger_handle + self.__initialize() + '''歌曲搜索''' + def search(self, keyword): + self.logger_handle.info('正在%s中搜索 ——> %s...' % (self.source, keyword)) + cfg = self.config.copy() + params = { + 'w': keyword, + 'format': 'json', + 'p': '1', + 'n': cfg['search_size_per_source'] + } + response = self.session.get(self.search_url, headers=self.headers, params=params) + all_items = response.json()['data']['song']['list'] + songinfos = [] + for item in all_items: + params = { + 'guid': str(random.randrange(1000000000, 10000000000)), + 'loginUin': '3051522991', + 'format': 'json', + 'platform': 'yqq', + 'cid': '205361747', + 'uin': '3051522991', + 'songmid': item['songmid'], + 'needNewCode': '0' + } + ext = '' + download_url = '' + filesize = '-MB' + for quality in [("A000", "ape", 800), ("F000", "flac", 800), ("M800", "mp3", 320), ("C400", "m4a", 128), ("M500", "mp3", 128)]: + params['filename'] = '%s%s.%s' % (quality[0], item['songmid'], quality[1]) + response = self.session.get(self.mobile_fcg_url, headers=self.ios_headers, params=params) + response_json = response.json() + if response_json['code'] != 0: continue + vkey = response_json.get('data', {}).get('items', [{}])[0].get('vkey', '') + if vkey: + ext = quality[1] + download_url = 'http://dl.stream.qqmusic.qq.com/{}?vkey={}&guid={}&uin=3051522991&fromtag=64'.format('%s%s.%s' % (quality[0], item['songmid'], quality[1]), vkey, params['guid']) + if ext in ['ape', 'flac']: + filesize = item['size%s' % ext] + elif ext in ['mp3', 'm4a']: + filesize = item['size%s' % quality[-1]] + break + if not download_url: + response = self.session.get(self.fcg_url.format(item['songmid'])) + response_json = response.json() + if response_json['code'] == 0: + ext = '.m4a' + download_url = str(response_json["req"]["data"]["freeflowsip"][0]) + str(response_json["req_0"]["data"]["midurlinfo"][0]["purl"]) + filesize = item['size128'] + if (not download_url) or (filesize == '-MB') or (filesize == 0): continue + filesize = str(round(filesize/1024/1024, 2)) + 'MB' + duration = int(item.get('interval', 0)) + songinfo = { + 'source': self.source, + 'songid': str(item['songmid']), + 'singers': filterBadCharacter(','.join([s.get('name', '') for s in item.get('singer', [])])), + 'album': filterBadCharacter(item.get('albumname', '-')), + 'songname': filterBadCharacter(item.get('songname', '-')), + 'savedir': cfg['savedir'], + 'savename': '_'.join([self.source, filterBadCharacter(item.get('songname', '-'))]), + 'download_url': download_url, + 'filesize': filesize, + 'ext': ext, + 'duration': seconds2hms(duration) + } + songinfos.append(songinfo) + return songinfos + '''歌曲下载''' + def download(self, songinfos): + for songinfo in songinfos: + self.logger_handle.info('正在从%s下载 ——> %s...' % (self.source, songinfo['savename'])) + task = Downloader(songinfo, self.session) + if task.start(): + self.logger_handle.info('成功从%s下载到了 ——> %s...' % (self.source, songinfo['savename'])) + '''初始化''' + def __initialize(self): + self.ios_headers = { + 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1', + 'Referer': 'http://y.qq.com' + } + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36', + 'Referer': 'http://y.qq.com' + } + self.search_url = 'https://c.y.qq.com/soso/fcgi-bin/client_search_cp' + self.mobile_fcg_url = 'https://c.y.qq.com/base/fcgi-bin/fcg_music_express_mobile3.fcg' + self.fcg_url = 'https://u.y.qq.com/cgi-bin/musicu.fcg?data=%7B%22req%22%3A%7B%22module \ + %22%3A%22CDN.SrfCdnDispatchServer%22%2C%22method%22%3A%22GetCdnDispatch \ + %22%2C%22param%22%3A%7B%7D%7D%2C%22req_0%22%3A%7B%22module%22%3A%22vkey. \ + GetVkeyServer%22%2C%22method%22%3A%22CgiGetVkey%22%2C%22param%22%3A%7B%22 \ + guid%22%3A%2200%22%2C%22songmid%22%3A%5B%22{}%22%5D%2C%22songtype%22%3A%5 \ + B0%5D%2C%22uin%22%3A%2200%22%7D%7D%7D' \ No newline at end of file diff --git a/musicdl/modules/utils/__init__.py b/musicdl/modules/utils/__init__.py new file mode 100644 index 0000000..f255105 --- /dev/null +++ b/musicdl/modules/utils/__init__.py @@ -0,0 +1,4 @@ +'''import all''' +from .misc import * +from .logger import * +from .downloader import * \ No newline at end of file diff --git a/musicdl/modules/utils/downloader.py b/musicdl/modules/utils/downloader.py new file mode 100644 index 0000000..4e13889 --- /dev/null +++ b/musicdl/modules/utils/downloader.py @@ -0,0 +1,82 @@ +''' +Function: + 下载器类 +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import click +import warnings +import requests +from .misc import * +from contextlib import closing +warnings.filterwarnings('ignore') + + +'''下载器类''' +class Downloader(): + def __init__(self, songinfo, session=None, **kwargs): + self.songinfo = songinfo + self.session = requests.Session() if session is None else session + self.__initialize(songinfo['source']) + '''外部调用''' + def start(self): + songinfo, session, headers = self.songinfo, self.session, self.headers + is_success = False + checkDir(songinfo['savedir']) + with closing(session.get(songinfo['download_url'], headers=headers, stream=True, verify=False)) as response: + total_size = int(response.headers['content-length']) + chunk_size = 1024 + if response.status_code == 200: + label = '[FileSize]: %0.2fMB' % (total_size/1024/1024) + with click.progressbar(length=total_size, label=label) as progressbar: + with open(os.path.join(songinfo['savedir'], songinfo['savename']+'.'+songinfo['ext']), "wb") as fp: + for chunk in response.iter_content(chunk_size=chunk_size): + if chunk: + fp.write(chunk) + progressbar.update(chunk_size) + is_success = True + return is_success + '''初始化''' + def __initialize(self, source): + if source == 'baiduFlac': + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', + 'Referer': 'http://music.baidu.com/' + } + elif source == 'kugou': + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36', + 'Host': 'webfs.yun.kugou.com', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'Cache-Control': 'max-age=0', + 'Connection': 'keep-alive' + } + elif source == 'kuwo': + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' + } + elif source == 'netease': + self.headers = {} + elif source == 'qianqian': + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36', + 'Referer': 'http://music.baidu.com/' + } + elif source == 'qq': + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36', + 'Referer': 'http://y.qq.com' + } + elif source == 'migu': + self.headers = { + 'Referer': 'https://m.music.migu.cn/', + 'User-Agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Mobile Safari/537.36' + } + elif source == 'xiami': + pass + else: + raise ValueError('Unsupport download music from source <%s>...' % source) \ No newline at end of file diff --git a/musicdl/modules/utils/logger.py b/musicdl/modules/utils/logger.py new file mode 100644 index 0000000..d9ae426 --- /dev/null +++ b/musicdl/modules/utils/logger.py @@ -0,0 +1,44 @@ +''' +Function: + 一些终端打印工具 +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import logging +from prettytable import PrettyTable + + +'''打印日志类''' +class Logger(): + def __init__(self, logfilepath, **kwargs): + logging.basicConfig(level=logging.INFO, + format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', + handlers=[logging.FileHandler(logfilepath), + logging.StreamHandler()]) + @staticmethod + def log(level, message): + logging.log(level, message) + @staticmethod + def debug(message): + Logger.log(logging.DEBUG, message) + @staticmethod + def info(message): + Logger.log(logging.INFO, message) + @staticmethod + def warning(message): + Logger.log(logging.WARNING, message) + @staticmethod + def error(message): + Logger.log(logging.ERROR, message) + + +'''打印表格''' +def printTable(title, items): + assert isinstance(title, list) and isinstance(items, list), ' and <items> should be list in printTable...' + table = PrettyTable(title) + for item in items: table.add_row(item) + print(table) + return table \ No newline at end of file diff --git a/musicdl/modules/utils/misc.py b/musicdl/modules/utils/misc.py new file mode 100644 index 0000000..8bddb02 --- /dev/null +++ b/musicdl/modules/utils/misc.py @@ -0,0 +1,40 @@ +''' +Function: + 一些工具函数 +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import os +import json + + +'''检查文件夹是否存在''' +def checkDir(dirpath): + if not os.path.exists(dirpath): + os.mkdir(dirpath) + return False + return True + + +'''导入配置文件''' +def loadConfig(filepath='config.json'): + f = open(filepath, 'r', encoding='utf-8') + return json.load(f) + + +'''清楚可能出问题的字符''' +def filterBadCharacter(string): + string = string.replace('<em>', '').replace('</em>', '') \ + .replace('<', '').replace('>', '').replace('\\', '').replace('/', '') \ + .replace('?', '').replace(':', '').replace('"', '').replace(':', '') \ + .replace('|', '').replace('?', '').replace('*', '') + return string.strip().encode('utf-8', 'ignore').decode('utf-8') + + +'''秒转时分秒''' +def seconds2hms(seconds): + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + return '%02d:%02d:%02d' % (h, m, s) \ No newline at end of file diff --git a/musicdl/musicdl.py b/musicdl/musicdl.py new file mode 100644 index 0000000..9f8149a --- /dev/null +++ b/musicdl/musicdl.py @@ -0,0 +1,141 @@ +''' +Function: + 音乐下载器 +Author: + Charles +微信公众号: + Charles的皮卡丘 +''' +import sys +from modules import * + + +'''basic info''' +BASICINFO = '''************************************************************ +Function: 音乐下载器 V2.1.0 +Author: Charles +微信公众号: Charles的皮卡丘 +操作帮助: + 输入r: 重新初始化程序(即返回主菜单) + 输入q: 退出程序 + 下载多首歌曲: 选择想要下载的歌曲时,输入{1,2,5}可同时下载第1,2,5首歌 +歌曲保存路径: + 当前路径下的results文件夹内 +************************************************************''' + + +'''音乐下载器''' +class musicdl(): + def __init__(self, configpath, **kwargs): + self.config = loadConfig('config.json') + self.logger_handle = Logger(self.config['logfilepath']) + self.initializeAllSources() + '''非开发人员外部调用''' + def run(self): + while True: + print(BASICINFO) + # 音乐搜索 + user_input = self.dealInput('请输入歌曲搜索的关键词: ') + target_srcs = ['baiduFlac', 'kugou', 'kuwo', 'qq', 'qianqian', 'netease', 'migu', 'xiami'] + search_results = self.search(user_input, target_srcs) + # 打印搜索结果 + title = ['序号', '歌手', '歌名', '大小', '时长', '专辑', '来源'] + items = [] + records = {} + idx = 0 + for key, values in search_results.items(): + for value in values: + items.append([str(idx), value['singers'], value['songname'], value['filesize'], value['duration'], value['album'], value['source']]) + records.update({str(idx): value}) + idx += 1 + printTable(title, items) + # 音乐下载 + user_input = self.dealInput('请输入想要下载的音乐编号: ') + need_download_numbers = user_input.split(',') + songinfos = [] + for item in need_download_numbers: + songinfo = records.get(item, '') + if songinfo: songinfos.append(songinfo) + self.download(songinfos) + '''音乐搜索''' + def search(self, keyword, target_srcs): + search_results = {} + if 'baiduFlac' in target_srcs: + try: + search_results.update({'baiduFlac': self.baiduFlac.search(keyword)}) + except: + self.logger_handle.warning('无法在%s中搜索 ——> %s...' % ('baiduFlac', keyword)) + if 'kugou' in target_srcs: + try: + search_results.update({'kugou': self.kugou.search(keyword)}) + except: + self.logger_handle.warning('无法在%s中搜索 ——> %s...' % ('kugou', keyword)) + if 'kuwo' in target_srcs: + try: + search_results.update({'kuwo': self.kuwo.search(keyword)}) + except: + self.logger_handle.warning('无法在%s中搜索 ——> %s...' % ('kuwo', keyword)) + if 'netease' in target_srcs: + try: + search_results.update({'netease': self.netease.search(keyword)}) + except: + self.logger_handle.warning('无法在%s中搜索 ——> %s...' % ('netease', keyword)) + if 'qianqian' in target_srcs: + try: + search_results.update({'qianqian': self.qianqian.search(keyword)}) + except: + self.logger_handle.warning('无法在%s中搜索 ——> %s...' % ('qianqian', keyword)) + if 'qq' in target_srcs: + try: + search_results.update({'qq': self.qq.search(keyword)}) + except: + self.logger_handle.warning('无法在%s中搜索 ——> %s...' % ('qq', keyword)) + if 'migu' in target_srcs: + try: + search_results.update({'migu': self.migu.search(keyword)}) + except: + self.logger_handle.warning('无法在%s中搜索 ——> %s...' % ('migu', keyword)) + return search_results + '''音乐下载''' + def download(self, songinfos): + for songinfo in songinfos: + if songinfo['source'] == 'baiduFlac': + self.baiduFlac.download([songinfo]) + elif songinfo['source'] == 'kugou': + self.kugou.download([songinfo]) + elif songinfo['source'] == 'kuwo': + self.kuwo.download([songinfo]) + elif songinfo['source'] == 'netease': + self.netease.download([songinfo]) + elif songinfo['source'] == 'qianqian': + self.qianqian.download([songinfo]) + elif songinfo['source'] == 'qq': + self.qq.download([songinfo]) + elif songinfo['source'] == 'migu': + self.migu.download([songinfo]) + '''初始化所有支持的搜索/下载源''' + def initializeAllSources(self): + self.baiduFlac = baiduFlac(self.config, self.logger_handle) + self.kugou = kugou(self.config, self.logger_handle) + self.kuwo = kuwo(self.config, self.logger_handle) + self.netease = netease(self.config, self.logger_handle) + self.qianqian = qianqian(self.config, self.logger_handle) + self.qq = qq(self.config, self.logger_handle) + self.migu = migu(self.config, self.logger_handle) + '''处理用户输入''' + def dealInput(self, tip=''): + user_input = input(tip) + if user_input.lower() == 'q': + self.logger_handle.info('ByeBye...') + sys.exit() + elif user_input.lower() == 'r': + self.initializeAllSources() + self.run() + else: + return user_input + + +'''run''' +if __name__ == '__main__': + dl_client = musicdl('config.json') + dl_client.run() \ No newline at end of file diff --git a/Screenshot/pikachu.jpg b/pikachu.jpg similarity index 100% rename from Screenshot/pikachu.jpg rename to pikachu.jpg diff --git a/Log/LOG.md b/record/README.md similarity index 88% rename from Log/LOG.md rename to record/README.md index df6bab9..16b0fa9 100644 --- a/Log/LOG.md +++ b/record/README.md @@ -1,4 +1,5 @@ -# Log +# Develop Log + #### 2018-06-27 ``` v1.0: support four platforms. @@ -46,4 +47,8 @@ update to v2.0.6: support migu. #### 2020-01-07 ``` update to v2.0.7: fix the bugs in migu. +``` +#### 2020-04-01 +``` +update to v2.1.0: optimize, more humane, add docs, etc. ``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ec5e40d..bf6df58 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ pycryptodome requests -click \ No newline at end of file +click +prettytable \ No newline at end of file diff --git a/setup.py b/setup.py index d1ab61a..7072195 100644 --- a/setup.py +++ b/setup.py @@ -8,17 +8,25 @@ GitHub: https://github.com/CharlesPikachu ''' -import MusicDownloader +import musicdl from setuptools import setup, find_packages +'''readme''' +with open('README.md', 'r', encoding='utf-8') as f: + long_description = f.read() + + +'''setup''' setup( - name='MusicDownloader', - version=MusicDownloader.__version__, - description='MusicDownloader which supports qq, wangyiyun, xiami, kuwo, kugou and qianqian.', + name='musicdl', + version=musicdl.__version__, + description='Music Downloader.', + long_description=long_description, + long_description_content_type='text/markdown', classifiers=[ 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python', + 'Programming Language :: Python :: 3', 'Intended Audience :: Developers', 'Operating System :: OS Independent'], author='Charles', @@ -26,7 +34,7 @@ author_email='charlesjzc@qq.com', license='MIT', include_package_data=True, - install_requires=['requests', 'click', 'pycryptodome'], + install_requires=['requests >= 2.22.0', 'pycryptodome >= 3.8.1', 'click >= 7.0', 'PyExecJS >= 0.7.2'], zip_safe=True, packages=find_packages() ) \ No newline at end of file