lushapp 2.0.0

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.
@@ -0,0 +1,222 @@
1
+ # LUSH - Audio and Recording Engine
2
+ import time
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ try:
9
+ import mpv
10
+ HAS_MPV_LIB = True
11
+ except Exception:
12
+ HAS_MPV_LIB = False
13
+
14
+
15
+ class CLIPlayer:
16
+ """Universal subprocess-based audio player fallback (supports mpv, ffplay, vlc)."""
17
+ def __init__(self, volume: int = 100, user_agent: str = "LUSH/2.0 (Terminal Audio Player)"):
18
+ self.volume = volume
19
+ self.user_agent = user_agent
20
+ self.proc = None
21
+ self.pause = False
22
+ self.current_url = None
23
+
24
+ def play(self, url: str):
25
+ self.stop()
26
+ self.current_url = url
27
+ self.pause = False
28
+
29
+ if shutil.which("mpv"):
30
+ cmd = ["mpv", "--no-video", "--no-terminal", f"--volume={self.volume}", f"--user-agent={self.user_agent}", url]
31
+ elif shutil.which("ffplay"):
32
+ cmd = ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", "-volume", str(int(self.volume)), url]
33
+ elif shutil.which("cvlc"):
34
+ cmd = ["cvlc", "--no-video", "--gain", str(self.volume / 100.0), url]
35
+ else:
36
+ return
37
+
38
+ try:
39
+ self.proc = subprocess.Popen(
40
+ cmd,
41
+ stdin=subprocess.DEVNULL,
42
+ stdout=subprocess.DEVNULL,
43
+ stderr=subprocess.DEVNULL
44
+ )
45
+ except Exception:
46
+ self.proc = None
47
+
48
+ def stop(self):
49
+ if self.proc:
50
+ try:
51
+ self.proc.terminate()
52
+ self.proc.wait(timeout=0.2)
53
+ except Exception:
54
+ try:
55
+ self.proc.kill()
56
+ except Exception:
57
+ pass
58
+ self.proc = None
59
+
60
+ def terminate(self):
61
+ self.stop()
62
+
63
+
64
+ class AudioEngine:
65
+ def __init__(self, recordings_dir: Path):
66
+ self.recordings_dir = recordings_dir
67
+ self.recording = False
68
+ self.rec_proc = None
69
+ self.rec_start_time = 0
70
+ self.rec_file = None
71
+
72
+ # Main Station Player
73
+ self.player = None
74
+ if HAS_MPV_LIB:
75
+ try:
76
+ self.player = mpv.MPV(ytdl=True, video='no', user_agent='LUSH/2.0 (Terminal Audio Player)')
77
+ self.player.volume = 100
78
+ except Exception:
79
+ self.player = None
80
+
81
+ if self.player is None:
82
+ self.player = CLIPlayer(volume=100, user_agent='LUSH/2.0 (Terminal Audio Player)')
83
+
84
+ # Secondary Ambient Soundscape Player
85
+ self.ambient_player = None
86
+ if HAS_MPV_LIB:
87
+ try:
88
+ self.ambient_player = mpv.MPV(ytdl=False, video='no', user_agent='LUSH/2.0 (Ambient Player)')
89
+ self.ambient_player.volume = 40
90
+ except Exception:
91
+ self.ambient_player = None
92
+
93
+ if self.ambient_player is None:
94
+ self.ambient_player = CLIPlayer(volume=40, user_agent='LUSH/2.0 (Ambient Player)')
95
+
96
+ def play_station(self, url: str):
97
+ if not self.player: return
98
+ try:
99
+ self.player.pause = False
100
+ self.player.play(url)
101
+ except Exception:
102
+ pass
103
+
104
+ def toggle_play(self, currently_playing: bool) -> bool:
105
+ if not self.player: return False
106
+ try:
107
+ if currently_playing:
108
+ self.player.pause = True
109
+ return False
110
+ else:
111
+ self.player.pause = False
112
+ return True
113
+ except Exception:
114
+ return currently_playing
115
+
116
+ def set_volume(self, vol: int):
117
+ if self.player:
118
+ try:
119
+ self.player.volume = max(0, min(100, int(vol)))
120
+ except Exception:
121
+ pass
122
+
123
+ def set_ambient_volume(self, vol: int):
124
+ if self.ambient_player:
125
+ try:
126
+ self.ambient_player.volume = max(0, min(100, int(vol)))
127
+ except Exception:
128
+ pass
129
+
130
+ def play_ambient(self, url: str, vol: int):
131
+ if not self.ambient_player: return
132
+ try:
133
+ if not url:
134
+ self.ambient_player.stop()
135
+ else:
136
+ self.ambient_player.volume = vol
137
+ self.ambient_player.play(url)
138
+ except Exception:
139
+ pass
140
+
141
+ def toggle_recording(self, station_name: str, station_url: str, current_track: str = ""):
142
+ if not self.recording:
143
+ self.recordings_dir.mkdir(parents=True, exist_ok=True)
144
+ sanitized_st = re.sub(r'[^\w\-_\.]', '_', station_name)
145
+ sanitized_tr = re.sub(r'[^\w\-_\.]', '_', current_track) if current_track else ""
146
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
147
+ filename = f"{sanitized_st}_{sanitized_tr}_{timestamp}.flac" if sanitized_tr else f"{sanitized_st}_{timestamp}.flac"
148
+ rec_path = self.recordings_dir / filename
149
+
150
+ try:
151
+ cmd = [
152
+ "ffmpeg", "-y", "-nostdin",
153
+ "-i", station_url,
154
+ "-vn", "-c:a", "flac",
155
+ "-compression_level", "5",
156
+ "-metadata", f"title={current_track or station_name}",
157
+ "-metadata", f"artist={station_name}",
158
+ "-metadata", "album=LUSH Audiophile Live Recordings",
159
+ "-metadata", "comment=Lossless FLAC Master Capture",
160
+ "-metadata", "encoder=LUSH Audiophile FLAC Engine",
161
+ str(rec_path)
162
+ ]
163
+ self.rec_proc = subprocess.Popen(
164
+ cmd,
165
+ stdin=subprocess.DEVNULL,
166
+ stdout=subprocess.DEVNULL,
167
+ stderr=subprocess.DEVNULL
168
+ )
169
+ self.recording = True
170
+ self.rec_start_time = time.time()
171
+ self.rec_file = rec_path
172
+ return True, f"● REC (Lossless FLAC): {rec_path.name}", rec_path
173
+ except Exception as e:
174
+ return False, f"Recording error: {e}", None
175
+ else:
176
+ if self.rec_proc:
177
+ try:
178
+ import signal
179
+ self.rec_proc.send_signal(signal.SIGINT)
180
+ self.rec_proc.wait(timeout=3.0)
181
+ except Exception:
182
+ try:
183
+ self.rec_proc.terminate()
184
+ self.rec_proc.wait(timeout=1.0)
185
+ except Exception:
186
+ try:
187
+ self.rec_proc.kill()
188
+ except Exception:
189
+ pass
190
+ self.recording = False
191
+ saved_file = self.rec_file
192
+ size_mb = 0.0
193
+ if saved_file and saved_file.exists():
194
+ size_mb = saved_file.stat().st_size / (1024 * 1024)
195
+ return False, f"→ Saved Lossless FLAC ({size_mb:.2f} MB): {saved_file.name if saved_file else 'recording'}", saved_file
196
+
197
+ def shutdown(self):
198
+ if self.recording and self.rec_proc:
199
+ try:
200
+ self.rec_proc.terminate()
201
+ self.rec_proc.wait(timeout=0.2)
202
+ except Exception:
203
+ pass
204
+ self.rec_proc = None
205
+ if self.player:
206
+ try:
207
+ self.player.terminate()
208
+ except Exception:
209
+ pass
210
+ self.player = None
211
+ if self.ambient_player:
212
+ try:
213
+ self.ambient_player.terminate()
214
+ except Exception:
215
+ pass
216
+ self.ambient_player = None
217
+
218
+ def quit(self):
219
+ self.shutdown()
220
+
221
+ def __del__(self):
222
+ self.shutdown()