flow-twinx 0.4.2__tar.gz → 0.4.3__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 → flow_twinx-0.4.3}/PKG-INFO +11 -10
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/README.md +10 -9
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/Offline/commands.py +35 -34
- flow_twinx-0.4.3/flow_twinx/Offline/player.py +170 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/Online/commands.py +137 -23
- flow_twinx-0.4.3/flow_twinx/Online/player.py +274 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/Online/savan.py +17 -1
- flow_twinx-0.4.3/flow_twinx/Online/youtube.py +151 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/config.py +46 -2
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/help_detail.py +11 -8
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/main.py +16 -1
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/tui.py +7 -3
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx.egg-info/PKG-INFO +11 -10
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/pyproject.toml +1 -1
- flow_twinx-0.4.2/flow_twinx/Offline/player.py +0 -81
- flow_twinx-0.4.2/flow_twinx/Online/player.py +0 -140
- flow_twinx-0.4.2/flow_twinx/Online/youtube.py +0 -85
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/Offline/__init__.py +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/Offline/file.py +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/Offline/youtube.py +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/Online/__init__.py +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/__init__.py +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/imports.py +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/lyrics.py +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/ping.py +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/shortcuts.py +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx/visualizer.py +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx.egg-info/SOURCES.txt +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx.egg-info/dependency_links.txt +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx.egg-info/entry_points.txt +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx.egg-info/requires.txt +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/flow_twinx.egg-info/top_level.txt +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/requirements.txt +0 -0
- {flow_twinx-0.4.2 → flow_twinx-0.4.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flow-twinx
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: A terminal music player with online and offline modes
|
|
5
5
|
Project-URL: Homepage, https://github.com/philast-015/flow/
|
|
6
6
|
Project-URL: Issues, https://github.com/philast-015/flow/issues
|
|
@@ -73,7 +73,8 @@ uv run main.py
|
|
|
73
73
|
| `-kill` | Kill all background VLC processes |
|
|
74
74
|
| `-i` | Use it in help command to show detailed help |
|
|
75
75
|
| `-s` | Use it shuffle or play random songs |
|
|
76
|
-
| `-
|
|
76
|
+
| `-r` | Use it to repeat songs no of time [ -r n ] [ -r ] ( n = no of times ) |
|
|
77
|
+
| `-d` | Use it to download songs |
|
|
77
78
|
|
|
78
79
|
### Shell Mode
|
|
79
80
|
|
|
@@ -116,14 +117,6 @@ flow -dl never gonna give you up # dl → download
|
|
|
116
117
|
| `help` | Show available commands |
|
|
117
118
|
| `help -i` | Show available commands with detailed explanation |
|
|
118
119
|
|
|
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
120
|
### Config Options
|
|
128
121
|
|
|
129
122
|
| Target | Description | Range |
|
|
@@ -138,6 +131,14 @@ flow -dl never gonna give you up # dl → download
|
|
|
138
131
|
|
|
139
132
|
Usage: `config <target> <value>`
|
|
140
133
|
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
- Downloads and library are stored in `~/.flow/downloads/`
|
|
137
|
+
- Liked songs (online) are saved to `~/.flow/liked.txt`
|
|
138
|
+
- Liked songs (offline) are copied to `~/.flow/downloads/liked songs/`
|
|
139
|
+
- User shortcuts are stored in `~/.flow/shortcuts.json`
|
|
140
|
+
- Config file: `~/.flow/config.json`
|
|
141
|
+
|
|
141
142
|
## Project Structure
|
|
142
143
|
|
|
143
144
|
```
|
|
@@ -59,7 +59,8 @@ uv run main.py
|
|
|
59
59
|
| `-kill` | Kill all background VLC processes |
|
|
60
60
|
| `-i` | Use it in help command to show detailed help |
|
|
61
61
|
| `-s` | Use it shuffle or play random songs |
|
|
62
|
-
| `-
|
|
62
|
+
| `-r` | Use it to repeat songs no of time [ -r n ] [ -r ] ( n = no of times ) |
|
|
63
|
+
| `-d` | Use it to download songs |
|
|
63
64
|
|
|
64
65
|
### Shell Mode
|
|
65
66
|
|
|
@@ -102,14 +103,6 @@ flow -dl never gonna give you up # dl → download
|
|
|
102
103
|
| `help` | Show available commands |
|
|
103
104
|
| `help -i` | Show available commands with detailed explanation |
|
|
104
105
|
|
|
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
106
|
### Config Options
|
|
114
107
|
|
|
115
108
|
| Target | Description | Range |
|
|
@@ -124,6 +117,14 @@ flow -dl never gonna give you up # dl → download
|
|
|
124
117
|
|
|
125
118
|
Usage: `config <target> <value>`
|
|
126
119
|
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
- Downloads and library are stored in `~/.flow/downloads/`
|
|
123
|
+
- Liked songs (online) are saved to `~/.flow/liked.txt`
|
|
124
|
+
- Liked songs (offline) are copied to `~/.flow/downloads/liked songs/`
|
|
125
|
+
- User shortcuts are stored in `~/.flow/shortcuts.json`
|
|
126
|
+
- Config file: `~/.flow/config.json`
|
|
127
|
+
|
|
127
128
|
## Project Structure
|
|
128
129
|
|
|
129
130
|
```
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import random
|
|
3
|
-
|
|
4
|
-
from .. import shortcuts
|
|
3
|
+
|
|
4
|
+
from .. import help_detail, shortcuts
|
|
5
|
+
from ..imports import config, is_connected, merge_flags
|
|
5
6
|
from . import file as lib
|
|
6
7
|
from . import player
|
|
7
8
|
|
|
@@ -31,8 +32,10 @@ def _fork_bg(label):
|
|
|
31
32
|
os.dup2(devnull, 2)
|
|
32
33
|
return True
|
|
33
34
|
|
|
35
|
+
|
|
34
36
|
try:
|
|
35
37
|
import readline
|
|
38
|
+
|
|
36
39
|
_HAS_READLINE = True
|
|
37
40
|
except ImportError:
|
|
38
41
|
_HAS_READLINE = False
|
|
@@ -41,15 +44,15 @@ _last_results = []
|
|
|
41
44
|
_last_played = None
|
|
42
45
|
|
|
43
46
|
COMMANDS = {
|
|
44
|
-
"play":
|
|
45
|
-
"search":
|
|
46
|
-
"list":
|
|
47
|
-
"like":
|
|
48
|
-
"switch":
|
|
49
|
-
"help":
|
|
50
|
-
"short":
|
|
51
|
-
"config":
|
|
52
|
-
"exit":
|
|
47
|
+
"play": "Play song(s) from local library | all by play all | and liked",
|
|
48
|
+
"search": "Search local music library",
|
|
49
|
+
"list": "List local music library",
|
|
50
|
+
"like": "Like the currently playing song",
|
|
51
|
+
"switch": "Switch to Online mode (checks connection)",
|
|
52
|
+
"help": "Show this help message",
|
|
53
|
+
"short": "Show/update command shortcuts",
|
|
54
|
+
"config": "Change primary/secondary/tertiary colors",
|
|
55
|
+
"exit": "Exit Flow",
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
|
|
@@ -67,10 +70,9 @@ class _Completer:
|
|
|
67
70
|
songs = lib.get_song_names()
|
|
68
71
|
albums = lib.get_album_names()
|
|
69
72
|
all_names = songs + albums
|
|
70
|
-
options = sorted(
|
|
71
|
-
n for n in all_names
|
|
72
|
-
|
|
73
|
-
))
|
|
73
|
+
options = sorted(
|
|
74
|
+
set(n for n in all_names if n.lower().startswith(text.lower()))
|
|
75
|
+
)
|
|
74
76
|
else:
|
|
75
77
|
options = []
|
|
76
78
|
self.matches = options
|
|
@@ -85,12 +87,14 @@ def _setup_completion():
|
|
|
85
87
|
return
|
|
86
88
|
readline.set_completer(_Completer().complete)
|
|
87
89
|
readline.parse_and_bind("tab: complete")
|
|
88
|
-
readline.set_completer_delims(
|
|
90
|
+
readline.set_completer_delims(" \t\n;")
|
|
89
91
|
|
|
90
92
|
|
|
91
93
|
def run(cmd: str, extra: list[str], args):
|
|
92
94
|
_setup_completion()
|
|
93
95
|
cmd = shortcuts.resolve(cmd)
|
|
96
|
+
inf = "-i" in extra
|
|
97
|
+
extra = [x for x in extra if x != "-i"]
|
|
94
98
|
extra, args = merge_flags(extra, args)
|
|
95
99
|
if cmd == "play":
|
|
96
100
|
play(extra, args)
|
|
@@ -103,7 +107,7 @@ def run(cmd: str, extra: list[str], args):
|
|
|
103
107
|
elif cmd == "switch":
|
|
104
108
|
switch_mode()
|
|
105
109
|
elif cmd == "help":
|
|
106
|
-
show_help()
|
|
110
|
+
show_help(inf)
|
|
107
111
|
elif cmd == "short":
|
|
108
112
|
shortcuts.cmd_short(extra, m)
|
|
109
113
|
elif cmd == "config":
|
|
@@ -184,6 +188,7 @@ def _play_liked(args):
|
|
|
184
188
|
except KeyboardInterrupt:
|
|
185
189
|
pass
|
|
186
190
|
|
|
191
|
+
|
|
187
192
|
def _play_all(args):
|
|
188
193
|
global _last_played
|
|
189
194
|
all_songs = lib.get_all_songs()
|
|
@@ -212,6 +217,7 @@ def _play_all(args):
|
|
|
212
217
|
except KeyboardInterrupt:
|
|
213
218
|
pass
|
|
214
219
|
|
|
220
|
+
|
|
215
221
|
def _play_album(album: str, args):
|
|
216
222
|
global _last_played
|
|
217
223
|
songs = lib.get_album_songs(album)
|
|
@@ -295,20 +301,15 @@ def switch_mode():
|
|
|
295
301
|
e("No internet connection")
|
|
296
302
|
|
|
297
303
|
|
|
298
|
-
def show_help():
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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}")
|
|
304
|
+
def show_help(inf=False):
|
|
305
|
+
if inf:
|
|
306
|
+
print(f"{T}Offline Commands (detailed):{R}")
|
|
307
|
+
for cmd, lines in help_detail.OFFLINE_HELP.items():
|
|
308
|
+
for line in lines:
|
|
309
|
+
print(f" {line}")
|
|
310
|
+
print()
|
|
311
|
+
else:
|
|
312
|
+
print(f"{T}Offline Commands:{R}")
|
|
313
|
+
for cmd, desc in COMMANDS.items():
|
|
314
|
+
print(f" {T}{cmd:12s}{R} {G}{desc}{R}")
|
|
315
|
+
print(f"{G} Use 'help -i' for detailed usage{R}")
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import errno
|
|
2
|
+
import fcntl
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import sys
|
|
6
|
+
import termios
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import vlc
|
|
10
|
+
from ..imports import config
|
|
11
|
+
from .. import visualizer
|
|
12
|
+
|
|
13
|
+
P = config.Primary
|
|
14
|
+
S = config.Secondary
|
|
15
|
+
T = config.Tertiary
|
|
16
|
+
M = config.Muted
|
|
17
|
+
E = config.Red
|
|
18
|
+
R = config.Reset
|
|
19
|
+
|
|
20
|
+
m = lambda t: print(f"{M}{t}{R}")
|
|
21
|
+
e = lambda t: print(f"{E}{t}{R}")
|
|
22
|
+
i = lambda t: print(f"{P if config.Mode == 'Online' else S}{t}{R}")
|
|
23
|
+
t = lambda t: print(f"{T}{t}{R}")
|
|
24
|
+
|
|
25
|
+
_paused = False
|
|
26
|
+
_player = None
|
|
27
|
+
_original_term = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _sigusr1_toggle(sig, frame):
|
|
31
|
+
global _paused
|
|
32
|
+
_paused = not _paused
|
|
33
|
+
if _player:
|
|
34
|
+
_player.pause()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _setup_pause_input():
|
|
38
|
+
global _original_term
|
|
39
|
+
fd = sys.stdin.fileno()
|
|
40
|
+
try:
|
|
41
|
+
_original_term = termios.tcgetattr(fd)
|
|
42
|
+
new = termios.tcgetattr(fd)
|
|
43
|
+
new[0] &= ~termios.IXON
|
|
44
|
+
new[6][termios.VSUSP] = 0
|
|
45
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, new)
|
|
46
|
+
except (termios.error, OSError):
|
|
47
|
+
_original_term = None
|
|
48
|
+
return
|
|
49
|
+
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
50
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
|
51
|
+
signal.signal(signal.SIGUSR1, _sigusr1_toggle)
|
|
52
|
+
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
|
|
53
|
+
thread = threading.Thread(target=_input_reader, daemon=True)
|
|
54
|
+
thread.start()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _input_reader():
|
|
58
|
+
fd = sys.stdin.fileno()
|
|
59
|
+
try:
|
|
60
|
+
while True:
|
|
61
|
+
try:
|
|
62
|
+
ch = os.read(fd, 1)
|
|
63
|
+
except OSError as ex:
|
|
64
|
+
if ex.errno == errno.EAGAIN:
|
|
65
|
+
time.sleep(0.05)
|
|
66
|
+
continue
|
|
67
|
+
break
|
|
68
|
+
if ch == b"\x10":
|
|
69
|
+
os.kill(os.getpid(), signal.SIGUSR1)
|
|
70
|
+
except OSError:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _restore_pause_input():
|
|
75
|
+
global _original_term
|
|
76
|
+
if _original_term is not None:
|
|
77
|
+
try:
|
|
78
|
+
fd = sys.stdin.fileno()
|
|
79
|
+
flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
80
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
|
|
81
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, _original_term)
|
|
82
|
+
except (termios.error, OSError):
|
|
83
|
+
pass
|
|
84
|
+
_original_term = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _flags_str(args):
|
|
88
|
+
if not args:
|
|
89
|
+
return ""
|
|
90
|
+
parts = []
|
|
91
|
+
if getattr(args, "repeat", False):
|
|
92
|
+
count = getattr(args, "repeat_count", 0)
|
|
93
|
+
parts.append(f"[Repeat:{'∞' if count < 0 else count}]")
|
|
94
|
+
if getattr(args, "shuffle", False):
|
|
95
|
+
parts.append("[Shuffle:On]")
|
|
96
|
+
return " ".join(parts)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _display_loop(player, duration=0):
|
|
100
|
+
global _paused
|
|
101
|
+
display = config.Display
|
|
102
|
+
if display == "none":
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if display == "bars":
|
|
106
|
+
visualizer.start()
|
|
107
|
+
|
|
108
|
+
start = time.time()
|
|
109
|
+
paused_printed = False
|
|
110
|
+
try:
|
|
111
|
+
while player.get_state() not in (vlc.State.Ended, vlc.State.Error):
|
|
112
|
+
if _paused:
|
|
113
|
+
if not paused_printed:
|
|
114
|
+
sys.stdout.write(f"\r {T}[Paused]{R} ")
|
|
115
|
+
sys.stdout.flush()
|
|
116
|
+
paused_printed = True
|
|
117
|
+
try:
|
|
118
|
+
time.sleep(0.1)
|
|
119
|
+
except OSError:
|
|
120
|
+
pass
|
|
121
|
+
continue
|
|
122
|
+
if paused_printed:
|
|
123
|
+
paused_printed = False
|
|
124
|
+
if display == "bars":
|
|
125
|
+
bar_str = visualizer.render(color=S, reset=R)
|
|
126
|
+
sys.stdout.write(f"\r{bar_str}")
|
|
127
|
+
sys.stdout.flush()
|
|
128
|
+
try:
|
|
129
|
+
time.sleep(0.08)
|
|
130
|
+
except OSError:
|
|
131
|
+
pass
|
|
132
|
+
finally:
|
|
133
|
+
if display == "bars":
|
|
134
|
+
visualizer.stop()
|
|
135
|
+
sys.stdout.write("\r" + " " * 60 + "\r\n")
|
|
136
|
+
sys.stdout.flush()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def play_file(filepath, title, args=None):
|
|
140
|
+
global _player, _paused
|
|
141
|
+
_paused = False
|
|
142
|
+
instance = vlc.Instance("--no-video --quiet")
|
|
143
|
+
_player = instance.media_player_new()
|
|
144
|
+
media = instance.media_new(str(filepath))
|
|
145
|
+
_player.set_media(media)
|
|
146
|
+
_player.play()
|
|
147
|
+
_setup_pause_input()
|
|
148
|
+
|
|
149
|
+
duration = 0
|
|
150
|
+
for _ in range(50):
|
|
151
|
+
duration = _player.get_length() / 1000
|
|
152
|
+
if duration > 0:
|
|
153
|
+
break
|
|
154
|
+
time.sleep(0.1)
|
|
155
|
+
if duration <= 0:
|
|
156
|
+
duration = 0
|
|
157
|
+
dur_min, dur_sec = divmod(int(duration), 60)
|
|
158
|
+
flags = _flags_str(args)
|
|
159
|
+
i(f"\nPlaying : {title}")
|
|
160
|
+
m(f" [{dur_min}:{dur_sec:02d}] {flags}" if flags else f" [{dur_min}:{dur_sec:02d}]")
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
_display_loop(_player, duration=duration)
|
|
164
|
+
except KeyboardInterrupt:
|
|
165
|
+
_player.stop()
|
|
166
|
+
sys.stdout.write("\n")
|
|
167
|
+
sys.stdout.flush()
|
|
168
|
+
raise
|
|
169
|
+
finally:
|
|
170
|
+
_restore_pause_input()
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import errno
|
|
2
|
+
import fcntl
|
|
1
3
|
import os
|
|
2
4
|
import random
|
|
3
5
|
import signal
|
|
@@ -6,7 +8,9 @@ import termios
|
|
|
6
8
|
import threading
|
|
7
9
|
import time
|
|
8
10
|
|
|
9
|
-
from
|
|
11
|
+
from flow_twinx.config import MAX_SEARCH_RESULTS
|
|
12
|
+
|
|
13
|
+
from .. import help_detail, shortcuts
|
|
10
14
|
from ..imports import config, merge_flags
|
|
11
15
|
from . import player, savan, youtube
|
|
12
16
|
|
|
@@ -72,6 +76,8 @@ COMMANDS = {
|
|
|
72
76
|
|
|
73
77
|
def run(cmd: str, extra: list[str], args):
|
|
74
78
|
cmd = shortcuts.resolve(cmd)
|
|
79
|
+
inf = "-i" in extra
|
|
80
|
+
extra = [x for x in extra if x != "-i"]
|
|
75
81
|
extra, args = merge_flags(extra, args)
|
|
76
82
|
if cmd == "play":
|
|
77
83
|
play(extra, args)
|
|
@@ -88,7 +94,7 @@ def run(cmd: str, extra: list[str], args):
|
|
|
88
94
|
elif cmd == "switch":
|
|
89
95
|
switch_mode()
|
|
90
96
|
elif cmd == "help":
|
|
91
|
-
show_help()
|
|
97
|
+
show_help(inf)
|
|
92
98
|
elif cmd in ("radio", "rd"):
|
|
93
99
|
radio(extra, args)
|
|
94
100
|
elif cmd == "short":
|
|
@@ -116,7 +122,7 @@ def _do_search(query):
|
|
|
116
122
|
stop = False
|
|
117
123
|
t = threading.Thread(target=_spinner, args=(lambda: stop,), daemon=True)
|
|
118
124
|
t.start()
|
|
119
|
-
_last_results = youtube.search(query)
|
|
125
|
+
_last_results = youtube.search(query, MAX_SEARCH_RESULTS)
|
|
120
126
|
stop = True
|
|
121
127
|
t.join()
|
|
122
128
|
|
|
@@ -130,6 +136,10 @@ def play(extra: list[str], args):
|
|
|
130
136
|
print("No song specified")
|
|
131
137
|
return
|
|
132
138
|
|
|
139
|
+
if getattr(args, "download", False):
|
|
140
|
+
download(extra)
|
|
141
|
+
return
|
|
142
|
+
|
|
133
143
|
if arg == "liked":
|
|
134
144
|
_play_liked(args)
|
|
135
145
|
return
|
|
@@ -332,6 +342,16 @@ def like_track():
|
|
|
332
342
|
if not url:
|
|
333
343
|
e(" No URL for current song")
|
|
334
344
|
return
|
|
345
|
+
config.dev_print(
|
|
346
|
+
"Like Track",
|
|
347
|
+
{
|
|
348
|
+
"title": title,
|
|
349
|
+
"url": url,
|
|
350
|
+
"video_id": entry.get("id"),
|
|
351
|
+
"webpage_url": entry.get("webpage_url"),
|
|
352
|
+
"original_url": entry.get("original_url"),
|
|
353
|
+
},
|
|
354
|
+
)
|
|
335
355
|
config.liked_music.parent.mkdir(parents=True, exist_ok=True)
|
|
336
356
|
existing = (
|
|
337
357
|
config.liked_music.read_text().strip().splitlines()
|
|
@@ -380,6 +400,15 @@ def radio(extra, args):
|
|
|
380
400
|
if _radio_tracks and 0 <= idx < len(_radio_tracks):
|
|
381
401
|
title, vid, dur = _radio_tracks[idx]
|
|
382
402
|
url = f"https://www.youtube.com/watch?v={vid}"
|
|
403
|
+
config.dev_print(
|
|
404
|
+
"Radio (play by index)",
|
|
405
|
+
{
|
|
406
|
+
"title": title,
|
|
407
|
+
"video_id": vid,
|
|
408
|
+
"url": url,
|
|
409
|
+
"duration": f"{dur}s",
|
|
410
|
+
},
|
|
411
|
+
)
|
|
383
412
|
entry = youtube.get_entry(url)
|
|
384
413
|
if getattr(args, "bg", False):
|
|
385
414
|
if not _fork_bg("Now playing"):
|
|
@@ -398,6 +427,15 @@ def radio(extra, args):
|
|
|
398
427
|
if _radio_tracks and 0 <= idx < len(_radio_tracks):
|
|
399
428
|
title, vid, dur = _radio_tracks[idx]
|
|
400
429
|
url = f"https://www.youtube.com/watch?v={vid}"
|
|
430
|
+
config.dev_print(
|
|
431
|
+
"Radio (play by query+index)",
|
|
432
|
+
{
|
|
433
|
+
"title": title,
|
|
434
|
+
"video_id": vid,
|
|
435
|
+
"url": url,
|
|
436
|
+
"duration": f"{dur}s",
|
|
437
|
+
},
|
|
438
|
+
)
|
|
401
439
|
entry = youtube.get_entry(url)
|
|
402
440
|
if getattr(args, "bg", False):
|
|
403
441
|
if not _fork_bg("Now playing"):
|
|
@@ -420,6 +458,23 @@ def radio(extra, args):
|
|
|
420
458
|
|
|
421
459
|
_radio_tracks = tracks
|
|
422
460
|
|
|
461
|
+
if getattr(args, "download", False):
|
|
462
|
+
for idx, (title, vid, dur) in enumerate(_radio_tracks, 1):
|
|
463
|
+
url = f"https://www.youtube.com/watch?v={vid}"
|
|
464
|
+
config.dev_print(
|
|
465
|
+
"Radio Download",
|
|
466
|
+
{
|
|
467
|
+
"title": title,
|
|
468
|
+
"video_id": vid,
|
|
469
|
+
"url": url,
|
|
470
|
+
"duration": f"{dur}s",
|
|
471
|
+
"progress": f"{idx}/{len(_radio_tracks)}",
|
|
472
|
+
},
|
|
473
|
+
)
|
|
474
|
+
filepath = youtube.download_url(url, config.DOWNLOAD_DIR)
|
|
475
|
+
i(f" Downloaded ({idx}/{len(_radio_tracks)}): {title}")
|
|
476
|
+
return
|
|
477
|
+
|
|
423
478
|
if getattr(args, "shuffle", False):
|
|
424
479
|
random.shuffle(_radio_tracks)
|
|
425
480
|
|
|
@@ -433,18 +488,48 @@ def radio(extra, args):
|
|
|
433
488
|
|
|
434
489
|
old_sigint = signal.signal(signal.SIGINT, _radio_sigint)
|
|
435
490
|
old_sigquit = signal.signal(signal.SIGQUIT, _radio_sigquit)
|
|
491
|
+
old_sigusr1 = signal.getsignal(signal.SIGUSR1)
|
|
436
492
|
|
|
437
493
|
fd = sys.stdin.fileno()
|
|
438
494
|
old_term = None
|
|
495
|
+
old_fd_flags = None
|
|
439
496
|
try:
|
|
440
497
|
old_term = termios.tcgetattr(fd)
|
|
441
498
|
new = termios.tcgetattr(fd)
|
|
442
499
|
new[0] &= ~termios.IXON
|
|
443
500
|
new[6][termios.VQUIT] = 0x11 # Ctrl+Q -> SIGQUIT
|
|
501
|
+
new[6][termios.VSUSP] = 0 # disable Ctrl+Z suspend
|
|
444
502
|
termios.tcsetattr(fd, termios.TCSADRAIN, new)
|
|
503
|
+
old_fd_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
|
|
504
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, old_fd_flags | os.O_NONBLOCK)
|
|
445
505
|
except (termios.error, OSError):
|
|
446
506
|
pass
|
|
447
507
|
|
|
508
|
+
def _radio_sigusr1(sig, frame):
|
|
509
|
+
player._sigusr1_toggle(sig, frame)
|
|
510
|
+
|
|
511
|
+
signal.signal(signal.SIGUSR1, _radio_sigusr1)
|
|
512
|
+
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
|
|
513
|
+
player._radio_active = True
|
|
514
|
+
|
|
515
|
+
def _radio_input_reader():
|
|
516
|
+
try:
|
|
517
|
+
while True:
|
|
518
|
+
try:
|
|
519
|
+
ch = os.read(fd, 1)
|
|
520
|
+
except OSError as ex:
|
|
521
|
+
if ex.errno == errno.EAGAIN:
|
|
522
|
+
time.sleep(0.05)
|
|
523
|
+
continue
|
|
524
|
+
break
|
|
525
|
+
if ch == b"\x10":
|
|
526
|
+
os.kill(os.getpid(), signal.SIGUSR1)
|
|
527
|
+
except OSError:
|
|
528
|
+
pass
|
|
529
|
+
|
|
530
|
+
radio_reader = threading.Thread(target=_radio_input_reader, daemon=True)
|
|
531
|
+
radio_reader.start()
|
|
532
|
+
|
|
448
533
|
flags = {"quit": lambda: _radio_quit, "skip": lambda: _radio_skip}
|
|
449
534
|
|
|
450
535
|
repeat = getattr(args, "repeat", False)
|
|
@@ -457,6 +542,16 @@ def radio(extra, args):
|
|
|
457
542
|
while idx < len(_radio_tracks) and not _radio_quit:
|
|
458
543
|
title, vid, dur = _radio_tracks[idx]
|
|
459
544
|
url = f"https://www.youtube.com/watch?v={vid}"
|
|
545
|
+
config.dev_print(
|
|
546
|
+
"Radio (playing track)",
|
|
547
|
+
{
|
|
548
|
+
"title": title,
|
|
549
|
+
"video_id": vid,
|
|
550
|
+
"url": url,
|
|
551
|
+
"duration": f"{dur}s",
|
|
552
|
+
"position": f"{idx + 1}/{len(_radio_tracks)}",
|
|
553
|
+
},
|
|
554
|
+
)
|
|
460
555
|
entry = youtube.get_entry(url)
|
|
461
556
|
short = _truncate_title(title)
|
|
462
557
|
mins, secs = divmod(int(dur), 60)
|
|
@@ -465,7 +560,7 @@ def radio(extra, args):
|
|
|
465
560
|
n_title, n_vid, n_dur = _radio_tracks[idx + 1]
|
|
466
561
|
n_short = _truncate_title(n_title)
|
|
467
562
|
n_mins, n_secs = divmod(int(n_dur), 60)
|
|
468
|
-
print(f"{T}\n\t⥤ Next: {n_short:30s} {n_mins}:{n_secs:02d}{R}")
|
|
563
|
+
print(f"{T}\n\t[⥤ Next: {n_short:30s} {n_mins}:{n_secs:02d}]{R}")
|
|
469
564
|
|
|
470
565
|
_radio_skip = False
|
|
471
566
|
player.play_entry(entry, title, args, flags=flags)
|
|
@@ -478,10 +573,17 @@ def radio(extra, args):
|
|
|
478
573
|
except KeyboardInterrupt:
|
|
479
574
|
_radio_quit = True
|
|
480
575
|
finally:
|
|
576
|
+
player._radio_active = False
|
|
481
577
|
if old_term is not None:
|
|
482
|
-
|
|
578
|
+
try:
|
|
579
|
+
if old_fd_flags is not None:
|
|
580
|
+
fcntl.fcntl(fd, fcntl.F_SETFL, old_fd_flags)
|
|
581
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_term)
|
|
582
|
+
except (termios.error, OSError):
|
|
583
|
+
pass
|
|
483
584
|
signal.signal(signal.SIGINT, old_sigint)
|
|
484
585
|
signal.signal(signal.SIGQUIT, old_sigquit)
|
|
586
|
+
signal.signal(signal.SIGUSR1, old_sigusr1)
|
|
485
587
|
|
|
486
588
|
|
|
487
589
|
def download(extra: list[str]):
|
|
@@ -502,6 +604,15 @@ def download(extra: list[str]):
|
|
|
502
604
|
e(" No URL found for this entry")
|
|
503
605
|
return
|
|
504
606
|
title = entry.get("title", "Unknown")
|
|
607
|
+
config.dev_print(
|
|
608
|
+
"Download (by index)",
|
|
609
|
+
{
|
|
610
|
+
"title": title,
|
|
611
|
+
"url": url,
|
|
612
|
+
"video_id": entry.get("id"),
|
|
613
|
+
"output_dir": str(config.DOWNLOAD_DIR),
|
|
614
|
+
},
|
|
615
|
+
)
|
|
505
616
|
filepath = youtube.download_url(url, config.DOWNLOAD_DIR)
|
|
506
617
|
i(f" Downloaded: {title} -> {filepath}")
|
|
507
618
|
else:
|
|
@@ -515,6 +626,15 @@ def download(extra: list[str]):
|
|
|
515
626
|
e(" No URL found for this entry")
|
|
516
627
|
return
|
|
517
628
|
title = entry.get("title", "Unknown")
|
|
629
|
+
config.dev_print(
|
|
630
|
+
"Download (by search)",
|
|
631
|
+
{
|
|
632
|
+
"title": title,
|
|
633
|
+
"url": url,
|
|
634
|
+
"video_id": entry.get("id"),
|
|
635
|
+
"output_dir": str(config.DOWNLOAD_DIR),
|
|
636
|
+
},
|
|
637
|
+
)
|
|
518
638
|
filepath = youtube.download_url(url, config.DOWNLOAD_DIR)
|
|
519
639
|
i(f" Downloaded: {title} -> {filepath}")
|
|
520
640
|
|
|
@@ -523,21 +643,15 @@ def switch_mode():
|
|
|
523
643
|
config.Mode = "Offline"
|
|
524
644
|
|
|
525
645
|
|
|
526
|
-
def show_help():
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
print(f" {G}config barheight Bar height: 2-16 (current: {config.BarHeight}){R}")
|
|
539
|
-
print(f" {G}config barspacing 0-4, min, fit, max (current: {config.BarSpacing}){R}")
|
|
540
|
-
print(f"\n{T}Display Modes:{R}")
|
|
541
|
-
print(f" {G}bars{R} Audio-reactive spectrum analyzer (needs audio output){R}")
|
|
542
|
-
print(f" {G}lyrics{R} Synced lyrics display with colors{R}")
|
|
543
|
-
print(f" {G}none{R} No display during playback{R}")
|
|
646
|
+
def show_help(inf=False):
|
|
647
|
+
if inf:
|
|
648
|
+
print(f"{T}Online Commands (detailed):{R}")
|
|
649
|
+
for cmd, lines in help_detail.ONLINE_HELP.items():
|
|
650
|
+
for line in lines:
|
|
651
|
+
print(f" {line}")
|
|
652
|
+
print()
|
|
653
|
+
else:
|
|
654
|
+
print(f"{T}Online Commands:{R}")
|
|
655
|
+
for cmd, desc in COMMANDS.items():
|
|
656
|
+
print(f" {T}{cmd:12s}{R} {G}{desc}{R}")
|
|
657
|
+
print(f"{G} Use 'help -i' for detailed usage{R}")
|