""" Media Views: All UI views for the music cog. With auto-refresh, custom emojis, and beautiful styling. """ from __future__ import annotations import asyncio import random from typing import TYPE_CHECKING import discord if TYPE_CHECKING: from .media import Media, Track, GuildPlaybackState try: import wavelink except Exception: wavelink = None from bot.theme import ( NEON_CYAN, NEON_PURPLE, NEON_ORANGE, NEON_LIME, NEON_PINK, panel_divider, fancy_divider, fancy_header, progress_bar, music_embed, music_now_playing, music_status, beautiful_list, idle_embed_for_guild, idle_text, add_banner_to_embed ) def _emoji(key: str, default: str) -> str: """Get emoji from config or return default.""" from .media import _emoji as resolve_emoji return resolve_emoji(key, default) def _button_emoji(key: str, default: str) -> str | discord.PartialEmoji | None: """Return emoji for button labels. Returns PartialEmoji for custom tags.""" value = _emoji(key, default) if not value: return default # Try to parse as custom emoji tag if isinstance(value, str) and value.startswith("<") and value.endswith(">"): try: return discord.PartialEmoji.from_str(value) except Exception: pass return value if isinstance(value, str) else default # ═══════════════════════════════════════════════════════════════════════════════ # AUDIO FILTERS CONFIGURATION - جميع الفلاتر الصوتية # ═══════════════════════════════════════════════════════════════════════════════ AUDIO_FILTERS = { "none": { "name": "None", "name_ar": "بدون", "description": "No filter applied", "description_ar": "بدون فلتر", "emoji_key": "no" }, "nightcore": { "name": "Nightcore", "name_ar": "نايتكور", "description": "Speed up with higher pitch", "description_ar": "تسريع مع طبقة صوت أعلى", "emoji_key": "excitedmusicnote", "rate": 1.3, "pitch": 1.25 }, "vaporwave": { "name": "Vaporwave", "name_ar": "فايبوريف", "description": "Slowed down with reverb", "description_ar": "تبطيء مع صدى", "emoji_key": "y_moonstars", "rate": 0.85, "pitch": 0.9 }, "bassboost": { "name": "Bass Boost", "name_ar": "تعزيز الباس", "description": "Enhanced bass frequencies", "description_ar": "تعزيز ترددات الباس", "emoji_key": "soundwhite", "bands": [(0, 0.25), (1, 0.2), (2, 0.15), (3, 0.1), (4, 0.05)] }, "8d": { "name": "8D Audio", "name_ar": "صوت 8D", "description": "Surround sound effect", "description_ar": "تأثير صوت محيطي", "emoji_key": "gamer", "rotation": 0.2 }, "karaoke": { "name": "Karaoke", "name_ar": "كاريوكي", "description": "Remove vocals", "description_ar": "إزالة الغناء", "emoji_key": "microphonewhite", "karaoke": True }, "tremolo": { "name": "Tremolo", "name_ar": "تريمولو", "description": "Vibrating sound effect", "description_ar": "تأثير صوت مهتز", "emoji_key": "oof", "frequency": 2.0, "depth": 0.5 }, "vibrato": { "name": "Vibrato", "name_ar": "فيبراتو", "description": "Pitch oscillation effect", "description_ar": "تأثير تذبذب الطبقة", "emoji_key": "wave", "frequency": 4.0, "depth": 0.5 }, "echo": { "name": "Echo", "name_ar": "صدى", "description": "Echo/reverb effect", "description_ar": "تأثير صدى", "emoji_key": "sounddark", "delay": 0.5, "decay": 0.5 }, "chipmunk": { "name": "Chipmunk", "name_ar": "سنجاب", "description": "High pitched voice", "description_ar": "صوت عالي النبرة", "emoji_key": "uwu", "pitch": 1.5, "rate": 1.1 }, "deep": { "name": "Deep Voice", "name_ar": "صوت عميق", "description": "Lowered pitch voice", "description_ar": "صوت منخفض النبرة", "emoji_key": "gigachad", "pitch": 0.8, "rate": 0.95 }, "speedup": { "name": "Speed Up", "name_ar": "تسريع", "description": "Faster playback speed", "description_ar": "سرعة تشغيل أسرع", "emoji_key": "letsgo", "rate": 1.25 }, "slowdown": { "name": "Slow Down", "name_ar": "تبطيء", "description": "Slower playback speed", "description_ar": "سرعة تشغيل أبطأ", "emoji_key": "rip", "rate": 0.75 }, "bassboost_high": { "name": "Bass Boost+", "name_ar": "تعزيز باس قوي", "description": "Extreme bass boost", "description_ar": "تعزيز باس قوي جداً", "emoji_key": "Boombox", "bands": [(0, 0.35), (1, 0.3), (2, 0.25), (3, 0.2), (4, 0.15)] }, "pop": { "name": "Pop", "name_ar": "بوب", "description": "Pop music EQ preset", "description_ar": "مُعادل بوب", "emoji_key": "microphonedark", "bands": [(0, -0.05), (1, 0.1), (2, 0.15), (3, 0.1), (4, -0.05)] }, "rock": { "name": "Rock", "name_ar": "روك", "description": "Rock music EQ preset", "description_ar": "مُعادل روك", "emoji_key": "pepeguitar", "bands": [(0, 0.15), (1, 0.1), (2, 0.0), (3, 0.05), (4, 0.15)] }, "electronic": { "name": "Electronic", "name_ar": "إلكترونية", "description": "Electronic music EQ preset", "description_ar": "مُعادل إلكترونية", "emoji_key": "djpeepo", "bands": [(0, 0.1), (1, 0.05), (2, 0.0), (3, 0.1), (4, 0.15)] }, "classical": { "name": "Classical", "name_ar": "كلاسيكية", "description": "Classical music EQ preset", "description_ar": "مُعادل كلاسيكية", "emoji_key": "rushiapiano", "bands": [(0, 0.05), (1, 0.0), (2, 0.1), (3, 0.1), (4, 0.05)] }, "flat": { "name": "Flat", "name_ar": "مسطح", "description": "Flat EQ (no enhancement)", "description_ar": "مُعادل مسطح", "emoji_key": "flat", "bands": [(0, 0.0), (1, 0.0), (2, 0.0), (3, 0.0), (4, 0.0)] } } def get_filter_emoji(filter_key: str) -> str: """Get emoji for a filter, using custom emojis if available.""" filter_data = AUDIO_FILTERS.get(filter_key, {}) emoji_key = filter_data.get('emoji_key', 'musicbeat') default = filter_data.get('emoji', '🎵') return _emoji(emoji_key, default) # ═══════════════════════════════════════════════════════════════════════════════ # ERROR HANDLING DECORATOR - لمنع أخطاء interaction failed # ═══════════════════════════════════════════════════════════════════════════════ def safe_interaction(func): """Decorator to safely handle interactions without 'interaction failed' errors.""" async def wrapper(self, interaction: discord.Interaction, *args, **kwargs): try: return await func(self, interaction, *args, **kwargs) except discord.NotFound: pass except discord.HTTPException as e: if e.code == 40060: pass elif hasattr(self, 'cog') and hasattr(self.cog, 'bot') and hasattr(self.cog.bot, 'logger'): self.cog.bot.logger.warning(f"HTTP error in {func.__name__}: {e}") except discord.InteractionResponded: pass except Exception as e: if hasattr(self, 'cog') and hasattr(self.cog, 'bot') and hasattr(self.cog.bot, 'logger'): self.cog.bot.logger.error(f"Error in {func.__name__}: {e}") try: if not interaction.response.is_done(): await interaction.response.send_message("⚠️ An error occurred. Try again!", ephemeral=True) else: await interaction.followup.send("⚠️ An error occurred. Try again!", ephemeral=True) except: pass return wrapper # ═══════════════════════════════════════════════════════════════════════════════ # SAFE INTERACTION HELPERS - دوال مساعدة آمنة # ═══════════════════════════════════════════════════════════════════════════════ async def safe_defer(interaction: discord.Interaction, thinking: bool = False) -> bool: """Safely defer an interaction.""" try: if not interaction.response.is_done(): await interaction.response.defer(ephemeral=True, thinking=thinking) return True except (discord.InteractionResponded, discord.NotFound, discord.HTTPException): return True return False async def safe_send(interaction: discord.Interaction, content: str = None, *, view: discord.ui.View = None, embed: discord.Embed = None, ephemeral: bool = True) -> bool: """Safely send a message as response or followup.""" kwargs = {"ephemeral": ephemeral} if content: kwargs["content"] = content if view: kwargs["view"] = view if embed: kwargs["embed"] = embed try: if interaction.response.is_done(): await interaction.followup.send(**kwargs) else: await interaction.response.send_message(**kwargs) return True except (discord.InteractionResponded, discord.NotFound, discord.HTTPException): try: await interaction.followup.send(**kwargs) except: pass return False async def safe_edit(message: discord.Message, **kwargs) -> bool: """Safely edit a message.""" try: await message.edit(**kwargs) return True except (discord.NotFound, discord.HTTPException): return False # ═══════════════════════════════════════════════════════════════════════════════ # AUTO REFRESH MIXIN - ميزة التحديث التلقائي # ═══════════════════════════════════════════════════════════════════════════════ class AutoRefreshMixin: """Mixin class for auto-refresh functionality.""" _refresh_interval: int = 10 # seconds - minimum 10s to reduce rate limits _refresh_task: asyncio.Task = None _message: discord.Message = None _stopped: bool = False _consecutive_failures: int = 0 async def start_auto_refresh(self, message: discord.Message) -> None: """Start auto-refresh task.""" self._message = message self._stopped = False self._refresh_task = asyncio.create_task(self._auto_refresh_loop()) async def _auto_refresh_loop(self) -> None: """Auto refresh loop.""" while not self._stopped: try: await asyncio.sleep(self._refresh_interval) if self._stopped or not self._message: break # Build new content embed = await self._build_refresh_embed() self._build_refresh_view() await self._message.edit(embed=embed, view=self) self._consecutive_failures = 0 except discord.NotFound: break except discord.HTTPException: self._consecutive_failures += 1 if self._consecutive_failures >= 5: break await asyncio.sleep(2) continue except asyncio.CancelledError: break except Exception: self._consecutive_failures += 1 if self._consecutive_failures >= 5: break await asyncio.sleep(2) def stop_refresh(self) -> None: """Stop auto-refresh.""" self._stopped = True if self._refresh_task and not self._refresh_task.done(): self._refresh_task.cancel() async def _build_refresh_embed(self) -> discord.Embed: """Override this method to build refresh embed.""" raise NotImplementedError def _build_refresh_view(self) -> None: """Override this method to build refresh view.""" pass # ═══════════════════════════════════════════════════════════════════════════════ # FILTERS PANEL VIEW - بانل الفلاتر المنفصل مع التحديث التلقائي # ═══════════════════════════════════════════════════════════════════════════════ class FiltersPanelView(discord.ui.View, AutoRefreshMixin): """Separate Filters Panel with auto-refresh.""" def __init__(self, cog: "Media", guild_id: int) -> None: discord.ui.View.__init__(self, timeout=None) AutoRefreshMixin.__init__(self) self.cog = cog self.guild_id = guild_id self._build_view() def _build_view(self) -> None: """Build all UI components.""" self.clear_items() state = self.cog._guild_state(self.guild_id) # Row 0: Filter Select Menu self.add_item(FilterSelect(self.cog, self.guild_id, state.filter_preset)) # Row 1: Volume Quick Buttons (50, 100, 150, 200) for vol in [50, 100, 150, 200]: style = discord.ButtonStyle.success if state.volume >= vol else discord.ButtonStyle.secondary btn = discord.ui.Button( label=f"{vol}%", style=style, row=1, custom_id=f"filter:vol:{vol}" ) btn.callback = lambda interaction, v=vol: self._set_volume(interaction, v) self.add_item(btn) # Row 2: Volume display and mute vol_emoji = _emoji('soundwhite', '🔊') vol_display = discord.ui.Button( label=f"{state.volume}%", emoji=vol_emoji, style=discord.ButtonStyle.primary, row=2, custom_id="filter:vol_display", disabled=True ) self.add_item(vol_display) mute_emoji = _emoji('soundmutewhite', '🔇') mute_btn = discord.ui.Button( label="Mute", emoji=mute_emoji, style=discord.ButtonStyle.danger if state.volume == 0 else discord.ButtonStyle.secondary, row=2, custom_id="filter:mute" ) mute_btn.callback = self._mute self.add_item(mute_btn) # Row 3: Loop and Shuffle loop_emoji = _emoji('animatedarrowblue', '🔁') if state.loop_mode == "off" else (_emoji('animatedarrowpurple', '🔂') if state.loop_mode == "track" else _emoji('animatedarrowpink', '🔄')) loop_btn = discord.ui.Button( label=f"Loop: {state.loop_mode}", emoji=loop_emoji, style=discord.ButtonStyle.blurple if state.loop_mode != "off" else discord.ButtonStyle.secondary, row=3, custom_id="filter:loop" ) loop_btn.callback = self._toggle_loop self.add_item(loop_btn) shuffle_emoji = _emoji('emojidance', '🔀') shuffle_btn = discord.ui.Button( label="Shuffle", emoji=shuffle_emoji, style=discord.ButtonStyle.primary, row=3, custom_id="filter:shuffle" ) shuffle_btn.callback = self._shuffle self.add_item(shuffle_btn) # Row 4: Close button close_emoji = _emoji('no', '❌') close_btn = discord.ui.Button( label="Close", emoji=close_emoji, style=discord.ButtonStyle.danger, row=4, custom_id="filter:close" ) close_btn.callback = self._close self.add_item(close_btn) async def _build_refresh_embed(self) -> discord.Embed: """Build the filters embed with styling.""" state = self.cog._guild_state(self.guild_id) current_filter = AUDIO_FILTERS.get(state.filter_preset, AUDIO_FILTERS["none"]) # Get current track info current = self.cog.now_playing.get(self.guild_id) now_playing_str = "" if current: now_playing_str = f"\n{_emoji('spotify', '🎵')} **Now Playing:** [{current.title[:40]}]({current.webpage_url})" # Build embed with theme styling embed = discord.Embed( title="꧁⫷ 𝕄𝕦𝕤𝕚𝕔 𝔽𝕚𝕝𝕥𝕖𝕣𝕤 ⫸꧂", description=f"{panel_divider('purple')}" f"{now_playing_str}" f"\n**Filter | الفلتر:** {get_filter_emoji(state.filter_preset)} {current_filter['name']}" f"\n**Volume | الصوت:** {_emoji('soundwhite', '🔊')} {state.volume}%" f"\n**Loop | التكرار:** {_emoji('animatedarrowblue', '🔁')} {state.loop_mode}" f"\n{panel_divider('purple')}", color=NEON_PURPLE ) # Add filter list filter_list = [] for key, data in list(AUDIO_FILTERS.items())[:10]: active = _emoji('tick', '✅') if key == state.filter_preset else "○" filter_list.append(f"{active} {get_filter_emoji(key)} **{data['name']}**") embed.add_field( name=f"{_emoji('spotify', '🎵')} Available Filters | الفلاتر المتاحة", value=beautiful_list(filter_list, style="sparkle", numbered=False) + f"\n\n{fancy_divider('music')}", inline=False ) embed.set_footer(text=f"{_emoji('info', '💡')} Auto-refresh every {self._refresh_interval}s | تحديث تلقائي") return embed def _build_refresh_view(self) -> None: """Rebuild view for refresh.""" self._build_view() @safe_interaction async def _set_volume(self, interaction: discord.Interaction, volume: int) -> None: """Set volume to specific value.""" guild = interaction.guild if not guild: return await safe_defer(interaction) msg = await self.cog._set_volume(guild, volume) self._build_view() if self._message: await self._message.edit(embed=await self._build_refresh_embed(), view=self) await safe_send(interaction, msg) @safe_interaction async def _mute(self, interaction: discord.Interaction) -> None: """Mute/unmute volume.""" guild = interaction.guild if not guild: return await safe_defer(interaction) state = self.cog._guild_state(guild.id) if state.volume > 0: state._previous_volume = state.volume msg = await self.cog._set_volume(guild, 0) msg = f"{_emoji('soundmutedark', '🔇')} Muted! | تم كتم الصوت!" else: prev_vol = getattr(state, '_previous_volume', 80) msg = await self.cog._set_volume(guild, prev_vol) msg = f"{_emoji('soundwhite', '🔊')} Unmuted: {prev_vol}% | تم إلغاء الكتم!" self._build_view() if self._message: await self._message.edit(embed=await self._build_refresh_embed(), view=self) await safe_send(interaction, msg) @safe_interaction async def _toggle_loop(self, interaction: discord.Interaction) -> None: """Toggle loop mode.""" await safe_defer(interaction) state = self.cog._guild_state(self.guild_id) modes = ["off", "track", "queue"] current_idx = modes.index(state.loop_mode) if state.loop_mode in modes else 0 state.loop_mode = modes[(current_idx + 1) % len(modes)] self._build_view() if self._message: await self._message.edit(embed=await self._build_refresh_embed(), view=self) await safe_send(interaction, f"🔁 Loop: **{state.loop_mode}**") @safe_interaction async def _shuffle(self, interaction: discord.Interaction) -> None: """Shuffle the queue.""" await safe_defer(interaction) queue = self.cog.queues.get(self.guild_id, []) if len(queue) > 1: random.shuffle(queue) guild = interaction.guild if guild and wavelink: player = guild.voice_client if isinstance(player, wavelink.Player): try: await player.queue.shuffle() except: pass await safe_send(interaction, "🔀 Queue shuffled! | تم خلط الطابور!") else: await safe_send(interaction, "❌ Need more tracks to shuffle.") @safe_interaction async def _close(self, interaction: discord.Interaction) -> None: """Close the filters panel.""" self.stop_refresh() await safe_defer(interaction) try: await interaction.message.delete() except: pass # ═══════════════════════════════════════════════════════════════════════════════ # FILTER SELECT DROPDOWN - قائمة الفلاتر المنسدلة # ═══════════════════════════════════════════════════════════════════════════════ class FilterSelect(discord.ui.Select): """Dropdown select for audio filters.""" def __init__(self, cog: "Media", guild_id: int, current_filter: str) -> None: self.cog = cog self.guild_id = guild_id options = [] for key, data in AUDIO_FILTERS.items(): is_default = key == current_filter options.append(discord.SelectOption( label=data['name'], description=data['description'][:50], emoji=get_filter_emoji(key), value=key, default=is_default )) super().__init__( placeholder=f"{_emoji('settings', '🎛️')} Select a filter...", min_values=1, max_values=1, options=options, custom_id=f"filter_select:{guild_id}" ) @safe_interaction async def callback(self, interaction: discord.Interaction) -> None: if not self.values: return filter_key = self.values[0] state = self.cog._guild_state(self.guild_id) state.filter_preset = filter_key # Apply filter to wavelink player if available guild = interaction.guild if guild and wavelink: player = guild.voice_client if isinstance(player, wavelink.Player): try: await self._apply_wavelink_filter(player, filter_key) except Exception as e: if hasattr(self.cog.bot, 'logger'): self.cog.bot.logger.warning(f"Filter apply error: {e}") filter_name = AUDIO_FILTERS.get(filter_key, {}).get('name', filter_key) await interaction.response.send_message(f"🎛️ Filter: **{filter_name}**", ephemeral=True) # Update parent view if self.view and hasattr(self.view, '_message') and self.view._message: self.view._build_view() try: await self.view._message.edit(embed=await self.view._build_refresh_embed(), view=self.view) except: pass async def _apply_wavelink_filter(self, player, filter_key: str) -> None: """Apply filter to wavelink player.""" filter_data = AUDIO_FILTERS.get(filter_key, {}) if filter_key == "none": await player.set_filters(wavelink.Filters()) return filters = wavelink.Filters() if 'rate' in filter_data or 'pitch' in filter_data: rate = filter_data.get('rate', 1.0) pitch = filter_data.get('pitch', 1.0) filters.timescale.set(rate=rate, pitch=pitch) if 'rotation' in filter_data: filters.rotation.set(rotation=filter_data['rotation']) if 'frequency' in filter_data and 'depth' in filter_data: if 'tremolo' in filter_key: filters.tremolo.set(frequency=filter_data['frequency'], depth=filter_data['depth']) elif 'vibrato' in filter_key: filters.vibrato.set(frequency=filter_data['frequency'], depth=filter_data['depth']) if filter_data.get('karaoke'): filters.karaoke.set(level=1.0, mono_level=1.0, filter_band=220.0, filter_width=100.0) if 'bands' in filter_data: bands_payload = [] for i, band in enumerate(filter_data['bands']): gain = float(band[1] if isinstance(band, (list, tuple)) and len(band) > 1 else 0.0) bands_payload.append({"band": i, "gain": gain}) filters.equalizer.set(bands=bands_payload) await player.set_filters(filters) # ═══════════════════════════════════════════════════════════════════════════════ # MAIN MUSIC PANEL VIEW - القائمة الرئيسية للموسيقى مع التحديث التلقائي # ═══════════════════════════════════════════════════════════════════════════════ class MusicPanelView(discord.ui.View, AutoRefreshMixin): """Main music panel with auto-refresh.""" _refresh_interval: int = 4 def __init__(self, cog: "Media", guild_id: int | None = None) -> None: discord.ui.View.__init__(self, timeout=None) AutoRefreshMixin.__init__(self) self.cog = cog self.guild_id = guild_id or 0 self._build_view() def _build_view(self) -> None: """Build a modern compact 3-row music control layout.""" self.clear_items() # Row 1: Playback back_btn = discord.ui.Button(label="Back", emoji=_button_emoji("previous", "⏮️"), style=discord.ButtonStyle.secondary, row=0, custom_id="music:panel:back") back_btn.callback = self._previous self.add_item(back_btn) play_pause_btn = discord.ui.Button(label="Play/Pause", emoji=_button_emoji("play", "⏯️"), style=discord.ButtonStyle.primary, row=0, custom_id="music:panel:playpause") play_pause_btn.callback = self._pause self.add_item(play_pause_btn) skip_btn = discord.ui.Button(label="Skip", emoji=_button_emoji("skip", "⏭️"), style=discord.ButtonStyle.secondary, row=0, custom_id="music:panel:skip") skip_btn.callback = self._skip self.add_item(skip_btn) stop_btn = discord.ui.Button(label="Stop", emoji=_button_emoji("stop", "⏹️"), style=discord.ButtonStyle.danger, row=0, custom_id="music:panel:stop") stop_btn.callback = self._stop self.add_item(stop_btn) # Row 2: Open compact audio/actions menu actions_btn = discord.ui.Button(label="Audio Menu", emoji="📂", style=discord.ButtonStyle.secondary, row=1, custom_id="music:panel:actions") actions_btn.callback = self._audio_actions self.add_item(actions_btn) # Row 3: Quick Actions (requested Join / Play Music buttons) join_btn = discord.ui.Button(label="Join", emoji="🔊", style=discord.ButtonStyle.success, row=2, custom_id="music:panel:join") join_btn.callback = self._join self.add_item(join_btn) play_btn = discord.ui.Button(label="Play Music", emoji="🎵", style=discord.ButtonStyle.primary, row=2, custom_id="music:panel:play") play_btn.callback = self._play self.add_item(play_btn) search_btn = discord.ui.Button(label="Search", emoji="🔍", style=discord.ButtonStyle.secondary, row=2, custom_id="music:panel:search") search_btn.callback = self._search self.add_item(search_btn) queue_btn = discord.ui.Button(label="Queue", emoji=_button_emoji("spotifyqueueadd", "📜"), style=discord.ButtonStyle.primary, row=2, custom_id="music:panel:queue") queue_btn.callback = self._queue self.add_item(queue_btn) playlist_btn = discord.ui.Button(label="Playlist", emoji="📚", style=discord.ButtonStyle.secondary, row=3, custom_id="music:panel:playlist") playlist_btn.callback = self._playlist self.add_item(playlist_btn) save_queue_btn = discord.ui.Button(label="Save Queue", emoji="💾", style=discord.ButtonStyle.success, row=3, custom_id="music:panel:savequeue") save_queue_btn.callback = self._save_queue self.add_item(save_queue_btn) # Row 4: Queue Management clear_btn = discord.ui.Button(label="Clear", emoji=_button_emoji("trashcan", "🧹"), style=discord.ButtonStyle.secondary, row=3, custom_id="music:panel:clear") clear_btn.callback = self._clear_queue self.add_item(clear_btn) async def _build_refresh_embed(self) -> discord.Embed: """Build refresh embed.""" return await self.cog._music_panel_embed(self.guild_id) def _build_refresh_view(self) -> None: """Skip rebuilding view on auto-refresh to prevent 'unknown view' errors. The buttons are static and don't need rebuilding during auto-refresh. Rebuilding clears items and creates new button instances which breaks Discord's view caching and causes 'interaction referencing unknown view'. """ pass @safe_interaction async def _join(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) msg = await self.cog._join_member_voice(interaction) await safe_send(interaction, msg) await self._refresh_panel(interaction) @safe_interaction async def _leave(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) msg = await self.cog._leave_voice(interaction) await safe_send(interaction, msg) await self._refresh_panel(interaction) @safe_interaction async def _play(self, interaction: discord.Interaction) -> None: try: await interaction.response.send_modal(PlayModal(self.cog)) except Exception: await safe_send(interaction, "❌ Could not open play modal.") @safe_interaction async def _search(self, interaction: discord.Interaction) -> None: try: await interaction.response.send_modal(PlayModal(self.cog)) except Exception: await safe_send(interaction, "❌ Could not open search modal.") @safe_interaction async def _pause(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) msg = await self.cog.toggle_pause(interaction) await safe_send(interaction, msg) await self._refresh_panel(interaction) @safe_interaction async def _stop(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) msg = await self.cog.stop_music(interaction) await safe_send(interaction, msg) await self._refresh_panel(interaction) @safe_interaction async def _preview(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) msg = await self.cog.now_playing_preview(interaction) await safe_send(interaction, msg) @safe_interaction async def _skip(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) msg = await self.cog._skip_current(interaction) await safe_send(interaction, msg) await self._refresh_panel(interaction) @safe_interaction async def _previous(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) msg = await self.cog.play_previous(interaction) await safe_send(interaction, msg) await self._refresh_panel(interaction) @safe_interaction async def _queue(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) guild_id = interaction.guild.id if interaction.guild else 0 view = QueueView(self.cog, guild_id) embed = await view._build_embed() msg = await interaction.followup.send(embed=embed, view=view, ephemeral=True) view._message = msg await view.start_auto_refresh(msg) @safe_interaction async def _playlist(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) await self.cog.open_user_playlists_panel(interaction) @safe_interaction async def _save_queue(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) result = await self.cog.save_queue_to_user_playlist(interaction, "quicksave") await safe_send(interaction, result) @safe_interaction async def _vol_up(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) if not interaction.guild: return state = self.cog._guild_state(interaction.guild.id) msg = await self.cog._set_volume(interaction.guild, min(200, state.volume + 10)) await safe_send(interaction, msg) await self._refresh_panel(interaction) @safe_interaction async def _vol_down(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) if not interaction.guild: return state = self.cog._guild_state(interaction.guild.id) msg = await self.cog._set_volume(interaction.guild, max(0, state.volume - 10)) await safe_send(interaction, msg) await self._refresh_panel(interaction) @safe_interaction async def _mute(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) if not interaction.guild: return state = self.cog._guild_state(interaction.guild.id) target = 0 if state.volume > 0 else 100 msg = await self.cog._set_volume(interaction.guild, target) await safe_send(interaction, msg) await self._refresh_panel(interaction) @safe_interaction async def _clear_queue(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) if not interaction.guild: return self.cog.queues[interaction.guild.id] = [] player = interaction.guild.voice_client if wavelink and isinstance(player, wavelink.Player): player.queue.clear() await safe_send(interaction, "🧹 Queue cleared.") await self._refresh_panel(interaction) @safe_interaction async def _loop_mode(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) if not interaction.guild: return state = self.cog._guild_state(interaction.guild.id) modes = ["off", "track", "queue"] state.loop_mode = modes[(modes.index(state.loop_mode) + 1) % len(modes)] if state.loop_mode in modes else "off" await safe_send(interaction, f"🔁 Loop mode: **{state.loop_mode}**") await self._refresh_panel(interaction) @safe_interaction async def _shuffle_queue(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) if not interaction.guild: return queue = self.cog.queues.get(interaction.guild.id, []) if len(queue) < 2: await safe_send(interaction, embed=await idle_embed_for_guild( "Shuffle Idle", "Need at least 2 tracks in queue to shuffle.", "Add tracks from Play/Search, then retry.", guild=interaction.guild, bot=self.cog.bot, )) return random.shuffle(queue) player = interaction.guild.voice_client if wavelink and isinstance(player, wavelink.Player): await player.queue.shuffle() await safe_send(interaction, "🔀 Queue shuffled.") await self._refresh_panel(interaction) @safe_interaction async def _filters(self, interaction: discord.Interaction) -> None: """Open the filters panel as a NEW separate message (ephemeral = only visible to user).""" await safe_defer(interaction) guild_id = interaction.guild.id if interaction.guild else 0 # Create new filters panel view = FiltersPanelView(self.cog, guild_id) embed = await view._build_refresh_embed() # Send as ephemeral message (only visible to the user who clicked) msg = await interaction.followup.send(embed=embed, view=view, ephemeral=True) view._message = msg await view.start_auto_refresh(msg) @safe_interaction async def _audio_actions(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) guild_id = interaction.guild.id if interaction.guild else 0 view = AudioActionsView(self.cog, guild_id) embed = await self.cog._music_panel_embed(guild_id) embed.title = f"🎛️ Audio Actions • {embed.title}" msg = await interaction.followup.send(embed=embed, view=view, ephemeral=True) view._message = msg await view.start_auto_refresh(msg) @safe_interaction async def _247(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) msg = await self.cog.toggle_stay_247(interaction) await safe_send(interaction, msg) await self._refresh_panel(interaction) @safe_interaction async def _refresh(self, interaction: discord.Interaction) -> None: await safe_defer(interaction) await self._refresh_panel(interaction) await safe_send(interaction, "🔄 Refreshed!") async def _refresh_panel(self, interaction: discord.Interaction) -> None: """Refresh the panel message.""" try: if not interaction.message: return guild_id = interaction.guild.id if interaction.guild else None embed = await self.cog._music_panel_embed(guild_id) self._build_view() await interaction.message.edit(embed=embed, view=self) except: pass # ═══════════════════════════════════════════════════════════════════════════════ # AUDIO ACTIONS VIEW - controls moved from main panel # ═══════════════════════════════════════════════════════════════════════════════ class AudioActionsView(discord.ui.View, AutoRefreshMixin): _refresh_interval: int = 5 def __init__(self, cog: "Media", guild_id: int) -> None: discord.ui.View.__init__(self, timeout=None) AutoRefreshMixin.__init__(self) self.cog = cog self.guild_id = guild_id self._build() def _build(self) -> None: self.clear_items() for label, emoji, cid, cb in [ ("Vol +", "🔊", "audio:volup", self._vol_up), ("Vol -", "🔉", "audio:voldown", self._vol_down), ("Mute", "🔇", "audio:mute", self._mute), ("24/7", "♾️", "audio:247", self._247), ("Shuffle", "🔀", "audio:shuffle", self._shuffle), ("Loop", "🔁", "audio:loop", self._loop), ("Filters", "🎛️", "audio:filters", self._filters), ]: btn = discord.ui.Button(label=label, emoji=emoji, style=discord.ButtonStyle.secondary, custom_id=cid) btn.callback = cb self.add_item(btn) async def _build_refresh_embed(self) -> discord.Embed: return await self.cog._music_panel_embed(self.guild_id) @safe_interaction async def _vol_up(self, interaction: discord.Interaction) -> None: await MusicPanelView._vol_up(self, interaction) # type: ignore[misc] @safe_interaction async def _vol_down(self, interaction: discord.Interaction) -> None: await MusicPanelView._vol_down(self, interaction) # type: ignore[misc] @safe_interaction async def _mute(self, interaction: discord.Interaction) -> None: await MusicPanelView._mute(self, interaction) # type: ignore[misc] @safe_interaction async def _247(self, interaction: discord.Interaction) -> None: await MusicPanelView._247(self, interaction) # type: ignore[misc] @safe_interaction async def _shuffle(self, interaction: discord.Interaction) -> None: await MusicPanelView._shuffle_queue(self, interaction) # type: ignore[misc] @safe_interaction async def _loop(self, interaction: discord.Interaction) -> None: await MusicPanelView._loop_mode(self, interaction) # type: ignore[misc] @safe_interaction async def _filters(self, interaction: discord.Interaction) -> None: await MusicPanelView._filters(self, interaction) # type: ignore[misc] # ═══════════════════════════════════════════════════════════════════════════════ # PLAY MODAL - نافذة إضافة مقطع # ═══════════════════════════════════════════════════════════════════════════════ class SearchPreviewActionView(discord.ui.View): def __init__(self, cog: "Media", query: str) -> None: super().__init__(timeout=None) self.cog = cog self.query = query @discord.ui.button(label="Play", emoji="▶️", style=discord.ButtonStyle.success, custom_id="music:search:play") async def play_now(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: await safe_defer(interaction) result = await self.cog.play_from_query(interaction, self.query) await safe_send(interaction, result) @discord.ui.button(label="Add to Queue", emoji="➕", style=discord.ButtonStyle.primary, custom_id="music:search:queue") async def add_queue(self, interaction: discord.Interaction, _: discord.ui.Button) -> None: await safe_defer(interaction) result = await self.cog.enqueue_from_query(interaction, self.query) await safe_send(interaction, result) class PlayModal(discord.ui.Modal, title="꧁⫷ 𝕄𝕦𝕤𝕚𝕔 𝕊𝕖𝕒𝕣𝕔𝕙 ⫸꧂"): query = discord.ui.TextInput( label="Enter song name or YouTube URL", placeholder="lofi hip hop / https://youtube.com/watch?v=...", max_length=200 ) def __init__(self, cog: "Media") -> None: super().__init__(timeout=None) self.cog = cog async def on_submit(self, interaction: discord.Interaction) -> None: if not interaction.guild: await interaction.response.send_message("Server only.", ephemeral=True) return query = str(self.query.value).strip() if not query: await interaction.response.send_message("Type a song name or URL.", ephemeral=True) return await interaction.response.defer(ephemeral=True, thinking=True) result = await self.cog.play_now_from_query(interaction, query) await interaction.followup.send(result, ephemeral=True) # ═══════════════════════════════════════════════════════════════════════════════ # QUEUE VIEW - عرض الطابور مع التحديث التلقائي # ═══════════════════════════════════════════════════════════════════════════════ class QueueView(discord.ui.View, AutoRefreshMixin): """Paginated queue view with auto-refresh.""" def __init__(self, cog: "Media", guild_id: int, page: int = 0) -> None: discord.ui.View.__init__(self, timeout=300) AutoRefreshMixin.__init__(self) self.cog = cog self.guild_id = guild_id self.page = page self._build_view() def _build_view(self) -> None: """Build the queue view.""" self.clear_items() queue = self.cog.queues.get(self.guild_id, []) # Add track select if queue has tracks if queue: self.add_item(QueueReorderSelect(self.cog, self.guild_id, queue, self.page)) # Navigation buttons prev_emoji = _emoji('animatedarroworange', '⬅️') prev_btn = discord.ui.Button( label="Previous", emoji=prev_emoji, style=discord.ButtonStyle.secondary, row=1, custom_id="queue:prev" ) prev_btn.callback = self._prev_page self.add_item(prev_btn) next_emoji = _emoji('animatedarrowgreen', '➡️') next_btn = discord.ui.Button( label="Next", emoji=next_emoji, style=discord.ButtonStyle.secondary, row=1, custom_id="queue:next" ) next_btn.callback = self._next_page self.add_item(next_btn) shuffle_emoji = _emoji('emojidance', '🔀') shuffle_btn = discord.ui.Button( label="Shuffle", emoji=shuffle_emoji, style=discord.ButtonStyle.primary, row=1, custom_id="queue:shuffle" ) shuffle_btn.callback = self._shuffle self.add_item(shuffle_btn) clear_emoji = _emoji('trashcan', '🗑️') clear_btn = discord.ui.Button( label="Clear", emoji=clear_emoji, style=discord.ButtonStyle.danger, row=1, custom_id="queue:clear" ) clear_btn.callback = self._clear self.add_item(clear_btn) async def _build_refresh_embed(self) -> discord.Embed: return await self._build_embed() def _build_refresh_view(self) -> None: self._build_view() async def _build_embed(self) -> discord.Embed: queue = self.cog.queues.get(self.guild_id, []) current = self.cog.now_playing.get(self.guild_id) embed = discord.Embed( title="⛩️ 『 𝔓𝔩𝔞𝔶𝔩𝔦𝔰𝔱 𝔐𝔢𝔫𝔲 』", description="🏮 〣 Queue Control 〣 ㊙️", color=NEON_CYAN ) if current: embed.add_field( name=f"{_emoji('spotify', '🎵')} Now Playing", value=f"**[{current.title[:60]}]({current.webpage_url})**\n⏱️ {current.format_duration()} | 👤 <@{current.requester_id}>", inline=False ) if queue: per_page = 10 start = self.page * per_page page_tracks = queue[start:start + per_page] lines = [f"`{i+start+1}.` **{t.title[:45]}** `{t.format_duration()}`" for i, t in enumerate(page_tracks)] total_pages = (len(queue) + per_page - 1) // per_page embed.add_field( name=f"📋 Up Next ({len(queue)} tracks)", value="\n".join(lines) + f"\n{fancy_divider('music')}", inline=False ) embed.set_footer(text=f"〣 Page {self.page + 1}/{max(1, total_pages)} | ▬▬🔘▬▬ refresh every 3s") else: embed.add_field( name="📋 Up Next", value=idle_text("Queue is empty.", "Add tracks with `/music play`.\n" + fancy_divider('music')), inline=False ) guild = self.cog.bot.get_guild(self.guild_id) if hasattr(self.cog, "bot") else None if guild: await add_banner_to_embed(embed, guild, self.cog.bot) return embed @safe_interaction async def _prev_page(self, interaction: discord.Interaction) -> None: if self.page > 0: self.page -= 1 self._build_view() await interaction.response.edit_message(embed=await self._build_embed(), view=self) @safe_interaction async def _next_page(self, interaction: discord.Interaction) -> None: queue = self.cog.queues.get(self.guild_id, []) if (self.page + 1) * 10 < len(queue): self.page += 1 self._build_view() await interaction.response.edit_message(embed=await self._build_embed(), view=self) @safe_interaction async def _shuffle(self, interaction: discord.Interaction) -> None: queue = self.cog.queues.get(self.guild_id, []) if len(queue) > 1: random.shuffle(queue) guild = interaction.guild if guild and wavelink: player = guild.voice_client if isinstance(player, wavelink.Player): try: await player.queue.shuffle() except: pass self._build_view() await interaction.response.edit_message(embed=await self._build_embed(), view=self) @safe_interaction async def _clear(self, interaction: discord.Interaction) -> None: self.cog.queues[self.guild_id] = [] guild = interaction.guild if guild and wavelink: player = guild.voice_client if isinstance(player, wavelink.Player): player.queue.clear() self._build_view() await interaction.response.edit_message(embed=await self._build_embed(), view=self) # ═══════════════════════════════════════════════════════════════════════════════ # QUEUE REORDER SELECT - إعادة ترتيب الطابور # ═══════════════════════════════════════════════════════════════════════════════ class QueueReorderSelect(discord.ui.Select): """Select menu to pick a track to move.""" def __init__(self, cog: "Media", guild_id: int, tracks: list, page: int = 0) -> None: self.cog = cog self.guild_id = guild_id self.tracks = tracks self.page = page self.page_size = 10 options = [] number_emojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"] start = page * self.page_size for idx, track in enumerate(tracks[start:start + self.page_size]): pos = start + idx + 1 title = track.title[:55] + "..." if len(track.title) > 55 else track.title options.append( discord.SelectOption( label=f"[{pos}] {title[:90]}", value=str(idx), description=f"[{pos}] {title[:60]} - [{track.format_duration()}]"[:100], emoji=number_emojis[idx] if idx < len(number_emojis) else "🎵" ) ) super().__init__( placeholder="🏮 选择您的曲目 (Select Your Track)", min_values=1, max_values=1, options=options, custom_id=f"queue_reorder_select:{guild_id}:{page}" ) @safe_interaction async def callback(self, interaction: discord.Interaction) -> None: await interaction.response.defer(ephemeral=True) if not self.values: return data_values = interaction.data.get("values", []) if isinstance(interaction.data, dict) else [] if not data_values: return selected_index = (self.page * 10) + int(data_values[0]) global_index = selected_index selected_pos = global_index + 1 queue = self.cog.queues.get(self.guild_id, []) if not queue or global_index >= len(queue): await interaction.followup.send("❌ Track not found.", ephemeral=True) return track = queue[global_index] track_title = track.title[:30] + "..." if len(track.title) > 30 else track.title await interaction.followup.send( f"📍 **#{selected_pos}** {track_title}\n👉 Select new position:", view=QueueMovePositionView(self.cog, self.guild_id, selected_pos, len(queue)), ephemeral=True ) # ═══════════════════════════════════════════════════════════════════════════════ # QUEUE MOVE POSITION VIEW - عرض نقل المقطع # ═══════════════════════════════════════════════════════════════════════════════ class QueueMovePositionView(discord.ui.View): """View for selecting new position or playing track immediately.""" def __init__(self, cog: "Media", guild_id: int, from_pos: int, queue_len: int) -> None: super().__init__(timeout=60) self.cog = cog self.guild_id = guild_id self.from_pos = from_pos self.queue_len = queue_len self._build_view() def _build_view(self) -> None: self.clear_items() # Play Now button play_now_btn = discord.ui.Button( label="▶️ Play Now", style=discord.ButtonStyle.success, row=0 ) play_now_btn.callback = self._play_now self.add_item(play_now_btn) # To Start button to_start_btn = discord.ui.Button( label="⏫ To Start", style=discord.ButtonStyle.primary, row=0 ) to_start_btn.callback = self._to_start self.add_item(to_start_btn) # To End button to_end_btn = discord.ui.Button( label="⏬ To End", style=discord.ButtonStyle.primary, row=0 ) to_end_btn.callback = self._to_end self.add_item(to_end_btn) # Cancel button cancel_btn = discord.ui.Button( label="❌ Cancel", style=discord.ButtonStyle.secondary, row=1 ) cancel_btn.callback = self._cancel self.add_item(cancel_btn) @safe_interaction async def _play_now(self, interaction: discord.Interaction) -> None: """Play selected track immediately.""" await interaction.response.defer(ephemeral=True) queue = self.cog.queues.get(self.guild_id, []) if not queue or self.from_pos > len(queue): await interaction.followup.send("❌ Track not found.", ephemeral=True) return # Move selected track to the front so it becomes next in line. track = queue.pop(self.from_pos - 1) queue.insert(0, track) guild = interaction.guild if guild and wavelink: player = guild.voice_client if isinstance(player, wavelink.Player): try: wl_queue = list(player.queue) selected_idx = self.from_pos - 1 if 0 <= selected_idx < len(wl_queue): wl_track = wl_queue.pop(selected_idx) wl_queue.insert(0, wl_track) player.queue.clear() for queued_track in wl_queue: await player.queue.put_wait(queued_track) # Do not use sequential skip; stop current track immediately # so the selected queue head plays next. await player.stop() except: pass await interaction.followup.send(f"▶️ Playing **{track.title[:40]}** now!", ephemeral=True) @safe_interaction async def _to_start(self, interaction: discord.Interaction) -> None: """Move track to start of queue.""" await interaction.response.defer(ephemeral=True) queue = self.cog.queues.get(self.guild_id, []) if not queue or self.from_pos > len(queue): await interaction.followup.send("❌ Track not found.", ephemeral=True) return if self.from_pos == 1: await interaction.followup.send("⚠️ Track is already at start.", ephemeral=True) return track = queue.pop(self.from_pos - 1) queue.insert(0, track) # Sync with wavelink queue guild = interaction.guild if guild and wavelink: player = guild.voice_client if isinstance(player, wavelink.Player): try: wl_queue = list(player.queue) if self.from_pos - 1 < len(wl_queue): wl_track = wl_queue.pop(self.from_pos - 1) wl_queue.insert(0, wl_track) player.queue.clear() for t in wl_queue: await player.queue.put_wait(t) except: pass await interaction.followup.send(f"⏫ Moved **{track.title[:40]}** to start!", ephemeral=True) @safe_interaction async def _to_end(self, interaction: discord.Interaction) -> None: """Move track to end of queue.""" await interaction.response.defer(ephemeral=True) queue = self.cog.queues.get(self.guild_id, []) if not queue or self.from_pos > len(queue): await interaction.followup.send("❌ Track not found.", ephemeral=True) return if self.from_pos == len(queue): await interaction.followup.send("⚠️ Track is already at end.", ephemeral=True) return track = queue.pop(self.from_pos - 1) queue.append(track) # Sync with wavelink queue guild = interaction.guild if guild and wavelink: player = guild.voice_client if isinstance(player, wavelink.Player): try: wl_queue = list(player.queue) if self.from_pos - 1 < len(wl_queue): wl_track = wl_queue.pop(self.from_pos - 1) wl_queue.append(wl_track) player.queue.clear() for t in wl_queue: await player.queue.put_wait(t) except: pass await interaction.followup.send(f"⏬ Moved **{track.title[:40]}** to end!", ephemeral=True) @safe_interaction async def _cancel(self, interaction: discord.Interaction) -> None: """Cancel the move.""" await interaction.response.defer() try: await interaction.message.delete() except: pass # ═══════════════════════════════════════════════════════════════════════════════ # ALIASES FOR COMPATIBILITY # ═══════════════════════════════════════════════════════════════════════════════ # Alias for backward compatibility FiltersView = FiltersPanelView