From c2133705fe7de956a0a206eafc872973dd926a80 Mon Sep 17 00:00:00 2001 From: esefxdz Date: Thu, 9 Jul 2026 17:40:23 +0300 Subject: [PATCH] trimmed down music.py --- .gitignore | 3 + CODEBASE_GUIDE.md | 0 ai/ai_roleplay.py | 7 +- cogs/__pycache__/__init__.cpython-312.pyc | Bin 159 -> 0 bytes cogs/__pycache__/__init__.cpython-314.pyc | Bin 137 -> 0 bytes cogs/__pycache__/chance.cpython-312.pyc | Bin 3127 -> 0 bytes cogs/__pycache__/chance.cpython-314.pyc | Bin 3843 -> 0 bytes cogs/__pycache__/copypasta.cpython-314.pyc | Bin 19021 -> 0 bytes cogs/__pycache__/general.cpython-312.pyc | Bin 9690 -> 0 bytes cogs/__pycache__/general.cpython-314.pyc | Bin 1581 -> 0 bytes cogs/__pycache__/gifs.cpython-312.pyc | Bin 3657 -> 0 bytes cogs/__pycache__/gifs.cpython-314.pyc | Bin 3004 -> 0 bytes cogs/__pycache__/sysinfo.cpython-314.pyc | Bin 9283 -> 0 bytes main.py | 4 +- music/__pycache__/__init__.cpython-314.pyc | Bin 138 -> 0 bytes music/__pycache__/music.cpython-314.pyc | Bin 34110 -> 0 bytes music/{ => backup}/music.py | 0 music/music_wrapper.py | 103 +++++++ music/player/__init__.py | 1 + music/player/commands.py | 263 ++++++++++++++++++ music/player/events.py | 131 +++++++++ music/player/queue.py | 109 ++++++++ music/radio/__init__.py | 1 + music/radio/commands.py | 175 ++++++++++++ music/radio/events.py | 40 +++ music/radio/views.py | 102 +++++++ music/shared/__init__.py | 1 + music/shared/constants.py | 23 ++ music/shared/state.py | 18 ++ music/shared/utils.py | 33 +++ tgbridge/__pycache__/__init__.cpython-312.pyc | Bin 163 -> 0 bytes .../__pycache__/forwarder.cpython-312.pyc | Bin 11491 -> 0 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 168 -> 0 bytes twitterbridge/__pycache__/rss.cpython-312.pyc | Bin 5687 -> 0 bytes wavelink/__pycache__/__init__.cpython-314.pyc | Bin 141 -> 0 bytes wavelink/__pycache__/music.cpython-314.pyc | Bin 4604 -> 0 bytes 36 files changed, 1010 insertions(+), 4 deletions(-) create mode 100644 CODEBASE_GUIDE.md delete mode 100644 cogs/__pycache__/__init__.cpython-312.pyc delete mode 100644 cogs/__pycache__/__init__.cpython-314.pyc delete mode 100644 cogs/__pycache__/chance.cpython-312.pyc delete mode 100644 cogs/__pycache__/chance.cpython-314.pyc delete mode 100644 cogs/__pycache__/copypasta.cpython-314.pyc delete mode 100644 cogs/__pycache__/general.cpython-312.pyc delete mode 100644 cogs/__pycache__/general.cpython-314.pyc delete mode 100644 cogs/__pycache__/gifs.cpython-312.pyc delete mode 100644 cogs/__pycache__/gifs.cpython-314.pyc delete mode 100644 cogs/__pycache__/sysinfo.cpython-314.pyc delete mode 100644 music/__pycache__/__init__.cpython-314.pyc delete mode 100644 music/__pycache__/music.cpython-314.pyc rename music/{ => backup}/music.py (100%) create mode 100644 music/music_wrapper.py create mode 100644 music/player/__init__.py create mode 100644 music/player/commands.py create mode 100644 music/player/events.py create mode 100644 music/player/queue.py create mode 100644 music/radio/__init__.py create mode 100644 music/radio/commands.py create mode 100644 music/radio/events.py create mode 100644 music/radio/views.py create mode 100644 music/shared/__init__.py create mode 100644 music/shared/constants.py create mode 100644 music/shared/state.py create mode 100644 music/shared/utils.py delete mode 100644 tgbridge/__pycache__/__init__.cpython-312.pyc delete mode 100644 tgbridge/__pycache__/forwarder.cpython-312.pyc delete mode 100644 twitterbridge/__pycache__/__init__.cpython-312.pyc delete mode 100644 twitterbridge/__pycache__/rss.cpython-312.pyc delete mode 100644 wavelink/__pycache__/__init__.cpython-314.pyc delete mode 100644 wavelink/__pycache__/music.cpython-314.pyc diff --git a/.gitignore b/.gitignore index 25cda4f..d7f3246 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ cogs/archived/ .vscode music/__pycache__/music.cpython-312.pyc yuuka-radio-streams.md +esef_learns.md +icy_supported.json +icy_metadata_radios.md diff --git a/CODEBASE_GUIDE.md b/CODEBASE_GUIDE.md new file mode 100644 index 0000000..e69de29 diff --git a/ai/ai_roleplay.py b/ai/ai_roleplay.py index 6ad62bd..939392b 100644 --- a/ai/ai_roleplay.py +++ b/ai/ai_roleplay.py @@ -12,7 +12,7 @@ from collections import defaultdict, deque # pyrefly: ignore [missing-import] from openai import AsyncOpenAI -#only last 12 messages on discord will be fed into the ai's memory +#only last 15 messages on discord will be fed into the ai's memory MEMORY_SIZE = 15 # These are the ONLY two valid DeepSeek V4 model IDs. deepseek-chat and @@ -38,6 +38,7 @@ MAX_TOKENS_THINKING = 4096 # Per-1M-token pricing (USD), per DeepSeek's official pricing page. # Pro costs roughly 3x flash on input and output — tracked separately so # !aicost is accurate when you've been toggling !turbo. +#i could not find a way to implement dynamic pricing, this is just basic maths based on their pricing PRICING = { MODEL_FLASH: {'cache_hit': 0.0028, 'cache_miss': 0.14, 'output': 0.28}, MODEL_PRO: {'cache_hit': 0.003625, 'cache_miss': 0.435, 'output': 0.87}, @@ -72,7 +73,7 @@ def list_personas(): names.append(fname.replace('_prompt.txt', '')) return sorted(names) -#this splits messages into chunks if they are too long (optimization) +#this splits messages (sent by bot) into chunks if they are too long (optimization) def split_msg(text, limit=2000): chunks = [] while len(text) > limit: @@ -81,6 +82,7 @@ def split_msg(text, limit=2000): split_at = limit chunks.append(text[:split_at]) text = text[split_at:].lstrip('\n') + #must be put outside the loop (dont try putting inside :sob:) if text: chunks.append(text) return chunks @@ -92,6 +94,7 @@ def split_msg(text, limit=2000): #dropdown for thinking mode — scoped to one channel, not global class ThinkingSelect(discord.ui.Select): def __init__(self, cog, channel_id): + self.cog = cog self.channel_id = channel_id current = cog.thinking_effort.get(channel_id, 'off') if channel_id in cog.thinking_channels else 'off' diff --git a/cogs/__pycache__/__init__.cpython-312.pyc b/cogs/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 790c86bfb0412264ff0c8eb30112203499513c89..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 159 zcmX@j%ge<81m*I|Ss?l`h(HIQS%4zb87dhx8U0o=6fpsLpFwJV**ROqgche37009& zr>4cYy@_1ma>4<0CU8BV!RWkOcq|CnvA~ diff --git a/cogs/__pycache__/__init__.cpython-314.pyc b/cogs/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 1e656f80f813038a31a13f867af755691b02c00d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 137 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%0DPE4Yq*aBlSWXm#iQdpq|pKObGl-HqMtTkaaG z1jnn&_=+g(BfcxXmo9bFc)zq1@`Ah}W7PTl1Z*8XqmXWYNQ=T&nIlzq=x>E5 z`UFqklw~CDd!uyGw6K<5^8rZ^8&`m#h@TyVlW^s=LQc<=aypmd7N(jcD_Dg*_a^j` z#XwL_g5H+o*Gp-3g}1y>D(cL%n311^0raI@ni5jqVHlYc&>{U_OQijbFf0G9t>f0a@4s6IjIAhRF5^mkJ6-bAc)PUJ;stpF;}|)11pgj{XNzD}QptJ* ze+w}Q2~ibVH%8?qCBI%%WP|+aX*>K)P~Uz14kXI)N-$nk;wwtr1=|yEr%MWr zw@FJrFUSnjP$JTX*E_s{!1d0rY14%)I3r&5H0|wDI#<)QY1$6KJd5R+IMr`FEi!dUrbf}p{f*GkwUea+#R9wbg%SVZ-hoX zyruSG5!Gib+hd$df;i#66Cb`v93=1}%avn}7anIS3=tQz7rIrEQu;3v{wryHB=yk~ VboPZ?gEt2s5tJV#KpD0l@_(aO%BlbW diff --git a/cogs/__pycache__/copypasta.cpython-314.pyc b/cogs/__pycache__/copypasta.cpython-314.pyc deleted file mode 100644 index 63c348468a8ee4f2337a77143333a91c7541fe4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19021 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 Z_7uJ|@xAqby8dH>-+cU4EZM@8{ugbobpQYW diff --git a/cogs/__pycache__/general.cpython-312.pyc b/cogs/__pycache__/general.cpython-312.pyc deleted file mode 100644 index 44e69f9d8cd6abeb70eaa15da599b95ac34bf4f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9690 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 a_H|0@=O@kw&IZ0D2tRm=rF4tN@P7fcQ$nTy diff --git a/cogs/__pycache__/general.cpython-314.pyc b/cogs/__pycache__/general.cpython-314.pyc deleted file mode 100644 index 4b292ec39aab12ac225b722c99a63c5b27b429e5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1581 zcmZux&2QX96rb_O+Sw-C1lVk%hBV1GTbW2%HKam9s6r|wfT~qwq3tE@$n`pj8?PN^ zW~DTz5DslFBoavUzyXf=Z)j7o#ejqam)_cJh1whM`C}WZjN~^zznM4hP@<=bx(M!^_DN(h__&C-Hp~n zeJ%Aizj-${8S_J5G8PYKG0f*0tiLco?2spKkA7?R0FwVjh-`d=5q<(-hMb3;O}Y$x zzd3BOm}!@wTz9~3i|(k`^&;5Vv`P$ey>MCesikb28&0_CZTQm-UT^4HnQ&F zNLO5DOh`xiE_0;h{_T$R1e`X)m>W1kw0u{(UJx(_Sq`00f*Dro<0M0lS0xeXL0km! z@*bphtWdr3tUwB-u3mnmmyh(yb5K=#Dk+Tpi-?zUOZOxlcpeZ&dQA`+VdONzR?uNG zntkd{P)OdO5ZW=PkRt)mk%rT1G3d552dr_DSS_PYc(Sv}Cqa!=igO^4uqVZ#{kgrl z@7Er<-vvj-YFDd1)~Xq4lNSAnUetazEb8{-149kIBrCiiX;v%!teeV8|5Ba$q-3jD zTpTSpd4k{tTft0{Vbi8ENEVd50?@Ruo;GY_+qlMg#6O&}w7BRCKLp-G*Grq??A7#1 zX+-kVs1vqgeUtm4wA7@f`3M{z>mJ$*%&1n&>N6aLP`uK|j| zVD`tc?)asLj&Yha_#Ix?6JJks>F7f`dPK*bD=K{#+Ef`oMwFcYA7|Gzk^lez diff --git a/cogs/__pycache__/gifs.cpython-312.pyc b/cogs/__pycache__/gifs.cpython-312.pyc deleted file mode 100644 index 55d73df723724a81ef965f7497cdd9d839aad7c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3657 zcmd5<-D@LN6u)=AGEI}zb?J6Rv!=URnq5rMB5J<&VZ{}M;)9D40z=G9w-b{l-kGqQ z*oR_^w4n4su?m9vX6Xw4AHKNBLzV$S*cW{esGC!y$sL+j{=lV`GsB@30b6>*{fXjR4N{^yDG53Se^DTY zRO1l`T?je!r@F8`(o0Gx8X_NhmLj2ODS%c((ZF}K_f#la3815)=tuxP9g0>1=vXLv zDu9lMqN4$HqBEV2InqsY)v69p5zhs6tONH+5c_0#%lTXX0_cg)@`e-n^v{SbnpwR)#h!hd>cZ@v^tKe zY390VYg#vsQDufxtW*nMvPmACzpx{Hrn)d8C!)sc--4f81MwE2Jn|aUb83i(w2QM^Ob;z~Dx@h?@oMs+O%Dr84m zHS5NK=*Y}y)jLka=k7>v;JU!@lW@eUeb>(#o5ls1Z_$l%?p#OC?sFi1C;x1c-xBBc z%w6-FcfT!vUH;{2wx7)HnEgb4pyqpO{@9HH@ZiXH2AYP!cI#>zMm{$B8mgG*i(zg>?uG`3i%i(zt+4HMqC6; zSLLKZ!{k_3K>YXQf5`NcuOZ*;K+X2l>}2tcbG|L3@-y8&N|_sHaa*T1n4f*Rft{7@FopHLcOq+jZ1qn)YG4QuizjuU3YQgA?() zU^a(KPovb4LnME^NLx zkZ149vweB)FIX(NB2g|Lp*GHZePq5q&YT;WbK}hUkvTukTpXE;(ZV=Ve$!?d zcDu!1f|VE0^b81oXFrI>_pa?;`$GR}`E#=$O%0?}Pf8tIyi2>ZNWY^C()Y?D{D;NP zgO4-qH;EbJuI-hYcw_id+?_3<7?qKu=Dk`ivX{Yt5n1yfjzmi7Lz4KFq#j8Nw6OE) RUVbQh#mj{ diff --git a/cogs/__pycache__/gifs.cpython-314.pyc b/cogs/__pycache__/gifs.cpython-314.pyc deleted file mode 100644 index c672fa5d16209f6b37c6f9db7211ed69e4b37c78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3004 zcmai0-ES0C6hC)9c4xQS-KElQfrkBPD2ye$eAG%zLnvs0fOQ?>`XDe_cW2Asc4wWL zhH9eOsEKVtQVKCl=Q^$mQH_{M)=X=TfZi3z^&1}q916Fhf5whJ_PlezcYuXE42 z=XZX02E&~}z;fmFO((+~z%APG3hxR{356OQ2Zgdm_!1T zIqoyr9?;Mp)Wt~*)_e|j5^6zNkT|TPrtg?P8*NwT$VnJ-2dO2VYz8Es8O(v%A^FiV z!Q%F-C8OY3KzC3Xy%3svP^iJ1(F(RsNka}Q#OviC>)o6;gspc{93<$6@5#8xTI}1J zX$9p8wIo;alZs(=$z?5HvWE4XwWs0g%(DWjz z$`rH-BZJzEboD(;kY%-^nzC$lQeVqaHk{7LZ~6c7linA zA=$)PD6|~H4u_xZdmte@%x7l4af%^I|g#GQ>eC}jhgTQL- zjl$Tg09BGB*+AP8SCp95&p6ZvkRuYpgFuzR@Ys-le1Ep{@jmV4TKeJ`;B4gCD-OK| z5P*%gMVfoA06jj|46h!|i)jXk7 z$1GpITE!8oQ=ZJfqsT58L{{&hvqDKND-t^rz3&Hcj`tbGM=VuNqM`DdgVj+^K_cS6~ zmLgl0WAQp$?>h6r8E!ec_KV2bNFzG15FMzSOVO=3(uk%OqN)1O_2`yn0M-ra^wMl3 zd1!Le2$~6;86h>i+2hQvPXLNG9$qIWFj7azTacj?!Q7_nbqpXNO|XWkV?Ovwnm(B~ z@p9xsId2$6wSX|Ub0bx=FTo1Qa=ud0%)F^!0<3WpU}dNq6&h=?dRT)8!1JlQw7S@D zH;&Qmz;40+Gu)D=MiXQkplnlRj*uL)!UacL`?7S`6D0{p(7hMk%Sg6CtjZ$9R)PO& zK+mxr(2-3Tye9UsWThE?4!`C!RwE<@aSS(V_d?KS)Bjm3p}ir5^p4hv660CF9~u87 z25p+u*u#7fPm$Pu@WWx>3#>{+?tPx7Kt}Jv^DuZE1ubPgURSE+_bk6lToyNm6Bt&u zU{2eSc394hWCtqo@NaE)0@5;Nl$d`@XG|{|;J$R(C6|0B0(e>r7?k~i%;y1Bx z`@ZhG#9u02%3T@yIlX5wIeJE1?AqH1?Oh7({oA0)@p1Yka+&Pne(=9UL}HsWF7lL2 zX{O1tPIJxjy9sHNvel6<7QK^2&Q4K#qfRYAwevkLhbSw#dH^V)R@WyN5jrEs*35(wiE{2C^SfuPZVOD4eeG5Y0{1GJXr;$sk z_+$EN{e*&pgrhASASPO9#c8*A*owj%%oLM`DP|1co+=|DT0^jd8D|b5$55)PvPh?7 zwbxD}S&G6Z$_j41GY~_}6Bt4~Pcd=)s*Fm})M2BEwMfz;!Et0(pP}idQnZOu--1V2 z+(}KXcsD|D*mWl)|N73i?RI+ znX$S#pPYYre)y{(c4k69$M!FW*UYK2>O6NL@MWOBb+I?I7~a+pwp|mp{e6#x-UH+Y zO2!6M`BCra268?;x|aKH6G~S*$>=)nYB+?l$T(imw|!g;>1k~7hd`dxic@9ET*zbZ zPUXuE_N<63msH&_%W6fbXb497XxW6XQ%Kp^)H77F-RnkDLdzJ1imO7KDF=z;BSa&#fcdXLikv%#Pd#6z>lc;^g^%l;=Wr diff --git a/cogs/__pycache__/sysinfo.cpython-314.pyc b/cogs/__pycache__/sysinfo.cpython-314.pyc deleted file mode 100644 index 479c045c0d292da9690ca7d58615589beb8548a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9283 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�{$p|zrZ30QRpq18tS>A7gHlt+Mx>paD%`7V+N;Ce`M zxol;{S}G`P(D{(!N0HZc3)vQ`ph z(&siFWOLy$dOw8mjFcM1^(1JnSN1Bi#GPhGdhLvE{tBhPV4?Fa?su)hLh}S^HlK)C(lYCX-+dPZKVmN8O9j8b3URpBZ~* z^qCkkviedwk&#V7S*?!AYIRIjtK&a1C5@*{SWa4A*fGk_8_Z+oZ<)uRoHJzIm3COl zU+X0@Q7iiyuO~O-_2gx|o=8L;BN6L^5?NhoLlJ+)#y8{(SC-Y4Bk`7nZzvGn%FZ#t1bzI!5d@@-bfg{@lyiI@zoV|HEeTnH8vOP5tkrmeu+Mc zV^N4|q7^iNFe^jiPDG{UXvWG)zIYYj|0`A&xw_M?Z5KS+a?Yb^t_ZI4tf7N9?IKom zbg+q#(jODZ7SS2 zVFe(p$V41Erc-C9Sf9G5M@P%|Jy5kgXxp&XNk<8o<*i7{n#C(PM#&od=%)3U!yPHP zvvmlz@*wGfn34#f91Hb|6{eCN930etj2*&+Lx&8M=M@aVootkbC58%+r~Q_#yZ1=( zR4m3MPrkZ1$$&J62pfAM<)noN$xN{h#Dd~Gv#HBWl^&%q)nLD9Q?u)T;5yL}^#gi{ zrLq%ZVpwYBr4nMP^+A@}z*uSnW2p^fOkYwuC7l~NJ91(5T;4;-nY{M>yoXNi8Q+Bu zMx~{X?;4lJn_;dQR`pIL{!cRIc7LfbE?8*l3ipI-zv&qdUfXqhoO%A0)P7yZ0 zC6|s6nkt^PltsBjB&N_Os-pY`Zl>~SIFkRCfEsR^BfJMn8$^{Dh)JC$7lnkR7IPW*%g;%#bxYY(E<;xJsv8M^T^MBlJd%CQEM&i z2}<3hxm#<=I=8`E>mJ&`S|AJh!KX19<4{1@icd;t2J5?~xAZvM$js&kAJ>*%2 z-0W+4_+TabI369F=Y&i|`z-0(9#Nm8WGj_YJId^U@%I3J<&);FQ0x)`wz^1ahAr|~ z7DnI*^+NlOMA);PJ&KUT5`T&JRNfPqZp_$;C%{R!PYNjLD36!zs$`MA4oy8cT7UIKFj<_Ua6fEa=7z4vbv zqv?h^SKNQ5Qqt9_efX469#2{h4-Hz0qpFE!uR;A2Y* z^u?vy27E7weKZI|tuueK^Q)acF?&YLo)vR%YP8~-TUt(+hgT!6xiiCU4Zcni%z)TJ zHEQsQ>DR^dN!O%fPF#9hLr29^j^YItUD&4y<(U;T-xuS)w)eJ%PFkW&2?Q()0;Sp0 zvP;US}i&ge%y%11ma zoRQ4I)=5o9EqdYh&?#Mxn=Q3-a1NWsq0DSq9tln%6#ePBj zXP9d)vZ2!%VLV?5fru|WwQr*HWG9Qt0su0Ff{57H@OO(Si}fP-iBb_>zn)>K2F+vrz;ZLA$o{y5vE?V0 zpK?q#y!7~NT!Bw0m=g*h5FmntV;XbeAeWqfV5A0Lsz*_|%g^mGN6rJob{6@d>4u*Jv~s7l_?_f5KX zg1}V*B=yL?Fm)8`CO{*@`pLR;Uf#SZ8^;Z`*lFbHcWjV0Art;{F0yVdXnCIh4VU;E z&iorL0q~zV>pyXMU?--uTx{*fdEU2EcQWy`MALk5XS(riwWbRUoelG7+BB? zX3g=q+va#a|0W0UyIg!t{rzkMhkNW(DY{{RxI Bzs3Ln diff --git a/music/music.py b/music/backup/music.py similarity index 100% rename from music/music.py rename to music/backup/music.py diff --git a/music/music_wrapper.py b/music/music_wrapper.py new file mode 100644 index 0000000..1e78d2a --- /dev/null +++ b/music/music_wrapper.py @@ -0,0 +1,103 @@ +import wavelink +import discord +import asyncio +import json +import logging +from pathlib import Path +from discord.ext import commands + +from .shared.constants import C_MAIN +from .shared.state import GuildState +from .player.commands import PlayerCommands +from .player.queue import QueueCommands +from .player.events import MusicEvents +from .radio.commands import RadioCommands +from .radio.events import RadioEvents + +log = logging.getLogger(__name__) + + +class Music(PlayerCommands, QueueCommands, MusicEvents, RadioCommands, RadioEvents, commands.Cog): + """ + Main Music cog. Inherits all command/event mixins: + - PlayerCommands (play, pause, resume, skip, stop, seek, loop, autoplay, nowplaying) + - QueueCommands (queue, remove, move, shuffle, clear, history) + - MusicEvents (on_ready, on_voice_state_update, on_wavelink_track_end/start) + - RadioCommands (radio, _play_radio_stream, _notify_radio_drop) + - RadioEvents (on_wavelink_track_exception, on_wavelink_track_stuck) + """ + + def __init__(self, bot: commands.Bot): + self.bot = bot + + # Per-guild state and infrastructure + self._states: dict[int, GuildState] = {} + self._voice_channels: dict[int, int] = {} # guild_id → voice channel id (for status cleanup) + self._radio_channels: dict[int, int] = {} # guild_id → text channel id (for drop notifications) + self._locks: dict[int, asyncio.Lock] = {} + + # Load radio stations + stations_path = Path(__file__).parent / "radio_stations.json" + with open(stations_path, "r", encoding="utf-8") as f: + self._radio_stations: dict[str, list[dict]] = json.load(f) + log.info( + f"Loaded radio stations: " + f"{sum(len(v) for v in self._radio_stations.values())} stations " + f"in {len(self._radio_stations)} categories" + ) + + # ── shared helpers (used by all mixins via self) ────────────────────────── + def state(self, guild_id: int) -> GuildState: + if guild_id not in self._states: + self._states[guild_id] = GuildState() + return self._states[guild_id] + + def _get_player(self, ctx: commands.Context) -> wavelink.Player | None: + """Get the wavelink Player for a context, or None.""" + vc = ctx.voice_client + if isinstance(vc, wavelink.Player): + return vc + return None + + def _lock(self, guild_id: int) -> asyncio.Lock: + """Get or create a per-guild asyncio.Lock.""" + if guild_id not in self._locks: + self._locks[guild_id] = asyncio.Lock() + return self._locks[guild_id] + + def _is_current_track(self, st: GuildState, event_track: wavelink.Playable | None) -> bool: + """True if event_track is the same track currently stored in guild state.""" + if st.current is None or event_track is None: + return False + return event_track.identifier == st.current.identifier + + # ── !music ──────────────────────────────────────────────────────────────── + @commands.command() + async def music(self, ctx: commands.Context): + """List all music commands.""" + embed = discord.Embed(title="🎵 Music Commands", color=C_MAIN) + embed.add_field(name="Playback", value=( + "`!play ` · `!p`\n" + "`!pause` · `!resume` · `!skip` · `!stop`\n" + "`!seek <1:30>` · `!looptrack` · `!loopqueue`\n" + "`!autoplay`" + ), inline=False) + embed.add_field(name="Radio", value=( + "`!radio` — Browse & play radio stations\n" + "Use `!stop` to stop either music or radio" + ), inline=False) + embed.add_field(name="Queue", value=( + "`!queue` · `!q`\n" + "`!remove ` · `!move `\n" + "`!shuffle` · `!clear`" + ), inline=False) + embed.add_field(name="Info", value=( + "`!nowplaying` · `!np`\n" + "`!history` · `!h`" + ), inline=False) + embed.set_footer(text="🎧 DJ role required for control commands") + await ctx.reply(embed=embed) + + +async def setup(bot: commands.Bot): + await bot.add_cog(Music(bot)) diff --git a/music/player/__init__.py b/music/player/__init__.py new file mode 100644 index 0000000..9554d93 --- /dev/null +++ b/music/player/__init__.py @@ -0,0 +1 @@ +# player package diff --git a/music/player/commands.py b/music/player/commands.py new file mode 100644 index 0000000..2f68f9b --- /dev/null +++ b/music/player/commands.py @@ -0,0 +1,263 @@ +import wavelink +import discord +import logging +from discord.ext import commands +from ..shared.constants import ( + C_MAIN, C_QUEUE, C_OK, C_WARN, C_ERR, + LOOP_OFF, LOOP_TRACK, LOOP_QUEUE, LOOP_LABEL, + MODE_MUSIC, MODE_RADIO, +) +from ..shared.utils import fmt_duration, progress_bar, is_dj + +log = logging.getLogger(__name__) + + +class PlayerCommands: + """Mixin: core playback commands (play, pause, resume, skip, stop, seek, loop, autoplay, nowplaying).""" + + # ── !play ───────────────────────────────────────────────────────────────── + @commands.command(aliases=["p"]) + async def play(self, ctx: commands.Context, *, query: str): + """Play a song or playlist. Accepts search terms or YouTube URLs.""" + if not ctx.author.voice: + return await ctx.reply(embed=discord.Embed( + description="❌ Join a voice channel first.", color=C_ERR)) + + st = self.state(ctx.guild.id) + + # Mode check: refuse if radio is active + if st.mode == MODE_RADIO: + return await ctx.reply(embed=discord.Embed( + description="📻 Radio is currently active. Use `!stop` first to switch back to music.", + color=C_WARN)) + + async with self._lock(ctx.guild.id): + vc = self._get_player(ctx) + if not vc: + vc = await ctx.author.voice.channel.connect(cls=wavelink.Player) + vc.autoplay = wavelink.AutoPlayMode.disabled + vc.inactive_timeout = None + self._voice_channels[ctx.guild.id] = ctx.author.voice.channel.id + + async with ctx.typing(): + results = await wavelink.Playable.search(query) + + if not results: + return await ctx.reply(embed=discord.Embed( + description="❌ Nothing found.", color=C_ERR)) + + # ── playlist ────────────────────────────────────────────────────── + if isinstance(results, wavelink.Playlist): + tracks = list(results.tracks) + for t in tracks: + t.extras = {"requester_id": ctx.author.id} + st.queue.append(t) + + if not vc.playing and not vc.paused and st.queue: + st.current = st.queue.popleft() + await vc.play(st.current) + await self._set_channel_status(vc, st.current) + + st.mode = MODE_MUSIC + + embed = discord.Embed( + title="📋 Playlist Queued", + description=f"**{results.name}**", + color=C_QUEUE, + ) + embed.add_field(name="Tracks", value=str(len(tracks))) + embed.add_field(name="Added by", value=ctx.author.mention) + await ctx.reply(embed=embed) + + # ── single track ────────────────────────────────────────────────── + else: + track = results[0] + track.extras = {"requester_id": ctx.author.id} + + if vc.playing or vc.paused: + st.queue.append(track) + embed = discord.Embed( + title="➕ Added to Queue", + description=f"**{track.title}**", + color=C_QUEUE, + ) + embed.add_field(name="Duration", value=fmt_duration(track.length)) + embed.add_field(name="Position", value=f"#{len(st.queue)}") + embed.add_field(name="Requested by", value=ctx.author.mention) + if track.artwork: + embed.set_thumbnail(url=track.artwork) + return await ctx.reply(embed=embed) + + st.current = track + await vc.play(track) + await self._set_channel_status(vc, track) + st.mode = MODE_MUSIC + + embed = discord.Embed( + title="▶️ Now Playing", + description=f"**{track.title}**", + color=C_MAIN, + ) + embed.add_field(name="Duration", value=fmt_duration(track.length)) + embed.add_field(name="Requested by", value=ctx.author.mention) + embed.add_field(name="Loop", value=LOOP_LABEL[st.loop], inline=True) + embed.add_field(name="Autoplay", value="On" if st.autoplay else "Off", inline=True) + if track.artwork: + embed.set_thumbnail(url=track.artwork) + await ctx.reply(embed=embed) + + # ── !pause / !resume ────────────────────────────────────────────────────── + @commands.command() + @is_dj() + async def pause(self, ctx: commands.Context): + vc = self._get_player(ctx) + if vc and vc.playing: + await vc.pause(True) + await ctx.reply(embed=discord.Embed(description="⏸️ Paused.", color=C_OK)) + else: + await ctx.reply(embed=discord.Embed(description="❌ Nothing is playing.", color=C_ERR)) + + @commands.command() + @is_dj() + async def resume(self, ctx: commands.Context): + vc = self._get_player(ctx) + if vc and vc.paused: + await vc.pause(False) + await ctx.reply(embed=discord.Embed(description="▶️ Resumed.", color=C_OK)) + else: + await ctx.reply(embed=discord.Embed(description="❌ Nothing is paused.", color=C_ERR)) + + # ── !skip ───────────────────────────────────────────────────────────────── + @commands.command() + @is_dj() + async def skip(self, ctx: commands.Context): + vc = self._get_player(ctx) + if vc and (vc.playing or vc.paused): + await vc.skip() + await ctx.reply(embed=discord.Embed(description="⏭️ Skipped.", color=C_OK)) + else: + await ctx.reply(embed=discord.Embed(description="❌ Nothing to skip.", color=C_ERR)) + + # ── !stop / !leave ──────────────────────────────────────────────────────── + @commands.command(aliases=["leave", "disconnect"]) + @is_dj() + async def stop(self, ctx: commands.Context): + """Stop playback (music or radio), clear the queue, and disconnect from voice.""" + st = self.state(ctx.guild.id) + current_mode = st.mode + + st.queue.clear() + st.current = None + st.loop = LOOP_OFF + st.autoplay = False + st.mode = None + + vc = self._get_player(ctx) + if vc: + await self._set_channel_status(vc) + await vc.disconnect() + + self._voice_channels.pop(ctx.guild.id, None) + self._radio_channels.pop(ctx.guild.id, None) + self._locks.pop(ctx.guild.id, None) + + msg = "⏹️ Stopped radio and disconnected." if current_mode == MODE_RADIO else "⏹️ Stopped and disconnected." + await ctx.reply(embed=discord.Embed(description=msg, color=C_OK)) + + # ── !seek ───────────────────────────────────────────────────────────────── + @commands.command() + @is_dj() + async def seek(self, ctx: commands.Context, timestamp: str): + """Seek to a timestamp. Format: 1:30 or 90 (seconds).""" + vc = self._get_player(ctx) + if not vc or not vc.playing: + return await ctx.reply(embed=discord.Embed( + description="❌ Nothing is playing.", color=C_ERR)) + try: + if ":" in timestamp: + parts = timestamp.split(":") + if len(parts) == 2: + ms = (int(parts[0]) * 60 + int(parts[1])) * 1000 + else: + ms = (int(parts[0]) * 3600 + int(parts[1]) * 60 + int(parts[2])) * 1000 + else: + ms = int(timestamp) * 1000 + except ValueError: + return await ctx.reply(embed=discord.Embed( + description="❌ Invalid format. Use `1:30` or `90`.", color=C_ERR)) + + await vc.seek(ms) + await ctx.reply(embed=discord.Embed(description=f"⏩ Seeked to `{timestamp}`.", color=C_OK)) + + # ── !looptrack / !loopqueue ─────────────────────────────────────────────── + @commands.command() + @is_dj() + async def looptrack(self, ctx: commands.Context): + st = self.state(ctx.guild.id) + st.loop = LOOP_OFF if st.loop == LOOP_TRACK else LOOP_TRACK + await ctx.reply(embed=discord.Embed( + description=f"🔂 Loop track: **{'On' if st.loop == LOOP_TRACK else 'Off'}**", + color=C_WARN)) + + @commands.command() + @is_dj() + async def loopqueue(self, ctx: commands.Context): + st = self.state(ctx.guild.id) + st.loop = LOOP_OFF if st.loop == LOOP_QUEUE else LOOP_QUEUE + await ctx.reply(embed=discord.Embed( + description=f"🔁 Loop queue: **{'On' if st.loop == LOOP_QUEUE else 'Off'}**", + color=C_WARN)) + + # ── !autoplay ───────────────────────────────────────────────────────────── + @commands.command() + @is_dj() + async def autoplay(self, ctx: commands.Context): + st = self.state(ctx.guild.id) + vc = self._get_player(ctx) + st.autoplay = not st.autoplay + if vc: + mode = wavelink.AutoPlayMode.enabled if st.autoplay else wavelink.AutoPlayMode.disabled + await vc.set_autoplay(mode) + await ctx.reply(embed=discord.Embed( + description=f"✨ Autoplay: **{'On' if st.autoplay else 'Off'}**", + color=C_OK)) + + # ── !nowplaying ─────────────────────────────────────────────────────────── + @commands.command(aliases=["np"]) + async def nowplaying(self, ctx: commands.Context): + vc = self._get_player(ctx) + st = self.state(ctx.guild.id) + + if not vc or not vc.playing or not st.current: + return await ctx.reply(embed=discord.Embed( + description="❌ Nothing is playing.", color=C_ERR)) + + track = st.current + pos_ms = vc.position + dur_ms = track.length + bar = progress_bar(pos_ms, dur_ms) + + if track.extras and isinstance(track.extras, dict): + requester_id = track.extras.get("requester_id") + requester = f"<@{requester_id}>" if requester_id else "Unknown" + else: + requester = "Unknown" + + embed = discord.Embed( + title="🎵 Now Playing", + description=f"### {track.title}", + color=C_MAIN, + ) + embed.add_field( + name="Progress", + value=f"`{fmt_duration(pos_ms)}` {bar} `{fmt_duration(dur_ms)}`", + inline=False, + ) + embed.add_field(name="Requested by", value=requester, inline=True) + embed.add_field(name="Loop", value=LOOP_LABEL[st.loop], inline=True) + embed.add_field(name="Autoplay", value="On" if st.autoplay else "Off", inline=True) + embed.add_field(name="Queue", value=f"{len(st.queue)} track(s) up next", inline=True) + if track.artwork: + embed.set_thumbnail(url=track.artwork) + embed.set_footer(text="yuuka music") + await ctx.reply(embed=embed) diff --git a/music/player/events.py b/music/player/events.py new file mode 100644 index 0000000..6114d50 --- /dev/null +++ b/music/player/events.py @@ -0,0 +1,131 @@ +import wavelink +import discord +import logging +from discord.ext import commands +from ..shared.constants import LOOP_TRACK, LOOP_QUEUE, MODE_RADIO + +log = logging.getLogger(__name__) + + +class MusicEvents: + """Mixin: wavelink + Discord events that belong to the music player.""" + + # ── channel status helpers ──────────────────────────────────────────────── + async def _set_channel_status( + self, + player: wavelink.Player, + track: wavelink.Playable | None = None, + *, + title: str | None = None, + ): + """Set (or clear) the voice channel status. Pass *title* for radio; otherwise uses track.title.""" + channel = player.channel + if channel is None: + return + try: + if title is not None: + status = f"📻 {title}"[:500] + elif track is not None: + status = f"🎵 {track.title}"[:500] + else: + status = None + route = discord.http.Route( + "PUT", + "/channels/{channel_id}/voice-status", + channel_id=channel.id, + ) + await self.bot.http.request(route, json={"status": status}) + except Exception: + log.warning(f"Failed to set voice channel status in {player.guild.id}", exc_info=True) + + async def _clear_channel_status(self, guild_id: int): + """Clear voice channel status after the bot was disconnected.""" + channel_id = self._voice_channels.pop(guild_id, None) + if channel_id is None: + return + try: + route = discord.http.Route( + "PUT", + "/channels/{channel_id}/voice-status", + channel_id=channel_id, + ) + await self.bot.http.request(route, json={"status": None}) + except Exception: + log.warning(f"Failed to clear voice channel status in guild {guild_id}", exc_info=True) + + # ── connect lavalink ────────────────────────────────────────────────────── + @commands.Cog.listener() + async def on_ready(self): + node = wavelink.Node(uri="http://127.0.0.1:2333", password="yuukabot") + await wavelink.Pool.connect(nodes=[node], client=self.bot) + log.info("Wavelink connected to Lavalink") + + # ── voice state update → clean up on disconnect ─────────────────────────── + @commands.Cog.listener() + async def on_voice_state_update( + self, + member: discord.Member, + before: discord.VoiceState, + after: discord.VoiceState, + ): + if member.id != self.bot.user.id: + return + # Bot left a channel (kicked, moved out, or disconnected) + if before.channel is not None and after.channel is None: + guild_id = member.guild.id + st = self._states.pop(guild_id, None) + if st: + st.queue.clear() + self._locks.pop(guild_id, None) + self._radio_channels.pop(guild_id, None) + await self._clear_channel_status(guild_id) + + # ── track end → advance queue ───────────────────────────────────────────── + @commands.Cog.listener() + async def on_wavelink_track_end(self, payload: wavelink.TrackEndEventPayload): + player: wavelink.Player = payload.player + if player is None: + return + + st = self.state(player.guild.id) + + # Ignore stale events (e.g. track_end for an old radio stream after a switch) + if not self._is_current_track(st, payload.track): + return + + if st.loop == LOOP_TRACK and st.current: + await player.play(st.current) + await self._set_channel_status(player, st.current) + return + + if st.loop == LOOP_QUEUE and st.current: + st.queue.append(st.current) + + if st.current: + st.push_history(st.current) + + if st.queue: + next_track = st.queue.popleft() + st.current = next_track + await player.play(next_track) + await self._set_channel_status(player, next_track) + elif st.autoplay: + await player.set_autoplay(wavelink.AutoPlayMode.enabled) + else: + if st.mode == MODE_RADIO: + await self._notify_radio_drop(player.guild.id) + st.current = None + st.mode = None + await self._set_channel_status(player) + + # ── track start → update voice status ───────────────────────────────────── + @commands.Cog.listener() + async def on_wavelink_track_start(self, payload: wavelink.TrackStartEventPayload): + player: wavelink.Player = payload.player + if player is None: + return + st = self.state(player.guild.id) + st.current = payload.track + # Radio sets its own title; only update status for music mode + if st.mode != MODE_RADIO: + await self._set_channel_status(player, st.current) diff --git a/music/player/queue.py b/music/player/queue.py new file mode 100644 index 0000000..cca62d7 --- /dev/null +++ b/music/player/queue.py @@ -0,0 +1,109 @@ +import discord +import random +from collections import deque +from discord.ext import commands +from ..shared.constants import C_QUEUE, C_OK, C_ERR, C_HIST, LOOP_LABEL +from ..shared.utils import fmt_duration, is_dj + + +class QueueCommands: + """Mixin: queue management commands (queue, remove, move, shuffle, clear, history).""" + + # ── !queue ──────────────────────────────────────────────────────────────── + @commands.command(aliases=["q"]) + async def queue(self, ctx: commands.Context): + st = self.state(ctx.guild.id) + + if not st.current and not st.queue: + return await ctx.reply(embed=discord.Embed( + description="📋 Queue is empty.", color=C_QUEUE)) + + embed = discord.Embed(title="📋 Queue", color=C_QUEUE) + + if st.current: + embed.add_field( + name="▶️ Now Playing", + value=f"**{st.current.title}** `{fmt_duration(st.current.length)}`", + inline=False, + ) + + if st.queue: + lines = [] + for i, t in enumerate(list(st.queue)[:15], 1): + lines.append(f"`{i:02}.` **{t.title}** `{fmt_duration(t.length)}`") + if len(st.queue) > 15: + lines.append(f"*... and {len(st.queue) - 15} more*") + embed.add_field(name="Up Next", value="\n".join(lines), inline=False) + + total_ms = sum(t.length for t in st.queue) + embed.set_footer(text=f"{len(st.queue)} tracks · {fmt_duration(total_ms)} remaining · Loop: {LOOP_LABEL[st.loop]}") + await ctx.reply(embed=embed) + + # ── !remove ─────────────────────────────────────────────────────────────── + @commands.command() + @is_dj() + async def remove(self, ctx: commands.Context, position: int): + st = self.state(ctx.guild.id) + if position < 1 or position > len(st.queue): + return await ctx.reply(embed=discord.Embed( + description=f"❌ Position must be between 1 and {len(st.queue)}. Use `!queue` to see positions.", + color=C_ERR)) + q = list(st.queue) + removed = q.pop(position - 1) + st.queue = deque(q) + await ctx.reply(embed=discord.Embed( + description=f"🗑️ Removed **{removed.title}** from queue.", color=C_OK)) + + # ── !move ───────────────────────────────────────────────────────────────── + @commands.command() + @is_dj() + async def move(self, ctx: commands.Context, from_pos: int, to_pos: int): + st = self.state(ctx.guild.id) + size = len(st.queue) + if not (1 <= from_pos <= size) or not (1 <= to_pos <= size): + return await ctx.reply(embed=discord.Embed( + description=f"❌ Invalid positions. Queue has {size} items. Use `!queue` to see positions.", + color=C_ERR)) + q = list(st.queue) + track = q.pop(from_pos - 1) + q.insert(to_pos - 1, track) + st.queue = deque(q) + await ctx.reply(embed=discord.Embed( + description=f"↕️ Moved **{track.title}** to position **{to_pos}**.", color=C_OK)) + + # ── !shuffle ────────────────────────────────────────────────────────────── + @commands.command() + @is_dj() + async def shuffle(self, ctx: commands.Context): + st = self.state(ctx.guild.id) + if not st.queue: + return await ctx.reply(embed=discord.Embed( + description="❌ Queue is empty.", color=C_ERR)) + q = list(st.queue) + random.shuffle(q) + st.queue = deque(q) + await ctx.reply(embed=discord.Embed(description="🔀 Queue shuffled.", color=C_OK)) + + # ── !clear ──────────────────────────────────────────────────────────────── + @commands.command() + @is_dj() + async def clear(self, ctx: commands.Context): + st = self.state(ctx.guild.id) + st.queue.clear() + await ctx.reply(embed=discord.Embed(description="🗑️ Queue cleared.", color=C_OK)) + + # ── !history ────────────────────────────────────────────────────────────── + @commands.command(aliases=["h"]) + async def history(self, ctx: commands.Context): + st = self.state(ctx.guild.id) + if not st.history: + return await ctx.reply(embed=discord.Embed( + description="📜 No history yet.", color=C_HIST)) + lines = [f"`{i:02}.` **{t.title}** `{fmt_duration(t.length)}`" + for i, t in enumerate(st.history, 1)] + embed = discord.Embed( + title="📜 Recently Played", + description="\n".join(lines), + color=C_HIST, + ) + await ctx.reply(embed=embed) diff --git a/music/radio/__init__.py b/music/radio/__init__.py new file mode 100644 index 0000000..740f43e --- /dev/null +++ b/music/radio/__init__.py @@ -0,0 +1 @@ +# radio package diff --git a/music/radio/commands.py b/music/radio/commands.py new file mode 100644 index 0000000..e3be220 --- /dev/null +++ b/music/radio/commands.py @@ -0,0 +1,175 @@ +import wavelink +import discord +import logging +from discord.ext import commands +from ..shared.constants import ( + C_RADIO, C_ERR, C_WARN, + LOOP_OFF, MODE_MUSIC, MODE_RADIO, +) +from .views import _CategoryView + +log = logging.getLogger(__name__) + + +class RadioCommands: + """Mixin: radio browsing command, stream playback, and drop notifications.""" + + # ── !radio ──────────────────────────────────────────────────────────────── + @commands.command() + async def radio(self, ctx: commands.Context): + """Browse and play radio stations with a two-step interactive picker.""" + if not ctx.author.voice: + return await ctx.reply(embed=discord.Embed( + description="❌ Join a voice channel first.", color=C_ERR)) + + st = self.state(ctx.guild.id) + + # Mode check: refuse if music is active + if st.mode == MODE_MUSIC and (st.current or st.queue): + return await ctx.reply(embed=discord.Embed( + description="🎵 Music is currently playing. Use `!stop` first to switch to radio.", + color=C_WARN)) + + # Step 1: show category buttons + view = _CategoryView(self.bot, self._radio_stations) + embed = discord.Embed( + title="📻 Radio — Choose a Category", + description="Select a genre to browse stations:", + color=C_RADIO, + ) + await ctx.reply(embed=embed, view=view) + + # ── radio playback helper (called from _StationSelect callback) ────────── + async def _play_radio_stream( + self, + interaction: discord.Interaction, + category: str, + station: dict, + ): + """Connect, play a radio stream, and set mode. Called from the station picker UI.""" + guild = interaction.guild + if guild is None: + return + + member = guild.get_member(interaction.user.id) + if member is None or member.voice is None: + await interaction.followup.send( + embed=discord.Embed(description="❌ You must be in a voice channel.", color=C_ERR), + ephemeral=True, + ) + return + + st = self.state(guild.id) + + async with self._lock(guild.id): + # Re-check mode — music may have started between !radio and now + if st.mode == MODE_MUSIC: + await interaction.followup.send( + embed=discord.Embed( + description="🎵 Music is currently playing. Use `!stop` first to switch to radio.", + color=C_WARN, + ), + ephemeral=True, + ) + return + + # Connect or reuse existing player + vc = guild.voice_client + if not isinstance(vc, wavelink.Player): + try: + vc = await member.voice.channel.connect(cls=wavelink.Player) + except Exception as e: + await interaction.followup.send( + embed=discord.Embed(description=f"❌ Failed to connect: {e}", color=C_ERR), + ephemeral=True, + ) + return + vc.autoplay = wavelink.AutoPlayMode.disabled + vc.inactive_timeout = None + self._voice_channels[guild.id] = member.voice.channel.id + + # Clear mode before skipping so the old track_end handler + # doesn't interfere with the new station's state + if vc.playing or vc.paused: + st.mode = None + st.current = None + await vc.skip() + + # Resolve and play the stream URL via Lavalink + try: + results = await wavelink.Playable.search(station["url"]) + if not results: + await interaction.followup.send( + embed=discord.Embed( + description=( + f"❌ Could not resolve stream: `{station['url']}`\n" + "Lavalink may need the **LavaSrc** plugin for direct HTTP/Icecast URLs." + ), + color=C_ERR, + ), + ephemeral=True, + ) + return + track = results[0] + except Exception as e: + await interaction.followup.send( + embed=discord.Embed(description=f"❌ Failed to load stream: {e}", color=C_ERR), + ephemeral=True, + ) + return + + # Set radio state *before* play so on_wavelink_track_start + # sees MODE_RADIO and doesn't overwrite our status text + st.queue.clear() + st.current = track + st.loop = LOOP_OFF + st.autoplay = False + st.mode = MODE_RADIO + self._radio_channels[guild.id] = interaction.channel_id + + try: + await vc.play(track) + except Exception as e: + st.mode = None + self._radio_channels.pop(guild.id, None) + await interaction.followup.send( + embed=discord.Embed(description=f"❌ Failed to play stream: {e}", color=C_ERR), + ephemeral=True, + ) + return + + # Set channel status with the JSON station name + await self._set_channel_status(vc, title=station["name"]) + + # Send now-playing embed + embed = discord.Embed( + title="📻 Now Playing", + description=f"### {station['name']}", + color=C_RADIO, + ) + if station["desc"]: + embed.add_field(name="Description", value=station["desc"], inline=False) + embed.add_field(name="Category", value=category, inline=True) + embed.add_field(name="Source", value=station["url"], inline=True) + embed.set_footer(text="yuuka radio · use !stop to disconnect") + await interaction.followup.send(embed=embed) + + # ── radio drop notification ─────────────────────────────────────────────── + async def _notify_radio_drop(self, guild_id: int): + """Send a notification to the channel where radio was started.""" + channel_id = self._radio_channels.pop(guild_id, None) + if channel_id is None: + return + guild = self.bot.get_guild(guild_id) + if guild is None: + return + channel = guild.get_channel_or_thread(channel_id) + if channel is None: + return + try: + await channel.send(embed=discord.Embed( + description="📻 Radio stream ended unexpectedly. Use `!radio` to restart.", + color=C_WARN, + )) + except Exception: + log.warning(f"Failed to send radio-drop notification in guild {guild_id}", exc_info=True) diff --git a/music/radio/events.py b/music/radio/events.py new file mode 100644 index 0000000..891a2a2 --- /dev/null +++ b/music/radio/events.py @@ -0,0 +1,40 @@ +import wavelink +import logging +from discord.ext import commands +from ..shared.constants import MODE_RADIO + +log = logging.getLogger(__name__) + + +class RadioEvents: + """Mixin: wavelink failure events that only apply to radio mode.""" + + # ── track exception → radio stream failure ──────────────────────────────── + @commands.Cog.listener() + async def on_wavelink_track_exception(self, payload: wavelink.TrackExceptionEventPayload): + player: wavelink.Player = payload.player + if player is None: + return + st = self.state(player.guild.id) + if st.mode == MODE_RADIO: + if not self._is_current_track(st, payload.track): + return + log.warning(f"Radio stream exception in guild {player.guild.id}: {payload.exception}") + await self._notify_radio_drop(player.guild.id) + st.mode = None + await self._set_channel_status(player) + + # ── track stuck → radio stream failure ──────────────────────────────────── + @commands.Cog.listener() + async def on_wavelink_track_stuck(self, payload: wavelink.TrackStuckEventPayload): + player: wavelink.Player = payload.player + if player is None: + return + st = self.state(player.guild.id) + if st.mode == MODE_RADIO: + if not self._is_current_track(st, payload.track): + return + log.warning(f"Radio stream stuck in guild {player.guild.id}, threshold {payload.threshold_ms}ms") + await self._notify_radio_drop(player.guild.id) + st.mode = None + await self._set_channel_status(player) diff --git a/music/radio/views.py b/music/radio/views.py new file mode 100644 index 0000000..d2afb54 --- /dev/null +++ b/music/radio/views.py @@ -0,0 +1,102 @@ +import discord +from discord.ext import commands +from ..shared.constants import C_RADIO + + +class _CategoryView(discord.ui.View): + """Step 1: buttons for each radio category.""" + + def __init__(self, bot: commands.Bot, stations: dict, timeout: float = 120.0): + super().__init__(timeout=timeout) + self.bot = bot + self.stations = stations + self.message: discord.Message | None = None + + for i, category in enumerate(stations.keys()): + label = category[:80] # Discord button label limit + btn = discord.ui.Button( + label=label, + style=discord.ButtonStyle.primary, + row=i // 5, # 5 buttons per row max + ) + btn.callback = self._make_category_callback(category) + self.add_item(btn) + + def _make_category_callback(self, category: str): + async def callback(interaction: discord.Interaction): + station_list = self.stations[category] + view = _StationSelect(self.bot, category, station_list, interaction, timeout=self.timeout) + embed = discord.Embed( + title=f"📻 {category}", + description="Pick a station from the dropdown below:", + color=C_RADIO, + ) + await interaction.response.edit_message(embed=embed, view=view) + view.message = interaction.message + return callback + + async def on_timeout(self): + if self.message: + try: + for child in self.children: + child.disabled = True + await self.message.edit(view=self) + except Exception: + pass + + +class _StationSelect(discord.ui.View): + """Step 2: dropdown to pick a specific station.""" + + def __init__( + self, + bot: commands.Bot, + category: str, + stations: list[dict], + interaction: discord.Interaction, + timeout: float = 120.0, + ): + super().__init__(timeout=timeout) + self.bot = bot + self.category = category + self.stations = stations + self._interaction = interaction + self.message: discord.Message | None = None + self._picked: dict | None = None + + options = [] + for s in stations: + label = s["name"][:100] # Discord select option limit + desc = s["desc"][:100] if s["desc"] else "\u200b" # zero-width space if empty + options.append(discord.SelectOption(label=label, description=desc, value=s["name"])) + + select = discord.ui.Select(placeholder="Choose a station…", options=options) + select.callback = self._on_select + self.add_item(select) + + async def _on_select(self, interaction: discord.Interaction): + station_name = interaction.data["values"][0] # type: ignore + for s in self.stations: + if s["name"] == station_name: + self._picked = s + break + + # Disable the dropdown immediately + for child in self.children: + child.disabled = True + await interaction.response.edit_message(view=self) + + # Delegate playback to the cog + cog = self.bot.get_cog("Music") + if cog and self._picked: + await cog._play_radio_stream(interaction, self.category, self._picked) + self.stop() + + async def on_timeout(self): + if self.message: + try: + for child in self.children: + child.disabled = True + await self.message.edit(view=self) + except Exception: + pass diff --git a/music/shared/__init__.py b/music/shared/__init__.py new file mode 100644 index 0000000..ad5da11 --- /dev/null +++ b/music/shared/__init__.py @@ -0,0 +1 @@ +# shared package diff --git a/music/shared/constants.py b/music/shared/constants.py new file mode 100644 index 0000000..973f68d --- /dev/null +++ b/music/shared/constants.py @@ -0,0 +1,23 @@ +# Colour palette +C_MAIN = 0x8BE9FD # cyan – now playing +C_QUEUE = 0xBD93F9 # purple – queue +C_OK = 0x50FA7B # green – success +C_WARN = 0xFFB86C # orange – warnings / loop +C_ERR = 0xFF5555 # red – errors +C_HIST = 0x6272A4 # muted – history +C_RADIO = 0xFF79C6 # pink – radio + +# Loop modes +LOOP_OFF = 0 +LOOP_TRACK = 1 +LOOP_QUEUE = 2 + +LOOP_LABEL = { + LOOP_OFF: "Off", + LOOP_TRACK: "🔂 Track", + LOOP_QUEUE: "🔁 Queue", +} + +# Playback modes +MODE_MUSIC = "music" +MODE_RADIO = "radio" diff --git a/music/shared/state.py b/music/shared/state.py new file mode 100644 index 0000000..470fd5b --- /dev/null +++ b/music/shared/state.py @@ -0,0 +1,18 @@ +import wavelink +from collections import deque +from .constants import LOOP_OFF + + +class GuildState: + def __init__(self): + self.queue: deque = deque() + self.history: list = [] # last 10 tracks + self.loop: int = LOOP_OFF + self.autoplay: bool = False + self.current: wavelink.Playable | None = None + self.mode: str | None = None + + def push_history(self, track: wavelink.Playable): + self.history.insert(0, track) + if len(self.history) > 10: + self.history.pop() diff --git a/music/shared/utils.py b/music/shared/utils.py new file mode 100644 index 0000000..087bbbc --- /dev/null +++ b/music/shared/utils.py @@ -0,0 +1,33 @@ +import discord +from discord.ext import commands +from .constants import C_ERR + + +def fmt_duration(ms: int) -> str: + s = ms // 1000 + m, s = divmod(s, 60) + h, m = divmod(m, 60) + return f"{h}:{m:02}:{s:02}" if h else f"{m}:{s:02}" + + +def progress_bar(position_ms: int, duration_ms: int, width: int = 14) -> str: + if duration_ms == 0: + return "▬" * width + pct = min(position_ms / duration_ms, 1.0) + filled = int(pct * width) + return "▬" * filled + "🔘" + "▬" * (width - filled) + + +def is_dj(): + """Soft DJ check — allows anyone to use info/play, restricts control commands.""" + async def predicate(ctx: commands.Context): + if any(r.name.lower() == "dj" for r in ctx.author.roles): + return True + await ctx.reply( + embed=discord.Embed( + description="🎧 You need the **DJ** role to use this command.", + color=C_ERR, + ) + ) + return False + return commands.check(predicate) diff --git a/tgbridge/__pycache__/__init__.cpython-312.pyc b/tgbridge/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index f52be0280f8d73562ce0193e7341e7b7b5039188..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 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 bQ(3IMC2m^(ivAP*84bOU{+J>!LixV|ry22j diff --git a/twitterbridge/__pycache__/__init__.cpython-312.pyc b/twitterbridge/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 2c7be730d5a578f0c32faa4554cb5fb808f9a9a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 168 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 QF^KVrnURsPh#ANN0BVaKod5s; diff --git a/wavelink/__pycache__/music.cpython-314.pyc b/wavelink/__pycache__/music.cpython-314.pyc deleted file mode 100644 index 5aeb880db62787c0a00cd5252596dd4be806065d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4604 zcmdrPTWlN0ahG>Tov0Hj)0QaPN<>nz%(ElXlBoER$Te*lmH|aznSu#WS;Uzz0L+s~5~HVyUVc-Z z!q|4&Hp=KsH)$t65@S3h#-3rKd{w~4JIs_}or`fEqT6G%hlutEh#VKG$tRVW87Z66 zk{c|9RCvhxK1zrLJ2*ph8Y8-G(ylWdM1lQ)EcOf+73$f_aMblUE?hyyY#vgDbsqTL zkJI{U6mc?0@oX7B#zNm#<8(ZY6VF9qC`td5&d8g}z!WqZeW z_;mnD5)4~w=x$%f9ex0SpL7G|L~Sx!f0hasTn=0yW;nK8}*JDX^!iivUI${D(Ws)sa@v28<4nu_+h_Gw+u9SMc{ z2Mz`gz_0(vz~JDZ$g(cYwz&B{q#PI-03J0@L{Q2nCaMnIvQb6Jm`xRO%a$gWQ`1?nuZkgW zver_TeZ4B4$K;i4LY1Y|Y}IzvG16{y&x zm@%9^MQ6|T@b%ssJAT{werLhiQ*s_R_~Qls_$Pd8!8z~|Ke)z_<`eXhhE8}j!bhH^ z%MKdpVwYRo09)=*bqU-VW`K1sbT83)5+{?kL*F1I1=ud~Wjak`^cDJ562e?!dw}#N zcQG%kvyf33O;e)oV9! z+d#0k1G>-W$!EmH9bLn2iLqX)6DDebhI>cHv)p(Nc2subIE@0bv2Etkdn-g!yYP&_ zSIxn7%rR1RIdDu`@0dNv0a?7U)ue(AEvbzJFy3r8n}tQR=K32#CTjUNit1=+5TF&C zHtsjayew?K;~Y~bv#miO45Qm0$Hi@1=_SX;*_sZXq*-Pw37}bak~|N*)t2MpOtif^ z+IYun38C?{-CNm{jdRhC4GC39AOl;T2$BtWJtUid!Sf4vAhnKt1)eZGZB_!}=KQlt z-kX)nx#FzsO=rE5_mYxM%HHHO__v(#;pNLDGuoUJB49e5o${Vj^4XLxkN$rDhrjvz zjYV%(x$Mnlq*<6f;uQtcF6H%UMKw7~l*y@bE;DNi)(D~u6Bi_&@&aln$Wc*EracNW zWYy&HEuGECrd^XIH92jvDBa}rw4RYgdpSEXN&Sl2jeNFCNz<;YQu2ZZG}aqkytnr` zueY4I)DY4+%8`q*&|$Q{L5KP3p3A*Bw=ie8y5V1Nb+7E~dVA+jb{2Y0m3EFA+eZt+ zD3EMS2ll_?SaKMFL&d-$BXF!3IA#P!i-FPKzxId3?-GT;Xen^U@Sj;Ye$T(h@DCLI z1J{m!>ghH-fubj1ctS-_=vQ3H^MbMMg=_Xdx!P;%4VHF>jqTxr5Wd&e@z%v3G+*OZ zoNbF!3sY~&-_9C*SAp+Zac)~oFQf~*g4gr!yuS4M^_fEFOC{&9!4Kc&hu3zK9jED< zo%Br7HJ-Q+{&{;CM$OOM<%b_>m*Bux`$t}+@6wSF`(8Uh%Z> zbYzgdNm>BDDeS=BOGk#-n?4`Fw`_FeAbX41gK&V39At^P3f zwfqM=uyVGQ|K5Yalk#6RB2!17kd+|UH@0d;1@T4dv)`8SMeCxvfdfk)d?0Fk@Hni< zv5n`kc)Y*(KDxiAE4iGU@`;SP53r`gs(-9oSQP=WRK8Euet;|GQu$(Gk+bTS2>X%Z z6m*tbwm6NJ-S988>|SxV8*YEm?Kj+gMR#Ax9WsPaK?ni7^k2;5xTe}$RrIA7HA~MF1L0fyoW{(u*<#82p^!(t2_{n zi*5M5la*$$a&#mTGfFC-K^SVvi+L$inQQNXw?A(2G@R@YcFx0&umZv<7DS5LaYAzN#qPwCIR{)=Tj$#EY5#|j3KMIwsySP zhay>z!x1WBMWU4&6Hv7_QM*0jHMf8AT>y809q#}Y0|CAtnxs@JkyNJ2xvn0r24@%=#@U{{{-zYu*3=