flow-twinx 0.2.0__tar.gz → 0.4.0__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.2.0 → flow_twinx-0.4.0}/PKG-INFO +27 -29
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/README.md +25 -27
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Offline/commands.py +22 -14
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Offline/player.py +13 -5
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Online/commands.py +162 -40
- flow_twinx-0.4.0/flow_twinx/Online/player.py +122 -0
- flow_twinx-0.4.0/flow_twinx/Online/savan.py +48 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Online/youtube.py +1 -1
- flow_twinx-0.4.0/flow_twinx/config.py +146 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/help_detail.py +28 -6
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/imports.py +1 -1
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/main.py +29 -6
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/ping.py +0 -3
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/shortcuts.py +3 -0
- flow_twinx-0.4.0/flow_twinx/tui.py +27 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/PKG-INFO +27 -29
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/SOURCES.txt +1 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/pyproject.toml +2 -2
- flow_twinx-0.2.0/flow_twinx/Online/player.py +0 -71
- flow_twinx-0.2.0/flow_twinx/config.py +0 -38
- flow_twinx-0.2.0/flow_twinx/tui.py +0 -69
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Offline/__init__.py +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Offline/file.py +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Offline/youtube.py +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Online/__init__.py +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/__init__.py +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/dependency_links.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/entry_points.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/requires.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/top_level.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/requirements.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.4.0}/setup.cfg +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flow-twinx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A terminal music player with online and offline modes
|
|
5
|
-
Requires-Python: >=3.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
Requires-Dist: python-vlc
|
|
8
8
|
Requires-Dist: yt_dlp
|
|
@@ -13,7 +13,7 @@ A terminal-based music player with online streaming and offline library modes.
|
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- **Dual-mode operation** —
|
|
16
|
+
- **Dual-mode operation** — It will automatically detect internet and switch between online streaming and offline playback.
|
|
17
17
|
- **Online mode** — Search and stream audio from YouTube via `yt-dlp` and `python-vlc`
|
|
18
18
|
- **Offline mode** — Play local audio files with album support, search, and a liked-songs collection
|
|
19
19
|
- **Download** — Save tracks from YouTube to your local library with the `-d` flag
|
|
@@ -30,10 +30,9 @@ A terminal-based music player with online streaming and offline library modes.
|
|
|
30
30
|
## Installation
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
git clone https://github.com/
|
|
33
|
+
git clone https://github.com/Philast-015/Flow.git
|
|
34
34
|
cd flow
|
|
35
|
-
|
|
36
|
-
.venv\Scripts\pip install -r requirements.txt
|
|
35
|
+
uv run flow_twinx/main.py
|
|
37
36
|
```
|
|
38
37
|
|
|
39
38
|
Or through pip:
|
|
@@ -42,19 +41,17 @@ Or through pip:
|
|
|
42
41
|
pip install flow-twinx
|
|
43
42
|
```
|
|
44
43
|
|
|
45
|
-
### Note: Make sure you
|
|
44
|
+
### Note: Make sure you vlc installed or it will not play music.
|
|
46
45
|
|
|
47
46
|
## Usage
|
|
48
47
|
|
|
49
48
|
```bash
|
|
50
49
|
cd flow_twinx
|
|
51
|
-
|
|
50
|
+
uv run main.py
|
|
52
51
|
```
|
|
53
|
-
|
|
54
52
|
Or direct if you have used pypi
|
|
55
|
-
|
|
56
53
|
```bash
|
|
57
|
-
flow
|
|
54
|
+
flow
|
|
58
55
|
```
|
|
59
56
|
|
|
60
57
|
| Flag | Description |
|
|
@@ -62,6 +59,7 @@ flow <command> <flag>
|
|
|
62
59
|
| `-r` | Repeat mode (loop current track) |
|
|
63
60
|
| `-s` | Shuffle mode (random order) |
|
|
64
61
|
| `-d` | Download mode (save streamed audio to library) |
|
|
62
|
+
| `-i` | Use it in help command to show detailed help |
|
|
65
63
|
|
|
66
64
|
### Commands
|
|
67
65
|
|
|
@@ -74,6 +72,7 @@ flow <command> <flag>
|
|
|
74
72
|
| `download <name or #>` | Save a streamed song to the local library |
|
|
75
73
|
| `switch` | Toggle between online and offline mode |
|
|
76
74
|
| `help` | Show available commands |
|
|
75
|
+
| `help -i` | Show available commands with detailed explanation |
|
|
77
76
|
|
|
78
77
|
## Configuration
|
|
79
78
|
|
|
@@ -85,29 +84,28 @@ flow <command> <flag>
|
|
|
85
84
|
|
|
86
85
|
```
|
|
87
86
|
flow/
|
|
88
|
-
├── flow_twinx/
|
|
87
|
+
├── flow_twinx/
|
|
89
88
|
│ ├── __init__.py
|
|
90
|
-
│ ├── config.py
|
|
91
|
-
│ ├── imports.py
|
|
92
|
-
│ ├── main.py
|
|
93
|
-
│ ├── ping.py
|
|
94
|
-
│ ├── tui.py
|
|
95
|
-
│ ├── Online/
|
|
89
|
+
│ ├── config.py
|
|
90
|
+
│ ├── imports.py
|
|
91
|
+
│ ├── main.py
|
|
92
|
+
│ ├── ping.py
|
|
93
|
+
│ ├── tui.py
|
|
94
|
+
│ ├── Online/
|
|
96
95
|
│ │ ├── __init__.py
|
|
97
|
-
│ │ ├── commands.py
|
|
98
|
-
│ │ ├── player.py
|
|
99
|
-
│ │ └── youtube.py
|
|
100
|
-
│ └── Offline/
|
|
96
|
+
│ │ ├── commands.py
|
|
97
|
+
│ │ ├── player.py
|
|
98
|
+
│ │ └── youtube.py
|
|
99
|
+
│ └── Offline/
|
|
101
100
|
│ ├── __init__.py
|
|
102
|
-
│ ├── commands.py
|
|
103
|
-
│ ├── file.py
|
|
104
|
-
│ ├── player.py
|
|
105
|
-
│ └── youtube.py
|
|
106
|
-
├── pyproject.toml
|
|
107
|
-
├── requirements.txt # Dependencies (python-vlc, yt_dlp)
|
|
101
|
+
│ ├── commands.py
|
|
102
|
+
│ ├── file.py
|
|
103
|
+
│ ├── player.py
|
|
104
|
+
│ └── youtube.py
|
|
105
|
+
├── pyproject.toml
|
|
108
106
|
└── README.md
|
|
109
107
|
```
|
|
110
108
|
|
|
111
109
|
## License
|
|
112
110
|
|
|
113
|
-
|
|
111
|
+
Use however you want just mention me for inspiration.
|
|
@@ -4,7 +4,7 @@ A terminal-based music player with online streaming and offline library modes.
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Dual-mode operation** —
|
|
7
|
+
- **Dual-mode operation** — It will automatically detect internet and switch between online streaming and offline playback.
|
|
8
8
|
- **Online mode** — Search and stream audio from YouTube via `yt-dlp` and `python-vlc`
|
|
9
9
|
- **Offline mode** — Play local audio files with album support, search, and a liked-songs collection
|
|
10
10
|
- **Download** — Save tracks from YouTube to your local library with the `-d` flag
|
|
@@ -21,10 +21,9 @@ A terminal-based music player with online streaming and offline library modes.
|
|
|
21
21
|
## Installation
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
git clone https://github.com/
|
|
24
|
+
git clone https://github.com/Philast-015/Flow.git
|
|
25
25
|
cd flow
|
|
26
|
-
|
|
27
|
-
.venv\Scripts\pip install -r requirements.txt
|
|
26
|
+
uv run flow_twinx/main.py
|
|
28
27
|
```
|
|
29
28
|
|
|
30
29
|
Or through pip:
|
|
@@ -33,19 +32,17 @@ Or through pip:
|
|
|
33
32
|
pip install flow-twinx
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
### Note: Make sure you
|
|
35
|
+
### Note: Make sure you vlc installed or it will not play music.
|
|
37
36
|
|
|
38
37
|
## Usage
|
|
39
38
|
|
|
40
39
|
```bash
|
|
41
40
|
cd flow_twinx
|
|
42
|
-
|
|
41
|
+
uv run main.py
|
|
43
42
|
```
|
|
44
|
-
|
|
45
43
|
Or direct if you have used pypi
|
|
46
|
-
|
|
47
44
|
```bash
|
|
48
|
-
flow
|
|
45
|
+
flow
|
|
49
46
|
```
|
|
50
47
|
|
|
51
48
|
| Flag | Description |
|
|
@@ -53,6 +50,7 @@ flow <command> <flag>
|
|
|
53
50
|
| `-r` | Repeat mode (loop current track) |
|
|
54
51
|
| `-s` | Shuffle mode (random order) |
|
|
55
52
|
| `-d` | Download mode (save streamed audio to library) |
|
|
53
|
+
| `-i` | Use it in help command to show detailed help |
|
|
56
54
|
|
|
57
55
|
### Commands
|
|
58
56
|
|
|
@@ -65,6 +63,7 @@ flow <command> <flag>
|
|
|
65
63
|
| `download <name or #>` | Save a streamed song to the local library |
|
|
66
64
|
| `switch` | Toggle between online and offline mode |
|
|
67
65
|
| `help` | Show available commands |
|
|
66
|
+
| `help -i` | Show available commands with detailed explanation |
|
|
68
67
|
|
|
69
68
|
## Configuration
|
|
70
69
|
|
|
@@ -76,29 +75,28 @@ flow <command> <flag>
|
|
|
76
75
|
|
|
77
76
|
```
|
|
78
77
|
flow/
|
|
79
|
-
├── flow_twinx/
|
|
78
|
+
├── flow_twinx/
|
|
80
79
|
│ ├── __init__.py
|
|
81
|
-
│ ├── config.py
|
|
82
|
-
│ ├── imports.py
|
|
83
|
-
│ ├── main.py
|
|
84
|
-
│ ├── ping.py
|
|
85
|
-
│ ├── tui.py
|
|
86
|
-
│ ├── Online/
|
|
80
|
+
│ ├── config.py
|
|
81
|
+
│ ├── imports.py
|
|
82
|
+
│ ├── main.py
|
|
83
|
+
│ ├── ping.py
|
|
84
|
+
│ ├── tui.py
|
|
85
|
+
│ ├── Online/
|
|
87
86
|
│ │ ├── __init__.py
|
|
88
|
-
│ │ ├── commands.py
|
|
89
|
-
│ │ ├── player.py
|
|
90
|
-
│ │ └── youtube.py
|
|
91
|
-
│ └── Offline/
|
|
87
|
+
│ │ ├── commands.py
|
|
88
|
+
│ │ ├── player.py
|
|
89
|
+
│ │ └── youtube.py
|
|
90
|
+
│ └── Offline/
|
|
92
91
|
│ ├── __init__.py
|
|
93
|
-
│ ├── commands.py
|
|
94
|
-
│ ├── file.py
|
|
95
|
-
│ ├── player.py
|
|
96
|
-
│ └── youtube.py
|
|
97
|
-
├── pyproject.toml
|
|
98
|
-
├── requirements.txt # Dependencies (python-vlc, yt_dlp)
|
|
92
|
+
│ ├── commands.py
|
|
93
|
+
│ ├── file.py
|
|
94
|
+
│ ├── player.py
|
|
95
|
+
│ └── youtube.py
|
|
96
|
+
├── pyproject.toml
|
|
99
97
|
└── README.md
|
|
100
98
|
```
|
|
101
99
|
|
|
102
100
|
## License
|
|
103
101
|
|
|
104
|
-
|
|
102
|
+
Use however you want just mention me for inspiration.
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import random
|
|
2
|
-
from ..imports import config, merge_flags,
|
|
2
|
+
from ..imports import config, merge_flags, is_connected
|
|
3
3
|
from .. import shortcuts
|
|
4
4
|
from .. import help_detail
|
|
5
5
|
from . import file as lib
|
|
6
6
|
from . import player
|
|
7
7
|
|
|
8
|
+
P = config.Primary
|
|
9
|
+
S = config.Secondary
|
|
10
|
+
T = config.Tertiary
|
|
11
|
+
M = config.Muted
|
|
12
|
+
E = config.Red
|
|
13
|
+
G = config.Grey
|
|
14
|
+
R = config.Reset
|
|
15
|
+
|
|
16
|
+
m = lambda t: print(f"{M}{t}{R}")
|
|
17
|
+
e = lambda t: print(f"{E}{t}{R}")
|
|
18
|
+
i = lambda t: print(f"{P if config.Mode == 'Online' else S}{t}{R}")
|
|
19
|
+
|
|
8
20
|
try:
|
|
9
21
|
import readline
|
|
10
22
|
_HAS_READLINE = True
|
|
@@ -22,13 +34,10 @@ COMMANDS = {
|
|
|
22
34
|
"switch": "Switch to Online mode (checks connection)",
|
|
23
35
|
"help": "Show this help message",
|
|
24
36
|
"short": "Show/update command shortcuts",
|
|
37
|
+
"config": "Change primary/secondary/tertiary colors",
|
|
25
38
|
"exit": "Exit Flow",
|
|
26
39
|
}
|
|
27
40
|
|
|
28
|
-
m = tprint(color="grey", border="none")
|
|
29
|
-
e = tprint(color="red", border="none")
|
|
30
|
-
i = tprint(color="theme", border="none")
|
|
31
|
-
|
|
32
41
|
|
|
33
42
|
class _Completer:
|
|
34
43
|
def __init__(self):
|
|
@@ -84,6 +93,8 @@ def run(cmd: str, extra: list[str], args):
|
|
|
84
93
|
show_help(inf)
|
|
85
94
|
elif cmd == "short":
|
|
86
95
|
shortcuts.cmd_short(extra, m)
|
|
96
|
+
elif cmd == "config":
|
|
97
|
+
config.cmd_config(extra, args)
|
|
87
98
|
else:
|
|
88
99
|
e(f"Unknown command: {cmd}")
|
|
89
100
|
|
|
@@ -212,23 +223,20 @@ def list_library():
|
|
|
212
223
|
|
|
213
224
|
def switch_mode():
|
|
214
225
|
if is_connected():
|
|
215
|
-
|
|
226
|
+
pass
|
|
216
227
|
else:
|
|
217
228
|
e("No internet connection")
|
|
218
229
|
|
|
219
230
|
|
|
220
231
|
def show_help(inf=False):
|
|
221
|
-
_t = config.THEMES[config.THEME]["theme"]
|
|
222
|
-
_g = "\033[90m"
|
|
223
|
-
_r = "\033[0m"
|
|
224
232
|
if inf:
|
|
225
|
-
print(f"{
|
|
233
|
+
print(f"{T}Offline Commands (detailed):{R}")
|
|
226
234
|
for cmd, lines in help_detail.OFFLINE_HELP.items():
|
|
227
235
|
for line in lines:
|
|
228
|
-
print(f" {line
|
|
236
|
+
print(f" {line}")
|
|
229
237
|
print()
|
|
230
238
|
else:
|
|
231
|
-
print(f"{
|
|
239
|
+
print(f"{T}Offline Commands:{R}")
|
|
232
240
|
for cmd, desc in COMMANDS.items():
|
|
233
|
-
print(f" {
|
|
234
|
-
print(f"{
|
|
241
|
+
print(f" {T}{cmd:12s}{R} {G}{desc}{R}")
|
|
242
|
+
print(f"{G} Use 'help -i' for detailed usage{R}")
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import time
|
|
3
3
|
import vlc
|
|
4
|
-
from ..imports import
|
|
4
|
+
from ..imports import config
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
P = config.Primary
|
|
7
|
+
S = config.Secondary
|
|
8
|
+
T = config.Tertiary
|
|
9
|
+
M = config.Muted
|
|
10
|
+
E = config.Red
|
|
11
|
+
R = config.Reset
|
|
12
|
+
|
|
13
|
+
m = lambda t: print(f"{M}{t}{R}")
|
|
14
|
+
e = lambda t: print(f"{E}{t}{R}")
|
|
15
|
+
i = lambda t: print(f"{P if config.Mode == 'Online' else S}{t}{R}")
|
|
16
|
+
t = lambda t: print(f"{T}{t}{R}")
|
|
9
17
|
|
|
10
18
|
BAR_WIDTH = 40
|
|
11
19
|
|
|
@@ -48,7 +56,7 @@ def play_file(filepath, title, args=None):
|
|
|
48
56
|
while player.get_state() not in (vlc.State.Ended, vlc.State.Error):
|
|
49
57
|
elapsed = time.time() - start
|
|
50
58
|
bar = _progress_bar(elapsed, duration)
|
|
51
|
-
sys.stdout.write(f"\r
|
|
59
|
+
sys.stdout.write(f"\r{S}{bar}{R}")
|
|
52
60
|
sys.stdout.flush()
|
|
53
61
|
time.sleep(0.5)
|
|
54
62
|
except KeyboardInterrupt:
|
|
@@ -1,33 +1,60 @@
|
|
|
1
1
|
import pathlib
|
|
2
2
|
import random
|
|
3
|
+
import signal
|
|
3
4
|
import sys
|
|
5
|
+
import termios
|
|
4
6
|
import threading
|
|
5
7
|
import time
|
|
6
|
-
from ..imports import config, merge_flags
|
|
8
|
+
from ..imports import config, merge_flags
|
|
7
9
|
from .. import shortcuts
|
|
8
10
|
from .. import help_detail
|
|
9
11
|
from . import youtube
|
|
10
12
|
from . import player
|
|
13
|
+
from . import savan
|
|
14
|
+
|
|
15
|
+
P = config.Primary
|
|
16
|
+
S = config.Secondary
|
|
17
|
+
T = config.Tertiary
|
|
18
|
+
M = config.Muted
|
|
19
|
+
E = config.Red
|
|
20
|
+
G = config.Grey
|
|
21
|
+
R = config.Reset
|
|
22
|
+
|
|
23
|
+
m = lambda t: print(f"{M}{t}{R}")
|
|
24
|
+
e = lambda t: print(f"{E}{t}{R}")
|
|
25
|
+
i = lambda t: print(f"{P if config.Mode == 'Online' else S}{t}{R}")
|
|
11
26
|
|
|
12
27
|
_last_results = []
|
|
13
28
|
_last_played = None
|
|
14
29
|
|
|
30
|
+
_radio_quit = False
|
|
31
|
+
_radio_skip = False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _radio_sigint(sig, frame):
|
|
35
|
+
global _radio_skip
|
|
36
|
+
_radio_skip = True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _radio_sigquit(sig, frame):
|
|
40
|
+
global _radio_quit
|
|
41
|
+
_radio_quit = True
|
|
42
|
+
|
|
15
43
|
COMMANDS = {
|
|
16
44
|
"play": "Play a song from YouTube",
|
|
17
45
|
"search": "Search YouTube for tracks",
|
|
46
|
+
"savan": "Play a song from JioSaavn (alias: svn)",
|
|
47
|
+
"savan-s": "Search JioSaavn for tracks (alias: svn-s)",
|
|
18
48
|
"radio": "Generate a radio mix from a reference song",
|
|
19
49
|
"like": "Like a song",
|
|
20
50
|
"download": "Download audio from YouTube",
|
|
21
51
|
"switch": "Switch to Offline mode",
|
|
22
52
|
"help": "Show this help message",
|
|
23
53
|
"short": "Show/update command shortcuts",
|
|
54
|
+
"config": "Change primary/secondary/tertiary colors",
|
|
24
55
|
"exit": "Exit Flow",
|
|
25
56
|
}
|
|
26
57
|
|
|
27
|
-
m = tprint(color="grey", border="none")
|
|
28
|
-
e = tprint(color="red", border="none")
|
|
29
|
-
i = tprint(color="theme", border="none")
|
|
30
|
-
|
|
31
58
|
def run(cmd: str, extra: list[str], args):
|
|
32
59
|
cmd = shortcuts.resolve(cmd)
|
|
33
60
|
inf = "-i" in extra
|
|
@@ -36,6 +63,10 @@ def run(cmd: str, extra: list[str], args):
|
|
|
36
63
|
play(extra, args)
|
|
37
64
|
elif cmd == "search":
|
|
38
65
|
search(" ".join(extra) if extra else "")
|
|
66
|
+
elif cmd in ("savan", "svn"):
|
|
67
|
+
savan_cmd(extra, args)
|
|
68
|
+
elif cmd in ("savan-s", "svn-s"):
|
|
69
|
+
savan_search(" ".join(extra) if extra else "")
|
|
39
70
|
elif cmd == "like":
|
|
40
71
|
like_track()
|
|
41
72
|
elif cmd == "download":
|
|
@@ -48,6 +79,8 @@ def run(cmd: str, extra: list[str], args):
|
|
|
48
79
|
radio(extra, args)
|
|
49
80
|
elif cmd == "short":
|
|
50
81
|
shortcuts.cmd_short(extra, m)
|
|
82
|
+
elif cmd == "config":
|
|
83
|
+
config.cmd_config(extra, args)
|
|
51
84
|
else:
|
|
52
85
|
print(f"Unknown command: {cmd}")
|
|
53
86
|
|
|
@@ -56,7 +89,7 @@ def _spinner(stop):
|
|
|
56
89
|
chars = "|/-\\"
|
|
57
90
|
i = 0
|
|
58
91
|
while not stop():
|
|
59
|
-
sys.stdout.write(f"\r
|
|
92
|
+
sys.stdout.write(f"\r{P}Searching... {chars[i]}{R}")
|
|
60
93
|
sys.stdout.flush()
|
|
61
94
|
time.sleep(0.1)
|
|
62
95
|
i = (i + 1) % len(chars)
|
|
@@ -134,6 +167,75 @@ def _play_liked(args):
|
|
|
134
167
|
_last_played = (entry, title)
|
|
135
168
|
player.play_entry(entry, title, args)
|
|
136
169
|
|
|
170
|
+
_savan_results = []
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def savan_cmd(extra, args):
|
|
174
|
+
global _savan_results, _last_played
|
|
175
|
+
arg = " ".join(extra) if extra else None
|
|
176
|
+
if not arg:
|
|
177
|
+
e("No song specified")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
if arg.isdigit():
|
|
181
|
+
idx = int(arg) - 1
|
|
182
|
+
if idx < 0 or idx >= len(_savan_results):
|
|
183
|
+
e("Index out of range")
|
|
184
|
+
return
|
|
185
|
+
entry, title, dur = _savan_results[idx]
|
|
186
|
+
else:
|
|
187
|
+
stop = False
|
|
188
|
+
t = threading.Thread(target=_spinner, args=(lambda: stop,), daemon=True)
|
|
189
|
+
t.start()
|
|
190
|
+
_savan_results = savan.search(arg)
|
|
191
|
+
stop = True
|
|
192
|
+
t.join()
|
|
193
|
+
if not _savan_results:
|
|
194
|
+
e("No results found")
|
|
195
|
+
return
|
|
196
|
+
entry, title, dur = _savan_results[0]
|
|
197
|
+
|
|
198
|
+
url = savan.best_url(entry)
|
|
199
|
+
if not url:
|
|
200
|
+
e("No playable URL found")
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
if getattr(args, "d", False):
|
|
204
|
+
safe = "".join(c if c.isalnum() or c in " -_" else "" for c in title).strip()
|
|
205
|
+
dest = config.DOWNLOAD_DIR / f"{safe}.mp4"
|
|
206
|
+
config.DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
m(f"\n Downloading {title}...")
|
|
208
|
+
try:
|
|
209
|
+
savan.download(url, str(dest))
|
|
210
|
+
except Exception as ex:
|
|
211
|
+
e(f"Download failed: {ex}")
|
|
212
|
+
return
|
|
213
|
+
i(f" Downloaded: {title} -> {dest}")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
_last_played = (entry, title)
|
|
217
|
+
player.play_url(url, title, args, dur)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def savan_search(query):
|
|
221
|
+
global _savan_results
|
|
222
|
+
if not query:
|
|
223
|
+
e("Search query required")
|
|
224
|
+
return
|
|
225
|
+
stop = False
|
|
226
|
+
t = threading.Thread(target=_spinner, args=(lambda: stop,), daemon=True)
|
|
227
|
+
t.start()
|
|
228
|
+
_savan_results = savan.search(query)
|
|
229
|
+
stop = True
|
|
230
|
+
t.join()
|
|
231
|
+
if not _savan_results:
|
|
232
|
+
e("No results found")
|
|
233
|
+
return
|
|
234
|
+
for i, (_, title, dur) in enumerate(_savan_results, 1):
|
|
235
|
+
mins, secs = divmod(int(dur), 60)
|
|
236
|
+
m(f" {i}. {title} ({mins}:{secs:02d})")
|
|
237
|
+
|
|
238
|
+
|
|
137
239
|
def like_track():
|
|
138
240
|
global _last_played
|
|
139
241
|
if not _last_played:
|
|
@@ -196,30 +298,55 @@ def radio(extra, args):
|
|
|
196
298
|
playlist_dir.mkdir(parents=True, exist_ok=True)
|
|
197
299
|
m(f"\tDownloading to {playlist_dir}")
|
|
198
300
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
301
|
+
global _radio_quit, _radio_skip
|
|
302
|
+
_radio_quit = False
|
|
303
|
+
_radio_skip = False
|
|
304
|
+
|
|
305
|
+
old_sigint = signal.signal(signal.SIGINT, _radio_sigint)
|
|
306
|
+
old_sigquit = signal.signal(signal.SIGQUIT, _radio_sigquit)
|
|
307
|
+
|
|
308
|
+
fd = sys.stdin.fileno()
|
|
309
|
+
old_term = None
|
|
310
|
+
try:
|
|
311
|
+
old_term = termios.tcgetattr(fd)
|
|
312
|
+
new = termios.tcgetattr(fd)
|
|
313
|
+
new[0] &= ~termios.IXON
|
|
314
|
+
new[6][termios.VQUIT] = 0x11 # Ctrl+Q -> SIGQUIT
|
|
315
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, new)
|
|
316
|
+
except (termios.error, OSError):
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
flags = {"quit": lambda: _radio_quit, "skip": lambda: _radio_skip}
|
|
320
|
+
|
|
321
|
+
idx = 0
|
|
322
|
+
try:
|
|
323
|
+
while idx < len(tracks) and not _radio_quit:
|
|
324
|
+
title, vid, dur = tracks[idx]
|
|
325
|
+
url = f"https://www.youtube.com/watch?v={vid}"
|
|
326
|
+
entry = youtube.get_entry(url)
|
|
327
|
+
short = _truncate_title(title)
|
|
328
|
+
mins, secs = divmod(int(dur), 60)
|
|
329
|
+
|
|
330
|
+
if idx + 1 < len(tracks):
|
|
331
|
+
n_title, n_vid, n_dur = tracks[idx + 1]
|
|
332
|
+
n_short = _truncate_title(n_title)
|
|
333
|
+
n_mins, n_secs = divmod(int(n_dur), 60)
|
|
334
|
+
print(f"{T}\n\t⥤ Next: {n_short:30s} {n_mins}:{n_secs:02d}{R}")
|
|
335
|
+
|
|
336
|
+
filepath = None
|
|
337
|
+
if playlist_dir:
|
|
338
|
+
m(f"\tDownloading {short}...")
|
|
339
|
+
filepath = youtube.download_url(url, str(playlist_dir))
|
|
340
|
+
m(f"\tDownloaded to {filepath}")
|
|
341
|
+
|
|
342
|
+
_radio_skip = False
|
|
343
|
+
player.play_entry(entry, title, args, filepath, flags=flags)
|
|
344
|
+
idx += 1
|
|
345
|
+
finally:
|
|
346
|
+
if old_term:
|
|
347
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_term)
|
|
348
|
+
signal.signal(signal.SIGINT, old_sigint)
|
|
349
|
+
signal.signal(signal.SIGQUIT, old_sigquit)
|
|
223
350
|
|
|
224
351
|
|
|
225
352
|
def download(extra: list[str]):
|
|
@@ -259,22 +386,17 @@ def download(extra: list[str]):
|
|
|
259
386
|
|
|
260
387
|
def switch_mode():
|
|
261
388
|
config.Mode = "Offline"
|
|
262
|
-
config.THEME = config.OFFLINE_THEME
|
|
263
|
-
m("Switched to Offline mode")
|
|
264
389
|
|
|
265
390
|
|
|
266
391
|
def show_help(inf=False):
|
|
267
|
-
_t = config.THEMES[config.THEME]["theme"]
|
|
268
|
-
_g = "\033[90m"
|
|
269
|
-
_r = "\033[0m"
|
|
270
392
|
if inf:
|
|
271
|
-
print(f"{
|
|
393
|
+
print(f"{T}Online Commands (detailed):{R}")
|
|
272
394
|
for cmd, lines in help_detail.ONLINE_HELP.items():
|
|
273
395
|
for line in lines:
|
|
274
|
-
print(f" {line
|
|
396
|
+
print(f" {line}")
|
|
275
397
|
print()
|
|
276
398
|
else:
|
|
277
|
-
print(f"{
|
|
399
|
+
print(f"{T}Online Commands:{R}")
|
|
278
400
|
for cmd, desc in COMMANDS.items():
|
|
279
|
-
print(f" {
|
|
280
|
-
print(f"{
|
|
401
|
+
print(f" {T}{cmd:12s}{R} {G}{desc}{R}")
|
|
402
|
+
print(f"{G} Use 'help -i' for detailed usage{R}")
|