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/piwave.py CHANGED
@@ -1,5 +1,6 @@
1
- # piwave/pw.py
2
- # pi_fm_rds is required !!! Check https://github.com/ChristopheJacquet/PiFmRds
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
- class Log:
19
- COLORS = { # absolutely not taken from stackoverflow trust
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
- Log.broadcast_message(f"Playing {wav_file} ({loop_status}) at {self.frequency}MHz")
340
- self.current_process = subprocess.Popen(
341
- cmd,
342
- stdout=subprocess.PIPE,
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,