piwave 2.1.0__py3-none-any.whl → 2.1.1__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.
@@ -21,13 +21,20 @@ def get_best_backend(mode: str, frequency: float):
21
21
  return "pi_fm_rds"
22
22
  elif "fm_transmitter" in backends:
23
23
  return "fm_transmitter"
24
-
25
24
  else:
26
25
  if "fm_transmitter" in backends:
27
26
  return "fm_transmitter"
28
27
  elif "pi_fm_rds" in backends:
29
28
  return "pi_fm_rds"
30
29
 
30
+ elif mode == "live_broadcast":
31
+ if "fm_transmitter" in backends:
32
+ backend = backends["fm_transmitter"]()
33
+ min_freq, max_freq = backend.frequency_range
34
+ if min_freq <= frequency <= max_freq:
35
+ return "fm_transmitter"
36
+
37
+ return None
31
38
 
32
39
  return None
33
40
 
piwave/backends/base.py CHANGED
@@ -38,6 +38,11 @@ class Backend(ABC):
38
38
  @abstractmethod
39
39
  def supports_rds(self):
40
40
  pass
41
+
42
+ @property
43
+ @abstractmethod
44
+ def supports_live_streaming(self):
45
+ pass
41
46
 
42
47
  @property
43
48
  def cache_file(self):
@@ -205,6 +210,10 @@ class Backend(ABC):
205
210
  @abstractmethod
206
211
  def build_command(self, wav_file: str):
207
212
  pass
213
+
214
+ @abstractmethod
215
+ def build_live_command(self):
216
+ pass
208
217
 
209
218
  def validate_settings(self):
210
219
  min_freq, max_freq = self.frequency_range
@@ -19,15 +19,26 @@ class FmTransmitterBackend(Backend):
19
19
  def supports_rds(self):
20
20
  return False
21
21
 
22
+ @property
23
+ def supports_live_streaming(self):
24
+ return True
25
+
22
26
  def _get_executable_name(self):
23
27
  return "fm_transmitter"
24
28
 
25
29
  def _get_search_paths(self):
26
- return ["/opt/PiWave/fm_transmitter", "/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home", "/home/pi"]
30
+ return ["/opt/PiWave/fm_transmitter", "/opt", "/usr/local/bin", "/usr/bin", "/bin", "/home"]
27
31
 
28
32
  def build_command(self, wav_file: str):
29
33
  return [
30
34
  'sudo', self.required_executable,
31
35
  '-f', str(self.frequency),
32
36
  wav_file
37
+ ]
38
+
39
+ def build_live_command(self):
40
+ return [
41
+ 'sudo', self.required_executable,
42
+ '-f', str(self.frequency),
43
+ '-'
33
44
  ]
@@ -19,6 +19,10 @@ class PiFmRdsBackend(Backend):
19
19
  def supports_rds(self):
20
20
  return True
21
21
 
22
+ @property
23
+ def supports_live_streaming(self):
24
+ return False
25
+
22
26
  def _get_executable_name(self):
23
27
  return "pi_fm_rds"
24
28
 
@@ -40,3 +44,6 @@ class PiFmRdsBackend(Backend):
40
44
  cmd.extend(['-pi', self.pi])
41
45
 
42
46
  return cmd
47
+
48
+ def build_live_command(self):
49
+ return None # not supported sadly
piwave/piwave.py CHANGED
@@ -10,7 +10,7 @@ import time
10
10
 
11
11
  import tempfile
12
12
  import shutil
13
- import sys
13
+ import queue
14
14
  from typing import Optional, Callable
15
15
  from pathlib import Path
16
16
  from urllib.parse import urlparse
@@ -24,16 +24,17 @@ class PiWaveError(Exception):
24
24
 
25
25
  class PiWave:
26
26
  def __init__(self,
27
- frequency: float = 90.0,
28
- ps: str = "PiWave",
29
- rt: str = "PiWave: The best python module for managing your pi radio",
30
- pi: str = "FFFF",
31
- debug: bool = False,
32
- silent: bool = False,
33
- loop: bool = False,
34
- backend: str = "auto",
35
- on_track_change: Optional[Callable] = None,
36
- on_error: Optional[Callable] = None):
27
+ frequency: float = 90.0,
28
+ ps: str = "PiWave",
29
+ rt: str = "PiWave: The best python module for managing your pi radio",
30
+ pi: str = "FFFF",
31
+ debug: bool = False,
32
+ silent: bool = False,
33
+ loop: bool = False,
34
+ backend: str = "auto",
35
+ used_for: str = "file_broadcast",
36
+ on_track_change: Optional[Callable] = None,
37
+ on_error: Optional[Callable] = None):
37
38
  """Initialize PiWave FM transmitter.
38
39
 
39
40
  :param frequency: FM frequency to broadcast on (80.0-108.0 MHz)
@@ -50,7 +51,9 @@ class PiWave:
50
51
  :type silent: bool
51
52
  :param loop: Loop the current track continuously (default: False)
52
53
  :type loop: bool
53
- :param backend: Chose a specific backend to handle the broadcast (default: auto)
54
+ :param backend: Chose a specific backend to handle the broadcast (default: auto). Supports `pi_fm_rds`, `fm_transmitter` and `auto`
55
+ :type backend: str
56
+ :param backend: Give the main use for the current instance, will be used if backend: auto (default: file_broadcast). Supports `file_broadcast` and `live_broadcast`
54
57
  :type backend: str
55
58
  :param on_track_change: Callback function called when track changes
56
59
  :type on_track_change: Optional[Callable]
@@ -59,7 +62,7 @@ class PiWave:
59
62
  :raises PiWaveError: If not running on Raspberry Pi or without root privileges
60
63
 
61
64
  .. note::
62
- This class requires pi_fm_rds to be installed and accessible.
65
+ This class requires pi_fm_rds or fm_transmitter to be installed and accessible.
63
66
  Must be run on a Raspberry Pi with root privileges.
64
67
  """
65
68
 
@@ -79,6 +82,10 @@ class PiWave:
79
82
  self.playback_thread: Optional[threading.Thread] = None
80
83
  self.stop_event = threading.Event()
81
84
  self.temp_dir = tempfile.mkdtemp(prefix="piwave_")
85
+
86
+ self.is_live_streaming = False
87
+ self.live_thread: Optional[threading.Thread] = None
88
+ self.audio_queue: Optional[queue.Queue] = None
82
89
 
83
90
  Log.config(silent=silent)
84
91
 
@@ -87,11 +94,13 @@ class PiWave:
87
94
 
88
95
  discover_backends()
89
96
 
97
+ self.backend_use = used_for
98
+
90
99
  if backend == "auto":
91
- backend_name = get_best_backend("file_broadcast", self.frequency)
100
+ backend_name = get_best_backend(self.backend_use, self.frequency)
92
101
  if not backend_name:
93
102
  available = list(backends.keys())
94
- raise PiWaveError(f"No suitable backend found for {self.frequency}MHz. Available backends: {available}")
103
+ raise PiWaveError(f"No suitable backend found for {self.frequency}MHz and {self.backend_use} mode. Available backends: {available}")
95
104
  else:
96
105
  if backend not in backends:
97
106
  available = list(backends.keys())
@@ -113,6 +122,7 @@ class PiWave:
113
122
  pi=self.pi
114
123
  )
115
124
 
125
+
116
126
  min_freq, max_freq = self.backend.frequency_range
117
127
  rds_support = "with RDS" if self.backend.supports_rds else "no RDS"
118
128
  Log.info(f"Using {self.backend.name} backend ({min_freq}-{max_freq}MHz, {rds_support})")
@@ -255,7 +265,7 @@ class PiWave:
255
265
  except (subprocess.CalledProcessError, subprocess.TimeoutExpired, ValueError):
256
266
  return 0.0
257
267
 
258
- def _play_wav(self, wav_file: str) -> bool:
268
+ def _play_file(self, wav_file: str) -> bool:
259
269
  if self.stop_event.is_set():
260
270
  return False
261
271
 
@@ -315,6 +325,119 @@ class PiWave:
315
325
  self.on_error(e)
316
326
  self._stop_current_process()
317
327
  return False
328
+
329
+
330
+ def _play_live(self, audio_source, sample_rate: int, channels: int, chunk_size: int) -> bool:
331
+ if self.is_playing or self.is_live_streaming:
332
+ self.stop()
333
+
334
+ if not self.backend.supports_live_streaming:
335
+ raise PiWaveError(
336
+ f"Backend '{self.backend_name}' doesn't support live streaming. Try using fm_transmitter instead.")
337
+
338
+
339
+ min_freq, max_freq = self.backend.frequency_range
340
+ if not (min_freq <= self.frequency <= max_freq):
341
+ raise PiWaveError(
342
+ f"Backend '{self.backend_name}' doesn't support {self.frequency}MHz"
343
+ )
344
+
345
+ self.stop_event.clear()
346
+ self.is_live_streaming = True
347
+ self.audio_queue = queue.Queue(maxsize=20)
348
+
349
+ try:
350
+ cmd = self.backend.build_live_command()
351
+ if not cmd:
352
+ raise PiWaveError(f"Backend doesn't support live streaming") # since we checked before, we shouldnt get this but meh
353
+
354
+ self.current_process = subprocess.Popen(
355
+ cmd,
356
+ stdin=subprocess.PIPE,
357
+ stdout=subprocess.PIPE,
358
+ stderr=subprocess.PIPE,
359
+ preexec_fn=os.setsid
360
+ )
361
+ except Exception as e:
362
+ Log.error(f"Failed to start live stream: {e}")
363
+ self.is_live_streaming = False
364
+ if self.on_error:
365
+ self.on_error(e)
366
+ return False
367
+
368
+ self.live_thread = threading.Thread(
369
+ target=self._live_producer_worker,
370
+ args=(audio_source, chunk_size)
371
+ )
372
+ self.live_thread.daemon = True
373
+ self.live_thread.start()
374
+
375
+ consumer_thread = threading.Thread(target=self._live_consumer_worker)
376
+ consumer_thread.daemon = True
377
+ consumer_thread.start()
378
+
379
+ Log.broadcast_message(f"Live streaming at {self.frequency}MHz ({sample_rate}Hz, {channels}ch)")
380
+ return True
381
+
382
+ def _live_producer_worker(self, audio_source, chunk_size: int):
383
+ # producer: reads from audio source, puts in queue; consumer will play it
384
+ try:
385
+ if hasattr(audio_source, '__iter__') and not isinstance(audio_source, (str, bytes)):
386
+ for chunk in audio_source:
387
+ if self.stop_event.is_set():
388
+ break
389
+ if chunk:
390
+ self.audio_queue.put(chunk, timeout=1)
391
+
392
+ elif callable(audio_source):
393
+ while not self.stop_event.is_set():
394
+ chunk = audio_source()
395
+ if not chunk:
396
+ break
397
+ self.audio_queue.put(chunk, timeout=1)
398
+
399
+ elif hasattr(audio_source, 'read'):
400
+ while not self.stop_event.is_set():
401
+ chunk = audio_source.read(chunk_size)
402
+ if not chunk:
403
+ break
404
+ self.audio_queue.put(chunk, timeout=1)
405
+
406
+ except Exception as e:
407
+ Log.error(f"Producer error: {e}")
408
+ if self.on_error:
409
+ self.on_error(e)
410
+ finally:
411
+ self.audio_queue.put(None)
412
+
413
+ def _live_consumer_worker(self):
414
+ # consumer reads from queue and puts in process
415
+ try:
416
+ while not self.stop_event.is_set():
417
+ try:
418
+ chunk = self.audio_queue.get(timeout=0.1)
419
+ if chunk is None:
420
+ break
421
+
422
+ if self.current_process and self.current_process.stdin:
423
+ self.current_process.stdin.write(chunk)
424
+ self.current_process.stdin.flush()
425
+
426
+ except queue.Empty:
427
+ continue
428
+ except BrokenPipeError:
429
+ Log.error(f"Stream process terminated: make sure that the stream you provided compiles with {self.backend_name} stdin support.")
430
+ break
431
+ except Exception as e:
432
+ Log.error(f"Write error: {e}")
433
+ break
434
+ finally:
435
+ if self.current_process and self.current_process.stdin:
436
+ try:
437
+ self.current_process.stdin.close()
438
+ except:
439
+ pass
440
+ self.is_live_streaming = False
318
441
 
319
442
 
320
443
  def _stop_current_process(self):
@@ -365,38 +488,28 @@ class PiWave:
365
488
  self.stop()
366
489
  os._exit(0)
367
490
 
368
- def play(self, file_path: str) -> bool:
369
- """Start playing the specified audio file.
370
-
371
- :param file_path: Path to local audio file
372
- :type file_path: str
373
- :return: True if playback started successfully, False otherwise
374
- :rtype: bool
491
+ def play(self, source, sample_rate: int = 44100, channels: int = 2, chunk_size: int = 4096):
492
+ """Play audio from file or live source.
375
493
 
376
- .. note::
377
- Files are automatically converted to WAV format if needed.
378
- Only local files are supported. If loop is enabled, the file will
379
- repeat continuously until stop() is called.
494
+ :param source: Either a file path (str) or live audio source (generator/callable/file-like)
495
+ :param sample_rate: Sample rate for live audio (ignored for files)
496
+ :param channels: Channels for live audio (ignored for files)
497
+ :param chunk_size: Chunk size for live audio (ignored for files)
498
+ :return: True if playback/streaming started successfully
499
+ :rtype: bool
380
500
 
381
501
  Example:
382
- >>> pw.play('song.mp3')
383
- >>> pw.play('audio.wav')
502
+ >>> pw.play('song.mp3') # File playback
503
+ >>> pw.play(mic_generator()) # Live streaming
384
504
  """
385
-
386
- if self.is_playing:
387
- self.stop()
388
-
389
- self.current_file = file_path
390
- self.stop_event.clear()
391
- self.is_stopped = False
392
- self.is_playing = True
393
-
394
- self.playback_thread = threading.Thread(target=self._playback_worker)
395
- self.playback_thread.daemon = True
396
- self.playback_thread.start()
397
-
398
- Log.success("Playback started")
399
- return True
505
+
506
+ # autodetect if source is live or file
507
+ if isinstance(source, str):
508
+ # file (string)
509
+ return self._play_file(source)
510
+ else:
511
+ # live
512
+ return self._play_live(source, sample_rate, channels, chunk_size)
400
513
 
401
514
  def stop(self):
402
515
  """Stop all playback and streaming.
@@ -407,14 +520,21 @@ class PiWave:
407
520
  Example:
408
521
  >>> pw.stop()
409
522
  """
410
- if not self.is_playing:
523
+ if not self.is_playing and not self.is_live_streaming:
411
524
  return
412
525
 
413
- Log.warning("Stopping playback...")
414
-
526
+ Log.warning("Stopping...")
527
+
415
528
  self.is_stopped = True
416
529
  self.stop_event.set()
417
-
530
+
531
+ if self.audio_queue:
532
+ while not self.audio_queue.empty():
533
+ try:
534
+ self.audio_queue.get_nowait()
535
+ except queue.Empty:
536
+ break
537
+
418
538
  if self.current_process:
419
539
  try:
420
540
  os.killpg(os.getpgid(self.current_process.pid), signal.SIGTERM)
@@ -423,12 +543,15 @@ class PiWave:
423
543
  pass
424
544
  finally:
425
545
  self.current_process = None
426
-
546
+
427
547
  if self.playback_thread and self.playback_thread.is_alive():
428
548
  self.playback_thread.join(timeout=5)
429
-
549
+ if self.live_thread and self.live_thread.is_alive():
550
+ self.live_thread.join(timeout=3)
551
+
430
552
  self.is_playing = False
431
- Log.success("Playback stopped")
553
+ self.is_live_streaming = False
554
+ Log.success("Stopped")
432
555
 
433
556
  def pause(self):
434
557
  """Pause the current playback.
@@ -455,16 +578,17 @@ class PiWave:
455
578
  self.play(self.current_file)
456
579
 
457
580
  def update(self,
458
- frequency: Optional[float] = None,
459
- ps: Optional[str] = None,
460
- rt: Optional[str] = None,
461
- pi: Optional[str] = None,
462
- debug: Optional[bool] = None,
463
- silent: Optional[bool] = None,
464
- loop: Optional[bool] = None,
465
- backend: Optional[str] = None,
466
- on_track_change: Optional[Callable] = None,
467
- on_error: Optional[Callable] = None):
581
+ frequency: Optional[float] = None,
582
+ ps: Optional[str] = None,
583
+ rt: Optional[str] = None,
584
+ pi: Optional[str] = None,
585
+ debug: Optional[bool] = None,
586
+ silent: Optional[bool] = None,
587
+ loop: Optional[bool] = None,
588
+ backend: Optional[str] = None,
589
+ used_for: Optional[str] = None,
590
+ on_track_change: Optional[Callable] = None,
591
+ on_error: Optional[Callable] = None):
468
592
  """Update PiWave settings.
469
593
 
470
594
  :param frequency: FM frequency to broadcast on (80.0-108.0 MHz)
@@ -483,6 +607,8 @@ class PiWave:
483
607
  :type loop: Optional[bool]
484
608
  :param backend: Backend used to broadcast
485
609
  :type backend: Optional[str]
610
+ :param backend: Give the main use for the current instance, will be used if backend: auto. Supports `file_broadcast` and `live_broadcast`
611
+ :type backend: Optional[str]
486
612
  :param on_track_change: Callback function called when track changes
487
613
  :type on_track_change: Optional[Callable]
488
614
  :param on_error: Callback function called when an error occurs
@@ -499,9 +625,12 @@ class PiWave:
499
625
 
500
626
  freq_to_use = frequency if frequency is not None else self.frequency
501
627
 
628
+ if used_for is not None:
629
+ self.backend_use = used_for
630
+
502
631
  if backend is not None:
503
632
  if backend == "auto":
504
- backend_name = get_best_backend("file_broadcast", freq_to_use)
633
+ backend_name = get_best_backend(self.backend_use, freq_to_use)
505
634
  if not backend_name:
506
635
  available = list(backends.keys())
507
636
  raise PiWaveError(f"No suitable backend found for {freq_to_use}MHz. Available: {available}")
@@ -608,6 +737,7 @@ class PiWave:
608
737
  The returned dictionary contains:
609
738
 
610
739
  - **is_playing** (bool): Whether playback is active
740
+ - **is_live_streaming** (bool): Whether live playback is active
611
741
  - **frequency** (float): Current broadcast frequency
612
742
  - **current_file** (str|None): Path of currently playing file
613
743
  - **current_backend** (str): Currently used backend
@@ -627,6 +757,7 @@ class PiWave:
627
757
  """
628
758
  return {
629
759
  'is_playing': self.is_playing,
760
+ 'is_live_streaming': self.is_live_streaming,
630
761
  'frequency': self.frequency,
631
762
  'current_file': self.current_file,
632
763
  'current_backend': self.backend_name,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: piwave
3
- Version: 2.1.0
3
+ Version: 2.1.1
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
@@ -37,6 +37,7 @@ Dynamic: license-file
37
37
  - **Multi-Backend Architecture**: Supports multiple backends for different actions
38
38
  - **Wide Frequency Support**: 1-250 MHz coverage through different backends
39
39
  - **RDS Support**: Program Service, Radio Text, and Program Identifier broadcasting
40
+ - **Live Stream Support**: Broadcast live from a stream source.
40
41
  - **Smart Backend Selection**: Automatically chooses the best backend to suit your needs
41
42
  - **Audio Format Support**: Converts most audio formats (MP3, FLAC, M4A, etc.) to WAV
42
43
  - **Real-time Settings Updates**: Change frequency, RDS data, and settings without restart
@@ -51,12 +52,14 @@ Dynamic: license-file
51
52
  ### PiFmRds Backend
52
53
  - **Frequency Range**: 80.0 - 108.0 MHz (Standard FM band)
53
54
  - **RDS Support**: ✅ Full support (PS, RT, PI)
55
+ - **Live Support**: ❌ No live support
54
56
  - **Repository**: [ChristopheJacquet/PiFmRds](https://github.com/ChristopheJacquet/PiFmRds)
55
57
  - **Best For**: Standard FM broadcasting with RDS features
56
58
 
57
59
  ### FmTransmitter Backend
58
60
  - **Frequency Range**: 1.0 - 250.0 MHz (Extended range)
59
61
  - **RDS Support**: ❌ No RDS support
62
+ - **Live Support**: ✅ Experimental support
60
63
  - **Repository**: [markondej/fm_transmitter](https://github.com/markondej/fm_transmitter)
61
64
  - **Best For**: Non-standard frequencies and experimental broadcasting
62
65
 
@@ -141,42 +144,6 @@ curl -sL https://setup.piwave.xyz/uninstall | sudo bash
141
144
  make
142
145
  ```
143
146
 
144
- ## Backend Management
145
-
146
- ### CLI Commands
147
-
148
- ```bash
149
- # Search for available backends on system
150
- python3 -m piwave search
151
-
152
- # List cached backends
153
- python3 -m piwave list
154
-
155
- # Manually add backend executable path
156
- python3 -m piwave add pi_fm_rds /path/to/pi_fm_rds
157
-
158
- # Show package information
159
- python3 -m piwave info
160
-
161
- # Broadcast a file directly
162
- python3 -m piwave broadcast song.mp3 --frequency 101.5 --ps "MyRadio"
163
- ```
164
-
165
- ### Programmatic Backend Discovery
166
-
167
- ```python
168
- from piwave.backends import discover_backends, list_backends, search_backends
169
-
170
- # Load cached backends
171
- discover_backends()
172
-
173
- # Search for new backends (ignores cache)
174
- search_backends()
175
-
176
- # List available backends with details
177
- backends_info = list_backends()
178
- ```
179
-
180
147
  ## Quick Start
181
148
 
182
149
  ### Basic Usage
@@ -256,6 +223,44 @@ print(f"Backend supports RDS: {status['backend_supports_rds']}")
256
223
  print(f"Frequency range: {status['backend_frequency_range']}")
257
224
  ```
258
225
 
226
+
227
+ ## Backend Management
228
+
229
+ ### CLI Commands
230
+
231
+ ```bash
232
+ # Search for available backends on system
233
+ python3 -m piwave search
234
+
235
+ # List cached backends
236
+ python3 -m piwave list
237
+
238
+ # Manually add backend executable path
239
+ python3 -m piwave add pi_fm_rds /path/to/pi_fm_rds
240
+
241
+ # Show package information
242
+ python3 -m piwave info
243
+
244
+ # Broadcast a file directly
245
+ python3 -m piwave broadcast song.mp3 --frequency 101.5 --ps "MyRadio"
246
+ ```
247
+
248
+ ### Programmatic Backend Discovery
249
+
250
+ ```python
251
+ from piwave.backends import discover_backends, list_backends, search_backends
252
+
253
+ # Load cached backends
254
+ discover_backends()
255
+
256
+ # Search for new backends (ignores cache)
257
+ search_backends()
258
+
259
+ # List available backends with details
260
+ backends_info = list_backends()
261
+ ```
262
+
263
+
259
264
  ## Complete Examples
260
265
 
261
266
  <details>
@@ -443,16 +448,17 @@ if __name__ == "__main__":
443
448
 
444
449
  ```python
445
450
  PiWave(
446
- frequency=90.0, # Broadcast frequency (1.0-250.0 MHz)
447
- ps="PiWave", # Program Service name (max 8 chars)
448
- rt="PiWave: ...", # Radio Text (max 64 chars)
449
- pi="FFFF", # Program Identifier (4 hex digits)
450
- debug=False, # Enable debug logging
451
- silent=False, # Disable all logging
452
- loop=False, # Loop current track continuously
453
- backend="auto", # Backend selection ("auto", "pi_fm_rds", "fm_transmitter")
454
- on_track_change=None, # Callback for track changes
455
- on_error=None # Callback for errors
451
+ frequency=90.0, # Broadcast frequency (1.0-250.0 MHz)
452
+ ps="PiWave", # Program Service name (max 8 chars)
453
+ rt="PiWave: ...", # Radio Text (max 64 chars)
454
+ pi="FFFF", # Program Identifier (4 hex digits)
455
+ debug=False, # Enable debug logging
456
+ silent=False, # Disable all logging
457
+ loop=False, # Loop current track continuously
458
+ backend="auto", # Backend selection ("auto", "pi_fm_rds", "fm_transmitter")
459
+ used_for="file_broadcast", # Backend main purpose, used if backend = auto ("file_broadcast", "live_broadcast")
460
+ on_track_change=None, # Callback for track changes
461
+ on_error=None # Callback for errors
456
462
  )
457
463
  ```
458
464
 
@@ -495,6 +501,7 @@ status = pw.get_status()
495
501
  # Returns:
496
502
  {
497
503
  'is_playing': bool,
504
+ 'is_live_streaming': bool,
498
505
  'frequency': float,
499
506
  'current_file': str|None,
500
507
  'current_backend': str,
@@ -0,0 +1,13 @@
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=p9rsvwx5BdV7bHIW30vj1ipWOrSJOOMjczvs_DtghYA,31335
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.1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
10
+ piwave-2.1.1.dist-info/METADATA,sha256=4nXRHn0KaoiUzo7KegQyG5tEMDx9ZDn4WWBlpzxYa2c,20563
11
+ piwave-2.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ piwave-2.1.1.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
13
+ piwave-2.1.1.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=jmd10s2FUzL18GXjhyvIS6XwJl2K34G-xhMNHsvMKYk,25437
5
- piwave/backends/__init__.py,sha256=1lEfdEl-I9I4awRi0EHTVtRYAoSNC5T2zJhF0RS_xtA,3462
6
- piwave/backends/base.py,sha256=DGlDG9ybe6RNB7dUM7xedOD8DAo2e6nrtbS_LjnicAY,7513
7
- piwave/backends/fm_transmitter.py,sha256=Zj6NUKvDJ6yC6xEV67leusaNZAYo_FIFAvw7eVp2diw,1105
8
- piwave/backends/pi_fm_rds.py,sha256=TEHjn11GFIp4ZizEaD-0P3Cx_Agu7XFbyT6OxB1PpGk,1274
9
- piwave-2.1.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
10
- piwave-2.1.0.dist-info/METADATA,sha256=HxoDpTSGbLXhQR86G9gFI-Ko1_QtP-V-4Zu3-Ahq08c,20247
11
- piwave-2.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- piwave-2.1.0.dist-info/top_level.txt,sha256=xUbZ7Rk6OymSdDxmb9bfO8N-avJ9VYxP41GnXfwKYi8,7
13
- piwave-2.1.0.dist-info/RECORD,,
File without changes