lushapp 2.0.3 → 2.0.5
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 +12 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/setup.py +1 -1
- package/src/lush/__init__.py +1 -1
- package/src/lush/__main__.py +65 -1
- package/src/lush/audio.py +51 -0
- package/src/lush/constants.py +33 -33
- package/src/lush/ui_helpers.py +5 -5
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
|
+
"version": "2.0.5",
|
|
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.
|
|
7
|
+
version = "2.0.5"
|
|
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
package/src/lush/__init__.py
CHANGED
package/src/lush/__main__.py
CHANGED
|
@@ -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.
|
|
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()
|
package/src/lush/constants.py
CHANGED
|
@@ -646,41 +646,41 @@ SHIMMER_EFFECT_LABELS = {
|
|
|
646
646
|
}
|
|
647
647
|
|
|
648
648
|
SHIMMER_PALETTES = {
|
|
649
|
-
"Brat Lime": [231, 190, 154, 118,
|
|
650
|
-
"Toxic Latex": [231, 219,
|
|
651
|
-
"Blohsh Acid": [231, 155, 118,
|
|
652
|
-
"Lover Pastel": [231, 225, 218, 189,
|
|
649
|
+
"Brat Lime": [231, 190, 154, 118, 154],
|
|
650
|
+
"Toxic Latex": [231, 219, 205, 198, 198],
|
|
651
|
+
"Blohsh Acid": [231, 155, 118, 82, 46],
|
|
652
|
+
"Lover Pastel": [231, 225, 218, 189, 218],
|
|
653
653
|
"Guts Sour": [231, 207, 177, 141, 129],
|
|
654
|
-
"Born to Die": [231, 222, 174, 160,
|
|
655
|
-
"After Hours": [231, 220,
|
|
656
|
-
"Random Access": [231, 254, 220, 214,
|
|
657
|
-
"Short n Sweet": [231, 230,
|
|
658
|
-
"She Wolf": [231, 221, 214, 208,
|
|
659
|
-
"Padam Fever": [231, 205, 198, 161,
|
|
660
|
-
"Spice World": [231,
|
|
661
|
-
"Barbie Dreamhouse": [231, 225, 213, 200,
|
|
662
|
-
"Cyber Lotus": [231, 219, 207,
|
|
663
|
-
"Moonlight": [231, 189,
|
|
654
|
+
"Born to Die": [231, 222, 174, 160, 160],
|
|
655
|
+
"After Hours": [231, 220, 203, 196, 196],
|
|
656
|
+
"Random Access": [231, 254, 220, 214, 220],
|
|
657
|
+
"Short n Sweet": [231, 230, 225, 218, 218],
|
|
658
|
+
"She Wolf": [231, 221, 214, 208, 214],
|
|
659
|
+
"Padam Fever": [231, 205, 198, 161, 161],
|
|
660
|
+
"Spice World": [231, 117, 75, 39, 27],
|
|
661
|
+
"Barbie Dreamhouse": [231, 225, 213, 200, 200],
|
|
662
|
+
"Cyber Lotus": [231, 219, 207, 171, 207],
|
|
663
|
+
"Moonlight": [231, 189, 147, 105, 105],
|
|
664
664
|
"Hot Pink": [231, 225, 219, 205, 198],
|
|
665
665
|
"Cyber Barbie": [231, 225, 213, 207, 201],
|
|
666
|
-
"Pink Velvet": [231, 224, 218, 206,
|
|
667
|
-
"Cotton Candy": [231, 224, 218, 189,
|
|
668
|
-
"Strawberry Milk": [231, 225, 224, 217,
|
|
669
|
-
"Sakura Bloom": [231, 225, 218, 211,
|
|
670
|
-
"Plur1bus": [231, 159, 51, 45
|
|
671
|
-
"Synthwave": [231, 225, 205, 198,
|
|
672
|
-
"Cyberpunk 2077": [231, 226, 220, 214,
|
|
666
|
+
"Pink Velvet": [231, 224, 218, 206, 198],
|
|
667
|
+
"Cotton Candy": [231, 224, 218, 189, 218],
|
|
668
|
+
"Strawberry Milk": [231, 225, 224, 217, 217],
|
|
669
|
+
"Sakura Bloom": [231, 225, 218, 211, 218],
|
|
670
|
+
"Plur1bus": [231, 159, 123, 51, 45],
|
|
671
|
+
"Synthwave": [231, 225, 205, 198, 198],
|
|
672
|
+
"Cyberpunk 2077": [231, 226, 220, 214, 226],
|
|
673
673
|
"Laser Purple": [231, 219, 198, 141, 129],
|
|
674
|
-
"Neon Genesis": [231, 226, 208,
|
|
675
|
-
"Matrix": [231, 157, 120,
|
|
676
|
-
"OLED Obsidian": [231, 255,
|
|
677
|
-
"Ghost White": [231, 255, 254, 250,
|
|
678
|
-
"Blood Moon": [231, 224, 203, 196,
|
|
679
|
-
"Sunset Horizon": [231, 223, 215, 208,
|
|
680
|
-
"Vapor Mint": [231, 159, 121, 85,
|
|
681
|
-
"Peach Blossom": [231, 223, 222, 216,
|
|
682
|
-
"Oceanic Abyss": [231, 195,
|
|
683
|
-
"Midnight Lavender": [231, 225, 189, 141,
|
|
684
|
-
"Emerald Glow": [231, 157, 120,
|
|
685
|
-
"Amber Terminal": [231, 229, 220, 214,
|
|
674
|
+
"Neon Genesis": [231, 226, 208, 165, 129],
|
|
675
|
+
"Matrix": [231, 157, 120, 84, 46],
|
|
676
|
+
"OLED Obsidian": [231, 255, 254, 252, 252],
|
|
677
|
+
"Ghost White": [231, 255, 254, 250, 231],
|
|
678
|
+
"Blood Moon": [231, 224, 203, 196, 196],
|
|
679
|
+
"Sunset Horizon": [231, 223, 215, 208, 208],
|
|
680
|
+
"Vapor Mint": [231, 159, 121, 85, 85],
|
|
681
|
+
"Peach Blossom": [231, 223, 222, 216, 216],
|
|
682
|
+
"Oceanic Abyss": [231, 195, 123, 51, 45],
|
|
683
|
+
"Midnight Lavender": [231, 225, 189, 141, 141],
|
|
684
|
+
"Emerald Glow": [231, 157, 120, 84, 48],
|
|
685
|
+
"Amber Terminal": [231, 229, 220, 214, 214]
|
|
686
686
|
}
|
package/src/lush/ui_helpers.py
CHANGED
|
@@ -82,19 +82,19 @@ def apply_theme(theme_name: str, themes: dict, force: bool = False):
|
|
|
82
82
|
|
|
83
83
|
# Shimmer gradient color pairs 10 - 14
|
|
84
84
|
if is_256:
|
|
85
|
-
shimmer_cols = SHIMMER_PALETTES.get(theme_name,
|
|
85
|
+
shimmer_cols = SHIMMER_PALETTES.get(theme_name, [231, accent, accent, accent, accent])
|
|
86
86
|
curses.init_pair(10, shimmer_cols[0], -1) # Peak White Hot Sparkle
|
|
87
87
|
curses.init_pair(11, shimmer_cols[1], -1) # Sheen Highlight
|
|
88
88
|
curses.init_pair(12, shimmer_cols[2], -1) # Neon Glow
|
|
89
89
|
curses.init_pair(13, shimmer_cols[3], -1) # Mid Glow
|
|
90
|
-
curses.init_pair(14,
|
|
90
|
+
curses.init_pair(14, accent, -1) # Base Tone: EXACT Theme Primary Accent!
|
|
91
91
|
curses.init_pair(15, 196, -1) # Glowing Red (REC indicator)
|
|
92
92
|
else:
|
|
93
93
|
curses.init_pair(10, curses.COLOR_WHITE, -1)
|
|
94
94
|
curses.init_pair(11, curses.COLOR_WHITE, -1)
|
|
95
|
-
curses.init_pair(12,
|
|
96
|
-
curses.init_pair(13,
|
|
97
|
-
curses.init_pair(14,
|
|
95
|
+
curses.init_pair(12, accent, -1)
|
|
96
|
+
curses.init_pair(13, accent, -1)
|
|
97
|
+
curses.init_pair(14, accent, -1)
|
|
98
98
|
curses.init_pair(15, curses.COLOR_RED, -1)
|
|
99
99
|
|
|
100
100
|
import math
|