flow-twinx 0.2.0__tar.gz → 0.3.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.3.0}/PKG-INFO +1 -1
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Offline/commands.py +22 -14
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Offline/player.py +13 -5
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Online/commands.py +86 -40
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Online/player.py +26 -10
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Online/youtube.py +1 -1
- flow_twinx-0.3.0/flow_twinx/config.py +138 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/help_detail.py +8 -6
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/imports.py +1 -1
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/main.py +9 -6
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/ping.py +0 -3
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/shortcuts.py +1 -0
- flow_twinx-0.3.0/flow_twinx/tui.py +27 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/PKG-INFO +1 -1
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/pyproject.toml +1 -1
- 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.3.0}/README.md +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Offline/__init__.py +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Offline/file.py +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Offline/youtube.py +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Online/__init__.py +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/__init__.py +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/SOURCES.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/dependency_links.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/entry_points.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/requires.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/top_level.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/requirements.txt +0 -0
- {flow_twinx-0.2.0 → flow_twinx-0.3.0}/setup.cfg +0 -0
|
@@ -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,17 +1,44 @@
|
|
|
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
|
|
11
13
|
|
|
14
|
+
P = config.Primary
|
|
15
|
+
S = config.Secondary
|
|
16
|
+
T = config.Tertiary
|
|
17
|
+
M = config.Muted
|
|
18
|
+
E = config.Red
|
|
19
|
+
G = config.Grey
|
|
20
|
+
R = config.Reset
|
|
21
|
+
|
|
22
|
+
m = lambda t: print(f"{M}{t}{R}")
|
|
23
|
+
e = lambda t: print(f"{E}{t}{R}")
|
|
24
|
+
i = lambda t: print(f"{P if config.Mode == 'Online' else S}{t}{R}")
|
|
25
|
+
|
|
12
26
|
_last_results = []
|
|
13
27
|
_last_played = None
|
|
14
28
|
|
|
29
|
+
_radio_quit = False
|
|
30
|
+
_radio_skip = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _radio_sigint(sig, frame):
|
|
34
|
+
global _radio_skip
|
|
35
|
+
_radio_skip = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _radio_sigquit(sig, frame):
|
|
39
|
+
global _radio_quit
|
|
40
|
+
_radio_quit = True
|
|
41
|
+
|
|
15
42
|
COMMANDS = {
|
|
16
43
|
"play": "Play a song from YouTube",
|
|
17
44
|
"search": "Search YouTube for tracks",
|
|
@@ -21,13 +48,10 @@ COMMANDS = {
|
|
|
21
48
|
"switch": "Switch to Offline mode",
|
|
22
49
|
"help": "Show this help message",
|
|
23
50
|
"short": "Show/update command shortcuts",
|
|
51
|
+
"config": "Change primary/secondary/tertiary colors",
|
|
24
52
|
"exit": "Exit Flow",
|
|
25
53
|
}
|
|
26
54
|
|
|
27
|
-
m = tprint(color="grey", border="none")
|
|
28
|
-
e = tprint(color="red", border="none")
|
|
29
|
-
i = tprint(color="theme", border="none")
|
|
30
|
-
|
|
31
55
|
def run(cmd: str, extra: list[str], args):
|
|
32
56
|
cmd = shortcuts.resolve(cmd)
|
|
33
57
|
inf = "-i" in extra
|
|
@@ -48,6 +72,8 @@ def run(cmd: str, extra: list[str], args):
|
|
|
48
72
|
radio(extra, args)
|
|
49
73
|
elif cmd == "short":
|
|
50
74
|
shortcuts.cmd_short(extra, m)
|
|
75
|
+
elif cmd == "config":
|
|
76
|
+
config.cmd_config(extra, args)
|
|
51
77
|
else:
|
|
52
78
|
print(f"Unknown command: {cmd}")
|
|
53
79
|
|
|
@@ -56,7 +82,7 @@ def _spinner(stop):
|
|
|
56
82
|
chars = "|/-\\"
|
|
57
83
|
i = 0
|
|
58
84
|
while not stop():
|
|
59
|
-
sys.stdout.write(f"\r
|
|
85
|
+
sys.stdout.write(f"\r{P}Searching... {chars[i]}{R}")
|
|
60
86
|
sys.stdout.flush()
|
|
61
87
|
time.sleep(0.1)
|
|
62
88
|
i = (i + 1) % len(chars)
|
|
@@ -196,30 +222,55 @@ def radio(extra, args):
|
|
|
196
222
|
playlist_dir.mkdir(parents=True, exist_ok=True)
|
|
197
223
|
m(f"\tDownloading to {playlist_dir}")
|
|
198
224
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
global _radio_quit, _radio_skip
|
|
226
|
+
_radio_quit = False
|
|
227
|
+
_radio_skip = False
|
|
228
|
+
|
|
229
|
+
old_sigint = signal.signal(signal.SIGINT, _radio_sigint)
|
|
230
|
+
old_sigquit = signal.signal(signal.SIGQUIT, _radio_sigquit)
|
|
231
|
+
|
|
232
|
+
fd = sys.stdin.fileno()
|
|
233
|
+
old_term = None
|
|
234
|
+
try:
|
|
235
|
+
old_term = termios.tcgetattr(fd)
|
|
236
|
+
new = termios.tcgetattr(fd)
|
|
237
|
+
new[0] &= ~termios.IXON
|
|
238
|
+
new[6][termios.VQUIT] = 0x11 # Ctrl+Q -> SIGQUIT
|
|
239
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, new)
|
|
240
|
+
except (termios.error, OSError):
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
flags = {"quit": lambda: _radio_quit, "skip": lambda: _radio_skip}
|
|
244
|
+
|
|
245
|
+
idx = 0
|
|
246
|
+
try:
|
|
247
|
+
while idx < len(tracks) and not _radio_quit:
|
|
248
|
+
title, vid, dur = tracks[idx]
|
|
249
|
+
url = f"https://www.youtube.com/watch?v={vid}"
|
|
250
|
+
entry = youtube.get_entry(url)
|
|
251
|
+
short = _truncate_title(title)
|
|
252
|
+
mins, secs = divmod(int(dur), 60)
|
|
253
|
+
|
|
254
|
+
if idx + 1 < len(tracks):
|
|
255
|
+
n_title, n_vid, n_dur = tracks[idx + 1]
|
|
256
|
+
n_short = _truncate_title(n_title)
|
|
257
|
+
n_mins, n_secs = divmod(int(n_dur), 60)
|
|
258
|
+
print(f"{T}\n\t⥤ Next: {n_short:30s} {n_mins}:{n_secs:02d}{R}")
|
|
259
|
+
|
|
260
|
+
filepath = None
|
|
261
|
+
if playlist_dir:
|
|
262
|
+
m(f"\tDownloading {short}...")
|
|
263
|
+
filepath = youtube.download_url(url, str(playlist_dir))
|
|
264
|
+
m(f"\tDownloaded to {filepath}")
|
|
265
|
+
|
|
266
|
+
_radio_skip = False
|
|
267
|
+
player.play_entry(entry, title, args, filepath, flags=flags)
|
|
268
|
+
idx += 1
|
|
269
|
+
finally:
|
|
270
|
+
if old_term:
|
|
271
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_term)
|
|
272
|
+
signal.signal(signal.SIGINT, old_sigint)
|
|
273
|
+
signal.signal(signal.SIGQUIT, old_sigquit)
|
|
223
274
|
|
|
224
275
|
|
|
225
276
|
def download(extra: list[str]):
|
|
@@ -259,22 +310,17 @@ def download(extra: list[str]):
|
|
|
259
310
|
|
|
260
311
|
def switch_mode():
|
|
261
312
|
config.Mode = "Offline"
|
|
262
|
-
config.THEME = config.OFFLINE_THEME
|
|
263
|
-
m("Switched to Offline mode")
|
|
264
313
|
|
|
265
314
|
|
|
266
315
|
def show_help(inf=False):
|
|
267
|
-
_t = config.THEMES[config.THEME]["theme"]
|
|
268
|
-
_g = "\033[90m"
|
|
269
|
-
_r = "\033[0m"
|
|
270
316
|
if inf:
|
|
271
|
-
print(f"{
|
|
317
|
+
print(f"{T}Online Commands (detailed):{R}")
|
|
272
318
|
for cmd, lines in help_detail.ONLINE_HELP.items():
|
|
273
319
|
for line in lines:
|
|
274
|
-
print(f" {line
|
|
320
|
+
print(f" {line}")
|
|
275
321
|
print()
|
|
276
322
|
else:
|
|
277
|
-
print(f"{
|
|
323
|
+
print(f"{T}Online Commands:{R}")
|
|
278
324
|
for cmd, desc in COMMANDS.items():
|
|
279
|
-
print(f" {
|
|
280
|
-
print(f"{
|
|
325
|
+
print(f" {T}{cmd:12s}{R} {G}{desc}{R}")
|
|
326
|
+
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
|
|
|
@@ -20,7 +28,8 @@ def _progress_bar(elapsed, total):
|
|
|
20
28
|
return f" [{bar}] {pct}%"
|
|
21
29
|
|
|
22
30
|
|
|
23
|
-
def play_entry(entry, title, args=None, filepath=None,
|
|
31
|
+
def play_entry(entry, title, args=None, filepath=None, flags=None):
|
|
32
|
+
title = title.split("|")[0]
|
|
24
33
|
repeat = args and getattr(args, "r", False)
|
|
25
34
|
|
|
26
35
|
while True:
|
|
@@ -45,27 +54,34 @@ def play_entry(entry, title, args=None, filepath=None, stop_on_interrupt=False):
|
|
|
45
54
|
|
|
46
55
|
duration = entry.get("duration", 0)
|
|
47
56
|
dur_min, dur_sec = divmod(int(duration), 60)
|
|
48
|
-
i(f"\
|
|
57
|
+
i(f"\n⤘ Now : {title}")
|
|
49
58
|
m(f" {dur_min}:{dur_sec:02d} | repeat:{args.r} | shuffle:{args.s}")
|
|
50
59
|
|
|
51
60
|
start = time.time()
|
|
61
|
+
skipped = False
|
|
52
62
|
try:
|
|
53
63
|
while player.get_state() not in (vlc.State.Ended, vlc.State.Error):
|
|
64
|
+
if flags:
|
|
65
|
+
if flags.get("quit", lambda: False)():
|
|
66
|
+
player.stop()
|
|
67
|
+
return
|
|
68
|
+
if flags.get("skip", lambda: False)():
|
|
69
|
+
skipped = True
|
|
70
|
+
player.stop()
|
|
71
|
+
break
|
|
54
72
|
elapsed = time.time() - start
|
|
55
73
|
bar = _progress_bar(elapsed, duration)
|
|
56
|
-
sys.stdout.write(f"\r
|
|
74
|
+
sys.stdout.write(f"\r{P}{bar}{R}")
|
|
57
75
|
sys.stdout.flush()
|
|
58
76
|
time.sleep(0.5)
|
|
59
77
|
except KeyboardInterrupt:
|
|
60
78
|
player.stop()
|
|
61
79
|
sys.stdout.write("\n")
|
|
62
80
|
sys.stdout.flush()
|
|
63
|
-
if stop_on_interrupt:
|
|
64
|
-
raise
|
|
65
81
|
break
|
|
66
82
|
|
|
67
83
|
sys.stdout.write("\n")
|
|
68
84
|
sys.stdout.flush()
|
|
69
85
|
|
|
70
|
-
if not repeat:
|
|
86
|
+
if skipped or not repeat:
|
|
71
87
|
break
|
|
@@ -28,7 +28,7 @@ ydl_opts_play = {
|
|
|
28
28
|
"noplaylist": True, "format": "bestaudio/best", "skip_download": True,
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
def search(query, limit=
|
|
31
|
+
def search(query, limit=3):
|
|
32
32
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
33
33
|
info = ydl.extract_info(f"ytsearch{limit}:{query}", download=False)
|
|
34
34
|
if not info.get("entries"):
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pathlib
|
|
3
|
+
|
|
4
|
+
CONFIG_FILE = pathlib.Path.home() / ".flow/config.json"
|
|
5
|
+
|
|
6
|
+
def merge_flags(extra: list[str], args) -> tuple[list[str], object]:
|
|
7
|
+
mapping = {"-r": "r", "-s": "s", "-d": "d" , "-i":"i" , "-h":"h"}
|
|
8
|
+
rest = []
|
|
9
|
+
for item in extra:
|
|
10
|
+
if item in mapping:
|
|
11
|
+
setattr(args, mapping[item], True)
|
|
12
|
+
elif item.startswith("-"):
|
|
13
|
+
print(f"Unknown flag: {item}")
|
|
14
|
+
else:
|
|
15
|
+
rest.append(item)
|
|
16
|
+
return rest, args
|
|
17
|
+
|
|
18
|
+
Red = "\033[0;31m"
|
|
19
|
+
Green = "\033[0;32m"
|
|
20
|
+
Brown = "\033[0;33m"
|
|
21
|
+
Blue = "\033[0;34m"
|
|
22
|
+
Purple = "\033[0;35m"
|
|
23
|
+
Cyan = "\033[0;36m"
|
|
24
|
+
Grey = "\033[90m"
|
|
25
|
+
YELLOW = "\033[1;33m"
|
|
26
|
+
White = "\033[1;37m"
|
|
27
|
+
MAROON = "\033[38;5;124m"
|
|
28
|
+
OLIVE = "\033[38;5;100m"
|
|
29
|
+
DARK_KHAKI = "\033[38;5;136m"
|
|
30
|
+
GOLD = "\033[38;5;220m"
|
|
31
|
+
TAN = "\033[38;5;215m"
|
|
32
|
+
SALMON = "\033[38;5;209m"
|
|
33
|
+
CORAL = "\033[38;5;203m"
|
|
34
|
+
HOT_PINK = "\033[38;5;205m"
|
|
35
|
+
VIOLET = "\033[38;5;141m"
|
|
36
|
+
DEEP_PURPLE = "\033[38;5;129m"
|
|
37
|
+
TEAL = "\033[38;5;30m"
|
|
38
|
+
SKY_BLUE = "\033[38;5;117m"
|
|
39
|
+
STEEL_BLUE = "\033[38;5;67m"
|
|
40
|
+
NAVY = "\033[38;5;18m"
|
|
41
|
+
INDIGO = "\033[38;5;54m"
|
|
42
|
+
ORANGE = "\033[38;5;202m"
|
|
43
|
+
PINK = "\033[38;5;205m"
|
|
44
|
+
LIME = "\033[38;5;154m"
|
|
45
|
+
Reset = "\033[0m"
|
|
46
|
+
|
|
47
|
+
_COLORS = {k.lower().replace("_", ""): v for k, v in vars().items()
|
|
48
|
+
if isinstance(v, str) and v.startswith("\033") and k != "Reset"}
|
|
49
|
+
|
|
50
|
+
_COLOR_NAMES = {v: k for k, v in _COLORS.items()}
|
|
51
|
+
|
|
52
|
+
_TARGET_ALIASES = {"pri": "primary", "sec": "secondary", "ter": "tertiary"}
|
|
53
|
+
_TARGETS = {"primary", "secondary", "tertiary"}
|
|
54
|
+
|
|
55
|
+
Primary = Cyan
|
|
56
|
+
Secondary = Purple
|
|
57
|
+
Tertiary = Blue
|
|
58
|
+
Muted = Grey
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _load_config():
|
|
62
|
+
global Primary, Secondary, Tertiary
|
|
63
|
+
if not CONFIG_FILE.exists():
|
|
64
|
+
return
|
|
65
|
+
try:
|
|
66
|
+
data = json.loads(CONFIG_FILE.read_text())
|
|
67
|
+
if "primary" in data and data["primary"] in _COLORS:
|
|
68
|
+
Primary = _COLORS[data["primary"]]
|
|
69
|
+
if "secondary" in data and data["secondary"] in _COLORS:
|
|
70
|
+
Secondary = _COLORS[data["secondary"]]
|
|
71
|
+
if "tertiary" in data and data["tertiary"] in _COLORS:
|
|
72
|
+
Tertiary = _COLORS[data["tertiary"]]
|
|
73
|
+
except (json.JSONDecodeError, OSError):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _save_config():
|
|
78
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
data = {
|
|
80
|
+
"primary": _COLOR_NAMES.get(Primary, "cyan"),
|
|
81
|
+
"secondary": _COLOR_NAMES.get(Secondary, "purple"),
|
|
82
|
+
"tertiary": _COLOR_NAMES.get(Tertiary, "blue"),
|
|
83
|
+
}
|
|
84
|
+
CONFIG_FILE.write_text(json.dumps(data, indent=2))
|
|
85
|
+
|
|
86
|
+
DOWNLOAD_DIR = pathlib.Path.home() / ".flow/downloads"
|
|
87
|
+
|
|
88
|
+
VERSION = 0.3
|
|
89
|
+
|
|
90
|
+
Mode = "Online"
|
|
91
|
+
|
|
92
|
+
liked_music = pathlib.Path.home() / ".flow/liked.txt"
|
|
93
|
+
|
|
94
|
+
MAX_RESULTS_RADIO = 35
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cmd_config(extra: list[str], args=None):
|
|
98
|
+
global Primary, Secondary, Tertiary
|
|
99
|
+
if (args and getattr(args, "h", False)) or (extra and extra[0] == "-h"):
|
|
100
|
+
print(f"{Tertiary}Available targets:{Reset}")
|
|
101
|
+
print(f" {Primary}primary{Reset} (aliases: pri)")
|
|
102
|
+
print(f" {Secondary}secondary{Reset} (aliases: sec)")
|
|
103
|
+
print(f" {Tertiary}tertiary{Reset} (aliases: ter)")
|
|
104
|
+
print(f"\n{Tertiary}Available colors:{Reset}")
|
|
105
|
+
for name, code in _COLORS.items():
|
|
106
|
+
print(f" {code}{name}{Reset}")
|
|
107
|
+
print(f"\n{Grey}Config file: {CONFIG_FILE}{Reset}")
|
|
108
|
+
return
|
|
109
|
+
if not extra:
|
|
110
|
+
print(f"{Primary}Usage: config <primary|secondary|tertiary> <color>{Reset}")
|
|
111
|
+
print(f" config -h{Reset}")
|
|
112
|
+
return
|
|
113
|
+
if len(extra) < 2:
|
|
114
|
+
print(f"Usage: config <primary|secondary|tertiary> <color>{Reset}")
|
|
115
|
+
return
|
|
116
|
+
target = _TARGET_ALIASES.get(extra[0].lower(), extra[0].lower())
|
|
117
|
+
color_name = extra[1].lower()
|
|
118
|
+
if color_name not in _COLORS:
|
|
119
|
+
print(f"Unknown color '{color_name}'. Use 'config -h' to see available colors.{Reset}")
|
|
120
|
+
return
|
|
121
|
+
if target == "primary":
|
|
122
|
+
Primary = _COLORS[color_name]
|
|
123
|
+
_save_config()
|
|
124
|
+
print(f"{Primary}Primary color changed to {color_name}{Reset}")
|
|
125
|
+
elif target == "secondary":
|
|
126
|
+
Secondary = _COLORS[color_name]
|
|
127
|
+
_save_config()
|
|
128
|
+
print(f"{Secondary}Secondary color changed to {color_name}{Reset}")
|
|
129
|
+
elif target == "tertiary":
|
|
130
|
+
Tertiary = _COLORS[color_name]
|
|
131
|
+
_save_config()
|
|
132
|
+
print(f"{Tertiary}Tertiary color changed to {color_name}{Reset}")
|
|
133
|
+
else:
|
|
134
|
+
aliases = ", ".join(f"{k}->{v}" for k, v in _TARGET_ALIASES.items())
|
|
135
|
+
print(f"Unknown target '{target}'. Use: primary, sec, ter ({aliases}){Reset}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_load_config()
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
from . import config
|
|
2
|
+
|
|
3
|
+
_T = config.Primary
|
|
4
|
+
_M = config.Tertiary
|
|
5
|
+
_B = config.Secondary
|
|
6
|
+
_G = config.Muted
|
|
7
|
+
_R = config.Reset
|
|
6
8
|
|
|
7
9
|
ONLINE_HELP = {
|
|
8
10
|
"play": [
|
|
@@ -44,7 +46,7 @@ ONLINE_HELP = {
|
|
|
44
46
|
f"{_T}> radio{_R} {_M}<song_name>{_R}",
|
|
45
47
|
f"{_G} Generate a radio mix based on a reference song.{_R}",
|
|
46
48
|
f"{_G} Plays related songs sequentially from YouTube Music.{_R}",
|
|
47
|
-
f"{_G} Press Ctrl+C to
|
|
49
|
+
f"{_G} Press Ctrl+C for next track, Ctrl+Q to quit. Alias:{_R} {_M}rd{_R}",
|
|
48
50
|
],
|
|
49
51
|
"switch": [
|
|
50
52
|
f"{_T}> switch{_R}",
|
|
@@ -10,15 +10,19 @@ if __name__ == "__main__" and __package__ is None:
|
|
|
10
10
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
11
11
|
|
|
12
12
|
from .imports import config, show_banner, tui_input, is_connected
|
|
13
|
-
from .imports import tprint as tui_print
|
|
14
13
|
from . import shortcuts
|
|
15
14
|
|
|
15
|
+
P = config.Primary
|
|
16
|
+
S = config.Secondary
|
|
17
|
+
M = config.Muted
|
|
18
|
+
R = config.Reset
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
def _spinner(stop):
|
|
18
22
|
chars = "|/-\\"
|
|
19
23
|
i = 0
|
|
20
24
|
while not stop():
|
|
21
|
-
sys.stdout.write(f"\r
|
|
25
|
+
sys.stdout.write(f"\r{P}Checking connection... {chars[i]}{R}")
|
|
22
26
|
sys.stdout.flush()
|
|
23
27
|
time.sleep(0.1)
|
|
24
28
|
i = (i + 1) % len(chars)
|
|
@@ -56,7 +60,7 @@ def main():
|
|
|
56
60
|
if args.command:
|
|
57
61
|
commands.run(args.command, unknown, args)
|
|
58
62
|
elif unknown:
|
|
59
|
-
|
|
63
|
+
print(f"Unknown argument: {' '.join(unknown)}")
|
|
60
64
|
else:
|
|
61
65
|
while True:
|
|
62
66
|
try:
|
|
@@ -67,7 +71,7 @@ def main():
|
|
|
67
71
|
extra = parts[1:]
|
|
68
72
|
cmd = shortcuts.resolve(cmd)
|
|
69
73
|
if cmd in ("exit", "quit", "q"):
|
|
70
|
-
|
|
74
|
+
print(f"{M}Goodbye!{R}")
|
|
71
75
|
break
|
|
72
76
|
cmd_args = argparse.Namespace(**vars(args))
|
|
73
77
|
for flag in ("r", "s", "d"):
|
|
@@ -77,9 +81,8 @@ def main():
|
|
|
77
81
|
mode = config.Mode
|
|
78
82
|
commands = _load_commands()
|
|
79
83
|
show_banner()
|
|
80
|
-
tui_print(color="grey", border="none")(f"Mode changed to {mode}")
|
|
81
84
|
except (EOFError, KeyboardInterrupt):
|
|
82
|
-
|
|
85
|
+
print(f"{M}\nGoodbye!{R}")
|
|
83
86
|
break
|
|
84
87
|
|
|
85
88
|
|
|
@@ -14,7 +14,6 @@ def is_connected(timeout: int = 2, hosts: list[str] | None = None) -> bool:
|
|
|
14
14
|
with urllib.request.urlopen(f"https://{host}", timeout=timeout):
|
|
15
15
|
pass
|
|
16
16
|
config.Mode = "Online"
|
|
17
|
-
config.THEME = config.ONLINE_THEME
|
|
18
17
|
return True
|
|
19
18
|
except Exception:
|
|
20
19
|
pass
|
|
@@ -22,11 +21,9 @@ def is_connected(timeout: int = 2, hosts: list[str] | None = None) -> bool:
|
|
|
22
21
|
try:
|
|
23
22
|
socket.create_connection((host, 80), timeout=timeout).close()
|
|
24
23
|
config.Mode = "Online"
|
|
25
|
-
config.THEME = config.ONLINE_THEME
|
|
26
24
|
return True
|
|
27
25
|
except Exception:
|
|
28
26
|
continue
|
|
29
27
|
|
|
30
28
|
config.Mode = "Offline"
|
|
31
|
-
config.THEME = config.OFFLINE_THEME
|
|
32
29
|
return False
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
from . import config
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
BANNER = r"""
|
|
6
|
+
███████╗██╗ ██████╗ ██╗ ██╗
|
|
7
|
+
██╔════╝██║ ██╔═══██╗██║ ██║
|
|
8
|
+
█████╗ ██║ ██║ ██║██║ █╗ ██║
|
|
9
|
+
██╔══╝ ██║ ██║ ██║██║███╗██║
|
|
10
|
+
██║ ███████╗╚██████╔╝╚███╔███╔╝
|
|
11
|
+
╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def show_banner():
|
|
16
|
+
c = config.Primary if config.Mode == "Online" else config.Secondary
|
|
17
|
+
builtins.print(f"{c}{BANNER}{config.Reset}")
|
|
18
|
+
builtins.print(f"{config.Muted} Flow Music Player v{config.VERSION}{config.Reset}")
|
|
19
|
+
builtins.print(f"{c} Mode : {config.Mode}{config.Reset}")
|
|
20
|
+
builtins.print()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def input(prompt: str = "") -> str:
|
|
24
|
+
if config.Mode == "Online":
|
|
25
|
+
return builtins.input(f"{config.Primary}{prompt}$ {config.Reset}")
|
|
26
|
+
elif config.Mode == "Offline":
|
|
27
|
+
return builtins.input(f"{config.Secondary}{prompt}$ {config.Reset}")
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import pathlib
|
|
2
|
-
|
|
3
|
-
def merge_flags(extra: list[str], args) -> tuple[list[str], object]:
|
|
4
|
-
mapping = {"-r": "r", "-s": "s", "-d": "d"}
|
|
5
|
-
rest = []
|
|
6
|
-
for item in extra:
|
|
7
|
-
if item in mapping:
|
|
8
|
-
setattr(args, mapping[item], True)
|
|
9
|
-
elif item.startswith("-"):
|
|
10
|
-
print(f"Unknown flag: {item}")
|
|
11
|
-
else:
|
|
12
|
-
rest.append(item)
|
|
13
|
-
return rest, args
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
ONLINE_THEME = "cyan"
|
|
17
|
-
OFFLINE_THEME = "magenta"
|
|
18
|
-
|
|
19
|
-
THEMES = {
|
|
20
|
-
"blue": {"white": "\033[37m", "grey": "\033[90m", "theme": "\033[34m"},
|
|
21
|
-
"green": {"white": "\033[37m", "grey": "\033[90m", "theme": "\033[32m"},
|
|
22
|
-
"red": {"white": "\033[37m", "grey": "\033[90m", "theme": "\033[31m"},
|
|
23
|
-
"yellow": {"white": "\033[37m", "grey": "\033[90m", "theme": "\033[33m"},
|
|
24
|
-
"magenta": {"white": "\033[37m", "grey": "\033[90m", "theme": "\033[35m"},
|
|
25
|
-
"cyan": {"white": "\033[37m", "grey": "\033[90m", "theme": "\033[36m"},
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
DOWNLOAD_DIR = pathlib.Path.home() / ".flow/downloads"
|
|
29
|
-
|
|
30
|
-
VERSION = 0.1
|
|
31
|
-
|
|
32
|
-
Mode = "Online"
|
|
33
|
-
|
|
34
|
-
THEME = ONLINE_THEME
|
|
35
|
-
|
|
36
|
-
liked_music = pathlib.Path.home() / ".flow/liked.txt"
|
|
37
|
-
|
|
38
|
-
MAX_RESULTS_RADIO = 35
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import builtins
|
|
2
|
-
from . import config
|
|
3
|
-
|
|
4
|
-
RESET = "\033[0m"
|
|
5
|
-
|
|
6
|
-
BORDER_CHARS = {
|
|
7
|
-
"none": None,
|
|
8
|
-
"single": ("-", "|", "+"),
|
|
9
|
-
"double": ("=", "|", "+"),
|
|
10
|
-
"dashed": ("- ", "!", "+"),
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _color_map():
|
|
15
|
-
colors = config.THEMES.get(config.THEME, config.THEMES["cyan"])
|
|
16
|
-
return {
|
|
17
|
-
"white": colors["white"],
|
|
18
|
-
"grey": colors["grey"],
|
|
19
|
-
"theme": colors["theme"],
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def print(color: str = "white", border: str = "none"):
|
|
24
|
-
def styled_print(text: str):
|
|
25
|
-
cm = _color_map()
|
|
26
|
-
color_code = cm.get(color.lower(), cm["white"])
|
|
27
|
-
border_style = BORDER_CHARS.get(border.lower())
|
|
28
|
-
|
|
29
|
-
if border_style is None:
|
|
30
|
-
print_line = f"{color_code}{text}{RESET}"
|
|
31
|
-
builtins.print(print_line)
|
|
32
|
-
else:
|
|
33
|
-
h_char, v_char, corner_char = border_style
|
|
34
|
-
lines = text.split("\n")
|
|
35
|
-
max_w = max(len(l) for l in lines)
|
|
36
|
-
width = max_w + 2
|
|
37
|
-
top_bottom = corner_char + h_char * width + corner_char
|
|
38
|
-
builtins.print(f"{color_code}{top_bottom}{RESET}")
|
|
39
|
-
for line in lines:
|
|
40
|
-
padded = line.ljust(max_w)
|
|
41
|
-
builtins.print(f"{color_code}{v_char} {padded} {v_char}{RESET}")
|
|
42
|
-
builtins.print(f"{color_code}{top_bottom}{RESET}")
|
|
43
|
-
|
|
44
|
-
return styled_print
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
BANNER = r"""
|
|
48
|
-
███████╗██╗ ██████╗ ██╗ ██╗
|
|
49
|
-
██╔════╝██║ ██╔═══██╗██║ ██║
|
|
50
|
-
█████╗ ██║ ██║ ██║██║ █╗ ██║
|
|
51
|
-
██╔══╝ ██║ ██║ ██║██║███╗██║
|
|
52
|
-
██║ ███████╗╚██████╔╝╚███╔███╔╝
|
|
53
|
-
╚═╝ ╚══════╝ ╚═════╝ ╚══╝╚══╝
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def show_banner():
|
|
58
|
-
p = print(color="theme", border="none")
|
|
59
|
-
p(BANNER)
|
|
60
|
-
p = print(color="grey", border="none")
|
|
61
|
-
p(f" Flow Music Player v{config.VERSION}")
|
|
62
|
-
p = print(color="theme", border="none")
|
|
63
|
-
p(f" Mode : {config.Mode}")
|
|
64
|
-
builtins.print()
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def input(prompt: str = "") -> str:
|
|
68
|
-
cm = _color_map()
|
|
69
|
-
return builtins.input(f"{cm['theme']}{prompt}$ {RESET}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|