flow-twinx 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- flow_twinx-0.4.2/PKG-INFO +171 -0
- flow_twinx-0.4.2/README.md +157 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/Offline/commands.py +105 -33
- flow_twinx-0.4.2/flow_twinx/Offline/player.py +81 -0
- flow_twinx-0.4.2/flow_twinx/Online/commands.py +543 -0
- flow_twinx-0.4.2/flow_twinx/Online/player.py +140 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/Online/youtube.py +24 -13
- flow_twinx-0.4.2/flow_twinx/config.py +295 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/help_detail.py +24 -12
- flow_twinx-0.4.2/flow_twinx/lyrics.py +62 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/main.py +65 -19
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/ping.py +0 -4
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/tui.py +2 -1
- flow_twinx-0.4.2/flow_twinx/visualizer.py +127 -0
- flow_twinx-0.4.2/flow_twinx.egg-info/PKG-INFO +171 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx.egg-info/SOURCES.txt +2 -0
- flow_twinx-0.4.2/flow_twinx.egg-info/requires.txt +5 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/pyproject.toml +5 -1
- flow_twinx-0.4.2/requirements.txt +5 -0
- flow_twinx-0.4.0/PKG-INFO +0 -111
- flow_twinx-0.4.0/README.md +0 -102
- flow_twinx-0.4.0/flow_twinx/Offline/player.py +0 -72
- flow_twinx-0.4.0/flow_twinx/Online/commands.py +0 -402
- flow_twinx-0.4.0/flow_twinx/Online/player.py +0 -122
- flow_twinx-0.4.0/flow_twinx/config.py +0 -146
- flow_twinx-0.4.0/flow_twinx.egg-info/PKG-INFO +0 -111
- flow_twinx-0.4.0/flow_twinx.egg-info/requires.txt +0 -2
- flow_twinx-0.4.0/requirements.txt +0 -2
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/Offline/__init__.py +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/Offline/file.py +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/Offline/youtube.py +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/Online/__init__.py +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/Online/savan.py +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/__init__.py +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/imports.py +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx/shortcuts.py +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx.egg-info/dependency_links.txt +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx.egg-info/entry_points.txt +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/flow_twinx.egg-info/top_level.txt +0 -0
- {flow_twinx-0.4.0 → flow_twinx-0.4.2}/setup.cfg +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flow-twinx
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: A terminal music player with online and offline modes
|
|
5
|
+
Project-URL: Homepage, https://github.com/philast-015/flow/
|
|
6
|
+
Project-URL: Issues, https://github.com/philast-015/flow/issues
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: python-vlc
|
|
10
|
+
Requires-Dist: yt_dlp
|
|
11
|
+
Requires-Dist: ytmusicapi
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: sounddevice
|
|
14
|
+
|
|
15
|
+
# Flow
|
|
16
|
+
|
|
17
|
+
A terminal-based music player with online streaming and offline library modes.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Dual-mode operation** — It will automatically detect internet and switch between online streaming and offline playback.
|
|
22
|
+
- **Online mode** — Search and stream audio from YouTube via `yt-dlp` and `python-vlc`
|
|
23
|
+
- **Offline mode** — Play local audio files with album support, search, and a liked-songs collection
|
|
24
|
+
- **Download** — Save tracks from YouTube to your local library with the `-d` flag
|
|
25
|
+
- **Repeat & shuffle** — Loop tracks n times or play in random order
|
|
26
|
+
- **Like/unlike** — Save favorites to a dedicated playlist
|
|
27
|
+
- **Tab completion** — Auto-complete commands and song names in offline mode
|
|
28
|
+
- **Colored TUI** — Cyan theme for online, magenta for offline, with borders and banners
|
|
29
|
+
- **Background play** — Play music in background and return to your shell
|
|
30
|
+
- **Audio-reactive bars** — Real-time spectrum analyzer with configurable width, height, and spacing
|
|
31
|
+
- **Synced lyrics** — Display color-coded lyrics that scroll with the song
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
- Python 3
|
|
36
|
+
- [VLC](https://www.videolan.org/vlc/) media player (for `python-vlc` bindings)
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/Philast-015/Flow.git
|
|
42
|
+
cd flow
|
|
43
|
+
uv run flow_twinx/main.py
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or through pip:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install flow-twinx
|
|
50
|
+
flow
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Note: Make sure you vlc installed or it will not play music.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
flow
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
OR if you cloned the repo:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cd flow_twinx
|
|
65
|
+
uv run main.py
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Flags
|
|
69
|
+
|
|
70
|
+
| Flag | Description |
|
|
71
|
+
| ------- | --------------------------------------------------------------------- |
|
|
72
|
+
| `-bg` | Play in background and exit to shell |
|
|
73
|
+
| `-kill` | Kill all background VLC processes |
|
|
74
|
+
| `-i` | Use it in help command to show detailed help |
|
|
75
|
+
| `-s` | Use it shuffle or play random songs |
|
|
76
|
+
| `-i` | Use it to repeat songs no of time [ -t n ] [ -t ] ( n = no of times ) |
|
|
77
|
+
|
|
78
|
+
### Shell Mode
|
|
79
|
+
|
|
80
|
+
Run commands directly from your shell without entering interactive mode.
|
|
81
|
+
Play-like commands (`-pl`, `-rd`) automatically run in background.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
flow -pl never gonna give you up # play (auto-bg)
|
|
85
|
+
flow -rd daft punk # radio (auto-bg)
|
|
86
|
+
flow -sh daft punk # search (show results, exit)
|
|
87
|
+
flow -kill # kill VLC
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Also works with positional commands:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
flow play never gonna give you up
|
|
94
|
+
flow radio daft punk
|
|
95
|
+
flow search daft punk
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Shell shortcuts use your user-defined shortcuts with `-` prefix:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
flow -svn hello # svn → savan
|
|
102
|
+
flow -dl never gonna give you up # dl → download
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Commands
|
|
106
|
+
|
|
107
|
+
| Command | Description |
|
|
108
|
+
| ---------------------- | ------------------------------------------------- |
|
|
109
|
+
| `play <name or #>` | Play a song by name or search result number |
|
|
110
|
+
| `search <query>` | Search YouTube (online) or library (offline) |
|
|
111
|
+
| `list` | Show all songs, albums, or liked tracks |
|
|
112
|
+
| `like <name or #>` | Add a song to your liked collection |
|
|
113
|
+
| `download <name or #>` | Save a streamed song to the local library |
|
|
114
|
+
| `radio <name> [#]` | Generate a radio mix, or play specific track |
|
|
115
|
+
| `switch` | Toggle between online and offline mode |
|
|
116
|
+
| `help` | Show available commands |
|
|
117
|
+
| `help -i` | Show available commands with detailed explanation |
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
- Downloads and library are stored in `~/.flow/downloads/`
|
|
122
|
+
- Liked songs (online) are saved to `~/.flow/liked.txt`
|
|
123
|
+
- Liked songs (offline) are copied to `~/.flow/downloads/liked songs/`
|
|
124
|
+
- User shortcuts are stored in `~/.flow/shortcuts.json`
|
|
125
|
+
- Config file: `~/.flow/config.json`
|
|
126
|
+
|
|
127
|
+
### Config Options
|
|
128
|
+
|
|
129
|
+
| Target | Description | Range |
|
|
130
|
+
| ------------ | ---------------------------- | ------------------ |
|
|
131
|
+
| `primary` | Color for online songs | Any color |
|
|
132
|
+
| `secondary` | Color for offline songs | Any color |
|
|
133
|
+
| `tertiary` | Color for labels | Any color |
|
|
134
|
+
| `display` | Playback display mode | none, bars, lyrics |
|
|
135
|
+
| `barwidth` | Number of bars in visualizer | 4-80 |
|
|
136
|
+
| `barheight` | Height of bars | 2-16 |
|
|
137
|
+
| `barspacing` | Space between bars | 0-4 |
|
|
138
|
+
|
|
139
|
+
Usage: `config <target> <value>`
|
|
140
|
+
|
|
141
|
+
## Project Structure
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
flow/
|
|
145
|
+
├── flow_twinx/
|
|
146
|
+
│ ├── __init__.py
|
|
147
|
+
│ ├── config.py
|
|
148
|
+
│ ├── imports.py
|
|
149
|
+
│ ├── main.py
|
|
150
|
+
│ ├── ping.py
|
|
151
|
+
│ ├── tui.py
|
|
152
|
+
│ ├── lyrics.py # Synced lyrics fetching
|
|
153
|
+
│ ├── visualizer.py # Audio-reactive bars
|
|
154
|
+
│ ├── Online/
|
|
155
|
+
│ │ ├── __init__.py
|
|
156
|
+
│ │ ├── commands.py
|
|
157
|
+
│ │ ├── player.py
|
|
158
|
+
│ │ └── youtube.py
|
|
159
|
+
│ └── Offline/
|
|
160
|
+
│ ├── __init__.py
|
|
161
|
+
│ ├── commands.py
|
|
162
|
+
│ ├── file.py
|
|
163
|
+
│ ├── player.py
|
|
164
|
+
│ └── youtube.py
|
|
165
|
+
├── pyproject.toml
|
|
166
|
+
└── README.md
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
Use however you want just mention me for inspiration.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Flow
|
|
2
|
+
|
|
3
|
+
A terminal-based music player with online streaming and offline library modes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Dual-mode operation** — It will automatically detect internet and switch between online streaming and offline playback.
|
|
8
|
+
- **Online mode** — Search and stream audio from YouTube via `yt-dlp` and `python-vlc`
|
|
9
|
+
- **Offline mode** — Play local audio files with album support, search, and a liked-songs collection
|
|
10
|
+
- **Download** — Save tracks from YouTube to your local library with the `-d` flag
|
|
11
|
+
- **Repeat & shuffle** — Loop tracks n times or play in random order
|
|
12
|
+
- **Like/unlike** — Save favorites to a dedicated playlist
|
|
13
|
+
- **Tab completion** — Auto-complete commands and song names in offline mode
|
|
14
|
+
- **Colored TUI** — Cyan theme for online, magenta for offline, with borders and banners
|
|
15
|
+
- **Background play** — Play music in background and return to your shell
|
|
16
|
+
- **Audio-reactive bars** — Real-time spectrum analyzer with configurable width, height, and spacing
|
|
17
|
+
- **Synced lyrics** — Display color-coded lyrics that scroll with the song
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Python 3
|
|
22
|
+
- [VLC](https://www.videolan.org/vlc/) media player (for `python-vlc` bindings)
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git clone https://github.com/Philast-015/Flow.git
|
|
28
|
+
cd flow
|
|
29
|
+
uv run flow_twinx/main.py
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or through pip:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install flow-twinx
|
|
36
|
+
flow
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Note: Make sure you vlc installed or it will not play music.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
flow
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
OR if you cloned the repo:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd flow_twinx
|
|
51
|
+
uv run main.py
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Flags
|
|
55
|
+
|
|
56
|
+
| Flag | Description |
|
|
57
|
+
| ------- | --------------------------------------------------------------------- |
|
|
58
|
+
| `-bg` | Play in background and exit to shell |
|
|
59
|
+
| `-kill` | Kill all background VLC processes |
|
|
60
|
+
| `-i` | Use it in help command to show detailed help |
|
|
61
|
+
| `-s` | Use it shuffle or play random songs |
|
|
62
|
+
| `-i` | Use it to repeat songs no of time [ -t n ] [ -t ] ( n = no of times ) |
|
|
63
|
+
|
|
64
|
+
### Shell Mode
|
|
65
|
+
|
|
66
|
+
Run commands directly from your shell without entering interactive mode.
|
|
67
|
+
Play-like commands (`-pl`, `-rd`) automatically run in background.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
flow -pl never gonna give you up # play (auto-bg)
|
|
71
|
+
flow -rd daft punk # radio (auto-bg)
|
|
72
|
+
flow -sh daft punk # search (show results, exit)
|
|
73
|
+
flow -kill # kill VLC
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Also works with positional commands:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
flow play never gonna give you up
|
|
80
|
+
flow radio daft punk
|
|
81
|
+
flow search daft punk
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Shell shortcuts use your user-defined shortcuts with `-` prefix:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
flow -svn hello # svn → savan
|
|
88
|
+
flow -dl never gonna give you up # dl → download
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Commands
|
|
92
|
+
|
|
93
|
+
| Command | Description |
|
|
94
|
+
| ---------------------- | ------------------------------------------------- |
|
|
95
|
+
| `play <name or #>` | Play a song by name or search result number |
|
|
96
|
+
| `search <query>` | Search YouTube (online) or library (offline) |
|
|
97
|
+
| `list` | Show all songs, albums, or liked tracks |
|
|
98
|
+
| `like <name or #>` | Add a song to your liked collection |
|
|
99
|
+
| `download <name or #>` | Save a streamed song to the local library |
|
|
100
|
+
| `radio <name> [#]` | Generate a radio mix, or play specific track |
|
|
101
|
+
| `switch` | Toggle between online and offline mode |
|
|
102
|
+
| `help` | Show available commands |
|
|
103
|
+
| `help -i` | Show available commands with detailed explanation |
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
- Downloads and library are stored in `~/.flow/downloads/`
|
|
108
|
+
- Liked songs (online) are saved to `~/.flow/liked.txt`
|
|
109
|
+
- Liked songs (offline) are copied to `~/.flow/downloads/liked songs/`
|
|
110
|
+
- User shortcuts are stored in `~/.flow/shortcuts.json`
|
|
111
|
+
- Config file: `~/.flow/config.json`
|
|
112
|
+
|
|
113
|
+
### Config Options
|
|
114
|
+
|
|
115
|
+
| Target | Description | Range |
|
|
116
|
+
| ------------ | ---------------------------- | ------------------ |
|
|
117
|
+
| `primary` | Color for online songs | Any color |
|
|
118
|
+
| `secondary` | Color for offline songs | Any color |
|
|
119
|
+
| `tertiary` | Color for labels | Any color |
|
|
120
|
+
| `display` | Playback display mode | none, bars, lyrics |
|
|
121
|
+
| `barwidth` | Number of bars in visualizer | 4-80 |
|
|
122
|
+
| `barheight` | Height of bars | 2-16 |
|
|
123
|
+
| `barspacing` | Space between bars | 0-4 |
|
|
124
|
+
|
|
125
|
+
Usage: `config <target> <value>`
|
|
126
|
+
|
|
127
|
+
## Project Structure
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
flow/
|
|
131
|
+
├── flow_twinx/
|
|
132
|
+
│ ├── __init__.py
|
|
133
|
+
│ ├── config.py
|
|
134
|
+
│ ├── imports.py
|
|
135
|
+
│ ├── main.py
|
|
136
|
+
│ ├── ping.py
|
|
137
|
+
│ ├── tui.py
|
|
138
|
+
│ ├── lyrics.py # Synced lyrics fetching
|
|
139
|
+
│ ├── visualizer.py # Audio-reactive bars
|
|
140
|
+
│ ├── Online/
|
|
141
|
+
│ │ ├── __init__.py
|
|
142
|
+
│ │ ├── commands.py
|
|
143
|
+
│ │ ├── player.py
|
|
144
|
+
│ │ └── youtube.py
|
|
145
|
+
│ └── Offline/
|
|
146
|
+
│ ├── __init__.py
|
|
147
|
+
│ ├── commands.py
|
|
148
|
+
│ ├── file.py
|
|
149
|
+
│ ├── player.py
|
|
150
|
+
│ └── youtube.py
|
|
151
|
+
├── pyproject.toml
|
|
152
|
+
└── README.md
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
Use however you want just mention me for inspiration.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import random
|
|
2
3
|
from ..imports import config, merge_flags, is_connected
|
|
3
4
|
from .. import shortcuts
|
|
4
|
-
from .. import help_detail
|
|
5
5
|
from . import file as lib
|
|
6
6
|
from . import player
|
|
7
7
|
|
|
@@ -17,6 +17,20 @@ m = lambda t: print(f"{M}{t}{R}")
|
|
|
17
17
|
e = lambda t: print(f"{E}{t}{R}")
|
|
18
18
|
i = lambda t: print(f"{P if config.Mode == 'Online' else S}{t}{R}")
|
|
19
19
|
|
|
20
|
+
|
|
21
|
+
def _fork_bg(label):
|
|
22
|
+
config.kill_stored()
|
|
23
|
+
pid = os.fork()
|
|
24
|
+
if pid > 0:
|
|
25
|
+
config.save_pid(pid)
|
|
26
|
+
i(f"{label} in background (PID: {pid})")
|
|
27
|
+
return False
|
|
28
|
+
devnull = os.open(os.devnull, os.O_RDWR)
|
|
29
|
+
os.dup2(devnull, 0)
|
|
30
|
+
os.dup2(devnull, 1)
|
|
31
|
+
os.dup2(devnull, 2)
|
|
32
|
+
return True
|
|
33
|
+
|
|
20
34
|
try:
|
|
21
35
|
import readline
|
|
22
36
|
_HAS_READLINE = True
|
|
@@ -77,7 +91,6 @@ def _setup_completion():
|
|
|
77
91
|
def run(cmd: str, extra: list[str], args):
|
|
78
92
|
_setup_completion()
|
|
79
93
|
cmd = shortcuts.resolve(cmd)
|
|
80
|
-
inf = "-i" in extra
|
|
81
94
|
extra, args = merge_flags(extra, args)
|
|
82
95
|
if cmd == "play":
|
|
83
96
|
play(extra, args)
|
|
@@ -90,7 +103,7 @@ def run(cmd: str, extra: list[str], args):
|
|
|
90
103
|
elif cmd == "switch":
|
|
91
104
|
switch_mode()
|
|
92
105
|
elif cmd == "help":
|
|
93
|
-
show_help(
|
|
106
|
+
show_help()
|
|
94
107
|
elif cmd == "short":
|
|
95
108
|
shortcuts.cmd_short(extra, m)
|
|
96
109
|
elif cmd == "config":
|
|
@@ -137,41 +150,95 @@ def play(extra: list[str], args):
|
|
|
137
150
|
return
|
|
138
151
|
|
|
139
152
|
_last_played = song_path
|
|
153
|
+
if getattr(args, "bg", False):
|
|
154
|
+
if not _fork_bg("Now playing"):
|
|
155
|
+
return
|
|
140
156
|
player.play_file(song_path, song_path.stem, args)
|
|
141
157
|
|
|
142
158
|
|
|
143
159
|
def _play_liked(args):
|
|
160
|
+
global _last_played
|
|
144
161
|
liked = lib.get_liked_songs()
|
|
145
162
|
if not liked:
|
|
146
163
|
e("No liked songs yet")
|
|
147
164
|
return
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
songs = list(liked)
|
|
166
|
+
if getattr(args, "shuffle", False):
|
|
167
|
+
random.shuffle(songs)
|
|
168
|
+
repeat = getattr(args, "repeat", False)
|
|
169
|
+
repeat_count = getattr(args, "repeat_count", 0)
|
|
170
|
+
iteration = 0
|
|
171
|
+
if getattr(args, "bg", False):
|
|
172
|
+
if not _fork_bg("Playing liked songs"):
|
|
173
|
+
return
|
|
174
|
+
try:
|
|
175
|
+
while True:
|
|
176
|
+
for song in songs:
|
|
177
|
+
_last_played = song
|
|
178
|
+
player.play_file(song, song.stem, args)
|
|
179
|
+
if not repeat:
|
|
180
|
+
break
|
|
181
|
+
iteration += 1
|
|
182
|
+
if repeat_count > 0 and iteration >= repeat_count:
|
|
183
|
+
break
|
|
184
|
+
except KeyboardInterrupt:
|
|
185
|
+
pass
|
|
153
186
|
|
|
154
187
|
def _play_all(args):
|
|
155
|
-
|
|
156
|
-
|
|
188
|
+
global _last_played
|
|
189
|
+
all_songs = lib.get_all_songs()
|
|
190
|
+
if not all_songs:
|
|
157
191
|
e("No downloaded songs yet")
|
|
158
192
|
return
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
193
|
+
songs = list(all_songs)
|
|
194
|
+
if getattr(args, "shuffle", False):
|
|
195
|
+
random.shuffle(songs)
|
|
196
|
+
repeat = getattr(args, "repeat", False)
|
|
197
|
+
repeat_count = getattr(args, "repeat_count", 0)
|
|
198
|
+
iteration = 0
|
|
199
|
+
if getattr(args, "bg", False):
|
|
200
|
+
if not _fork_bg("Playing all songs"):
|
|
201
|
+
return
|
|
202
|
+
try:
|
|
203
|
+
while True:
|
|
204
|
+
for song in songs:
|
|
205
|
+
_last_played = song
|
|
206
|
+
player.play_file(song, song.stem, args)
|
|
207
|
+
if not repeat:
|
|
208
|
+
break
|
|
209
|
+
iteration += 1
|
|
210
|
+
if repeat_count > 0 and iteration >= repeat_count:
|
|
211
|
+
break
|
|
212
|
+
except KeyboardInterrupt:
|
|
213
|
+
pass
|
|
164
214
|
|
|
165
215
|
def _play_album(album: str, args):
|
|
216
|
+
global _last_played
|
|
166
217
|
songs = lib.get_album_songs(album)
|
|
167
218
|
if not songs:
|
|
168
219
|
e(f"No songs found in album '{album}'")
|
|
169
220
|
return
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
221
|
+
tracks = list(songs)
|
|
222
|
+
if getattr(args, "shuffle", False):
|
|
223
|
+
random.shuffle(tracks)
|
|
224
|
+
repeat = getattr(args, "repeat", False)
|
|
225
|
+
repeat_count = getattr(args, "repeat_count", 0)
|
|
226
|
+
iteration = 0
|
|
227
|
+
if getattr(args, "bg", False):
|
|
228
|
+
if not _fork_bg(f"Playing album: {album}"):
|
|
229
|
+
return
|
|
230
|
+
try:
|
|
231
|
+
while True:
|
|
232
|
+
for song in tracks:
|
|
233
|
+
_last_played = song
|
|
234
|
+
player.play_file(song, song.stem, args)
|
|
235
|
+
if not repeat:
|
|
236
|
+
break
|
|
237
|
+
iteration += 1
|
|
238
|
+
if repeat_count > 0 and iteration >= repeat_count:
|
|
239
|
+
break
|
|
240
|
+
except KeyboardInterrupt:
|
|
241
|
+
pass
|
|
175
242
|
|
|
176
243
|
|
|
177
244
|
def like_track():
|
|
@@ -223,20 +290,25 @@ def list_library():
|
|
|
223
290
|
|
|
224
291
|
def switch_mode():
|
|
225
292
|
if is_connected():
|
|
226
|
-
|
|
293
|
+
config.Mode = "Online"
|
|
227
294
|
else:
|
|
228
295
|
e("No internet connection")
|
|
229
296
|
|
|
230
297
|
|
|
231
|
-
def show_help(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
298
|
+
def show_help():
|
|
299
|
+
print(f"{T}Offline Commands:{R}")
|
|
300
|
+
for cmd, desc in COMMANDS.items():
|
|
301
|
+
print(f" {T}{cmd:12s}{R} {G}{desc}{R}")
|
|
302
|
+
print(f"\n{T}Config:{R}")
|
|
303
|
+
print(f" {T}config{R} {G}Configure colors, display mode, bars{R}")
|
|
304
|
+
print(f" {G}config help Show all config targets and colors{R}")
|
|
305
|
+
print(f" {G}config display Set display: none, bars, lyrics{R}")
|
|
306
|
+
print(f" {G}config primary Set primary color (online songs){R}")
|
|
307
|
+
print(f" {G}config secondary Set secondary color (offline songs){R}")
|
|
308
|
+
print(f" {G}config tertiary Set tertiary color (labels){R}")
|
|
309
|
+
print(f" {G}config barwidth Bar count: 4-80 (current: {config.BarWidth}){R}")
|
|
310
|
+
print(f" {G}config barheight Bar height: 2-16 (current: {config.BarHeight}){R}")
|
|
311
|
+
print(f" {G}config barspacing 0-4, min, fit, max (current: {config.BarSpacing}){R}")
|
|
312
|
+
print(f"\n{T}Display Modes:{R}")
|
|
313
|
+
print(f" {G}bars{R} Audio-reactive spectrum analyzer (needs audio output){R}")
|
|
314
|
+
print(f" {G}none{R} No display during playback{R}")
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import vlc
|
|
4
|
+
from ..imports import config
|
|
5
|
+
from .. import visualizer
|
|
6
|
+
|
|
7
|
+
P = config.Primary
|
|
8
|
+
S = config.Secondary
|
|
9
|
+
T = config.Tertiary
|
|
10
|
+
M = config.Muted
|
|
11
|
+
E = config.Red
|
|
12
|
+
R = config.Reset
|
|
13
|
+
|
|
14
|
+
m = lambda t: print(f"{M}{t}{R}")
|
|
15
|
+
e = lambda t: print(f"{E}{t}{R}")
|
|
16
|
+
i = lambda t: print(f"{P if config.Mode == 'Online' else S}{t}{R}")
|
|
17
|
+
t = lambda t: print(f"{T}{t}{R}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _flags_str(args):
|
|
21
|
+
if not args:
|
|
22
|
+
return ""
|
|
23
|
+
parts = []
|
|
24
|
+
if getattr(args, "repeat", False):
|
|
25
|
+
count = getattr(args, "repeat_count", 0)
|
|
26
|
+
parts.append(f"[Repeat:{'∞' if count < 0 else count}]")
|
|
27
|
+
if getattr(args, "shuffle", False):
|
|
28
|
+
parts.append("[Shuffle:On]")
|
|
29
|
+
return " ".join(parts)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _display_loop(player, duration=0):
|
|
33
|
+
display = config.Display
|
|
34
|
+
if display == "none":
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if display == "bars":
|
|
38
|
+
visualizer.start()
|
|
39
|
+
|
|
40
|
+
start = time.time()
|
|
41
|
+
try:
|
|
42
|
+
while player.get_state() not in (vlc.State.Ended, vlc.State.Error):
|
|
43
|
+
if display == "bars":
|
|
44
|
+
bar_str = visualizer.render(color=S, reset=R)
|
|
45
|
+
sys.stdout.write(f"\r{bar_str}")
|
|
46
|
+
sys.stdout.flush()
|
|
47
|
+
time.sleep(0.08)
|
|
48
|
+
finally:
|
|
49
|
+
if display == "bars":
|
|
50
|
+
visualizer.stop()
|
|
51
|
+
sys.stdout.write("\r" + " " * 60 + "\r\n")
|
|
52
|
+
sys.stdout.flush()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def play_file(filepath, title, args=None):
|
|
56
|
+
instance = vlc.Instance("--no-video --quiet")
|
|
57
|
+
player = instance.media_player_new()
|
|
58
|
+
media = instance.media_new(str(filepath))
|
|
59
|
+
player.set_media(media)
|
|
60
|
+
player.play()
|
|
61
|
+
|
|
62
|
+
duration = 0
|
|
63
|
+
for _ in range(50):
|
|
64
|
+
duration = player.get_length() / 1000
|
|
65
|
+
if duration > 0:
|
|
66
|
+
break
|
|
67
|
+
time.sleep(0.1)
|
|
68
|
+
if duration <= 0:
|
|
69
|
+
duration = 0
|
|
70
|
+
dur_min, dur_sec = divmod(int(duration), 60)
|
|
71
|
+
flags = _flags_str(args)
|
|
72
|
+
i(f"\nPlaying : {title}")
|
|
73
|
+
m(f" [{dur_min}:{dur_sec:02d}] {flags}" if flags else f" [{dur_min}:{dur_sec:02d}]")
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
_display_loop(player, duration=duration)
|
|
77
|
+
except KeyboardInterrupt:
|
|
78
|
+
player.stop()
|
|
79
|
+
sys.stdout.write("\n")
|
|
80
|
+
sys.stdout.flush()
|
|
81
|
+
raise
|