lushapp 2.0.3 → 2.0.4

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/README.md CHANGED
@@ -44,6 +44,18 @@ lush
44
44
  lushapp
45
45
  ```
46
46
 
47
+ ### Update to Latest Version
48
+ ```bash
49
+ lush update
50
+ # or
51
+ lushapp update
52
+
53
+ # Manual package updates:
54
+ npm install -g lushapp@latest
55
+ # or
56
+ pip install --upgrade lushapp
57
+ ```
58
+
47
59
  ### Clone & Run from Source
48
60
  ```bash
49
61
  git clone https://github.com/pseudoshell/lush.git
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lushapp",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
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.3"
7
+ version = "2.0.4"
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.3",
5
+ version="2.0.4",
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.3"
5
+ __version__ = "2.0.4"
6
6
 
7
7
  def run_app():
8
8
  try:
@@ -3,7 +3,66 @@ import sys
3
3
  import argparse
4
4
  from . import __version__, run_app
5
5
 
6
+ def update_lush():
7
+ """Detects installation method and automatically updates the lushapp package."""
8
+ import shutil
9
+ import subprocess
10
+ from . import __version__
11
+
12
+ print("\033[1;95m┌────────────────────────────────────────────────────────────────────────┐\033[0m")
13
+ print(f"\033[1;95m│\033[0m \033[1;96mLUSH (Audiophile Edition)\033[0m — Automated Package Updater \033[1;95m│\033[0m")
14
+ print(f"\033[1;95m│\033[0m Current Version: \033[1;92mv{__version__:<10}\033[0m Package Name: \033[1;93mlushapp\033[0m \033[1;95m│\033[0m")
15
+ print("\033[1;95m└────────────────────────────────────────────────────────────────────────┘\033[0m\n")
16
+
17
+ # 1. Check if installed via npm
18
+ if shutil.which("npm"):
19
+ try:
20
+ check_npm = subprocess.run(["npm", "ls", "-g", "lushapp", "--depth=0"], capture_output=True, text=True)
21
+ if "lushapp@" in check_npm.stdout or "lushapp" in check_npm.stdout:
22
+ print("\033[1;96m> Detected global npm installation. Updating lushapp...\033[0m")
23
+ print("Running: \033[1;92mnpm install -g lushapp@latest\033[0m\n")
24
+ res = subprocess.run(["npm", "install", "-g", "lushapp@latest"])
25
+ if res.returncode == 0:
26
+ print("\n\033[1;92m✓ Successfully updated lushapp to the latest version via npm!\033[0m")
27
+ print("Run \033[1;96mlush\033[0m or \033[1;96mlushapp\033[0m to launch.\n")
28
+ sys.exit(0)
29
+ except Exception:
30
+ pass
31
+
32
+ # 2. Check if installed via pipx
33
+ if shutil.which("pipx"):
34
+ try:
35
+ check_pipx = subprocess.run(["pipx", "list"], capture_output=True, text=True)
36
+ if "lushapp" in check_pipx.stdout:
37
+ print("\033[1;96m> Detected pipx installation. Updating lushapp...\033[0m")
38
+ print("Running: \033[1;92mpipx upgrade lushapp\033[0m\n")
39
+ res = subprocess.run(["pipx", "upgrade", "lushapp"])
40
+ if res.returncode == 0:
41
+ print("\n\033[1;92m✓ Successfully updated lushapp via pipx!\033[0m\n")
42
+ sys.exit(0)
43
+ except Exception:
44
+ pass
45
+
46
+ # 3. Check if installed via pip / Python
47
+ pip_cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "lushapp"]
48
+ print("\033[1;96m> Updating lushapp via pip...\033[0m")
49
+ print(f"Running: \033[1;92m{' '.join(pip_cmd)}\033[0m\n")
50
+ res = subprocess.run(pip_cmd)
51
+ if res.returncode == 0:
52
+ print("\n\033[1;92m✓ Successfully updated lushapp to the latest version!\033[0m")
53
+ print("Run \033[1;96mlush\033[0m or \033[1;96mlushapp\033[0m to launch.\n")
54
+ sys.exit(0)
55
+
56
+ # 4. Fallback info
57
+ print("\n\033[1;93m[NOTICE]\033[0m To manually update LUSH, run:")
58
+ print(" • npm: \033[1;96mnpm install -g lushapp@latest\033[0m")
59
+ print(" • pip: \033[1;96mpip install --upgrade lushapp\033[0m\n")
60
+ sys.exit(0)
61
+
6
62
  def main():
63
+ if any(arg in sys.argv[1:] for arg in ("update", "upgrade", "--update", "-U")):
64
+ update_lush()
65
+
7
66
  parser = argparse.ArgumentParser(
8
67
  prog="lush",
9
68
  description="LUSH - A hyper-aesthetic terminal radio player with 560+ live stations and real-time CAVA DSP visualizers.",
@@ -11,10 +70,15 @@ def main():
11
70
  )
12
71
  parser.add_argument("-v", "--version", action="version", version=f"LUSH v{__version__} (Audiophile Edition)")
13
72
  parser.add_argument("--test", action="store_true", help="Run internal self-test diagnostics")
73
+ parser.add_argument("--update", action="store_true", help="Update lushapp package to the latest release")
74
+ parser.add_argument("command", nargs="?", help="Optional subcommand (e.g. 'update', 'test')")
14
75
 
15
76
  args, unknown = parser.parse_known_args()
16
77
 
17
- if args.test:
78
+ if args.command in ("update", "upgrade") or args.update:
79
+ update_lush()
80
+
81
+ if args.test or args.command == "test":
18
82
  from pathlib import Path
19
83
  test_cands = [
20
84
  Path(__file__).resolve().parent.parent.parent / "tests" / "run_tests.py",
package/src/lush/audio.py CHANGED
@@ -12,6 +12,9 @@ except Exception:
12
12
  HAS_MPV_LIB = False
13
13
 
14
14
 
15
+ import threading
16
+ import requests
17
+
15
18
  class CLIPlayer:
16
19
  """Universal subprocess-based audio player fallback (supports mpv, ffplay, vlc)."""
17
20
  def __init__(self, volume: int = 100, user_agent: str = "LUSH/2.0 (Terminal Audio Player)"):
@@ -20,6 +23,9 @@ class CLIPlayer:
20
23
  self.proc = None
21
24
  self._pause = False
22
25
  self.current_url = None
26
+ self.metadata = {}
27
+ self._meta_thread = None
28
+ self._stop_meta = False
23
29
 
24
30
  @property
25
31
  def pause(self) -> bool:
@@ -66,10 +72,50 @@ class CLIPlayer:
66
72
  except Exception:
67
73
  pass
68
74
 
75
+ def _meta_worker(self, url: str):
76
+ try:
77
+ r = requests.get(
78
+ url,
79
+ headers={"Icy-MetaData": "1", "User-Agent": self.user_agent},
80
+ stream=True,
81
+ timeout=5
82
+ )
83
+ icy_br = r.headers.get("icy-br") or r.headers.get("ice-bitrate")
84
+ if icy_br:
85
+ self.metadata["icy-br"] = str(icy_br)
86
+
87
+ metaint = int(r.headers.get("icy-metaint", 0))
88
+ if metaint > 0:
89
+ while self.current_url == url and not self._stop_meta:
90
+ data = r.raw.read(metaint)
91
+ if not data:
92
+ break
93
+ len_byte = r.raw.read(1)
94
+ if not len_byte:
95
+ break
96
+ meta_len = len_byte[0] * 16
97
+ if meta_len > 0:
98
+ meta_bytes = r.raw.read(meta_len)
99
+ try:
100
+ meta_str = meta_bytes.decode("utf-8", errors="ignore")
101
+ except Exception:
102
+ meta_str = meta_bytes.decode("latin-1", errors="ignore")
103
+ m = re.search(r"StreamTitle='(.*?)';", meta_str)
104
+ if m:
105
+ stream_title = m.group(1).strip()
106
+ if stream_title:
107
+ self.metadata["icy-title"] = stream_title
108
+ self.metadata["title"] = stream_title
109
+ r.close()
110
+ except Exception:
111
+ pass
112
+
69
113
  def play(self, url: str):
70
114
  self.stop()
71
115
  self.current_url = url
72
116
  self._pause = False
117
+ self.metadata = {}
118
+ self._stop_meta = False
73
119
 
74
120
  if shutil.which("mpv"):
75
121
  cmd = [
@@ -101,7 +147,12 @@ class CLIPlayer:
101
147
  except Exception:
102
148
  self.proc = None
103
149
 
150
+ if url.startswith("http://") or url.startswith("https://"):
151
+ self._meta_thread = threading.Thread(target=self._meta_worker, args=(url,), daemon=True)
152
+ self._meta_thread.start()
153
+
104
154
  def stop(self):
155
+ self._stop_meta = True
105
156
  if self.proc:
106
157
  try:
107
158
  self.proc.terminate()