From 6fa26727534d91a4bda540a8ff3b3273a3d74b10 Mon Sep 17 00:00:00 2001 From: Home Assistant Version Control Date: Mon, 24 Aug 2026 22:07:33 +0000 Subject: [PATCH] 9 files --- .../my_music_library/__init__.py | 3 +- .../__pycache__/__init__.cpython-314.pyc | Bin 16488 -> 0 bytes .../__pycache__/api.cpython-314.pyc | Bin 75409 -> 0 bytes .../__pycache__/config_flow.cpython-314.pyc | Bin 8754 -> 0 bytes .../__pycache__/const.cpython-314.pyc | Bin 857 -> 0 bytes custom_components/my_music_library/api.py | 140 ++++- .../my_music_library/manifest.json | 2 +- .../www/my-music-library-card.js | 534 ++++++++++++++++-- zigbee2mqtt/state.json | 50 +- 9 files changed, 643 insertions(+), 86 deletions(-) delete mode 100644 custom_components/my_music_library/__pycache__/__init__.cpython-314.pyc delete mode 100644 custom_components/my_music_library/__pycache__/api.cpython-314.pyc delete mode 100644 custom_components/my_music_library/__pycache__/config_flow.cpython-314.pyc delete mode 100644 custom_components/my_music_library/__pycache__/const.cpython-314.pyc diff --git a/custom_components/my_music_library/__init__.py b/custom_components/my_music_library/__init__.py index 927efddf..d1241c18 100644 --- a/custom_components/my_music_library/__init__.py +++ b/custom_components/my_music_library/__init__.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.loader import async_get_integration -from .api import ImageProxyView, MAQueueView, MusicAssistantBrowseView, MusicAssistantLibraryView, MusicAssistantProvidersView, MusicAssistantRecommendationsView, MusicAssistantSearchView, MusicAssistantSubitemsView, PlayerGroupView, PlayerQueueJumpView, PlayerQueueView +from .api import ImageProxyView, MAThumbnailView, MAQueueView, MusicAssistantBrowseView, MusicAssistantLibraryView, MusicAssistantProvidersView, MusicAssistantRecommendationsView, MusicAssistantSearchView, MusicAssistantSubitemsView, PlayerGroupView, PlayerQueueJumpView, PlayerQueueView from .const import CARD_JS_FILENAME, CARD_URL, CONF_DEBUG_MODE, CONF_EXCLUDED_PLAYERS, CONF_MA_URL, DOMAIN, ICON_URL, MUSIC_ASSISTANT_DOMAIN, WS_CONFIG_COMMAND _LOGGER = logging.getLogger(__name__) @@ -110,6 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.http.register_view(MusicAssistantRecommendationsView) hass.http.register_view(MusicAssistantProvidersView) hass.http.register_view(ImageProxyView) + hass.http.register_view(MAThumbnailView) # Register WebSocket command so the card can fetch its config _register_websocket_commands(hass) diff --git a/custom_components/my_music_library/__pycache__/__init__.cpython-314.pyc b/custom_components/my_music_library/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 69459e0371e14495e1a1bcfe589bd5c9fc05e0b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16488 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 U4PTXE^!V!@Q0BnBO8lk&AIGnJw*UYD diff --git a/custom_components/my_music_library/__pycache__/api.cpython-314.pyc b/custom_components/my_music_library/__pycache__/api.cpython-314.pyc deleted file mode 100644 index 416b1003ea15f26370e255a9320039d816ee604e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75409 zcmdSCd30OXc`ten=8*shfP*-JqezK^25O#6ilj(U19e0})?m{V2}!gmk_JFqqJ10I z>1wI8x278P3ToUMUQKKICU=E-$*b9wyjHQ~L{8FoL4q-0gsqgu>-P1o`v(=N*GZH0 z-ur#~3^=4fN^al%;~k0n?6a|_v(Mh&{^q?t%WmOtH3m<;STe_PKc@%zvWgM@p`his zUT&P*z=gRAPT<1)^PIqUXxg=5Z3&0Crb8FjvAaI3XLm!`!0yJd5qE8esofklv%Ix4mJFP`?% z(;y$N7oHKC>Up7A*pIM9XszcPwBZKUb{lK^0CKmpvKxd;LI<7>N@X_+op?ILe%mA* zM*IkiZx)Us-o@gL!ZE}J7T+QqN4%TGw|co!PB?*Q|8tH;%r&~g+gR#Jq&}-k-Of@^ zA@#H>bq7m5gVeLC)SWE#IixaAopzrV%-{H~F zK%ei>z`0;=@QSZ`BshHeiZ47Yr8k8_1EFy5P`IYPhEG|0hlYm3z2Sl3q0kXLTd2z8 zNEi*_N!v7Zg>J6~&f(^2AHEb&iu=(d675S8k&xRPx-!()b0HA!=^Gpf4244hv^YFe zqfJ@&A3f66bGWJJgm5TjXgPYgspCj0=kST+9s7Hljvwzh-raPhyGKm#qlG6eK2C=? zJ}m#xhsZeR=bqPW{~eC&M_M6wjK9bWyr2=Zf~J86M)%JeWJ_s>&%GF=k|{$l5FQN< zy~m+@1`N`r-f)PjE=z4}=o=n7KX9R;Z!{DhzSPq95e+-IDN_dK9rGPFpyNQgZ>#IGECC5q5Ra1-6v zKirLwUMaAaUgoU)%1Aj&ufeXf^crH!pu|jaY_qOW%84r2B@T?AD_>e4gVmIw%O>Vi zhmX}^r~df4t}JyfF^+nja%xw$982FzQBH=ON}s;;*!y|p63g4Z5N3JPZRE%~(|2_n zPVNLhs%hXxHEruT?qye3UOJx~J6oW{isaZyK2uJ)H}V^t({$ExoS7@;Ml>UsXnxHO ze5HOL>!VVaAN!cV%kXDJ>(>SxEQivrUn|$>*9aPdh<;4hC11Q)?wQ;NW%L@TE_{tr zIY;{5C`B;}x)b_QEgJ_H$GwVK5p4DA(r@n9%RRt5Vd~O5jcP~qBf4e%D3fbHTcO0L z!|Xa+Eyps`=!;=gQ%_$6^Jm1M>VrY9X2gIVnB{uYv!@(2l;CfK z9}r`#w^AIX2DlpS*fT<)Z!{PhcqK4+#fJs`N+1~a9d7bn8txAa`Tzz3ec=#6z=Ow+ z9;pijf&;yS1EJn?g8^Uv0Dh`JlF|=c>b(%C@un;Tp@AWSk9~oZ?%eS3U`jhM6i(^S z4-WT+Q`%5Cn9>D?MlYpwtrY5C=^Y#mq^$kD;oiQ%-VlIY$~F+{QJzz}@RgAOz|KHm zus@X24fS3Mr0hN6;hq;m!$Uox-t&Q!5x}xHOf^$?(3f!F60)L?cnFV<3#i;wM;jbV6cI9{7v8p|ZoV{~7dy|gr3Cn`fHCcW=eB;WsD+y!ioUs(CZ(g~2 zC1%Q7FlHx=C3D7-s5W7&m@`&<{q&6XyWNSp=DE7&h33PF=I*)X?%0WE=bKN(ZKr3O zPt6>A(|^@}{kgAho7()o;hTnd`L?)s`<#6{ew*N9w$n*#cBFLPTCk9vA1RN#I;D$e z*G=q4lVAG6LeaXZ&GDj!Nq%zeHA~XroG?j!OBf61jD^V!o8LEo)BJ7gykqw_t?%ik z_(}fyo~SGCE}wIjf73eW*c~(NW<}o8y%qTC@vqm!avKxIEpx^#pRB8kJL=ynj~=^j zkMw@*^u1R;>!^>J>KCk86QSSVK7RC9zwmPQGyJa}W^v9vzxqcOFlBaF$Ey=$i*=i3I^QxF7+IAi=*HK8U5viTjoKJ!#Kj z8xNpTdgICCvmS5=BHfF0i}tanAi>xr8;>3dY~yJLc%Y4^OWAVpi*x~yNvXSd83bKm zyKM=;+z9ViQl+hiFJGx91?Y`v&Y}kK@@vvt+GC(@D+3{#jQ!GO=obSyB><)wODG^O z^P3#@E$NlAy*}0}kTU?l8hJFt_wN3DW1)b)$NSR2(Uy_%4U8GyS^5_BoRe$Y0RXF0 zy@hU-vBliMTj(;jqZ2gpTOYo02KgQ77QR4foI=HZ}?)U)*_;Ffan(kzVidYP}ny#JX9CBG!nkTvH%et z9qO;Qq|8c>Qe2R5h?I`1NNGodgDKM`KwW~G5J#j(X+O9ZdDwdW86xA{qK&h=-fX|xK6xf? zD^1we#%yb&!g%XFqb=$1OxW(*ob1<=aa+-NE5WbIw>$}B@tmGa+(Uo%Il;?B~9 zb63o{D|+JHQ}3Lb8jV+O5ff&zuj&`f?#b4-HYdy_bLNtFTTyC4uhi~vzj^uU<%lKj zC{H+QVvd?CXL9=K~|>tbJhNY*}frBLf#5UK^A4i6}gSiZv2c zG~&}^ZiE;bF9JpeBA)~p+tFBva4Evb?fhx}0SlwR$?0h-EIy}uiLC4*ycoUIKfH{7 z6L37wmc78&1MJ~Xi$7gG*RTv-CKc_5zYnLVe0W;OM1L%GetW1BVKw484QrMuAe9k?RVfRiPj@8IXv< z-d8c#nh_7~r=W#`R{F^h?Cl>I4q^3IP+A%$?50Or&1{cG>1Pm6=@=rXeJOB-L5tuX zB!+0KWpF_R3;8_~EF23UMz7Q(&+miC$tLYzAwt+$)*D|)I`blBk%6h~sb{ByGrR6; zVvf$3sdK@a7s-uVOUGLXDpb6sOBjphj72|ncK~I|D}38@(-d`o#h%DpAIqZ!oVR{b zx8N>{^nbM_;jWo?*GyNuU;WMMnO%uZ2j({&SSYNY3dai@CtL637G4`o)^3cPiRaZk zG;{g2544=;AP-RG^h{WjHqYd_NKI6j%A4La{o1YOyZRr8V#iPZ=t|6bDyBQd7E{V0 z(#$}FMJB_)9W#<)fbJqH3(}Nj0AS1lo`mmp-w}}MPehQOLn(za&*z;FI0TVBi*%bh z&7K~I0Yrr`81RU}fU|fMFRa!JSaCd1CZJBsuP*x9%dYgiMp;ZNpg{B@>`KQ#4Pv{M zDMw;s$ZQu6lQIUudhr+yzb zr76+d*Z@ZW3o>yD>bdmT9pwe>5nj+A;kOuouW%B~z*}W5i_{D}3Q(=sdKo1SnpHI( zgd#7y0AG}pZvEpBR4~e;Oen2FXX-#vn#SG~7{`cV7>u2!u)X#(o|cH*5dH!11o|*= zA;=JywIN?{bSOMvsuM4?ll^?S_oaX@GzvtGjRa#m zDg&h+7T*Bjd7!EX2T@1wP(Ls|s<|HYFxGL6Iprm`;Qpo~EgdaQ-K`Q2@OUaqWY6_T z&8F-kb5VK>R^r7{#-2k*4;*L}Qu_YDxzP(L6MOHTf&P?19QKs))!yLHz|aK%b8MMh z@L4LRXW3E)aR5_1hVAa6Z2t;j{rkY!AE+|Tr|PIX}W5fEWNSr+PXPY;U})f zMLnl?E^?^jk%iOSe(&gSA6?XF%!m0$2F_fZG`SO|qL`^@@|8zcgWm8Ui?ca@^^1ID z57El}Agg%)7TpIM3lRRhfxP%x6d3=n7O+{K7RFyeXA~H3BN&e$Vqc`&)H?R`IE*J) z&0zfX$6-9a>AxAq?;;ovLR29m(T;R=DcmhN1u$E|dkE66m41+5Be-n9)iObP1ut9% z&wD;EJU^?zSQ*pAJ_Xd}^Sr}Zg+DCUk>OoxpYB~GvQkS-{ISDo7~Dj3B=~wnSpr@L zuV6V7>=ogI43;Fw%E0OHXwWzO>X4M9K1jPxif=elV+sn029HzFO^G@p0)vFt0qeue z8yE=^r#wg>tVWjx8Nt((O7(Z9H`^6kIp$i5m%=B~Lhc5N(L{^c*sJF6ZV6(C?+1_Zvn^<&r034Jol{pO2T zUwm`u>d;$f;#oBl+I!ZltDBSVoEtl@?Tqw&dEcC^48&RXt_LQpVi8lbNpg*z;i17R zZA+Fg%?hjz{^17p0xODJbb?;c2?oI^m;|$65v&acd^sEAXr-5Q%5bT-hsK4d>6HHE z(LnG@NCnv&`V+yGsu z_nQO*(NTgO>+x?;gW1p9IKSD(ZPJWb3OK(Z6ErzFi|+xxH>&N?$XMF} zUNF6gUoZ3H+CzE^H)8c$yJ)5_yPm(h9Xn)y7h@G!yX?z;#v^@i`RxwiX-TcNG(_mi zgqKlWPUu z_k>nDZ;239FqLyg?$Dp$?`8Zo;m_&g)^n_1m@ndp8X?qEU^wPWAvb_t7Gc)Vzs~>0 zal5od+yL+6&Jr(*T?VedoN|uXRqsNpV%a6wPW_j3zl+fG5&J%SE8mF`7UKhukcjnf z(GKu6Sz~qkfzJR&0w)z(k9Pxr63NNL#r*?8h(sVP877cQB43gP+eO^A8`$gcX!t!I zEH8+L2ZICXOYlVmkYxn1dTG7ZXi|oe-e50?Y9pb$C_v(48{lymh*b{>J;q7^pw&$i zdRiF*#@N>v2mt+K2yBK7B7YB;C`DZ|+f636M*HJg>&Fj#l2s5bo6o8o zKaec0jE+uS_;zS!%eTg2Ma|=F_l)jjDYm1D-O)p-Z}QCruGg#1P9Wnb0n3 zOy+}pGPn4~Yu8?jZk~B{^0h?n0sNoOJ#d$pd^dJo+Z8RJcULA$E8oq3Cx1p4&5xJv zNfhjvY+3LW+&FaYP^`51*8X`<`$9qEba%X9=VxXvubmg`0H*26laXERe?9ZjnR(}_ z&#Y30+1vxzrCeJc@j<~Jk*Na;E;WwdM7@;?sG_4-~d-{@=3eEQ_`2G7V->cM8yn1bm(fEObZ{aN; zI4daLXh-^Oo^R2bZfkTDH)v7zw%LbYZ`W8UrIByZ8*Xo5zwR*5ulsnU-$5y(@s5_o z^(-_RT1u^V?0k#Uc_+(4@q7)11yuG<3H9nuDfQ$|g|*eCztfP@YSVwHHzNH*n;t12 zx~TOfo3aHj1mMj-1TKY-zUro|Y{`tBW?i-+i<6ahW45X@ZNkBq=+`H)S-*}xJ8 z=37CVF9Npt;(p(nwW!Bk+I%Sn3MqHdN--O4xpu_FEf?>_F4{1O3iu*zxmn7Vs}_;V zn*=c(&I4(H%7ns<$;5&IGez2l0q%Kh8B{;J{5r5&(gJ&xw4)!35%s2X5}KprJZw|_ zqEr8Zt>17SpXZ5v(yLgiI=O!tw<_t~#J&U7;sqm636o#jpp~B*G)fwaHSi5wy?(^# zC-$-r$b*Rx2B=H$oAHa^)XeKRY}&kfLme@nRknG*S?-Za(u0x1rsu`hrJ(!Rx`6%^ zVrwhnI>Am&tAr?4=*NqW< zZjYTf72k7uyd!CJ&KdKR^84>}->tZPI??*4w@$r+ zAd$0m;=qEtcIsr@y=Bg}1)Gq&^nr&qAs3gs?U$cA@@T*Drfi-CifYVx%FeTAW^~?m z@E_c{zh2ZM{Wl>ZDc@)=ZPD`|RM_^LG#_l>_Zti!Y|NtgJ|og^8+gQTo2)Gw{cX3F zQgTcbFX1Wl?DA12=x{qlvWMlyX4&f1>`HOI`udsw_Z^-qdftNN03l&0D-_1kMt@M zO%ZXuq$nPZ7$D{5KYxqpH&D3|qpEgANVsTU_7eGxkZO|QELiip zOaz@@sT1Ub#P~kh?AKPIlFmPqm_2FMwH~Z%&Di$CfgnWX;a6qg}B){)rRrEmCmD2Y1gU#AIG7=c- zuQ3O&Qcl86LGx2D`hy*mvX+94l&lK{0z;yNtO3s*)`;3 z(+}^B>2tr>H{SMB$1Vt`ou0{)H_lu;6LrVEl?m_qm=|KNxU+Ub|4Hq}xMS0MtGa$%T{A^o*R3RUw_USEx>3smGv}=XGLu^fEg>sFgmd>|HfPHoKm4m-I5_8KCik26 zG_N%*S+hEHoUK_OdxMn`IMNFWZbqgjuE&2wCWZH@hk5$&Fo2Bh?tJb zMazpV#Co<6KZgR)7IC{@p@f2Hsg`KrswEUIh^hp7Mks}#V?0=Ozoi#QOR80+R&Qq& zwHvep>AMZGc)teR6>TO^AoEWEMNMP15?2y}7FtL?tOBNY-7C}A(rTlHQcsE|11Te0 zG>}I)MJz$4kq7#NP^jU8m~MH${hRHvJxAk> zU1CbC+dsZPX?2hkVX`f5El60m#;jW-;kPf}ygX%$muwIdrklrGl3A{a6O*NrEt6X( z_9d*oS*tH@EsN>O(o;?TY)?Doncnt$I+0GH;TF|zo@!6}AIPrgdbIGo;Q>cs6sv&l zQ;p(tx^u|SF5UNavOjRP?j%J1T zF$h|j`}VZDBmLmfL1}CS<+LUBDchWiAF5lSC(8DhvHvP^>;m}iO!rxyR?-Kgo1hRY zkJnNUy<@+DHXpkBvDQGC(eh!J9~MTnYvd=8FEm-%C6A9%61mX+vizB6i)u9VE4(-O zzs9SPn%&Cm8($c?coG$>j6k=|69s}C{#L-4Rh!9E31$kZasHE@d_s6%)oWmpc z8#$~`tRr&{RULw+8@fB)8o}GG6>_?Df~i|CSo}uii!%tB{uXJ%r3;X2`6aJnC>dJ? zo8P3=Vxv(y>y`7T38cehF`DH!FUv_vdF)EFtXR(6vx)@8ZF2ZoVwv4w32F^B9hF;nho=xQkJbOi)v7VZ&UI+6#O{_U!~w{ z6nq^)iocZ7Vh~`90ac|CF~4QXRF=tzjIrFFA+jMH9Dp9DEJP<1Klm*aVd!Wp;0F+K zNr&4Xw-qLA+hVqDQQf%;_I=h?eU@=2@9pn2;=+j zySz8@uH{8)r!;Zb`tgIL{kiU~{)Dl7&RCxGmfU#$+Vjx^Q$pOkVZ!pMvkdGuXZ3i; zeW;l~d-d7Lp}4&=VP7A!gE)p9$m&XF=iI2iRvp<9&#p{lua9NphY907YxecA=-R2$ zzgYM6{zUHPx!lc3Z(-!vAD?FRw@o$2y$us>>CPDAt_>@8=49NvWx{eFVx#TXwnvV{ z-Rl$X_L#eUD*TPh?_Hj;#2faD3AbJzKa^aelwKWkR7YPPZ@(|9w8lys<9S;WdAnkH zyJoUuI`6`oqOVkrcPx}w{rQ%dE@#1BJi#wmy_3CAleZSnS&Ng|?i-cYDr0#yQ=8)1 z^%MK=J6$*Q*YvTRt#RkJ&p53scaegm+jC>fwJovy+PJ&!Gp(F_Urug_yElBMmy^MN z^yDs@p}hYY#8B>}GoPeck-m9n**ymy%M#A2S!Y!={4q4)wNRY>)SesRp@wfSnX{K9 zZN&*&*{rQBy76OM<)_)+>lY%uvC>V`&GDRViJU#LoINw>?B0p}3+^1Kg+=&SLH)EQ z?%eztr=k8LSa7e2Y>XA}y`_n}_kX69lkdB89vXF?tceyVqCM8JFNIZIOQ36;xvY}K zMoyob&=t?>ilNM}D@*Efq^B$Mx|*cUp3r$`b>7Ky^Sb=>)4;sWci-fg^d?PfBKwo3 z!pMuz6`MSjH04ALNt5UOHP`zh>%Kgk$XP#|vwl9O?p7{-`o`I}w2?#ec{MXHCr#P! zUcTNMslI-6`WT8{_eVP7-YV4e*7;cFws`UOMX%LnSmdmF1EbnRfaoTRVtKjAirrR9 zlpdJ!b}D;GHc{={!g8@VV{t9bBq@utOk!Nr0QahEQWVEic1l(rg9iw5ENN6!98a2oEC3_?L)tEhJYB)& znUrv3d7pula0HuRZ!lt8%UUk?V^DFa+%IK1-}}mN5a5ex;G_)0=g-5wF!(MS4Zc7D zAxr;irEWj1PR?#%XQ7iQMqJ`%p5%fQGQl84?mOL!dWuM*K^8S}VD6h))WX@Fi&hr3 zad~Bnb`}-2bSNs4^27p{FOpw;k$B5xkVuJYI%Z9%{=d54Ee%PC=`mIH zMIJ7o(7XyMRY
H2J4v|g88G6F5n>qecJ0my!q!22~ktO8FsB=c+3HR3pnQ4_DT zC34KK5O{JuFvtdO#nkJaAS@N@zy3)!Khj9GQ!X*$@;lL1w$v7Y;YfD3-k+U`pah#j zBHJD(>FT*1=+=dDYG=jr3evt}dB97UO0F&cVz8b)!1=hdKKUC!QVuvK%Fspf@GSZxUihto zej9PCdY5TsW_9!3?(~|2au`42-UnAVr0*ccm&wsxV|8tTFqFZh{glaoLd<-CY4R^4 zgdkDh`Z*=)QpHRbJyt@Z_j)E=C!73H5KVA8;!|YsV`V2`bT8`jio$$R+P)K(q42;_ zUm2>d86#=D++9f+&&)g-_gxailXxEX4!1CXq7?pnh>a<(P%6o8pnjA!BQU(9`yMKgw#!BRj=h;}2A_0o1r2E?7U!pot zY7$i@?y3YZYqB#6{{IF)iA0f~Vx<(kLBStVKv01R<5Jqez)(sD(L;#Q9HNODHb73) z4e*G%0c^W}ky39_Kw}~Hpo?NmOF(meNEAOf#L-AA3Ft=1mhE?ulia6}6{bpzXHf_s zNdL|S@a6%L0(%YlLmmXA%BBeRB=Sk5p4KpBY3WP$kXBn-x8Uzkl3@>Fz)ezZdjz>! zX5E6WdDF4SbPJM3+Z+EuQ6JbbZ`z40_s#b4<^`uO;jEZ-Rzy!vXMaqJZwq$M_p6eCVuQ} z;#gqzSYY0Co|-AE7(aT?nDs8+I(9A;D-VBc8wF+H zeU`tkb0=Y>`z$;j=%+5v+=l%!s+#f@uQER#ZX3C3_0fPPYQ}| z?v5I6?oN2uPP8plZ<^lv-Nr=qGjr9?TyKfwTt9T{JZw+o_u4)4;*ZPk27a_|-hVoF zrZ;is{OpD@w(1L-LY8R zvAevqSKPu2sdWnhR)jWkYxd)B}J0D0R>Z@MY>H_u&2ibXi`7) z5C5mskBn?|GHt|art7KAqeXECpRqnNk5uYY`JoV5kVdg3U;v8XE|X@3j6pF!2k>Sg z=m`Jl7qK}7A5ROhpf{-CZ|c+<{n+*+GBe26>!WQ9yBl$XWd8@?Dni+M_|he)NH7~g zY*>R}(bWeTeBOq3856KZ4;e7oEeE5N^bG{qIV5}Kl%7$NaQm{Geq=jA6~zcU6uDx= zd`ji0SW^&Lc0I8~uoYqxE@sQ1@!LUUyHP z{&GpeRW<9Xns-&tG>;#=Z*;}HZS%&q1qZk!aYt>;RJ&j;1t7Mrjp^2=XBlQByQEq6 z#M!QP93bw0G#428I_6?jHxc2_JuMOYCank`YM>SIJgCkGEM!D1J>J*nJkV1l-DB2W z1hm0pGuA(R1of6xs8P(jr=lKLrbb~@tD*#9c=lBFsi-LlL(~dB&|}YFg{q8HPFASu zhB#J5Ws=$syV4G&2yvn-ZM@d4hk#jHsh}y9RVC)55cH_xx=clnYM)An)}UIE$_QmL zLzbb1hz~bF)#Le14g6*egoWC+X}q(cK&897f;%%UsKruHiw2?=VccWdCcW8XnzTk0 z{d*vN1@egQs{8+=6U>Wy50V4!9}~=*>bs;ex>CR zZ+g5gbh5tMF&>yTk99-J;V*_K$xEaCE16LXC?Q19`~2y(1ISV9Vk{5X*w3I+cH z8H0ByU_{YhQtVv{rYIn2!6f!@(K#dw0=v_!J2*-N!Ar_@RV5RH!`@{+9*Geb>t1!2%iu9gReQ>IsY2f{sI!=-tI!4Ihg z07I3LtEg2=G#z6ornHxOFQ@b&SObJpHp%Q)G%@-j<++TYrbwivx+sCM48aZTgH@5J z-SPuU{#^=wh#+N@Kjn|;39goj&<)aBOxdMRB59d8~D=e@mH(bPe^u zM;AUhixS&WG$b$#h^|vLBbS1};Wz9v)%pDWXvnA;-%VKx9P_b{F}XWsIM>?0YZcYyJzWVg%)&PU36A4?$+W zrex=|)MjcaU-R(1=?F0#?Jk%=ICo}XWxjPW8hm^3t-+)>7i>Y9|99Z7XWn*%_?lME zwF_LzMv!F?Lb@W8mIkm2D`3#Ze8>B{>awrgLe(Et&+R2?Yr)o zliw+vx3wiJ*G?Q$*^>_Ndn-4^wj7Rs`o-HD=R zSX|%IL^Ssbihj1f?x9w@tN4@kbkq^(gkqyt5@*LgqvR<6-lhjs58iVZ^xG*gaAg&XMy{wlR=#z%aN8pj zSGaSgDqheu*>bO-_Flod>BjfBe{*|mcUNNbvANC1k~=$MI}S!#Vx?PP`F5|MdfM}T z-Z%4R)+aW$&24N;ZrnTVh_pltW((_*p5lb3a@JEB?N4~tKPchWlrL6r&jf-9CBrrzJHJZL+NL-L>zmjjh`oFWZ+WYmJq)-r6)*)*5NPS5kU27(Ex; z(DrdvTXJh_Vr$3j){eVncYEVoyJE+?f3meZ>VTSyZ}USvSKh{hJ?>lgnTg9=3-zAp z7iNwob|0PHeRRIOD`H9(tee{YlY++YYG)39$1z`UAh}_C41Mi596NSuzT?!?#cvF~ zH#E~7uW!D!@#Ff|SkHyX(fiId31{gnKBuQ7>U!7vjyHNGUb-<++8!%ypFZ~fiEo~m z+4`;L#OGV*lAbk@CidB@zH&HG*c2;lnmYE46Yrgv-uAWU#pjvevP=!J!iFg!iNX3p z(z7;Jvoq-_h@4)u8_TmlfHaFpbFXHlLriSyjy+3v5G805tBc_?!X=_B-!Oz=TRMv>(BjAmu^5fh2@2$(C za8HvLx7+Sg!A=(K0$H>-JMi#*eL00Sc?f@?VPQd2J|6CFu^{}2(^06a6HJzmwz?4h z&}N`8+l=%d7WydMlY{U_9y^6)EWTqc;y*4mQdm=r@b64k3iGJ8zbjpj@c*`LP<$)3KkK0Q>^cvHdyVM!oSsK~&ZMW%zV7%I{oFPkrSCE!9@p^{>M12|W+~Y$ z#hr6}gFaqEHN-dQDbE%y;!Bw727qXw#Qa0rGRId}w}H9=)N2gO)N4{%^8O`h{aDgm z$EP!|TqK?&kxs;OT*DVXpn!24alh{=Uex1`6>o4pFd`;$9u*E7%F#<;>()Q4#jA*% zNAR|gC|;S|7M5moVgn8rFVkM&aA}s+in2GXHi5YlWig@~fg@H}Zi0u`2AY`kdNQ^KSFJMMj+}proY2_G zm~%B_nvBkNK-{tmrVyEHRQj!=?PMw~a|wk5s49W)$T(*P;^zq_qHrZzeE=h-C`3zX zA-G_@uA<&wLmpiw&TCp>8k{_Rmm=GcUJAFg3O5w>-Ylir!e`%ip8CA{A zDkne6ILqIqggfa~h&8lJeg>d9(J0Z0T1F>&o8<{@TDnzEhPrV$1gJK+PTB`` zZ}Gm2VzZ9z6YXYYS-nqO#G({u8Fojm4b#d9V#fMl0-Zd<6ud=RxXO_F%(1Si`*EScCVe zv7b>uxTYv2x=FGBNC8tHI6*P+VmOgBVA#FnMc^|^{ci|Td@yATT!ykA1X&=TLU5Bv zuB?OqL^){A!$|@uTYz=|afSIMI}cDWV-YJZha*MmIIwqequWEyz~R0|Ee8t)|8Ep$ zFz)~WABp=HEna=`LS9KCuX--8dcj7nX>E-&=N@S_`4*_ax-ApN&kWcelkU=(t@LNR zI^ww({(A3p?|(h<(TUjcK;rQE`Mix0{%!qDeXMxH$9Wt7dhdiSmU|&tSUjQsX-*Mb zF6I@#ZMkWQZkuZUIIm$+m&`AEJMU&*tZZvMe_JAdPb?oQxQV=$*}Rr`UfZNTnX~4` z#cLO1B^%>8n-V#lv7FB7{`W6_^Wx0tx1i%n&v&yYwMlngb{2IJw22!Kb22!Hrw*gzj#+JvMI>l8&OE zI4WbN%3qY@m4EdMXhB}&fB6VralF6?FhX#!V83dQ<&dKt6a01;{lrv6{s;8MzX@H( zpuWMkdHCO{XszI;_|`(*dsf8$cOJoC(GTzI5c^JEYl-fAd0q*CAYyjSQfw4i;zgz&Cz(hE_P)KE90iF516}FzL>=CJK z<`PDk6{jk(Wz`aj@Jdxe-X=4*v5LhfK)c$H=5y5Y3h$Olw6%J>tEew7ZMVOawVT;@ zCzP}0tuNEItDqIy75c3RZ&l;9`UtNk7Rx-stBA$$raG{EMRkdN>^*f|q$0lT5}eQT z@_Vv#i2Oj>E{z;w%{}a1m-($$ zv7^LWL5*7O!K(Jgiug!1{t}V~l+>I?$sV&eHZK)1tYVeQR9XDX@@Irh29+|*N;bKQ zu<7w0!;;q`m|@eG1)IK{ZeEoGC?SIp3aa1F-cry*WhPh1>oyAc-6p~7x5%T`k68dz zFjx5^$_f+KKp`6j39#XlVl_o$os!xN+&M8H|Dv{v?5l(+&{Q`wi@P?)MfB0xc4T_j zLHaDB(x+r`w_KA(WcMzGgD~voao5G41T7_$Ak+9 zV)QZ;DrFX3FAfKh+{>1_nCc7f4z9;NTGc7-4o6 z{5gJ3!AmksUqY`AgfEIH-Cin$$wS_u2#|;xMn*Isnjz9l!CiI;2q+Gr()Z9=D6HR1 z+bI23qCY4Fqmz71#HmIK*iQQrw%h+t6oNp{njMoBSF;kPmYAs}vNKlGGF#9>mQ4+0 z@YHbM?2hHu&zkEO;jlUj8iHm!b7h?qH|2fC@#gYH3L-6Ux8H1!K98de(up&=zcu}} z=~ixhdq+A^c5`tcZY}&wE0<6D%BX$|*iJT=eja$|vEXQM(0?VmlATHy##KV#kh;?}v7y$v&z5o!683g>UEI%#UuFDvjsYUd@^?CaundwP4m- z5a~!*tLCg#u+E$8h#Zf4zZ#m_@#nu4{X(pCTgF4q^g*3u9%i2i zG4rrQaaXLk>u>Yld-kqTG8N07Y*Wm@zI>D=E;>wP^kvc;z=7SmZCP3xb;E3C*VCeq zOZ39nsvr)F$dJjS7zrU)J7tT`$x;wY3-5ZsA&4|C(k0Wkzv3xe>WzYV2_(d?{~0O;cjYL({DPL0vVlXsPf$g= zGFb=FxYCu8x1ykUgq#T$*0R!H!3dW*KmY+T<$(!SHDBpl|ItXhp8| z4fVb?t2Rud_TlL~udl7Gxk2_#0 z3R)1yG0i&G#~pPEN8_xcar$`Nu>-zat&SLubg|Za;?DiVs?tP6pV(zWh>_+Wk_)d@ z$SlQ%kPbG2A1~qusdOl5-w=%fNtP?xUlYz3{D6Wdt)HW`el{Q{aW(gmA|(RG)x;gZ zeNm4)fCjnfrkII*gPIW&R~7oNTtu#Yk@%aNK*Fq|S*aYG!gK+Y8Zi6db56FD83A_? zJVfOfqy8F(z@&38^$)6cCF5c1MxSXVfDkimR`d~}yO6F=yF^>_Y3tK3tuJkrbpg-| z2p2{|yfW!I3knG*YAg8l&_dI+ZIliNy{yuoMm>N9%tz>E&0(#O+rVvxYb*u5Gt%!s z{fvH|v?o`|VOpA_UQ1{((|%i&+Wnenf9JPAqY{Rm8T^sjREI;^R(3Ta+21Mr%d``5 zXzksPSNL6lpDX;X5Zx>MuDq9OOdTt0ruGBYV_v1l)X|ln?yVe?A-+uTgDl8u zl-?g4<3AQT#aLlZKRL1gzc6Yp)u<`N97c_BU3R4{5=XLC`k{)>-?F*rfPSE}3MCJ} z3th=fBdfHcl)NU+a^?9dd(Qk_NPiKqi>?v1Uga=b2_8Ssb~20&t(#0ba)LrEE+zdi zH~A5dAlJa)&Pp|`xC@txJyVB*Lh9jbxS=8GRo)ZY*XX4OM7787Mdf8>BRP z(qcS~G@gEECdii7y~OwZ?~)|IDcYNg+&XtRBnczAn+p(sTl< zr;i!NZ94XrKtAM9N4MY`P8+QQ3|c8U?<` z!NieV1B;f0qJ`qlBmWGhBzI(NPcx%~Au@QC{jw9N&?WdC4zj~8=?*W0bQArsh_;YH z@=O?_)1S&w38Ms4{Jw5DBMdR@CCJD^EK^BFp`c7-E~zhREhlk{cZ3Wl#W3A4)qVf zeg^K9e}GhI8VxaWK4liioctV{aE3A+0*Z6~NMj0qA>jYU%A%rmBU4fmg)?nbbvp%2 zRmp_0W%yi*9}?LB28l|x;FJS8&;W$JDDI$leay5`q?JX{hqp)S(o);TO1w5hg?<2- z!fYFD%XEN%5iV8xxnI3*{2afNf$xx5@wR!} z_5~1Kp2^EmPuyHJ-h9t!27#3@<K8eOlriAJ2 zsrE&quHG_Xf@lA=4e)F-(Xps0vgdqKQax1@E7?E&+HA@GME-uL`PAYttnY>swJmeC zEpO=}{ zDIar~6VI!js`y6rd)3ohg{_--ixN)w|X2R8Q@gJ{K?B z5i#B?C`;~cx^?`!hoZ-oGg_y6Kd#(PoJJXbc?^W_E!Gv+RoUyi?QB;*uY@y zQaCm^8tZu_(i92YY)>oh^nUEAevr$RRxReilSZ^Ex+RudGw-Z9LPft@R=7{<5g>G%0cOJOs^pdd2p7Wc~ zI2OYj&D^0}{sZk+3U@jY-nP0aEU|Xx>Ta*&J3Xe`>svPB;ck(U!peLK_chV&hxv5~ ze`Lr)i641*isxDnd5k|Q(@{#5iQ)}Bg&T4YIgCFxnvniuhaM?E_Gl5up>XV&&mQTJ zPZ#s{vLwkSInxr41$8R!V0xIr9XZL8?Cj(Oro*{B(b;qSL~}=X>*0+4T67o+;QF7> zPL07j#_J>Wbj-mHdlwzWkdK%r(pG;$v=!0jL|bL?1rI2g#4)$HMZUnER^d(DWd*T= zbEGpMdh_=~KE4?ah*w~u^J#p9vGuc8ICWdmeh{@n>Lq{NFK?DH5=mph25dtMOAru| zvSx>hBC{~81!pX%ad2QyIGg(9G3sSnOQ1$S`Ww8C#73wl4guuXwLV$6E@^~V@YMTzA`CuSq%y@CFmaP`4FVTr@q@bAZ@navUf|rNSjgC<< za$3}I`N~+sw(UEXRe%(+0+t!=2H76|4;Tf;`}pVo2NxJyKSu(TmDp4($9HB&&xBL_ zNJ@{r4M*V;5)d*V5Vc*!UCa*daFE>H(b-)C!ObYhCfhmWgP=Z}IF$S6jk!rj_GHJz zZ{eUoI&N&owH=WY(Wbb&>bDP#L)Ko%WbK>Za>Q+QHyx83zqawsV-v@2I>uXLwz_)- zzKOtO^B)dG`*Bu{HG5)TOtE4S z5^v-0z}v9kv=tk88}M9}fwz&R;yGlU&6c|))0#^EF#|w{6Lr9@G^YU&5RP9}ZG4D47jm0Az zJLg4sF4BL8UX$RolWB7)ykyXNAb>#Yxm52$YWK@7!F=kiZhP8uQMW?ZclnlO#Up+t zV1`Sw)p6}}b=GpXsHAIzK*e#eZ%K{j5?IGIYncBI=XBH*Erigr{NA- zVhu6MX0#stJT9d$|1uISN4tx(i7`q~av_8GSe=|7hB@M?G@$*+p-!6IWJmFeu69HV zn7>3bV>NO?*^!O-!qr45fl~CPJ>rZKF{jGydn}7cs*(XuNQ95BOtve*S^#d53Zxw* z_$sPZQHvka3NmWzIfg`cwQVou%NU4-l5+<6m#Ls;8@qmdEL36 z=N3;`89_MZiQDTz4my|$89N58^X{2>+o?}%9&9#_oS5AgEsNPIlXk~sP2^;(s_m{R z<~kOGf*D-+gVdWil+5x@oQ`D2Y=v?rS7pprg#&Xupy_a6jzZHJ?fIXSlty>NN_J0M zXG?Y`@^`OF)8Vj;SW)}ks(FF=$`Q`Sp1&~f98Gq1#g3hf2`4|=6VE;M(Vknq^LY*5 zw$Jp&H$3xkUeiZ=W^+&BNI2-ofj%kV@=6}mgFL|*8*BEEbCYuj4mhZ}0! z%8cJHG~n0om+6u6{k2-emtZWkuVWYge6#_Z_ePZb9GJ*IsOf=iyypRjVCuO=y3M@C zo*t(Mz#9}XjjyAht6&=BuL5rYDFC>7&?`q=yr5#HmaC-wP$>9JM!BFuBZ#tzv`crt z<}BfO?0Rfh#D2JhJB=fbmgnhrQ65kx8TQ3Ssa{4!Xwy5foGwX)v`k*%!5kv*5Xv5` zdS?Uq-IQfXD(Nj7A-s_ydCA0?R$7BTy1d`_i_{Kq4(6AR)zjGm-e~65aYXFXv`&gm zf;lZC?!2^Ojlg(TuCZH@LIC&5+#~B#mba+zi}V~akl#|deHq69Zl&nB_hT&-l72hB zV81vzaZr!COwtLpetTNcD&s87IvF|nH5E>z&*X6tJ|#`?)@T;suM#fHt!j^yu;2&Q zMAYjOT5_f_n6%%Nv!&96XPO>cWb&mHiF0mwp^gj3n}+3?<9W>^*CX{<`Z^z01N zQ@2z`Uk{SAG)yQLYgC6qc6ZjYma_K2rzP`ah*-KEcF3)}Hy>NDd?Jr>q~GBth3pUc zszr-a-+IP1uUVPjZ?Q1$yv#5NK^HAN8XO9VE_=Hq1gt}{9(@i*0HS-sG@UOUkymdy z38l8az5sasU`YoD&W(a^jq|sl|4xVQ!gc^YcL(}?J>lUVI&PMkveH52IE^<1Wi;PF zC^QOIM0KG4LcQ;JIM@oG?+n8`7a%{0y*@gzyl;?%3g|^&AP7|j^k%p}FzA!&2-VbE zj)*=Dw_${rz1l#?cVu`d;QPn%>*V%cerNWozENmeNT(x;%yOEdq7EC=bbFQ_-$RhH zDz>yR2B6QGGAl*9$9Ae}-ieM%X14?VgwTp#g<`TWLn#SVbgbz3O9RY<{gUnxzDgM9 zScR%;(TJQKRa{H%9hG|67K~1@3jl-LOj|H<{>4qMn`^_;c-bW^IS1zPMf5y>nH$Gx z-?DR5nvQJxX@Yw&{f>zEuO_9b-w^y2YG2}E%e?zSUsF4UJwBPegWz$U4K$X`IFBGfA-@Qm_%jm9ontzz#PEve`sVA4NeGjcCfm zC?8mIv+)!WbO$OB8B`+s4WY=XDqL+?g9lI_!-}*3U>T8h;M|M4TD@l>%RAX0=>%Gx zRX?FkTC={iOR>#3K5y%WlFcInw|3*S``i7As$FwcyT1GCtAiGAI3`?@EO-#C5k z^!2l`GtW=xKg}=vikHq-c25`i<(?}{-?h3uO66cntEkA7_ZowsCXt;@yyJ* zxUVVUJ22}z5chRV+V8o2(dVWw%vbLD#8U`Qi*KEb9-A^vm&J>=B#L&$is1Y{PMCMn zogX%v?1~la zn)b(YcTegT3VmPAopMce&UoUbd*g-sCe8P9%c3vMSM5NZk^*F;VBT4&;Mu2RXD`HF z9(@E=KS=8)(6q^B@)J_!r%U1Sg2LiV-!i)MWZj9&FHdM)L$b00NAACT--lD16? zHpisl_dDNm&(d^`7dFN7o8ZvVY>#Y~OA zeB@$+35`9C_3xRvJnwA(2L+@5g_Cn``Q?JuLnbmFMa;E*rhMMC_wOBUW*_7EP3V8a zJbEL$!^8h^ReL4(9SvgtN0SZF4=h^_Y|z~1j1(F*NV#p!qj*^sg&SDPCQ7+uEunDz zdW0Y5Q2K{?O*|gHpT7eSKgf2r`*c58YoM@>e*QrNk7j0YnU_kzlYbU;lmsoiyg!3I-J3j)x5wP_P{5h3cKXke>=1}}RvJ;_EJLnUVU+B7Mn3i1`@O0#@ z(T@E-9RSnoYj2W-=p_3I1YvzAjS@lCfpExom~mE`WX`@%%!grw2%#JL=-8J~2kZj_ z7sNAJa4aB-1$z4~N|mh*VS^obc{G4RRI9{QQdLyLryNql!N3KG5Q0~Z)aX*)i^G=! zicF#YVqg&Gtb~G{1Tf9a69@sqlv$}AM|UzBvV(GHaAb^;1lg!EcpAgPPN`%rdS$US z-24s?zQS1kN^gS4P?&vk7<=H8|BHpw@4lbqd~@*XV5BjgkGzMOv+^QV<8?QC`s|8niyza)>pG&Ot-4)YaQi-N@u!%X2Fy(Cvc_5 za;lDySJlbpK?(b=;`@ss4qN{VkZKlr{6VVe#5#+(L)QklUD_gUfM1paUyxcY;!d%V zlyfm$)iSnQmW>iEAdxDyJ^&&HibRu|l^|gK5uj}hB=)cZp_C{@hx7~pgC(U*a#bm# zm^H*0zYx!hxkAIEL71`Fq(_#!MswsyjY;Gj6QmK(KfjN+25k}Q8xFz?)4)(b{1qs>20b0H6OctDcg&oaL8M_gPhp>nZrudhGCn?P!^^V4t!9Rviia^i> z@F)U7`DKDVD8rz_Ylsj6`6-MqRHvOM9r-a+{-;Uy<8g26ge7V9&RI(rH72|F6HjsE^|+@N z?r_{|67H%wcUAP|gu7dD}SIgcFPYXwO0klBd>9_s2_iB}zJDC7m;`Pqrru zN+Nq>o|;8H=P7+;0}~i1Z3q19X4L{v8?)w&YZf&Yvuk0~jv06*Pi*R#+teW)pIkWa zt-l+Fg|x#v=P3OYooGCHU+3mr4F5mY{_^J`LO}w8tJ&Ri2?)~+nLdj*f zD`|5=m!9QbH1cNGZ$hMC|3;I&RnLFVS>960eXmhZ;SRn9QsSKjh<}hnDIXNLk>`Vk z-7TBA+qE8qcX$@&Xepj=z^`|3)Osm*r_|cgpuJP8p_F=_;tg7gZ?d*(w0HLC5dV

EJqOu^C7Z|6#8?RFblN#$l4r5WWtOfs#~Pn6nMF~J)Z54A!2N<9YGSZ)Cv-*kV>tMmXINA$p)u}v77rt1^5I84&I z#Nd>bW!^{tXO~}ss>gz@+aTCg(gMXG9A>OQGyGb4CP^l#B;R0Hdg~V)I5S8zNtI$X z&anoyCwCz7$7pNp0LuxiyFQYx1{tyr1tOsLL;cbLkHlR|*`@75)H-6Oq4Ft@#L5sQ zm-~@ZG|7xnteAou6#OR&{xb!CM!{ADP}rfJF1VKhg-0Xexdb7w1rncF7N? zR5VCnN^J2P^r$o&Vmsb4?ul3B0Y3c_!G6@iz|jRn(q>KK(cq;qYiT41OD0d=jrMEp zk)F7xF5%f6^K72Z#s-(sDllz}c+cut@WA)`yO-a&JZ+1w-IG|mKel%Nt?am`Wx}ku zn2)>GCR}wfSKZX6@q~|1uyh=rbkyCy z(Z-PwaX`+0ul<%nBBk^LL;ZovDhn>g)N`Vl)hr-`=tD?Uw)%|o^4(RgO;6=x zU)d?d)XbNyf}JkgzhiAz$ViRFZjp;9r6d_WQe#mcpS15 zA(6nq2~ulC=t8A;;-K6iQdp6YZ7~l6u$7@U33I2K;Efa0b!?lus3^%$(<1fhtjo@o zlBgT8O;!o1xv)z2KvFRLQukPmy6$vE@LWs61W-kcsm{DC85k`PWW4iJm>?pt!aOPw zGaPhGnkqR@FqlXa6*0mnDDjoZfhtowp%pG$6Nn^8zDQ2>P^XuQ2!bzCrI-bwzd(5ZRs1YPhOtOU8^uan{MG8q{E*$JcQAw$&h=^N3S1<9G==e z_9@f5GH?ssWFXvp1f)7~~1kbc{yN6Kv%<>^Cx$|cQy{^2Gh zjdRa&VP4?EV3mYn0UCx~XIL-j!jRyHje;Q;Cqm?MgU&GgtA)+X9@&!0V~jq9&v!h~ z8|=I29|*iU@CWz+V?z)JUcLg}9wlQMV-n%WU|JQ+7=&O89%$|MHC(#VBPs<;T4fDl z(S0Zwyz>9H_vJxxTxp(}R239eMcqJgi2`v7384!i#34X{bv7y?(9KGKBy1$u0&J-) zw|nFbH+Ia%ShJ3m+|#jQBYJk}?wvLfGuxq^_3r9ImfP-#EEOF^)AHKU<2^cJ|B!6d zYWH+(?C*PdR#k$)wx)MCCJA1?%H!qB%$MKy`@Z)b^>zQWp=5C2?7(P!-8rhAM8vHN zJwxXPFVv61A=(o1E!03LGrr|ovE|OMB&;xJ`5&5?wwNMMu=@&GU+OvORwN&j1+tFGGJ%{>SWV@!Uy15A`gUI9 zxU?@CaJI-#;6~2L?6=@InV`@yyN$}l%v;A{u`O z`$TB*64ky-1r4paG}Z)=l1OX&X~#v{VZ!|QP*7~Iy{WUcyXjE5dCoY}KL`nxkOD&^ ztV_?+N%VNdY4|g!G1~!I{mGDrsIARikX~bQiIHj2(-QdjbVc#C7v6bcvg8*l){PrK zg_juSgQG0WG7`wIk2ou%&W&Lw($H5UQjWtr>&c9Iio%{Ep}6TzZM5Z3xaCm9b6Bt) z{>gdURPtinI#Istkt;TWg{Hvo8SJ8O#Cst-pyZepZLRQO-t{L0z2RRj!+piZ3 z2Tsi7oQ$TQlvSjdT}OxRqeB^y%qqcI^_h%|T5pU2b3qV}I{aaWf7W*B0eNgv=Jzj5 z2B!tOR7`p2aPFi0BPW;9%YVtj{$6S+Z_DFvSMY6F#@nl#eE2bKqoUI4g@x&k*)Zmw+r{~l_aAb543iNYB*pSi#C#c?@ zo*+?b(Of5uSUl{KMtV++Ar=%Hy6HVVu+%|x(H_-!2`7<|$at_gSO#f?Q9*c!&6vH; zJxVf|%6bNYVp^d>)ao-1IJ0^V<3ruZl=MkA2B>^;A6GEeP zB19um_BDcN-$x67e25lsF-OOOMF5&f!#I4Um3 zxvvB*Eo$N?8C1#Fhx6Ccfu&UgEK}q@scRWnCMwJWHYhkUh3XabT$1cdfMO2KSfZ~dZjqlFj8&kLVkX*b z*1RkEILCeg3MbST7HuDyjvgJNBMzu|kL)4k z+tQ`_OGkdpq!E@iEp$KIsc5F6l?r+?<(}XukGcW{gUIy21z)5BG!QOOR69=v$v$EL zvR_k;>6IAx`#Jp#(IGVF%)(?dEEv3ip9s%&k+!N34{`0Kbmupyc9V*KOGPP)5hB4d z&XXcJGB%&LFzLtRVif;PTt^4#rRQEbcKKK|y(FAoGV$VMFp|DLnqCu5uZg7BvZ-t# zZ~u&SzXtDZyR$0Vd@$U6Fyc8R*bXfL?|HJX)L*U_@gh=oXGA>4*eaCzU__kF|F_G^3IL6(j+$XGIY<~kqAt&RA$MqG73d$}t$`<}mE7|E@V=C*`$ zTOzrw(cC>k?w)&In!J~RBP<+G2}5HKDWuze@)=6)O!_RNYRY?K6Vi7_GR{Sv=Kw~n z&Nu57(@YG8L+f!5g4_FZ-cL2Ja(CYak0y1_NXKXP;Fy8sc3@g^^|+S+aW zw4HA^8>byj+4ym{oNu=n@2<*6{e26MQbdTsZ__Y|gKe+VHYx}gjS#8Oz_t>CZN&;~ zBbKgV?Ci8&oS`gVAGpP-*~CCM47YuZ{*9f(rKI3Dl9?Ptvi^|^BQz;D8tm_1P`xIG zU}+?0$~qJc7Mk`#1*Y}>p<(j*Mqoj*ejftGVR9mc2CUT8w25u$XxhXb&lI4=sn5iR zK@TVfpZ!mInKC|0GqvKiQAzlW)`ZV!orupA721Erkoe=`GaWvUG#~0TuO@ie(lLhV z_&u3t%6w0?7K0_7g7EY##YkDDPP(XAse2{T7UPN20#0mp~wX3Q-QGN50W z8jEDqN13jJ*o%grH0$sam`*uKxj;;#n8F85W=$ncwO>CV6tqMf6h|4seOwJ+ju6JU z)STP0hQGCfZ>cce%5UQF<92}+nh9F z=Mx37U?mWnNQ5zr-=U*pS8+{A){jj#0uoQT2$w1J$F?!Tn8>f;nE!I2)Ldb$|H#W? z9fpUinJ?37KP8t$2_^;4nbFdwvZp*nq@|D1;e`pMF`iKVcN{JTH!hJJsYLBx9N-8ferc8HE_?MS(L^GZeBi6S zzA@t47Ikl)0pCA#MEH z$a%edosIY|Xi>2=jHV0$TZ}F>O*}?h29h;N+UP5eTjci1S&PsbEvE}4>aEva%2L}? zgDU9-UkI4vCZRF0EH0!QlqaXbTN0+A72cxPJ8eoj1-r{Fb4nsYAbSPR>rY`bU7fWn zIS$1)Gyakm^2rmma@3$$MIDQj0}ZAoTuvxQ(bkD{LaVO}`IH%5kc5~$75n2ZQ461{#{8@=TQ# zQsP-)TudP*Fr?tYN!l`v#IRx!Y6Nb`h)@G5myZ~REvoe`l3Ya#OSA1myoq*#=1vgvO;n5ot@F`hr`9g#B)%v9aKb-<{NED|2$*vevq+pV*Av}g`|-^dK}!MN+V^HZLxBQgUKN@ z@-Jh@&5pI65h;Xf}|=6O-d9#7{RWD(#U4S-hgsFi)slv9^2@jq=(G5Vp0 z((@O4=%7gKSvTS2M2hqvyBh9W*>zEG-FWH)MfF@UdEwnxCttnZEEF_G9NPr*Hl(Ob zum5r+9jyRPQejS~^YtqisM> zmSfBb!_HJ@*qIuzj9LO#IC|UW939Mbcpv$8u#dJGT^y^xMLYU~YiJ4|Zr`vOLK>Qp z(V-yBc9@!24CgX%5(ZLCGCKkPVo~80T-(?G;y|ygiq!4iu0!!7t5hI%K-9A7Y-DECIuYRoL<}#Sh8g)A}x^5VBDf- z{d!aZ?eM6Rlz4i_1x)IGio&3G3j?GFuV~*iC`}I@(W^`ms*lqj9<+^mYJMC)lw-qiHCPY`z>di*3#tU+KCD> zLRveun*B=rmK75uw_$nTGJJM;z07DEalf=ADjz!y9eRS{BL1)elitLWe1&0SXPjW% zsh?pj`6#I~*wUdWF6Av+(KC>;Kpz+NPYswCILj=n26QH(;P*Ui>P+fTXo%V$Ju@YB zT1-Prk`Hz19UDkhkLfg)pyAGB-6yZ4ARSKWRIR=@k&2-+Mb|z(6~3jZ+L(IqQq^zU zs`fz@)Z@nl43895zyi~FhUwIRe-IwB zEoq2=BK2wi zXkY)S6Jvv;jP}l1!~i%WxDyQ>Hphwl$OQ;!BzF8TwPO{-(DhI}J+j$GzY-0j=ZwQc zBcpTPc=}9R8A&>VdrKtH50eb3`}x`Yl4}>=x%d~~j%KYH-;U7lp3Ey{m&>l!MBGJD z_bS1?YBDM6S{HV$i?}wx`Z$JcH{7vAw{?ZL!A!YZuyw-$HO>20=aqw(4_-YTNiT_} zuNKl*PZrMFyl?eUminvbBN=7Uj4C0cYI6HSlfh*fH{p_?_5+s(q8Y2g8LR$XHB75B zs>iLfo9m+H6=Cy=tLG=(5%cQFuCRIIr_LP8^)ZnK>uoW;sXH_3E)2U1XPv&Q6%%KE zk-e7HuC0A%?PS?hb0m8+u2W?Hf-`s=h!c_KAbS@FvcV|xlSPxpiQI^zQZQG3zJ|-L z{M`eG4>?BY*{BxbTi&aUZrvB&x-a4h+_wb~4Fdar|HzNFz7%8idfQU?E1p&__ajmq z{Md-vyP4Q_GsQ=xzXs*4tZXVbHgj0r*~qt)D9M1`U!qa=Dh=1~Q1K^JT%tm;m=3;1KiO#fPgMJ7R1k?1g%^S|!GA;Bs36f| zgw#r8V`IkLsh6`Bk@T}tl71c=0Y9I3WmYD(L1)q+R~88^@n3wC0#G}Bd`tj3k6w=b z4p-9%K_RMI9Hb3Hye+YS|U4BC`^2~Q>Ovw`-X44L6QK%FpnjK7wLcbGn5W%$9 zNQ?51GL86Usr*|{URjzn;BPnTtwT0c3~IEQiIl+c<)nTCxs`3uxx$tw2^{)p(! zPf^Sv7(V-AgFnG9gpi0c6lDyX4h&ACSX2s82Op#mHs<~qQi$7kW!2?XSGy(x5qH^m ziYk1NmU+kZmxO}$h~qiI{M?tpDcsRlcX@rf{BaxYU#4$Q)+J<;{DFex53ET3z$)>( zQ*;U2|55%xm@{3RzNqwpnAavL<~4XJzB5F37ne?r{y^3#5WA&Cih&J-`c5UjgS4%f zwjnNdxs-D%naL?S`UgD;LB;bb{TNO;bXxCN=sFboAjv5yy7#X{uo*UYn&ms_6Xg`M zZai`eObzrOsHW=l6U_DhmiDs_P0@+0FaMFYy@x{2T%UlZEPbeSnWPU!kcjI2Lr5Q= z=C8bV{+;uasgeBk(fk@Azh>$riio=_oL_U*_1@uo{6`z2_1}7X-Aw0!E9>4`ht1Sx6MydNEt~X#UzE4 zkrd)*QV2@_X4G%_OjPFi;Kq5Y)zMNDPvJa53lLa0(@aC?JRm-GyZh1bbwP1RZHZa=qMCRU)|% zRf)2KXtl{qGG!q{o(F;9CP^ficv)n2MZV+`B@fX|f|^B0emKXo+_E(DLOqDNAOsRU zkwnKNOYVY>NtV3pnTjPo%{z?|N&Z#~i~J`0BMt=#Bv{cD3s#A+NKwnajre;O$o+vGl4AGNWkeiX zlkC%=P2txkhQv~jgnCpx5+Sin6j0+-H7O|7=_1{}E6UgfLswXr+;cK(XN&6Bxs0hd z+wlb3Hl&`BC#B44>Uns`Dt@EOXy3kwhiodQ6;cYg?t@4s9Waf$m0OYYtpT=rGr>Z z+yYSO>aqpgu_7%?_dX1;)+a$XD@z5lNM|SL03lYx+sCE5(F@UG#IB3I&g|*#%5D=&43J=t#N?F$A5B zo*TPA3fJ~L?Yv0^324D}RD4Lq%P2-@MqTl;WjtMDKIqVLPBh9G<|nvLB5eOUv$~(< z!&5bq-w@4ec*BnDBUZ?o3&=%x8^I&?wGOdR2mhETknLT z?Z?9H$0DBNg6;Uzy?vkV$@|clnri=LcFDwfL}ba{9rf-WZ~f)2j!4$N4>lo%V78J% zFuUo4P2)R-tbK}qZsQH#M;q>iK5U$^9Z>_noSqC_uf1_zDDH@OkP&g;=jB{>YynwDv0Qu90sqH{Lawsh*bCUI8E8t*F0W zoP=ZEhgUD^6(%Z?IHbYw{??l3wixbh;8DM~5tyF8x5Y&D1_SEPQup<94Aln&@?^bp zrKt;gXDUhiMQc)j(K-?6(U7A>ffnE;K`@drTU0+|N#2C|=_g;KP-=xmy_kp2d z9$M8-6|mp>bG#`_o8twjQ?z4;ie$zYKP%0k=jqO55qfr}h-U2z%r2WXS5~Zb@J^*g zz6P#LmV#`dIrKo13?oRVwdfTrOT9v;DPU!RG2UWNO8?y`FlIQlGj-AHrfIHAnwz*q z_s-1FgQ<6B>9j0*1-s@7i$1#?uG3P$9l9W2GyaVMie<>%>rgBTcVu z9`>w`(6iFK8*{+3j63iy?+y@E7PNwvZ{QB}zl6+A6N7w9w0Hp=P1@~v_IgSNdw&Ah z8;!e`2zIR+rw_ewGOkXm{O%XD^Kf@skAdPp6~BP0!9A`366`p736hPH7n(kQ)RrempnW|c?rfTK=P%MBTQw; zw-6F%ATX3Gs-#5p4Ly|>Sy#e-MC}Aiv>kFQSCdpdrIn#<%~5eJ1ew}7l&_#>?V)Py z;gDBv`<$tFu>V9b&|$Jwa&FJ1rZ9x+wVLdZ95unI~wsE6Kuy81mv1i;{9=a`5Np! zYxUi?`ZY(l+&LU=JrZs`67d`rY)9e!^VYE$Yawaa$^7R+B%^%1eb$lj=BqIA6r&{y z<@N7{qKyZ_jRzu`U4pag0pj52O{M|fBSud1ti$zYNOQ0X-ZPI2&f^c0Oldv*FEe~s zeUXfcsIx+FR;Y0gi>8c|xsl9j!C6faZrpEm&scNC_ER8=xT+J|SMN9Tpx``6?H>}` z{}+Q%XQALMT&Q`cSB$18tlTiwHdP}O)V=q7w4pQH&>3+Y5X=WYKfo_byZ`&oorL-Q zoNM*(KJ(-vZ6NNHk{!YTO7J(`*f8U4S07A8;|&8M^OS3!&o7+ESvv6^SSq*nV#dkM{4?RqR-0_@FciTRte~slLic^(r1Me6ZfSi!*#s zU$b+o;b)t8)PGiO!rq^4HKG0?XF&Z!1CKU8G+B3h3?DjccH0dfnR(PdN~I$|vYV*x zp(7WW)%XzfI!Fag8H|w1NHePueFiJDY>B~ z996fEIm@vtiV!nShrL)FZ4v%118vg8PY~)Pn1FXtmg-iDfh{J+hzkvs;O?!0PP%n= z<&hM$BwiOh(vJmOt7-ueXU$Be7# z6Rz)PY0%%Uj}Axo^oIBJMm&9jt?x6>YQ(nQ6!A1g)0)PUK5@8|V`rUv_58FoQKK|g z*^i2sg`55!c?DhB->(%MC1l2ZRBOhq^)tF}J3?mMM*)s3oV^*5TrS{gU-GsV0^PLK z0Nu1y1KptZZYH+fOmR_}mA$>3ySX8S>RYQ(-de}E`i-}ck`MKU8kD!Id7N~+reb@E z;m*n=Y`Ig+Q@zAM_40MCd4}n1+CH7jQ$5d!x(GXvH|r7&p;u`LENg(_8!4J`^mEgz8fpf@WB3)jNUvCNJC87;Yd!gI}xS`qG_6)V$ zb2&R=o@5NvsE>sjjRY;UH<}Jqbd{aP&A-oTT~Ws z$1QwKqH*?^t(o`N1Ybb zyB$LxU|4}aSr^`p-`ZZ}r7VNBPT? z1jdTBDIgLoa#`BsrH?kh`o!d#=K05Xyh*mBYufrpk+Ug=Fd{xR{KnRdAe0Ao^Z_}I zjs-zEM&>~c=klJYw>a!Ap6G{@v9~JftqyyU)uU!S^%IA0A~ovY z81`?R>Wcd7?)&R7IeVq<&+C2=ik8-gOY3Jm4T7!VGk5u9QN+D=Jmq1MJ++bYfV_P9 zLy3uefftk&*W@lDRuo=4^vnih|f>P9{Q!P|_==f=DU!6Q%K*z)S6vt1O@Ki5j5Y8rX8ZIe3+i{dBr4%EXXWlDC*j7N##kuK_lGWa{f2> zu*QnCJLS!DIXyjqGJOMLfM+?u7$R1UAcF~c@=?B&Q)BqLks&2W%p&+H6_h9{NEYyO zDMq*6zWpA+XsD|?5|B0~>A)JP@K zh#+RWxjA?IF~R;I$sBV_Pgtq2x#RM}uVkBpR4gKHIre ze15ypRU`pofc60GhCs^&8>rYxMSzOqRPE|F7uTw#>VOexW7SoBv zh!OoIZ_&c8nPD7Jr9Nhvy$~tsirR zf5+K>#kqgQS%1aZKjv~i<}$G5V=nW*aa%v;R)5Sr_dmFXFxT*@#TB*W+_z9(Lrd{@ zQXX4ey!UaLkGDUrPU8>r4~w|$Dq&r-;A;`M^vB6ryz_C5jdwmQ%tK-0vh$y$QEfY4 zgwjI4t*AZT%iDPO;}u4JY(mvtrhmF8J06nUxPyjB2Y!KIyUD&gVQStVC<4_|13(=Xopo zo?FUiKknp_as2T%9@$*yH-kfB56^e; kf_vpW$7=K24TadBT`|vLJ-?GL;2VYXlHYPzGve}p0Bg|e@&Et; diff --git a/custom_components/my_music_library/__pycache__/config_flow.cpython-314.pyc b/custom_components/my_music_library/__pycache__/config_flow.cpython-314.pyc deleted file mode 100644 index 139f19e9ea4db335ebb89c88745f2ad444e9682a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8754 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 diff --git a/custom_components/my_music_library/__pycache__/const.cpython-314.pyc b/custom_components/my_music_library/__pycache__/const.cpython-314.pyc deleted file mode 100644 index d961fc20c01ba28eaef92a6142e342b4d05fd3bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 857 zcmZWn&2HL25MBe}&tObwP@<{WQvY38^m-DKF5q1Ba$7?e5ICGv9ogoo8G&4d{AlpI_P$0R9BddPVOH z=5|004?uvy7XX*Eh3Zpm%V|W8@;5dxq1We#0OyU$w;WSKR7P2@4GdK&g z7ugJ`MKA}si)aSGTrG-op~X{_hxt8lusJW)t%-x>33kdu%Pvc^vgA6tQf})5TefG_ z1BS5jWMW|%>ok(^<_wuZ1TyX-=KTE}!l~L&b`6ZChCI`3!mfQR4@lgUk!u?yt!e{z zgiK4-2(Nup3|H0ERfa?2cd7Hz7~|=YAdD?jlO0D7rB)3~k&UC*hi_hym`UOJwmGYW zswyj06-Bny>ZLQ^6z&Ta2qVl>g05JT>U1)iCj@E@^-<>_PZ9{JmVyvTBQ$ieYikGv zp9Dy}aVCka4vBR{sYwd;v(5=>G*85SyN5ou#q%a4{6An%JSJ?2NFwUPHp#7?kNc-c zI%_n^RtPjd*W3L@v$1075_U}+FmP*UMN*Xd-Bulm-EOPf6FWV$lEv3W*gzk;=%m$d zh7yw@5)@9hXrWL-{!%bZ#hjgH0t{iE%2 zaCJH($+z_PTtj(%&L_t~ str: return "" +def _make_thumb_url(raw_path: str) -> str: + """Wrap a raw image path in our thumbnail proxy endpoint. + + Instead of exposing raw image URLs (which may require provider auth + or be unreachable from the browser), route everything through our + server-side proxy that resolves via the MA server. + """ + if not raw_path: + return "" + return f"/my_music_library/thumb?path={quote(raw_path, safe='')}" + + def _serialize_search_results(results: Any) -> dict: """Convert a MA SearchResults object (or dict) to a plain dict.""" if results is None: @@ -77,10 +90,9 @@ def _serialize_search_results(results: Any) -> dict: if isinstance(safe, dict): for key in ("tracks", "artists", "albums", "playlists", "radios"): for item in safe.get(key) or []: - if isinstance(item, dict) and not item.get("thumbnail"): - thumb = _extract_thumbnail(item) - if thumb: - item["thumbnail"] = thumb + if isinstance(item, dict): + raw = item.get("thumbnail") or _extract_thumbnail(item) + item["thumbnail"] = _make_thumb_url(raw) return safe return {"tracks": [], "artists": [], "albums": [], "playlists": [], "raw": str(safe)} @@ -242,7 +254,7 @@ def _normalize_library_item(item: dict) -> dict: if isinstance(media_type, dict): media_type = media_type.get("value", "") - thumbnail = _extract_thumbnail(item) + thumbnail = _make_thumb_url(_extract_thumbnail(item)) artist = item.get("media_artist") or "" if not artist: @@ -554,7 +566,7 @@ def _normalize_browse_item(item: dict) -> dict: if _path.startswith("folder/"): uri = f"{_scheme}://{_path[len('folder/'):]}" - thumbnail = _extract_thumbnail(item) + thumbnail = _make_thumb_url(_extract_thumbnail(item)) artist = item.get("media_artist") or "" if not artist: @@ -680,6 +692,45 @@ def _parse_ma_uri(uri: str) -> tuple[str, str]: return item_id, scheme +_SUBITEM_REST_PATHS: dict[str, list[str]] = { + "artist_albums": ["/api/music/artists/{id}/albums", "/api/artists/{id}/albums"], + "album_tracks": ["/api/music/albums/{id}/tracks", "/api/albums/{id}/tracks"], + "playlist_tracks": ["/api/music/playlists/{id}/tracks", "/api/playlists/{id}/tracks"], +} + + +async def _get_subitems_via_rest( + hass: HomeAssistant, + action: str, + item_id: str, + limit: int, +) -> list | None: + """Fallback: fetch sub-items via MA REST API.""" + ma_url = _get_mass_url(hass) + if not ma_url: + return None + + session = async_get_clientsession(hass) + paths = _SUBITEM_REST_PATHS.get(action, []) + for path_tpl in paths: + url = f"{ma_url}{path_tpl.format(id=item_id)}" + try: + async with session.get( + url, + timeout=aiohttp.ClientTimeout(total=10), + ) as resp: + if resp.status != 200: + _LOGGER.debug("Subitems REST %s → HTTP %s", url, resp.status) + continue + data = await resp.json(content_type=None) + items_raw = data if isinstance(data, list) else (data.get("items") or []) + _LOGGER.info("Subitems REST %s → %d items", url, len(items_raw)) + return [_normalize_library_item(i) for i in items_raw[:limit]] + except Exception as err: # noqa: BLE001 + _LOGGER.debug("Subitems REST %s failed: %s", url, err) + return None + + async def _get_subitems( hass: HomeAssistant, action: str, @@ -690,11 +741,11 @@ async def _get_subitems( mass = _get_mass_client(hass) if mass is None: _LOGGER.warning("No Music Assistant (mass) client available for subitems") - return None + return await _get_subitems_via_rest(hass, action, uri, limit) music = getattr(mass, "music", None) if not music: - return None + return await _get_subitems_via_rest(hass, action, uri, limit) methods = _SUBITEM_METHODS.get(action, []) if not methods: @@ -723,10 +774,11 @@ async def _get_subitems( _LOGGER.info("Subitems %s: %d items (args=%s kwargs=%s)", action, len(items), call_args, call_kwargs) return [_normalize_library_item(_to_json_safe(i)) for i in items[:limit]] except Exception as err: # noqa: BLE001 - _LOGGER.debug("Subitems %s args=%s kwargs=%s → %s: %s", method_name, call_args, call_kwargs, type(err).__name__, err) + _LOGGER.warning("Subitems %s args=%s kwargs=%s → %s: %s", method_name, call_args, call_kwargs, type(err).__name__, err) continue - return None + _LOGGER.warning("Subitems %s: all MA client attempts failed for %r, trying REST fallback", action, uri) + return await _get_subitems_via_rest(hass, action, item_id, limit) # ── Providers ───────────────────────────────────────────────────────────────── @@ -853,9 +905,10 @@ def _normalize_queue_item(item: dict) -> dict: queue_item_id = item.get("queue_item_id") or "" duration = item.get("duration") or 0 - thumbnail = _extract_thumbnail(item) - if not thumbnail: - thumbnail = _extract_thumbnail(item.get("media_item") or {}) + raw_thumb = _extract_thumbnail(item) + if not raw_thumb: + raw_thumb = _extract_thumbnail(item.get("media_item") or {}) + thumbnail = _make_thumb_url(raw_thumb) media_item = item.get("media_item") or {} uri = media_item.get("uri") or "" @@ -1361,3 +1414,64 @@ class ImageProxyView(HomeAssistantView): ) except Exception: # noqa: BLE001 return web.Response(status=HTTPStatus.BAD_GATEWAY) + + +class MAThumbnailView(HomeAssistantView): + """Proxy thumbnail requests through the MA server. + + GET /my_music_library/thumb?path= + + Resolves image paths server-side via the Music Assistant server, + handling provider-specific auth (Plex tokens, etc.) and internal + MA references that browsers cannot access directly. + """ + + url = "/my_music_library/thumb" + name = "my_music_library:thumb" + requires_auth = False + + async def get(self, request: web.Request) -> web.Response: + """Resolve an image path via MA and return the image bytes.""" + hass: HomeAssistant = request.app["hass"] + raw_path = request.query.get("path", "").strip() + if not raw_path: + return web.Response(status=HTTPStatus.BAD_REQUEST) + + ma_url = _get_mass_url(hass) + session = async_get_clientsession(hass) + + urls_to_try: list[str] = [] + if raw_path.startswith("http"): + urls_to_try.append(raw_path) + if ma_url: + urls_to_try.append( + f"{ma_url}/api/image/{quote(raw_path, safe='')}" + ) + elif ma_url: + urls_to_try.append( + f"{ma_url}/api/image/{quote(raw_path, safe='')}" + ) + if raw_path.startswith("/"): + urls_to_try.append(f"{ma_url}{raw_path}") + + for image_url in urls_to_try: + try: + async with session.get( + image_url, + timeout=aiohttp.ClientTimeout(total=10), + ) as resp: + if resp.status != 200: + continue + ct = resp.content_type or "image/jpeg" + if not ct.startswith("image"): + continue + body = await resp.read() + return web.Response( + body=body, + content_type=ct, + headers={"Cache-Control": "public, max-age=3600"}, + ) + except Exception: # noqa: BLE001 + continue + + return web.Response(status=HTTPStatus.NOT_FOUND) diff --git a/custom_components/my_music_library/manifest.json b/custom_components/my_music_library/manifest.json index 068d08b4..a17ce954 100644 --- a/custom_components/my_music_library/manifest.json +++ b/custom_components/my_music_library/manifest.json @@ -9,5 +9,5 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/Patafoin/ha-my-music-library/issues", "requirements": [], - "version": "3.10.4" + "version": "3.12.4" } diff --git a/custom_components/my_music_library/www/my-music-library-card.js b/custom_components/my_music_library/www/my-music-library-card.js index 73579b49..64162228 100644 --- a/custom_components/my_music_library/www/my-music-library-card.js +++ b/custom_components/my_music_library/www/my-music-library-card.js @@ -5,7 +5,7 @@ * @version 1.0.0 */ -const CARD_VERSION = "3.10.4"; +const CARD_VERSION = "3.12.4"; /* ─── Icons (inline SVG strings) ─────────────────────────── */ const ICONS = { @@ -169,6 +169,32 @@ const TRANSLATIONS = { search_layout_rows: "Rows", search_layout_columns: "Columns", show_device_select: "Show device selection", + type_custom_element: "Custom element", + btn_element_name: "Element tag", + btn_element_config: "Config (JSON)", + action_mml_navigate_tab: "Show tab (MML)", + action_mml_navigate_section: "Show section (MML)", + action_mml_control: "Player control (MML)", + btn_mml_tab: "Tab", + btn_mml_section: "Section", + btn_mml_command: "Command", + mml_cmd_play_pause: "Play / Pause", + mml_cmd_next: "Next", + mml_cmd_prev: "Previous", + mml_cmd_shuffle: "Shuffle", + mml_cmd_repeat: "Repeat", + mml_cmd_mute: "Mute", + nav_bar_section: "Navigation bar", + nav_bar_position: "Position", + nav_bar_pos_top: "Top", + nav_bar_pos_bottom: "Bottom", + nav_bar_pos_left: "Left", + nav_bar_pos_right: "Right", + nav_bar_align: "Alignment", + nav_bar_align_start: "Start", + nav_bar_align_center: "Center", + nav_bar_align_end: "End", + nav_bar_align_space_between: "Space between", }, }, fr: { @@ -290,6 +316,32 @@ const TRANSLATIONS = { search_layout_rows: "Lignes", search_layout_columns: "Colonnes", show_device_select: "Afficher la sélection de l'appareil", + type_custom_element: "Élément custom", + btn_element_name: "Balise élément", + btn_element_config: "Config (JSON)", + action_mml_navigate_tab: "Afficher un onglet (MML)", + action_mml_navigate_section: "Afficher une section (MML)", + action_mml_control: "Contrôle lecteur (MML)", + btn_mml_tab: "Onglet", + btn_mml_section: "Section", + btn_mml_command: "Commande", + mml_cmd_play_pause: "Lecture / Pause", + mml_cmd_next: "Suivant", + mml_cmd_prev: "Précédent", + mml_cmd_shuffle: "Aléatoire", + mml_cmd_repeat: "Répéter", + mml_cmd_mute: "Muet", + nav_bar_section: "Barre de navigation", + nav_bar_position: "Position", + nav_bar_pos_top: "Haut", + nav_bar_pos_bottom: "Bas", + nav_bar_pos_left: "Gauche", + nav_bar_pos_right: "Droite", + nav_bar_align: "Alignement", + nav_bar_align_start: "Début", + nav_bar_align_center: "Centre", + nav_bar_align_end: "Fin", + nav_bar_align_space_between: "Réparti", }, }, de: { @@ -411,10 +463,120 @@ const TRANSLATIONS = { search_layout_rows: "Zeilen", search_layout_columns: "Spalten", show_device_select: "Geräteauswahl anzeigen", + type_custom_element: "Benutzerelement", + btn_element_name: "Element-Tag", + btn_element_config: "Konfiguration (JSON)", + action_mml_navigate_tab: "Tab anzeigen (MML)", + action_mml_navigate_section: "Abschnitt anzeigen (MML)", + action_mml_control: "Player-Steuerung (MML)", + btn_mml_tab: "Tab", + btn_mml_section: "Abschnitt", + btn_mml_command: "Befehl", + mml_cmd_play_pause: "Wiedergabe / Pause", + mml_cmd_next: "Weiter", + mml_cmd_prev: "Zurück", + mml_cmd_shuffle: "Zufällig", + mml_cmd_repeat: "Wiederholen", + mml_cmd_mute: "Stummschalten", + nav_bar_section: "Navigationsleiste", + nav_bar_position: "Position", + nav_bar_pos_top: "Oben", + nav_bar_pos_bottom: "Unten", + nav_bar_pos_left: "Links", + nav_bar_pos_right: "Rechts", + nav_bar_align: "Ausrichtung", + nav_bar_align_start: "Anfang", + nav_bar_align_center: "Mitte", + nav_bar_align_end: "Ende", + nav_bar_align_space_between: "Verteilt", }, }, }; +/* ─── YAML utilities (no external deps) ──────────────────── */ +function _yamlDump(obj, indent = 0) { + if (obj === null || obj === undefined) return "null"; + if (typeof obj === "boolean" || typeof obj === "number") return String(obj); + if (typeof obj === "string") { + if (!obj || /[\r\n:#\[\]{},&*?|<>=!%@`"']/.test(obj) || /^\s|\s$/.test(obj) || + /^(true|false|yes|no|on|off|null|~)$/i.test(obj) || /^\d/.test(obj)) { + return `"${obj.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`; + } + return obj; + } + if (Array.isArray(obj)) { + if (!obj.length) return "[]"; + const pad = " ".repeat(indent); + return obj.map(v => `${pad}- ${_yamlDump(v, indent + 2)}`).join("\n"); + } + if (typeof obj === "object") { + const keys = Object.keys(obj); + if (!keys.length) return ""; + const pad = " ".repeat(indent); + return keys.map(k => { + const v = obj[k]; + if (v !== null && typeof v === "object") { + const nested = _yamlDump(v, indent + 2); + return nested ? `${pad}${k}:\n${nested}` : `${pad}${k}: {}`; + } + return `${pad}${k}: ${_yamlDump(v, indent)}`; + }).join("\n"); + } + return String(obj); +} + +function _parseYamlScalar(v) { + if (v === "true" || v === "yes" || v === "on") return true; + if (v === "false" || v === "no" || v === "off") return false; + if (v === "null" || v === "~" || v === "") return null; + if (/^-?\d+$/.test(v)) return parseInt(v, 10); + if (/^-?\d+\.\d+$/.test(v)) return parseFloat(v); + if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) { + return v.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"'); + } + return v; +} + +function _yamlLoad(text) { + if (window.jsyaml?.load) return window.jsyaml.load(text); + const t = text.trim(); + if (!t || t === "{}") return {}; + // Basic line-by-line parser for HA card config subset + const lines = t.split("\n"); + const stack = [{ obj: {}, indent: -1 }]; + const arrKeys = new Map(); // tracks which keys hold arrays + for (const raw of lines) { + const trimEnd = raw.trimEnd(); + if (!trimEnd || trimEnd.trimStart().startsWith("#")) continue; + const il = trimEnd.length - trimEnd.trimStart().length; + const content = trimEnd.trimStart(); + while (stack.length > 1 && stack[stack.length - 1].indent >= il) stack.pop(); + const top = stack[stack.length - 1]; + if (content.startsWith("- ")) { + const val = _parseYamlScalar(content.slice(2).trim()); + const arrKey = arrKeys.get(top); + if (arrKey !== undefined && Array.isArray(top.obj[arrKey])) top.obj[arrKey].push(val); + continue; + } + const ci = content.indexOf(": "); + const colonEnd = content === content.replace(/:$/, "") ? -1 : content.length - 1; + if (ci === -1 && colonEnd === -1) continue; + const key = ci >= 0 ? content.slice(0, ci).trim() : content.slice(0, colonEnd).trim(); + const val = ci >= 0 ? content.slice(ci + 2).trim() : ""; + if (!val) { + const newObj = {}; + top.obj[key] = newObj; + stack.push({ obj: newObj, indent: il }); + } else if (val === "[]") { + top.obj[key] = []; + arrKeys.set(stack[stack.length - 1], key); + } else { + top.obj[key] = _parseYamlScalar(val); + } + } + return stack[0].obj; +} + /* ─── CSS ─────────────────────────────────────────────────── */ const STYLES = ` :host { @@ -545,6 +707,56 @@ const STYLES = ` .nav-btn ha-icon { --mdc-icon-size: 20px; display: block; pointer-events: none; } .nav-btn svg { width: 20px; height: 20px; fill: currentColor; flex-shrink: 0; } .nav-btn-label { font-size: 10px; font-weight: 500; line-height: 1; pointer-events: none; } + + /* ── CUSTOM ELEMENT SLOT ── */ + .nav-btn-custom { padding: 0; min-width: 36px; min-height: 44px; overflow: hidden; flex-shrink: 0; } + .nav-btn-custom > * { pointer-events: none; display: block; width: 100%; height: 100%; } + + /* ── NAV BAR POSITION VARIANTS ── */ + .card-root[data-nav-pos="bottom"] { flex-direction: column-reverse; } + .card-root[data-nav-pos="left"], + .card-root[data-nav-pos="right"] { flex-direction: row; } + .card-root[data-nav-pos="right"] { flex-direction: row-reverse; } + + .card-root[data-nav-pos="left"] .nav, + .card-root[data-nav-pos="right"] .nav { + flex-direction: column; + overflow-y: auto; overflow-x: hidden; + border-bottom: none; border-right: 1px solid var(--border); + scroll-snap-type: y mandatory; + width: auto; height: 100%; + } + .card-root[data-nav-pos="right"] .nav { border-right: none; border-left: 1px solid var(--border); } + + .card-root[data-nav-pos="left"] .nav-tabs, + .card-root[data-nav-pos="right"] .nav-tabs { flex-direction: column; flex: none; width: 100%; } + + .card-root[data-nav-pos="left"] .nav-tab, + .card-root[data-nav-pos="right"] .nav-tab { + border-right: none; border-bottom: none; + justify-content: flex-start; padding: 10px 14px; + border-left: 3px solid transparent; flex: none; + } + .card-root[data-nav-pos="right"] .nav-tab { border-left: none; border-right: 3px solid transparent; } + + .card-root[data-nav-pos="left"] .nav-tab.active { + border-left-color: var(--accent); border-bottom: none; + background: color-mix(in srgb, var(--accent) 10%, transparent); + } + .card-root[data-nav-pos="right"] .nav-tab.active { + border-right-color: var(--accent); border-bottom: none; + background: color-mix(in srgb, var(--accent) 10%, transparent); + } + .card-root[data-nav-pos="left"] .nav-fade-left, + .card-root[data-nav-pos="left"] .nav-fade-right, + .card-root[data-nav-pos="right"] .nav-fade-left, + .card-root[data-nav-pos="right"] .nav-fade-right { display: none; } + + /* ── NAV TABS ALIGNMENT ── */ + .nav-tabs[data-align="center"] { justify-content: center; } + .nav-tabs[data-align="end"] { justify-content: flex-end; } + .nav-tabs[data-align="space-between"] { justify-content: space-between; } + /* ── CONTENT AREA ── */ /* position:relative + inset:0 on children is the most reliable way to give tab panels a definite pixel height without relying on flex cross-axis @@ -1146,7 +1358,7 @@ const STYLES = ` /* ══════════════════════════════════════════ LIBRARY TAB ══════════════════════════════════════════ */ - .library-panel { flex: 1; overflow-y: auto; padding: 0 0 16px; display: flex; flex-direction: column; } + .library-panel { flex: 1; overflow: hidden; padding: 0 0 16px; display: flex; flex-direction: column; } .lib-filters { display: flex; @@ -1197,7 +1409,7 @@ const STYLES = ` .lib-filter-fav.active { background: var(--accent); color: #000; border-color: var(--accent); } .lib-filter-fav svg { width: 14px; height: 14px; fill: currentColor; } - .lib-content { flex: 1; overflow-y: auto; } + .lib-content { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; } .lib-section { margin-bottom: 8px; } .lib-section-header { @@ -1219,6 +1431,7 @@ const STYLES = ` scrollbar-width: none; position: relative; } + .lib-scroll.scroll-locked { overflow-x: hidden !important; } .lib-scroll::-webkit-scrollbar { display: none; } @media (hover: hover) and (pointer: fine) { .lib-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,.2) transparent; } @@ -1815,6 +2028,12 @@ class MyMusicLibraryCard extends HTMLElement { const typeCounts = {}; return config.tabs.map(t => { const type = t.type || "button"; + if (type === "custom_element") { + return { type: "custom_element", id: `ce-${idx++}`, element: t.element || "", + element_config: t.element_config || {}, name: t.name || "", + tap_action: t.tap_action, hold_action: t.hold_action, double_tap_action: t.double_tap_action, + width: t.width, height: t.height }; + } if (type === "button") { return { type: "button", id: `btn-${idx++}`, icon: t.icon, name: t.name || "", entity: t.entity, tap_action: t.tap_action, hold_action: t.hold_action, double_tap_action: t.double_tap_action, @@ -1823,7 +2042,7 @@ class MyMusicLibraryCard extends HTMLElement { typeCounts[type] = (typeCounts[type] || 0) + 1; const id = type === "settings" ? "settings" : (typeCounts[type] > 1 ? `${type}-${typeCounts[type] - 1}` : type); const tab = { type, id, label: t.label || null, iconOverride: t.icon || null, - defaultIcon: TAB_ICONS[type] || null }; + defaultIcon: TAB_ICONS[type] || null, show_in_nav: t.show_in_nav !== false }; if (type === "library") { const sections = Array.isArray(t.sections) ? t.sections.filter(s => VALID_SECTIONS.includes(s)) : null; tab.sections = sections && sections.length ? sections : DEFAULT_SECTIONS; @@ -2068,8 +2287,10 @@ class MyMusicLibraryCard extends HTMLElement { const card = document.createElement("div"); card.className = `card-root${this._isMobile ? " mml-mobile" : ""}`; + const navPos = this._config.nav_bar?.position || "top"; + if (navPos !== "top") card.dataset.navPos = navPos; - const panels = this._resolvedTabs.filter(t => t.type !== "button"); + const panels = this._resolvedTabs.filter(t => t.type !== "button" && t.type !== "custom_element"); const panelRenderers = { player: (t) => this._renderPlayerTab(), search: (t) => this._renderSearchTab(), @@ -2107,8 +2328,20 @@ class MyMusicLibraryCard extends HTMLElement { this._updatePlayerContent(card); } + _renderCustomElementSlot(tab) { + const sizeParts = []; + if (tab.width) sizeParts.push(`width:${typeof tab.width === "number" ? tab.width + "px" : tab.width}`); + if (tab.height) sizeParts.push(`height:${typeof tab.height === "number" ? tab.height + "px" : tab.height}`); + const sizeStyle = sizeParts.length ? ` style="${sizeParts.join(";")}"` : ""; + return `

`; + } + _renderNav() { - const items = this._resolvedTabs.map(t => { + const align = this._config.nav_bar?.align || "start"; + const items = this._resolvedTabs.filter(t => t.show_in_nav !== false).map(t => { + if (t.type === "custom_element") { + return this._renderCustomElementSlot(t); + } if (t.type === "button") { return this._renderNavButton(t); } @@ -2131,12 +2364,14 @@ class MyMusicLibraryCard extends HTMLElement { `; }).join(""); + const alignAttr = align !== "start" ? ` data-align="${align}"` : ""; + const navStyle = this._config.nav_bar?.style ? ` style="${this._esc(this._config.nav_bar.style)}"` : ""; return ` `; } @@ -2511,6 +2746,27 @@ class MyMusicLibraryCard extends HTMLElement { }); }); + this._mountCustomElements(card); + } + + _mountCustomElements(card) { + card.querySelectorAll("[data-ce-slot]").forEach(slot => { + if (slot._ceMounted) return; + const id = slot.dataset.ceSlot; + const tab = this._resolvedTabs.find(t => t.id === id); + if (!tab?.element) return; + // Strip Lovelace "custom:" prefix — the actual DOM tag name never has it + const tagName = tab.element.replace(/^custom:/, ""); + const el = document.createElement(tagName); + if (typeof el.setConfig === "function") { + try { el.setConfig(tab.element_config || {}); } catch(e) { + console.warn(`[mml] custom_element "${tagName}" setConfig error:`, e); + } + } + if (this._hass) el.hass = this._hass; + slot.appendChild(el); + slot._ceMounted = true; + }); } /* ── Nav button action handler ── */ @@ -2577,6 +2833,75 @@ class MyMusicLibraryCard extends HTMLElement { })); break; } + case "mml_navigate_tab": { + const tabTypeOrId = action.tab; + if (!tabTypeOrId) break; + const card = this.shadowRoot?.querySelector(".card-root"); + if (!card) break; + const tabDef = this._resolvedTabs.find(t => t.id === tabTypeOrId) || + this._resolvedTabs.find(t => t.type === tabTypeOrId); + if (!tabDef) break; + if (tabDef.type === "settings") { + this._openSettings(card); + } else { + this._setActiveTab(tabDef.id, card); + if (tabDef.type === "library" && !this._libLoadedTabs.has(tabDef.id)) this._loadLibrary(); + } + break; + } + case "mml_navigate_section": { + const section = action.section; + if (!section) break; + const card = this.shadowRoot?.querySelector(".card-root"); + if (!card) break; + const libTab = this._resolvedTabs.find(t => t.type === "library" && t.sections?.includes(section)) || + this._resolvedTabs.find(t => t.type === "library"); + if (!libTab) break; + const alreadyLoaded = this._libLoadedTabs.has(libTab.id); + this._setActiveTab(libTab.id, card); + if (!alreadyLoaded) this._loadLibrary(); + setTimeout(() => { + const panel = card.querySelector(`[data-panel="${libTab.id}"]`); + const secEl = panel?.querySelector(`#lib-sec-${section}`); + if (secEl) secEl.scrollIntoView({ behavior: "smooth", block: "start" }); + }, alreadyLoaded ? 50 : 600); + break; + } + case "mml_control": { + const cmd = action.command; + const player = this._activePlayer; + if (!cmd || !player) break; + switch (cmd) { + case "play_pause": { + const state = this._hass?.states[player]?.state; + this._hass?.callService("media_player", state === "playing" ? "media_pause" : "media_play", {}, { entity_id: player }); + break; + } + case "next": + this._hass?.callService("media_player", "media_next_track", {}, { entity_id: player }); + break; + case "prev": + this._hass?.callService("media_player", "media_previous_track", {}, { entity_id: player }); + break; + case "shuffle": { + const shuffleOn = this._hass?.states[player]?.attributes?.shuffle; + this._hass?.callService("media_player", "shuffle_set", { shuffle: !shuffleOn }, { entity_id: player }); + break; + } + case "repeat": { + const cur = this._hass?.states[player]?.attributes?.repeat || "off"; + const next = cur === "off" ? "all" : cur === "all" ? "one" : "off"; + this._hass?.callService("media_player", "repeat_set", { repeat: next }, { entity_id: player }); + break; + } + case "mute": { + const isMuted = this._hass?.states[player]?.attributes?.is_volume_muted; + this._hass?.callService("media_player", "volume_mute", { is_volume_muted: !isMuted }, { entity_id: player }); + break; + } + } + break; + } default: break; } @@ -2601,12 +2926,17 @@ class MyMusicLibraryCard extends HTMLElement { _updateNavButtons(card) { for (const tab of this._resolvedTabs) { - if (tab.type !== "button" || !tab.entity) continue; - const st = this._hass?.states[tab.entity]; - const isActive = st ? ["on", "playing", "active", "home"].includes(st.state) : false; - const el = card.querySelector(`[data-tab-btn="${tab.id}"]`); - if (el) el.classList.toggle("active", isActive); + if (tab.type === "button" && tab.entity) { + const st = this._hass?.states[tab.entity]; + const isActive = st ? ["on", "playing", "active", "home"].includes(st.state) : false; + const el = card.querySelector(`[data-tab-btn="${tab.id}"]`); + if (el) el.classList.toggle("active", isActive); + } } + // Propagate hass updates to mounted custom elements + card.querySelectorAll("[data-ce-slot] > *").forEach(el => { + if (this._hass) el.hass = this._hass; + }); } _resolveImageUrl(url) { @@ -3183,6 +3513,12 @@ class MyMusicLibraryCard extends HTMLElement { return null; } + _makeThumbUrl(rawPath) { + if (!rawPath) return null; + if (rawPath.startsWith("/my_music_library/thumb")) return rawPath; + return `/my_music_library/thumb?path=${encodeURIComponent(rawPath)}`; + } + /* Parse the result of music_assistant/search WebSocket command. MA returns either a flat {tracks,artists,albums,playlists} or a nested {results: {tracks,...}}. */ @@ -3193,12 +3529,13 @@ class MyMusicLibraryCard extends HTMLElement { for (const [key, type] of Object.entries(map)) { const items = root[key] || []; for (const item of items) { + const rawThumb = item.thumbnail || item.metadata?.images?.[0]?.path || item.image?.path || (typeof item.image === "string" ? item.image : null) || null; out[key].push({ id: item.uri || item.item_id || "", type, title: item.name || item.title || "", subtitle: item.artists?.[0]?.name || item.artist?.name || item.owner_name || "", - thumbnail: item.thumbnail || item.metadata?.images?.[0]?.path || item.image?.path || (typeof item.image === "string" ? item.image : null) || null, + thumbnail: this._makeThumbUrl(rawThumb), can_play: true, }); } @@ -3831,6 +4168,38 @@ class MyMusicLibraryCard extends HTMLElement { } this._attachLibInfiniteScroll(libEl); + this._attachLibDirectionLock(libEl); + } + + _attachLibDirectionLock(libEl) { + if (!libEl || libEl._dirLockBound) return; + libEl._dirLockBound = true; + const THRESHOLD = 8; + let startX = 0, startY = 0, locked = null; + + libEl.addEventListener("touchstart", (e) => { + startX = e.touches[0].pageX; + startY = e.touches[0].pageY; + locked = null; + }, { passive: true }); + + libEl.addEventListener("touchmove", (e) => { + if (locked) return; + const dx = Math.abs(e.touches[0].pageX - startX); + const dy = Math.abs(e.touches[0].pageY - startY); + if (dx < THRESHOLD && dy < THRESHOLD) return; + locked = dy >= dx ? "v" : "h"; + if (locked === "v") { + libEl.querySelectorAll(".lib-scroll").forEach(el => el.classList.add("scroll-locked")); + } + }, { passive: true }); + + libEl.addEventListener("touchend", () => { + if (locked === "v") { + libEl.querySelectorAll(".lib-scroll").forEach(el => el.classList.remove("scroll-locked")); + } + locked = null; + }, { passive: true }); } _attachLibInfiniteScroll(libEl) { @@ -4573,6 +4942,14 @@ const EDITOR_STYLES = ` .add-tab-menu button:hover { border-color: var(--primary-color, #03a9f4); background: rgba(3,169,244,0.04); } .expand-chevron { transition: transform 0.2s; font-size: 12px; } .expand-chevron.open { transform: rotate(90deg); } + .tab-item-type.custom_element { background: #9c27b0; } + .editor-row textarea { + flex: 1; padding: 8px; border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; font-size: 12px; font-family: monospace; + background: var(--card-background-color, #fff); + color: var(--primary-text-color, #212121); min-width: 0; resize: vertical; + } + .editor-row textarea:focus { outline: none; border-color: var(--primary-color, #03a9f4); } `; class MyMusicLibraryCardEditor extends HTMLElement { @@ -4640,6 +5017,7 @@ class MyMusicLibraryCardEditor extends HTMLElement { _tabDisplayLabel(tab) { if (tab.label) return tab.label; if (tab.name) return tab.name; + if (tab.type === "custom_element") return tab.element || this._t("editor.type_custom_element"); if (tab.type === "button") return tab.icon || "Button"; return this._t(`tabs.${tab.type}`) || tab.type; } @@ -4667,7 +5045,7 @@ class MyMusicLibraryCardEditor extends HTMLElement { ${this._showAddMenu ? `
- ${["player","search","library","settings","button"].map(type => ` + ${["player","search","library","settings","button","custom_element"].map(type => ` `).join("")}
` : ` @@ -4682,6 +5060,8 @@ class MyMusicLibraryCardEditor extends HTMLElement { _renderBasicFields(defaultTabOptions) { const cfg = this._config; + const navPos = cfg.nav_bar?.position || "top"; + const navAlign = cfg.nav_bar?.align || "start"; return `
@@ -4702,12 +5082,31 @@ class MyMusicLibraryCardEditor extends HTMLElement {
+
+
+
${this._t("editor.nav_bar_section")}
+
+ + +
+
+ + +
`; } _renderTabItem(tab, index, total) { const isExpanded = this._expandedTab === index; - const typeClass = tab.type === "button" ? " button" : ""; + const typeClass = tab.type === "button" ? " button" : tab.type === "custom_element" ? " custom_element" : ""; return `
@@ -4724,8 +5123,55 @@ class MyMusicLibraryCardEditor extends HTMLElement {
`; } + _renderActionSelect(tab, index) { + const actionType = tab.tap_action?.action || "none"; + const allActions = ["none","toggle","more-info","navigate","url","call-service","assist", + "mml_navigate_tab","mml_navigate_section","mml_control"]; + let actionFields = ""; + if (actionType === "navigate") { + actionFields = `
`; + } else if (actionType === "url") { + actionFields = `
`; + } else if (actionType === "call-service" || actionType === "perform-action") { + actionFields = `
`; + } else if (actionType === "mml_navigate_tab") { + const panelTabs = this._getResolvedTabs().filter(t => t.type && !["button","custom_element"].includes(t.type)); + actionFields = `
`; + } else if (actionType === "mml_navigate_section") { + const sections = ["artists","albums","playlists","tracks","radios","recently_played","recently_added","recommended","flows"]; + actionFields = `
`; + } else if (actionType === "mml_control") { + const cmds = ["play_pause","next","prev","shuffle","repeat","mute"]; + actionFields = `
`; + } + return ` +
+ + +
+ ${actionFields}`; + } + + _renderCustomElementBody(tab, index) { + return ` +
+
+ + +
+
+ + +
+ ${this._renderActionSelect(tab, index)} +
`; + } + _renderTabBody(tab, index) { if (tab.type === "button") return this._renderButtonBody(tab, index); + if (tab.type === "custom_element") return this._renderCustomElementBody(tab, index); let body = `
@@ -4797,27 +5243,6 @@ class MyMusicLibraryCardEditor extends HTMLElement { } _renderButtonBody(tab, index) { - const actionType = tab.tap_action?.action || "none"; - let actionFields = ""; - if (actionType === "navigate") { - actionFields = ` -
- - -
`; - } else if (actionType === "url") { - actionFields = ` -
- - -
`; - } else if (actionType === "call-service" || actionType === "perform-action") { - actionFields = ` -
- - -
`; - } return `
@@ -4832,15 +5257,7 @@ class MyMusicLibraryCardEditor extends HTMLElement {
-
- - -
- ${actionFields} + ${this._renderActionSelect(tab, index)}
`; } @@ -4871,6 +5288,17 @@ class MyMusicLibraryCardEditor extends HTMLElement { this._fireChanged(); }); + const _setNavBar = (key, val, defaultVal) => { + const navBar = { ...(this._config.nav_bar || {}) }; + if (val === defaultVal) delete navBar[key]; else navBar[key] = val; + this._config = { ...this._config }; + if (Object.keys(navBar).length) this._config.nav_bar = navBar; + else delete this._config.nav_bar; + this._fireChanged(); + }; + wrap.querySelector("#ed-nav-pos")?.addEventListener("change", (e) => _setNavBar("position", e.target.value, "top")); + wrap.querySelector("#ed-nav-align")?.addEventListener("change", (e) => _setNavBar("align", e.target.value, "start")); + // Toggle expand wrap.querySelectorAll("[data-toggle-idx]").forEach(el => { el.addEventListener("click", (e) => { @@ -4918,6 +5346,7 @@ class MyMusicLibraryCardEditor extends HTMLElement { const newTab = { type }; if (type === "library") newTab.sections = ["artists", "albums", "playlists", "tracks"]; if (type === "button") { newTab.icon = "mdi:gesture-tap"; newTab.tap_action = { action: "none" }; } + if (type === "custom_element") { newTab.element = ""; newTab.element_config = {}; newTab.tap_action = { action: "none" }; } t.push(newTab); this._showAddMenu = false; this._expandedTab = t.length - 1; @@ -5018,6 +5447,19 @@ class MyMusicLibraryCardEditor extends HTMLElement { this._updateTabs(t); }); }); + + // Custom element JSON config textarea + wrap.querySelectorAll("[data-ce-config]").forEach(ta => { + ta.addEventListener("change", () => { + const idx = parseInt(ta.dataset.idx); + try { + const cfg = _yamlLoad(ta.value.trim() || "{}"); + const t = [...tabs]; + t[idx] = { ...t[idx], element_config: cfg }; + this._updateTabs(t); + } catch(_) { /* invalid YAML — ignore, keep previous value */ } + }); + }); } _esc(str) { diff --git a/zigbee2mqtt/state.json b/zigbee2mqtt/state.json index 0ab1ce18..dd81a6f0 100644 --- a/zigbee2mqtt/state.json +++ b/zigbee2mqtt/state.json @@ -4,7 +4,7 @@ "state": "ON", "led_brightness": 100, "countdown_to_turn_off": 0, - "voltage": 121.2, + "voltage": 121.7, "countdown_to_turn_on": 0, "ac_frequency": 60, "power_factor": 0.14, @@ -15,22 +15,22 @@ "latest_source": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/SmartPlug_Zigbee_PROD_OTA_V101_1.01.01.ota", "latest_release_notes": null }, - "power": 0.5, + "power": 0.4, "linkquality": 102, "current": 0.03, "power_on_behavior": "on" }, "0xffffb40e0607af27": { "state": "ON", - "voltage": 121, + "voltage": 121.1, "ac_frequency": 60, "led_brightness": 100, "countdown_to_turn_off": 0, "countdown_to_turn_on": 0, - "power": 2.2, + "power": 1.9, "current": 0.1, "energy": 32.61, - "power_factor": 0.16, + "power_factor": 0.17, "update": { "state": "idle", "installed_version": 268513381, @@ -45,9 +45,9 @@ "state": "ON", "led_brightness": 100, "countdown_to_turn_off": 0, - "voltage": 120.8, + "voltage": 120.6, "countdown_to_turn_on": 0, - "energy": 65.52, + "energy": 65.54, "power_factor": 0.2, "ac_frequency": 60, "update": { @@ -58,7 +58,7 @@ "latest_release_notes": null }, "linkquality": 138, - "power": 45.4, + "power": 0.2, "current": 0.01, "power_on_behavior": "on" }, @@ -74,13 +74,13 @@ "led_brightness": 100, "countdown_to_turn_off": 0, "countdown_to_turn_on": 0, - "voltage": 119.3, + "voltage": 120.1, "state": "ON", "ac_frequency": 60, - "energy": 132.57, - "power": 95.9, - "current": 0.88, - "power_factor": 0.93, + "energy": 132.59, + "power": 101.8, + "current": 0.96, + "power_factor": 0.94, "update": { "state": "idle", "installed_version": 268513381, @@ -95,11 +95,11 @@ "led_brightness": 100, "countdown_to_turn_off": 0, "countdown_to_turn_on": 0, - "voltage": 121, - "energy": 63.35, + "voltage": 121.5, + "energy": 63.37, "state": "ON", - "power": 40.7, - "current": 0.47, + "power": 48.4, + "current": 0.62, "ac_frequency": 60, "power_factor": 0.69, "update": { @@ -114,12 +114,12 @@ }, "0xffffb40e060895b3": { "state": "ON", - "voltage": 120.3, + "voltage": 120.8, "ac_frequency": 60, "energy": 8.79, "current": 0.01, - "power": 0.1, - "power_factor": 0.1, + "power": 0, + "power_factor": 0.11, "linkquality": 109, "update": { "state": "idle", @@ -136,7 +136,7 @@ "0xffffb40e0608864e": { "led_brightness": 100, "countdown_to_turn_off": 0, - "voltage": 121.3, + "voltage": 121.8, "energy": 20.09, "countdown_to_turn_on": 0, "state": "ON", @@ -172,10 +172,10 @@ "0xffffb40e060893d8": { "state": "ON", "led_brightness": 100, - "voltage": 121.3, + "voltage": 121.8, "countdown_to_turn_off": 0, "countdown_to_turn_on": 0, - "energy": 3.68, + "energy": 3.69, "power_on_behavior": "on", "linkquality": 142, "current": 0.09, @@ -187,12 +187,12 @@ "latest_source": "https://raw.githubusercontent.com/Koenkk/zigbee-OTA/master/images/ThirdReality/SmartPlug_Zigbee_PROD_OTA_V101_1.01.01.ota", "latest_release_notes": null }, - "power_factor": 0.84, + "power_factor": 0.86, "power": 9.6 }, "0xa4c1380d0679ffff": { "battery": 100, - "temperature": 27.4, + "temperature": 27.3, "temperature_units": "celsius", "temperature_calibration": 0, "update": {