piwave 2.1.3__py3-none-any.whl → 2.1.4__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/__main__.py CHANGED
@@ -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}")
piwave/backends/base.py CHANGED
@@ -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),
piwave/logger.py CHANGED
@@ -1,99 +1,46 @@
1
1
  # PiWave is available at https://piwave.xyz
2
2
  # Licensed under GPLv3.0, main GitHub repository at https://github.com/douxxtech/piwave/
3
- # piwave/Logger.py : Main logging manager
3
+ # piwave/Logger.py : Main logging manage
4
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
- }
5
+ from dlogger import DLogger
27
6
 
7
+ class Logger(DLogger):
8
+
28
9
  ICONS = {
29
10
  'success': 'OK',
30
11
  'error': 'ERR',
31
12
  'warning': 'WARN',
32
13
  'info': 'INFO',
33
- 'client': 'CLIENT',
34
- 'server': 'SERVER',
35
14
  'file': 'FILE',
36
- 'broadcast': 'BCAST',
37
- 'version': 'VER',
38
- 'update': 'UPD',
15
+ 'broadcast': 'BCAST'
39
16
  }
40
-
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
+
41
27
  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')
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()
piwave/piwave.py CHANGED
@@ -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):
@@ -214,7 +212,7 @@ class PiWave:
214
212
  if self._is_wav_file(filepath):
215
213
  return filepath
216
214
 
217
- Log.file_message(f"Converting {filepath} to WAV")
215
+ Log.file(f"Converting {filepath} to WAV")
218
216
 
219
217
  output_file = f"{os.path.splitext(filepath)[0]}_converted.wav"
220
218
 
@@ -267,35 +265,37 @@ class PiWave:
267
265
  def _play_file(self, wav_file: str) -> bool:
268
266
  if self.stop_event.is_set():
269
267
  return False
270
-
268
+
271
269
  duration = self._get_file_duration(wav_file)
270
+
272
271
  if duration <= 0:
273
272
  Log.error(f"Could not determine duration for {wav_file}")
274
273
  return False
275
-
274
+
276
275
  try:
277
276
  # update settings
278
277
  self.backend.frequency = self.frequency
279
278
  self.backend.ps = self.ps
280
279
  self.backend.rt = self.rt
281
280
  self.backend.pi = self.pi
282
-
281
+
283
282
  # validate frequency
284
283
  min_freq, max_freq = self.backend.frequency_range
285
284
  if not (min_freq <= self.frequency <= max_freq):
286
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.")
287
-
288
-
286
+
289
287
  loop_status = "looping" if self.loop else f"Duration: {duration:.1f}s"
290
288
  rds_info = f" (PS: {self.ps})" if self.backend.supports_rds and self.ps else ""
291
- Log.broadcast_message(f"Playing {wav_file} ({loop_status}) at {self.frequency}MHz{rds_info}")
292
-
293
- 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)
294
292
 
295
293
  if self.on_track_change:
296
294
  self.on_track_change(wav_file)
297
295
 
298
- 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
299
299
  while not self.stop_event.is_set():
300
300
  if self.stop_event.wait(timeout=0.1):
301
301
  self._stop_current_process()
@@ -304,26 +304,24 @@ class PiWave:
304
304
  if self.current_process.poll() is not None:
305
305
  Log.error("Process ended unexpectedly while looping")
306
306
  return False
307
+
307
308
  else:
308
- start_time = time.time()
309
- 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():
310
311
  if self.stop_event.wait(timeout=0.1):
311
312
  self._stop_current_process()
312
313
  return False
313
-
314
- elapsed = time.time() - start_time
315
- if elapsed >= duration:
316
- self._stop_current_process()
314
+ if not self.loop and self.current_process.poll() is not None:
317
315
  break
318
-
319
316
  return True
320
-
317
+
321
318
  except Exception as e:
322
319
  Log.error(f"Error playing {wav_file}: {e}")
323
320
  if self.on_error:
324
321
  self.on_error(e)
325
322
  self._stop_current_process()
326
323
  return False
324
+
327
325
 
328
326
  def _playback_worker_wrapper(self):
329
327
  # wrapper for non-blocking playback
@@ -397,7 +395,7 @@ class PiWave:
397
395
  consumer_thread.daemon = True
398
396
  consumer_thread.start()
399
397
 
400
- 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)")
401
399
  return True
402
400
 
403
401
  def _live_producer_worker(self, audio_source, chunk_size: int):
@@ -741,7 +739,7 @@ class PiWave:
741
739
  >>> pw.set_frequency(101.5)
742
740
  """
743
741
  self.frequency = frequency
744
- 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.")
745
743
 
746
744
  def set_loop(self, loop: bool):
747
745
  """Enable or disable looping for the current track.
@@ -758,7 +756,7 @@ class PiWave:
758
756
  """
759
757
  self.loop = loop
760
758
  loop_status = "enabled" if loop else "disabled"
761
- 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.")
762
760
 
763
761
  def get_status(self) -> dict:
764
762
  """Get current status information.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: piwave
3
- Version: 2.1.3
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"
@@ -614,6 +615,10 @@ class CustomBackend(Backend):
614
615
  @property
615
616
  def supports_rds(self):
616
617
  return True # RDS capability
618
+
619
+ @property
620
+ def supports_loop(self):
621
+ return True # loop support capability
617
622
 
618
623
  def _get_executable_name(self):
619
624
  return "my_transmitter"
@@ -621,10 +626,13 @@ class CustomBackend(Backend):
621
626
  def _get_search_paths(self):
622
627
  return ["/opt", "/usr/local/bin", "/usr/bin"]
623
628
 
624
- def build_command(self, wav_file: str):
629
+ def build_command(self, wav_file: str, loop: bool):
625
630
  cmd = ['sudo', self.required_executable, '-f', str(self.frequency)]
626
631
  if self.supports_rds and self.ps:
627
632
  cmd.extend(['-ps', self.ps])
633
+
634
+ if self.supports_rds and loop:
635
+ cmd.extend(['-loop'])
628
636
  cmd.append(wav_file)
629
637
  return cmd
630
638
  ```
@@ -0,0 +1,13 @@
1
+ piwave/__init__.py,sha256=jz2r-qclltKTxJjlGnqhAwIlUgjvRTR31f4hjTHP5NA,230
2
+ piwave/__main__.py,sha256=-Z3RI7TiicE5UwWc4dZaHX82-MpwVXWkfMZXOC457_Q,4089
3
+ piwave/logger.py,sha256=ipWzdNcEYob0xjXP8bbTq9enA3nryU-GKzAOKayRqlg,1149
4
+ piwave/piwave.py,sha256=LdBwjdP_MWC-FSacQYOUUHZUGEVk4wR8K5YmdkgJnzo,32434
5
+ piwave/backends/__init__.py,sha256=DUbdyYf2V2XcDB05vmFWEkuJ292YTNiNJjzh1raJ5Cg,3756
6
+ piwave/backends/base.py,sha256=ttpaxkE-x9l_rQROs3rkrz5qg8kpsIqbmMWyK90Xaf4,7775
7
+ piwave/backends/fm_transmitter.py,sha256=JzO5AswPzuoLOkTPLQrFy8r3trQvxXZrDPIGsrPDtmI,1419
8
+ piwave/backends/pi_fm_rds.py,sha256=GGcOeazzPBV0TF12EESaI_PldZyG_Jowh64cGMuF7R4,1475
9
+ piwave-2.1.4.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
10
+ piwave-2.1.4.dist-info/METADATA,sha256=6Pi4mVKXh-mflSfrpNuF4FAHArI-pEKg7-QBw7arJq0,20819
11
+ piwave-2.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ piwave-2.1.4.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
13
+ piwave-2.1.4.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- piwave/__init__.py,sha256=jz2r-qclltKTxJjlGnqhAwIlUgjvRTR31f4hjTHP5NA,230
2
- piwave/__main__.py,sha256=ygl6F8VmOZjsnnUbAbzLoA7_1qv18ruG6TW4EocSaQE,3927
3
- piwave/logger.py,sha256=lPG3cz3ByqC1p1UKpwlv3R9KgiAb0zzyu6c3bhTpST0,2766
4
- piwave/piwave.py,sha256=f3v0n3RBr7sxQ7D1jXunAmplxe0D-6_mdaDLJmGwVpM,32423
5
- piwave/backends/__init__.py,sha256=DUbdyYf2V2XcDB05vmFWEkuJ292YTNiNJjzh1raJ5Cg,3756
6
- piwave/backends/base.py,sha256=amjdR3pwx-0XN2ngpwfYNt74j5kWrW1cI0zMOTMU-q8,7668
7
- piwave/backends/fm_transmitter.py,sha256=6CuYpkCgb40PEpemMlbTkQ6Gq8qFe3C1TSSLLnwXXX4,1338
8
- piwave/backends/pi_fm_rds.py,sha256=l1y9JUKjw-d9lh9X7HLQNEwbNVO-whYlzaYnqEfRviI,1429
9
- piwave-2.1.3.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
10
- piwave-2.1.3.dist-info/METADATA,sha256=opQ0LdqJda66i1iaOGccjdw0gdZT9b7CKwDC1Ym_xWM,20612
11
- piwave-2.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- piwave-2.1.3.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
13
- piwave-2.1.3.dist-info/RECORD,,
File without changes