From cefd7abe8a28a808324f8be77dfce4bc75898478 Mon Sep 17 00:00:00 2001 From: MassiveBox Date: Thu, 29 May 2025 22:52:44 +0200 Subject: [PATCH] Initial upload --- .gitignore | 2 + LICENSE | 21 ++++++ README.md | 21 ++++++ app/FyneApp.toml | 8 +++ app/Icon.png | Bin 0 -> 4480 bytes app/controller.go | 87 ++++++++++++++++++++++++ app/main.go | 15 +++++ app/model.go | 156 ++++++++++++++++++++++++++++++++++++++++++ app/view.go | 141 ++++++++++++++++++++++++++++++++++++++ assets/castView.png | Bin 0 -> 11310 bytes assets/mainView.png | Bin 0 -> 24310 bytes fcast/connection.go | 78 +++++++++++++++++++++ fcast/discovery.go | 57 ++++++++++++++++ fcast/events.go | 56 +++++++++++++++ fcast/message.go | 47 +++++++++++++ fcast/protocol.go | 76 +++++++++++++++++++++ fcast/raw.go | 54 +++++++++++++++ go.mod | 47 +++++++++++++ go.sum | 161 ++++++++++++++++++++++++++++++++++++++++++++ 19 files changed, 1027 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app/FyneApp.toml create mode 100644 app/Icon.png create mode 100644 app/controller.go create mode 100644 app/main.go create mode 100644 app/model.go create mode 100644 app/view.go create mode 100644 assets/castView.png create mode 100644 assets/mainView.png create mode 100644 fcast/connection.go create mode 100644 fcast/discovery.go create mode 100644 fcast/events.go create mode 100644 fcast/message.go create mode 100644 fcast/protocol.go create mode 100644 fcast/raw.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1296d0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.apk +.idea diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..08fce05 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 MassiveBox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..08e6f7c --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# FCaster Logo FCaster + +FCaster is a native cross-platform application to cast media to a [FCast](https://fcast.org) Receiver, built with [Fyne](https://fyne.io). +The protocol logic all packaged into the [FCast Library](https://pkg.go.dev/git.massivebox.net/massivebox/fcaster/fcast), which you can use in your own projects to build a FCast Sender! + +Currently, the app only allows you to stream from URLs, but I will add a way to stream local files and Jellyfin media directly. (You can already stream Jellyfin media by copying the stream URL from Jellyfin and pasting it in the app - [detailed instructions](https://s.massive.box/fcaster-jellyfin)). + +If you're on Android, I recommend you [disable battery optimizations](https://support.google.com/pixelphone/thread/299966895/turn-off-battery-optimization-for-an-app?hl=en) for FCaster, otherwise the app will disconnect when your device locks. + +## Screenshots + +| ![Main view](assets/mainView.png) | ![Cast view](assets/castView.png) | +| --------------------------------- |-----------------------------------| + +## Download + +Head over to the [Releases](/releases) page to find builds for your device. You can use [Obtanium](https://obtainium.imranr.dev/) to get automatic updates on Android. + +## License + +Both the FCast Library and application are (c) MassiveBox 2025 and distributed under the MIT license. Read the LICENSE file to learn more. \ No newline at end of file diff --git a/app/FyneApp.toml b/app/FyneApp.toml new file mode 100644 index 0000000..893a05c --- /dev/null +++ b/app/FyneApp.toml @@ -0,0 +1,8 @@ +Website = "https://git.massive.box/massivebox/fincaster" + +[Details] + Icon = "Icon.png" + Name = "FCaster" + ID = "box.massive.fcaster" + Version = "0.1.0" + Build = 3 diff --git a/app/Icon.png b/app/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c97fd76d10115d3a0b4744950e561991d8e0bcc9 GIT binary patch literal 4480 zcmV-`5r6K9P)005u}1^@s6i_d2*00004XF*Lt006O% z3;baP00001b5ch_0olnce*gdgAY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy24YJ`L;&^x>j2w|fhwN>000SaNLh0L04^f{04^f|c%?sf00007 zbV*G`2k8YE3nC8ejg%|^01(7UL_t(|+U=crcopTn$3O2O1Of)a77(~)Q$#?BwxI|_ zf>j^|sfcnFdXe&oJc19B2wO1{wmXKph|j zNCv6_i9mw!%_~4LZ~-U+&HCxN3t0dNS&1M<<-chcCpR2;)r4@d{@0NMj>fmSyC z`X1m*U?;F0*ny^w34pN;Tq@8F$N)M6X)d!c7svs!f%RzWqyQNG(4_%cKzATB%Bg1t zzRtjCptmq$i&g+r&{U29==D7bc!5U3g{?XQXlQC~d;;k8H2}r{PYDZ-YBn$#O&yOP z0KL9Wz*uxo6_N8=1x!Lyn_?e8ukRsX9J&Lj$UXWJn24s{k6i%uv%PUZJ?U>(s?dON zcI*M@^}PU$2P#QFyIQ5d1T^(hYyhbH>Wv4Um%fcEO*KHg7`*^`eJKWfBcwm0QZoz) z&qN=9I(!q*oe4zItC_&~NI|F~IPGdO^2O9h=#9j5?1_-UGVR8eMi_mH`vKHK|KwW<)^=-)#; znGw%w{ySLreoqJiM56^hk-@-dg9+Q#fTNHABGEPVHkhzs3fALxmm{H*B7P9^d0(J$ z$H9z%BLzNDIZ8Izh~N~gziN|=1)_>{pTS53reHl$(8oxw77?boXntljAyObC;4=cB z$bwn~;)JXM$j|o)k|Irn<r~!DCz~)a^uR{KD*!TrJ^9dCB95tt# zS-M~=Bj7Nnte?~t9_s=1YYSLbiFwIAQVfUeLdrz!#PgQNaUu!Lpw_X2Ou* zj$!m7FoW&3_SK(2uR8^taI1GGnBBpVpB`+m6z~XWdr5@M>hM?=DA{O)T}r`_9&ooa zrw4-q15ghvA85Cg<9b51ssc--lvEYG_d4>^0}ZAu?}8u3=GtqmD?_K-tX*4r6^bqh zP-T@|&Dxr|OFL-20(O_!YF=Z5skf~H*x&93e7qf0sce46wR#Io`3GbRfZtO_HSpd8 z-P84>n2J;-5r(#d=eF5yV*m00^i)u9*Y=X99a@@Svv3{sS|RY2r|b)mwG0-0Y@w}h zcErS<23w|Uz`jmcSG$J)&yRltgEk6iX8NBEaN^t0+B$;jYX!hQPFPl>n*Yxa91*yz z>R$z+wRHs5$3K8xUm6aTxlc4%|6M8;u&nCaBG;OhZZKBi2q*#OF`!NR{eQl{0H*`m zD+I8+bYJvq%6`b*SN>;3=VAIfN6qVAK7d|dDv&AN7aJP52sZ41;xgxb2MXZ1`LO(q zqo!pVtO4IY2khqN2JP4ejx{6fzH^r&d?0(N7kv%JfqhrP^SirCpxXz~ z@*~Ks5BWu|a8xbJ+I!annBisst0(Dyv8uF|aQE#n=uhyEbK$%1zALcLeJ2PR{|2z^ z8nClWT+XUdJ$U(nxOmjg-(LfIef5AeLBM&ny9v@`-ytK-VD3L7V7k*5m6i&8011_# zU3`Qfq?ZNo4yUa-{;j|VaHSM>d=nRsdPi9R+dFN|ho7477UMxX_rvzfaq+0_%L3?l ze&>mMV8L3rawUG@JA49u^S{JXo@o?bUlrhzTde9=8y>tJ>Ze3DD5;W1_l=9Bk`k?j z7hEcaop~^6GvvkBKVVgb*LNH6g-m`TCpP^apt-bz$RRcN08OPGL=LH`2b+VBM4~ck z>;W1|JBS=oLl2ND?I6Ora6~r_|OShw-*)_O0UI`k`-Rxd7zrpmb6QRl}|uplNIUdA~<*yw(N!1cfc1CEf>wY zpz!)iu%0VAV)`fxe$gec?_4f{z4@>%7hcSDJRPkla9vS&-Qk0B=xJyYLiyuUXJGvf zc=;39dP#aVTI8A>q*X%9!CyB89_j|`e*+(Mhn5M_xzPfkEBr5x6oxN&sb5#vJQC(f zrd8KZ!5+XB%lP>l!|MxJs|GyY8@?I}{p(6kyHv#z)w}7;pbRdJ((Lxxx5EjdUwi~oV;r0C;xF?#de&WC2!E`-cM=(jv&zytb zy$i1w2muajT_rMJ>dt>NNf`1G`p;E>jMfT*8{c9%fk)zI%% z_d6?lL{vGO#l&O-XT@a9VEY1eK@f~miRUNwaV2Xx2-zB~Dk`Q6D@;pGk8;@Ojgjh=8wZqVFF63Fvjn}Lp%ehdI!+hzqHFw_te|iks->kz2x?@3FOZZh5Ec-3&8UkZ)34DXn<89cJZ>^%_ z(4NpSN$9XaU&1wates9Z;5(3i-2B>(iQiS=unG=Q;>z~%vZjbSU=ei@!v0B0@aU%s;~jJin(iLkbp1#pMc z)&!afODXti8(8)%Xt!BErkVPoo&?hD-jClaghW_duK}p3V}PSe?}NvJ`(AQ&7}ejp zyVdXa!HP}h*R^O2Q`-wA5mGLiD!kq$KgUUn=6nnnF9kn(4(|Pd$#&ABMTPLfLO6CR_;@vO1r!&UyIP$+3iqe`-vHH3fn7Uc&qbjo zOxl2^=2jGzxz=sgzIg;X{RN(U!}@Ergar8Y{pO*WOHRS|z2@u70Y4UM!l0BR~1<9x=g>b(;F{5L4QYGG5- z0S237TbO$o@{any=0^?SAxUCKSkC@*#{6bRSmySp7`h4GU2APB9-3)>-KV?F*Ok56 z2t8rYGApS$mPW&q{3=^7$ZV#cw1Rt063{K!W(7X}UucNzPjd=v#^Aoi^W*2;I z;%1dv8wS+#f6cao<~DYef)>H5z+lQEfSS4pEVR?YSGvNjP0QZ^Ja890>Ss5*@-qv& zXA_T^)d$S)aZ`OEDM&3em?0BpHBF^{wspmd>I~13Mwu#8Tpy#Car-pJA z6RZ{lo}f}wyMg(3Sy)CG9G*s99Fv3$#wiOyGdDt;j?N3|0s)fSSq&W=Q*rOfr`a0L1Y2c)J9;Mw72LwO*7cArC8xKV1=}~ z$k2*l2c_Tu)KoDr1vnyYANM)}Ofi_Sv0SUBasW-*KklU&EMR}VU_HWpw)8>VYIfNy z2Tm2No{WnF+C@Y%z0Vz7Fdi5o{TRg~ zpQg@4>IYQBAHnNOG9Z*SVYIFJA~^Ed2jDdjUob$Fs=T=VsAU40ddZF-Q?@>u*Y^}K z4yY&njGGUcZF|43Y=1NFDDwrG&$Aahr@EV9u0$!kzuz=m3%vz}ae9;e}4q<0tG|*ePz^)jM zDWP~W#}irrw4a$OU7eO-Z(Nboyh4;qK*!Guo67l7YC zGaLc55OxSPSIZ1L&-P*iKpny+z%#(pNNPBONRc)Rm>Ec}76Gv0{Di}?DM===k(GXq z)f8z0pmh*4fnmVI!Wb157@CKsHbjfTDk_`V>&pO!0E2}&Krg59mRUk@0Wka^wl=2a zfk5LpEPu7o$i=)nS|38iL|5oOhYth&fu6*A)k(eZ-{MI7>Is0s!Po}q3-kffiIqy@ zda>uF<&``Y0No2?8oI|#cOWy$tlt2v1y)^O=3W4dP8d^xZa@am8Ax-Pg}FcukPWOy zQzzr0TjB@69|Bp=n4<3h+M_$jwzBEh_n~FvOdG@pSem}obktMuCbq4~05X#HFP=i3Ob%3`D z?sf1>P(#(*S$>{X-sUrP12e9^|)A zgCft>*q?MILb#<8>ujbZIp_!rW4FkJDd#`WK^g z!0=?Rnye8!#^24qd$qXIRGH%K$dJ)inIg)i0+-`ukrXQqzL}XM63QYkN^?M_BGY7% zYjZ+X&RMqn#IH-`U15WwomMRTa>xIb@{{5|E+o<9olIK!^V4*q56{9hn zDpZn1ddy;%jvs_7_`NGxXtz%ia#yKirO(qPzSy z9W}3e=g}KFl(TXVwJ58WpHy?;e&uI7PuehfGCR6BnviE^AARthn=6!iuD0mUvXv8u z3iPea%^MpzqKjX?e3>ZZFjnczsNV9l)ckc+=2dIcKL(`P^#m@=`9x)Qkr_)mIIQV~ z6vSwYucefitHM(2%CbXqlnvL4kuGi(9y?|7;q5*)ohfv*tN$EEF%CZdnBy0azL^jY z<%YMJV+bsCYotBmPCDJA14218NuaHJUytRWxD588Q~WuBW5W#-ub#djR&kv05i!c| zarSajdG$J#=%a*;%;vQ0Z~%Py*XtEr9L1*H$t`UQdi{#?8TIItKUQ7aTc&LYFDg&_ zy~1rA(p&AzwMA%W$?DqrK$o`_mz~TF(v1RMyu=9wudDKC|K_Ip3%6x6U25FD0TmJ&=;N)GNg zE`P#f#8FNzJ$<8LX@Q|P-l|kR-H+&D`||-#r=&eXY*bSt+f~lf93bMpf_GWmgEw^= zL;@XYzOW>sFq)8Hd_5FiEjOa2$XWQBE?*DHE-5)VH612zgdbNc%Zd_NvM#kKep@Y|s;$uQioGJGbmO*b|#vWjj!!O_BJqe0^i;Z9I>F zxMPd6_z{f+>S(5|_AHRVZISW;b?J+fA7foc8WzpW=3gvcsHRX|RLEIdVze|YttiHh z{bq*Sl6bfjlzp|*J#bs5wrE4n0%^b8mA^kYO3^hryT3e%o#ye3S_|l8*2QJ2^%^WdERS(vhpxqMZvFDF@go@X-K2>}go#LJ+j@amlQn~-wgNFYe%wVd z;g4B{j)0Ndz1t*~()J$&=18nx_I;Now--55Qo&ViWp7j~e9p!zBxo5;gG{oZ6d{Zg z=_pE!A^OJV#?>(*jK`pPbjYK3y1D(UK|ebh4WpYtZh1H!;N#$RD&Le+_dFNH|np9Z#L(S)YQk1XZ&jQPBwv|fEMz`Y zcyQGfPd8ibCZP73_9*BARPJG#L}gS>el3hOw53-VTYt?`#BIQ`7fE5;9tT4k`n;ZL zt*shxxAMdYlyMOf-<5WH(@sSfWjs<$iwqOLsTV1+RP>zU(-nnr*fRFf0M^<4D_64E zI8|}w?T=z}Wvrs4w`T81UPvhE29Z1}c6a_YU~*S2KJ{BH4|WH|`G|r-Q7L&tc_gSQ zHpjRs0n2Z%)!k0l z`t_M-x(rhA2}*Odw~q9F_AXi|(A2C04iDvHBq}s?JvqGm;2+yDUaoDR$DknQx|af% zV&E+7)bB@cdp`NDtr@%2M0c{|6`S;!E?I=M*0!S@-Hbi|<*jEm@ogHk`=9RV95 zaps21e+)igsw;%A);vhpK0A~S5c&CRvO~mOFPh7s(5PH}NH`xm849y22}WppR;SjT zW;Smq#GJq64J`x_=^P+*aE_No;($Z=Un}mgh#V08%D<=y((J^FJ>BSx&wcq$c z(wCkAtKaV=q-sl*?zY<|FI>*gubiOiJp0ty7S`2gt#y3t=wWMmLrIr|Ca>o}HuQvAwohd)q)upXJi5z08LjQLuHJw<#tD59PfjCVEHYAer!SZhn&I;rX8>DlU&F zn8uhD`R<7`kvAs?NCWP|b9ZcTJ}aR$O2^>a_QSKuDemh5{?x1r3LJ|cKT7-c&0rlM z){+b~SlR+4!+U5cB(W7PG0L8z!hdo@2WPG)Ps&Jh7^g#D;a4RG%q=y_!Vlf z^Ki2%%C{Hgf7jA(e<4Fk2mgb0(c#nrDoRiiT~cBqp4ueioqJRiKVC(3$BwvdeITgc zmwYg_m}(NH<=yWXXvOLjljza1xM=AT(v(JNmC7H;M2{Pgrx~;1PX9r(21VY2 zq~S6EX?qt+4te`1h!cq035-kF<|AAPBAf!NE!N)$md`{?+lk z^~;x49|?(QmYwYE#(PWzx;7`PPy8;^pNG&%DmePsIXXIiXCIb%e2<8dwBFKgLtGio zy(uLnSvE*jj|fM24{mR7AFK`YN5qe^!`s1iwdo>+1I~jgD@BMx=CRnA)YR#oXZ91v zotPK|os^%>UY1I-7$p^z<4Ex{_86hEQOnnkj+@KNPuy1r_T7{NuMK?YUZNK%M#~b@ zE3>6z`t{+(6E}E(%Wu-x!1_ZXuc()y_-kG zb@8k1z}VPW!1<1BP-J8z$*)TD;ljxv5+|o3m=q;D39jSRR}tqqm<4wBPsq{gAfime zZ>HWU@58|GFumYve^;_NJXYcNpFd`%rg6KxW-pEF{IXn^x<1?{ta4lFi|4nhsj0b5 zuw7bK#{Rff+Up4?BV)!TapS~<-$Lu%pvay1mY7%tA*ZR1ng-x`Jw3gJ#l_3R@rE2& zD)>S~cb=Y@sPFE6fNj?X@zY2%Rb)JUH05gyg9&qRtPSM$i;9ZwEp`g>@g@AFP4n&) z$)=_zcXtPKa{;R^`{&P}XYcZ$&mWO~CLkn~4W}7JmFjzYHwEKU0Dlaj0|SA^Mj-gT zwqFNBop;T>%+1X&oG?vI79dQuv|@8}ZA?r|Fvjr)+@8nVCR02cqCgJ5#T`)znNC6jp(12TvLA0)m}Umgs0} zTAG`C^JH&H+;j7RnA_t$wb!p_=9)r*5y=S&ziWc!qgi6%aM>3xMn*+gSYe07AIf7og&YQolzP=u;Lt|r5lPSl&JKzaOKgUhWyYmVOS=P<)$5`;ir>D>M zX2@P#;4m>WO9oy}U@%`u=GMlYCac_|S(Fvz_I*?G9!JAb@mCP61G zCN|dF%OG){i9`zS^-b0KvcOW#mwPkz_ucgMsaZU7l;V@%a8F)zjFte5#HgEvcEwB(_JKKs^K>6t7=c+kg>d}Jf6Ftx*tGmG(Z2MIc- z^^so<4g7@$C@3zZeBDwhasN{{!%l94<0b&W;e6faxT0>$+kgx~nACdhkV;ES z!(cGu=?cxIJhfc6xkmYp`Qzi`y`}E>xVU@w@7KC*LyBT<2jxdc_kX&}7}wI|x zg~rd)vR@xKZ_hLUv?fUh)~#@KO;&rNz4=Ql#g8^8uXs$_!l;F_Rg!@sTwCn|EVnjP zfXM-%O?+Y^!I0_e8R2`hIcPvwTw1y|TAC9ajCy6QfO{ABSlN#P;sCo5iw^>U&$2^j z(W*U`tHTcv3u*`h;HXpW5%%<<*Xh2Eqa&UA<8N|OC+5rL4$?Po`qA8VHfxQ1n}7qz z&*aVa{>;qE<>jR<53e{0J3D)(TCN5=7aQBj;kXn2on3E<6b*LuWLkJk3@`D`>&d&j zcaQ-7w2L<0-l$#Xz^iK%+(}!Ylz;Z@nTw0rA>NZUx;zKIrA=6>l%xe_0fWgaE8Cx& z3uN^Hz@>Zgx*iY;1!r8~!D@`-WR>6#SNmdqq@_>V#eP9)qB(ei=Nj65{3OKRP@#HHuQmVpTc!o@-1sYYEp3*kKJS*14jq_Hnm1 zoV2jA;<)#PL%T3f`A0BRoi4)8!lKR3YUzy#G(FOrjgj%sWNm0kc4;X;0~7zAO=t6m z4<7*kp>6srlnh)%l0F7d0k#D%6_s$Xvg+hSbP{phOJVfV@4Us{%sj;^GBeK^R(ouW zmRen$y5my>@OrF^KyejMo>@Odx`4Byqs=KSBTCYdqFBf-JJAYM#ZOznln2L}fy zCX!BsY)wqM{4!`HeeS7G4-E}9G`#cXB!@q7?Z$w$$;(qoHmbvxW@avcu7o0-+h(|s zmLaHR{^aMc;(9xm9UNF?#%ge{d6turOxR&fW zWoi2tKKWl-vAcbJeR+Adi#MqvT3p@Ej~I9N_a|yj^s(|a38}XeJ3XA8RXsM=*4k^- z-#wjKK6aY%*ccle9)^>cT3QOZuYTvZ>f*aEHM5eh=V(09eIJttt!t&zc*HR;ZsSu)2snn}Z;pzR2aoOkyqO<~A zwbs~}m@q0qkcj4i(rIgpxI9^!1zN@4-X6H;uh`6xjgWaUF@kb!IhuAdS=HCor4{!O z5EEnP+g+qE<}|2$b2pk#zuexi!f~=d|J$FBga~vCvtq2JrKM4=4|a31va#`u-|{9u zw$w6se|eUSf`XcY0>qy;$T8UxKz}eXnPV|Ayu6;96DUJN!`jyk@AdRAFVD6v&d-1R zaFlsD)zRG@5gKZS@jY0*PePJbR8-W4Qv&dnnVH!R=b1zYZk-<30np6O24-bv%YbC@ za*BW`rOthgSV!ubi1X;SVabjm@$3*BMD>HMlr0?cHuDYsf#b14~GJ25_AyraV zmX9m~2_r?!on7MGTw|c1D|zxqMus+Ep?~hycy_cEMk5-P>#;MJ1;kl%i>+!vg7lUJCVjLWZl)CRRG0d^CvG?y|nVC(Yt(X{&An5D_YA*d}M*;`(kuUW=p-8aTGAI`KiHkR){g%}zCQ*6EIcl_T65&t)}*}qkP|78*P zX}s?S#9ADrNuPsNlvxWAa@VFmTSi6(q!?sRWg7D=?EMYvkc^B!V5L`cXSTLd6B35L zGQQVJH)+2mBqRhtPl^FdhK#*Y!(xK?4*q}ob7P_`-oWuyQ3@vjO`VnCy&zr03E zEUsdT#6Ao&8%Tl>BI=AAW-J-pgK`zillE+$NvP zC!mtChZzCgGSicjlY{lqwMoy5DZjl==lNzn9#A$Ojr__00*R3J>q`bZTidY;r&NHs zcJq?e)m88z@Obti=n641GJ?C&2aK@9C}gRv5KG(pUR@j=?C;|;D8_P9@|xastpsHz zh+R+|WgAh$V3kx0rTp1f8TQhF-#}9uP7w zAtmX|(h;|MoeLK8^XJcm&V;x)4UO-f%RLRPtv}P}Z$o}+lIkX#| zYg8g$5QsZ+F$$Xg6Hzl%F zd4fe>zI?ImdhZsbCuq?->(yi1MNLq+0Bu7?b*ky*RjJjSq1vjz6xE*t z3keFsy?I?yvKCbM;CbYkqIP$8x3|$i;1^_MWFQdYR$c4Bx8J{i!ea=f;2|O)$j?!> zva%v3CKgIm#&vdaNf34YSUyGilpoB2sIo9UxCu*x0RMx#e@o@7OXH*UQJ^90_EK5G zW&8b0($kIcf?0NUJVC(-dMbIE#(Y@2q5R(AVYII>H#fJ6wzgB#m-|X5tf>T)PwHXF zTjXoONK(+jNe#SY123t|&9>;kDHK_$S)dXB5 z?8<6uSFnAs8L7orQc?orOJ`^2P8aB>)zyzElsBmL6pQTd-U;@TDoUFXEmgR_{iScA zL~o)lE#5}5hzdve)3qL=iHEmsgClSCP))3o*#0x+4MKl~{D*GYf615sxvlnZ2j*Te z$dHFNEv+rDP?z_B=rYo@Aa&N%3?55u@;6~=H~pm)L@uWTdHb@bUs&L-Ua=16;BN|O zZ1=Fuq3ShA8Jq31Q)sIxm`L}$uzmK0%=o|W>HXWy8O&$z3;B-?bT4%-?s#!YlDrkm zoesuNyLEBiPYe=f9BBfai$|eNVgcvowt7?yjX+1gcqFf^Y!};b-TEd2 zegUp)MCJGI1MlCzhiet)=O@ugD8gWHc^!ZM+SGva*&_9v;i9W--P`zScY`>~#L(v@ zR~@6Sm>l&S6&JTRpCGI_^biRCz~8+9b^c2WZD?GEQma5q3mfl?myIY_ijM)Hy;iz4 z4U1~a#9!PaOHO{xgPOf zBNgGC236(^3`{~o++Hu`OilZo(6m(I9s^NL_=W~acnoVpPMmCPs*(#8(?od3?COC>JR}}mA(|zE823Sk1X0S}zNsOPp?u0a!KMeRNTZZV=_|jkyqNapugQ;N6f`5) zy)KhT=9Y%hKYl>jqa8*|3W7fpZ(r{)Pakx4d$SGds{mXEpf&H}-UMB+i=|ZAkcjh? zr1O!sHs>)AYO;nkUZT9bZLO`=13$lQ8ctcK|GM$hOjj4FE_F9SuYCD6BthY^07KBH z7cwB4yb3K?Ki|apusVn{nwTz*=e^1XfhG>fH-1=|$;3q#n zzo4L?!;0!t>UWXBLkK(VBl4Br@$t2e6=}2ql13F@dAK$5;P7yNV_19qgZF3Y0AjNM zpTm6<1)rk;Mct8(57)-t=ivB@@fC~uu8$2~gcKy=F%Z*Vx}JG-q&?fBKi{Cx)`ly486Q8IP*hjrwt^X+_S4tV`MIGw*flj3?)^ILYRlO5$&(&!^1f!Qi&a;g z$9ZF0M@LQ`*3mI}>Tu&|FWgXXokK(f%()Q}bj%2HF)pmsz`0rUq)8vw%@S$B73*XTdQ0xkhL7`lOB3@U!YkQ8AGx zley$2qV@`tNTL4s*-FV`@+Sj*eNi3}d3cJryn=$*HzE|=Mn<$%LmeHQ=0gNyH{J^6 zf8*C>2J;1rD}9OO4@X1!lab%PD>a)L1#kVjY8R-q`9D362UL5+Gql;L2klB&q z5HDrxrA9a$ZZ*c@g1oQgY7BPW%WvbC{c5h!_TB)+> z>1i19)k6|6CVA7JO$bfL+qfo>c=z3(>|4t58OpX)o6=S%rd z-N7H9&Ve}%LV*W+205N<7YRJ+aY`|H@ZdqdVX4nx9`^K52iK}I|nqjP@>*2)Y@o`U&=A>kmD(zE)iOKsM&%ta( z10QtXg`K8c_DAsY424S>(e#46LM%ZeBRW8VC8P}*6mRRT*IDW}oMwBDB$x|I$r#+W34fOhJKtFKr(^DDV7PM^`RUgMG5ZLcentO)qhN*6@7e@L)bd+?G5$JL{r& zLw3VM1F?w8Mg)j^Y+4pxvni(o*`FX@(rGQWGl)c1HUwaLqCkHbxGZvXulPB6!jEqZ z)D)iDiy$9H{&E{nz@J7wPc?J^-tQ_dM#)j>xQnJD7titE${K*FDaUQ^(7% zz6M{nezcKvd_67w#50eHg@xCy zdHsU|TCViSF~w+542oW^;5xuhTMdpZ$jh_-_dk8Huo&D7Ug9KAEy~DolFLQ&eXh*Q z`?1Is+50b<<#BEZn6+BpRq?w9dG()e@h@Z9e=p(w=bpp=f8hU`TK|9d{J#Gsc^NLo}p1O#FP3E?;I9OE{p3^lRZW)OBI7;Q1Xk=7#7AdWtJ)AaRA ztwW&$AK%5;QEo=U{1|P(7XpztckbMIbN_y!eA)@FIPsDb(@D>gAk&+L_>;>zB5hLx zLqmh8#i+5du_tdNB!nV{!g?%7!`g&Naqb`>oO0iNgOxZVoFq@%aq#%zS1pnI!sO^T zYsz;J{&)nu9u)1Rof!vvVf;oax<4nM}@OZOtnebH6uC z_>xKbdFEsncvxBrfUaH^8@w$0fYm!{n|O#Gc<2R&7; zsWB$8cjnVLSs}!sVJl>$99+D!eeC=s{aJ3S+5~^CKojJmaKWo%r60>>T8I)dS{cDZ zsN7W}Sr(toWTTyZnR9`L67DkfqOWBEjl_X*gW=hj zXOi~6hj@w&a|`Q4bIgX737C4hsq=pQE>X_E9R?a!nz4StsJ2v0|NEq2|K7{1#kAYm z@zVI18YMOADfkPMhKcL-X5r#W4zV5G^^`wuIFjBJrRPph(jQfS`0NP*ZEKQEX~1d?FE*YwNbjsurw{?e^(Jw*HX{ zXUF6K%&kB!6RsMgoU2~~7ds-o_%$r=!|pA46vXn!#h1<5*ZKX@Tje!NUis5aF>d=o zOTVx6`HF+4T;>lu1i#FU+2jbE^K>N9Aa~dF7IkA|=`3M<2?_EwWLc%5Bek(K<{!C9 z`mfBJad3ss0=b-?*j9W^}nd_B58PbcLqf|wzVg08f%p7lfF%Z4W zbpG$ufhp3Cxfb7$Tl=A!OZUSj!;yBvR}w9a86j4KpK0rE9!uU_{3hbI{j@V(o1LA_ zSu>BAlb!9n+MDF{2yLFXy-4ICzJw-ae$>kbQtwYF`Xl}u6Mp1*+NbMT1vBHErh)jj zwl3=%t&2-;Oc_obwZknRH1zt_MrvZGGrTVump6?Jd;jAy(<7I0oF;PjA8uH?@&4X| zp09~RlJ4gv4YU-c9Cz3Kkf#(xOxo}9XT%Uh#3((d(|(ne$fV)(m1ba@h<7_ml{Og< zO^wOIRp;Kb06&dGhW9-d)b;{qaqn~K$F0pjef+ystKoVu=n|q)^KVKywxLU^!fkQW zi#5dW7q{Ncoxooa`r9@&_>sx%beDYE+kbg?-7pZnW#hTGCM#_^e$cbAv2iwRPgV~6 z`$xsXq8KOD($B5VS;~$&LCS|!4I8tPXhB_bV*UB~dltp9jX#t66RA0Y(KRm}aEKM# zU}rDUJ6U=lJ8|5(A^vJ6ms9ocd7E8VRer1uRY&{`jjXCZ*H zgNQM7&PbSnVXYT7(m!B9+;D9&G;F=sP-Zq^ZEYPC6lA|WnU$SQ6Y@Jzz+<}146j>J zaf0|`A(WLRGylqX6O$bECq<~-W?JYbVJW7uKW(>(E!xtx^k~NZD$7lW6YzBO9XyyJ z7_&Z)KBzlkJe{t$Y&NXH5mA!~mRD0B>mLeine5n07o^(l z#>w2#ckW8c_K#JJvBJY*1NSC;Ip%54lGDG;Qd*H`@RxZcOj>DAa^i(+DfO5=bqytW zu%#gX&PVLMiLr5+=@@B9@jPO+(}BTc;WI>U9}y*H>>NWHF%Ikj4wIvK<($R`j{Lcr zdXx8id?N#cVrS2OTN|}f%<{||p5Gj9L|~E#ij&U{@3*zsiOVPk`yr*x)4dt9?aeJT z*W&37d)?Hli;;pZ7~H1fKFw)4S>n@*{mbFRtw-BgY4ET-n00i$WP3Ver%Mo#M3{Ww zalz{8cLmjOjr)9~9nZ=u+Y z6WT3kvB=EN%|%B;MIXSlVdc9}PjpM%FE^(yotO_U9xLa_<^I`-5w=y^W7@FxCHwnq z@rGBys$F8S%TmlZ+s0*km^d#dWp;MvviC*z*x9;;55@~bk_xS*R?4~;FERZQnbH2a z+vK4>|2}NBNH5hQW8AUCpw=QINxK;PcA`$^kAS%9>8|K2Yn^HnK}MS=4}GLn2H3Y! z3#5r@b9zmRS&+C$g@;#{4KgHNBa+0_RaUace?~7jw6Uj76nu&G?RQyBijhI8tnmHh zS1W3&?k9zc7q-8!Fi4qsRPl->ZVnmla>-MXD`L^_Y2MwVdSh;$pOi#GnAm*qL|Ufg zAG5$%q$1gdG@}~3Q7L7t{5!qW>Gk-#^v+uE$fe41zw6p>oDi2#9pp*Bq)f$rQRePSt^f|^5#dV>p++NLIacI!6VhR!-jSZRG$)@}DdVs~ulN?#i%)ABVbp8-Y zdwD^>*TWmN@AIk)1Ad2%$X}kP_r$xBV7k?~u-H!wVxeU`Ki@f9yS%F95uZMA(40({ z#IO!fBqW&msYTK1YPZ|dKcGYzA)$a_4?7<*DW1mx7Z-PXyc8J?_ghEjSs5P-s&uvcHRQ4;6LiM5>hB~Hq z2lM-^BWo-V2!1v^_=A>REF*^Gotit3SG4eJtwJ()b>@ORjg2Wk>U9ApA4)H{lVD)O*D z6Gds34=InZ>KR1-Q!TuamVv%^ET zN@*_wRX2J@%FgFgue{mn5MmDFLyCBi^;F4em?g#ac4VqL@j%mlJz`KXi!pBIpL-42=D4wQZ`}|5_~(?{)^IDL^U@%m4S^} zV?MDrt+XeFC0BZ^YCN01oQut)x!V{0!=2MU=(^eK&NM#K zY2jzCAM9k@4D6{xTP=(+-zi&B>0w4olcFQCl}_fyDGCR8B!^gi0wLxg{b0woiZ-AXf^+LyiCmqAU3sl67*z2<;bw;sy5-uv$$ zD`F7>!mropdUp_Bd;Y6=7@a_h2tOeBMfrb#xc~Kf;(q{(@4or=;Q#Fh{O1eWp6l`0 zuKp@0kfVs0=r+*Rb={r)TVSH9k(-xyvORSm6QJ<+Etli&udiRf*5Hkgj(##R@rYnY zK)7zyds1|Dae|D77U<`QfqTwNO>MQ*9%-gGJUEEY)}BLNkHaJ>!^G?AM)2|RpS?A@=uE8tm7czwX#M~eqinirRz^lf zYU)#T^gk;H7=k-9_0J0cdty<_TH4yeBh^|p%2`ML^zmstbCj%6_uTngCY2_C8)5pbga@QiuZ;L;SZX0`rMbv$;p_Q zn6YBRFmduMC4PR&)*u`#0s=)XEt`$uu6(U(NqtL8%L=;<0~r}JTiednzLXFmfsg^Q z5d5_b7a3x73d;JSfdODg#>VU9m|6Mx`9(zv_)Hjr#>U3vm>4AQpc<;FQKKWG3ks9& zEw*7TEj_cCsf*`z>Q55#9nY7AQF{)(Sc0DpSKrFYdK~96YH|t~cE^Ph2`n!zu5N6Qc$buv zY|qrU8WcA)>Zq!zon2fEtl4^h3+(vJ^s(iibtmwUC(K8BPQ26mOC4wg+^mkfv#}f& z=jYl5nVGKy0)S^#u$BXo(8XFrOHb?!@kx)>A z{Qd3i?DkI3INdKtf6!}GXfnjh5;5!T20Zh?3$?d_Im?SJK0t7l{(W|^l985XHd*nQ z@?KWZ3k#~5iUpd2-Jgvf~@TbR+pBz%*I(QEOubsiV5Ei z`_S-kSMeT-J{trf-uZpl}=;-KRj!Q~Q69v%B9IDS;`EUEu z{5&S6zi>()F3e}HEb~A6wf?h4-%w8A7xN1XO*wIKaYaQ%O%wZ$yvWFbiW%3lmYmz$ z+h~NmmJSXpmYj^*HQ!AAYiqTvt&8-4D;=%&)uyM5X5%1|p!zn>S&r9^AbchY!B?zU z-`FV7sh^u;nxC$9sW@rXW5&gEy1elZbg^6O*Kd=ddWsY#zPML0&)XVGbaS|d<@H7k zk&TtLq)c>q6b_20P@~R-cBS>wP!4C3yyMO^AFrD3 zAXS0O$@bhH5)*DP16PG--`ZhIi>TFG4vXoafB@5z?z;%?np@H2yN5=*i)|4Yf=*6n z6dlyQl<0$R##V-osA@znVz2R^cXQICPqCgt0>Q?kh=|~C(*Y^H70^m zP7v{J)*!4Yf0eVdsW@&sV<`E1Qh0cHN=i!J^3KlA>gwaiTN4Tj3e3#Rsm89CoZ>1f z4%?GlKTS5pX>ODN-THD(Lx(vv?MMDW|oen6}LbwoS>))gk-*VI0 zKc3|l5i_+?!2tnK)}frfL5z%wf~t!~xS0UU=lk~;xaT!zn_tMp+>xI=fil<~%kg+; zI&FKjkU*>!*7v)2?`m9eBE$FDsaBZ+n?(@`1l(Qc8hrZt7@Jd~M>FISp*(o3bZ`Cq zx!N7iQ{Fc?oQ*}xzyLEpv_6!hQy&r<5a4>YpXPWtJDjHhwSdpjR>n;;IDk{|)$F|OB7DYwH=~^kR>SIdpUKlh~LSEa&e{+kAsSY~| zJ&6Lo3#BY-fAu0VGBSK2>F3w44}Aow85yH+3!aii zw9#Sb!0eQjDKp2n>1Ah9MMbIj85S#Ky<=l5?eFhj_L+_SyX!qrR;nET!e3aoSS|c0 z0K;FHo}N8)L{&-Ys5yYVaLhDGp33flw1!4?7EEjrX1(647qArd_4TbIK`OFp!vGwC z@DDkm0U*KuP9XdLg&tBp+`qmi{w7Y2tWkoDGVst-`E4iT8#PV6T>EQui z*7H^d+1ahr)~_ZG^d8^88{hp-{s;kKm(Qy5D10yEHF2iNr_##i^0!KAG-^9DUy;Sh z`yD>PZM!~FxDzay=8@=}Msu$5&FAf1&<4N0d#yN0<@q!#V7B4;{}lTE`{y8c)1U|J zB_)L%b*EcY^ebXgTwG7mhq*-lv;`%tYI_M;cUqUXx0q{|5f^tIkK=N4ex5Fx(_%Vq z_S*`Sh(vz3fVzZf0dzeHas^*h0&l?&5)%IMRF@R`0b`@KF-#w;0s{7@s!zPUg!IPr zrm7OZd`b9cM?Bv3=hs4w)3hl&4;|g*a+k(TQj0|g{47}{kllRpFb@fzsX<6aW^0^n z#zKi0VJ7(N*Yh!!45%7kgc4>dtXSBN2*SiSC&%p1OQ^TK9@;xtzf)0hIe(WTaIoy;+y>iaA3eA+M`Ps1}nIFS*Y0UK4*ZHg-OsusVvd+uM7*zHWT_ zhQ&IPT1@V^FQpZRtfk`=heeP141}7eNcz(?5B&Wlgh>lEI4=78nYm7ox5kQHM)LWH z1l&#Q+8Y`Kp}FtQ&78W{)ik?5qQma^_wQ3*a$03&rH&46q$Hoiw)D;QJ-s$*iU>dn z6XgZPC{A|}bPSA)=H}-w7bSM{H0~(mU^B$6EDm<(d*Qz+OY+oEP@susw%>g5@#Df= zqu7tAC^Bzv!MTl*$Bxgu{=Q(=`(0a05G^e$>%Or=^w}2!+MsCYv2O9$($W$JMi>)Q zAz;Yg<>j%=`qoZPV-b{p!;_Td{0E0km66cJn>8aQcbX;FO-R^Q4CHYC!=aasvFo+8uIc8~~t_4e`M zbHg>Yuvl4NPfknI($=;=Sk9ZAtSLi73~a{bu}2a7v_9B|kGgWYhvoRFwuZN~WVE@t z+1;Jv-~hR#WVG??6X-cmV?TVDgWrw^{BF+G)o8k}UOaf<^X82lG^QvzwcPCNBGWNU z@7|>j6~IFpIqFJEc6;+aG&CLtb=FJpugIdJ{R*p(dAW3zn%f5}NiYiC-Q9L}cDCGR zqra0#!{EVMVO-31zDGx|Wxh=T%m8YZ`Sc+p?Zp9YgfvAc?$Xk1FQCb$=H{9D8w8TU za*M<|w~Nf292kp>^XbgoT+?bx1U)fwOqD$KtO75E1kv;g*tZOtIO=WYXk4`KcuOu{zSuHjfpwm-kSJ+c2FBduMyz;03dkh zoFyqK>Ba`$=H})nR@``5N_521QntuQB^jCEaG%xn_1Od`eSN6WY=WQQVE{^Wb$3fh zORLv7_QkN}Sau(6jL_@UvC`Ai^YFL;Vt{esf8hJ~mrkv7+t?Tv6H``l@_dv3M{Vr{ z$DyB0DZ{yHrH*^ao13OJ$O9R2CZA?S#l$w&*P;3Ni(WLaw2hi#`v{66DyfXTQ&uMN zc0Sp5TWDc>`O?68UjuFf(?1B^+RBQFjV&-Ru&1jFNZIGW4g~LS)fc}2vg+ySwKO%+ z(9%XnM}y)EGCLRmYHCVqYA?|d$1`EN9wg2#bP6T`z&txY%DMNbDm^Vt>D{|eMn)vw z=SLgGA6psWzFaQY)3USMTUVOCk$6o|4gdW+cYb8@6p4$0Vfx>{e={?3Vk9_yKRT*{ii!%Y*7(Bm4)Jq9Q&;D~-SK8qQ;+;_Oij6= zplV@YV3WZ8Qepb{PkVd=bn+NhU8~$r(rX^q#Z1lS_Pz+fYcH5|Ikg$6s3gV3U$L_X z1UQfB0?;o9Uewg_j>fLZZw>%93kOF*!AcyzTUl0?MBG73FdhI0*InY@XO@3q>)yZn zp}~7kr9iup+GHeutlUCi`;URXemvJfYnEaL_0x7Bc~OiyNAu0d-o5b3?a8y^aslVV zPe9}zV^J*heeFrK;V?ZtgOyxyCq114_BaNbaIRW0Hsk#VeEy-Ku#+5N$b#?f&o#Ej z#EknOKfizf{w)9X`Vg&~TLEbB!pswS8rS9KXAAlu)52c!_3@b)9ZgNIYrL>K+2*TZ zCtngF$Ax#?@-EJ*E#MrOi51eFEuel@(Dn9KBCEoo9!mJF&S~acaLZU&}~H|pcui-ii*Xl zYKQO~6ciL7WZVvAY_DFuMr4Xc!Ssho&&qa9Y1t}t+*qrPL^zzn1!89;PM|n6f zI{LFQOAL|E_0)26wC_1%LbdzU4p3$tf%(Z&lc=9Re=aYjrltneAEzX1cgJyg36qu% z%WU_j6^Hid5cK=`{WBU!C-$zg---zjA9rQ_1LXN+Th^3aLraUmBi4OM8-Z8^-%y0K zFIUY585#MRii*7~s^{z9rKLuB4rv7iz9ZP!?HH>|FBlNfLVvQf;@tqrMLNllmXXou zbe(T1G@Ecw>iYx;zq$D(XJK8P`+O61hHU)t?k*y#JF~90*ufLuCwS{yd?OPs`LVG% zgESAlJkjT^EAQUXA)Xx>9wrpHc2QHq@e%y{{7y3$9xm?g-C)rL>q><+Tkb0l#v64u z%U>Ifz9&_-Yns~HeRWQES#g736N~@wFPNICToCx!VoO-Ccd*hk|6w>NNE!{T=lAdD zw90g7NJyJtlkgaS7tR=HcA=NpZ^}+e0t-hz>pQj-aYP$Arhn^##BxU8Dk2GPFpblT zLKrLUdiSp0-rt#-oRnh2{zL#ETgS$VYoFnMz!3cKfe~F$F+@o9Uc zqe9bkZGD-t*4wwBQ8@3BDr95j<>$Ylh=5MI%f%BW-b6s_EpKj~5G+G!C99~2#SIu37}(j_%@f4r<30EteJ*WHWu{-;Lcld+3IfF~_oo_0zvm7#)1~Lu}djzK@H8W)5U601? zvPut@yVk~vTWej=7S~(cw|ojff>2UcPT;n~bZGJuPOhxP32bhl%rj+2PJAo-^A+LB z$iM&#D{E5++;7EB+X8Su_IBo_Vkk#YImSzKZd(C5$O(@8%h0C;Jg#dT_jK`b47*~e z`3LG9b`rdV%02ICS&2DvPNMyxTMv*Y9A#~UNhNm8@2xNMeDTsa;EivrDc z0IHSRUSrqcal9D~*WAQDVqQPqszyx8kQ1`Ci>?4EjxQM7Iho$!B#YGN({{lvxI)EM*dkd|fKTnJl=pa9K+P~Ool}kAM zdj^A_uTzgp2-@4hY$UA;uj5!R953c+kjbDFQm~lJxw+`98 zRvj(w?fnM$s;P-QZaPKm1cQuMJO z_&C94eb6Xwpu3xalaq>k(MVXhA|++vua_@qozrO#9{EnzxzW;XMgvxk*BY5?^ffXm zzr2V6WQ~I4Y`yVAM|G41aArltDbQKdnL2uY{^OyXPrT(QBu{a1*)0+rgoRIw4do6F z4)#R2*w}_J$iDJ$aY;rxogYU3{=Msqw&UX~Q15!y0CL#gyS#>m2Bn-~>3Hs!{QT@B zB<4feONbxoKb-Dth%i2Nl2%3)qek ztNK9?VQd$+zp%m@%0sU2TFJU++^S5h0=4+}xTo zb?m^GPm!8agv=J7jwsxUDYmQ#PcxD{$+U-i2Yn(gfu`220B(gD9pT=U?-T%oWmOx2ss$r7@JSRxVsJSI> z#L|bG{1)toQuThD2H}PdvFJu0w1gX5EQHe=RCo7>|My7Ee==*Z<{o?X%ZYl!Cp0R$!M^YG~Ceo9y!S5|eb+E?KcY1>1O$QPbP8=n zMY3yZK?z>-T6wBe`6ZWkC7bJ9rTDVJ?`l1&^B~Ccr~JY5Is`C5{y&-;|Gf+Hi-`K) z_4x)r2$ddH&k#vUte4u|ug*!m|LHMnL&^xd4OcIe@QR-AD&lmfE7qIQT-VI(Dl&NE zKkN*d%l?4@MGcJ$+Kx*KO)V{HX=w~WKt%OWlzmZ|O9O;S(=t5P$BHxb=9jx-W#jk% zc7VDx+X$+E>+j#auy6c|-7Zg)JZ`RMoCQFdp=|Bx~Az476lwMm9TFhV)mj5@-H{^mKL_R{rPnD+v zFbMoUJ3k-f&i0$K^9H6z2$#t4L?WbbVTD^+73z`t3#)^153)OjT!N{YnUcD?{n7f+ z2GXLj)7_jOc0dw*x1pcfyYo>}J~C8(`!<^2 zEyl-31PCe&NZPnRdJM0V!n?r^ZFtq@SPIm}`DQ76RKw-YXb^3H^{n*hfTqlu5ggpr zuHoY9i*3(oWNM_58p*NHLo+4{vl4z zpjEX9@*xdPyOovI@USvC3VokgpFFX<>q(561g)^WUDES)=B?>uIWa!Y&3ozdcP;XtbW?^A@iL9ci2nH6=XsBWHpGx5)Q3<$f+zBjVUE`UF{InWgx|7w>et4(j8x1S_;IZys)s+erp}H+xvIj1sh`f zQp9$~OR)?$X6k23O;mtH0#jvkJ3j>SIDZIZ48?w{5z06G|4JL*{Coyk!{RZiA9U&R zREQ+lfoxXu-iL>W6%`eQA6qx4s#i=x=mo{q)GEu$@`F)QppbJ~&Z@w$$Ww8BMSl1M zSI@}EKWbrq-u=o>KpPbWh1czzRd98>_QGPC+!iz~28Qjm0ZO39Ad56PE%zk$%nj0O zl;M-ug2wy^Z5xoxb*RAgpT-z4u;3>1-q}SlXsK7(wt*oiBzTR>fcW_F7^nt<|7HY; z`T6r*P6z=;-0CkN;efqlH3x4L&uP`*k42e=)cH^RW+}=HF=?eoC?%N`uJq;cPfJU4 zIo@n>VmBSNygUVn%t-8wiG>A(hLCGt0|fmq50Kr@^e46^$}t50{{0JBv@2B3%4)AO zx_H-GU%xqlZ|iXp2vEbrKmPtYSsN&qOPp5BkToGY`2C@=nZOciX$1Zoc6$1)_Q)JZ zoW#wK1v;$@KCraL$JJlHOk18fUhUJOddi?vhf)s!XWr>9EafgTQxomlfJS6j{`%x( zP`e(JEG{lKtsH33X)0J-Gi(1@S-@nJvWf+Lf{vcPtE3`3m~HKzQpJ?HKO{4hUH;u`}GjJbOl+%M4!|Y=*U@~kA}lIO%z!s@>jYn;5$aUlwK zuXwV-<8R?*-!^1k&kDwUQMVMJk?K{B2*xzqr1Z(Z^ z=tzH*#K@IjfOz&8iUa&;H~68WtcjJCgthfPtj@g$`Jga>`ocD&OpZA>+hI^#Tf0)G z>Qhj#1NI~V0Rg%o$Q;L;YM`gw`VBS-d@i>t2k%2D0EMH@Hz~l?(8o)R27WSq952j3 z_`D+;Ld;{m_%HM3F_QD7{W8$rZ{G~xyb1lNn=B(Ev#_88#+my-dN4J$g`&dr`Ptdp z>Z)sD3rrxRX2mP+^EAC&qiu+nfL3yGwvuQy2UVs?+A0ans8b*JOV$0_zym6 zM*g-1OXjF4dsIiHih_=y9&ox)LG!h*UwLJ}PJW_0i=1QGpzN>fBjXXoaA4=V2GQ+xL889al|%xen8 zIu|T(DIO{7%=~%Z3!BE+!eGw7YGL6oct*lCcex|MEZY*rvK~HK)$0Rig9DAy8H{?K_aM|4d6JG z#*0B@089>7SJ_P?*_Yt)9x2Co2JZmrT1J$UAL;kL>Qg3J!jvp{f7AH%vAo zPzU^AiLT}=T*!QJ*>7^}NvWtrefvhaMJp*MCnq7Xrokr70PTZ~g~g1g=Xk4rbbNfW z#)+1R$*Lu24Q{1j4YW}E&CDCE7;27t_q>{#Mqu42thKa!)wxtS!xi#H{ZOo?qk|eI zj?dK(evAzBOIFrYiy3|$or<5Ho^Ko-S3wab$HGBzHZ?5)_`#988n}B6*^7=+B``N! z-3m0T?L|Ipokzg-6&4-^bN{JUpTqH!ugK6G3-q3jmTg5bK72ank zLM(+*kcyjoS>qWH=98@nMNLh*NO0PZTK!oD1?4{fvrn`kn~^*sah1%rc) zjZIUPydzgPVF(G2zGA~r=uj6YMCnjL#8V|AC;-1Xtq)SeT9ZqBI59S6x3kS^HGltk z)E#J~;BxE&@jTlfie|~UrPYQjnzhbPW`;W6_rlJ9udfdpw1VQyEeHnrYpHz+^hOTz z$)+Jda$d5A-oEvJ7$IvDAAdo_e`(lSrorLD$SB7g4G|vTMA;b`&oTE=3ZRJl72Djj zw50#}^QWT1(YqJi2baTDZEfx80-c$ao<;>lMW|F;+uLy8@8soQJbiiyR*tuPDB%=z zy;j{qDaimgHw)Mq#NKgn_`nc_?u`uCkoya}9#T=Uv)ey5z&?(=+XESY=YKOp?r?DM9UfjRzuP&4gg}u? zDJd~+|K0|YsFRZuDB7Tu{csi7obfKC6j&Hhuy)g%84Dj2Oo4g_xT>I$p5Ww%c-fL}x zx-~v#CnGMtI$F5kgS@h98CzO9G+t^_W^x9UXq+s#d+b3{+sq z54pHhbJ-a97t6%3&aL*5rKRbEm+kfN2`pn?AHlb8->MbrR#wDCvlwC^Nvon zwPBDfZf@3ql8M8To{^r8{rvgPM0v*93djdr=XNvZQ&qL@@x8shmU9hUdlQgAnU3dH zt9Ot878zM;G9rr>I>&hhAGXjE>~=s=SW`0(HyZS-@emjXwNy}C!XF|-{aBPCHkFfr`2KoL^~Cc8&#)ts zK(&D~b+zANf7g@Bsk{mje<>-AuuCxnA@%%|-K-t_Hh_mQKtXa$W_LO4H-82Nb)KG% z0L7aqzxl#(n`ZcN69R2(hi;BV4B=7gudB+zfgmIM2uuy zBp9GcMAAkP@f?GTqou3c57HuNEPlv^<>h!xx~KVCF4qw!haG(pSBGl@!+8_vw=P;d z&jdFQk4}YUU(e2Rdt?XH!=DKWp6CE1Zq_o`LWv&Ly2XA$S5Q_KMVzX%>FL>_5Tv47 zm9Vtzg+c-zCTJ!0o1;D#33YWI_EZ!yG3^UiPPz^6Up;(c9m{5tUvUS<0zSym2{r{h zDiNn8Vmalj-8-@)BO?$IUH8M#2FI?YHWj?;ME)mr&PU@t0tIgSi)*nQ{)kB=LX!ji z(^{3*&q&C*@C)_Y5)&`M!1D8ZB_z}c&a(G6NQkWVeYCP@`1@BIScMSAi&H8Jifl#7 z4N#IV+u~S_jKN6~%*@REba8A3@yW2uDu@;qmX+bMenOm<;p3}?tqc<4()y4Ibb=?3 zosReQZB1Hd25h`RqFroL^bwr9xvD-oFAqR%*Aw zetCSc6Grw=GQsNU^XK3RIAc&#Q$J>emJb1H`*l5nr-zH6Y5?Ne?o06nHyS3w>MPK( zJ!PS*C^)#Y3rHt5 zxKF2h3!tW1E_~I2oFe#JansOBLGc4Mo#}=2B}wZV4o>u`j2Ai$V}ZDY#NUo5PoCT= z49IQ}UAfJgl9HulzRULa&swaAg}9X#0W9&c(=>=UZu){U8V2WTR9_L*zf;hiM?Hno z$ziSuVhAL9zJ32rSE`gDI}BlP8v6<8QPR>+6NleRQe&BN^JCC6gF-^qhP$qNlgKzd6cmaoGSn0la#IH081HOuR!MHF z1H>3F6#%mS^w~2E|0Wm^b=99N(UREM29BSahlkTMGZDp9y&)@v*VZo@y#M{OC6f24 zzWx&Utn*~b%E}Z@b{vqy;=F=p4uN^dS3`v1d?=>^s>kw;Bp?Im0G?46le-hu4&~r# zz-;EISk3b)EB`n?K8Cz6VA*%;I);=*m#{jisk7!~V5lL-`Eux?nAjtRsPa#J4L4UN zr_0L*C=XLq3KHhM$hcSs+1u_#CM1wt|Gp*N+Gg1G0)YVm5EaeK&DGV_9Vxisf*chE zg>p-kJEt|ubH9t9h)L|SL@`)MH|?EE)_Y{eVA*rHolA4aFK@Uk{Ro@-C6`d0Kqhr=ztl2t4M&b3JRu9m1LDKnU02qajmlsrR0MDS1|BBaAab>VS zG5-w0vXPORfq{X^1!N0OT|F12{Q0hE3F=3TO6t4BKD@dft{ELw1-My6AOhxiY|QmI z`)#AQczAyjQul&F_fA3uT)Pwaf_n(b%Z*J`%=rpsv|!TU2#4QDK-Au-kDk`i(UXegD8>|^x*seoAmoIF6#+X2S{6P5 z0T+JAsfk4P%dWZZf_JsH+**JyV9y|9cP<<}fG&}rea@mPpY;xPrQnVtsxO%~I}_9P zVAcRs+Ls;ApHH_4)Q;WMdWxoSb8&?b@No0-O%2$jLLK<{5nLrK-ut&2J!n@Td@o%7 z{Y#RNkP;Xug-XN^i5(^EK%ZM_X~7?>6^I!!XmH5Lgoep)XEqj7fo|iC6H05}Es-y9vs=YlbzdS@lfNXZP zw!43tS+6Ygz>^4bMhwo+52+|H*46#eQ;!a3GCW4B>?WYFa#&~+@s9Gn+6{BWa&xOS zpV}PByTGY$Qk2_O!&C;htT3hSG&ZrzU}R{B#2aq%1xiaK?Ja$mN4_>z)zX@34vd0i zd5zme8Tb~klnnoVaq_sdS_bdnG&h4l?2VLEpfKqiM>NPT+Y^!okd1kYbCfvdfuy7R#GZu zdfyNEjXF3|cA5rz@HH_N4Gk`{-YZD;3Akg1yaBPkWo90_0!U!<7-$eZYvV&g^XQvz zPGkhn35$pirs5IWT?n*mYSsh>BFjP|<~2Dc%qbldR}d8dqr9xggMk7|VROM|eZKi% zAYG;w5)crCN|%ZCyl~sz_8RXahb7q7*zf8r^x*@9S&a_L2ViAQyp0b63t*S{@tdOS zVgv-jZ@1qI0F{uH6|&GkfcN$aATm0Sa(?a%$QrtDwa1O;e9T_F+t$;3n0fw03;m;& zPRk^DRUI8hwzsaJz?@DR!`7^-;)cxmJZ<;sIYLy&(!qn%3nu`nz?okoIypNBJj!dr zGHEi5M@)iKKeCDZaDPuijptAhlT-@k!R%tUnD#Sy_x3FubZO3kZjqOJ9oL`_YA=jq z)W*88Q;n!=OOrvdm51S2QCc$gg{&Fm9a#*dTlsQdN?}7o6R)iV+23w6Q@~*xAJgC4 zD-23_kk3r@$)D-zTYv4eVr^{=2N!q#fFJ*jBQHZ}YonO;aBeiXH?U@>YdgBTwad3D zUDX;J8t#Uw;J$b(Q37_;Ovl&C*Z?2Fn}H_M8kGk~ z3ls}<=t+pDthi6)K9@rD%W^hRMe(DqF2scQi2&y2QqqYV?d+3HXxn^i_%h4PyhbFB6v$(0cjrYz7QqH zyj8u!`jn@19*IdQ9&Ehy^0f&2s`oO;JT9G$^Rb?Si4MNGweOG!n+A zNRVKyH0UErV6QM zS)jv41_t&kAs+DXa5?}=Az;`56*wj2bo>0;-qw}8grVj3TRo>GUt1SI5^>lz#lyQm z^5Cm?#9(qgeO2jP1l$gIsM*c68~9Xz-}Aa0yFz;L^zQeJ3`0vxc{@89^KC=O!@#lI z7M&6ZQ%=>ov_g&%Le$YLD^PPRRiY%*{3wyzVIaB47ax1RMn}94CzqZ(pvKrZIOJqz zKGhNY)2Is;CN(_0l9G(Lqt^;&0#8uIF(ja<$QhVGf#_Trl>NqyZa&`KHkf4s*M457Axz0kCe;9 zfvTH+lnr+An{N>JfLd2rBOuWD{?$+E^i%Kee}4hSf_OI=xBwlYuR;X4Z*Y)`mNp9I zJN!_Zq8JXn@%{oMX0Lw6^1DCS#Xf>rxw#NL6ax=3grK@92jVC%P@F|X(iS^;AbXOU z>OCPr8m3Ao1KydnEfJ-p*~=8BkD^^MYy$%}C2#~RUN(xC&q5Xg^w5~(!fKw)C84zK zM@y#{6VVm)Jff$|klXCToayRsXWy80%# z&21l?c?;FaF-skfM5O|uZVtGXsB44!*pu)8eDB=cTxlU8t#twhvWH&z$;pt6A>+dE z`TGkdseH$8YrGU9HKr3~T8fIrpkD8vq{j^O>mHolby|oAQxW0;4(d-s3=Fff=y zND9LJTRidt_xTeGQc~h$V`*j2*gqBlP7nY1>&X+%wsPcr)uIJ2KoxVI@0iAJ24D-r z%N%V$Km^isdU{C`om5sRO8OranzA@88fc*CLHZdOB>QdiTL`0gOAC&CKms||z z0+KDb@w-xWA0(27JzNGG9W*%z)s0U~z}e85T9=(f^QQ<1;$#wCr*9P%b8;5^EY!{< z!Of?nq;H3u1^Oo~Eelw` zI(7n|K=4#oSGTfi6PWr|&&tLI>T-Ho?M4@e z8EH9VztPf)hpy}O=2m>8B>Dx1gae4tS>YIi4M`mUE95si>);>?8S*9`W9u zgXw}q6O0|SaKMyyt(M5Q9?+liyT$P8|21zDo}l%b7(MeHx1z)nkuhX#F2a9KLX@>ZN{`v=MtMFYte^kPeQZ47; z{r}hR=lQQz(|?om`GywT1?cBxL>du&sysxiS=aX)|hQkh(|o}5m`KQf4^>QY_v#gQ(1+w znwry6yME(W)O3X`#3aa_*sL&~U(Ea{$A)T3D-}4ZQs6C!A^2~y)AaJRke6_$U1`R? z1P5UZLPpGQm6f@Fd!R+M1qFI&sl6SmET)fWYpU00HJW#{*cl@vOp%n7ns~e4;B&wy z=4`vuSAIr=>2JH=)F&ICdwUF3s4?>UcMydk4!o7Af4qqkK@s9W_$o6g=?=;FpWiPB z#-D)ry+wtDQ281@kBNiBkU1I_8+7+pY6zYnl2B6m_I<2D7ZfPC@o!y<=W^kvrL6`D z0KVh_=K22JxyB1L!yYXF{cw_OZPdRM++nT0&kF*K^kUHz=zB zF3*p*@-98LrW{liR|p^N>f9wq|H0f~%Q*S@-u*o0226}MPyhM^OsMA!88>sAZ2itd z&YPZaDIM;s;w~9{i(er|P$B>N^>-Y>hjHHIC!q^nBEm?+iUp@vyiSBYTs1qOkM%@F zo&1k>uKXYB_TB4Ar1DrQ%Mcn%A`!ASB$Gj6nf~|2 z#tMrdk2X47-TYIJ4-W*XxV%}EF7M)cn&5!OcFM@mYNVx*zfJsoo3@~=H~D+g+q*Qp1+Bj0`(Qd+1f1$fT1Ral0l1Y- z-zqex{!P(rc(fkR3OZBrT$3x8zqMU#StgbH2)pnuunH;deQD1iyhy-osLd@BIsXg_`V~E z^|>vU^%>#s*r1+))UdEdp!+UeVv0bZUJwR~oo%pK8z5g!pn?d|TvAbt!G2rQj?f*& zZ&h6y4~;t0{gc*XMJbI^dvw#fz&@urUTVJm-A>~98LU))+T~@TYpLPkQ16KP;vz_V z|Fc!f3yaXdKjsZ0qxw_Sa1qkWckkSO&CImnNgHZx>U?SuF30hjy@Pnmf%Dpz#ZgWA z+G>9hof#k2(q6Nd*s0#1;kcK+jSnKo$`$ZvNgX@XqSM;)7CqZ7r^>xFI4T^@<)@Dg zU`S>bzH|Tah`%c5ag^^WEtep!cD8rO-1h3(otUAtDnGwsCtU$qkuf0RsXtNz@JZTY zlDX6_l)vE@EquhduiAp3nF3_aDH=`zs{^a_ggdkGr#L#kYMutwOjoaZ8q#m`StZrm zf8=T-KjYXJ8r6*GCPYJ0g*W*H%F)_M2aULKyy9EEj3e37YP|U}11;obu=hheo^x=p zV6dWkq*lgJEk&9$?8pJH%2RvsOh=~L#+TM+Db=)?wi0Lhj|Y{;9&CsDm&=jD!hO6( zuk^+5g8>IXp1TU=_h~r=+%A>;oZf>?Qgg#QT^j3lkn>QnSg=GnJ2@S(X$zUOF?bkd znah3X0LTloZypB)ht#<5>kfwl6V83=W5!5JYcqB*Jwji+)N{e#z~CGQ+>XF+h)KOo zdGp5S_tb)2r!0~-K6m`+QA%!#v=3*^GBosSxaCNbv(nPiBV{x6mX+EvCc+JC*u zMVrvjb#ZaAj6a*CsrhuU*x3ydN6HK2wq0@MO>79JlI(Nk3fsBkhdk#8lA<2w+BI`6 zv?hFfDXHKxV1A{*G+W`(csi`|n4x-cF%Q8V4H2fW7zlDDxeR;=Lvyz8W&5j;fm^S? z>Cm5^TXMf>0L(b7Vf+^YG@>y#FA?=af*4zqcXX}CN^8>6K70^$EJ`HwW?%2UFTXQF zQ&d8l3&{{Ybc-juvn}<|lE%JorZcbMGa_k`EjjU`* z3CRi9ve8sHVP$3Dj9lrj0l^p%k)EZqHoHWU?LTauV2{NL$4e$E;)=&BvH^wfd-9Ie zdEc;jXXm9_h4jOz`wS(Fvdiv3Vs=`E3?(f4!O2-LwHS zpW&tQi06VQV;V^$K_x;F3yTe5YOJoA)tPbF_TZhzUJb>7rZhuFnuFt9oJ5lD zJtVu@Etpm!F57P)&4-aO;GkKw_=lZP3;oD2d48p;l1X811w?EJsqoFd}1 zWaQ)mJ|;ae`QDuQGiZqg2vz7B`BA|c8K6Bc-E_F4p@BhV_Vx9(c;5>N4BVK^9+WPT zv-a_k#7f3#8;x5ENIThX6WZH<0nEtkp~ZI!RrX3q#)ddQx8*B~Xv9jt)jQ0}l2g(5 zi=Z(&iiy6sw^cF=(7I-`kQNC6ew8N3(FKKu28S^A7-*g0@_DqXFpHlXu!A~lQ~%;~ zK<;igp$}6nu(cF#wYKMVw{5Ar7Vdffd_0^o2;lMH>m&7eROFjlvv-9hjD5%?2p}kY)c|jY{YzJ_V<8_oYAmZd97MoXys;fW3WHemi}XTN#~` zZF=5|LK*MK-pL>mi#|V*-8Bs(=(2ATy;l}x&YgSo1P_%x*J8QuDI3=bHQ!j2tna6vRNVnL@Au~~U=m1yZf-R!)ayKGBypT$b#7)b^Iqh$rf1#VM?pg2 zfzX3Liwrl)?2%O84Z?JT z#K6zV`_528-EuXd1RO?HYAWQjq7!)+g$R-1tAA*AVw}e>oV|Jj<-e%^Or(9J4fhyI z>J`aXoE`O-KL|du%j7 zD}EG&^+FP1LR5rpHXXC6c9~Eg&}9)$99nVt%a+r0^gpW7j-sd;NNIO-&l&q-tt_x>^cMz_3ujgk8<(%ID4QaU37B z1}0fft^iM} zy5j6)wi&v+de3Za4L{;CvINfne(BSvfBCGe$jhD22A0vuZlb5+XZ!2Y%E}9RdXakS z7LDUeka%Ghtjsk8%~NW9Zja;SAOWLW9A(3e@9Rl={aTzk9|C~te~G>Wj99_T>*8_( zGXIX*eTCOzN;l5E!*KYl&EI2oFXEc%>zG@&IX(OS#+zPrp?m)oM1+0IRFqRqvUw(xVL3#fEs%y07&bOnIHI)Pv&04+(5`{kDSj$$ZXMOTWGNB( zrzh0br$#Yw95EOUHJMm~eI-g#pL4BM(Tug);eJf zAsF4{tHManb(bPza0~j|+jDVHbgHkR)9s5!+R2d0?aMb@I~gxY5Lcyt1rWf)$H&Zk z-B6;3G$EOi4K6AKhp5<#L9!41OCIP)rh6u^|6UVd?j$Mq2jqAGbb#+XO9woE2awvE zb5~FFq$B-94w9)75zRogppy;33dTQP)>$DFZiG%?{_ES zH3L4jLa5bcp!5Z0IN#0FkroE__1k7K<0SAbczBKML$3d$aF zQyfoz|9(G5+^biPMFXJaXB%UkoGPt1Ipeg8<^~sN{h`pbdCJKSlweC-=Hfv-TwfQ| z$L6=RWWnAI3Sm5jyA7cBQQYS=*hk9BWeaUqx5cE~JRb6LDmU~uo|BO1)W3{mImpIV z;W`m{$SAl0`kThFXilUyk81g?DpjuCcMLUpsnAiBOl5^J8{MUfzYL zQNh($SQz=a(=iNaSURQO4I-U92u^^v!RBv^)HW^sGGuN@HU{Pd(u_WC_7MnPP+{U@3xfzeTI7da_DQ-BX5v90 zAk=EO=W@of@`C!FLFnIe;H1BKD^fidJiL-6xRFK~M=LAAFp)2Hb;`oy0vy>Rc?J?v z%d4x9UTTU|-*Me)xl`=>sBDPrQ{r&zR9k{v^FWj1OfRl~gloiQ(7@chGyTR)Y3c2J zvfmE94Eu8(rga?;YZ)82;nhlti?i4(UrZcRR#HOX69))Z4)=bE$|-W6=-Ue{Dq8)} z_)iMVTFIrWkZ7C{(`cKUhr9HyeW2-cG^VM!8OvWnA^|f1&oQLf;odznV=*_+g;$jr zXI)*FmG(Yr9L(eBm?0Lf1b_u;^MaY#AW1es>pjt+53J8^RM-MEcRi^@@DLaq8j8n# z4q5B!?2IP-cHL}KpYwonuIS&nI-`gfIJm7I^J;gYhF^zU-d=sLaHOVY8wMu8vB>nV zEIgUH$&cYl?>#OOf_e7rbx5%SWCm(OH#x4t1com8Wijui&UiMXxvsg zeoqWWXCRLsIU37=ggF0J2R4FCWD literal 0 HcmV?d00001 diff --git a/fcast/connection.go b/fcast/connection.go new file mode 100644 index 0000000..aef02d8 --- /dev/null +++ b/fcast/connection.go @@ -0,0 +1,78 @@ +package fcast + +import ( + "encoding/binary" + "encoding/json" + "errors" + "log" + "net" + "strconv" +) + +type Connection struct { + net.Conn +} + +func Connect(host string) (*Connection, error) { + conn, err := net.Dial("tcp", host+":"+strconv.Itoa(DefaultPort)) + return &Connection{conn}, err +} + +func (c *Connection) ListenForMessages(em *EventManager) error { + + c.addPingHandlerIfNotSet(em) + + for { + + var ( + bodyLenBuf = make([]byte, 4) + opCodeBuf [1]byte + // bodyBuf's size depends on bodyLen's value + ) + + _, err := c.Read(bodyLenBuf) + bodyBuf := make([]byte, binary.LittleEndian.Uint32(bodyLenBuf)-1) + _, err = c.Read(opCodeBuf[:]) + _, err = c.Read(bodyBuf) + + if err != nil { + if errors.Is(err, net.ErrClosed) { + return nil + } + return err + } + + rawMsg, err := DeserializeRaw(bodyLenBuf, opCodeBuf[:], bodyBuf) + if err != nil { + return err + } + + err = em.dispatchMessage(rawMsg) + if err != nil { + return err + } + + } + + return nil + +} + +func (c *Connection) addPingHandlerIfNotSet(em *EventManager) { + if em.codes[Ping].handler == nil { + em.SetHandler(PingMessage{}, func(message Message) { + err := c.SendMessage(PongMessage{}) + if err != nil { + log.Println("[ERROR} fcast: error in default pong responder:", err) + } + }) + } +} + +func (c *Connection) SendMessage(message Message) error { + body, _ := json.Marshal(message) + rawMessage := NewMessage(message.getOpCode(), body) + serialized := SerializeRaw(rawMessage) + _, err := c.Write(serialized) + return err +} diff --git a/fcast/discovery.go b/fcast/discovery.go new file mode 100644 index 0000000..37b7539 --- /dev/null +++ b/fcast/discovery.go @@ -0,0 +1,57 @@ +package fcast + +import ( + "github.com/hashicorp/mdns" + "log" +) + +type DiscoveredHost struct { + Name string + IPv4 string + IPv6 string +} + +func Discover() ([]DiscoveredHost, error) { + + entriesCh := make(chan *mdns.ServiceEntry) + var discoveredHosts []DiscoveredHost + + go func() { + for entry := range entriesCh { + if !validateEntry(entry) { + continue + } + discoveredHosts = append(discoveredHosts, DiscoveredHost{ + Name: entry.Name, + IPv4: entry.AddrV4.String(), + IPv6: entry.AddrV6.String(), + }) + } + }() + + done := make(chan struct{}) + go func() { + err := mdns.Lookup("_fcast._tcp", entriesCh) + if err != nil { + log.Println("[ERROR] mdns: lookup error:", err) + } + close(entriesCh) + close(done) + }() + + select { + case <-done: + return discoveredHosts, nil + } + +} + +func validateEntry(entry *mdns.ServiceEntry) bool { + + if entry.Port != DefaultPort { + return false + } + + return true + +} diff --git a/fcast/events.go b/fcast/events.go new file mode 100644 index 0000000..c1f8037 --- /dev/null +++ b/fcast/events.go @@ -0,0 +1,56 @@ +package fcast + +import ( + "encoding/json" + "log" + "reflect" + "sync" +) + +type EventHandler func(message Message) +type CodeRegistry struct { + handler EventHandler + constructor func() Message +} + +type EventManager struct { + codes map[OpCode]CodeRegistry + mu sync.RWMutex +} + +func (em *EventManager) SetHandler(message Message, handler EventHandler) { + em.mu.Lock() + defer em.mu.Unlock() + + if em.codes == nil { + em.codes = make(map[OpCode]CodeRegistry) + } + em.codes[message.getOpCode()] = CodeRegistry{ + handler: handler, + constructor: func() Message { + return reflect.New(reflect.TypeOf(message)).Interface().(Message) + }, + } +} + +func (em *EventManager) dispatchMessage(raw *RawMessage) error { + + if _, exists := em.codes[raw.Header.OpCode]; !exists { + log.Printf("[INFO] fcast: Got a message without a listener - OpCode=%d, Size=%d, Body=%s\n", raw.Header.OpCode, raw.Header.Size, string(raw.Body)) + return nil + } + + constructor := em.codes[raw.Header.OpCode].constructor + handler := em.codes[raw.Header.OpCode].handler + + msg := constructor() + if len(raw.Body) > 0 { + if err := json.Unmarshal(raw.Body, msg); err != nil { + return err + } + } + + handler(msg) + return nil + +} diff --git a/fcast/message.go b/fcast/message.go new file mode 100644 index 0000000..990d929 --- /dev/null +++ b/fcast/message.go @@ -0,0 +1,47 @@ +package fcast + +type Message interface { + getOpCode() OpCode +} + +func (m PlayMessage) getOpCode() OpCode { + return Play +} +func (m SeekMessage) getOpCode() OpCode { + return Seek +} +func (m PlaybackUpdateMessage) getOpCode() OpCode { + return PlaybackUpdate +} +func (m SetVolumeMessage) getOpCode() OpCode { + return SetVolume +} +func (m VolumeUpdateMessage) getOpCode() OpCode { + return VolumeUpdate +} +func (m PlaybackErrorMessage) getOpCode() OpCode { + return PlaybackError +} +func (m VersionMessage) getOpCode() OpCode { + return Version +} +func (m SetSpeedMessage) getOpCode() OpCode { + return SetSpeed +} + +// body-less +func (m PauseMessage) getOpCode() OpCode { + return Pause +} +func (m ResumeMessage) getOpCode() OpCode { + return Resume +} +func (m StopMessage) getOpCode() OpCode { + return Stop +} +func (m PingMessage) getOpCode() OpCode { + return Ping +} +func (m PongMessage) getOpCode() OpCode { + return Pong +} diff --git a/fcast/protocol.go b/fcast/protocol.go new file mode 100644 index 0000000..913d90b --- /dev/null +++ b/fcast/protocol.go @@ -0,0 +1,76 @@ +package fcast + +const ( + DefaultPort = 46899 +) + +type PlayMessage struct { + Container string `json:"container"` + Url string `json:"url,omitempty"` + Content string `json:"content,omitempty"` + Time int `json:"time"` // start time of playback (s) + Speed float32 `json:"speed,omitempty"` // 1=100%, 1 is default + Headers map[string]string `json:"headers,omitempty"` // headers to be passed to the server when requesting media +} + +type SeekMessage struct { + Time int `json:"time"` // time to seek in seconds +} + +type PlaybackUpdateMessage struct { + GenerationTime int64 `json:"generationTime"` // generation time in UNIX (ms) + Time float32 `json:"time"` // current time playing (s) + Duration float32 `json:"duration"` + State PlaybackState `json:"state"` +} + +type SetVolumeMessage struct { + Volume float32 `json:"volume"` // range: 0-1 +} + +type VolumeUpdateMessage struct { + GenerationTime int64 `json:"generationTime"` + Volume float32 `json:"volume"` // range: 0-1 +} + +type SetSpeedMessage struct { + Speed float32 `json:"speed"` +} + +type PlaybackErrorMessage struct { + Message string `json:"message"` +} + +type VersionMessage struct { + Version float32 `json:"version"` +} + +type PauseMessage struct{} +type ResumeMessage struct{} +type StopMessage struct{} +type PingMessage struct{} +type PongMessage struct{} + +type PlaybackState uint8 + +const ( + Play OpCode = 1 + Pause OpCode = 2 + Resume OpCode = 3 + Stop OpCode = 4 + Seek OpCode = 5 + PlaybackUpdate OpCode = 6 + VolumeUpdate OpCode = 7 + SetVolume OpCode = 8 + PlaybackError OpCode = 9 + SetSpeed OpCode = 10 + Version OpCode = 11 + Ping OpCode = 12 + Pong OpCode = 13 +) + +const ( + Idle PlaybackState = 0 + Playing PlaybackState = 1 + Paused PlaybackState = 2 +) diff --git a/fcast/raw.go b/fcast/raw.go new file mode 100644 index 0000000..73ea178 --- /dev/null +++ b/fcast/raw.go @@ -0,0 +1,54 @@ +package fcast + +import ( + "encoding/binary" + "errors" +) + +type OpCode uint8 + +type MessageHeader struct { + Size uint32 // little endian (4) 0-3 + OpCode OpCode // (1) 4-4 +} + +type RawMessage struct { + Header MessageHeader // 0-4 + Body []byte // 5- +} + +func NewMessage(op OpCode, body []byte) *RawMessage { + return &RawMessage{ + Header: MessageHeader{ + Size: uint32(len(body) + 1), + OpCode: op, + }, + Body: body, + } +} + +func SerializeRaw(message *RawMessage) []byte { + buf := make([]byte, 4+message.Header.Size) + binary.LittleEndian.PutUint32(buf[0:4], message.Header.Size) + buf[4] = byte(message.Header.OpCode) + copy(buf[5:], message.Body) + + return buf +} + +var ( + ErrHeaderTooShort = errors.New("header is too short") + ErrBodyTooShort = errors.New("body is too short") +) + +func DeserializeRaw(bodyLen, opCode, body []byte) (*RawMessage, error) { + + var msg RawMessage + + msg.Header.Size = binary.LittleEndian.Uint32(bodyLen) + msg.Header.OpCode = OpCode(uint8(opCode[0])) + msg.Body = body + + return &msg, nil + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..59011bd --- /dev/null +++ b/go.mod @@ -0,0 +1,47 @@ +module git.massive.box/massivebox/fcaster + +go 1.24.3 + +require ( + fyne.io/fyne/v2 v2.6.1 + github.com/hashicorp/mdns v1.0.6 +) + +require ( + fyne.io/systray v1.11.0 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fredbi/uri v1.1.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fyne-io/gl-js v0.1.0 // indirect + github.com/fyne-io/glfw-js v0.2.0 // indirect + github.com/fyne-io/image v0.1.1 // indirect + github.com/fyne-io/oksvg v0.1.0 // indirect + github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect + github.com/go-text/render v0.2.0 // indirect + github.com/go-text/typesetting v0.2.1 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/hack-pad/go-indexeddb v0.3.2 // indirect + github.com/hack-pad/safejs v0.1.0 // indirect + github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect + github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/miekg/dns v1.1.55 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rymdport/portal v0.4.1 // indirect + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect + github.com/stretchr/testify v1.10.0 // indirect + github.com/yuin/goldmark v1.7.8 // indirect + golang.org/x/image v0.24.0 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/tools v0.23.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5b04289 --- /dev/null +++ b/go.sum @@ -0,0 +1,161 @@ +fyne.io/fyne/v2 v2.6.1 h1:kjPJD4/rBS9m2nHJp+npPSuaK79yj6ObMTuzR6VQ1Is= +fyne.io/fyne/v2 v2.6.1/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU= +fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg= +fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8= +github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM= +github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= +github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM= +github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk= +github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= +github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= +github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw= +github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc= +github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU= +github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8= +github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= +github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= +github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= +github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= +github.com/hashicorp/mdns v1.0.6 h1:SV8UcjnQ/+C7KeJ/QeVD/mdN2EmzYfcGfufcuzxfCLQ= +github.com/hashicorp/mdns v1.0.6/go.mod h1:X4+yWh+upFECLOki1doUPaKpgNQII9gy4bUdCYKNhmM= +github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc= +github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= +github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA= +github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=