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.
Files changed (30) hide show
  1. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/PKG-INFO +1 -1
  2. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Offline/commands.py +22 -14
  3. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Offline/player.py +13 -5
  4. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Online/commands.py +86 -40
  5. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Online/player.py +26 -10
  6. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Online/youtube.py +1 -1
  7. flow_twinx-0.3.0/flow_twinx/config.py +138 -0
  8. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/help_detail.py +8 -6
  9. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/imports.py +1 -1
  10. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/main.py +9 -6
  11. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/ping.py +0 -3
  12. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/shortcuts.py +1 -0
  13. flow_twinx-0.3.0/flow_twinx/tui.py +27 -0
  14. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/PKG-INFO +1 -1
  15. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/pyproject.toml +1 -1
  16. flow_twinx-0.2.0/flow_twinx/config.py +0 -38
  17. flow_twinx-0.2.0/flow_twinx/tui.py +0 -69
  18. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/README.md +0 -0
  19. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Offline/__init__.py +0 -0
  20. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Offline/file.py +0 -0
  21. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Offline/youtube.py +0 -0
  22. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/Online/__init__.py +0 -0
  23. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx/__init__.py +0 -0
  24. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/SOURCES.txt +0 -0
  25. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/dependency_links.txt +0 -0
  26. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/entry_points.txt +0 -0
  27. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/requires.txt +0 -0
  28. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/flow_twinx.egg-info/top_level.txt +0 -0
  29. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/requirements.txt +0 -0
  30. {flow_twinx-0.2.0 → flow_twinx-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flow-twinx
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A terminal music player with online and offline modes
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -1,10 +1,22 @@
1
1
  import random
2
- from ..imports import config, merge_flags, tprint, is_connected
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
- i("Switched to Online mode")
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"{_t}Offline Commands (detailed):{_r}")
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.replace('{theme}', _t)}")
236
+ print(f" {line}")
229
237
  print()
230
238
  else:
231
- print(f"{_t}Offline Commands:{_r}")
239
+ print(f"{T}Offline Commands:{R}")
232
240
  for cmd, desc in COMMANDS.items():
233
- print(f" {_t}{cmd:12s}{_r} {_g}{desc}{_r}")
234
- print(f"{_g} Use 'help -i' for detailed usage{_r}")
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 tprint
4
+ from ..imports import config
5
5
 
6
- m = tprint(color="grey", border="none")
7
- e = tprint(color="red", border="none")
8
- i = tprint(color="theme", border="none")
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\x1b[35m{bar}\x1b[0m")
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, tprint
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\x1b[36mSearching... {chars[i]}\x1b[0m")
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
- GREEN = "\033[32m"
200
- RESET = "\033[0m"
201
- for idx, (title, vid, dur) in enumerate(tracks):
202
- url = f"https://www.youtube.com/watch?v={vid}"
203
- entry = youtube.get_entry(url)
204
- short = _truncate_title(title)
205
- mins, secs = divmod(int(dur), 60)
206
-
207
- if idx + 1 < len(tracks):
208
- n_title, n_vid, n_dur = tracks[idx + 1]
209
- n_short = _truncate_title(n_title)
210
- n_mins, n_secs = divmod(int(n_dur), 60)
211
- print(f"{GREEN}\t→ Next: {n_short:30s} {n_mins}:{n_secs:02d}{RESET}")
212
-
213
- filepath = None
214
- if playlist_dir:
215
- m(f"\tDownloading {short}...")
216
- filepath = youtube.download_url(url, str(playlist_dir))
217
- m(f"\tDownloaded to {filepath}")
218
-
219
- try:
220
- player.play_entry(entry, title, args, filepath, stop_on_interrupt=True)
221
- except KeyboardInterrupt:
222
- break
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"{_t}Online Commands (detailed):{_r}")
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.replace('{theme}', _t)}")
320
+ print(f" {line}")
275
321
  print()
276
322
  else:
277
- print(f"{_t}Online Commands:{_r}")
323
+ print(f"{T}Online Commands:{R}")
278
324
  for cmd, desc in COMMANDS.items():
279
- print(f" {_t}{cmd:12s}{_r} {_g}{desc}{_r}")
280
- print(f"{_g} Use 'help -i' for detailed usage{_r}")
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 tprint
4
+ from ..imports import config
5
5
 
6
- m = tprint(color="grey", border="none")
7
- e = tprint(color="red", border="none")
8
- i = tprint(color="theme", border="none")
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, stop_on_interrupt=False):
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"\nPlaying : {title}")
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\x1b[36m{bar}\x1b[0m")
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=5):
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
- _G = "\033[90m"
2
- _B = "\033[34m"
3
- _M = "\033[35m"
4
- _R = "\033[0m"
5
- _T = "{theme}"
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 stop. Alias:{_R} {_M}rd{_R}",
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}",
@@ -1,4 +1,4 @@
1
1
  from . import config
2
- from .tui import print as tprint, show_banner, input as tui_input
2
+ from .tui import show_banner, input as tui_input
3
3
  from .ping import is_connected
4
4
  from .config import merge_flags
@@ -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\x1b[36mChecking connection... {chars[i]}\x1b[0m")
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
- tui_print(color="white", border="double")(f"Unknown argument: {' '.join(unknown)}")
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
- tui_print(color="grey", border="none")("Goodbye!")
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
- tui_print(color="grey", border="none")("\nGoodbye!")
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
@@ -12,6 +12,7 @@ DEFAULT_SHORTCUTS = {
12
12
  "rd": "radio",
13
13
  "sw": "switch",
14
14
  "hl": "help",
15
+ "cf": "config",
15
16
  "ex": "exit",
16
17
  }
17
18
 
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flow-twinx
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A terminal music player with online and offline modes
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flow-twinx"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "A terminal music player with online and offline modes"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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