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.
Files changed (32) hide show
  1. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/PKG-INFO +27 -29
  2. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/README.md +25 -27
  3. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Offline/commands.py +22 -14
  4. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Offline/player.py +13 -5
  5. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Online/commands.py +162 -40
  6. flow_twinx-0.4.0/flow_twinx/Online/player.py +122 -0
  7. flow_twinx-0.4.0/flow_twinx/Online/savan.py +48 -0
  8. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Online/youtube.py +1 -1
  9. flow_twinx-0.4.0/flow_twinx/config.py +146 -0
  10. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/help_detail.py +28 -6
  11. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/imports.py +1 -1
  12. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/main.py +29 -6
  13. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/ping.py +0 -3
  14. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/shortcuts.py +3 -0
  15. flow_twinx-0.4.0/flow_twinx/tui.py +27 -0
  16. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/PKG-INFO +27 -29
  17. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/SOURCES.txt +1 -0
  18. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/pyproject.toml +2 -2
  19. flow_twinx-0.2.0/flow_twinx/Online/player.py +0 -71
  20. flow_twinx-0.2.0/flow_twinx/config.py +0 -38
  21. flow_twinx-0.2.0/flow_twinx/tui.py +0 -69
  22. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Offline/__init__.py +0 -0
  23. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Offline/file.py +0 -0
  24. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Offline/youtube.py +0 -0
  25. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/Online/__init__.py +0 -0
  26. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx/__init__.py +0 -0
  27. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/dependency_links.txt +0 -0
  28. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/entry_points.txt +0 -0
  29. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/requires.txt +0 -0
  30. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/flow_twinx.egg-info/top_level.txt +0 -0
  31. {flow_twinx-0.2.0 → flow_twinx-0.4.0}/requirements.txt +0 -0
  32. {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.2.0
3
+ Version: 0.4.0
4
4
  Summary: A terminal music player with online and offline modes
5
- Requires-Python: >=3.9
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** — Automatically detects internet and switches between online streaming and offline playback
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/yourusername/flow.git
33
+ git clone https://github.com/Philast-015/Flow.git
34
34
  cd flow
35
- python -m venv .venv
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 have python installed
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
- python main.py [-r] [-s] [-d]
50
+ uv run main.py
52
51
  ```
53
-
54
52
  Or direct if you have used pypi
55
-
56
53
  ```bash
57
- flow <command> <flag>
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/ # Main application package
87
+ ├── flow_twinx/
89
88
  │ ├── __init__.py
90
- │ ├── config.py # Configuration, themes, paths
91
- │ ├── imports.py # Central import hub
92
- │ ├── main.py # Entry point / CLI
93
- │ ├── ping.py # Connection checker
94
- │ ├── tui.py # Terminal UI helpers (colors, banner, input)
95
- │ ├── Online/ # Online streaming mode
89
+ │ ├── config.py
90
+ │ ├── imports.py
91
+ │ ├── main.py
92
+ │ ├── ping.py
93
+ │ ├── tui.py
94
+ │ ├── Online/
96
95
  │ │ ├── __init__.py
97
- │ │ ├── commands.py # Online command handlers
98
- │ │ ├── player.py # VLC-based stream player
99
- │ │ └── youtube.py # YouTube search & download via yt-dlp
100
- │ └── Offline/ # Offline local library mode
96
+ │ │ ├── commands.py
97
+ │ │ ├── player.py
98
+ │ │ └── youtube.py
99
+ │ └── Offline/
101
100
  │ ├── __init__.py
102
- │ ├── commands.py # Offline command handlers
103
- │ ├── file.py # Local file & library management
104
- │ ├── player.py # VLC-based local file player
105
- │ └── youtube.py # Placeholder
106
- ├── pyproject.toml # Package metadata & build config
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
- MIT
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** — Automatically detects internet and switches between online streaming and offline playback
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/yourusername/flow.git
24
+ git clone https://github.com/Philast-015/Flow.git
25
25
  cd flow
26
- python -m venv .venv
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 have python installed
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
- python main.py [-r] [-s] [-d]
41
+ uv run main.py
43
42
  ```
44
-
45
43
  Or direct if you have used pypi
46
-
47
44
  ```bash
48
- flow <command> <flag>
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/ # Main application package
78
+ ├── flow_twinx/
80
79
  │ ├── __init__.py
81
- │ ├── config.py # Configuration, themes, paths
82
- │ ├── imports.py # Central import hub
83
- │ ├── main.py # Entry point / CLI
84
- │ ├── ping.py # Connection checker
85
- │ ├── tui.py # Terminal UI helpers (colors, banner, input)
86
- │ ├── Online/ # Online streaming mode
80
+ │ ├── config.py
81
+ │ ├── imports.py
82
+ │ ├── main.py
83
+ │ ├── ping.py
84
+ │ ├── tui.py
85
+ │ ├── Online/
87
86
  │ │ ├── __init__.py
88
- │ │ ├── commands.py # Online command handlers
89
- │ │ ├── player.py # VLC-based stream player
90
- │ │ └── youtube.py # YouTube search & download via yt-dlp
91
- │ └── Offline/ # Offline local library mode
87
+ │ │ ├── commands.py
88
+ │ │ ├── player.py
89
+ │ │ └── youtube.py
90
+ │ └── Offline/
92
91
  │ ├── __init__.py
93
- │ ├── commands.py # Offline command handlers
94
- │ ├── file.py # Local file & library management
95
- │ ├── player.py # VLC-based local file player
96
- │ └── youtube.py # Placeholder
97
- ├── pyproject.toml # Package metadata & build config
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
- MIT
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, 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,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, 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
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\x1b[36mSearching... {chars[i]}\x1b[0m")
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
- 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
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"{_t}Online Commands (detailed):{_r}")
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.replace('{theme}', _t)}")
396
+ print(f" {line}")
275
397
  print()
276
398
  else:
277
- print(f"{_t}Online Commands:{_r}")
399
+ print(f"{T}Online Commands:{R}")
278
400
  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}")
401
+ print(f" {T}{cmd:12s}{R} {G}{desc}{R}")
402
+ print(f"{G} Use 'help -i' for detailed usage{R}")