soco-cli 0.4.75__tar.gz → 0.4.77__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.
Files changed (39) hide show
  1. {soco-cli-0.4.75/soco_cli.egg-info → soco_cli-0.4.77}/PKG-INFO +2 -2
  2. {soco-cli-0.4.75 → soco_cli-0.4.77}/README.md +17 -1
  3. {soco-cli-0.4.75 → soco_cli-0.4.77}/requirements.txt +1 -1
  4. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/__init__.py +1 -1
  5. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/action_processor.py +28 -4
  6. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/alarms.py +20 -16
  7. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/http_api.py +17 -4
  8. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/interactive.py +6 -4
  9. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/match_speaker_names.py +3 -2
  10. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/play_local_file.py +23 -5
  11. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/sonos.py +3 -2
  12. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/speakers.py +9 -7
  13. {soco-cli-0.4.75 → soco_cli-0.4.77/soco_cli.egg-info}/PKG-INFO +2 -2
  14. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli.egg-info/requires.txt +1 -1
  15. {soco-cli-0.4.75 → soco_cli-0.4.77}/LICENSE +0 -0
  16. {soco-cli-0.4.75 → soco_cli-0.4.77}/MANIFEST.in +0 -0
  17. {soco-cli-0.4.75 → soco_cli-0.4.77}/PYPI_README.md +0 -0
  18. {soco-cli-0.4.75 → soco_cli-0.4.77}/pyproject.toml +0 -0
  19. {soco-cli-0.4.75 → soco_cli-0.4.77}/setup.cfg +0 -0
  20. {soco-cli-0.4.75 → soco_cli-0.4.77}/setup.py +0 -0
  21. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/__main__.py +0 -0
  22. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/aliases.py +0 -0
  23. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/api.py +0 -0
  24. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/check_for_update.py +0 -0
  25. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/cmd_parser.py +0 -0
  26. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/keystroke_capture.py +0 -0
  27. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/m3u_parser.py +0 -0
  28. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/play_local_file_lists.py +0 -0
  29. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/sonos_discover.py +0 -0
  30. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/speaker_info.py +0 -0
  31. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/track_follow.py +0 -0
  32. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/utils.py +0 -0
  33. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli/wait_actions.py +0 -0
  34. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli.egg-info/SOURCES.txt +0 -0
  35. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli.egg-info/dependency_links.txt +0 -0
  36. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli.egg-info/entry_points.txt +0 -0
  37. {soco-cli-0.4.75 → soco_cli-0.4.77}/soco_cli.egg-info/top_level.txt +0 -0
  38. {soco-cli-0.4.75 → soco_cli-0.4.77}/tests/test_cli.py +0 -0
  39. {soco-cli-0.4.75 → soco_cli-0.4.77}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: soco-cli
3
- Version: 0.4.75
3
+ Version: 0.4.77
4
4
  Summary: Sonos command line control utility, based on SoCo
5
5
  Author-email: Avantrec Ltd <soco_cli@avantrec.com>
6
6
  Project-URL: Homepage, https://github.com/avantrec/soco-cli
@@ -12,7 +12,7 @@ Requires-Python: >=3.5
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: soco==0.27.1; python_version < "3.6"
15
- Requires-Dist: soco>=0.29.0; python_version >= "3.6"
15
+ Requires-Dist: soco>=0.30.2; python_version >= "3.6"
16
16
  Requires-Dist: ifaddr==0.1.7; python_version < "3.7"
17
17
  Requires-Dist: ifaddr>=0.2.0; python_version >= "3.7"
18
18
  Requires-Dist: tabulate
@@ -62,6 +62,7 @@
62
62
  * [Using the Local Speaker Cache](#using-the-local-speaker-cache)
63
63
  * [HTTP Request Structure](#http-request-structure)
64
64
  * [Return Values](#return-values)
65
+ * [Asynchronous Commands (Experimental)](#asynchronous-commands-experimental)
65
66
  * [Macros: Defining Custom HTTP API Server Actions](#macros-defining-custom-http-api-server-actions)
66
67
  * [Macro Definition and Usage](#macro-definition-and-usage)
67
68
  * [Macro Arguments](#macro-arguments)
@@ -84,7 +85,7 @@
84
85
  * [Resources](#resources)
85
86
 
86
87
  <!-- Created by https://github.com/ekalinin/github-markdown-toc -->
87
- <!-- Added by: pwt, at: Mon Oct 9 14:49:13 BST 2023 -->
88
+ <!-- Added by: pwt, at: Sat May 11 11:52:19 BST 2024 -->
88
89
 
89
90
  <!--te-->
90
91
 
@@ -409,6 +410,7 @@ sonos Kitchen play_from_queue 5
409
410
  - **`play_m3u <m3u_file> <options>`** (or **`play_local_m3u`**): Plays a local M3U/M3U8 playlist consisting of local audio files (in supported audio formats). Can be followed by options `p` to print each filename before it plays, and/or `s` to shuffle the playlist, or `r` to play a single, random track from the playlist. (If using multiple options, concatenate them: e.g., `ps`.) Example: `sonos Study play_m3u my_playlist.m3u ps`. Add the `i` option to invoke **interactive** mode, which allows use of the keyboard to go to the (N)ext track, to (P)ause, or to (R)esume playback.
410
411
  - **`play_mode` (or `mode`)**: Returns the play mode of the speaker, one of `NORMAL`, `REPEAT_ONE`, `REPEAT_ALL`, `SHUFFLE`, `SHUFFLE_REPEAT_ONE`, or `SHUFFLE_NOREPEAT`.
411
412
  - **`play_mode <mode>` (or `mode`)**: Sets the play mode of the speaker to `<mode>`, which is one of the values above.
413
+ - **`play_sharelink <sharelink>`**: Adds a sharelink to the end of the queue and starts playback.
412
414
  - **`play_uri <uri> <title>` (or `uri`, `pu`)**: Plays the audio object given by the `<uri>` parameter (e.g., a radio stream URL). `<title>` is optional, and if present will be used for the title of the audio stream.
413
415
  - **`previous` (or `prev`)**: Move to the previous track (if applicable for the audio source).
414
416
  - **`repeat` (or `rpt`)**: Returns the repeat mode state: 'off', 'one', or 'all'.
@@ -1075,6 +1077,20 @@ If the command is successful, the **`result`** field contains the result string,
1075
1077
 
1076
1078
  If the command is unsuccessful, the **`error_msg`** field contains an error message describing the error.
1077
1079
 
1080
+ ### Asynchronous Commands (Experimental)
1081
+
1082
+ It's sometimes useful for the HTTP API server to respond immediately while its invoked action continues to run in the background. For example, if one wants to invoke a `play_file` action, and allow it to run in the background.
1083
+
1084
+ This can be achieved by prefixing the action with `async_`. For example:
1085
+
1086
+ ```commandline
1087
+ http://192.168.0.100:8000/Kitchen/async_play_file/my_file.mp3
1088
+ ```
1089
+
1090
+ Note that the data returned in this case may not be useful: it will simply indicate whether the background command was invoked successfully, not whether the command was successful.
1091
+
1092
+ The `async_` functionality does not (yet) apply to the Macros feature described below.
1093
+
1078
1094
  ### Macros: Defining Custom HTTP API Server Actions
1079
1095
 
1080
1096
  The **macros** feature allows the creation of custom actions or sequences of actions to be executed by the HTTP API server, and available at the `/macro/<macro_name>` endpoint. Macros are defined in a text file that is loaded when the server starts, and which can subsequently be reloaded using the `/macros/reload` endpoint.
@@ -1,5 +1,5 @@
1
1
  soco == 0.27.1; python_version < "3.6"
2
- soco >= 0.29.0; python_version >= "3.6"
2
+ soco >= 0.30.2; python_version >= "3.6"
3
3
  ifaddr == 0.1.7; python_version < "3.7"
4
4
  ifaddr >= 0.2.0; python_version >= "3.7"
5
5
  tabulate
@@ -10,4 +10,4 @@ server, to control Sonos via HTTP requests.
10
10
  For more information, please see: https://github.com/avantrec/soco-cli
11
11
  """
12
12
 
13
- __version__ = "0.4.75"
13
+ __version__ = "0.4.77"
@@ -1659,8 +1659,7 @@ def wait_stopped_for_core(speaker, action, duration_arg, not_paused=False):
1659
1659
  start_time = current_time
1660
1660
  remaining_time = duration - (current_time - start_time)
1661
1661
  logging.info(
1662
- "Elapsed since last 'STOPPED' = {}s | total elapsed = {}s | remaining = {}s"
1663
- .format(
1662
+ "Elapsed since last 'STOPPED' = {}s | total elapsed = {}s | remaining = {}s".format(
1664
1663
  int(current_time - start_time),
1665
1664
  int(current_time - original_start_time),
1666
1665
  int(remaining_time),
@@ -2358,8 +2357,9 @@ def trueplay(speaker, action, args, soco_function, use_local_speaker_list):
2358
2357
  parameter_type_error(action, "on|off")
2359
2358
  except:
2360
2359
  error_report(
2361
- "No Trueplay profile available for '{}' (or Trueplay not supported)"
2362
- .format(speaker.player_name)
2360
+ "No Trueplay profile available for '{}' (or Trueplay not supported)".format(
2361
+ speaker.player_name
2362
+ )
2363
2363
  )
2364
2364
  return False
2365
2365
  return True
@@ -2579,6 +2579,29 @@ def add_sharelink_to_queue(
2579
2579
  return True
2580
2580
 
2581
2581
 
2582
+ @one_parameter
2583
+ def play_sharelink(speaker, action, args, soco_function, use_local_speaker_list):
2584
+ share_link = ShareLinkPlugin(speaker)
2585
+ uri = args[0]
2586
+
2587
+ if not share_link.is_share_link(uri):
2588
+ error_report("Invalid sharelink: '{}'".format(uri))
2589
+ return False
2590
+
2591
+ position = speaker.queue_size + 1
2592
+
2593
+ try:
2594
+ # Return the queue position of the first added item
2595
+ queue_position = share_link.add_share_link_to_queue(uri, position)
2596
+ save_queue_insertion_position(queue_position)
2597
+ except SoCoUPnPException as e:
2598
+ error_report("Unable to play sharelink to queue: {}".format(e))
2599
+ return False
2600
+
2601
+ speaker.play_from_queue(queue_position - 1)
2602
+ return True
2603
+
2604
+
2582
2605
  @zero_parameters
2583
2606
  def reboot_count(speaker, action, args, soco_function, use_local_speaker_list):
2584
2607
  print(speaker.boot_seqnum)
@@ -3142,6 +3165,7 @@ actions = {
3142
3165
  "channel": SonosFunction(get_channel, "", True),
3143
3166
  "add_sharelink_to_queue": SonosFunction(add_sharelink_to_queue, "", True),
3144
3167
  "sharelink": SonosFunction(add_sharelink_to_queue, "", True),
3168
+ "play_sharelink": SonosFunction(play_sharelink, "", True),
3145
3169
  "is_indexing": SonosFunction(is_indexing, "", False),
3146
3170
  "reboot_count": SonosFunction(reboot_count, "", False),
3147
3171
  "play_directory": SonosFunction(play_directory, "", True),
@@ -53,18 +53,20 @@ def list_alarms(speaker, action, args, soco_function, use_local_speaker_list):
53
53
  duration = alarm.duration.strftime("%H:%M")
54
54
  else:
55
55
  duration = "No Limit"
56
- details.append([
57
- alarm.alarm_id,
58
- alarm.zone.player_name,
59
- time,
60
- duration,
61
- alarm.recurrence,
62
- convert_true_false(alarm.enabled),
63
- title,
64
- alarm.play_mode,
65
- alarm.volume,
66
- convert_true_false(alarm.include_linked_zones),
67
- ])
56
+ details.append(
57
+ [
58
+ alarm.alarm_id,
59
+ alarm.zone.player_name,
60
+ time,
61
+ duration,
62
+ alarm.recurrence,
63
+ convert_true_false(alarm.enabled),
64
+ title,
65
+ alarm.play_mode,
66
+ alarm.volume,
67
+ convert_true_false(alarm.include_linked_zones),
68
+ ]
69
+ )
68
70
 
69
71
  # Sort alarms by start time, room. Apply sorts in reverse order.
70
72
  details.sort(key=lambda field: field[1]) # Room
@@ -341,8 +343,9 @@ def copy_modify_alarm(speaker, action, args, soco_function, use_local_speaker_li
341
343
  break
342
344
  else:
343
345
  error_report(
344
- "Alarm ID '{}' not found; use the 'alarms' action to find the integer ID"
345
- .format(alarm_id)
346
+ "Alarm ID '{}' not found; use the 'alarms' action to find the integer ID".format(
347
+ alarm_id
348
+ )
346
349
  )
347
350
  return False
348
351
 
@@ -360,8 +363,9 @@ def copy_modify_alarm(speaker, action, args, soco_function, use_local_speaker_li
360
363
  new_alarm.save()
361
364
  except SoCoUPnPException as e:
362
365
  error_report(
363
- "Failed to copy/move alarm; did you remember to modify the start time?: {}"
364
- .format(e)
366
+ "Failed to copy/move alarm; did you remember to modify the start time?: {}".format(
367
+ e
368
+ )
365
369
  )
366
370
  return False
367
371
 
@@ -10,7 +10,7 @@ import argparse
10
10
  import pprint
11
11
  import shlex
12
12
  from os.path import abspath
13
- from subprocess import STDOUT, CalledProcessError, check_output
13
+ from subprocess import STDOUT, CalledProcessError, Popen, check_output
14
14
  from typing import Dict, Tuple
15
15
 
16
16
  import uvicorn # type: ignore
@@ -33,6 +33,7 @@ PREFIX_MACRO = PREFIX + "Macro: "
33
33
  MACROS: Dict[str, str] = {}
34
34
  MACRO_FILE = ""
35
35
  PP = pprint.PrettyPrinter(indent=len(PREFIX_MACRO))
36
+ ASYNC_PREFIX = "async_"
36
37
 
37
38
  # Gets used with the local speaker list only
38
39
  SPEAKER_LIST = Speakers(network_timeout=1.0)
@@ -56,9 +57,21 @@ def command_core(
56
57
  device, error_msg = get_speaker(speaker, use_local_speaker_list=use_local)
57
58
  if device:
58
59
  speaker = device.player_name
59
- exit_code, result, error_msg = sc_run(
60
- device, action, *args, use_local_speaker_list=use_local
61
- )
60
+ if not action.startswith(ASYNC_PREFIX):
61
+ exit_code, result, error_msg = sc_run(
62
+ device, action, *args, use_local_speaker_list=use_local
63
+ )
64
+ else:
65
+ action = action.replace(ASYNC_PREFIX, "")
66
+ try:
67
+ Popen(["sonos", str(device.ip_address), action, *args])
68
+ exit_code = 0
69
+ error_msg = ""
70
+ result = ""
71
+ except Exception as e:
72
+ exit_code = 1
73
+ error_msg = str(e)
74
+ result = ""
62
75
  else:
63
76
  exit_code = 1
64
77
  result = ""
@@ -443,8 +443,9 @@ def interactive_loop(
443
443
  name = args.pop(0)
444
444
  if name in ACTIONS_TO_EXEC_NO_SPEAKER:
445
445
  print(
446
- "Please set an active speaker to use the '{}' action"
447
- .format(name)
446
+ "Please set an active speaker to use the '{}' action".format(
447
+ name
448
+ )
448
449
  )
449
450
  continue
450
451
  else:
@@ -649,8 +650,9 @@ def _get_speaker_names(use_local_speaker_list=False):
649
650
  names = speaker_cache().get_all_speaker_names()
650
651
  except Exception as e:
651
652
  print(
652
- "Speaker listing failed: please check your network connection [{}]"
653
- .format(e)
653
+ "Speaker listing failed: please check your network connection [{}]".format(
654
+ e
655
+ )
654
656
  )
655
657
  names = []
656
658
  return names
@@ -36,8 +36,9 @@ def speaker_name_matches(name_supplied, name_stored):
36
36
  name_stored = name_stored.replace("’", "'")
37
37
  if name_supplied == name_stored:
38
38
  logging.info(
39
- "Found apostrophe-normalised exact speaker name match for '{}' as '{}'"
40
- .format(name_supplied_original, name_stored_original)
39
+ "Found apostrophe-normalised exact speaker name match for '{}' as '{}'".format(
40
+ name_supplied_original, name_stored_original
41
+ )
41
42
  )
42
43
  return True, True
43
44
 
@@ -3,6 +3,7 @@
3
3
  import functools
4
4
  import http.client
5
5
  import logging
6
+ import socket
6
7
  import sys
7
8
  import time
8
9
  import urllib.parse
@@ -148,19 +149,36 @@ def get_server_ip(speaker: SoCo) -> Optional[str]:
148
149
  for adapter in adapters:
149
150
  for ip in adapter.ips:
150
151
  if ip.is_IPv4 and ip.ip != "127.0.0.1":
152
+ # Find an available port
153
+ for port in range(PORT_START, PORT_END + 1):
154
+ try:
155
+ logging.info("Checking if port {} is available".format(port))
156
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
157
+ s.bind((ip.ip, port))
158
+ available_port = port
159
+ logging.info("Port {} is available".format(port))
160
+ break
161
+ except socket.error:
162
+ continue
163
+ else:
164
+ logging.info("No available ports for IP address '{}'".format(ip.ip))
165
+ continue
151
166
  try:
152
167
  logging.info(
153
- "Checking target speaker's reachability from IP address '{}'"
154
- .format(ip.ip)
168
+ "Checking target speaker's reachability from IP address '{}'".format(
169
+ ip.ip
170
+ )
155
171
  )
156
172
  http_connection = http.client.HTTPConnection(
157
173
  speaker.ip_address,
158
174
  port=1400,
159
- timeout=0.5,
160
- source_address=(ip.ip, PORT_END),
175
+ timeout=5.0,
176
+ source_address=(ip.ip, available_port),
161
177
  )
162
178
  http_connection.request("GET", "/status/info")
163
- if http_connection.getresponse().status == 200:
179
+ response = http_connection.getresponse()
180
+ http_connection.close()
181
+ if response.status == 200:
164
182
  return ip.ip
165
183
  except:
166
184
  continue
@@ -410,8 +410,9 @@ def main():
410
410
  if env_speaker:
411
411
  if env_spkr_inserted[sequence_pointer] is False:
412
412
  logging.info(
413
- "Getting speaker name '{}' from the $SPKR environment variable"
414
- .format(env_speaker)
413
+ "Getting speaker name '{}' from the $SPKR environment variable".format(
414
+ env_speaker
415
+ )
415
416
  )
416
417
  sequence.insert(0, env_speaker)
417
418
  speaker_name = env_speaker
@@ -305,13 +305,15 @@ class Speakers:
305
305
  visible = "Visible"
306
306
  else:
307
307
  visible = "Hidden"
308
- households[device.household_id].append((
309
- device.speaker_name,
310
- device.ip_address,
311
- device.model_name.replace("Sonos ", ""),
312
- visible,
313
- device.display_version,
314
- ))
308
+ households[device.household_id].append(
309
+ (
310
+ device.speaker_name,
311
+ device.ip_address,
312
+ device.model_name.replace("Sonos ", ""),
313
+ visible,
314
+ device.display_version,
315
+ )
316
+ )
315
317
  num_devices += 1
316
318
 
317
319
  headers = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: soco-cli
3
- Version: 0.4.75
3
+ Version: 0.4.77
4
4
  Summary: Sonos command line control utility, based on SoCo
5
5
  Author-email: Avantrec Ltd <soco_cli@avantrec.com>
6
6
  Project-URL: Homepage, https://github.com/avantrec/soco-cli
@@ -12,7 +12,7 @@ Requires-Python: >=3.5
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: soco==0.27.1; python_version < "3.6"
15
- Requires-Dist: soco>=0.29.0; python_version >= "3.6"
15
+ Requires-Dist: soco>=0.30.2; python_version >= "3.6"
16
16
  Requires-Dist: ifaddr==0.1.7; python_version < "3.7"
17
17
  Requires-Dist: ifaddr>=0.2.0; python_version >= "3.7"
18
18
  Requires-Dist: tabulate
@@ -9,7 +9,7 @@ soco==0.27.1
9
9
  ifaddr==0.1.7
10
10
 
11
11
  [:python_version >= "3.6"]
12
- soco>=0.29.0
12
+ soco>=0.30.2
13
13
 
14
14
  [:python_version >= "3.7"]
15
15
  ifaddr>=0.2.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes