spci-sonic-pulse 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
spci/__init__.py ADDED
File without changes
spci/getmusic.py ADDED
@@ -0,0 +1,54 @@
1
+ import yt_dlp
2
+ import os
3
+
4
+ def get_music(query):
5
+ """
6
+ Searches YouTube and filters results to include only song-length videos.
7
+ """
8
+ # Define your maximum duration in seconds (e.g., 6 minutes)
9
+ MAX_DURATION = 360
10
+
11
+ ydl_opts = {
12
+ 'format': 'bestaudio/best',
13
+ 'quiet': True,
14
+ 'no_warnings': True,
15
+ 'extract_flat': True,
16
+ 'nocheckcertificate': True,
17
+ }
18
+
19
+ # We add "audio" and "song" to the query for better results
20
+ search_query = f"ytsearch15:{query} song audio"
21
+
22
+ songs = []
23
+ try:
24
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
25
+ result = ydl.extract_info(search_query, download=False)
26
+
27
+ if 'entries' in result:
28
+ for entry in result['entries']:
29
+ duration_sec = entry.get('duration')
30
+
31
+ # FILTER: Skip videos that are too long or have no duration info
32
+ if not duration_sec or duration_sec > MAX_DURATION:
33
+ continue
34
+
35
+ duration_str = f"{int(duration_sec // 60)}:{int(duration_sec % 60):02d}"
36
+
37
+ songs.append({
38
+ 'title': entry.get('title'),
39
+ 'videoId': entry.get('id'),
40
+ 'artists': entry.get('uploader') or "Unknown",
41
+ 'album': "YouTube",
42
+ 'duration': duration_str
43
+ })
44
+ except Exception as e:
45
+ print(f"\n[bold red][!] Search Error:[/bold red] {e}")
46
+
47
+ return songs
48
+
49
+ if __name__ == "__main__":
50
+ query = input("Search YouTube: ")
51
+ results = get_music(query)
52
+ if results:
53
+ for i, song in enumerate(results, start=1):
54
+ print(f"{i}. {song['title']} by {song['artists']} - {song['duration']}")
spci/mp.py ADDED
@@ -0,0 +1,508 @@
1
+ import os
2
+ import platform
3
+ import subprocess
4
+ import time
5
+ import sys
6
+ import shutil
7
+ import typer
8
+ import requests
9
+ import yt_dlp
10
+ import zipfile
11
+ import io
12
+ import random
13
+ from rich.console import Console
14
+ from rich.table import Table
15
+ from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
16
+ from rich.layout import Layout
17
+ from rich.panel import Panel
18
+ from rich.live import Live
19
+ from rich.align import Align
20
+ from rich import box
21
+ import math
22
+ import random
23
+ from rich.panel import Panel
24
+
25
+ # External project modules
26
+ from .getmusic import get_music
27
+ from tinydb import TinyDB, Query
28
+
29
+ __version__ = "2.0.0"
30
+
31
+ console = Console()
32
+ app = typer.Typer()
33
+
34
+ HISTORY_FILE = "play_history.txt"
35
+
36
+ # --- CONFIGURATION & PATHS ---
37
+ # Storing everything in a hidden folder in the User's home directory
38
+ APP_DIR = os.path.join(os.path.expanduser("~"), ".spci")
39
+ BIN_DIR = os.path.join(APP_DIR, "bin")
40
+ FAV_DIR = os.path.join(APP_DIR, "fav_audio") # Store actual .mp3 files here
41
+ FAV_DB_PATH = os.path.join(APP_DIR, "favorites.json") # NoSQL Metadata
42
+
43
+ # Windows Binary Paths
44
+ FFPLAY_PATH = os.path.join(BIN_DIR, "ffplay.exe")
45
+ FFMPEG_PATH = os.path.join(BIN_DIR, "ffmpeg.exe")
46
+ FFPROBE_PATH = os.path.join(BIN_DIR, "ffprobe.exe")
47
+
48
+ # Initialize directories
49
+ os.makedirs(BIN_DIR, exist_ok=True)
50
+ os.makedirs(FAV_DIR, exist_ok=True)
51
+
52
+ # Initialize NoSQL Database
53
+ db = TinyDB(FAV_DB_PATH)
54
+ fav_table = db.table('favorites')
55
+
56
+ # --- UI COMPONENTS ---
57
+
58
+ def make_layout() -> Layout:
59
+ """Creates a structured grid for the CLI interface."""
60
+ layout = Layout(name="root")
61
+ layout.split(
62
+ Layout(name="header", size=3),
63
+ Layout(name="main", ratio=1),
64
+ Layout(name="footer", size=7)
65
+ )
66
+ layout["main"].split_row(
67
+ Layout(name="left", ratio=2),
68
+ Layout(name="right", ratio=1),
69
+ )
70
+ return layout
71
+
72
+ def get_header():
73
+ return Panel(
74
+ Align.center(f"[bold cyan]SPCI SONIC PULSE[/bold cyan] v{__version__} | [bold yellow]Play once, play again & again[/bold yellow]"),
75
+ box=box.ROUNDED,
76
+ style="white on black"
77
+ )
78
+
79
+
80
+ def get_now_playing_panel(title, artist, is_offline=False, t=0.0):
81
+ """
82
+ Elegant ribbon visualizer:
83
+ - Adapts to terminal size (console.size)
84
+ - Smooth sine backbone + per-column micro-noise
85
+ - Soft falloff with graded glyphs and color shift
86
+ """
87
+ # Terminal-aware sizing (keeps left panel compact)
88
+ term = console.size
89
+ viz_width = max(24, min(48, term.width // 3))
90
+ viz_height = max(8, min(16, term.height // 4))
91
+
92
+ cx = viz_width // 2
93
+ cy = viz_height // 2
94
+
95
+ # Glyph ramp from faint -> bold
96
+ glyphs = [" ", "·", "•", "●", "█"]
97
+ colors = ["dim", "red", "cyan", "indigo", "magenta", "green", "yellow", "orange", "bright_white"]
98
+
99
+ # Parameters that control elegance
100
+ base_freq = 0.9 + (viz_width / 80) # spatial frequency
101
+ speed = 1 # temporal speed
102
+ amplitude = (viz_height / 2.5) # vertical swing
103
+ smoothness = 1.6 # how soft the falloff is
104
+
105
+ # Build grid rows (top->bottom)
106
+ grid_rows = []
107
+ for y in range(viz_height):
108
+ row = []
109
+ for x in range(viz_width):
110
+ # backbone: smooth sine across x, offset by time and small noise
111
+ backbone = math.sin((x / viz_width) * base_freq * 2 * math.pi + t * speed)
112
+ micro = math.sin((x * 0.7 + y * 0.4) * 0.4 + t * 1.7) * 0.15
113
+ y_center = cy + (backbone + micro) * amplitude
114
+
115
+ # distance from ribbon centerline for this column
116
+ dist = abs(y - y_center)
117
+
118
+ # normalized intensity (1 at center, decays to 0)
119
+ intensity = max(0.0, 1.0 - (dist / smoothness))
120
+ idx = int(intensity * (len(glyphs) - 1))
121
+
122
+ # subtle phase-based color shift across x
123
+ color_shift = int(((math.sin(t * 0.6 + x * 0.12) + 1) / 2) * (len(colors) - 1))
124
+ color_idx = min(len(colors) - 1, max(0, idx + color_shift - 1))
125
+
126
+ ch = glyphs[idx]
127
+ color = colors[color_idx]
128
+ # keep markup lean
129
+ row.append(f"[{color}]{ch}[/{color}]")
130
+ grid_rows.append("".join(row))
131
+
132
+ vis = "\n".join(grid_rows)
133
+ source_tag = (
134
+ "[bold red]● OFFLINE (LOCAL)[/bold red]" if is_offline
135
+ else "[bold green]● STREAMING (YOUTUBE)[/bold green]"
136
+ )
137
+
138
+ content = f"""
139
+ [bold white]TITLE :[/bold white] [yellow]{title}[/yellow]
140
+ [bold white]ARTIST:[/bold white] [cyan]{artist}[/cyan]
141
+ [bold white]STATUS:[/bold white] {source_tag}
142
+ {vis}
143
+ """.rstrip()
144
+
145
+ return Panel(Align.center(content), title="[bold green]NOW PLAYING[/bold green]", border_style="green")
146
+
147
+
148
+
149
+ def get_controls_panel():
150
+ return Panel(
151
+ Align.center("[bold white]ACTIVE SESSION[/bold white]\n[dim]Press Ctrl+C to stop playback and return to terminal[/dim]"),
152
+ title="Controls",
153
+ border_style="blue"
154
+ )
155
+
156
+ def get_stats_panel():
157
+ """Sidebar showing database status and history."""
158
+ try:
159
+ fav_count = len(fav_table.all())
160
+ content = f"[bold green]Offline Songs: {fav_count}[/bold green]\n\n"
161
+ content += "[bold white]Recent Activity:[/bold white]\n"
162
+ if os.path.exists(HISTORY_FILE):
163
+ with open(HISTORY_FILE, "r") as f:
164
+ lines = f.readlines()[-3:]
165
+ content += "".join([f"[dim]» {line.split('|')[0].strip()[:20]}...[/dim]\n" for line in lines])
166
+ else:
167
+ content += "[dim]No recent plays.[/dim]"
168
+ except:
169
+ content = "[red]DB Access Error[/red]"
170
+
171
+ return Panel(content, title="SPCI Stats", border_style="green")
172
+
173
+ # --- CORE BACKEND LOGIC ---
174
+ def auto_install_dependencies(system):
175
+ """Uses subprocess to install ffmpeg and mpv based on the OS."""
176
+ try:
177
+ if system == "Darwin": # macOS
178
+ if shutil.which("brew"):
179
+ console.print("[yellow]macOS detected. Installing via Homebrew...[/yellow]")
180
+ subprocess.run(["brew", "install", "ffmpeg", "mpv"], check=True)
181
+ else:
182
+ console.print("[red]Homebrew not found. Please install Homebrew first.[/red]")
183
+
184
+ elif system == "Linux":
185
+ # Check for common Linux package managers
186
+ if shutil.which("apt"):
187
+ console.print("[yellow]Linux (Debian/Ubuntu) detected. Installing via apt...[/yellow]")
188
+ # Using sudo may require user password input in terminal
189
+ subprocess.run(["sudo", "apt", "update"], check=True)
190
+ subprocess.run(["sudo", "apt", "install", "-y", "ffmpeg", "mpv"], check=True)
191
+ elif shutil.which("pacman"):
192
+ console.print("[yellow]Arch Linux detected. Installing via pacman...[/yellow]")
193
+ subprocess.run(["sudo", "pacman", "-S", "--noconfirm", "ffmpeg", "mpv"], check=True)
194
+ elif shutil.which("dnf"):
195
+ console.print("[yellow]Fedora detected. Installing via dnf...[/yellow]")
196
+ subprocess.run(["sudo", "dnf", "install", "-y", "ffmpeg", "mpv"], check=True)
197
+ except subprocess.CalledProcessError as e:
198
+ console.print(f"[bold red]Installation failed:[/bold red] {e}")
199
+
200
+ def get_player_command():
201
+ """Checks for binaries. Uses local Trinity on Windows, system mpv on Linux/Mac."""
202
+ system = platform.system()
203
+
204
+ if system == "Windows":
205
+ ffplay_flags = ["-nodisp", "-autoexit", "-loglevel", "quiet", "-infbuf"]
206
+ if all(os.path.exists(p) for p in [FFPLAY_PATH, FFMPEG_PATH, FFPROBE_PATH]):
207
+ return [FFPLAY_PATH] + ffplay_flags
208
+ return download_trinity_windows(ffplay_flags)
209
+
210
+ # LINUX/MAC: Look for mpv first as it's the most stable
211
+ mpv_path = shutil.which("mpv")
212
+ if mpv_path:
213
+ return [mpv_path, "--no-video", "--gapless-audio=yes"]
214
+
215
+ # Fallback to ffplay only if mpv is missing
216
+ ffplay_path = shutil.which("ffplay")
217
+ if ffplay_path:
218
+ return [ffplay_path, "-nodisp", "-autoexit", "-loglevel", "quiet"]
219
+
220
+ console.print("[bold red]Error: No player found.[/bold red] Please run: sudo apt install mpv")
221
+ sys.exit(1)
222
+
223
+
224
+ def download_trinity_windows(flags):
225
+ """Fixed: Path-agnostic extraction for Windows binaries."""
226
+ console.print("\n[bold yellow]Requirement Missing: Audio Engine not found.[/bold yellow]")
227
+ url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
228
+
229
+ response = requests.get(url, stream=True)
230
+ total_size = int(response.headers.get('content-length', 0))
231
+ buffer = io.BytesIO()
232
+
233
+ with Progress(SpinnerColumn(), TextColumn("[green]Fetching Engine..."), BarColumn(), console=console) as progress:
234
+ task = progress.add_task("Downloading", total=total_size)
235
+ for chunk in response.iter_content(chunk_size=8192):
236
+ buffer.write(chunk)
237
+ progress.update(task, advance=len(chunk))
238
+
239
+ buffer.seek(0)
240
+ with zipfile.ZipFile(buffer) as z:
241
+ for member in z.namelist():
242
+ filename = os.path.basename(member)
243
+ # Extracts specifically into your local .spci/bin folder
244
+ if filename == "ffplay.exe":
245
+ with open(FFPLAY_PATH, "wb") as f: f.write(z.read(member))
246
+ elif filename == "ffmpeg.exe":
247
+ with open(FFMPEG_PATH, "wb") as f: f.write(z.read(member))
248
+ elif filename == "ffprobe.exe":
249
+ with open(FFPROBE_PATH, "wb") as f: f.write(z.read(member))
250
+ return [FFPLAY_PATH] + flags
251
+
252
+
253
+
254
+ def log_history(name, video_id):
255
+ with open(HISTORY_FILE, "a") as f:
256
+ f.write(f"{name} | {video_id}\n")
257
+
258
+ # --- USER COMMANDS ---
259
+
260
+
261
+ @app.command()
262
+ def setup():
263
+ subprocess.run(["pip", "install", "-e", "."], check=True)
264
+
265
+ @app.command(short_help="Add song to storage (Raw format for mpv)")
266
+ def add_fav(video_id: str):
267
+ """Downloads raw audio without needing ffmpeg conversion on Linux/Mac."""
268
+ system = platform.system()
269
+ url = f"https://www.youtube.com/watch?v={video_id}"
270
+
271
+ # On Windows, we still use our local ffmpeg to make .mp3s
272
+ # On Linux/Mac, we download the raw file to avoid ffmpeg dependencies
273
+ if system == "Windows":
274
+ ydl_opts = {
275
+ 'format': 'bestaudio/best',
276
+ 'outtmpl': os.path.join(FAV_DIR, video_id),
277
+ 'ffmpeg_location': BIN_DIR,
278
+ 'postprocessors': [{'key': 'FFmpegExtractAudio','preferredcodec': 'mp3','preferredquality': '64'}],
279
+ }
280
+ else:
281
+ # NO POST-PROCESSING: Just get the raw audio file (usually .webm or .m4a)
282
+ ydl_opts = {
283
+ 'format': 'bestaudio/best',
284
+ 'outtmpl': os.path.join(FAV_DIR, f"{video_id}.%(ext)s"),
285
+ 'quiet': True,
286
+ }
287
+
288
+ with console.status(f"[bold green]Downloading '{video_id}'...[/bold green]"):
289
+ try:
290
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
291
+ info = ydl.extract_info(url, download=True)
292
+ # Find exactly what file was saved
293
+ ext = info.get('ext', 'mp3') if system == "Windows" else info['ext']
294
+ final_path = os.path.join(FAV_DIR, f"{video_id}.{ext}")
295
+
296
+ fav_table.upsert({
297
+ 'video_id': video_id,
298
+ 'title': info.get('title'),
299
+ 'artist': info.get('uploader'),
300
+ 'path': final_path
301
+ }, Query().video_id == video_id)
302
+ console.print(f"[bold green]Success![/bold green] Saved as {info['ext']} for mpv playback.")
303
+ except Exception as e:
304
+ console.print(f"[bold red]Download Error:[/bold red] {e}")
305
+
306
+ @app.command(short_help="View and manage your offline favorites")
307
+ def show_fav():
308
+ """Lists all structured data in the favorites NoSQL box."""
309
+ favs = fav_table.all()
310
+ if not favs:
311
+ console.print("[dim]No offline songs found. Try 'add-fav <VideoID>'[/dim]")
312
+ return
313
+
314
+ table = Table(title="OFFLINE FAVORITES", box=box.HEAVY_EDGE)
315
+ table.add_column("No.", style="dim")
316
+ table.add_column("Song Title", style="bold white")
317
+ table.add_column("Artist", style="cyan")
318
+ table.add_column("Video ID", style="green")
319
+
320
+ for i, song in enumerate(favs, start=1):
321
+ table.add_row(str(i), song['title'], song['artist'], song['video_id'])
322
+
323
+ console.print(table, justify="center")
324
+
325
+
326
+
327
+ @app.command(short_help="show help")
328
+ def help():
329
+ table = Table(show_header=True, header_style="bold blue", box=box.ROUNDED)
330
+ table.add_column("Command", width=14, style="cyan")
331
+ table.add_column("Description", style="dim", width=50)
332
+ table.add_row("search", "Search for a song")
333
+ table.add_row("play", "Play a song")
334
+ table.add_row("add-fav", "Add a song to favorites for offline playback")
335
+ table.add_row("show-fav", "Show offline favorite songs")
336
+ table.add_row("delete-fav", "Delete a song from offline favorites")
337
+ table.add_row("show-history", "Show the play history")
338
+ table.add_row("clear-history", "Clear the play history")
339
+ table.add_row("setup", "setup the environment to global")
340
+
341
+ console.print(
342
+ Panel(
343
+ Align.center(
344
+ """[bold green]
345
+ ░██████╗██████╗░░█████╗░██╗
346
+ ██╔════╝██╔══██╗██╔══██╗██║
347
+ ╚█████╗░██████╔╝██║░░╚═╝██║
348
+ ░╚═══██╗██╔═══╝░██║░░██╗██║
349
+ ██████╔╝██║░░░░░╚█████╔╝██║
350
+ ╚═════╝░╚═╝░░░░░░╚════╝░╚═╝
351
+ [/bold green]
352
+ [dim]Sonic Pulse Command Interface[/dim]
353
+ [dim]A simple yet elegant CLI music player[/dim]
354
+ """,
355
+ vertical="middle"
356
+ ),
357
+ box=box.DOUBLE,
358
+ style="green",
359
+ subtitle="Welcome"
360
+ ),
361
+ Align.right("""developed by [bold blue][link=https://github.com/ojaswi1234]@ojaswi1234[/link][/bold blue]""")
362
+ )
363
+ console.print(table, justify="center")
364
+
365
+
366
+ @app.command(short_help="Find music online")
367
+ def search(query: str):
368
+ """Searches Online and displays results with their unique IDs."""
369
+ with console.status(f"[bold green]Searching music online '{query}'...[/bold green]"):
370
+ results = get_music(query)
371
+
372
+ if results:
373
+ table = Table(title=f"YouTube Results for: {query}", box=box.MINIMAL_DOUBLE_HEAD)
374
+ table.add_column("ID", style="green")
375
+ table.add_column("Title", style="bold white")
376
+ table.add_column("Channel/Artist", style="cyan")
377
+ table.add_column("Length", justify="right")
378
+
379
+ for song in results:
380
+ table.add_row(song['videoId'], song['title'], song['artists'], song['duration'])
381
+ console.print(table, justify="center")
382
+ else:
383
+ console.print("[bold red]No results found.[/bold red]")
384
+
385
+
386
+
387
+
388
+ @app.command(short_help="Play a song (Checks offline first)")
389
+ def play(query: str):
390
+ """Handles playback with robust variable initialization to prevent crashes."""
391
+ Song = Query()
392
+ offline_entry = fav_table.get((Song.video_id == query) | (Song.title == query))
393
+
394
+ # 1. INITIALIZE VARIABLES (Prevents UnboundLocalError)
395
+ title = "Unknown Title"
396
+ artist = "Unknown Artist"
397
+ vid = query
398
+ audio_source = None
399
+ is_offline = False
400
+
401
+ if offline_entry:
402
+ # Check for the file (supports multiple extensions for mpv)
403
+ base_path = os.path.splitext(offline_entry['path'])[0]
404
+ for ext in ['.webm', '.m4a', '.mp3', '.opus']:
405
+ test_path = base_path + ext
406
+ if os.path.exists(test_path):
407
+ audio_source = test_path
408
+ title = offline_entry.get('title', 'Unknown')
409
+ artist = offline_entry.get('artist', 'Unknown')
410
+ vid = offline_entry.get('video_id', query)
411
+ is_offline = True
412
+ break
413
+
414
+ if not is_offline:
415
+ # 2. ONLINE FALLBACK
416
+ try:
417
+ with console.status(f"[bold green]Searching online for '{query}'...[/bold green]"):
418
+ results = get_music(query)
419
+ if not results:
420
+ return console.print("[bold red]Song not found offline or online.[/bold red]")
421
+
422
+ song = results[0]
423
+ vid, title, artist = song['videoId'], song['title'], song['artists']
424
+
425
+ # Double check search result against DB
426
+ second_check = fav_table.get(Song.video_id == vid)
427
+ if second_check and os.path.exists(second_check['path']):
428
+ audio_source, is_offline = second_check['path'], True
429
+ else:
430
+ # Stream 64kbps to save bandwidth
431
+ ydl_opts = {'format': 'bestaudio[abr<=64]/bestaudio/best', 'quiet': True}
432
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
433
+ info = ydl.extract_info(f"https://www.youtube.com/watch?v={vid}", download=False)
434
+ audio_source = info.get('url')
435
+ is_offline = False
436
+ except Exception:
437
+ return console.print("[bold red]Error:[/bold red] Check internet or favorites.")
438
+
439
+ # 3. LOG HISTORY (Variables are now guaranteed to exist)
440
+ log_history(title, vid)
441
+
442
+ # UI EXECUTION
443
+ layout = make_layout()
444
+ try:
445
+ with Live(layout, refresh_per_second=20, screen=True):
446
+ # Uses mpv or ffplay based on OS detection
447
+ process = subprocess.Popen(get_player_command() + [audio_source],
448
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
449
+ t = 0.0
450
+ while process.poll() is None:
451
+ layout["header"].update(get_header())
452
+ layout["left"].update(get_now_playing_panel(title, artist, is_offline, t))
453
+ layout["right"].update(get_stats_panel())
454
+ layout["footer"].update(get_controls_panel())
455
+ t += 0.12
456
+ time.sleep(0.05)
457
+ except KeyboardInterrupt:
458
+ process.terminate()
459
+
460
+ @app.command(short_help="Remove a song from your offline favorites")
461
+ def delete_fav(video_id: str):
462
+ """Deletes the local audio file and removes metadata from TinyDB."""
463
+ Song = Query()
464
+ item = fav_table.get(Song.video_id == video_id)
465
+
466
+ if not item:
467
+ console.print(f"[bold red]Error:[/bold red] Video ID '{video_id}' not found in favorites.")
468
+ return
469
+
470
+ # 1. Delete the physical file
471
+ file_path = item.get('path')
472
+ try:
473
+ if file_path and os.path.exists(file_path):
474
+ os.remove(file_path)
475
+ console.print(f"[dim]Physical file removed: {video_id}.mp3[/dim]")
476
+ except Exception as e:
477
+ console.print(f"[bold yellow]Warning:[/bold yellow] Could not delete file: {e}")
478
+
479
+ # 2. Remove from NoSQL Database
480
+ fav_table.remove(Song.video_id == video_id)
481
+ console.print(f"[bold green]Deleted![/bold green] '{item['title']}' has been removed from SPCI.")
482
+
483
+ @app.command()
484
+ def show_history():
485
+ if os.path.exists(HISTORY_FILE):
486
+ with open(HISTORY_FILE) as f:
487
+ history_text = f.read()
488
+
489
+ panel = Panel(
490
+ history_text,
491
+ title="[bold blue]Play History[/bold blue]",
492
+ border_style="blue",
493
+ box=box.ROUNDED
494
+ )
495
+ console.print(panel)
496
+ else:
497
+ console.print("[dim]No history found.[/dim]")
498
+
499
+ @app.command()
500
+ def clear_history():
501
+ if os.path.exists(HISTORY_FILE):
502
+ os.remove(HISTORY_FILE)
503
+ console.print("[bold green]History cleared.[/bold green]")
504
+
505
+
506
+
507
+ if __name__ == "__main__":
508
+ app()
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: spci-sonic-pulse
3
+ Version: 2.0.1
4
+ Requires-Dist: typer
5
+ Requires-Dist: rich
6
+ Requires-Dist: requests
7
+ Requires-Dist: yt-dlp
8
+ Requires-Dist: python-vlc
9
+ Requires-Dist: ytmusicapi
10
+ Requires-Dist: tinydb
11
+ Dynamic: requires-dist
@@ -0,0 +1,8 @@
1
+ spci/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ spci/getmusic.py,sha256=4IOfsWqVxmTpV2yfhgwxlsBn91os_iM-ibLXFLO3c6c,1925
3
+ spci/mp.py,sha256=q6bXuAz-BqAqnD5ekQvHgUqEeFg-OZEdqlwS-3qCepU,20103
4
+ spci_sonic_pulse-2.0.1.dist-info/METADATA,sha256=H-aKF5dc5E37yM46h4ylR4M5Hdwfd4xYCMoyXFVW0s4,255
5
+ spci_sonic_pulse-2.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ spci_sonic_pulse-2.0.1.dist-info/entry_points.txt,sha256=Jhr15wSXKlpmphjf6NlRFdDoAMBtKIjhf8HvIrdM6O0,37
7
+ spci_sonic_pulse-2.0.1.dist-info/top_level.txt,sha256=hx42bb8jVcX4BeZkgkaTpcA5Lh9x3ONYicda0hyczxU,5
8
+ spci_sonic_pulse-2.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ spci = spci.mp:app
@@ -0,0 +1 @@
1
+ spci