piwave 2.0.9__py3-none-any.whl → 2.1.0__py3-none-any.whl
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.
- piwave/__init__.py +3 -1
- piwave/__main__.py +105 -0
- piwave/backends/__init__.py +119 -0
- piwave/backends/base.py +236 -0
- piwave/backends/fm_transmitter.py +33 -0
- piwave/backends/pi_fm_rds.py +42 -0
- piwave/logger.py +99 -0
- piwave/piwave.py +97 -122
- piwave-2.1.0.dist-info/METADATA +743 -0
- piwave-2.1.0.dist-info/RECORD +13 -0
- piwave-2.0.9.dist-info/METADATA +0 -419
- piwave-2.0.9.dist-info/RECORD +0 -7
- {piwave-2.0.9.dist-info → piwave-2.1.0.dist-info}/WHEEL +0 -0
- {piwave-2.0.9.dist-info → piwave-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {piwave-2.0.9.dist-info → piwave-2.1.0.dist-info}/top_level.txt +0 -0
piwave/piwave.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
# piwave
|
|
2
|
-
#
|
|
1
|
+
# PiWave is available at https://piwave.xyz
|
|
2
|
+
# Licensed under GPLv3.0, main GitHub repository at https://github.com/douxxtech/piwave/
|
|
3
|
+
# piwave/piwave.py : main entry
|
|
3
4
|
|
|
4
5
|
import os
|
|
5
6
|
import subprocess
|
|
@@ -15,99 +16,8 @@ from pathlib import Path
|
|
|
15
16
|
from urllib.parse import urlparse
|
|
16
17
|
import atexit
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
'reset': '\033[0m',
|
|
21
|
-
'bold': '\033[1m',
|
|
22
|
-
'underline': '\033[4m',
|
|
23
|
-
'red': '\033[31m',
|
|
24
|
-
'green': '\033[32m',
|
|
25
|
-
'yellow': '\033[33m',
|
|
26
|
-
'blue': '\033[34m',
|
|
27
|
-
'magenta': '\033[35m',
|
|
28
|
-
'cyan': '\033[36m',
|
|
29
|
-
'white': '\033[37m',
|
|
30
|
-
'bright_red': '\033[91m',
|
|
31
|
-
'bright_green': '\033[92m',
|
|
32
|
-
'bright_yellow': '\033[93m',
|
|
33
|
-
'bright_blue': '\033[94m',
|
|
34
|
-
'bright_magenta': '\033[95m',
|
|
35
|
-
'bright_cyan': '\033[96m',
|
|
36
|
-
'bright_white': '\033[97m',
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
ICONS = {
|
|
40
|
-
'success': 'OK',
|
|
41
|
-
'error': 'ERR',
|
|
42
|
-
'warning': 'WARN',
|
|
43
|
-
'info': 'INFO',
|
|
44
|
-
'client': 'CLIENT',
|
|
45
|
-
'server': 'SERVER',
|
|
46
|
-
'file': 'FILE',
|
|
47
|
-
'broadcast': 'BCAST',
|
|
48
|
-
'version': 'VER',
|
|
49
|
-
'update': 'UPD',
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
SILENT = False
|
|
53
|
-
|
|
54
|
-
@classmethod
|
|
55
|
-
def config(cls, silent: bool = False):
|
|
56
|
-
cls.SILENT = silent
|
|
57
|
-
|
|
58
|
-
@classmethod
|
|
59
|
-
def print(cls, message: str, style: str = '', icon: str = '', end: str = '\n'):
|
|
60
|
-
|
|
61
|
-
if cls.SILENT: return
|
|
62
|
-
|
|
63
|
-
color = cls.COLORS.get(style, '')
|
|
64
|
-
icon_char = cls.ICONS.get(icon, '')
|
|
65
|
-
if icon_char:
|
|
66
|
-
if color:
|
|
67
|
-
print(f"{color}[{icon_char}]\033[0m {message}", end=end)
|
|
68
|
-
else:
|
|
69
|
-
print(f"[{icon_char}] {message}", end=end)
|
|
70
|
-
else:
|
|
71
|
-
if color:
|
|
72
|
-
print(f"{color}{message}\033[0m", end=end)
|
|
73
|
-
else:
|
|
74
|
-
print(f"{message}", end=end)
|
|
75
|
-
sys.stdout.flush()
|
|
76
|
-
|
|
77
|
-
@classmethod
|
|
78
|
-
def header(cls, text: str):
|
|
79
|
-
cls.print(text, 'bright_blue', end='\n\n')
|
|
80
|
-
sys.stdout.flush()
|
|
81
|
-
|
|
82
|
-
@classmethod
|
|
83
|
-
def section(cls, text: str):
|
|
84
|
-
cls.print(f" {text} ", 'bright_blue', end='')
|
|
85
|
-
cls.print("─" * (len(text) + 2), 'blue', end='\n\n')
|
|
86
|
-
sys.stdout.flush()
|
|
87
|
-
|
|
88
|
-
@classmethod
|
|
89
|
-
def success(cls, message: str):
|
|
90
|
-
cls.print(message, 'bright_green', 'success')
|
|
91
|
-
|
|
92
|
-
@classmethod
|
|
93
|
-
def error(cls, message: str):
|
|
94
|
-
cls.print(message, 'bright_red', 'error')
|
|
95
|
-
|
|
96
|
-
@classmethod
|
|
97
|
-
def warning(cls, message: str):
|
|
98
|
-
cls.print(message, 'bright_yellow', 'warning')
|
|
99
|
-
|
|
100
|
-
@classmethod
|
|
101
|
-
def info(cls, message: str):
|
|
102
|
-
cls.print(message, 'bright_cyan', 'info')
|
|
103
|
-
|
|
104
|
-
@classmethod
|
|
105
|
-
def file_message(cls, message: str):
|
|
106
|
-
cls.print(message, 'yellow', 'file')
|
|
107
|
-
|
|
108
|
-
@classmethod
|
|
109
|
-
def broadcast_message(cls, message: str):
|
|
110
|
-
cls.print(message, 'bright_magenta', 'broadcast')
|
|
19
|
+
from .backends import discover_backends, backends, get_best_backend, search_backends, list_backends
|
|
20
|
+
from .logger import Log
|
|
111
21
|
|
|
112
22
|
class PiWaveError(Exception):
|
|
113
23
|
pass
|
|
@@ -121,6 +31,7 @@ class PiWave:
|
|
|
121
31
|
debug: bool = False,
|
|
122
32
|
silent: bool = False,
|
|
123
33
|
loop: bool = False,
|
|
34
|
+
backend: str = "auto",
|
|
124
35
|
on_track_change: Optional[Callable] = None,
|
|
125
36
|
on_error: Optional[Callable] = None):
|
|
126
37
|
"""Initialize PiWave FM transmitter.
|
|
@@ -139,6 +50,8 @@ class PiWave:
|
|
|
139
50
|
:type silent: bool
|
|
140
51
|
:param loop: Loop the current track continuously (default: False)
|
|
141
52
|
:type loop: bool
|
|
53
|
+
:param backend: Chose a specific backend to handle the broadcast (default: auto)
|
|
54
|
+
:type backend: str
|
|
142
55
|
:param on_track_change: Callback function called when track changes
|
|
143
56
|
:type on_track_change: Optional[Callable]
|
|
144
57
|
:param on_error: Callback function called when an error occurs
|
|
@@ -162,21 +75,47 @@ class PiWave:
|
|
|
162
75
|
self.current_file: Optional[str] = None
|
|
163
76
|
self.is_playing = False
|
|
164
77
|
self.is_stopped = False
|
|
165
|
-
|
|
166
78
|
self.current_process: Optional[subprocess.Popen] = None
|
|
167
79
|
self.playback_thread: Optional[threading.Thread] = None
|
|
168
80
|
self.stop_event = threading.Event()
|
|
169
|
-
|
|
170
81
|
self.temp_dir = tempfile.mkdtemp(prefix="piwave_")
|
|
171
|
-
|
|
172
|
-
Log.config(silent=silent)
|
|
173
|
-
|
|
174
|
-
self.pi_fm_rds_path = self._find_pi_fm_rds_path()
|
|
175
82
|
|
|
83
|
+
Log.config(silent=silent)
|
|
84
|
+
|
|
176
85
|
self._validate_environment()
|
|
177
|
-
|
|
178
86
|
atexit.register(self.cleanup)
|
|
87
|
+
|
|
88
|
+
discover_backends()
|
|
89
|
+
|
|
90
|
+
if backend == "auto":
|
|
91
|
+
backend_name = get_best_backend("file_broadcast", self.frequency)
|
|
92
|
+
if not backend_name:
|
|
93
|
+
available = list(backends.keys())
|
|
94
|
+
raise PiWaveError(f"No suitable backend found for {self.frequency}MHz. Available backends: {available}")
|
|
95
|
+
else:
|
|
96
|
+
if backend not in backends:
|
|
97
|
+
available = list(backends.keys())
|
|
98
|
+
raise PiWaveError(f"Backend '{backend}' not available. Available: {available}. Use 'python3 -m piwave search' to refresh.")
|
|
99
|
+
|
|
100
|
+
# Validate that the chosen backend supports the frequency
|
|
101
|
+
backend_instance = backends[backend]()
|
|
102
|
+
min_freq, max_freq = backend_instance.frequency_range
|
|
103
|
+
if not (min_freq <= self.frequency <= max_freq):
|
|
104
|
+
raise PiWaveError(f"Backend '{backend}' doesn't support {self.frequency}MHz (supports {min_freq}-{max_freq}MHz)")
|
|
105
|
+
|
|
106
|
+
backend_name = backend
|
|
179
107
|
|
|
108
|
+
self.backend_name = backend_name
|
|
109
|
+
self.backend = backends[backend_name](
|
|
110
|
+
frequency=self.frequency,
|
|
111
|
+
ps=self.ps,
|
|
112
|
+
rt=self.rt,
|
|
113
|
+
pi=self.pi
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
min_freq, max_freq = self.backend.frequency_range
|
|
117
|
+
rds_support = "with RDS" if self.backend.supports_rds else "no RDS"
|
|
118
|
+
Log.info(f"Using {self.backend.name} backend ({min_freq}-{max_freq}MHz, {rds_support})")
|
|
180
119
|
|
|
181
120
|
Log.info(f"PiWave initialized - Frequency: {frequency}MHz, PS: {ps}, Loop: {loop}")
|
|
182
121
|
|
|
@@ -325,42 +264,38 @@ class PiWave:
|
|
|
325
264
|
Log.error(f"Could not determine duration for {wav_file}")
|
|
326
265
|
return False
|
|
327
266
|
|
|
328
|
-
cmd = [
|
|
329
|
-
'sudo', self.pi_fm_rds_path,
|
|
330
|
-
'-freq', str(self.frequency),
|
|
331
|
-
'-ps', self.ps,
|
|
332
|
-
'-rt', self.rt,
|
|
333
|
-
'-pi', self.pi,
|
|
334
|
-
'-audio', wav_file
|
|
335
|
-
]
|
|
336
|
-
|
|
337
267
|
try:
|
|
268
|
+
# update settings
|
|
269
|
+
self.backend.frequency = self.frequency
|
|
270
|
+
self.backend.ps = self.ps
|
|
271
|
+
self.backend.rt = self.rt
|
|
272
|
+
self.backend.pi = self.pi
|
|
273
|
+
|
|
274
|
+
# validate frequency
|
|
275
|
+
min_freq, max_freq = self.backend.frequency_range
|
|
276
|
+
if not (min_freq <= self.frequency <= max_freq):
|
|
277
|
+
raise PiWaveError(f"Current backend '{self.backend.name}' doesn't support {self.frequency}MHz (supports {min_freq}-{max_freq}MHz). Use update() to change backend or frequency.")
|
|
278
|
+
|
|
279
|
+
|
|
338
280
|
loop_status = "looping" if self.loop else f"Duration: {duration:.1f}s"
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
stderr=subprocess.PIPE,
|
|
344
|
-
preexec_fn=os.setsid
|
|
345
|
-
)
|
|
281
|
+
rds_info = f" (PS: {self.ps})" if self.backend.supports_rds and self.ps else ""
|
|
282
|
+
Log.broadcast_message(f"Playing {wav_file} ({loop_status}) at {self.frequency}MHz{rds_info}")
|
|
283
|
+
|
|
284
|
+
self.current_process = self.backend.play_file(wav_file)
|
|
346
285
|
|
|
347
286
|
if self.on_track_change:
|
|
348
287
|
self.on_track_change(wav_file)
|
|
349
288
|
|
|
350
289
|
if self.loop:
|
|
351
|
-
# if looping is enabled, let pi_fm_rds handle the looping
|
|
352
|
-
# and just wait for stop event
|
|
353
290
|
while not self.stop_event.is_set():
|
|
354
291
|
if self.stop_event.wait(timeout=0.1):
|
|
355
292
|
self._stop_current_process()
|
|
356
293
|
return False
|
|
357
294
|
|
|
358
|
-
# check if process exists cuz why not
|
|
359
295
|
if self.current_process.poll() is not None:
|
|
360
296
|
Log.error("Process ended unexpectedly while looping")
|
|
361
297
|
return False
|
|
362
298
|
else:
|
|
363
|
-
# if not looping wait for stopevent or end of file
|
|
364
299
|
start_time = time.time()
|
|
365
300
|
while True:
|
|
366
301
|
if self.stop_event.wait(timeout=0.1):
|
|
@@ -527,6 +462,7 @@ class PiWave:
|
|
|
527
462
|
debug: Optional[bool] = None,
|
|
528
463
|
silent: Optional[bool] = None,
|
|
529
464
|
loop: Optional[bool] = None,
|
|
465
|
+
backend: Optional[str] = None,
|
|
530
466
|
on_track_change: Optional[Callable] = None,
|
|
531
467
|
on_error: Optional[Callable] = None):
|
|
532
468
|
"""Update PiWave settings.
|
|
@@ -545,6 +481,8 @@ class PiWave:
|
|
|
545
481
|
:type silent: Optional[bool]
|
|
546
482
|
:param loop: Loop the current track continuously
|
|
547
483
|
:type loop: Optional[bool]
|
|
484
|
+
:param backend: Backend used to broadcast
|
|
485
|
+
:type backend: Optional[str]
|
|
548
486
|
:param on_track_change: Callback function called when track changes
|
|
549
487
|
:type on_track_change: Optional[Callable]
|
|
550
488
|
:param on_error: Callback function called when an error occurs
|
|
@@ -558,6 +496,35 @@ class PiWave:
|
|
|
558
496
|
>>> pw.update(rt="Updated radio text", debug=True, loop=True)
|
|
559
497
|
"""
|
|
560
498
|
updated_settings = []
|
|
499
|
+
|
|
500
|
+
freq_to_use = frequency if frequency is not None else self.frequency
|
|
501
|
+
|
|
502
|
+
if backend is not None:
|
|
503
|
+
if backend == "auto":
|
|
504
|
+
backend_name = get_best_backend("file_broadcast", freq_to_use)
|
|
505
|
+
if not backend_name:
|
|
506
|
+
available = list(backends.keys())
|
|
507
|
+
raise PiWaveError(f"No suitable backend found for {freq_to_use}MHz. Available: {available}")
|
|
508
|
+
else:
|
|
509
|
+
if backend not in backends:
|
|
510
|
+
available = list(backends.keys())
|
|
511
|
+
raise PiWaveError(f"Backend '{backend}' not available. Available: {available}")
|
|
512
|
+
backend_name = backend
|
|
513
|
+
|
|
514
|
+
backend_instance = backends[backend_name](
|
|
515
|
+
frequency=freq_to_use,
|
|
516
|
+
ps=ps or self.ps,
|
|
517
|
+
rt=rt or self.rt,
|
|
518
|
+
pi=pi or self.pi
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
min_freq, max_freq = backend_instance.frequency_range
|
|
522
|
+
if not (min_freq <= freq_to_use <= max_freq):
|
|
523
|
+
raise PiWaveError(f"Backend '{backend_name}' doesn't support {freq_to_use}MHz (supports {min_freq}-{max_freq}MHz)")
|
|
524
|
+
|
|
525
|
+
self.backend_name = backend_name
|
|
526
|
+
self.backend = backend_instance
|
|
527
|
+
updated_settings.append(f"backend: {backend_name}")
|
|
561
528
|
|
|
562
529
|
if frequency is not None:
|
|
563
530
|
self.frequency = frequency
|
|
@@ -643,6 +610,10 @@ class PiWave:
|
|
|
643
610
|
- **is_playing** (bool): Whether playback is active
|
|
644
611
|
- **frequency** (float): Current broadcast frequency
|
|
645
612
|
- **current_file** (str|None): Path of currently playing file
|
|
613
|
+
- **current_backend** (str): Currently used backend
|
|
614
|
+
- **backend_frequency_range** (str): Frequency range supported by the backend
|
|
615
|
+
- **backend_supports_rds** (bool): Backend support of Radio Data System
|
|
616
|
+
- **avalible_backends** (list): List of avalible backends
|
|
646
617
|
- **ps** (str): Program Service name
|
|
647
618
|
- **rt** (str): Radio Text message
|
|
648
619
|
- **pi** (str): Program Identification code
|
|
@@ -658,6 +629,10 @@ class PiWave:
|
|
|
658
629
|
'is_playing': self.is_playing,
|
|
659
630
|
'frequency': self.frequency,
|
|
660
631
|
'current_file': self.current_file,
|
|
632
|
+
'current_backend': self.backend_name,
|
|
633
|
+
'backend_frequency_range': f"{self.backend.frequency_range[0]}-{self.backend.frequency_range[1]}MHz",
|
|
634
|
+
'backend_supports_rds': self.backend.supports_rds,
|
|
635
|
+
'available_backends': list(backends.keys()),
|
|
661
636
|
'ps': self.ps,
|
|
662
637
|
'rt': self.rt,
|
|
663
638
|
'pi': self.pi,
|