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.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/bin/lush +62 -0
- package/bin/lush.js +48 -0
- package/package.json +61 -0
- package/pyproject.toml +52 -0
- package/scripts/postinstall.js +58 -0
- package/scripts/sync-version.js +38 -0
- package/setup.py +23 -0
- package/src/lush/__init__.py +30 -0
- package/src/lush/__main__.py +42 -0
- package/src/lush/audio.py +222 -0
- package/src/lush/cava.py +665 -0
- package/src/lush/constants.py +686 -0
- package/src/lush/data/cava.conf +19 -0
- package/src/lush/data/stations.json +3586 -0
- package/src/lush/modals.py +1382 -0
- package/src/lush/net.py +27 -0
- package/src/lush/notify.py +154 -0
- package/src/lush/search.py +185 -0
- package/src/lush/state.py +550 -0
- package/src/lush/stats.py +555 -0
- package/src/lush/ui.py +690 -0
- package/src/lush/ui_helpers.py +569 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pseudoshell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# LUSH (`lushapp`)
|
|
2
|
+
|
|
3
|
+
> Hyper-aesthetic terminal radio & music player with 560+ live streams, real-time CAVA DSP visualizers, retro studio decks, and ambient soundscapes.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
__ __ __ _____ __ __
|
|
7
|
+
/ / / / / / / ___// / / /
|
|
8
|
+
/ / / / / / \__ \/ /_/ / [ 560+ LIVE STATIONS ]
|
|
9
|
+
/ /__/ /_/ / ___/ / __ / [ 19 CAVA VISUALIZERS ]
|
|
10
|
+
/_____/\____/ /____/_/ /_/ [ 37 RICED THEMES ]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- **560+ Live Stations**: Lo-Fi, Synthwave, Cyberpunk, Ambient, Classical, ASMR, and curated Artist Discographies.
|
|
18
|
+
- **Real-Time CAVA DSP Visualizers**: 19 spectrum analyzers (up to 144Hz) and 36 audio-reactive shimmers.
|
|
19
|
+
- **Retro Studio Decks**: Kinetic Technics SL-1200 vinyl turntable and Nakamichi cassette deck animations.
|
|
20
|
+
- **Ambient Soundscapes**: Layer rain, vinyl crackle, campfire, or café sounds over any stream.
|
|
21
|
+
- **Lossless FLAC Recording**: Capture audio directly to `~/Music` in 24-bit FLAC with <kbd>Shift+R</kbd>.
|
|
22
|
+
- **Listening Diary**: 100% offline analytics with a 16-week GitHub-style heatmap and streak tracking.
|
|
23
|
+
- **Fuzzy Search**: Instant, typo-tolerant search across all stations with <kbd>/</kbd>.
|
|
24
|
+
- **37 Themes**: Highly customizable color palettes with live preview.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🚀 Quick Start
|
|
29
|
+
|
|
30
|
+
### Run Instantly (No Install)
|
|
31
|
+
```bash
|
|
32
|
+
npx lushapp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Install via pip / npm
|
|
36
|
+
```bash
|
|
37
|
+
pip install lushapp
|
|
38
|
+
# or
|
|
39
|
+
npm install -g lushapp
|
|
40
|
+
|
|
41
|
+
# Start with either command:
|
|
42
|
+
lush
|
|
43
|
+
# or
|
|
44
|
+
lushapp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Clone & Run from Source
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/pseudoshell/lush.git
|
|
50
|
+
cd lush
|
|
51
|
+
./bin/lush
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 📦 Prerequisites
|
|
57
|
+
|
|
58
|
+
LUSH requires **Python 3.9+** and **mpv**. Install **cava** for live visualizers.
|
|
59
|
+
|
|
60
|
+
| System | Command |
|
|
61
|
+
| :--- | :--- |
|
|
62
|
+
| **Arch / Manjaro** | `sudo pacman -S mpv cava python` |
|
|
63
|
+
| **Ubuntu / Debian** | `sudo apt install mpv cava python3` |
|
|
64
|
+
| **Fedora** | `sudo dnf install mpv cava python3` |
|
|
65
|
+
| **macOS (Homebrew)** | `brew install mpv cava python` |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## 🕹️ Keybindings
|
|
70
|
+
|
|
71
|
+
| Key | Action |
|
|
72
|
+
| :--- | :--- |
|
|
73
|
+
| <kbd>Space</kbd> | Play / Pause active stream |
|
|
74
|
+
| <kbd>/</kbd> | Live fuzzy search |
|
|
75
|
+
| <kbd>1</kbd> – <kbd>5</kbd> | Jump to Music categories (Top Artists, Gen Z, Legends, New Age, All) |
|
|
76
|
+
| <kbd>0</kbd> | Jump to All Web Radios |
|
|
77
|
+
| <kbd>p</kbd> | Open Now Playing Studio deck |
|
|
78
|
+
| <kbd>d</kbd> | Switch deck style (Turntable ↔ Cassette) |
|
|
79
|
+
| <kbd>v</kbd> | Cycle CAVA visualizers |
|
|
80
|
+
| <kbd>t</kbd> / <kbd>T</kbd> | Cycle / Select theme |
|
|
81
|
+
| <kbd>e</kbd> / <kbd>E</kbd> | Toggle / Configure ambient sounds |
|
|
82
|
+
| <kbd>Shift</kbd> + <kbd>R</kbd> | Toggle lossless FLAC recording |
|
|
83
|
+
| <kbd>b</kbd> | Add / Remove favorite station |
|
|
84
|
+
| <kbd>s</kbd> | Open Settings menu |
|
|
85
|
+
| <kbd>?</kbd> | Help modal |
|
|
86
|
+
| <kbd>q</kbd> | Quit |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 🧪 Diagnostics & Testing
|
|
91
|
+
|
|
92
|
+
Run the live streaming test suite:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
lush --test
|
|
96
|
+
# or
|
|
97
|
+
python3 tests/run_tests.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 📄 License
|
|
103
|
+
|
|
104
|
+
MIT © 2026 pseudoshell
|
package/bin/lush
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# Ensure lush package paths are in sys.path
|
|
7
|
+
for path_cand in [
|
|
8
|
+
Path(__file__).resolve().parent.parent / 'src',
|
|
9
|
+
Path(__file__).resolve().parent.parent / 'lib',
|
|
10
|
+
Path.home() / '.local' / 'lib',
|
|
11
|
+
]:
|
|
12
|
+
if (path_cand / 'lush').exists():
|
|
13
|
+
p_str = str(path_cand)
|
|
14
|
+
if p_str not in sys.path:
|
|
15
|
+
sys.path.insert(0, p_str)
|
|
16
|
+
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
|
|
20
|
+
def ensure_audio_backend():
|
|
21
|
+
"""Detects if an audio player backend is available; offers auto-install if missing."""
|
|
22
|
+
if shutil.which("mpv") or shutil.which("ffplay") or shutil.which("cvlc"):
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
# No backend detected
|
|
26
|
+
pkg_managers = [
|
|
27
|
+
("pacman", "sudo pacman -S --noconfirm mpv cava"),
|
|
28
|
+
("apt-get", "sudo apt-get update && sudo apt-get install -y mpv cava"),
|
|
29
|
+
("dnf", "sudo dnf install -y mpv cava"),
|
|
30
|
+
("brew", "brew install mpv cava"),
|
|
31
|
+
("zypper", "sudo zypper install -y mpv cava"),
|
|
32
|
+
("apk", "sudo apk add mpv cava"),
|
|
33
|
+
("pkg", "pkg install -y mpv"),
|
|
34
|
+
("winget", "winget install mpv.mpv")
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
detected_cmd = None
|
|
38
|
+
for pm, cmd in pkg_managers:
|
|
39
|
+
if shutil.which(pm):
|
|
40
|
+
detected_cmd = cmd
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
if sys.stdin.isatty() and detected_cmd:
|
|
44
|
+
print("\033[1;95m┌────────────────────────────────────────────────────────────────────────┐\033[0m")
|
|
45
|
+
print("\033[1;95m│\033[0m \033[1;96mLUSH Setup:\033[0m Audio player backend (\033[1;93mmpv\033[0m) not detected. \033[1;95m│\033[0m")
|
|
46
|
+
print(f"\033[1;95m│\033[0m Auto-install via \033[1;92m{detected_cmd:<44}\033[0m \033[1;95m│\033[0m")
|
|
47
|
+
print("\033[1;95m└────────────────────────────────────────────────────────────────────────┘\033[0m")
|
|
48
|
+
try:
|
|
49
|
+
ans = input("Proceed with auto-install? [Y/n]: ").strip().lower()
|
|
50
|
+
if ans in ("", "y", "yes"):
|
|
51
|
+
print("\n\033[1;96mInstalling audio dependencies...\033[0m")
|
|
52
|
+
subprocess.run(detected_cmd, shell=True)
|
|
53
|
+
print("\033[1;92m✓ Installation complete! Launching LUSH...\033[0m\n")
|
|
54
|
+
except (KeyboardInterrupt, EOFError):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
ensure_audio_backend()
|
|
58
|
+
|
|
59
|
+
from lush.__main__ import main
|
|
60
|
+
|
|
61
|
+
if __name__ == '__main__':
|
|
62
|
+
main()
|
package/bin/lush.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn, execSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const binPath = path.join(__dirname, 'lush');
|
|
8
|
+
|
|
9
|
+
function getPythonExecutable() {
|
|
10
|
+
const candidates = process.platform === 'win32'
|
|
11
|
+
? ['python3', 'python', 'py']
|
|
12
|
+
: ['python3', 'python'];
|
|
13
|
+
|
|
14
|
+
for (const cmd of candidates) {
|
|
15
|
+
try {
|
|
16
|
+
const checkCmd = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`;
|
|
17
|
+
execSync(checkCmd, { stdio: 'ignore' });
|
|
18
|
+
return cmd;
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
return 'python3';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pythonCmd = getPythonExecutable();
|
|
25
|
+
|
|
26
|
+
const child = spawn(pythonCmd, [binPath, ...process.argv.slice(2)], {
|
|
27
|
+
stdio: 'inherit',
|
|
28
|
+
env: {
|
|
29
|
+
...process.env,
|
|
30
|
+
PYTHONPATH: path.join(__dirname, '..', 'src')
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.on('error', (err) => {
|
|
35
|
+
if (err.code === 'ENOENT') {
|
|
36
|
+
console.error('\x1b[1;95m┌────────────────────────────────────────────────────────────────────────┐\x1b[0m');
|
|
37
|
+
console.error('\x1b[1;95m│\x1b[0m \x1b[1;91m[ERROR]\x1b[0m Python 3 is required to run LUSH. \x1b[1;95m│\x1b[0m');
|
|
38
|
+
console.error('\x1b[1;95m│\x1b[0m Install via: \x1b[1;96msudo apt install python3 mpv cava\x1b[0m (or \x1b[1;96mbrew install python mpv cava\x1b[0m) \x1b[1;95m│\x1b[0m');
|
|
39
|
+
console.error('\x1b[1;95m└────────────────────────────────────────────────────────────────────────┘\x1b[0m');
|
|
40
|
+
} else {
|
|
41
|
+
console.error('\x1b[31m[ERROR]\x1b[0m Failed to start LUSH:', err.message);
|
|
42
|
+
}
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
child.on('exit', (code, signal) => {
|
|
47
|
+
process.exit(code !== null ? code : (signal ? 1 : 0));
|
|
48
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lushapp",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "A hyper-aesthetic terminal radio player with 560+ live stations, real-time CAVA DSP audio visualizers, ambient soundscapes, and artist discographies.",
|
|
5
|
+
"main": "bin/lush.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"lush": "bin/lush.js",
|
|
8
|
+
"lushapp": "bin/lush.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/lush.js",
|
|
12
|
+
"dev": "node bin/lush.js",
|
|
13
|
+
"build": "python3 -m compileall -q src/lush && npm pack --dry-run",
|
|
14
|
+
"pack": "npm pack",
|
|
15
|
+
"diag": "node bin/lush.js --test",
|
|
16
|
+
"test": "python3 tests/run_tests.py",
|
|
17
|
+
"clean": "node -e \"const fs=require('fs');fs.readdirSync('.').filter(f=>f.endsWith('.tgz')).forEach(f=>fs.unlinkSync(f))\"",
|
|
18
|
+
"postinstall": "node scripts/postinstall.js",
|
|
19
|
+
"version": "node scripts/sync-version.js && git add pyproject.toml setup.py src/lush/__init__.py",
|
|
20
|
+
"prepack": "python3 tests/run_tests.py",
|
|
21
|
+
"prepublishOnly": "python3 tests/run_tests.py"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"tui",
|
|
25
|
+
"radio",
|
|
26
|
+
"music",
|
|
27
|
+
"terminal",
|
|
28
|
+
"audio-player",
|
|
29
|
+
"visualizer",
|
|
30
|
+
"cava",
|
|
31
|
+
"cli",
|
|
32
|
+
"ambient",
|
|
33
|
+
"curses",
|
|
34
|
+
"python",
|
|
35
|
+
"lofi",
|
|
36
|
+
"synthwave"
|
|
37
|
+
],
|
|
38
|
+
"author": "pseudoshell",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/pseudoshell/lush.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/pseudoshell/lush/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/pseudoshell/lush#readme",
|
|
48
|
+
"files": [
|
|
49
|
+
"bin/",
|
|
50
|
+
"scripts/",
|
|
51
|
+
"src/lush/*.py",
|
|
52
|
+
"src/lush/data/*",
|
|
53
|
+
"LICENSE",
|
|
54
|
+
"README.md",
|
|
55
|
+
"pyproject.toml",
|
|
56
|
+
"setup.py"
|
|
57
|
+
],
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=14.0.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lushapp"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "A hyper-aesthetic terminal radio player with 560+ live stations, real-time CAVA DSP visualizers, and artist discographies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "pseudoshell" }]
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
|
+
"Environment :: Console :: Curses",
|
|
16
|
+
"Intended Audience :: End Users/Desktop",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Operating System :: Microsoft :: Windows",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.8",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Programming Language :: Python :: 3.14",
|
|
29
|
+
"Topic :: Multimedia :: Sound/Audio :: Players",
|
|
30
|
+
]
|
|
31
|
+
dependencies = [
|
|
32
|
+
"python-mpv>=0.5.2",
|
|
33
|
+
"pyperclip>=1.8.2",
|
|
34
|
+
"requests>=2.28.0",
|
|
35
|
+
"yt-dlp>=2024.1.0",
|
|
36
|
+
"windows-curses>=2.3.0; sys_platform == 'win32'",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
lush = "lush.__main__:main"
|
|
41
|
+
lushapp = "lush.__main__:main"
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://github.com/pseudoshell/lush"
|
|
45
|
+
Repository = "https://github.com/pseudoshell/lush.git"
|
|
46
|
+
Issues = "https://github.com/pseudoshell/lush/issues"
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["src"]
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.package-data]
|
|
52
|
+
lush = ["data/*.json", "data/*.conf"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
|
|
4
|
+
function which(cmd) {
|
|
5
|
+
try {
|
|
6
|
+
const checkCmd = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`;
|
|
7
|
+
execSync(checkCmd, { stdio: 'ignore' });
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const missing = [];
|
|
15
|
+
if (!which('python3') && !which('python')) missing.push('python3');
|
|
16
|
+
if (!which('mpv')) missing.push('mpv');
|
|
17
|
+
if (!which('cava')) missing.push('cava');
|
|
18
|
+
if (!which('ffmpeg')) missing.push('ffmpeg');
|
|
19
|
+
|
|
20
|
+
if (missing.length === 0) {
|
|
21
|
+
console.log('\x1b[1;92m✓ [LUSH]\x1b[0m All system dependencies (python3, mpv, cava, ffmpeg) are verified.');
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`\x1b[1;95m┌────────────────────────────────────────────────────────────────────────┐\x1b[0m`);
|
|
26
|
+
console.log(`\x1b[1;95m│\x1b[0m \x1b[1;96mLUSH Setup:\x1b[0m Missing dependencies detected: \x1b[1;93m${missing.join(', ')}\x1b[0m \x1b[1;95m│\x1b[0m`);
|
|
27
|
+
console.log(`\x1b[1;95m│\x1b[0m Attempting automated installation via system package manager... \x1b[1;95m│\x1b[0m`);
|
|
28
|
+
console.log(`\x1b[1;95m└────────────────────────────────────────────────────────────────────────┘\x1b[0m`);
|
|
29
|
+
|
|
30
|
+
let installCmd = null;
|
|
31
|
+
if (which('pacman')) {
|
|
32
|
+
installCmd = `sudo pacman -S --noconfirm --needed ${missing.join(' ')}`;
|
|
33
|
+
} else if (which('apt-get')) {
|
|
34
|
+
installCmd = `sudo apt-get update -qq && sudo apt-get install -y ${missing.join(' ')}`;
|
|
35
|
+
} else if (which('dnf')) {
|
|
36
|
+
installCmd = `sudo dnf install -y ${missing.join(' ')}`;
|
|
37
|
+
} else if (which('brew')) {
|
|
38
|
+
installCmd = `brew install ${missing.join(' ')}`;
|
|
39
|
+
} else if (which('zypper')) {
|
|
40
|
+
installCmd = `sudo zypper install -y ${missing.join(' ')}`;
|
|
41
|
+
} else if (which('apk')) {
|
|
42
|
+
installCmd = `sudo apk add ${missing.join(' ')}`;
|
|
43
|
+
} else if (which('winget')) {
|
|
44
|
+
installCmd = `winget install mpv.mpv`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (installCmd) {
|
|
48
|
+
try {
|
|
49
|
+
console.log(`\x1b[96m> Running: ${installCmd}\x1b[0m\n`);
|
|
50
|
+
execSync(installCmd, { stdio: 'inherit' });
|
|
51
|
+
console.log('\n\x1b[1;92m✓ [LUSH] Dependencies successfully installed! Ready to launch `lush`.\x1b[0m\n');
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.log(`\x1b[1;93m[NOTICE]\x1b[0m Automated installation required manual elevation.`);
|
|
54
|
+
console.log(`Run: \x1b[1;96m${installCmd}\x1b[0m to finalize setup.\n`);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
console.log(`\x1b[1;93m[NOTICE]\x1b[0m Please install missing tools (\x1b[1;96m${missing.join(', ')}\x1b[0m) with your package manager.\n`);
|
|
58
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
6
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'));
|
|
7
|
+
const newVer = pkg.version;
|
|
8
|
+
|
|
9
|
+
console.log(`\x1b[96m[LUSH Version Sync]\x1b[0m Syncing version \x1b[92mv${newVer}\x1b[0m across all manifests...`);
|
|
10
|
+
|
|
11
|
+
// 1. Update pyproject.toml
|
|
12
|
+
const pyprojectPath = path.join(rootDir, 'pyproject.toml');
|
|
13
|
+
if (fs.existsSync(pyprojectPath)) {
|
|
14
|
+
let content = fs.readFileSync(pyprojectPath, 'utf8');
|
|
15
|
+
content = content.replace(/^version\s*=\s*["'][^"']+["']/m, `version = "${newVer}"`);
|
|
16
|
+
fs.writeFileSync(pyprojectPath, content, 'utf8');
|
|
17
|
+
console.log(` ✓ Updated pyproject.toml -> ${newVer}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 2. Update setup.py
|
|
21
|
+
const setupPath = path.join(rootDir, 'setup.py');
|
|
22
|
+
if (fs.existsSync(setupPath)) {
|
|
23
|
+
let content = fs.readFileSync(setupPath, 'utf8');
|
|
24
|
+
content = content.replace(/version\s*=\s*["'][^"']+["']/m, `version="${newVer}"`);
|
|
25
|
+
fs.writeFileSync(setupPath, content, 'utf8');
|
|
26
|
+
console.log(` ✓ Updated setup.py -> ${newVer}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 3. Update src/lush/__init__.py
|
|
30
|
+
const initPath = path.join(rootDir, 'src', 'lush', '__init__.py');
|
|
31
|
+
if (fs.existsSync(initPath)) {
|
|
32
|
+
let content = fs.readFileSync(initPath, 'utf8');
|
|
33
|
+
content = content.replace(/__version__\s*=\s*["'][^"']+["']/m, `__version__ = "${newVer}"`);
|
|
34
|
+
fs.writeFileSync(initPath, content, 'utf8');
|
|
35
|
+
console.log(` ✓ Updated src/lush/__init__.py -> ${newVer}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log('\x1b[1;92m✓ All package versions synchronized successfully!\x1b[0m\n');
|
package/setup.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="lushapp",
|
|
5
|
+
version="2.0.0",
|
|
6
|
+
package_dir={"": "src"},
|
|
7
|
+
packages=find_packages(where="src"),
|
|
8
|
+
include_package_data=True,
|
|
9
|
+
package_data={"lush": ["data/*.json", "data/*.conf"]},
|
|
10
|
+
entry_points={
|
|
11
|
+
"console_scripts": [
|
|
12
|
+
"lush = lush.__main__:main",
|
|
13
|
+
"lushapp = lush.__main__:main",
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
install_requires=[
|
|
17
|
+
"python-mpv>=0.5.2",
|
|
18
|
+
"pyperclip>=1.8.2",
|
|
19
|
+
"requests>=2.28.0",
|
|
20
|
+
"yt-dlp>=2024.1.0",
|
|
21
|
+
"windows-curses>=2.3.0; sys_platform == 'win32'",
|
|
22
|
+
],
|
|
23
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
os.environ["ESCDELAY"] = "25"
|
|
3
|
+
|
|
4
|
+
__version__ = "2.0.0"
|
|
5
|
+
|
|
6
|
+
def run_app():
|
|
7
|
+
try:
|
|
8
|
+
import curses
|
|
9
|
+
except ImportError:
|
|
10
|
+
try:
|
|
11
|
+
import windows_curses as curses
|
|
12
|
+
except ImportError:
|
|
13
|
+
print("\033[1;91m[ERROR]\033[0m Python curses library is required to run LUSH.")
|
|
14
|
+
if os.name == "nt":
|
|
15
|
+
print("On Windows, install it via: \033[1;96mpip install windows-curses\033[0m")
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
import threading
|
|
19
|
+
from .state import PlayerState, metadata_loop
|
|
20
|
+
from .ui import curses_main
|
|
21
|
+
|
|
22
|
+
state = PlayerState()
|
|
23
|
+
t_meta = threading.Thread(target=metadata_loop, args=(state,), daemon=True)
|
|
24
|
+
t_meta.start()
|
|
25
|
+
|
|
26
|
+
curses.wrapper(curses_main, state)
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
run_app()
|
|
30
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# LUSH - CLI Main Entrypoint
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
from . import __version__, run_app
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
prog="lush",
|
|
9
|
+
description="LUSH - A hyper-aesthetic terminal radio player with 560+ live stations and real-time CAVA DSP visualizers.",
|
|
10
|
+
epilog="Keybindings: [Space] Play/Pause | [/] Search | [p] Now Playing | [d] Decks | [1-5] Music | [0] Radio | [v] Visualizers | [t] Themes | [e] Ambient | [Shift+R] Record | [q] Quit"
|
|
11
|
+
)
|
|
12
|
+
parser.add_argument("-v", "--version", action="version", version=f"LUSH v{__version__} (Audiophile Edition)")
|
|
13
|
+
parser.add_argument("--test", action="store_true", help="Run internal self-test diagnostics")
|
|
14
|
+
|
|
15
|
+
args, unknown = parser.parse_known_args()
|
|
16
|
+
|
|
17
|
+
if args.test:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
test_cands = [
|
|
20
|
+
Path(__file__).resolve().parent.parent.parent / "tests" / "run_tests.py",
|
|
21
|
+
Path.home() / "lush" / "tests" / "run_tests.py",
|
|
22
|
+
Path.cwd() / "tests" / "run_tests.py"
|
|
23
|
+
]
|
|
24
|
+
runner_path = next((p for p in test_cands if p.exists()), None)
|
|
25
|
+
if runner_path:
|
|
26
|
+
import subprocess
|
|
27
|
+
res = subprocess.run([sys.executable, str(runner_path)])
|
|
28
|
+
sys.exit(res.returncode)
|
|
29
|
+
else:
|
|
30
|
+
from .state import PlayerState
|
|
31
|
+
state = PlayerState()
|
|
32
|
+
stations = state.get_filtered_stations()
|
|
33
|
+
print(f"LUSH v{__version__} Self-Test:")
|
|
34
|
+
print(f" [OK] Station Catalog: {len(stations)} live streams loaded.")
|
|
35
|
+
state.shutdown()
|
|
36
|
+
print(" [OK] All core subsystems verified successfully.")
|
|
37
|
+
sys.exit(0)
|
|
38
|
+
|
|
39
|
+
run_app()
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|