diff options
| author | salmonik <raisersoft@proton.me> | 2025-09-26 09:39:11 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2025-09-26 09:39:11 +0200 |
| commit | 87a55bf43e61af57c6c88f1be57e2e0f2036d869 (patch) | |
| tree | a8eee32d1f94ca1c5c40bcd4b9b570eeb4bb0f79 /cogs/music/play_utils.py | |
| parent | add requirements.txt (diff) | |
| download | senator-sperma-87a55bf43e61af57c6c88f1be57e2e0f2036d869.tar.gz senator-sperma-87a55bf43e61af57c6c88f1be57e2e0f2036d869.zip | |
add local music playing
Diffstat (limited to 'cogs/music/play_utils.py')
| -rw-r--r-- | cogs/music/play_utils.py | 141 |
1 files changed, 131 insertions, 10 deletions
diff --git a/cogs/music/play_utils.py b/cogs/music/play_utils.py index b837ba0..bc20d7b 100644 --- a/cogs/music/play_utils.py +++ b/cogs/music/play_utils.py | |||
| @@ -1,4 +1,7 @@ | |||
| 1 | import json | ||
| 1 | import logging | 2 | import logging |
| 3 | |||
| 4 | import mutagen | ||
| 2 | from youtubesearchpython import VideosSearch | 5 | from youtubesearchpython import VideosSearch |
| 3 | import yt_dlp | 6 | import yt_dlp |
| 4 | import discord | 7 | import discord |
| @@ -7,11 +10,29 @@ import os | |||
| 7 | 10 | ||
| 8 | logger = logging.getLogger(__name__) | 11 | logger = logging.getLogger(__name__) |
| 9 | 12 | ||
| 13 | class Utils: | ||
| 14 | @staticmethod | ||
| 15 | def levenshtein_distance(s1, s2): | ||
| 16 | if len(s1) > len(s2): | ||
| 17 | s1, s2 = s2, s1 | ||
| 18 | distances = range(len(s1) + 1) | ||
| 19 | for i2, c2 in enumerate(s2): | ||
| 20 | distances_ = [i2+1] | ||
| 21 | for i1, c1 in enumerate(s1): | ||
| 22 | if c1 == c2: | ||
| 23 | distances_.append(distances[i1]) | ||
| 24 | else: | ||
| 25 | distances_.append( | ||
| 26 | min(distances[i1], distances[i1 + 1], distances_[-1]) + 1) | ||
| 27 | distances = distances_ | ||
| 28 | return distances[-1] | ||
| 29 | |||
| 10 | class PlayUtils: | 30 | class PlayUtils: |
| 11 | def __init__(self, bot): | 31 | def __init__(self, bot): |
| 12 | self.bot = bot | 32 | self.bot = bot |
| 13 | 33 | ||
| 14 | async def download(self, ctx, url): | 34 | @staticmethod |
| 35 | async def download(ctx, url): | ||
| 15 | 36 | ||
| 16 | # Create temp directory if it doesn't exist | 37 | # Create temp directory if it doesn't exist |
| 17 | temp_dir = os.path.join('cogs', 'temp', 'music') | 38 | temp_dir = os.path.join('cogs', 'temp', 'music') |
| @@ -42,7 +63,8 @@ class PlayUtils: | |||
| 42 | logger.error(f"Error downloading video: {e}") | 63 | logger.error(f"Error downloading video: {e}") |
| 43 | await ctx.respond("eror pobirajync wideło", ephemeral=True) | 64 | await ctx.respond("eror pobirajync wideło", ephemeral=True) |
| 44 | 65 | ||
| 45 | async def get_url(self, ctx, query): | 66 | @staticmethod |
| 67 | async def get_url(ctx, query): | ||
| 46 | search = VideosSearch(query, limit=1) | 68 | search = VideosSearch(query, limit=1) |
| 47 | if search.result()["result"]: | 69 | if search.result()["result"]: |
| 48 | title = search.result()["result"][0]["title"] | 70 | title = search.result()["result"][0]["title"] |
| @@ -54,7 +76,8 @@ class PlayUtils: | |||
| 54 | await ctx.respond("nic ni ma", ephemeral=True) | 76 | await ctx.respond("nic ni ma", ephemeral=True) |
| 55 | return None | 77 | return None |
| 56 | 78 | ||
| 57 | async def compress(self, ctx, filename): | 79 | @staticmethod |
| 80 | async def compress(ctx, filename): | ||
| 58 | if not filename: | 81 | if not filename: |
| 59 | await ctx.respond("dzie wideło", ephemeral=True) | 82 | await ctx.respond("dzie wideło", ephemeral=True) |
| 60 | return | 83 | return |
| @@ -76,12 +99,29 @@ class PlayUtils: | |||
| 76 | logger.error(f"Error compressing file: {e}") | 99 | logger.error(f"Error compressing file: {e}") |
| 77 | await ctx.respond("eror kompresujync pilk", ephemeral=True) | 100 | await ctx.respond("eror kompresujync pilk", ephemeral=True) |
| 78 | 101 | ||
| 79 | async def play(self, ctx, filename, title): | 102 | @staticmethod |
| 103 | async def play(ctx, filepath): | ||
| 80 | # Check if user is in a voice channel | 104 | # Check if user is in a voice channel |
| 81 | if not ctx.author.voice or not ctx.author.voice.channel: | 105 | if not ctx.author.voice or not ctx.author.voice.channel: |
| 82 | await ctx.respond("Musisz być na kanale głosowym, żebym mógł dołączyć!", ephemeral=True) | 106 | await ctx.respond("Musisz być na kanale głosowym, żebym mógł dołączyć!", ephemeral=True) |
| 83 | return | 107 | return |
| 84 | 108 | ||
| 109 | logger.info(f"Attempting to play: {filepath}") | ||
| 110 | |||
| 111 | # Get absolute path and verify file exists | ||
| 112 | if not os.path.isabs(filepath): | ||
| 113 | filepath = os.path.abspath(filepath) | ||
| 114 | logger.debug(f"Converted to absolute path: {filepath}") | ||
| 115 | |||
| 116 | if not os.path.exists(filepath): | ||
| 117 | error_msg = f"File not found: {filepath}" | ||
| 118 | logger.error(error_msg) | ||
| 119 | await ctx.respond("Nie znaleziono pliku do odtworzenia", ephemeral=True) | ||
| 120 | return | ||
| 121 | |||
| 122 | filename = os.path.basename(filepath) | ||
| 123 | logger.info(f"Playing file: {filename} (from: {filepath})") | ||
| 124 | |||
| 85 | # Get or create voice client | 125 | # Get or create voice client |
| 86 | vc = ctx.voice_client | 126 | vc = ctx.voice_client |
| 87 | target_channel = ctx.author.voice.channel | 127 | target_channel = ctx.author.voice.channel |
| @@ -89,29 +129,110 @@ class PlayUtils: | |||
| 89 | try: | 129 | try: |
| 90 | # Connect to voice channel if not connected | 130 | # Connect to voice channel if not connected |
| 91 | if vc is None: | 131 | if vc is None: |
| 132 | logger.info(f"Connecting to voice channel: {target_channel.name}") | ||
| 92 | vc = await target_channel.connect() | 133 | vc = await target_channel.connect() |
| 93 | logger.info(f"Connected to voice channel: {target_channel.name}") | 134 | logger.info(f"Connected to voice channel: {target_channel.name}") |
| 94 | # Move to user's channel if in a different one | 135 | # Move to user's channel if in a different one |
| 95 | elif vc.channel != target_channel: | 136 | elif vc.channel != target_channel: |
| 137 | logger.info(f"Moving to voice channel: {target_channel.name}") | ||
| 96 | await vc.move_to(target_channel) | 138 | await vc.move_to(target_channel) |
| 97 | logger.info(f"Moved to voice channel: {target_channel.name}") | 139 | logger.info(f"Moved to voice channel: {target_channel.name}") |
| 98 | 140 | ||
| 99 | if not filename or not os.path.exists(filename): | ||
| 100 | await ctx.respond("Nie znaleziono pliku do odtworzenia", ephemeral=True) | ||
| 101 | logger.warning(f"No file to play, filename: {filename}") | ||
| 102 | return | ||
| 103 | |||
| 104 | # Stop any currently playing audio | 141 | # Stop any currently playing audio |
| 105 | if vc.is_playing() or vc.is_paused(): | 142 | if vc.is_playing() or vc.is_paused(): |
| 106 | vc.stop() | 143 | vc.stop() |
| 107 | 144 | ||
| 108 | # Play the audio | 145 | # Play the audio |
| 109 | source = discord.FFmpegPCMAudio(source=filename) | 146 | source = discord.FFmpegPCMAudio(source=filepath) |
| 110 | vc.play(source) | 147 | vc.play(source) |
| 111 | 148 | ||
| 149 | try: | ||
| 150 | metadata = mutagen.File(filepath) | ||
| 151 | title = metadata.get('title', None) | ||
| 152 | logger.debug(f"Read metadata - title: {title}") | ||
| 153 | except Exception as e: | ||
| 154 | logger.warning(f"Could not read metadata from {filepath}: {str(e)}") | ||
| 155 | title = None | ||
| 156 | |||
| 157 | if title is None: | ||
| 158 | title = os.path.splitext(os.path.basename(filepath))[0] | ||
| 159 | logger.debug(f"Using filename as title: {title}") | ||
| 160 | |||
| 112 | logger.info(f"Playing file: {title}") | 161 | logger.info(f"Playing file: {title}") |
| 113 | await ctx.respond(f"grom: {title}") | 162 | await ctx.respond(f"grom: {title}") |
| 114 | 163 | ||
| 115 | except Exception as e: | 164 | except Exception as e: |
| 116 | logger.error(f"Error in play: {str(e)}", exc_info=True) | 165 | logger.error(f"Error in play: {str(e)}", exc_info=True) |
| 117 | await ctx.respond(f"eror: {str(e)}", ephemeral=True) | 166 | await ctx.respond(f"eror: {str(e)}", ephemeral=True) |
| 167 | |||
| 168 | # -- LOCAL PLAY -- | ||
| 169 | |||
| 170 | @staticmethod | ||
| 171 | async def search_local(query): | ||
| 172 | logger.info(f"Starting local search for query: '{query}'") | ||
| 173 | try: | ||
| 174 | with open('settings.json') as f: | ||
| 175 | data = json.load(f) | ||
| 176 | paths = data.get('audio_paths', []) | ||
| 177 | logger.info(f"Searching in paths: {paths}") | ||
| 178 | candidates = [] | ||
| 179 | files_checked = 0 | ||
| 180 | |||
| 181 | for path in paths: | ||
| 182 | if not os.path.exists(path): | ||
| 183 | logger.warning(f"Path does not exist: {path}") | ||
| 184 | continue | ||
| 185 | |||
| 186 | logger.debug(f"Searching in directory: {path}") | ||
| 187 | # Walk through all subdirectories | ||
| 188 | for root, dirs, files in os.walk(path): | ||
| 189 | logger.debug(f"Scanning directory: {root} (contains {len(files)} files)") | ||
| 190 | for file in files: | ||
| 191 | files_checked += 1 | ||
| 192 | # Check if file has a valid extension | ||
| 193 | if file.lower().endswith(('.mp4', '.mp3', '.wav', '.opus', '.m4a')): | ||
| 194 | # Get relative path from the search path | ||
| 195 | rel_path = os.path.relpath(root, path) | ||
| 196 | |||
| 197 | # Get filename without extension | ||
| 198 | filename_without_ext = os.path.splitext(file)[0] | ||
| 199 | |||
| 200 | # Create searchable string with both filename and relative path | ||
| 201 | searchable_name = f"{os.path.join(rel_path, filename_without_ext)}".lower() | ||
| 202 | |||
| 203 | # Calculate Levenshtein distance for the searchable string | ||
| 204 | distance = Utils.levenshtein_distance(query.lower(), searchable_name) | ||
| 205 | |||
| 206 | # Also check just the filename for better matching of partial queries | ||
| 207 | filename_distance = Utils.levenshtein_distance(query.lower(), filename_without_ext.lower()) | ||
| 208 | |||
| 209 | # Use the better (lower) distance score | ||
| 210 | final_distance = min(distance, filename_distance) | ||
| 211 | |||
| 212 | # Add to candidates with distance and full path | ||
| 213 | file_path = os.path.abspath(os.path.join(root, file)) | ||
| 214 | candidates.append((final_distance, file_path)) | ||
| 215 | logger.debug(f"Found match: {file_path} (distance: {distance})") | ||
| 216 | |||
| 217 | logger.info(f"Search complete. Checked {files_checked} files, found {len(candidates)} audio files.") | ||
| 218 | |||
| 219 | if not candidates: | ||
| 220 | logger.warning("No matching audio files found.") | ||
| 221 | return [] | ||
| 222 | |||
| 223 | # Sort by distance and get top 5 matches | ||
| 224 | sorted_candidates = sorted(candidates, key=lambda x: x[0]) | ||
| 225 | top_matches = sorted_candidates[:5] | ||
| 226 | |||
| 227 | logger.info(f"Top {len(top_matches)} matches:") | ||
| 228 | for i, (distance, path) in enumerate(top_matches, 1): | ||
| 229 | logger.info(f" {i}. {path} (distance: {distance})") | ||
| 230 | |||
| 231 | return [c[1] for c in top_matches] | ||
| 232 | |||
| 233 | except Exception as e: | ||
| 234 | logger.error(f"Error during local search: {str(e)}", exc_info=True) | ||
| 235 | return [] | ||
| 236 | |||
| 237 | |||
| 238 | |||
