lushapp 2.0.2 → 2.0.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lushapp",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "A hyper-aesthetic terminal radio player with 560+ live stations, real-time CAVA DSP audio visualizers, ambient soundscapes, and artist discographies.",
5
5
  "main": "bin/lush.js",
6
6
  "bin": {
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lushapp"
7
- version = "2.0.2"
7
+ version = "2.0.3"
8
8
  description = "A hyper-aesthetic terminal radio player with 560+ live stations, real-time CAVA DSP visualizers, and artist discographies."
9
9
  readme = "README.md"
10
10
  authors = [{ name = "pseudoshell" }]
package/setup.py CHANGED
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="lushapp",
5
- version="2.0.2",
5
+ version="2.0.3",
6
6
  package_dir={"": "src"},
7
7
  packages=find_packages(where="src"),
8
8
  include_package_data=True,
@@ -2,7 +2,7 @@ import os
2
2
  import sys
3
3
  os.environ["ESCDELAY"] = "25"
4
4
 
5
- __version__ = "2.0.2"
5
+ __version__ = "2.0.3"
6
6
 
7
7
  def run_app():
8
8
  try:
package/src/lush/audio.py CHANGED
@@ -15,30 +15,86 @@ except Exception:
15
15
  class CLIPlayer:
16
16
  """Universal subprocess-based audio player fallback (supports mpv, ffplay, vlc)."""
17
17
  def __init__(self, volume: int = 100, user_agent: str = "LUSH/2.0 (Terminal Audio Player)"):
18
- self.volume = volume
18
+ self._volume = volume
19
19
  self.user_agent = user_agent
20
20
  self.proc = None
21
- self.pause = False
21
+ self._pause = False
22
22
  self.current_url = None
23
23
 
24
+ @property
25
+ def pause(self) -> bool:
26
+ return self._pause
27
+
28
+ @pause.setter
29
+ def pause(self, val: bool):
30
+ self._pause = val
31
+ if val:
32
+ # Pause playback
33
+ if self.proc and self.proc.poll() is None:
34
+ if shutil.which("mpv") and self.proc.stdin:
35
+ try:
36
+ self.proc.stdin.write(b"set pause yes\n")
37
+ self.proc.stdin.flush()
38
+ return
39
+ except Exception:
40
+ pass
41
+ self.stop()
42
+ else:
43
+ # Resume playback
44
+ if self.proc and self.proc.poll() is None:
45
+ if shutil.which("mpv") and self.proc.stdin:
46
+ try:
47
+ self.proc.stdin.write(b"set pause no\n")
48
+ self.proc.stdin.flush()
49
+ return
50
+ except Exception:
51
+ pass
52
+ if self.current_url:
53
+ self.play(self.current_url)
54
+
55
+ @property
56
+ def volume(self) -> int:
57
+ return self._volume
58
+
59
+ @volume.setter
60
+ def volume(self, val: int):
61
+ self._volume = max(0, min(100, int(val)))
62
+ if self.proc and self.proc.poll() is None and self.proc.stdin and shutil.which("mpv"):
63
+ try:
64
+ self.proc.stdin.write(f"set volume {self._volume}\n".encode())
65
+ self.proc.stdin.flush()
66
+ except Exception:
67
+ pass
68
+
24
69
  def play(self, url: str):
25
70
  self.stop()
26
71
  self.current_url = url
27
- self.pause = False
72
+ self._pause = False
28
73
 
29
74
  if shutil.which("mpv"):
30
- cmd = ["mpv", "--no-video", "--no-terminal", f"--volume={self.volume}", f"--user-agent={self.user_agent}", url]
75
+ cmd = [
76
+ "mpv",
77
+ "--no-video",
78
+ "--no-terminal",
79
+ "--input-terminal=no",
80
+ f"--volume={self._volume}",
81
+ f"--user-agent={self.user_agent}",
82
+ url
83
+ ]
84
+ stdin_mode = subprocess.PIPE
31
85
  elif shutil.which("ffplay"):
32
- cmd = ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", "-volume", str(int(self.volume)), url]
86
+ cmd = ["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", "-volume", str(int(self._volume)), url]
87
+ stdin_mode = subprocess.DEVNULL
33
88
  elif shutil.which("cvlc"):
34
- cmd = ["cvlc", "--no-video", "--gain", str(self.volume / 100.0), url]
89
+ cmd = ["cvlc", "--no-video", "--gain", str(self._volume / 100.0), url]
90
+ stdin_mode = subprocess.DEVNULL
35
91
  else:
36
92
  return
37
93
 
38
94
  try:
39
95
  self.proc = subprocess.Popen(
40
96
  cmd,
41
- stdin=subprocess.DEVNULL,
97
+ stdin=stdin_mode,
42
98
  stdout=subprocess.DEVNULL,
43
99
  stderr=subprocess.DEVNULL
44
100
  )
@@ -2,8 +2,8 @@ import re
2
2
  # LUSH - Constants and Presets
3
3
 
4
4
  LOGO_LINES = [
5
- " █ █ █ █▀▀ █ █ █▀▀█",
6
- " █▄▄ █▄█ ▄██ █▀█ ██ ██"
5
+ " █ █ █ █▀▀ █ █ █▀▀▀█",
6
+ " █▄▄ █▄█ ▄██ █▀█ ██ ██"
7
7
  ]
8
8
 
9
9
  COMPACT_LOGO = "LUSH ♫"
@@ -139,16 +139,30 @@ class NotificationManager:
139
139
 
140
140
  @staticmethod
141
141
  def _dispatch_windows(title: str, body: str):
142
- safe_title = title.replace('"', '`"')
143
- safe_body = body.replace('"', '`"')
144
- ps_script = f'''
145
- [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
146
- $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
147
- $textNodes = $template.GetElementsByTagName("text")
148
- $textNodes.Item(0).AppendChild($template.CreateTextNode("{safe_title} - Now Playing")) > $null
149
- $textNodes.Item(1).AppendChild($template.CreateTextNode("{safe_body}")) > $null
150
- $notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("LUSH")
151
- $notification = [Windows.UI.Notifications.ToastNotification]::new($template)
152
- $notifier.Show($notification)
153
- '''
154
- subprocess.run(["powershell", "-NoProfile", "-Command", ps_script], capture_output=True, timeout=3.0)
142
+ import base64
143
+ import html
144
+ safe_title = html.escape(f"{title} - Now Playing")
145
+ safe_body = html.escape(body)
146
+ ps_script = f"""[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
147
+ [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
148
+ $template = @"
149
+ <toast>
150
+ <visual>
151
+ <binding template="ToastGeneric">
152
+ <text>{safe_title}</text>
153
+ <text>{safe_body}</text>
154
+ </binding>
155
+ </visual>
156
+ </toast>
157
+ "@
158
+ $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
159
+ $xml.LoadXml($template)
160
+ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
161
+ $appId = '{{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}}\\WindowsPowerShell\\v1.0\\powershell.exe'
162
+ [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($appId).Show($toast)
163
+ """
164
+ try:
165
+ enc = base64.b64encode(ps_script.encode("utf-16le")).decode("ascii")
166
+ subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-EncodedCommand", enc], capture_output=True, timeout=3.0)
167
+ except Exception:
168
+ pass
package/src/lush/ui.py CHANGED
@@ -179,7 +179,10 @@ def curses_main(stdscr, state: PlayerState):
179
179
  cava_bars = list(state.cava.bars) if (hasattr(state, "cava") and state.cava) else None
180
180
  p_id = get_shimmer_color_pair(shimmer_eff, x_norm, c_idx, row_i, t_now, cava_bars=cava_bars, is_playing=state.playing)
181
181
  attr = curses.color_pair(p_id) | curses.A_BOLD
182
- stdscr.addstr(1 + row_i, 2 + c_idx, ch, attr)
182
+ try:
183
+ stdscr.addstr(1 + row_i, 2 + c_idx, ch, attr)
184
+ except Exception:
185
+ pass
183
186
  main_start_y = 4
184
187
  else:
185
188
  for c_idx, ch in enumerate(COMPACT_LOGO):
@@ -187,7 +190,10 @@ def curses_main(stdscr, state: PlayerState):
187
190
  cava_bars = list(state.cava.bars) if (hasattr(state, "cava") and state.cava) else None
188
191
  p_id = get_shimmer_color_pair(shimmer_eff, x_norm, c_idx, 0, t_now, cava_bars=cava_bars, is_playing=state.playing)
189
192
  attr = curses.color_pair(p_id) | curses.A_BOLD
190
- stdscr.addstr(0, 2 + c_idx, ch, attr)
193
+ try:
194
+ stdscr.addstr(0, 2 + c_idx, ch, attr)
195
+ except Exception:
196
+ pass
191
197
  main_start_y = 2
192
198
 
193
199
  # Right side stats: Real Live Network Speeds (↓ Down / ↑ Up) + Bitrate + Rec
@@ -23,9 +23,21 @@ def trim(text: str, width: int) -> str:
23
23
  if width <= 3: return text[:width]
24
24
  return text[:width-3] + "..."
25
25
 
26
- def apply_theme(theme_name: str, themes: dict):
26
+ _CURRENT_THEME = None
27
+
28
+ def apply_theme(theme_name: str, themes: dict, force: bool = False):
29
+ global _CURRENT_THEME
27
30
  if not curses.has_colors():
28
31
  return
32
+ if not force and _CURRENT_THEME == theme_name:
33
+ return
34
+ _CURRENT_THEME = theme_name
35
+
36
+ try:
37
+ curses.use_default_colors()
38
+ except Exception:
39
+ pass
40
+
29
41
  t = themes.get(theme_name, themes.get("Plur1bus", list(themes.values())[0]))
30
42
  is_256 = curses.COLORS >= 256
31
43