piwave 2.1.2__tar.gz → 2.1.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: piwave
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: A python module to broadcast radio waves with your Raspberry Pi.
5
5
  Home-page: https://github.com/douxxtech/piwave
6
6
  Author: Douxx
@@ -18,6 +18,7 @@ Classifier: Operating System :: POSIX :: Linux
18
18
  Requires-Python: >=3.7
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
+ Requires-Dist: dlogger==1.0.1
21
22
  Provides-Extra: dev
22
23
  Requires-Dist: pytest>=6.0; extra == "dev"
23
24
  Requires-Dist: pytest-cov>=2.0; extra == "dev"
@@ -518,8 +519,11 @@ status = pw.get_status()
518
519
  ##### `cleanup()`
519
520
  Clean up resources and temporary files.
520
521
 
522
+ > [!WARNING]
523
+ > Always clean up behind you! Dropped support of auto-cleanup on version > 2.1.2
524
+
521
525
  ```python
522
- pw.cleanup() # Called automatically on object destruction
526
+ pw.cleanup()
523
527
  ```
524
528
 
525
529
  ### Backend Management
@@ -611,6 +615,10 @@ class CustomBackend(Backend):
611
615
  @property
612
616
  def supports_rds(self):
613
617
  return True # RDS capability
618
+
619
+ @property
620
+ def supports_loop(self):
621
+ return True # loop support capability
614
622
 
615
623
  def _get_executable_name(self):
616
624
  return "my_transmitter"
@@ -618,10 +626,13 @@ class CustomBackend(Backend):
618
626
  def _get_search_paths(self):
619
627
  return ["/opt", "/usr/local/bin", "/usr/bin"]
620
628
 
621
- def build_command(self, wav_file: str):
629
+ def build_command(self, wav_file: str, loop: bool):
622
630
  cmd = ['sudo', self.required_executable, '-f', str(self.frequency)]
623
631
  if self.supports_rds and self.ps:
624
632
  cmd.extend(['-ps', self.ps])
633
+
634
+ if self.supports_rds and loop:
635
+ cmd.extend(['-loop'])
625
636
  cmd.append(wav_file)
626
637
  return cmd
627
638
  ```
@@ -491,8 +491,11 @@ status = pw.get_status()
491
491
  ##### `cleanup()`
492
492
  Clean up resources and temporary files.
493
493
 
494
+ > [!WARNING]
495
+ > Always clean up behind you! Dropped support of auto-cleanup on version > 2.1.2
496
+
494
497
  ```python
495
- pw.cleanup() # Called automatically on object destruction
498
+ pw.cleanup()
496
499
  ```
497
500
 
498
501
  ### Backend Management
@@ -584,6 +587,10 @@ class CustomBackend(Backend):
584
587
  @property
585
588
  def supports_rds(self):
586
589
  return True # RDS capability
590
+
591
+ @property
592
+ def supports_loop(self):
593
+ return True # loop support capability
587
594
 
588
595
  def _get_executable_name(self):
589
596
  return "my_transmitter"
@@ -591,10 +598,13 @@ class CustomBackend(Backend):
591
598
  def _get_search_paths(self):
592
599
  return ["/opt", "/usr/local/bin", "/usr/bin"]
593
600
 
594
- def build_command(self, wav_file: str):
601
+ def build_command(self, wav_file: str, loop: bool):
595
602
  cmd = ['sudo', self.required_executable, '-f', str(self.frequency)]
596
603
  if self.supports_rds and self.ps:
597
604
  cmd.extend(['-ps', self.ps])
605
+
606
+ if self.supports_rds and loop:
607
+ cmd.extend(['-loop'])
598
608
  cmd.append(wav_file)
599
609
  return cmd
600
610
  ```
@@ -72,6 +72,7 @@ def main():
72
72
 
73
73
  args = parser.parse_args(sys.argv[2:])
74
74
 
75
+ pw = None
75
76
  try:
76
77
  pw = PiWave(
77
78
  frequency=args.frequency,
@@ -92,9 +93,17 @@ def main():
92
93
  except KeyboardInterrupt:
93
94
  pw.stop()
94
95
  Log.info("Broadcast stopped")
96
+ raise
97
+
95
98
  except PiWaveError as e:
96
99
  Log.error(f"PiWaveError: {e}")
97
100
  sys.exit(1)
101
+ except Exception:
102
+ pass
103
+ finally:
104
+ if pw is not None:
105
+ pw.cleanup()
106
+
98
107
 
99
108
  else:
100
109
  Log.error(f"Unknown command: {cmd}")
@@ -43,6 +43,11 @@ class Backend(ABC):
43
43
  @abstractmethod
44
44
  def supports_live_streaming(self):
45
45
  pass
46
+
47
+ @property
48
+ @abstractmethod
49
+ def supports_loop(self):
50
+ pass
46
51
 
47
52
  @property
48
53
  def cache_file(self):
@@ -208,7 +213,7 @@ class Backend(ABC):
208
213
  return False
209
214
 
210
215
  @abstractmethod
211
- def build_command(self, wav_file: str):
216
+ def build_command(self, wav_file: str, loop: bool):
212
217
  pass
213
218
 
214
219
  @abstractmethod
@@ -219,12 +224,12 @@ class Backend(ABC):
219
224
  min_freq, max_freq = self.frequency_range
220
225
  return min_freq <= self.frequency <= max_freq
221
226
 
222
- def play_file(self, wav_file: str) -> subprocess.Popen:
227
+ def play_file(self, wav_file: str, loop: bool) -> subprocess.Popen:
223
228
  if not self.validate_settings():
224
229
  min_freq, max_freq = self.frequency_range
225
230
  raise BackendError(f"{self.name} supports {min_freq}-{max_freq}MHz, got {self.frequency}MHz")
226
231
 
227
- cmd = self.build_command(wav_file)
232
+ cmd = self.build_command(wav_file, loop)
228
233
  self.current_process = subprocess.Popen(
229
234
  cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
230
235
  preexec_fn=os.setsid
@@ -23,13 +23,17 @@ class FmTransmitterBackend(Backend):
23
23
  def supports_live_streaming(self):
24
24
  return True
25
25
 
26
+ @property
27
+ def supports_loop(self):
28
+ return False
29
+
26
30
  def _get_executable_name(self):
27
31
  return "fm_transmitter"
28
32
 
29
33
  def _get_search_paths(self):
30
34
  return ["/opt/PiWave/fm_transmitter", "/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home"]
31
35
 
32
- def build_command(self, wav_file: str):
36
+ def build_command(self, wav_file: str, loop: bool):
33
37
  return [
34
38
  'sudo', self.required_executable,
35
39
  '-f', str(self.frequency),
@@ -13,7 +13,7 @@ class PiFmRdsBackend(Backend):
13
13
 
14
14
  @property
15
15
  def frequency_range(self):
16
- return (80.0, 108.0) # standard fm band with a bit less bcs it still kinda works
16
+ return (76.0, 108.0) # taken from pi_fm_rds.c
17
17
 
18
18
  @property
19
19
  def supports_rds(self):
@@ -23,13 +23,17 @@ class PiFmRdsBackend(Backend):
23
23
  def supports_live_streaming(self):
24
24
  return False
25
25
 
26
+ @property
27
+ def supports_loop(self):
28
+ return False
29
+
26
30
  def _get_executable_name(self):
27
31
  return "pi_fm_rds"
28
32
 
29
33
  def _get_search_paths(self):
30
34
  return ["/opt/PiWave/PiFmRds", "/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home"]
31
35
 
32
- def build_command(self, wav_file: str) -> list:
36
+ def build_command(self, wav_file: str, loop: bool) -> list:
33
37
  cmd = [
34
38
  'sudo', self.required_executable,
35
39
  '-freq', str(self.frequency),
@@ -0,0 +1,46 @@
1
+ # PiWave is available at https://piwave.xyz
2
+ # Licensed under GPLv3.0, main GitHub repository at https://github.com/douxxtech/piwave/
3
+ # piwave/Logger.py : Main logging manage
4
+
5
+ from dlogger import DLogger
6
+
7
+ class Logger(DLogger):
8
+
9
+ ICONS = {
10
+ 'success': 'OK',
11
+ 'error': 'ERR',
12
+ 'warning': 'WARN',
13
+ 'info': 'INFO',
14
+ 'file': 'FILE',
15
+ 'broadcast': 'BCAST'
16
+ }
17
+
18
+ STYLES = {
19
+ 'success': 'bright_green',
20
+ 'error': 'bright_red',
21
+ 'warning': 'bright_yellow',
22
+ 'info': 'bright_cyan',
23
+ 'file': 'yellow',
24
+ 'broadcast': 'bright_magenta'
25
+ }
26
+
27
+ SILENT = False
28
+
29
+ def __init__(self):
30
+ # Initialize with prebuilt icons & styles and silent support.
31
+
32
+ super().__init__(
33
+ icons=self.ICONS,
34
+ styles=self.STYLES
35
+ )
36
+
37
+ @classmethod
38
+ def config(self, silent: bool = False):
39
+ self.SILENT = silent
40
+
41
+ def print(self, message: str, style: str = '', icon: str = '', end: str = '\n'):
42
+ if self.SILENT:
43
+ return
44
+ super().print(message, style, icon, end)
45
+
46
+ Log = Logger()
@@ -13,10 +13,8 @@ import shutil
13
13
  import queue
14
14
  from typing import Optional, Callable
15
15
  from pathlib import Path
16
- from urllib.parse import urlparse
17
- import atexit
18
16
 
19
- from .backends import discover_backends, backends, get_best_backend, search_backends, list_backends
17
+ from .backends import discover_backends, backends, get_best_backend
20
18
  from .logger import Log
21
19
 
22
20
  class PiWaveError(Exception):
@@ -90,7 +88,6 @@ class PiWave:
90
88
  Log.config(silent=silent)
91
89
 
92
90
  self._validate_environment()
93
- atexit.register(self.cleanup)
94
91
 
95
92
  discover_backends()
96
93
 
@@ -215,7 +212,7 @@ class PiWave:
215
212
  if self._is_wav_file(filepath):
216
213
  return filepath
217
214
 
218
- Log.file_message(f"Converting {filepath} to WAV")
215
+ Log.file(f"Converting {filepath} to WAV")
219
216
 
220
217
  output_file = f"{os.path.splitext(filepath)[0]}_converted.wav"
221
218
 
@@ -268,35 +265,37 @@ class PiWave:
268
265
  def _play_file(self, wav_file: str) -> bool:
269
266
  if self.stop_event.is_set():
270
267
  return False
271
-
268
+
272
269
  duration = self._get_file_duration(wav_file)
270
+
273
271
  if duration <= 0:
274
272
  Log.error(f"Could not determine duration for {wav_file}")
275
273
  return False
276
-
274
+
277
275
  try:
278
276
  # update settings
279
277
  self.backend.frequency = self.frequency
280
278
  self.backend.ps = self.ps
281
279
  self.backend.rt = self.rt
282
280
  self.backend.pi = self.pi
283
-
281
+
284
282
  # validate frequency
285
283
  min_freq, max_freq = self.backend.frequency_range
286
284
  if not (min_freq <= self.frequency <= max_freq):
287
285
  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.")
288
-
289
-
286
+
290
287
  loop_status = "looping" if self.loop else f"Duration: {duration:.1f}s"
291
288
  rds_info = f" (PS: {self.ps})" if self.backend.supports_rds and self.ps else ""
292
- Log.broadcast_message(f"Playing {wav_file} ({loop_status}) at {self.frequency}MHz{rds_info}")
293
-
294
- self.current_process = self.backend.play_file(wav_file)
289
+ Log.broadcast(f"Playing {wav_file} ({loop_status}) at {self.frequency}MHz{rds_info}")
290
+
291
+ self.current_process = self.backend.play_file(wav_file, self.loop)
295
292
 
296
293
  if self.on_track_change:
297
294
  self.on_track_change(wav_file)
298
295
 
299
- if self.loop:
296
+ if self.loop and not self.backend.supports_loop:
297
+
298
+ # Only manually loop if the backend does not support it
300
299
  while not self.stop_event.is_set():
301
300
  if self.stop_event.wait(timeout=0.1):
302
301
  self._stop_current_process()
@@ -305,26 +304,24 @@ class PiWave:
305
304
  if self.current_process.poll() is not None:
306
305
  Log.error("Process ended unexpectedly while looping")
307
306
  return False
307
+
308
308
  else:
309
- start_time = time.time()
310
- while True:
309
+ # fi backend supports looping or we are not looping, just wait for the process to finish
310
+ while not self.stop_event.is_set():
311
311
  if self.stop_event.wait(timeout=0.1):
312
312
  self._stop_current_process()
313
313
  return False
314
-
315
- elapsed = time.time() - start_time
316
- if elapsed >= duration:
317
- self._stop_current_process()
314
+ if not self.loop and self.current_process.poll() is not None:
318
315
  break
319
-
320
316
  return True
321
-
317
+
322
318
  except Exception as e:
323
319
  Log.error(f"Error playing {wav_file}: {e}")
324
320
  if self.on_error:
325
321
  self.on_error(e)
326
322
  self._stop_current_process()
327
323
  return False
324
+
328
325
 
329
326
  def _playback_worker_wrapper(self):
330
327
  # wrapper for non-blocking playback
@@ -398,7 +395,7 @@ class PiWave:
398
395
  consumer_thread.daemon = True
399
396
  consumer_thread.start()
400
397
 
401
- Log.broadcast_message(f"Live streaming at {self.frequency}MHz ({sample_rate}Hz, {channels}ch)")
398
+ Log.broadcast(f"Live streaming at {self.frequency}MHz ({sample_rate}Hz, {channels}ch)")
402
399
  return True
403
400
 
404
401
  def _live_producer_worker(self, audio_source, chunk_size: int):
@@ -465,11 +462,9 @@ class PiWave:
465
462
  def _stop_current_process(self):
466
463
  if self.current_process:
467
464
  try:
468
- Log.info("Stopping current process...")
469
465
  os.killpg(os.getpgid(self.current_process.pid), signal.SIGTERM)
470
466
  self.current_process.wait(timeout=5)
471
467
  except (ProcessLookupError, subprocess.TimeoutExpired):
472
- Log.warning("Forcing kill of current process")
473
468
  try:
474
469
  os.killpg(os.getpgid(self.current_process.pid), signal.SIGKILL)
475
470
  except ProcessLookupError:
@@ -505,11 +500,6 @@ class PiWave:
505
500
  self._log_debug("Playback worker finished")
506
501
 
507
502
 
508
- def _handle_interrupt(self, signum, frame):
509
- Log.warning("Interrupt received, stopping playback...")
510
- self.stop()
511
- os._exit(0)
512
-
513
503
  def play(self, source, sample_rate: int = 44100, channels: int = 2, chunk_size: int = 4096, blocking: bool = False):
514
504
  """Play audio from file or live source.
515
505
 
@@ -559,7 +549,8 @@ class PiWave:
559
549
  Example:
560
550
  >>> pw.stop()
561
551
  """
562
- if not self.is_playing and not self.is_live_streaming:
552
+
553
+ if not self.is_playing and not self.is_live_streaming and not self.current_process:
563
554
  return
564
555
 
565
556
  Log.warning("Stopping...")
@@ -748,7 +739,7 @@ class PiWave:
748
739
  >>> pw.set_frequency(101.5)
749
740
  """
750
741
  self.frequency = frequency
751
- Log.broadcast_message(f"Frequency changed to {frequency}MHz. Will update on next file's broadcast.")
742
+ Log.broadcast(f"Frequency changed to {frequency}MHz. Will update on next file's broadcast.")
752
743
 
753
744
  def set_loop(self, loop: bool):
754
745
  """Enable or disable looping for the current track.
@@ -765,7 +756,7 @@ class PiWave:
765
756
  """
766
757
  self.loop = loop
767
758
  loop_status = "enabled" if loop else "disabled"
768
- Log.broadcast_message(f"Looping {loop_status}. Will update on next file's broadcast.")
759
+ Log.broadcast(f"Looping {loop_status}. Will update on next file's broadcast.")
769
760
 
770
761
  def get_status(self) -> dict:
771
762
  """Get current status information.
@@ -825,8 +816,6 @@ class PiWave:
825
816
 
826
817
  Log.info("Cleanup completed")
827
818
 
828
- def __del__(self):
829
- self.cleanup()
830
819
 
831
820
  def send(self, file_path: str):
832
821
  """Alias for the play method.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: piwave
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: A python module to broadcast radio waves with your Raspberry Pi.
5
5
  Home-page: https://github.com/douxxtech/piwave
6
6
  Author: Douxx
@@ -18,6 +18,7 @@ Classifier: Operating System :: POSIX :: Linux
18
18
  Requires-Python: >=3.7
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
+ Requires-Dist: dlogger==1.0.1
21
22
  Provides-Extra: dev
22
23
  Requires-Dist: pytest>=6.0; extra == "dev"
23
24
  Requires-Dist: pytest-cov>=2.0; extra == "dev"
@@ -518,8 +519,11 @@ status = pw.get_status()
518
519
  ##### `cleanup()`
519
520
  Clean up resources and temporary files.
520
521
 
522
+ > [!WARNING]
523
+ > Always clean up behind you! Dropped support of auto-cleanup on version > 2.1.2
524
+
521
525
  ```python
522
- pw.cleanup() # Called automatically on object destruction
526
+ pw.cleanup()
523
527
  ```
524
528
 
525
529
  ### Backend Management
@@ -611,6 +615,10 @@ class CustomBackend(Backend):
611
615
  @property
612
616
  def supports_rds(self):
613
617
  return True # RDS capability
618
+
619
+ @property
620
+ def supports_loop(self):
621
+ return True # loop support capability
614
622
 
615
623
  def _get_executable_name(self):
616
624
  return "my_transmitter"
@@ -618,10 +626,13 @@ class CustomBackend(Backend):
618
626
  def _get_search_paths(self):
619
627
  return ["/opt", "/usr/local/bin", "/usr/bin"]
620
628
 
621
- def build_command(self, wav_file: str):
629
+ def build_command(self, wav_file: str, loop: bool):
622
630
  cmd = ['sudo', self.required_executable, '-f', str(self.frequency)]
623
631
  if self.supports_rds and self.ps:
624
632
  cmd.extend(['-ps', self.ps])
633
+
634
+ if self.supports_rds and loop:
635
+ cmd.extend(['-loop'])
625
636
  cmd.append(wav_file)
626
637
  return cmd
627
638
  ```
@@ -1,3 +1,4 @@
1
+ dlogger==1.0.1
1
2
 
2
3
  [dev]
3
4
  pytest>=6.0
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = piwave
3
- version = 2.1.2
3
+ version = 2.1.4
4
4
  description = A python module to broadcast radio waves with your Raspberry Pi.
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -24,6 +24,7 @@ project_urls =
24
24
  packages = find:
25
25
  include_package_data = True
26
26
  install_requires =
27
+ dlogger==1.0.1
27
28
  python_requires = >=3.7
28
29
 
29
30
  [options.extras_require]
@@ -1,99 +0,0 @@
1
- # PiWave is available at https://piwave.xyz
2
- # Licensed under GPLv3.0, main GitHub repository at https://github.com/douxxtech/piwave/
3
- # piwave/Logger.py : Main logging manager
4
-
5
- import sys
6
-
7
- class Log:
8
- COLORS = { # absolutely not taken from stackoverflow trust
9
- 'reset': '\033[0m',
10
- 'bold': '\033[1m',
11
- 'underline': '\033[4m',
12
- 'red': '\033[31m',
13
- 'green': '\033[32m',
14
- 'yellow': '\033[33m',
15
- 'blue': '\033[34m',
16
- 'magenta': '\033[35m',
17
- 'cyan': '\033[36m',
18
- 'white': '\033[37m',
19
- 'bright_red': '\033[91m',
20
- 'bright_green': '\033[92m',
21
- 'bright_yellow': '\033[93m',
22
- 'bright_blue': '\033[94m',
23
- 'bright_magenta': '\033[95m',
24
- 'bright_cyan': '\033[96m',
25
- 'bright_white': '\033[97m',
26
- }
27
-
28
- ICONS = {
29
- 'success': 'OK',
30
- 'error': 'ERR',
31
- 'warning': 'WARN',
32
- 'info': 'INFO',
33
- 'client': 'CLIENT',
34
- 'server': 'SERVER',
35
- 'file': 'FILE',
36
- 'broadcast': 'BCAST',
37
- 'version': 'VER',
38
- 'update': 'UPD',
39
- }
40
-
41
- SILENT = False
42
-
43
- @classmethod
44
- def config(cls, silent: bool = False):
45
- cls.SILENT = silent
46
-
47
- @classmethod
48
- def print(cls, message: str, style: str = '', icon: str = '', end: str = '\n'):
49
-
50
- if cls.SILENT: return
51
-
52
- color = cls.COLORS.get(style, '')
53
- icon_char = cls.ICONS.get(icon, '')
54
- if icon_char:
55
- if color:
56
- print(f"{color}[{icon_char}]\033[0m {message}", end=end)
57
- else:
58
- print(f"[{icon_char}] {message}", end=end)
59
- else:
60
- if color:
61
- print(f"{color}{message}\033[0m", end=end)
62
- else:
63
- print(f"{message}", end=end)
64
- sys.stdout.flush()
65
-
66
- @classmethod
67
- def header(cls, text: str):
68
- cls.print(text, 'bright_blue', end='\n\n')
69
- sys.stdout.flush()
70
-
71
- @classmethod
72
- def section(cls, text: str):
73
- cls.print(f" {text} ", 'bright_blue', end='')
74
- cls.print("─" * (len(text) + 2), 'blue', end='\n\n')
75
- sys.stdout.flush()
76
-
77
- @classmethod
78
- def success(cls, message: str):
79
- cls.print(message, 'bright_green', 'success')
80
-
81
- @classmethod
82
- def error(cls, message: str):
83
- cls.print(message, 'bright_red', 'error')
84
-
85
- @classmethod
86
- def warning(cls, message: str):
87
- cls.print(message, 'bright_yellow', 'warning')
88
-
89
- @classmethod
90
- def info(cls, message: str):
91
- cls.print(message, 'bright_cyan', 'info')
92
-
93
- @classmethod
94
- def file_message(cls, message: str):
95
- cls.print(message, 'yellow', 'file')
96
-
97
- @classmethod
98
- def broadcast_message(cls, message: str):
99
- cls.print(message, 'bright_magenta', 'broadcast')
File without changes
File without changes
File without changes
File without changes