From d77e00f0294cf2baf25556747371ba1d72dbe9a0 Mon Sep 17 00:00:00 2001 From: smolkik-code Date: Fri, 26 Dec 2025 23:26:13 +0700 Subject: [PATCH] add finders --- .DS_Store | Bin 0 -> 8196 bytes .env | 0 README.md | 23 + bot/__pycache__/app.cpython-311.pyc | Bin 0 -> 1862 bytes bot/__pycache__/app.cpython-312.pyc | Bin 0 -> 1591 bytes bot/__pycache__/config.cpython-311.pyc | Bin 0 -> 1092 bytes bot/__pycache__/config.cpython-312.pyc | Bin 0 -> 1012 bytes bot/__pycache__/handlers.cpython-311.pyc | Bin 0 -> 23101 bytes bot/__pycache__/handlers.cpython-312.pyc | Bin 0 -> 27226 bytes bot/__pycache__/logging_conf.cpython-311.pyc | Bin 0 -> 483 bytes bot/__pycache__/logging_conf.cpython-312.pyc | Bin 0 -> 442 bytes bot/__pycache__/telegram_send.cpython-311.pyc | Bin 0 -> 2988 bytes bot/__pycache__/telegram_send.cpython-312.pyc | Bin 0 -> 2559 bytes bot/__pycache__/transcode.cpython-311.pyc | Bin 0 -> 2387 bytes bot/__pycache__/transcode.cpython-312.pyc | Bin 0 -> 2238 bytes bot/__pycache__/utils.cpython-311.pyc | Bin 0 -> 1554 bytes bot/__pycache__/utils.cpython-312.pyc | Bin 0 -> 1366 bytes bot/__pycache__/ytdlp_client.cpython-311.pyc | Bin 0 -> 4393 bytes bot/__pycache__/ytdlp_client.cpython-312.pyc | Bin 0 -> 3878 bytes bot/app.py | 24 ++ bot/config.py | 14 + bot/handlers.py | 396 ++++++++++++++++++ bot/init.py | 1 + bot/logging_conf.py | 7 + bot/telegram_send.py | 41 ++ bot/transcode.py | 46 ++ bot/utils.py | 32 ++ bot/ytdlp_client.py | 73 ++++ bot1.py | 330 --------------- downloads/.DS_Store | Bin 0 -> 6148 bytes requirements.txt | 4 + run.sh | 20 + 32 files changed, 681 insertions(+), 330 deletions(-) create mode 100644 .DS_Store create mode 100644 .env create mode 100644 README.md create mode 100644 bot/__pycache__/app.cpython-311.pyc create mode 100644 bot/__pycache__/app.cpython-312.pyc create mode 100644 bot/__pycache__/config.cpython-311.pyc create mode 100644 bot/__pycache__/config.cpython-312.pyc create mode 100644 bot/__pycache__/handlers.cpython-311.pyc create mode 100644 bot/__pycache__/handlers.cpython-312.pyc create mode 100644 bot/__pycache__/logging_conf.cpython-311.pyc create mode 100644 bot/__pycache__/logging_conf.cpython-312.pyc create mode 100644 bot/__pycache__/telegram_send.cpython-311.pyc create mode 100644 bot/__pycache__/telegram_send.cpython-312.pyc create mode 100644 bot/__pycache__/transcode.cpython-311.pyc create mode 100644 bot/__pycache__/transcode.cpython-312.pyc create mode 100644 bot/__pycache__/utils.cpython-311.pyc create mode 100644 bot/__pycache__/utils.cpython-312.pyc create mode 100644 bot/__pycache__/ytdlp_client.cpython-311.pyc create mode 100644 bot/__pycache__/ytdlp_client.cpython-312.pyc create mode 100644 bot/app.py create mode 100644 bot/config.py create mode 100644 bot/handlers.py create mode 100644 bot/init.py create mode 100644 bot/logging_conf.py create mode 100644 bot/telegram_send.py create mode 100644 bot/transcode.py create mode 100644 bot/utils.py create mode 100644 bot/ytdlp_client.py delete mode 100644 bot1.py create mode 100644 downloads/.DS_Store create mode 100644 requirements.txt create mode 100755 run.sh diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..24adf06d9ed9a28eb96c533cc23573fa60d87d54 GIT binary patch literal 8196 zcmeHMU2GIp6u#fI&>1?=0g5cR6BY_V$O5H+ZTT_VKjpu`wsc#5mf4+=4oqjt&hEBQ z8aF1sfM9&m_|KC@BM%0Y_@ar5=%b*y+5R6ok1lcoYF^6ePXKrR_-0W~5 zj6fKHFalu&!U%*B_%B3&?rg7!lid4U8`fb2!U+5?BjBA6F?u|i2xN&%|LCB~PXS1J zDS%(7O?iNC6ANS_kR>jCQ(BW}4+u;Vq!{4lq>p*iNhShW;*y&)aC3%WX9O7v{N2ee z=1*rxa2eKN1i}c6kAO!Xi86MF4Ke%f{M{YQSWbqtS2BZ^>E`R}zlKmzIc<7Xl~h$- zbtu{I3?=hgzTmcMxdUFW%WT!$YIO>> z?)tV#u8<^AN-L3(kqr+w$735BN8_=P=H`ZYY;D83hS5<;oL9GI)4ud^YruBSaAy!4 z2yEsU=gv;-ndKJSxMvl~vS;4}o{7?oYI&yGlj`j|Ag7f%MRK8YFmF1xyWhyUB-2Wb zORjFqI`*EN;puy_j#U`2-Lx`0t6ABCrR9wl%SxK33>vF(vpL7I_BgKTEz_II8^d`| zH%^u6bDdni&0j2K()xUsQgDrZ1}$n9jdohvtsUbgq>HoWFIxK0s)okRiH>c%_Fk`1 zX3v>hE6aTpQ#)@yZW_7nAyd!y_iAp|uyxZu+EX-LGh-QDwl-kQMqHAv*F+X9d@!o2 zlTA$(GUkbbnLnx2iK4$pY_TdI)ajjeIl8xtu3Q#r9ol`_VpTohY1(oI)p}sCVx=l~ zx#YZ}1rt}Vk>#%A1O~hUhi>K_UrEu} zOs5yJ!*t=wszP5bMexO?-?;eaC)gx7Z&5rbEIERHGIPu@rG^KqH#a zj8=4FFZLmcgXo8eV{kEyQy9T%JcToO8s~5x&*5dfiq~)fZ{i(X!ezXR4{!~i;d6Y6 zoA@3-;78oTpZE*6g=xZkAtEdgmI!fSm9Sc96q=c@NBwEnB%}ebbhfJ8Kv@wQ7kG)D2pS?t0DiUp!X}r5!ULg`;lm<+agu5)o0U;N5t%Q4$sK&PI8&M3hu2c~_MalB6)d6K#ujh(s$z{HO4K zgWY65v0vG3!uu@DMFh)HPbhx`Td|vP?xDOJhmnSc40H^@#vo1*;!k4~PoRV+ah4E& z9?#%eynq++5?;aUc!N;?7TzYzzlSS$A0OdkT*apW`oAUY|AODf;CWXW$z0<1iSb-a z=4{KckCL{`gnJFAM6ZIsWyH(>J0}1Ae@gfgzGN7IFaq}%0aUgp+gr(Yva`&~wPW-g zp~oxUyb_nb2~~bNPSQ`uN#6OxklHbt>e(a$S>lpLsQl|c1l;|7{QkdmhxdPY|4-@r F|8Fcrf6M>? literal 0 HcmV?d00001 diff --git a/.env b/.env new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..440e3a2 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Instagram DL Bot (yt-dlp) + +Бот скачивает видео (YouTube/видео-хостинги) и Instagram Stories через yt-dlp. +Требуется ffmpeg и актуальный yt-dlp. + +## Быстрый старт +1) Установить зависимости: + - apt install ffmpeg + - python3 -m venv .venv && source .venv/bin/activate + - pip install -r requirements.txt + +2) Заполнить .env (скопировать .env.example) + +3) Запустить: + - python -m bot.app + +## Авторизация Instagram +- Настройте COOKIES_FILE=/path/to/cookies.txt, или +- BROWSER=chrome|firefox|brave — тогда cookies будут взяты из профиля браузера. + +## Ограничения +- Instagram часто меняет схемы. Обновляйте yt-dlp регулярно. +- Для Stories почти всегда нужны cookies (без них Instagram отдаёт пустые ответы). diff --git a/bot/__pycache__/app.cpython-311.pyc b/bot/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a7c78ba0fdfe25888698dce337c690690e72c45 GIT binary patch literal 1862 zcmbVM%WoS+7@xJr@vgmo6}xFm8mGzuj8H0}M$VxEIf+{kiLE%Ug!Z!9c(;y|eOR+& ziB>6cQ6y+6$EYWy93T|ZBiuRg2YAsUStF_-K@YuI%B|wS%-WlcBOws$na{uPG4t*B z+HVty7@*G=fBm^Bh5`74G5!-8fs-8yo&Xi7oDK|5;*5|KGQv{WfD$w!QiNl=kUn8V zrKpF)I&TP)V8o=D5trhID2bjN>Iow$B`J>V0x6}X)roD-3lc*>OC3@_N4|GJt)lDR zF7Pjzv=AW|O6|7ZYACp6nTws4u4;%Rmn_3jO!a-bb;hr1wyiWZpU*BSy1t<_w%0lu zy5~!ycUwBvkX_(N!q#x7E$ddZ*)p32NRsvPO8IuJv?|xDAC@abT&&jR#nO6NzEfKv zsikVAQeLXd^$%9d)jM?}*2<-2pQHEjBOi9-G!_ab=%yJ4C%ZJ_CjfJS@_gKw*^QoY z-2;Flz6OAC-NBhtfeq#Zd`NvBo%%gEb5mf0#Q>kPj|Q3m;dFq{*$aUtKsX!VbN2B- z6QJ=JA8Tn(rgOm%+GC4*cVNdH*~BcqYJG86S^HFZj|F zxKA@o{lDf*{-^(ff8M(-=X2xlKhAm4T=Xr^{*Pyo&Eh&JgeoLrV+G;DBw}5H5{$4e zA`xtDYbIh9B>aYAYjOwahh?R|qNK@2P5vvf1Y_I_(aYa?- z&Eae!NEIl>2x~sXI*P!0`J+0CE>%}oOO<63vrO3-PB5Ba`ZyIdMk7q(sAI})OV`7?dy&(>k6~pnMae4CkF`-;Rk6-KC6CO-LD)JpO&88cCt6z>uxt%(WdBb5oJ9rmL!dnU@hkaA4l|`+nay z@4fHe{E*3{0DIoM^T(DJ2jF+Eh$j|;<30nofC)^=0Z7s%6w_lUuE&wA%P65IByNj2 zGbpJi0~~i0r0Ob4=_!=f(@4{`fR~*N%IaB$6E}dKv-0N5_E`Y15(8H5Fg#8IJIS47 zN%@oOr38JV-0wSf#~`-nRtC0XT9{_59zup|zR6yP>vhZbjjkp1Le+4bO{24YX<*?U z!6vWR4zaLbl4!=a$e<4$uiLfVZb_!uc5S1!-YVB&yZLslLA6S=4J+k$YVdMvgXXHu zMx$13!}eSCTJv(7YOPxNqR`lWN_d9f38M(a3|m=N!SQ*P%Ugg*!WhL$xIK6T_W>Y@ ze{;R0U?NErfe*udduhQ$l4t@?y>fzyB*_ar^?Fh;kt79yr(W}diR3g6>6+9#8Sw&< zrv);lYoNCvv`8L?ohMF_z<=ztB)%a>_$afUCvUuc4O+m?$}j+ITrx8MLf zn2$2ebv)O#Is_6MS>AwP-V{{BmSIjQ-V-#j$%l!i4by~MlR3gNGhp5yn71J2T}S11 z?cFv_R-5&Dxp9%EJQt$Lv|--gG|h-kIBqnJ2QKV;j>D%aMx`wpH~M{_7qnP>TwL+` zWW{z_R9$Q!XgYAyBP;x6Ud#O*DkH;o@oDDH&-VA(aveO7XKyqgDEXVI>*a^asiCqk zQnR~rpM5w~Pmk2u-4lB+4%MZRdSZ9!^QEC$8mV);3wzE`JrnrdoF8XELK)}T@=5cf z=3e8A)&27A^@H_c;guu#)nDY~g#K#v>$NY}zFqz9!Z#Owe7W+|nacO&dmDE*hJ{N< zaw~K@DxBS)y}fv_I4rCl$*+a%eR=Mf-IY}XLLLHyjnXXDh~-#3(q-#?5>#%I;UKv@ zd6Nw1Spt^BUlC;GyAqU}rZ~-2yBkzGsBchC928LF_ONpPq&axv_!V3OSr8ro%qIjd zvgNU|hCFlNSg+$3n9Bbc{&}{HCaS_Pcr&oWD SpZrx;K7L~y2lA<4$G-t<#71)f literal 0 HcmV?d00001 diff --git a/bot/__pycache__/config.cpython-311.pyc b/bot/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c450cf4c1136158927ea452124b3b40310dd42b4 GIT binary patch literal 1092 zcmaJ<&1(};5PzGb`P^j9CYH8>NbMyE-6oYbO-0GaDxqn%Bpa%!xFo*VtW9>yX8owA z9CPSl4*_%ZR3r6J#6Q44VOJIm?^Mu>H=*F6C*Rgqnppbgy%}bh-*0Ai=e^(W1=jif zqYi8Ud_QqcmGjgdeIfvO3m||*1C%XmYYfuJ2a_Atvnet%EdF_JL` zog*1zIKwuQ<$~_vtUcVpK7OaH&YJU`>&!cEp$B?CdWW(9b?F;sQ>HHHzz4};mSK$K zu`qkM^!~r{W%tMw{!xyyh4PMM1t4`#9YTrwkMvIXRU@++w!S73{e*pO?xqSPd{h1|8*VoMd|Ekvd{ZY9eWh5OkY zivV(^3fy{;Eg&k*<@0Qs7x?Td%awTXAzm6p7Ff)D7INc%9PuNY+_I?EdPA%#*0Lez zW=*Y0FBJ!N6l+ZtYa7s5a|?=!;0Q?&8G~X?ifxmJRH&nBSuNGg_N4xn5l)!lgh4Hv z)N-2~0P-84zXG~E?t=bj!8_|P#V|>vZUo_~r mNyppNfTBAsZApuDmwHe7yb+3-p_oCHV{5Pwe6d^$08OVxmcP+^IX>Lx|~2ndu9gRG`;l{zXIgq83@<2JD)$9#k- zZ0%HMO8$gE8Op@Mz{J9m%9UszLTbeV8&bDUJVOl?D7`1$y*vGWzq^y}V=(9k*u;0= z%D@4@S1Ym2ef&9mMF7wS5I~{=L@PQfMAacW2uvw~6&q*saD-0i+QfR6iLSn=0qFjN z8o($=U!(_mZR94N=9jL!t5>l+Eyo zw3Oy@$OqM@N<}Tex)5<7PpzPp6upkTrDt+om(*26rg(miV;7}cT!uved6GGPX_3t# zD#d5BY)X&>Zl2}yf_w*`2_kbWR<=uC4*-!Ma;SAnS}#}RqUOXaA@_1gFDXwn7fv+m z>@ROZB1>`3{Zv(n>wR^b-=pDf9 zPu<)qzLnp|??XFN{JMUERy2pBJ5+2tf7i_4!({)^+So>9Jz{Vj&Ge5oJqPqzLpF+S qxh3~5MzZOj_6_H1jm^Pnr3Lot(@d_ig>c$J@!FWja8 literal 0 HcmV?d00001 diff --git a/bot/__pycache__/handlers.cpython-311.pyc b/bot/__pycache__/handlers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54180e9e204ad64c09ffdd3da26690149dc89abe GIT binary patch literal 23101 zcmeHvdvH@%n&;K~VM&%HS(c440&K7_*gV4{G{j&NI}n?IO~8RD##b1DWI0z74@ag+ zLeDq}Gq`ECak4OYb|)EU!cIHsc;a*(oem_M{UcT9x^5S$yX#u2cT?NF|EyE#u1%%3 zYWMrj)#FOOvQ5%6Ra3Rs$LAiM_qmVn_r1>jd#lyVz`^~W|9kj*)u?TnGALl}%?NDn| zbG!dw=bk;Shno+Iw${fRA8pS5u7K%q-90f`<9IZ{^>v2>KDB5*9uJ0Cf22Pa6*XME z4+b(v1AVOjOppykQt9qE8;ponn$Jdh;^9DFELGHhG7^ix!pmTczGzQ`4ftabe>ieH z7z+5q{Tm0$j|ZYL+S#f)Owzs@Fg36#Q}ooyK(O~@Z2OPat=+QrSboLa(t!=Ln{8e{ zM=LB;YP4~#p|B9Hg(lp6PSoP8L=E&KY9PNydT^vT^B(*c_IT?WMx9h4XqOY~4;VhsWF2{mhkbWtj8g0zJ##b0a@lxkVa zC0&;_RceJ&TPkgXba5(O3~4)S50tPa0|tg+9RUZlbV9CkR;~+jUGxoci^i7jJ~kBK zqCNOvrw@FSH1Pk83!>{xkV!E9bQ#bBc(+t-@PL@`q4UEl)r~J%G!Qxw#a_JMt!UsP zkyu0a(Pl&bCsPB~_n#Myet3d|F~5JHBt4J%bQTUBwL)}}NtQ1f(q6U=!QVGH z4N)JLUSva$^lJ9tWYdQh5--C4H+Lc0%czi)sRlK%bUB>4@|bM7r!wg`pzN(oJx-s1 z+dTkt7}WGFiebO0gDZv-jXdYy0tvjSN!8UssZL5KRHrg?8Q8zP0?7#mqTWocdNa93 zK8F5Y;9n%aCLfWHw|U7sQ2SjnK|c1@!l18^x1q*GFZmT2=l_ySU}dN>?py7Jj2EHi zB-VU<{|3yNB=3=r$=h_o{{($}L?-zwt6|QN`wsaq)qIkC2>rhAeSH7M)tO1vLX~%+ z!FU6V`aCZ5MHm6nFW~qvV10ZOTu1ZFt!u;C6~KHaq4uw#@dx~6@*d1+0#@@pf5qDq ziJXSj*2m7p;%k*!rYg``d!55;0iv!osOFn`={t4n&G86FWID}zQA2=6`#-dyi{xF0C(P(#XKs0lK{?K_pVvbMC zErl|!2BQ^NON-f2e3HCyaLX`H2PBw_4cId85dB>SMbRq|{T}`x9a6Qby8ZC$4?GW#3TKP}8*4HN$A28qGcMrGdB(NP)K;zzX-V z_F>*~c&I69b6xJcXInXCTREoXZEFPE8e%LMxN4YF&Kqlj4En}a0^yO~Sa@$BQ#!Km zXTOS7jb>mzU5@A6H+N8z(2Qo@J9#};o%^;$m&r#`enZ)L-%wVY&;(Tp=2RLw7Terz*_d8cP7Nb#nx_T;#ymBEb(*J!jV+$1hMg^$U87e& zsFkm|z?F2woY^%ew7K`tzk7Qq#DloyNRD>ifMXF%>>1DbphmOKfx2>NayKZE&vKq=Y;e?`;#NUx%2gRzrb9Ts}A&~$=}g#E};i0UBA;j6?UlLf>wmj-f3yG8Zs z<58@GWH3bs&5=HTiii$4GsG~PiB~-O2q39M5tGHGmoEs#%SdtU=<$2DhACSEZ(AqW z)(vTr#f~c%UcNx8Hu1#|3B?c1Flv)C>F`{uzvrl%a@3L4LEdpnaGW9*g^;v3hV~>k zZ=dWL4W%Yp3ElUlpw=}pMB&K?%Y>C zp*fWy_E|Ox2n}-z0gxV}8IqgM!E-_zWO9j#ay?cBfB<7;Wye8a-m(On)tsg?-~zGY zZ2t*eF5!wU2DmY1%d;sqkHqD!51G4yE%U~Zw;a?bm{A1HSw}*b(C4m4-cDjCbJnL% zt#50=c^GonD({cZgX$CVC-T7aFsA0g#gJ`D9ju8p-!95s4}hY)1_IjLH4xC|u7Q9y zt46PO&`|K2IAZ1LIl{A+cZKGMG5H-BMG8mGjI)#=!l6c`TlH9#!+q!;u|5n_9^ApyK6{Hj1cR1GMGVK@@L;D9udW zq|yrrZr;uV`_i)D_%?5v^E!L>Y<))1ukVS38<52TF{_~ybYN|bk2g2e*Vo6FV3${Z z=_S{J|M(@aBq&z_Ly~5-pbobY$KIB%IKsb#0-q%MK_g_`CP8KdK?)cTCGHCQK#4HE z7$T4gfwj6p8$J&WKSaI<1VRwW$b_hS)(>hRZavmU3N~}==dXKFC)6+MpY>xRjszBX zs9$+ONz{)G;GE8U+Y3q~nhEFl8mKv&Gs@%S(w7dz=xy9qXd$Yf^@}P$hrkg>4oC6w z$c66#tq+&@5H>(wM!7*o2FJRav2K0Ax(d=cc^}(t!FI^FDUX5jw~R|9)aiD2txE&aIArdvcftaX8y%C3$Oe_hopae>BZdC3SZEWOhUntVe zN?K9TmH7>1OmU=k zw_vXs(j{%qE1{P|FZEsSBgV?v0m?5&1e+HaRht9(IPa9rJL()g&f9!~&3DhXX3Dl^ zEHHk6w`~z@TZT0Ei|m)Tjg(Clc}bCX+UXlx&O7S`XFc$>*0Q8~(LMK?Dfb#ue}Q-Z zSaAQC*cC$3?jF`BJ&W&o8m2rAWbG5Y=YZfjKpgpDXm`@#zUQc!a@36WjBVf@4T7VA zSPF*wR>$S8k%yKEl4HdF^mIwhXy}t#QnHsX*(;RnCH8%ob^4PsQqszov_F+K_a5u5G*LTt4MoKDv9Xgm=~n&bqNixWS;D$=3u66Gwi?yL;o>2fMto;W~Ivr{L)%j{J}}!~!3X)cunU@C@(i7Cha= zksne6teA4F7>$nY<{fJV$68`37^DYe+myA2SZibh9OXU71kW+z$PcLjYNs5vW6rT2 z-mzYAtS6R&VMfQ;;OVZRf)k}5ISMd@04hDb0Hi>Yo=l8fMa`;W8A%8fM;Yk{(kLRQ zjs(~!ZmZvboVPN$gP^Deb+nc$oMbXXLIsNF9LYz{WgUf*K`qEajI!JWyhWbeBySBo znx56AxSIc(&~noW?Pz93>Dl7>4fvNS-2YQTn^lp4vQMupRpqvd$+(|0pel`_G9z^) z)Cq0Oo&E(^%acvN0ax*@%$1bSA9z;7puR5@Ta>MxudI^SA2h(-LY^aaz^I1rJ8Hr( zs>o-b53)FIje%ZvEVW2oupTZTbfXh+S6;4pIK8v4-$lV zE(szf2Ndd|D0gl0t1Go!&cu-ANgdS;=L3nRP;VBLXl~}IdUN|c0;ZKg;{_vfz*6e0 zDt&!?*0zo}^`MZ7a4DOqJ`#_yK~6MF7J?IC2^9^;xyV`2R#E+yXonhpY|tO-J|6vuPPlI4Lv2G%%b6GGh{vdBxlwQ*F+c=;jzg?eB7F>G_`eKcESSm%#Wfd}># z@;+7Lp@jmtI`)DDH(>A~AJtP1J1rsn4L=0U-j;Lo+q^yphwmka zB!?P1sF8z1^{?piQ?qmWO8LXVa{-p~VUa!>KXD>>PSk}W$T;HT#-Y5=SujFbnZs9@ zb3p`VK(ynZ7EP3YM8k+^YChK!=#K>>eQ6eMEmlWPR@739xDjt&KR5)4+F;*_h^Xs{ z#KH7!!21XHNK^$BSUK)-=wP5M!@^}4Myk$z8VaMGaLdw`mU2_8YTCJYOgnM(%g}Gx z($VGZ+lL13 zyDG1>6PIt4yXRax=}hCRO=i_|X})wsgyMRqa^W_{xuVez}jYIwDjZAs&U0^e!FI-S;fH zWqZvwUOi&tJ(~s3WCnZ@*32J=cq`7LRQlF6QlP1^ZfJUwgmUHT>+$=ZDTG z-HY$JS5CQClG>wR1R#;8Df#oY0?q}P@cz0ZI$BDgQ_;$M3GsCFhcFW_Wqx_y@ z`IKWhsebg6Mu;HrjuyeuLM#gk-@-AY!EPZDMS1ViZug!d=FXz(CO30uXBnjLx;;(x zn!EKoApKeWB8WdPGVj@;`Mk7h&sNRnTXm3DW-?a5ld8mIBviRX7?rl5+EkA!DsI`l z+PUPFl2vs=1F)dEsTtJ9WNKPgD`#3_GR0}k3`++Zxu#zQotbsB!GR0X! z3o2Py)mun5=^>vDWoGoC&SOS1_P2CBJSPluGBd`6k*h9v6`&N>Xre1M-<(5fn*l)O zt64;Qu*?sk0E9>tQwIe?Q^DC;6Q%_Lq3MDt1%!~20imrez86MHQ~I0K2oKUUOr?(bVNRM!}*aI5bQwT`@z4wtgl5~VW@D2bwnq0la z{{-hOc^QB&!Gb|ieyQZ3NUsop>U%IuR`mtgOLbc?S_YA2-VFeuCawhL$eqNf5SVf- zmIfj6nI)Y?7VwPe`yd*TBSF_P?iAD%P5v|pav^LU#wY?&6kouM7DF*4Sv&Fw5JP^+gU4xbI!nPR9m~KePPizk zjzi!bX9UL?Vp&i~qtjW6PRNX2TD^NQ^Uf|K#Gh_o0`Z;2<|e!5&dRE$BF)_*9i%xl zMTkYHDhtNW`%eeX_he)*C_xaQ67XTU1Pqz!Lm7k;Ic{C7Dr@h^g@Zf*QZEpIG`F?+ z0EAG(>Sh6uY9Ro`YCvYubGH+EWgyCxYq=4*`v=(d;Pp~=6B{&v{E3`(>HzuE6hjU< zb;xDWIi+4T%+EB6toE#fRo~8=fr^23EVyppI*?WWnfgx4Z2<5xrXah7xdFakl`z7( zjU2K%sE?j?uz40fIdhSLb>)_MGW+b70OwWN0V$lD3(muoFfRzs%@@omIER!B&dqJ~ zHSE|Up(_F_cN`+>FGA5Z>xu&So+nh2kC#erHa7wG{|>P1eewYX&>SJsi!L+Z00VMp zKH^Q*{|R+7X>LLWkpOmfvFM4;#(k|FNBoaB?&luExfKLGlohjrpiDUI#R3Y66g4R> z7Ukh07l%rs=0rF~(U3chZ4d`VBgmyOe|O(`t`GAlA|l(6MZ^>&?uSezagZ7uhM_RJ z1z6EE7Ns%gjK>kU^MM(h5=bCa8dhDP48OK|KO+O33Xom5ObM8(8IR7^ZX#YFGI z#l*5ofZSs6^fOpDjkMfqf32ObTq9Jjp&8TO&67sn+bDP&Nkx7bHq5AXC7UEcuy^9e zUt0KzPNAZcxD-Ni$@1Zr`>sXT+pe~avBPb=YlGn0KwKM=uFC7}SKG&8!|l9lqu|;| zXaK2(DXm;^ZIFWW%#!8zsy0tmZJyLl_VQJGgsMHnGb@xG{9?;c%Y9t2#@efrA#@w> zZWr9`#9lCDz?jqqc=rjxeS+8vhBT-)Qcz9DGEm()2@>B<2)yGF!SM*OEGVQgwJ|I4 z?J8}4i21aldUqZ3>5es!zEfAe@r5hSN|Ma=7vAQBinfb7+BAph1x#5 zB7oIE5!HiA8c%Z&)Vkmh+1C>Qn|QD%CP5zpr3}|7no)40kY>z>G!*NyK*k{}Ex{Ku z)@k^n-s0R7kfVfM)Hr?{3ZwrqN00;T0#&ANBJ^eAw}B8jGYHbi6Y4`0qn@M~^(4ip zClRA8_A7f|-kYpgm8{!I4jv|bQL;RSVOe}erzy7qc`mno4K%v=j~F4fF8-5|sa`p+ z;uA@`&RUiWW{@#mzh%<(QH-x|66%{sdE1EVmghCk=t{nPl~BHll&|`tdRRB@_Kuc~ z)s8n)J*4}H;66g^3Soh`p`>_}B-X6vubp@77aaSErC|6Mff8gcFYR92auEWN@@y^iW82-T1|QWXD6 zeY^>qrFI(0WF-)=(EBQ1xPtZt#`l#d;t(nfC+3h5+ zDL-}BPuuV$CFMe}2{#J%#R`Gb0T^e%Gh5dvjrkiWjp}8RRjH|85uFN2P|FuCK{;Ij zX9zla>$+Uffea9Dez+0M6E5{7u9tutsM}&$a#HA3FPzQElja1*O z*g93Qb+Y;s17Fc1RJ0IRe&8)Fuw$#(iFo_8Tsk^Vjy^+bk3-)1syw%-y9l-eGQDr}@lS z)x28s*=il6mBH;Veki#8x!r8ETo4HRLV&F^CL{mUy+uagYb&CKbllHPr=LR59$sye9sY*_Hrt>P+3a&8^CKY8MnVV1s zdX(p3{(;a_l#+qCzl4h;nYz4?q9BvevCUG&Elmj})p=FXgJV`@a0Zdwujka;rDPWC zJXk@|0JXl7Ge%Y}p-Q)f001|Dt*LHpws3L~$_6TC)Bm(%t_bDP|16VFoTyt{xO}qs z0T2L*zcydufQ)JP^3g}f4pY^byHjv?61zf>i4u?T?!AH=oJtjf5>cn31ZciIi$RH3 zwQRxaFA7&0>s~p5e<1Z&EAM_xa6bmFu7yIHuBR1W==RX4lF{`)hx!Yj>Px%onyk#H z>wX0BowbV~zH2o%Gn%_)RZS0T?mn!8G*#NRiF(+b73cbLNWB5Np!THS!?Pl(zU2Ot zi5!2*T*XinyeNTlEwIrze*Du?&8P0lw{^y@Y<0nkMGhKMn%x zLU(245KC}OFho_C?Iv(U$nI+cnMJn{K#0jKFjQudamdsU5m~y_A@`J=6CSjzp2`4P zkXcX>kvhJ2e+Fd`U=6c<8ZTd?K8=c?p;SL(`_aOO5=?EBYZnd3Vd4gXS77EjU$)Rf z1~R_ijKDp(p5tSiVB|iwxz7amE&v%Y4YQAYHyHXns1l$u#Bb*OM3c2h!V}Ors0LV4 z?k8{eRlloX&AdH@=%&N1jNucXI`Tg$0XrtO%F5b2sTu0^s8t}hT|QKBy?HDGJli_( zxs{KeUt^)8gG7wJmz`|eR$F_tWH)`juH_~wf{Q7kzH8dLD1>Zlp<$ldQYUa!9gz`F4 z-Z;kn;>GGQ7 z^6HTm{QEXF{K#{BX`N772Ln4xp>@g9X;0#n3YNZSCX_52+C{Ij z%32PHKchBS>yj?_wbtSGQO7;!sww9x-nm+EuAZxe&SLH z$$SygG2#C##8(^?Dh?8tLbz{tTsK`cjdl#1c>5~BzKYQB?XX%g8(b`lokK zHLl)WroL0=+1;R1#@hdx5v%@%u~wVHTKL+Mgtfz}WA$^QZOJUO)h`5X(NEydhPc}7 zXMPe|){sKnTQGu5F9?_(k?(lU-)#)yOQs*{2a*LG(7u5m$U^Zy(+lVgGqVAY4VkzJ zFHq`WjkC3leY>cK$Aaspk$cw8i61s&!@_4gth!uExs3;&Ga{AG9Dfq1i8*au$@NU+ zuTaOrT+c)l&wfo^6~T1^KT`xQ7O<68NpXitLD)e`e&q!-6N+>nua@l~_5SYSHSkfy z-tb!-@KqB~Jc7?Y`4DPR&wXf`+3iYe(_;8S6n=9l;voafIF>EmiB}waaK#s= zrn;{gi00-`?7*K?RNnB7G{8QQ^0iatYx(l^Liu{w0qiWl`809XL3m&$C0V-Y<^@u^ z2EsJ#9ofZK?-amSrYb)?*xnIO<*4da(}?zg_m4O0KJ-8hI&W#F9q9zD35;D}(h3?JNE4B z4y!73t*SB241;Hdc$-6aRyEc$!{AvVURAC`tro`E3!XpOAe|NBEsj(@JY#Qp@hZ2j zUo{2>7d$J(>s{C$`p2_ET)#}`8t<5Fdi$vv29jUEyy27|wWu0&uCdrS`{r{q3?#pR ziNmR2B+*Q~mu`!afAQ5TN|Ls9y92ab|gWa{X42GMdp#)kU+J)s~<g(Tx6N+HSYBlCqMQ%`ckjFGYVlGdiAbr%HqJV7xy b>Ogd{XvX%eRt1lSa{M*T|6_qtdTIY3py(yR literal 0 HcmV?d00001 diff --git a/bot/__pycache__/handlers.cpython-312.pyc b/bot/__pycache__/handlers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ea13033879f2e837af77f55da690dec1fd4acb1 GIT binary patch literal 27226 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 Z{{K>=$dNsl{Tuyx1#S5ig(11z{{s-rtD*n^ literal 0 HcmV?d00001 diff --git a/bot/__pycache__/logging_conf.cpython-311.pyc b/bot/__pycache__/logging_conf.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..986aff8e3ac88e33c4383147a1baa270693050f9 GIT binary patch literal 483 zcmZXQJx{|h5Qgm}EgvOi0kL+enlQ9017bm{#1aO!A{4nv8d5n)WxGf)b?C@m@drQ% zehxzwsS_-0fx^;>vy~Ph9QnC>-}vqlKbYnwg7x|Rc6kT!Rf^@(ie$b6$prbx#}svt zuZ>X$`}hpm`nx>hc*Q{#44@8s-UdvNz{*wVuX6EgqW*FY^ncdwK|QS;3{&MC=cZA& z2=_!32R5&r*DV@c1vDX2UexMV9B@uT5Vn?;)Vr+QOIb{Wt!HaA4Z|o2vr3on$U9Dx zUKD1=iPJuPM05Emi~SZ$Q_+eNE=b5o?E2L0f+2TN?Rrqx99(B655#cb7B<_0dile= z2^gW7QF$@;X4TzWI^90VOM0pNfdF3;`Cl-3`L@Q&5L6$wAv5W+3ShB*7^nV_21m>U ZDX)(=0J)Aao}%hVJ-K0E==>k5(GR?GYG(id literal 0 HcmV?d00001 diff --git a/bot/__pycache__/logging_conf.cpython-312.pyc b/bot/__pycache__/logging_conf.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8240cca1f156c6e9bf12d4dae87cbf91fae1b255 GIT binary patch literal 442 zcmX@j%ge<81d)4QWXuH8k3k$5V1hC}3jrC^8B!Qh7;_k+7*d#87@`;}88w+-f)w~A zLzRIk79hzC#Gn0u#B_!dMzAQ5$by*$69CdBaCR0*9~wwuT*I^)s*91Kl0lQ%uS#82 zBe6KSBr`WvvsfWoRU;?0EHx)D5yXvEP}RsyEiO(>2XZu-Zn1*pZn3517v(0FXfoeo z&&f|u&&*4|#hsK`oSE#LpO==Iev8G^&&_`&!)K7?zclsp3rh4e^NLFn(~A;w<5P0t zlk!XSK^UqzJ{hP@ub}c4Z*gi#X+b6Y6mf!BAkiXjAn}XCCO1E&G$+-rh!@BOgP@V`Wb<2qt3)R{&5|6QpcKcUY$(Sj7>t?0 zbZ$^xGPoW#_;f^ht%~?kq@o)Nq{ijN!|jI}P?n zE3E*8q;lxE+g7N)Lr^}R(73j5gk}U8dnG5EnP<^FY7P%fOhKlSuTZZ?p1n<58 zxA{5yfLvu?b8)jUUxo-PK@`-EljG!7;_!2!PuUrA4!(mku*cep#>f1SJ@vlEed!ZZ zH!MHl%ohrl?K&yPg%jt@{EUqUv?Bl-^SM^KHOz|%Uv0Q?D`FNhay zzubGN_j`9W+Or<*sgkgyl&Gh6ewV$g_OGe^UZTT0dfe+d;T`F(MoD~NgGg~@3!zG+ z=!1YkJP}mD28Q24Xz2DGZEtVyyCxPk+u;~X{YizD6`oY(aytd|CD(Mb##jb#s@@)r zQ_@3lNmuQpE0nv+SK+Zo$gq$u31Y#~`?9BF{WdPDFGoGoLf^t}l{p zKAkg~xWSva5fkYpFz3SbDcEHrr__k>Ea+^6E?oQ-;30Cw;F>z4UY~cp}B1GkR1}ezG+*k(F73j6rs5m~` zxyth+Yw}1%9`RzGmnSbxF1@}Q>sycYVOdg3NiT8a%1d_>+FC;My1Ko-VXs%K%A{jx zgYXV@3!zGQ>_NaFn20C<=o|F}a?*GLqwgG~QcNxWz2ozm$a7jL(%s-x@bXeF}m zBM5x+h2#J$5AfuWTz;7XYKkx8Kdh`dWg{>ANWJ#kNF<+`c~=yEpTR$nT9OwRkN10x zYyuMTv%eR4BWLOJS>sK65Oi?q#EAh`8Ku-CPghzWkMvYppDLHBvNTgAAZ{EM;tms{ z^5Iz_cSGVnhg+H3r+%JmajN7$-tC8PsG@VJTP)o;CB&WAYK~{qdpjs}jN<-K;s#OT S4pE9N;-EI&5A7G>TK)wpE@#dF literal 0 HcmV?d00001 diff --git a/bot/__pycache__/telegram_send.cpython-312.pyc b/bot/__pycache__/telegram_send.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bfbc6806124351077c90404255507d4b50002763 GIT binary patch literal 2559 zcmcImO>7%Q6rS@VBye#^GtJC|=8AfHmP(;kO#hNXaaDm3$TJAukb=&V(pA13HL)!p@Kx`U9Jx3Lkbu#@KrbcZH90 zs;0XMbGlU04a-cM7M!+_D^3`AI70%WFqi8kt?2UXWX5u%rdCwbzTFkUz)c$v1Ej9+ z(s2{vZfskJ`}LC5pDUVHX2Qr6(rP|Erd$2^Sz2E6F`V&OY1WA2JV@}c<_AFTlKY<# zC(^vwwb1powI1p8NLYxjP)BZE%rE3O7mfY|~C(H4c&UeRt60dWN@ zXXUlwY@@TDEDVIl2m=9wpvGPS5v|b zUVqTUXa-#q!@+lcw~@dJ+G_--dpx%$+Mmd+MiN%a?wYE2m_ zN!7E{1+8ege*G$D3R;8zc!U39e%5ZQb6<5Y?6sb%RcAc|n1Q4X2UQ^dnS*_6a-Y-M z=CpP#jb0k9J4|`>JT#BNLXd}D^zw_TW@b~GiihnGM>!k;*p5?_n}t+|Xt#-gJm6lu zc)t?2K%Mw%sbQN7QJjp=rlA!j26j2jMdpf%tLryBPm+?(@0%^BdxUHSxghLj%8w15T`U zadcsH`Q`OkH&$qQCFLacFCD#fbR(gxB^0Nv-RbUkx)e_&$-@=Glkz6vG1)`1$-uz3 zx+jx(=knVtr@uRYvuRx(+K`6Uq#-W`N>Jl#T3en>BExjc9E3ex=szjZ9}lGlnVqDm z6JfiBqr9&JV0(a`40HRqfbt27_@Ho-iS`>3AWGB~@&7?SH)fzGU8(kb8CYjrmMdER z+vPHNL#s8Ad#3(FYjBD>1(X;xS6;9kTBZ>GZfrqstQ> z0d}p$XSbNoZohxFf~c3+p>aKX^VH8%HLWTyepJ_9Y4@>fr+jwncva(UCeVn|VTwy4 T$2F1TqR3&@d%j@202}=qmSF~= literal 0 HcmV?d00001 diff --git a/bot/__pycache__/transcode.cpython-311.pyc b/bot/__pycache__/transcode.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..611ea6c8f5b31af7af8d7ca2898541ceaf732816 GIT binary patch literal 2387 zcmZ`4e@Giw_+9RjoEMYSAFb`W=`q`_aWzgSTM=pPT(#Y(h?O#=U3%tSG&aeFyNlIK zN2+DhE~7FS6}B<|atu3gr83)Ew?DT3_Q$&tXgClG+o1a|AcIo&$G&&Q#BBE6d+)yY z{d(W`-S>U>t=lai7+wGTG5o$Aq2K9YdvJMR^8)~GB8)H-K?C${i!f2!fQ{M0u>-6{ z;|4ezlGzc^vo$cg$RM-?Yiq?<2oPSZ49v@?Jy^=m` zIzpjnTplu6O%0k3Rn`+~ELczktTnKvVQqc|*iX3k5CbMopanqve=GuOseslhb+&K- zrzX&s=xbZ4sZ)SmuobzA5dIJ6B%?caP+83BWySLZD%Fea*nypx$7NW+E?kb?*n_=z z53azKxC&R7$TFp}Lwm1tGhB$ld#~}6Hr-XcWu#z1si%|d1iRoY@)aSxZ^E_+{w)=J zoJM%xhwOQDll_=u&;+wkWZnUl>esw_MUjYWO6`Rh(oefT@fLg{L4-lRAuC3TtdM(T zo!lX-0I!qVWZ9T9(jow>fLtN#0E%aOTaSvyRg1m`Qdf;BAg>vplDmKw$r@080L*)dDeJ%ED(z7;&@`TwdM7A9vf9gLlJ366JHBSny%@Rs^=*uPj-sUEypffNAfw? z_anIt%9o8 zv`{FM9oHhvVf!sdX))~95HO;Bs>f$D9h#1ngl@fMRaL3sN$*z25cdQX``zRxLGHp}}GF+CiWPf;0_ zR+;I*au7&h07r#QHW9^nARy?{RVk)XNq>CYrw8 zqGe$P%XD1{>%##p5ekLJOrfjmZ11V=z}shf;Af(%rc=_!W5KXu?irGG>SZh(8wx}e zC2o4*6bQ!x@|YY<=n(e(V3*IX(gZSD4AL)Ah@;HvK>#&Mp^dFWeUVaXo)cEp2UMO4 zI3W3-K0!}V3gs$Mx%Wof^|smebbAgl_LIzpr;dmxR_kGdX?Qv^o({ryJQmzHnyxp^ z`wZbwMmUt}{FATD^5rl;3(nQg4yT8+yl1W~%U8^`QHaa()pMaNUpaRs%U8`^pg23- z92>lArgN50^SJ|Vmm}4gt7YvHlS8}je+#I*D&wj*T!%BR!>M<&g6EM?`%tKz*BAN? zp&=tQ5N?&}XT6@gS%&=! D#7djh literal 0 HcmV?d00001 diff --git a/bot/__pycache__/transcode.cpython-312.pyc b/bot/__pycache__/transcode.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59d7200dea08bd49ccb8afa52191f03441324ed5 GIT binary patch literal 2238 zcmaJ?T}%{L6uz@FyF2W!{3u#ln2H(~WmhPvN{m{lP}@j|5@QJMI_#bOb9ZMma~EL? zW(CtiC4n?)C25-SYOpDq5>b)1wn-oQGV3;BGt$PSeXwsN_+WhMxy#OCo7S7X_kQ>M z-E+>|bM_af(}rN2TY3^cXGZ7`dN2l81nU^UHG~mnVyKtyrWg}9^_rLs8r#bjcw8@M zLJ}J-STu}m?3kTnHqaoWQa!fUf;q{A&5|j|VapY+*E+;_tbb9VE)Sz~-69F=mS8ZR zltMbIDgoW1NF=2s0!2dwbJehp!CVgm^<(Zm#DJs2DEgvIK@g$2;;y;~TZ9nMF!}<0 zX)>G}g>w|r@b7_35+e+;7+lO^j+7avVPsgrW^BP$%;Pd_!*=YzPVB;Nyakuz3S5b+ zUeI9-ozT`#xN$DX;H_8q5fiZ+R-+CBZSF=SBkV96b#KxQgtrZw=1d!>F@(2$2=RVy zy2gHFHbZ`fnU`kAPRdJmstxZTFU%Qq5KX@RHG7fqY)>781lt4{+E>~_HlrN9|Ma z@3wXejL&B$z?p^YD7|ZJBh!J@fPf)tIx+6GLSngk7w#+>FGi}C8#V!-LLMkCm?Z8O@!J)+KvVgO)&tEh7{>VWYM=x&8s z33L5-SUzK}AyO)_Xz{55qckQMQo>QAkCZ3~y_|c4nJbp)2rcGP!EGAArV9rVQaqcb zgjtaMkIoZyZfY3_bJ%>tVwA`~8^93qSi3wX-J%jKr$}L!6h)yyrrjI;NC=9MX!^;_ zDH3cvq+24Pgse!=w=5tlE2_t;+tgG)e3F2qsyeHr64264cAwB~#V#geNq3x1B}gPL zouE1jtvlUdve>QOHh+Xbf8v#>nNpjG#dsY?@=W&$@FzVTi6 z{nYTY?+4$n?$hI3-tL|{HgRl5d}yy*t*D)?o3FZEbF*fpsXg1&{(Yy`)2B7H=bGNv z4t${1`EwQLHOKii7CD4Y<5vW7GyaRY$ zGU=e@QAm+)zZfB5znTgLBZIoFqvKTfiB5mp$u9VVAc}4k)uBWnBI{d15~0B*B8iYc zCd*0P4JUsj;g<%bK#D-Jw}W3EvqF2l&SKDhjUtW;D>VRB%ApPJxbiCHRR2#l6%TNE zs_-K$&z{gcu2mq1d#ZJ!b@Irnt4`Z_c(HzQt0o-FxsGf6@y9mjlyAZ}l^X1d66mfaJYC4$bE2dB8`O4`&N;A{NvdY`X+pqF#JDhe) zru}IxYZjTOHsq*WvDatq^*Q_Q%-eaJYsFTZwbjm$*`A!OVTEhZxQ54em*LtgT1`jJ z)v58FdAlp~?phf#?|ZIRKpbOj_O@o`fs1K7z&&v91nl7z`c!ml68@K=At|&#p*X@R z5{W5Q^kBXijs(KWPQZn%Tt`YR#Wa7<`6F=|r()7k#S2H;JJiFltg#HkJVLJD(Dp~j s`3RLicJ3HIJJmPQH?ueAtbgeA%?@U4Yup~jJxkUQZ1dbxx)n_S4dDlK5dZ)H literal 0 HcmV?d00001 diff --git a/bot/__pycache__/utils.cpython-311.pyc b/bot/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..edf4c48a88a968f8f6243564e9d9be3483be0c93 GIT binary patch literal 1554 zcma(RT}vZZ^xpZ%htp)@I=i~N+D4OF5FLi5fZF@eC&in}>bG;)8Q+qcsF3+ zvpi6cRCpf0>WMIkjU*yyCNV6f8L^sZS20P(4{YFazua< zLkS6#nTR&GhUhys=dzMs0wP|B!`nOwR6x&SrE7!Do!7eGl2gg%I@)LNftXxXu)-Z% zgf3dopf4j|KsHsrDnhOzD%hHH zUszI2%@>O*wM-xDerTm&T3MZxX^aI4=Tw8}1Z+*#llUpn0K2=Pl`X2~K)drPWlN?; z4Hbe7fdVry--47kQ8T27iYH(BVdMJ^r*G@Q!v~JqkmG;KqlfaSYaYmB4SB34crj(S z=W&m7`;ojO*F;ac;U?Yb~@jQ=6)N zX^CLx-Vlg~I6{#*cnBH#7T5HgoUp-MPKIDh#}UxlhB>j0x9LS_vi?)Y^uy0AV>$}CWmp_37(=Q;G@D^(grUpK_|5FX zZe|TJ9+qe#q$W(Ghdt7G@vawp^|A{GhlvLh58B(N^kDYn`(PKUF}~#cz4^ZPzW3hu zdvAX3>goWL_ka9z<+=dyJKMBHY<(QPj32w8g8@1*$j5@teJ(6=I={&4!lKYR2VE@1 z0ni#lt5_7(c*N&vE{ajZ%Tp^$+Hp~HxOiMgEIWoKJ``XcRBo=+XIieOEmLi6B5SWr zx}Hrq7ffr=R{h+>t% zqPSfMn-EIF)oe{tYl3=&2cYsLVPeg>!O$)+ykAfS)T_>NNklZ3apPEQvu-RobN_f5 zl6dkm3sdYu5`0*F{^cwKicY?3o$A~L+^;e={v|r!_9+ve{AZLi4^9`5LqbUr8{u;W zpyNKb+HT=-U4erz6!FaqB3xU&4C zKzRZSc)AW7`~s{KH8IyDZ#-efCYdaCo%Ws$QWS4Z(i6tJDp1z^NT7z1i-;3(UO|mT zqU+>L50ytvClXCdH#QBrIvJ5s8jQJvSm| zlvuG{FJtN(RAEVySu}Ip8}sezco{tu6z>8FIF$_%Ol|nm$nm!N?#+6rbYrfQ`_3s&2L8n589(B_l z#aDBr!%GN1sr`tL$rXlIlecd0kEaMC_KL?QA@jC<@ZAmvENMGT(k9j&Ul@&v}`ck*ATE2LK++)<5j)^sP=>- lrm*UBg@)g+PnSz=%cruvCre+e*;+{N`e3Y literal 0 HcmV?d00001 diff --git a/bot/__pycache__/ytdlp_client.cpython-311.pyc b/bot/__pycache__/ytdlp_client.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f2ebb838de2339bf59fea3dc73c76e91bf655a1f GIT binary patch literal 4393 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 gd0!zA>AzBe1vRzulxqjQ{`u literal 0 HcmV?d00001 diff --git a/bot/__pycache__/ytdlp_client.cpython-312.pyc b/bot/__pycache__/ytdlp_client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e8a4b9e94f8cc579caf15148b219d89cb05560d0 GIT binary patch literal 3878 zcma(UZERcB^}bJkC#mf?ACNSy6PlN{DFmW2s9NeKftC@vHS0$PGLG+a;$Zuk`<|N* z9A=EfQNdX*>5hFD+BM zajkdnx#ygF-nr+UFaOEoaUvLpe*gK@Ujh0v+gQW13S9dU0Lus?%q7q`TloZ+24wP8sWu?!D4Z$k*%RQoDq2bx25#N60`|Y5o3xvVj2k_ zjbl}#vX(J$oKR;})1Hp2v3NqIwh5xm=qjO2Knye9bRsgBi0cNGiE3m>Dk?MCtAm+H zfals%95ciuT9C7d__Ojf?EiaU4)a)egMVk0#-Xf4cr2zkEG^g!CdV9EE^C{2A!PAk z8S_~g+svPg?bz{#W5I4n7XS3k8m%I+(>guN>1`bPKisaHxY@^ZFF3LeB4r&}F!~OV zndQm)EI;kA-g#qvXB8xkH{D?Ej&Qz#oln`b0+F*qi~~%gDN!WN2Hdhf47UYk#WPLs z@b%p!<6dWS+YcbcJ4*c{BgwQnd60d;JzDChN(_xqA(_5U;i>SnI!7DQM2)FL#qjZx z5o7+2p59D4p+&Gt6g}M28^?;?-_xtk8j3!@uBX?C8wvFWYU~SU*LmCB{pL?`P#buH zkxVD3^h_qM8q}9m$)p+vY`X3u@D_NhrWHn#sjc>i}FKYWBb*=7|+ zRDwgZa|9v@QE%O$^%&8TtNzSFAqgTQlPa@NU4D299A8&#RF0(6Y6??9Hwfi5oywD{ zp{9;fS)B#f*9r4Y+Wxh3;fOw$iiRgh98ao>lQ6Rp=82{b#Ll4gY+^n`{A^kW6BV^I z2IMQM_wMasT&9od+-44_%Sb-4Ai4hRwy{!?}mQ zl##z>>FA2B)Uv7U>#eS9uCDK{w)a+j{_33_Ri&>QXs@;hulSr^IXCoWEAlj++`G7U zDOq-RUvLkf;eYLT+i`YFxpx=rJ{-smU$VKYtsQwWe`wM3scqfTbS1E<6xehzpj__g zT4{eh45TM6*xN5TnyPngT^Y}dMbCzkedDLLhRYk3i+w|XoV>X4U~$7CMgZMAKXEnY z9=!(lLebTbV6b`J7h%0N52@r)#6jwDpe(v|(y>At^H2A}M>usGJ^_nX*iEmcBJ*wL zWQF+xf1C5tH#{%g=Dhq3&x_cWXtp;eI8|aEfb`%HXX= zMyD<-g5#-4>JFz>h+7D72qD9gXj6u3#C^c=*qM(;%6t{?JEA9 z;o$u5Oe)Y6kqQvQRD!p})6}8jxDhrW=~2hLYb`V3;aw=Du9Ai zWc)g_K|MwZxDO(CSyLYXdIhG>(cg2Z#@i)N)#W}pxH!1f`omp?_&KVd5+u1cK%2C{7}`~R`CW)-r&km*{f9CO3AI%T*$qp=0u*pe_VF?uOd$JT(Y}s z0<8biAB4NTz=a;`VF34HKe%YuqO z7MIcgR&`Of!fOyMu*7r=r50_NJHuOQoq*Fny zN5OjA0+;3VeYfEfEv|ZkfL@Ky^3akZEWISKKpa9(f=`1HgElp}6H%5UF2ib@VP|nc?}tu^rWVM$Crd#=bPv+G8Ax(RASE$OJ?$v=(J`py zb}QRZN0{}ksYp_#KB7L8QFQ~dj78SJ_%{q{XFO@v{+bZuW>tUAfEo+Q;Ry6HnF)31 z+tdvaPa;tx98bkG>Wu4VHBLoUD&lz5AOm0~!aP{9Q(+D#r~pSPPcY@<1_=RyitMQA z_JjqH&ORf{A2}Fi2B89|H{V1uss0@aEgDJaJNp6iEynylOpr|e;_+3W{&_kIxano<*6*gVh zy`cT_?p>h);e3mi%>z>CL7O5_o7pSHHQEF@ISe;XL6rz6G%ZaVVF%U@tFvk}1HG&w zn`Sc&NJYj%MnSA;Tk;sQ5sSsAp6cp)=%H0}f&_8^kb{i652k;b8TqoyTX6+Tu3*{K zlN+iwuYZ0tHf!hr=0eAs^ yjQ 0: + await message.edit_text(f"✅ Stories отправлены! ({count} шт.)") + else: + await message.edit_text("ℹ️ Stories не найдены или доступ ограничен.") + except Exception as e: + await message.edit_text(f"⚠️ Ошибка Stories: {e}") + + # -------- Stories: все активные у автора -------- + async def download_all_stories(self, url: str, message, chat_id: int): + try: + username = extract_instagram_username(url) + if not username: + await message.edit_text("⚠️ Не удалось определить username из ссылки.") + return + + list_url = f"https://www.instagram.com/stories/{username}/" + opts = build_opts(for_video=True, outdir=DOWNLOAD_DIR, cookiefile=COOKIES_FILE, browser=BROWSER, message=message, out_for_playlist=True) + opts['format'] = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]' + _filepath, info = await run(list_url, opts) + + count = await self._send_playlist_mixed_with_images(info, message, chat_id) + if count: + await message.edit_text(f"✅ Отправлено элементов: {count} (у @{username})") + else: + await message.edit_text(f"ℹ️ У @{username} нет активных сторис или доступ ограничен.") + except Exception as e: + await message.edit_text(f"⚠️ Ошибка при загрузке всех Stories автора: {e}") + + # -------- Видео (с выбором качества) -------- + async def download_video(self, url: str, message, quality_key: str, chat_id: int): + try: + fmt = QUALITY_MAP.get(quality_key, QUALITY_MAP['max']) + opts = build_opts(for_video=True, outdir=DOWNLOAD_DIR, cookiefile=COOKIES_FILE, browser=BROWSER, message=message, out_for_playlist=False) + opts['format'] = fmt + filepath, info = await run(url, opts) + + sent_any = False + if filepath and filepath.exists(): + out_mp4 = await transcode_to_mobile_mp4(filepath) + try: + await self._send_video_safe(message, out_mp4, chat_id) + sent_any = True + finally: + out_mp4.unlink(missing_ok=True) + filepath.unlink(missing_ok=True) + + sent_any = (await self._send_playlist_mixed_with_images(info, message, chat_id)) or sent_any + + if sent_any: + await message.edit_text("✅ Видео отправлено!") + else: + await message.edit_text("❌ Файл не найден после загрузки.") + except Exception as e: + await message.edit_text(f"⚠️ Ошибка: {e}") + + # -------- Аудио -------- + async def download_audio(self, url: str, message, chat_id: int): + try: + opts = build_opts(for_video=False, outdir=DOWNLOAD_DIR, cookiefile=COOKIES_FILE, browser=BROWSER, message=message, out_for_playlist=False) + opts.update({ + 'format': 'bestaudio/best', + 'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192'}] + }) + filepath, info = await run(url, opts) + + if filepath and filepath.exists(): + title = (info.get('title') if isinstance(info, dict) else None) + await self._send_audio_safe(message, filepath, title, chat_id) + await message.edit_text("✅ Аудио отправлено!") + filepath.unlink(missing_ok=True) + return + + await message.edit_text("❌ Аудиофайл не найден.") + except Exception as e: + await message.edit_text(f"⚠️ Ошибка: {e}") + + # -------- Изображения -------- + async def download_images(self, url: str, message, chat_id: int): + try: + opts = build_opts(for_video=False, outdir=DOWNLOAD_DIR, cookiefile=COOKIES_FILE, browser=BROWSER, message=message, out_for_playlist=True) + opts['format'] = 'best' + filepath, info = await run(url, opts) + + sent = 0 + if filepath and filepath.exists(): + suffix = (filepath.suffix or '').lower() + if suffix in IMAGE_SUFFIXES: + await self._send_photo_safe(message, filepath, chat_id) + sent += 1 + else: + await self._send_document_safe(message, filepath, chat_id) + filepath.unlink(missing_ok=True) + + sent += await self._send_images_from_playlist_with_fallback(info, message, chat_id) + + if sent: + await message.edit_text(f"✅ Отправлено изображений: {sent}") + else: + await message.edit_text("ℹ️ Изображения не найдены.") + except Exception as e: + await message.edit_text(f"⚠️ Ошибка при загрузке изображений: {e}") + + # -------- Оригинал -------- + async def download_original(self, url: str, message, chat_id: int): + try: + opts = build_opts(for_video=False, outdir=DOWNLOAD_DIR, cookiefile=COOKIES_FILE, browser=BROWSER, message=message, out_for_playlist=False) + opts['format'] = 'best' + filepath, info = await run(url, opts) + + if filepath and filepath.exists(): + await self._send_document_safe(message, filepath, chat_id) + await message.edit_text("✅ Файл отправлен!") + filepath.unlink(missing_ok=True) + return + + sent_any = (await self._send_playlist_mixed_with_images(info, message, chat_id)) > 0 + if sent_any: + await message.edit_text("✅ Файлы отправлены!") + else: + await message.edit_text("❌ Файл не найден.") + except Exception as e: + await message.edit_text(f"⚠️ Ошибка: {e}") + + # -------- Вспомогательные -------- + async def _send_video_safe(self, message, filepath: Path, chat_id: int): + self.sending_active[chat_id] = True + try: + await send_video(message, filepath, CONNECT_TIMEOUT, READ_TIMEOUT) + finally: + self.sending_active[chat_id] = False + + async def _send_audio_safe(self, message, filepath: Path, title: str | None, chat_id: int): + self.sending_active[chat_id] = True + try: + await send_audio(message, filepath, title, CONNECT_TIMEOUT, READ_TIMEOUT) + finally: + self.sending_active[chat_id] = False + + async def _send_document_safe(self, message, filepath: Path, chat_id: int): + self.sending_active[chat_id] = True + try: + await send_document(message, filepath, CONNECT_TIMEOUT, READ_TIMEOUT) + finally: + self.sending_active[chat_id] = False + + async def _send_photo_safe(self, message, filepath: Path, chat_id: int): + self.sending_active[chat_id] = True + try: + await send_photo(message, filepath, CONNECT_TIMEOUT, READ_TIMEOUT) + finally: + self.sending_active[chat_id] = False + + async def _download_file(self, url: str, into: Path) -> Path: + into.parent.mkdir(parents=True, exist_ok=True) + timeout = httpx.Timeout(30.0) + async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client: + r = await client.get(url) + r.raise_for_status() + ctype = r.headers.get('content-type', '') + ext = '' + if 'image/jpeg' in ctype: + ext = '.jpg' + elif 'image/png' in ctype: + ext = '.png' + elif 'image/webp' in ctype: + ext = '.webp' + elif 'image/avif' in ctype: + ext = '.avif' + elif 'image/heic' in ctype or 'image/heif' in ctype: + ext = '.heic' + if not into.suffix: + into = into.with_suffix(ext or '.jpg') + into.write_bytes(r.content) + return into + + def _pick_path(self, entry) -> Path | None: + if isinstance(entry, dict): + rd = entry.get('requested_downloads') or [] + for it in rd[::-1]: + fp = it.get('filepath') or it.get('filename') + if fp: + p = Path(fp) + if p.exists(): + return p + fn = entry.get('_filename') + if fn: + p = Path(fn) + if p.exists(): + return p + return None + + async def _send_images_from_playlist_with_fallback(self, info: dict | None, message, chat_id: int) -> int: + if not isinstance(info, dict): + return 0 + entries = info.get('entries') or [] + sent = 0 + for idx, entry in enumerate(entries): + fp = self._pick_path(entry) + temp_file: Path | None = None + try: + if fp and fp.exists(): + suffix = (fp.suffix or '').lower() + if suffix in IMAGE_SUFFIXES: + await self._send_photo_safe(message, fp, chat_id) + sent += 1 + else: + await self._send_document_safe(message, fp, chat_id) + else: + image_url = None + thumbs = entry.get('thumbnails') if isinstance(entry, dict) else None + if isinstance(thumbs, list) and thumbs: + urls = [t.get('url') for t in thumbs if isinstance(t, dict) and t.get('url')] + if urls: + image_url = max(urls, key=len) + if not image_url and isinstance(entry, dict): + cand = entry.get('url') or entry.get('thumbnail') + if isinstance(cand, str) and cand.startswith(('http://', 'https://')): + image_url = cand + if image_url: + temp_file = await self._download_file(image_url, DOWNLOAD_DIR / f"img_{idx}") + await self._send_photo_safe(message, temp_file, chat_id) + sent += 1 + finally: + if temp_file: + temp_file.unlink(missing_ok=True) + if fp: + fp.unlink(missing_ok=True) + return sent + + async def _send_playlist_mixed_with_images(self, info: dict | None, message, chat_id: int) -> int: + if not isinstance(info, dict): + return 0 + entries = info.get('entries') or [] + sent = 0 + for idx, entry in enumerate(entries): + fp = self._pick_path(entry) + temp_file: Path | None = None + try: + if fp and fp.exists(): + suffix = (fp.suffix or '').lower() + if suffix in IMAGE_SUFFIXES: + await self._send_photo_safe(message, fp, chat_id) + sent += 1 + elif suffix in VIDEO_SUFFIXES: + out_mp4 = await transcode_to_mobile_mp4(fp) + try: + await self._send_video_safe(message, out_mp4, chat_id) + sent += 1 + finally: + out_mp4.unlink(missing_ok=True) + else: + await self._send_document_safe(message, fp, chat_id) + sent += 1 + else: + image_url = None + thumbs = entry.get('thumbnails') if isinstance(entry, dict) else None + if isinstance(thumbs, list) and thumbs: + urls = [t.get('url') for t in thumbs if isinstance(t, dict) and t.get('url')] + if urls: + image_url = max(urls, key=len) + if not image_url and isinstance(entry, dict): + cand = entry.get('url') or entry.get('thumbnail') + if isinstance(cand, str) and cand.startswith(('http://', 'https://')): + image_url = cand + if image_url: + temp_file = await self._download_file(image_url, DOWNLOAD_DIR / f"story_img_{idx}") + await self._send_photo_safe(message, temp_file, chat_id) + sent += 1 + finally: + if temp_file: + temp_file.unlink(missing_ok=True) + if fp: + fp.unlink(missing_ok=True) + return sent diff --git a/bot/init.py b/bot/init.py new file mode 100644 index 0000000..7e7ccf8 --- /dev/null +++ b/bot/init.py @@ -0,0 +1 @@ +# empty marker for package diff --git a/bot/logging_conf.py b/bot/logging_conf.py new file mode 100644 index 0000000..7c0ea01 --- /dev/null +++ b/bot/logging_conf.py @@ -0,0 +1,7 @@ +import logging + +def setup_logging(): + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s" + ) diff --git a/bot/telegram_send.py b/bot/telegram_send.py new file mode 100644 index 0000000..b581fe5 --- /dev/null +++ b/bot/telegram_send.py @@ -0,0 +1,41 @@ +from pathlib import Path +from telegram import Message + +async def send_video(message: Message, filepath: Path, connect_timeout: int, read_timeout: int): + with filepath.open('rb') as f: + await message.chat.send_video( + video=f, + supports_streaming=True, + connect_timeout=connect_timeout, + read_timeout=read_timeout + ) + +async def send_audio(message: Message, filepath: Path, title: str | None, connect_timeout: int, read_timeout: int): + with filepath.open('rb') as f: + await message.chat.send_audio( + audio=f, + title=title, + connect_timeout=connect_timeout, + read_timeout=read_timeout + ) + +async def send_document(message: Message, filepath: Path, connect_timeout: int, read_timeout: int): + with filepath.open('rb') as f: + await message.chat.send_document( + document=f, + filename=filepath.name, + connect_timeout=connect_timeout, + read_timeout=read_timeout + ) + +async def send_photo(message: Message, filepath: Path, connect_timeout: int, read_timeout: int): + # Фото до ~20MB лучше отправлять как фото, большие — как документ + if filepath.stat().st_size <= 20 * 1024 * 1024: + with filepath.open('rb') as f: + await message.chat.send_photo( + photo=f, + connect_timeout=connect_timeout, + read_timeout=read_timeout + ) + else: + await send_document(message, filepath, connect_timeout, read_timeout) diff --git a/bot/transcode.py b/bot/transcode.py new file mode 100644 index 0000000..7294ff8 --- /dev/null +++ b/bot/transcode.py @@ -0,0 +1,46 @@ +import asyncio +from pathlib import Path +import shutil +import subprocess + +FFMPEG_BIN = shutil.which("ffmpeg") or "ffmpeg" + +async def transcode_to_mobile_mp4(src: Path) -> Path: + """ + Перекодировать видео в MP4, совместимый с мобильными клиентами Telegram: + - H.264 (libx264), профиль main + - pix_fmt yuv420p + - movflags +faststart + - AAC 128k + Возвращает путь к новому файлу (.tg.mp4). + """ + if not src.exists(): + raise FileNotFoundError(f"Source file not found: {src}") + + dst = src.with_suffix(".tg.mp4") + cmd = [ + FFMPEG_BIN, + "-y", + "-i", str(src), + "-c:v", "libx264", + "-preset", "veryfast", + "-crf", "23", + "-profile:v", "main", + "-pix_fmt", "yuv420p", + "-movflags", "+faststart", + "-c:a", "aac", + "-b:a", "128k", + str(dst), + ] + + def _run(): + proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if proc.returncode != 0: + raise RuntimeError(f"ffmpeg failed: {proc.stderr.decode('utf-8', errors='ignore')}") + + loop = asyncio.get_running_loop() + await loop.run_in_executor(None, _run) + + if not dst.exists(): + raise RuntimeError("Transcode failed: output file not created") + return dst diff --git a/bot/utils.py b/bot/utils.py new file mode 100644 index 0000000..d1c027d --- /dev/null +++ b/bot/utils.py @@ -0,0 +1,32 @@ +def is_instagram_story_url(url: str) -> bool: + return "instagram.com/stories" in url + +VIDEO_SUFFIXES = ('.mp4', '.mov', '.mkv') +IMAGE_SUFFIXES = ('.jpg', '.jpeg', '.png', '.webp') + +def extract_instagram_username(url: str) -> str | None: + # Поддержка: + # https://www.instagram.com/stories/USERNAME/... + # https://www.instagram.com/USERNAME/ + try: + base = url.split("?")[0].strip("/") + parts = base.split("/") + if "instagram.com" not in url: + return None + if "stories" in parts: + i = parts.index("stories") + if i + 1 < len(parts): + return parts[i + 1] + # профиль + host_idx = 0 + for i, p in enumerate(parts): + if "instagram.com" in p: + host_idx = i + break + if host_idx + 1 < len(parts): + candidate = parts[host_idx + 1] + if candidate and candidate not in ("stories", "reel", "p", "tv"): + return candidate + except Exception: + return None + return None diff --git a/bot/ytdlp_client.py b/bot/ytdlp_client.py new file mode 100644 index 0000000..59cad1e --- /dev/null +++ b/bot/ytdlp_client.py @@ -0,0 +1,73 @@ +import asyncio +from functools import partial +from pathlib import Path +import os +import yt_dlp + +def build_opts(for_video: bool, outdir: Path, cookiefile: Path | None, browser: str | None, message=None, out_for_playlist: bool = True) -> dict: + postprocessors = [] + if for_video: + # На этапе постпроцессора делаем простую конвертацию в mp4 (дальше будет наша принудительная перекодировка) + postprocessors.append({'key': 'FFmpegVideoConvertor', 'preferedformat': 'mp4'}) + + # Для плейлистов (stories/галереи) — уникальные имена, чтобы не затирать + outtmpl = '%(uploader)s_%(id)s.%(ext)s' if out_for_playlist else '%(title)s.%(ext)s' + + opts: dict = { + 'outtmpl': str(outdir / outtmpl), + 'quiet': True, + 'merge_output_format': 'mp4', + 'postprocessors': postprocessors, + 'retries': 5, + 'fragment_retries': 5, + 'concurrent_fragment_downloads': 5, + 'noprogress': True, + 'source_address': '0.0.0.0', # форс IPv4 + } + + proxy = os.getenv('HTTPS_PROXY') or os.getenv('HTTP_PROXY') or '' + if proxy: + opts['proxy'] = proxy + + if cookiefile and cookiefile.exists(): + opts['cookiefile'] = str(cookiefile) + elif browser: + opts['cookiesfrombrowser'] = (browser,) + + if message: + opts['progress_hooks'] = [partial(progress_hook_async_bridge, message=message)] + + return opts + +async def progress_hook_async_bridge(d, message): + try: + if d.get('status') == 'downloading': + percent = d.get('_percent_str', '').strip() + speed = d.get('_speed_str', '').strip() + eta = d.get('_eta_str', '').strip() + text = f"⬇️ Загрузка: {percent or '?'}\n🚀 Скорость: {speed or '?'}\n⏱ Осталось: {eta or '?'}" + await message.edit_text(text) + except Exception: + pass + +async def run(url: str, opts: dict) -> tuple[Path | None, dict | None]: + loop = asyncio.get_running_loop() + info, filepath = None, None + + def _do(): + nonlocal info, filepath + with yt_dlp.YoutubeDL(opts) as ydl: + info = ydl.extract_info(url, download=True) + out = None + if isinstance(info, dict): + out = info.get('_filename') + if not out: + rd = info.get('requested_downloads') or [] + for it in rd[::-1]: + out = it.get('filepath') or it.get('filename') + if out: + break + filepath = Path(out) if out else None + + await loop.run_in_executor(None, _do) + return filepath, info diff --git a/bot1.py b/bot1.py deleted file mode 100644 index 12fb42b..0000000 --- a/bot1.py +++ /dev/null @@ -1,330 +0,0 @@ -import os -import logging -import asyncio -import json -import time -from functools import partial -from pathlib import Path -from typing import Optional, Tuple, Dict - -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.ext import ( - ApplicationBuilder, - CommandHandler, - MessageHandler, - CallbackQueryHandler, - ContextTypes, - filters, -) -import yt_dlp -import instaloader - -# Настройки из переменных окружения с дефолтами -BASE_URL = os.getenv("BOT_BASE_URL", "http://127.0.0.1:8081/bot") - -DOWNLOAD_DIR = Path(os.getenv("DOWNLOAD_DIR", "downloads")) -COOKIES_FILE = Path(os.getenv("COOKIES_FILE", "cookies.txt")) -INSTALOADER_SESSION = Path(os.getenv("INSTALOADER_SESSION", "session-smolkik_adm")) -INSTALOADER_USER = os.getenv("INSTALOADER_USER", "smolkik_adm") - -CONNECT_TIMEOUT = int(os.getenv("CONNECT_TIMEOUT", 600)) -READ_TIMEOUT = int(os.getenv("READ_TIMEOUT", 3600)) - -DOWNLOAD_DIR.mkdir(exist_ok=True) - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(message)s" -) - -class Downloader: - def __init__(self): - self.user_data = {} - self.cookies_enabled = COOKIES_FILE.exists() - self.session_enabled = INSTALOADER_SESSION.exists() - self._last_progress_update = 0 - - if self.cookies_enabled: - logging.info(f"Обнаружен файл cookies: {COOKIES_FILE}") - if self.session_enabled: - logging.info(f"Обнаружен файл сессии InstaLoader: {INSTALOADER_SESSION}") - - async def download_media(self, url: str, options: dict, message: Optional[object] = None) -> Tuple[str, dict]: - ydl_opts = { - 'outtmpl': str(DOWNLOAD_DIR / '%(title)s.%(ext)s'), - 'quiet': True, - **options - } - if self.cookies_enabled: - ydl_opts['cookiefile'] = str(COOKIES_FILE) - if message: - ydl_opts['progress_hooks'] = [partial(self.progress_hook, message=message)] - - logging.info(f"Начинаю загрузку: {url}") - try: - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - loop = asyncio.get_running_loop() - info = await loop.run_in_executor(None, ydl.extract_info, url, True) - filepath = ydl.prepare_filename(info) - if options.get('extract_audio'): - filepath = os.path.splitext(filepath)[0] + '.mp3' - logging.info(f"Загрузка завершена: {filepath}") - return filepath, info - except yt_dlp.utils.DownloadError as e: - if "Private video" in str(e) and self.cookies_enabled: - logging.error("Не удалось получить доступ к приватному видео даже с cookies") - raise Exception("Не удалось получить доступ к видео. Проверьте cookies.") - raise - - async def download_instagram_story(self, url: str, message: object): - if not self.session_enabled: - await message.edit_text("⚠️ Нет Instagram session файла. Загрузите session-smolkik_adm в папку с ботом.") - return - - try: - username = self.extract_username_from_url(url) - loop = asyncio.get_running_loop() - - await message.edit_text("⏳ Проверяю сессию Instagram...") - - valid = await loop.run_in_executor(None, self.check_instagram_session) - if not valid: - await message.edit_text("⚠️ Сессия Instagram недействительна. Пожалуйста, обновите session-smolkik_adm.") - return - - await message.edit_text(f"⏳ Ищу сторис пользователя @{username}...") - await loop.run_in_executor(None, self._instaloader_download, username) - - story_files = [f for f in DOWNLOAD_DIR.glob(f"{username}*")] - if not story_files: - await message.edit_text("❌ Нет доступных сторис у пользователя.") - return - - for file in story_files: - with file.open('rb') as story_file: - await message.bot.send_document( - chat_id=message.chat_id, - document=story_file, - filename=file.name, - connect_timeout=CONNECT_TIMEOUT, - read_timeout=READ_TIMEOUT - ) - file.unlink() - await message.edit_text("✅ Истории успешно отправлены!") - - except Exception as e: - logging.error(f"Ошибка скачивания сторис: {str(e)}") - await message.edit_text(f"⚠️ Ошибка скачивания Stories: {str(e)}") - - def extract_username_from_url(self, url: str) -> Optional[str]: - parts = url.split('/') - username = None - if 'stories' in parts: - idx = parts.index('stories') + 1 - if idx < len(parts): - username = parts[idx] - else: - for i, part in enumerate(parts): - if 'instagram.com' in part and i + 1 < len(parts): - username = parts[i + 1] - break - if username: - username = username.split('?')[0] - return username - - def check_instagram_session(self) -> bool: - """Проверяет, что сессия Instaloader действительна""" - try: - L = instaloader.Instaloader() - L.load_session_from_file(INSTALOADER_USER, str(INSTALOADER_SESSION)) - user = L.test_login() - logging.info(f"✅ Instagram-сессия активна: {user}") - return True - except Exception as e: - logging.warning(f"❌ Сессия Instagram недействительна: {e}") - return False - - def _instaloader_download(self, username: str): - L = instaloader.Instaloader( - dirname_pattern=str(DOWNLOAD_DIR), - download_videos=True, - download_video_thumbnails=False - ) - if not INSTALOADER_SESSION.exists(): - raise FileNotFoundError(f"Session file {INSTALOADER_SESSION} не найден.") - L.load_session_from_file(INSTALOADER_USER, str(INSTALOADER_SESSION)) - profile = instaloader.Profile.from_username(L.context, username) - - for story in L.get_stories(userids=[profile.userid]): - for item in story.get_items(): - try: - L.download_storyitem(item, target=username) - except Exception as e: - logging.error(f"Ошибка при обработке элемента сторис: {e}") - - async def progress_hook(self, d: dict, message: Optional[object] = None): - now = time.time() - if d.get('status') == 'downloading' and message: - # Обновлять не чаще чем раз в секунду - if now - self._last_progress_update > 1: - percent = d.get('_percent_str', '?') - speed = d.get('_speed_str', '?') - eta = d.get('_eta_str', '?') - text = f"⬇️ Загрузка: {percent}\n🚀 Скорость: {speed}\n⏱ Осталось: {eta}" - try: - await message.edit_text(text) - except Exception: - pass - self._last_progress_update = now - - # Все методы ниже получили аннотации типов - async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - text = "🎬 Пришлите ссылку на видео или профиль Instagram (для Stories)\n" - if self.cookies_enabled: - text += "\n🔐 Используется cookies для скачивания приватного контента." - if self.session_enabled: - text += "\n🔐 Используется Instagram-сессия для сторис." - await update.message.reply_text(text) - - async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - url = update.message.text.strip() - if not url.startswith(('http://', 'https://')): - await update.message.reply_text("⚠️ Пожалуйста, отправьте корректную ссылку") - return - self.user_data[update.message.from_user.id] = {'url': url} - keyboard = [ - [InlineKeyboardButton("🎥 Видео", callback_data='video')], - [InlineKeyboardButton("🎧 Аудио", callback_data='audio')], - [InlineKeyboardButton("📁 Оригинал", callback_data='original')], - [InlineKeyboardButton("🌟 Сторис Instagram", callback_data='story')], - ] - reply_markup = InlineKeyboardMarkup(keyboard) - await update.message.reply_text("Выберите тип загрузки:", reply_markup=reply_markup) - - async def handle_quality_choice(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - query = update.callback_query - await query.answer() - user_id = query.from_user.id - data = query.data - if data == 'story': - url = self.user_data[user_id]['url'] - msg = await query.edit_message_text("⏳ Начинаю загрузку Instagram Stories...") - await self.download_instagram_story(url, msg) - elif data == 'audio': - await self.download_audio(query) - elif data == 'original': - await self.download_original(query) - elif data == 'video': - await self.select_video_quality(query) - elif data.startswith('quality_'): - await self.download_video(query, data.split('_')[1]) - - async def select_video_quality(self, query: object) -> None: - keyboard = [ - [InlineKeyboardButton("🔥 Максимальное", callback_data='quality_max')], - [InlineKeyboardButton("🖥 1080p", callback_data='quality_1080')], - [InlineKeyboardButton("📺 720p", callback_data='quality_720')], - [InlineKeyboardButton("📱 480p", callback_data='quality_480')], - ] - reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text("Выберите качество видео:", reply_markup=reply_markup) - - async def download_audio(self, query: object) -> None: - user_id = query.from_user.id - url = self.user_data[user_id]['url'] - msg = await query.edit_message_text("⏳ Начинаю загрузку аудио...") - try: - options = { - 'format': 'bestaudio/best', - 'extract_audio': True, - 'audio_format': 'mp3', - 'postprocessors': [{ - 'key': 'FFmpegExtractAudio', - 'preferredcodec': 'mp3', - 'preferredquality': '192', - }], - } - filepath, info = await self.download_media(url, options, msg) - title = info.get('title', 'Аудио') - artist = info.get('uploader', 'Неизвестен') - with open(filepath, 'rb') as audio_file: - await query.message.reply_audio( - audio=audio_file, - title=title, - performer=artist, - connect_timeout=CONNECT_TIMEOUT, - read_timeout=READ_TIMEOUT - ) - await msg.edit_text("✅ Аудио успешно отправлено!") - os.remove(filepath) - except Exception as e: - logging.error(f"Ошибка: {str(e)}") - await msg.edit_text(f"⚠️ Ошибка: {str(e)}") - - async def download_original(self, query: object) -> None: - user_id = query.from_user.id - url = self.user_data[user_id]['url'] - msg = await query.edit_message_text("⏳ Начинаю загрузку оригинального файла...") - try: - options = {'format': 'best'} - filepath, info = await self.download_media(url, options, msg) - with open(filepath, 'rb') as file: - await query.message.reply_document( - document=file, - filename=os.path.basename(filepath), - connect_timeout=CONNECT_TIMEOUT, - read_timeout=READ_TIMEOUT - ) - await msg.edit_text("✅ Файл успешно отправлен!") - os.remove(filepath) - except Exception as e: - logging.error(f"Ошибка: {str(e)}") - await msg.edit_text(f"⚠️ Ошибка: {str(e)}") - - async def download_video(self, query: object, quality: str) -> None: - user_id = query.from_user.id - url = self.user_data[user_id]['url'] - quality_map = { - 'max': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best', - '1080': 'bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4]/best', - '720': 'bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/best[height<=720][ext=mp4]/best', - '480': 'bestvideo[height<=480][ext=mp4]+bestaudio[ext=m4a]/best[height<=480][ext=mp4]/best', - } - msg = await query.edit_message_text(f"⏳ Начинаю загрузку видео ({quality})...") - try: - options = { - 'format': quality_map.get(quality, 'best'), - 'merge_output_format': 'mp4', - } - filepath, info = await self.download_media(url, options, msg) - with open(filepath, 'rb') as video_file: - await query.message.reply_video( - video=video_file, - supports_streaming=True, - connect_timeout=CONNECT_TIMEOUT, - read_timeout=READ_TIMEOUT - ) - await msg.edit_text("✅ Видео успешно отправлено!") - os.remove(filepath) - except Exception as e: - logging.error(f"Ошибка: {str(e)}") - await msg.edit_text(f"⚠️ Ошибка: {str(e)}") - -def main(): - downloader = Downloader() - app = ( - ApplicationBuilder() - .token(TOKEN) - .base_url(BASE_URL) - .connect_timeout(CONNECT_TIMEOUT) - .read_timeout(READ_TIMEOUT) - .build() - ) - app.add_handler(CommandHandler("start", downloader.start)) - app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, downloader.handle_message)) - app.add_handler(CallbackQueryHandler(downloader.handle_quality_choice)) - logging.info("✅ Бот запущен и готов к работе (используется локальный Bot API)") - app.run_polling() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/downloads/.DS_Store b/downloads/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0=2025.1.1 +httpx>=0.25.2 +instaloader>=4.13.1 diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..6fb5d76 --- /dev/null +++ b/run.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" + +if [ -f ".env" ]; then + while IFS='=' read -r key value; do + [[ -n "$key" && ! "$key" =~ ^[[:space:]]*# ]] && export "$key"="$value" + done < <(grep -E '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]*$' .env | sed 's/[[:space:]]*#.*$//' | tr -d '\r') +fi + +python3 -m venv .venv || true +source .venv/bin/activate + +python -m pip install --upgrade pip +python -m pip install -U -r requirements.txt +python -m pip install -U yt-dlp + +mkdir -p downloads + +exec python -m bot.app