Showing preview only (666K chars total). Download the full file or copy to clipboard to get everything.
Repository: avantrec/soco-cli
Branch: master
Commit: f63983219530
Files: 53
Total size: 642.4 KB
Directory structure:
gitextract_h5uvuzqn/
├── .gitignore
├── CHANGELOG.txt
├── LICENSE
├── MANIFEST.in
├── Makefile
├── PYPI_README.md
├── README.md
├── RELEASING.txt
├── assets/
│ └── soco-cli-logo.afdesign
├── gh-md-toc
├── pylintrc
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── setup.cfg
├── setup.py
├── soco_cli/
│ ├── __init__.py
│ ├── __main__.py
│ ├── action_processor.py
│ ├── alarms.py
│ ├── aliases.py
│ ├── api.py
│ ├── check_for_update.py
│ ├── cmd_parser.py
│ ├── http_api.py
│ ├── interactive.py
│ ├── keystroke_capture.py
│ ├── m3u_parser.py
│ ├── match_speaker_names.py
│ ├── play_local_file.py
│ ├── play_local_file_lists.py
│ ├── sonos.py
│ ├── sonos_discover.py
│ ├── speaker_info.py
│ ├── speakers.py
│ ├── track_follow.py
│ ├── utils.py
│ └── wait_actions.py
└── tests/
├── test_action_processor.py
├── test_aliases.py
├── test_check_for_update.py
├── test_cli.py
├── test_cmd_parser.py
├── test_comprehensive.py
├── test_http_api.py
├── test_interactive.py
├── test_m3u_parser.py
├── test_match_speaker_names.py
├── test_play_local_file.py
├── test_speakers.py
├── test_utils.py
├── test_utils_extended.py
└── test_wait_actions.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
/venv/
.idea
/deploy.sh
__pycache__/
/build/
/dist/
/soco_cli.egg-info/
*.pyc
/README.md.orig.*
/README.md.toc.*
/exploration
/audio_files
/examples
/.mypy_cache
/.pytest_cache
================================================
FILE: CHANGELOG.txt
================================================
v0.4.86 - Add 'async_' prefix support for HTTP API Server macros
- Allow multiple sharelinks in a single 'add_sharelink_to_queue' action
- Allow multiple sharelinks in a single 'play_sharelink' action;
add optional 'position' argument
v0.4.85 - Add 'alarms_spec' and 'alarms_spec_zone' actions to list alarms in
alarm spec format for easy copy/paste into 'modify_alarm'/'add_alarm'
v0.4.84 - Interrupt command sequence on break (fixes #35)
- Add support for adding & removing surround sound satellite speakers
- Add requirement for SoCo v0.31.0
v0.4.83 - Fix tab completion in interactive mode for Python 3.10+
- Fix bug when checking for .soco-cli directory
v0.4.82 - Add 'redirect_io' boolean option to API 'run_command()' to
prevent capture of stdout and stderr (fixes #85)
v0.4.81 - Eliminate/alias inconsistent use of underscore in command line
option names (#84)
v0.4.80 - Minor fixes & bump SoCo version requirement
v0.4.79 - Add 'list_audio_files' path to HTTP API Server
v0.4.78 - Allow pass-through of '_end_on_pause' via HTTP API
- HTTP API server: new 'async' operation will cancel an existing,
running one
v0.4.77 - Changes to 'play_file' speaker reachability fallback logic
- Add support for HTTP API Server 'async_' actions
v0.4.76 - Add 'play_sharelink' action
v0.4.75 - Use dual-stage approach for determining 'play_file' server IP address
v0.4.74 - Determine HTTP server IP to use for 'play_file' using target speaker
reachability
v0.4.73 - Remove dependency on distutils.version.StrictVersion for Python 3.12
compatibility
v0.4.72 - Workaround for https://github.com/SoCo/SoCo/issues/950
v0.4.71 - Fix for https://github.com/avantrec/soco-cli/issues/64
v0.4.70 - Add 'is_not_coordinator' conditional action modifier
v0.4.69 - Add 'strict' option for all music library searches
- Consolidate queueing search results under action
'queue_search_results'. Earlier, equivalent actions remain but are
now undocumented
v0.4.68 - Add 'strict' option to 'search_artists'
- Add 'queue_multiple_search_results' action
v0.4.67 - Multiple artist search results from 'search_artists' are now saved
v0.4.66 - '_all_' now targets all visible speakers (including non-coordinators)
v0.4.65 - Add 'if_queue/if_no_queue' conditional action modifiers
v0.4.64 - Add 'multi_group/mg' action
v0.4.63 - Add 'if_coordinator' conditional action modifier
- Add 'relative_sub_gain/rel_sub_gain/rsb' action
- Add 'rel_bass' and 'rel_treble' synonyms
v0.4.62 - Use Sonos Favourites instead of URIs when creating alarms
v0.4.61 - Add 'set_queue_position' action
v0.4.60 - Add 'generic' HTTP macro and increase number of args to 12
- Fix remove_playlist / delete_playlist actions
v0.4.59 - Add queue position option for 'add_sharelink_to_queue'
v0.4.58 - Add 'stop_all' action
v0.4.57 - Defect fix for 'if_stopped/if_playing'
v0.4.56 - Suppress switch-to-coordinator for 'if_stopped/if_playing'
v0.4.55 - Display "Sonos Chime" for default alarm
- Add 'playing_tv' action
v0.4.54 - Display URIs for alarms that don't have Title metadata
v0.4.53 - Update for SoCo v0.29.0
v0.4.52 - Add 'last' option to 'play_from_queue' action
- Add 'random' option to 'play_from_queue' action
- Add 'last_added' option to 'play_from_queue' action
v0.4.51 - Constrain ifaddr package to 0.1.7 for Python < 3.7
- Allow URL/Path parameters in HTTP API Server (fixes #38)
v0.4.50 - Update to SoCo v0.28.0; pin SoCo to v0.27.1 for Python < 3.6
v0.4.49 - Maintenance update
v0.4.48 - Add 'sub_gain' action
- Add 'surround_volume_tv' action
- Add 'surround_volume_music' action
- Add 'surround_full_volume_enabled' action
v0.4.47 - Improvements to 'cue_line_in' + maintenance updates
- Update to SoCo v0.26.4 fixes regression in obtaining track titles
when playing from local libraries
v0.4.46 - Add 'cue_line_in' action
- Enable use of 'wait' and 'wait_until' with speaker names
and 'if_stopped/if_playing' tests
v0.4.45 - Allow use of local speaker cache with HTTP API server
v0.4.44 - Maintenance update
v0.4.43 - HTTP API server: Expand number of macro arguments to nine
- HTTP API server: populate OpenAPI doc metadata
v0.4.42 - Allow use of '_' to supply arguments to be ignored during
HTTP API server macro processing
v0.4.41 - Add 'ugaig' synonym for 'ungroup_all_in_group'
- Fix typing issue with Python < 3.8 in HTTP API server
v0.4.40 - Add '/macros/reload' operation to HTTP API server
- Add 'ungroup_all_in_group' action
v0.4.39 - Maintenance release
v0.4.38 - Allow parameterisation of HTTP API server macros
v0.4.37 - Add `group_volume_equalise` action
v0.4.36 - Document support for Apple Music share links
- Add 'macros' URL path to the HTTP API server
v0.4.35 - Add 'mic_enabled' action
v0.4.34 - Add macros capability to the HTTP API server
v0.4.33 - Add 'tv_audio_delay' action
- Add 'alarms_zone' action to list alarms for target speaker only
v0.4.32 - Allow 'copy_modify_alarm' to copy a modified alarm to a
different target speaker
v0.4.31 - Add 'copy_modify_alarm' action
v0.4.30 - Allow the use of 'loop' actions in interactive shell aliases
v0.4.29 - Maintenance release
v0.4.28 - Add 'audio_format' action for soundbars
v0.4.27 - Maintenance release
v0.4.26 - Add subwoofer/surround speaker status and control actions
v0.4.25 - Add 'switch_to_tv' action
v0.4.24 - Allow use of the 'loop' actions in interactive mode
v0.4.23 - Add '/speakers' path to the HTTP API server
v0.4.22 - Improve output of 'sleep_timer' action
v0.4.21 - Add 'play_directory' action & notes about CD playback on macOS
v0.4.20 - Add 'AIFF' to supported local file types, for direct CD playback
on macOS
v0.4.19 - Add 'reboot_count' action
- Add type annotations to the API calls
v0.4.18 - Add support for Deezer share links (require SoCo v0.24)
v0.4.17 - Add '_end_on_pause_' option for 'play_file' action
v0.4.16 - Improve behaviour of CTRL-C in single keystroke, interactive mode
- Find 'album_art' URIs in broader range of cases
v0.4.15 - Actions 'play_file' and 'play_m3u' can now be cancelled in the
interactive shell without exiting the shell
- Change behaviour of CTRL-C in the shell: now requires 'exit'
v0.4.14 - Improve signal handling (behaviour and outputs for CTRL-C, etc.)
v0.4.13 - All 'wait' actions can now be cancelled in the interactive shell
using CTRL-C, without exiting interactive mode
- Interactive shell commands which run in subprocesses now do so in
OS shell environments. Hence, commands like 'tf > tracks.txt' now
work in interactive mode
v0.4.12 - Display Plex track data in the output of 'list_queue'
- Add 'is_indexing' action
v0.4.11 - Internal improvements
- Action 'list_queue' now returns silently if the queue is empty
v0.4.10 - Fix regression: removal of 'Title' in 'track' output in some cases
v0.4.9 - Add 'Radio Show' details to output of 'track', etc.
- 'wait_end_track' now detects a change of radio show
v0.4.8 - Bugfixes and minor cosmetic changes
v0.4.7 - Improve output of 'track' and related actions, including support
for Audible audio book content.
v0.4.6 - Add 'track_follow_compact' action, and 'tf' and 'tfc' synonyms.
v0.4.5 - Add podcast information to 'track' output
- Improve 'album_art': now returns a URL in more cases
(e.g., Spotify).
- Minor tidy up to the output of 'track'
v0.4.4 - Add 'add_sharelink_to_queue/sharelink' action for Spotify/Tidal
- Add 'end_session' action
- Add 'get_channel' action
v0.4.3 - Add 'get_uri' action
- Add support for 'USE_LOCAL_SPKR_CACHE' env. variable
v0.4.2 - Rename 'sonos-http-server' to 'sonos-http-api-server'
- Additional HTTP API server logging
v0.4.1 - Minor changes to HTTP API server
v0.4.0 - Add HTTP API server functionality
v0.3.50 - Minor cosmetic improvements & bugfixes
v0.3.49 - Minor improvements to 'track_follow'
v0.3.48 - Bugfixes
v0.3.47 - Interactive mode 'track_follow' now runs in a subprocess
v0.3.46 - Improve output of 'track_follow'
v0.3.45 - Add 'track_follow' action outside interactive mode
v0.3.44 - Add 'track_follow' command to the interactive shell
v0.3.43 - Add support for imported local library playlists
v0.3.42 - Minor bugfixes only
v0.3.41 - Add 'relative_bass' and 'relative_treble' actions
v0.3.40 - Simplify 'snooze_alarm' action (backward-compatible)
v0.3.39 - Add 'snooze_alarm' action
v0.3.38 - Add '--check_for_update' option
v0.3.37 - Actions 'info' and 'sysinfo' now report correct playback state
for slave speakers
- Upgrade to SoCo v0.22.0, allowing some code simplification
v0.3.36 - Fix regression in exit code; send error messages to stderr
- Action 'copy_alarm' now returns the ID of the copy
v0.3.35 - Add 'move_alarm' action
- Remove 'alarm(s)_enabled' action ...
- Replace with 'enable_alarm(s)' and 'disable_alarm(s)' actions
v0.3.34 - Add 'copy_alarm' action
- Reorder columns in output of 'alarms' to match the sequencing
used in 'create_alarm' and 'modify_alarm'
v0.3.33 - Rename 'enable_alarm(s)' actions to 'alarm(s)_enabled'
(Original action names will still work, at least for now)
- Add 'list_alarms' synonym for 'alarms' action
- Add 'modify_alarm(s)' action
v0.3.32 - Add 'exec' and 'cd' commands to the interactive shell
- Add '--subnets' option to 'sonos-discover', to specify
which IP addresses / subnets to search
v0.3.31 - Add 'create_alarm/add_alarm' actions
- Add 'remove_alarm(s)' action to remove alarms by ID
- Add 'enable_alarm(s)' action to enable/disable alarms
- Include alarm ID in output from 'alarms' action
v0.3.30 - Fixes #18 (exception when no speakers are discovered in cached mode)
v0.3.29 - Cosmetic change to multi-line output (fixes a regression)
v0.3.28 - Minor bugfixes
v0.3.27 - Minor bugfixes
v0.3.26 - Alias processing bugfixes
v0.3.25 - Add argument substitution (%1, %2, etc.) to aliases
Note that the use of '_' to suppress arguments is now deprecated
- Add 'docs' command to shell
v0.3.24 - Add 'available_actions' action
- Add 'wait_end_track' action
v0.3.23 - Bugfix: Remove spurious newline when action returns no value (#17)
v0.3.22 - Multiple sequential actions will now attempt to proceed in the
event of an action in the sequence failing
v0.3.21 - Bugfix: correct issue where shell aliases can't be deleted
v0.3.20 - Add the ability to save, load, and overwrite shell aliases from
text files
v0.3.19 - Add "_" option to suppress pass-thru parameters in shell aliases
- Add single keystroke shell support for Windows
- Add 'playpause' as synonym for 'pauseplay'
v0.3.18 - Add 'pauseplay' action
- Improve the prompt in Shell single keystroke mode
v0.3.17 - Add 'version' command to the Shell
- Fix regression in 'remove_from_queue'
- Add 'groupstatus' action
v0.3.16 - Selected actions targeted at a non-coordinator speaker in a
group are now diverted to the coordinator instead of returning
an error
v0.3.15 - Add 'album_art' action
v0.3.14 - Bugfixes to API and alias loop detection
v0.3.13 - Shell aliases now accept parameters
- Network scan options now respected in normal discovery mode
v0.3.12 - Bugfix for 'play_fav_radio_station_no' picking the wrong
station (#12).
v0.3.11 - Allow aliases to include other aliases, with loop detection
- Add 'single keystroke' mode to the shell. (Not supported on
Windows.)
v0.3.10 - Shell aliases can now be used for shell commands (except
alias!)
- Add 'push' and 'pop' shell commands to save / restore the
active speaker
- Shell and API bugfixes
v0.3.9 - Add alias capability to the shell
v0.3.8 - Shell history is now saved across shell sessions in
~/.soco-cli/shell-history.txt
- Add 'play_fav_radio_station_no' action
- Shell now supports ' : ' for multiple actions, and 'wait' actions
v0.3.7 - Fix 'readline' import error on Windows and add shell warning
v0.3.6 - Add auto-completion for Interactive Shell commands
- Various shell improvements
v0.3.5 - Interactive mode: quickly select speaker by number
v0.3.4 - Fix refactoring bug
v0.3.3 - Significantly improved Interactive Mode
- Bugfix for numbering issue in 'play_favourite_number'
- API change / bugfix for 'get_soco_object()'
v0.3.2 - Interactive mode: add ability to set/unset active speaker
- Add 'play_favourite_number' action to play a favourite by its
number
- Remove API type hints for backward compatibility; bugfixes
v0.3.1 - API interface change/expansion, and bugfix for local cache
v0.3.0 - Add an API allowing the use of SoCo-CLI as a Python library
- Add early version of interactive mode
- Add the ability to get the speaker name from the $SPKR environment
variable
- Add 'wait_stopped_for_not_pause' action
v0.2.1 - Performance improvement for speaker discovery with partial name
match
- Action 'play_m3u' now accepts files that contain any list of audio
filenames, without requiring M3U/M3U8 conventions
v0.2.0 - Requires SoCo v0.21, and benefits from its big improvements
- If a speaker name is not found, discovery will now fall back to
scanning the network for a matching speaker
- Partial, case insensitive matches can now be used for speaker
naming when using normal discovery
- Supplying, ambiguous partial speaker names now results in an error
- 'Alternative Discovery' is now referred to as 'Cached Discovery'
- Add 'buttons' action to inspect/change whether speaker buttons
are enabled
- Add 'fixed_volume' action to inspect/change whether the Fixed
Volume feature is enabled (applies to Connect and Port)
- Add 'trueplay' action to inspect/change whether a Trueplay
tuning profile is enabled
v0.1.54 - Add 'wait_stop_not_pause" action
- Action 'play_file' now accepts multiple files as parameters
v0.1.53 - Action 'play_file' now supported back to Python 3.5
- Add simple 'interactive mode' option to 'play_m3u' action, allowing
'next track', 'pause', and 'resume' while playing a playlist
v0.1.52 - Bugfix only
v0.1.51 - Add queue position options to 'add_uri_to_queue'
- Restore Python 3.5+ compatibility
v0.1.50 - Action 'play_file' can now be paused without terminating the server
v0.1.49 - Minor fixes & update docs re: AAC playback issues
v0.1.48 - Add 'r' option to 'play_m3u' to play a single, random track
- Add support for 'm3u8' playlist files
- Album art now displayed when using 'play_file'
- Add WMA file support for 'play_file'
- Add AAC file support for 'play_file' (with issues)
v0.1.47 - Add 'play_m3u' action to play local M3U playlists
v0.1.46 - Fix behaviour of 'play_uri' when playing file URLs
- Add support for M4A and MP4 playback using 'play_file'
- Add support for seeking within a track when using 'play_file'
v0.1.45 - Add 'play_file' action for playback of local audio files
(Experimental: currently works for MP3, FLAC, OGG and WAV files)
v0.1.44 - Simplify output format of 'zones' (etc.)
- Add 'first/start' option for various queue actions
- Add 'rename' action to rename speakers
- Add '--actions' option (same as '--commands')
v0.1.43 - Add '_all_' option instead of targeting a named speaker
- Simplify the output of the 'groups' action
- Add 'commands' option to sonos, to print the list of available
commands
v0.1.42 - Patch SoCo to provide full Python 3.9 support (until SoCo 0.21)
- Add 'battery' action to print battery status for Sonos Move speakers
v0.1.41 - Improve time accuracy in 'wait_stopped_for'
- Improve playback state detection in 'wait_stopped_for'
- Further evolution of 'track' output for streams
- Add 'first/start' option to 'queue_search_number'
- Save search results when using 'list_playlist_tracks'
v0.1.40 - Add 'seek_forward' action to jump ahead within a track
- Add 'seek_back' action to jump back within a track
- Action 'seek' now supports more flexible time formats
- Add 'seek_to' synonym for 'seek'
- Improve 'track' output when reporting a stopped stream
- Add 'min_netmask' option for alternative discovery
- Improve network selection logic when using alternative discovery
- Improve network timeout logic when using alternative discovery
v0.1.39 - Added 'search_album', 'search_artist', search_track' synonyms
- Fix WARN(ING) setting for --log option
- Require SoCo >= 0.20
v0.1.38 - Add search caching and indexed playback for 'tracks_in_album'
- Add search caching and indexed playback for 'list_albums'
- Add search caching and indexed playback for 'search_artists'
- Add '--docs' option to print URL to online documentation
- Add 'soco-discover' synonym for 'sonos-discover'
- Add line_in 'right_input' parameter for stereo paired P:5/Fives
- Behaviour change: Line-In starts playback after being selected
v0.1.37 - Fix 'play_favourite_radio_station'
- Improve output from 'track' action for non-queue items
- Add 'cue_favourite_radio_station' action
v0.1.36 - Add 'last_search' action to cache track and album searches
- Add 'queue_last_search_number' action
v0.1.35 - Add 'queue_position' action
- Add 'play_next' option to 'queue_track' and 'queue_album'
- Add 'play_next' option for 'add_playlist_to_queue'
- Add 'play_next' option for 'add_favourite_to_queue'
v0.1.34 - Add 'fade' synonym for 'cross_fade'
- Add 'remove_current_track_from_queue' action
- Add 'remove_last_track_from_queue' action
v0.1.33 - Add 'none' as a synonym for 'off', in the 'repeat' action
- Add the ability to use sequences and ranges with 'remove_from_queue'
v0.1.32 - Add 'shuffle' action for direct inspection and control of shuffle
mode
- Add 'repeat' action for direct inspection and control of repeat mode
v0.1.31 - Add 'transfer_to' synonym for 'transfer_playback'
- Add 'create_playlist_from_queue' synonym for 'save_queue'
- Remove erroneous printout in 'tracks_in_albums'
v0.1.30 - Added 'SHUFFLE_REPEAT_ONE' playback mode
- Add 'transfer_playback' action
v0.1.29 - Updated logic fix for 'wait_stopped_for'
- Add 'status' synomym for 'playback'
v0.1.28 - Add 'cue_favourite' action
v0.1.27 - Add 'wait_for' synonym for 'wait'
- Fix minor timer expiry logic issue in 'wait_stopped_for'
- Improve SoCo version check
- Improve some error messages regarding use of ':'
v0.1.26 - Add 'queue_track' action
- Add 'list_queue <track_number>' action
v0.1.25 - Add music library functions: 'list_artists', 'list_albums',
'search_library', 'search_artists', 'search_albums'
'search_tracks', 'tracks_in_albums', 'queue_album'
- sonos-discover behaviour change: '-p' now prints the current speaker
data then exits, and '-s' has been removed.
v0.1.24 - Add 'loop_to_start' action
- Allow CTRL-C to break out of 'wait_stopped_for' state on
Windows (SIGTERM)
- Add 'soco' synonym for 'sonos' command
- Fix loop counting defect
v0.1.23 - Add conditional modifiers 'if_playing' and 'if_stopped'
_ Add actions 'loop_for' and 'loop_until'
- Reintroduce SIGKILL workaround for non-Windows platforms
v0.1.22_1 - Revert use of SIGKILL (was preventing running on Windows)
v0.1.22 - Add 'wait_stopped_for' action (experimental)
- Add 'loop' and 'loop <iterations>' actions (experimental)
- Fix 100 track display limit on 'list_playlist_tracks'
v0.1.21 - Add 'rfq' synonym for 'remove_from_queue'
- Added 'wait_start' and 'wait_stop' actions
v0.1.20 - Add 'list_all_playlist_tracks' action
v0.1.19 - Exact speaker name matching is now case sensitive
- Additional logging
- Add 'list_playlist_tracks' action
v0.1.18 - Add README notes on what sources can be played back
- 'add_playlist_to_queue' now returns the first track queue position
- Experimental support for 'add_fav_to_queue', with some issues
- Fix issue with WARN-level logging enabled by default
v0.1.17_1 - Remove backport requirement (was breaking Windows installs)
v0.1.17 - Added 'wait_until' action
- Fix for Python 3.7 requirement
v0.1.16 - Add ability to cancel sleep timers
- Add the 'sleep_at' action to schedule a sleep timer
- Allow 'wait', 'sleep', to use HH:MM:SS format for durations
- Miscellaneous minor fixes
v0.1.15 - Improve sleep timer action to allow durations in h/m/s
- Initial logging capability
v0.1.14 - Improved, faster discovery for local speaker list
- Add 'libraries' action
- Add 'sysinfo' action
v0.1.13 - Change to local speaker list file contents. Old speaker data files
will be removed and rediscovery will be required.
- Simple SIGINT handling added.
- Added '-v' option to sonos-discover
- 'sonos-discover -s' prints Sonos software version of each speaker
- Add 'alarms' action to list Sonos alarms
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: MANIFEST.in
================================================
include requirements.txt
================================================
FILE: Makefile
================================================
.DEFAULT_GOAL := no_op
SRC = setup.py soco_cli/*.py
TESTS = tests/*.py
MANIFEST = LICENSE README.md PYPI_README.md MANIFEST.in requirements.txt
BUILD_DIST = build dist soco_cli.egg-info
PYCACHE = soco_cli/__pycache__ tests/__pycache__ __pycache__
TOC = README.md.*
build: $(SRC) $(MANIFEST)
python -m build
clean:
rm -rf $(BUILD_DIST) $(PYCACHE) $(TOC)
install: build
pip install -U -e .
uninstall:
pip uninstall -y soco_cli
black: $(SRC)
black --preview --target-version=py37 $(SRC) $(TESTS)
isort: $(SRC)
isort --profile black $(SRC) $(TESTS)
format: isort black
mypy: $(SRC) $(TESTS)
mypy $(SRC) $(TESTS)
pypi_upload: clean build
python -m twine upload --repository pypi dist/*
pypi_test: clean build
python -m twine upload --repository testpypi dist/*
pypi_check: build
twine check dist/*
toc:
./gh-md-toc --insert README.md
update:
pip install -U -r requirements.txt -r requirements-dev.txt
no_op:
# Available targets are: build, clean, install, uninstall, black, pypi_upload, pypi_check
================================================
FILE: PYPI_README.md
================================================
# SoCo-CLI: Control Sonos from the Command Line
## Overview
SoCo-CLI is a powerful command line wrapper for the popular Python SoCo library [1], for controlling Sonos systems. SoCo-CLI is written entirely in Python and is portable across platforms.
A simple `sonos` command provides easy control over a huge range of speaker functions, including playback, volume, groups, EQ settings, sleep timers, alarms, speaker settings, the playback queue, etc. Multiple commands can be run in sequence, including the ability to insert delays between commands, to wait for speakers to stop or start playing, and to create repeated action sequences using loops. Audio files from the local filesystem can be played directly on Sonos.
SoCo-CLI has an orderly command structure and consistent return values, making it suitable for use in automated scripts, `cron` jobs, etc.
For interactive command line use, SoCo-CLI provides a powerful **Interactive Shell Mode** that improves speed of operation and reduces typing.
SoCo-CLI can be imported as a streamlined, high-level **API** library by other Python programs, and acts as an intermediate abstraction layer between the client program and the underlying SoCo library, simplifying the use of SoCo.
SoCo-CLI can also run as a simple **HTTP API server**, providing access to a huge range of actions via simple HTTP requests. (Requires Python 3.6 or above.)
## Supported Environments
- Requires Python 3.5+. (The HTTP API Server functionality requires Python 3.6 or above.)
- Runs on all platforms supported by Python. Tested on various versions of Linux, macOS and Windows.
- Works with Sonos 'S1' and 'S2' systems, as well as split S1/S2 systems.
## Installation
Install from PyPI using **`pip install soco-cli`**.
## User Guide
The installer adds the `sonos` command to the PATH. All commands have the form:
```
sonos SPEAKER ACTION <parameters>
```
- `SPEAKER` identifies the speaker, and can be the speaker's Sonos Room name or its IPv4 address in dotted decimal format. Note that the speaker name is case sensitive (unless using alternative discovery, discussed in the full documentation).
- `ACTION` is the operation to perform on the speaker. It can take zero or more parameters depending on the operation.
Actions that make changes to speakers do not generally provide return values. Instead, the program exit code can be inspected to test for successful operation (exit code 0). If an error is encountered, an error message will be printed to `stderr`, and the program will return a non-zero exit code.
### Simple Usage Examples:
- **`sonos "Living Room" volume`** Returns the current volume setting of the *Living Room* speaker.
- **`sonos Study volume 25`** Sets the volume of the *Study* speaker to 25.
- **`sonos Study group Kitchen`** Groups the *Study* speaker with the *Kitchen* speaker.
- **`sonos 192.168.0.10 mute`** Returns the mute state ('on' or 'off') of the speaker at the given IP address.
- **`sonos 192.168.0.10 mute on`** Mutes the speaker at the given IP address.
- **`sonos Kitchen play_favourite Jazz24 : wait 30m : Kitchen stop`** Plays 'Jazz24' for 30 minutes, then stops playback.
Please see [https://github.com/avantrec/soco-cli](https://github.com/avantrec/soco-cli) for full documentation.
## Links
[1] https://github.com/SoCo/SoCo
## Acknowledgments
All trademarks acknowledged. Avantrec Ltd has no connection with Sonos Inc.
================================================
FILE: README.md
================================================
# SoCo-CLI: Control Sonos from the Command Line
<!--ts-->
* [SoCo-CLI: Control Sonos from the Command Line](#soco-cli-control-sonos-from-the-command-line)
* [Overview](#overview)
* [Supported Environments](#supported-environments)
* [Installation](#installation)
* [User Guide](#user-guide)
* [The sonos Command](#the-sonos-command)
* [Speaker Discovery by Name](#speaker-discovery-by-name)
* [Simple Usage Examples](#simple-usage-examples)
* [The SPKR Environment Variable](#the-spkr-environment-variable)
* [Using Shell Aliases](#using-shell-aliases)
* [Options for the sonos Command](#options-for-the-sonos-command)
* [Firewall Rules](#firewall-rules)
* [Operating on All Speakers: Using _all_](#operating-on-all-speakers-using-_all_)
* [Redirection of Actions to Coordinator Devices](#redirection-of-actions-to-coordinator-devices)
* [Guidelines on Playing Content](#guidelines-on-playing-content)
* [Radio Stations](#radio-stations)
* [Single Tracks](#single-tracks)
* [Albums and Playlists](#albums-and-playlists)
* [Audio Files on the Local Filesystem](#audio-files-on-the-local-filesystem)
* [Local Playlists (M3U Files)](#local-playlists-m3u-files)
* [Directories of Audio Files](#directories-of-audio-files)
* [Spotify, Tidal, Deezer, and Apple Music Share Links](#spotify-tidal-deezer-and-apple-music-share-links)
* [Complete List of Available Actions](#complete-list-of-available-actions)
* [Volume and EQ Control](#volume-and-eq-control)
* [Playback Control](#playback-control)
* [Queue Actions](#queue-actions)
* [Favourites and Playlists](#favourites-and-playlists)
* [TuneIn Radio Station Favourites](#tunein-radio-station-favourites)
* [Grouping, Stereo Pairing, and Surround (Satellite) Speakers](#grouping-stereo-pairing-and-surround-satellite-speakers)
* [Alarms](#alarms)
* [Music Library Search Functions](#music-library-search-functions)
* [Speaker and Sonos System Information](#speaker-and-sonos-system-information)
* [Multiple Sequential Commands](#multiple-sequential-commands)
* [Chaining Commands Using the : Separator](#chaining-commands-using-the--separator)
* [Inserting Delays: wait and wait_until](#inserting-delays-wait-and-wait_until)
* [Waiting Until Playback has Started/Stopped: wait_start, wait_stop and wait_end_track](#waiting-until-playback-has-startedstopped-wait_start-wait_stop-and-wait_end_track)
* [The wait_stopped_for <duration> Action](#the-wait_stopped_for-duration-action)
* [Repeating Commands: The loop Actions](#repeating-commands-the-loop-actions)
* [Conditional Command Execution](#conditional-command-execution)
* [Interactive Shell Mode](#interactive-shell-mode)
* [Description](#description)
* [Usage](#usage)
* [Shell History and Auto-Completion](#shell-history-and-auto-completion)
* [Shell Aliases](#shell-aliases)
* [Push and Pop](#push-and-pop)
* [Alias Subroutines](#alias-subroutines)
* [Alias Arguments](#alias-arguments)
* [Saving and Loading Aliases](#saving-and-loading-aliases)
* [Single Keystroke Mode](#single-keystroke-mode)
* [Cached Discovery](#cached-discovery)
* [Usage](#usage-1)
* [Speaker Naming](#speaker-naming)
* [Refreshing the Local Speaker List](#refreshing-the-local-speaker-list)
* [Discovery Options](#discovery-options)
* [The sonos-discover Command](#the-sonos-discover-command)
* [Options for the sonos-discover Command](#options-for-the-sonos-discover-command)
* [The SoCo-CLI HTTP API Server](#the-soco-cli-http-api-server)
* [Server Usage](#server-usage)
* [Using the Local Speaker Cache](#using-the-local-speaker-cache)
* [HTTP Request Structure](#http-request-structure)
* [Return Values](#return-values)
* [Asynchronous Actions (Experimental)](#asynchronous-actions-experimental)
* [Macros: Defining Custom HTTP API Server Actions](#macros-defining-custom-http-api-server-actions)
* [Macro Definition and Usage](#macro-definition-and-usage)
* [Macro Arguments](#macro-arguments)
* [Using the Generic Macro](#using-the-generic-macro)
* [Troubleshooting](#troubleshooting)
* [Specifying the Macro Definition File](#specifying-the-macro-definition-file)
* [Reloading the Macro Definition File](#reloading-the-macro-definition-file)
* [Return Values](#return-values-1)
* [Listing Macros](#listing-macros)
* [Asynchronous Macros](#asynchronous-macros)
* [Listing Speakers](#listing-speakers)
* [Rediscovering Speakers](#rediscovering-speakers)
* [Inspecting the HTTP API](#inspecting-the-http-api)
* [Using SoCo-CLI as a Python Library](#using-soco-cli-as-a-python-library)
* [Importing the API](#importing-the-api)
* [Using the API](#using-the-api)
* [Convenience Functions](#convenience-functions)
* [Known Issues](#known-issues)
* [Uninstalling](#uninstalling)
* [Acknowledgments](#acknowledgments)
* [Resources](#resources)
<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
<!-- Added by: pwt, at: Tue Apr 7 08:43:38 BST 2026 -->
<!--te-->
## Overview
SoCo-CLI is a powerful command line wrapper for the popular Python SoCo library [1], for controlling Sonos systems. SoCo-CLI is written entirely in Python and is portable across platforms.
A simple `sonos` command provides easy control over a huge range of speaker functions, including playback, volume, groups, EQ settings, sleep timers, alarms, speaker settings, the playback queue, etc. Multiple commands can be run in sequence, including the ability to insert delays between commands, to wait for speakers to stop or start playing, and to create repeated action sequences using loops. Audio files from the local filesystem can be played directly on Sonos.
SoCo-CLI has an orderly command structure and consistent return values, making it suitable for use in automated scripts, `cron` jobs, etc.
For interactive command line use, SoCo-CLI provides a powerful [Interactive Shell Mode](#interactive-shell-mode) that improves speed of operation and reduces typing.
SoCo-CLI can be imported as a streamlined, high-level [API](#using-soco-cli-as-a-python-library) library by other Python programs, and acts as an intermediate abstraction layer between the client program and the underlying SoCo library, simplifying the use of SoCo.
SoCo-CLI can also run as a simple [HTTP API server](#the-soco-cli-http-api-server), providing access to a huge range of actions via simple HTTP requests. (Requires Python 3.7 or above.)
SoCo-CLI only uses **local network** (UPnP) interaction with Sonos devices. There is no support for the Sonos cloud API and there is no intention to change this. This means that support for music service content is limited to Sonos Playlists and music service shared links.
## Supported Environments
- Requires Python 3.5+
- For Python 3.5, the latest compatible version of the underlying SoCo library is v0.27.1. Python 3.6 and above supports the latest version of the SoCo library.
- The HTTP API Server functionality requires Python 3.7 or above.
- Should run on all platforms supported by Python. Tested on various versions of Linux, macOS and Windows.
- Works with Sonos 'S1' and 'S2' systems, as well as split S1/S2 systems.
## Installation
Install the latest version from PyPI [2] using **`pip install -U soco-cli`**.
Soco-CLI can also be easily installed as a self-contained application with [**pipx**](https://pypa.github.io/pipx/) using **`pipx install soco-cli`**.
Please see the CHANGELOG.txt file for a list of the user-facing changes in each release.
## User Guide
### The `sonos` Command
The installer adds the `sonos` command to the PATH. If the `sonos` command is not found, make sure your `PATH` is set up correctly for the Python installation you want to use.
All commands have the form:
```
sonos SPEAKER ACTION <parameters>
```
- `SPEAKER` identifies the speaker to operate on, and can be the speaker's Sonos Room (Zone) name or its IPv4 address in dotted decimal format. Partial, case-insensitive speaker names will be matched, e.g., `kit` will match `Kitchen`. Partial matches must be unambiguous, or an error is returned.
- `ACTION` is the operation to perform on the speaker. It can take zero or more parameters depending on the action.
As usual, command line arguments containing spaces must be surrounded by quotes: double quotes work on all supported OS platforms, while Linux and macOS also support single quotes.
The `soco` command is also added to the PATH, and can be used as an alias for the `sonos` command if preferred.
Actions that make changes to speakers do not generally provide return values. Instead, the program exit code can be inspected to test for successful operation (exit code 0). If an error is encountered, an error message will be printed to `stderr`, and the program will return a non-zero exit code. Note that `sonos` actions are executed without seeking user confirmation; please bear this in mind when manipulating the queue, playlists, etc.
### Speaker Discovery by Name
SoCo-CLI will try a number of approaches to find a speaker's IP address by speaker name, which escalate in cost until the speaker is discovered or discovery fails. If SoCo-CLI seems slow to find speakers (especially if you have a multi-household Sonos system), or if you occasionally experience problems with speakers not being found, please take a look at the generally faster [Cached Discovery](#cached-discovery) method.
### Simple Usage Examples
- **`sonos "Living Room" volume`** Returns the current volume setting of the *Living Room* speaker.
- **`sonos Study volume 25`** Sets the volume of the *Study* speaker to 25.
- **`sonos Study group Kitchen`** Groups the *Study* speaker with the *Kitchen* speaker.
- **`sonos 192.168.0.10 mute`** Returns the mute state ('on' or 'off') of the speaker at the given IP address.
- **`sonos 192.168.0.10 mute on`** Mutes the speaker at the given IP address.
- **`sonos Kitchen play_favourite Jazz24 : wait 30m : Kitchen stop`** Plays 'Jazz24' for 30 minutes, then stops playback.
- **`sonos Study play_file "Zoo Station.mp3"`** Plays a local audio file.
### The `SPKR` Environment Variable
To avoid typing the speaker name, or to parameterise the use of SoCo-CLI commands, it's possible to use the `SPKR` environment variable instead of supplying the speaker name (or IP address) on the command line.
**Example:** The following will set up all sonos commands to operate on the "Front Reception" speaker:
Linux and macOS:
```
$ export SPKR="Front Reception"
$ sonos play
$ sonos wait_stop : volume 10
```
Windows:
```
C:\ set SPKR="Front Reception"
C:\ sonos play
C:\ sonos wait_stop : volume 10
```
IP addresses also work, e.g.: `$ export SPKR=192.168.0.50`.
If you want to ignore the `SPKR` environment variable for a specific `sonos` invocation, use the `--no-env` command line option.
### Using Shell Aliases
If your shell supports it, shell aliasing can be very convenient in creating shortcuts to SoCo-CLI commands. For example, I have the following in my `.zshrc` file:
```
# Sonos Aliases
alias s="sonos"
alias sk="sonos Kitchen"
alias sr="sonos 'Rear Reception'"
alias sf="sonos 'Front Reception'"
alias sm="sonos Move"
alias sb="sonos Bedroom"
alias sb2="sonos 'Bedroom 2'"
alias ss="sonos Study"
alias sd="sonos-discover"
```
This allows the use of shorthand like `sk stop`, to stop playback on the Kitchen speaker. Note, however, that this won't work with sequences of commands using a single `sonos` invocation, separated with ` : ` (see [Multiple Sequential Commands](#multiple-sequential-commands)), only for the first command in such a sequence. (Normal, multiple `sonos` invocation, shell sequences using `;` or `&&` as separators will work, of course.)
### Options for the `sonos` Command
- **`--version, -v`**: Print the versions of SoCo-CLI, SoCo, and Python.
- **`--check-for-update`**: Check for a more recent version of SoCo-CLI.
- **`--actions`**: Print the list of available actions.
- **`--docs`**: Print the URL of this README documentation, for the version of SoCo-CLI being used.
- **`--log <level>`**: Turn on logging. Available levels are `NONE` (default), `CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`, in order of increasing verbosity. `INFO` level logging tends to be the most useful when troubleshooting SoCo-CLI issues.
The following options are for use with the cached discovery mechanism:
- **`--use-local-speaker-list, -l`**: Use the local speaker list instead of SoCo discovery. The speaker list will first be created and saved if it doesn't already exist.
- **`--refresh-local-speaker-list, -r`**: In conjunction with the `-l` option, the speaker list will be regenerated and saved.
- **`--network-discovery-threads, -t`**: The maximum number of parallel threads used to scan the local network.
- **`--network-discovery-timeout, -n`**: The timeout used when scanning each host on the local network (how long to wait for a socket connection on port 1400 before giving up).
- **`--min-netmask, -m`**: The minimum netmask to use when scanning networks. Used to constrain the IP search space.
Note that the `sonos-discover` utility (discussed below) can also be used to manage the local speaker list. This is the recommended way of using cached discovery: first run `sonos-discover` to create the local speaker database, then use `sonos` with the `-l` option to use the local database when invoking `sonos` actions.
If you set the environment variable **`USE_LOCAL_CACHE=TRUE`**, the `--use-local-speaker-list` option will always be used.
### Firewall Rules
If you're running on a host with its firewall enabled, note that some SoCo-CLI actions require the following incoming ports to be open: **TCP 1400-1499**, **TCP 54000-54099**.
The TCP/1400 range is used to receive notification events from Sonos players (used in the `wait_stop` action, etc.), the TCP/54000 range is used for the built-in Python HTTP server when playing files from the local filesystem (used in the `play_file` action).
When opening ports, SoCo-CLI will try port numbers starting at the beginning of the range and incrementing by one until a free port is found, up to the limit of the range. This allows multiple invocations of SoCo-CLI to run in parallel on the same host.
The standard speaker discovery mechanism uses SSDP multicast (multicast address 239.255.255.250 on UDP port 1900). The outgoing port chosen for the multicast request is variable and OS-dependent, e.g., for most Linux distributions it's in the ephemeral port range 32768–60999. Multicast responses are returned to the outgoing port, so if the firewall blocks incoming UDP traffic on the relevant port range, then standard discovery will fail. SoCo-CLI will automatically fall back to using network scan discovery if standard discovery fails, but this is slower, so either adjust your firewall to open the relevant ports or consider using SoCo-CLI [Cached Discovery](#cached-discovery).
If using the HTTP API Server functionality, its listen port must be open to incoming TCP requests. The default port is 8000.
### Operating on All Speakers: Using `_all_`
There is a limited set of operations where it can be desirable to operate on all speakers, e.g., muting every speaker in the house. This is done by using **`_all_`** as the speaker name. Operations will be performed on all visible devices.
**Examples**: `sonos _all_ mute on` and `sonos _all_ relative_volume -10`.
Note that `_all_` can be used with every `sonos` operation: no checking is performed to ensure that the use of `all` is appropriate, so use with caution.
### Redirection of Actions to Coordinator Devices
If an action is applied to a non-coordinator device, there are some cases where the action is automatically redirected to the coordinator. For example, if `lounge` is the coordinator speaker and `kitchen` is a grouped speaker:
- `sonos kitchen queue` will be redirected to the `lounge` speaker, because that's the queue in use
- `sonos kitchen volume 40` will remain directed to the `kitchen` speaker
## Guidelines on Playing Content
SoCo-CLI enables playback of content from the **Sonos Favourites** and **Sonos Playlists** collections, from **local libraries**, and from the **TuneIn 'My Radio Stations'** list. It also allows playback of **audio files from the local filesystem**, and can add **sharelinks** from the Spotify, Tidal, Apple Music, and Deezer music services to the Sonos queue. On macOS, the contents of physical CDs can be played direct to Sonos.
### Radio Stations
Radio stations can be played by adding them to your Sonos Favourites, and then starting playback using `play_fav`. Alternatively, stations can be added to the TuneIn 'My Radio Stations' list, and played using `play_favourite_radio_station`.
### Single Tracks
As with radio stations, add single tracks from local libraries and music services to your Sonos Favourites, and play them using `play_fav`.
`sonos <speaker_name> play_fav <favourite_name>`
Tracks from local music libraries can also be added to the queue using `sonos <speaker> queue_track <track_name>`, which returns the queue position of the track. It can then be played using `sonos <speaker> play_from_queue <track_number>`.
### Albums and Playlists
Albums and playlists from local libraries or music services can be added to your Sonos Playlists, and then played by adding them to the queue, followed by playing from the queue. For example:
`sonos <speaker_name> clear_queue : <speaker_name> add_playlist_to_queue <playlist> : <speaker_name> play_from_queue`
Or, to add to the current queue, then play the first playlist track:
```
sonos <speaker_name> add_playlist_to_queue <playlist>
24 <-- Returns queue position of the first playlist track
sonos <speaker_name> play_from_queue last_added
```
To add imported playlists from local libraries to the queue, use the `add_library_playlist_to_queue` action.
Albums from local music libraries can also be added to the queue using `sonos <speaker> queue_album <album_name>`. The action returns the queue position of the first track in the album, which can then be played as in the example above:
### Audio Files on the Local Filesystem
It's possible to play local audio files in **MP3, M4A, MP4, FLAC, OGG, WMA, WAV, and AIFF** formats directly on your Sonos speakers using the `play_file` (or `play_local_file`) action.
**Example**: `sonos Lounge play_file mozart.mp3`
SoCo-CLI establishes a temporary internal HTTP server from which the specified audio file can be streamed, and then instructs the speaker to play it. The `play_file` action will terminate once playback stops. Note that playback can be paused using a Sonos app (or SoCo-CLI), and the HTTP server will remain active so that playback can be resumed.
Unfortunately, one can pause but not fully stop playback when using the Sonos apps. Hence, stop playback by playing something else on Sonos, by issuing a 'CTRL-C' to the active `play_file` action, or by issuing `sonos <SPEAKER> stop` from another command line. Alternatively, use the `_end_on_pause_` option to terminate the `play_file` action if playback is paused.
The host running SoCo-CLI must remain on and connected to the network during playback, in order to serve the file to the speaker. The internal HTTP server is active only for the duration of the `play_file` action. For security reasons, it will only serve the specified audio file, and only to the IP addresses of the Sonos speakers in the system.
Multiple files can be played in sequence by providing multiple audio file names as parameters.
**Example**: `sonos Lounge play_file one.mp3 two.mp3 three.mp3`
### Local Playlists (M3U Files)
The `play_m3u` (or `play_local_m3u`) action will play a local filesystem playlist in M3U (or M3U8) format. Files in the playlist should be available on the local filesystem; any that are not will be skipped. Simple lists of audio files in non-M3U format can also be supplied. Comments can be inserted in the file by prefixing each comment line with `#`.
There are options to print the track filenames as they are played, to shuffle the playlist, and to select a single random track from the playlist. There is also an **interactive mode** option, which allows (N)ext track, (P)ause playback, and (R)esume playback, while the playlist is being played.
**Example**: `sonos Lounge play_m3u my_playlist.m3u`, or, to print filenames and invoke interactive mode: `sonos Lounge play_m3u my_playlist.m3u pi`.
This feature works by invoking the `play_file` action for each file in the playlist in sequence, so the same rules apply as for `play_file`. Note that `play_m3u` does not create a Sonos queue on the speaker -- the 'queue' is managed locally by SoCo-CLI -- so it's not possible to skip forward or back using a Sonos app.
### Directories of Audio Files
To play every audio file in a local directory, use the `play_directory` (or `play_dir`) action. As with the `play_m3u` action this invokes `play_file` for each valid audio file in the directory. It does not traverse into subdirectories.
**Example**: `sonos Lounge play_directory "Music/Mozart/The Magic Flute/CD 1"`
On macOS (but not on Windows or Linux), if you have an attached CD drive, this action can be used to play a CD directly to your Sonos speakers, e.g.:
`sonos Lounge play_dir "/Volumes/Audio CD"`.
The `play_file` action can be used to play individual tracks on the CD, e.g.:
`sonos Lounge play_file "/Volumes/Audio CD/1 Audio Track.aiff"`.
### Spotify, Tidal, Deezer, and Apple Music Share Links
The `add_sharelink_to_queue` (or `sharelink`) action can be used to add one or more share links from Spotify, Tidal, Deezer, or Apple Music to the queue, provided the Sonos system has a subscription to the required service.
Links can refer to tracks, albums, or playlists. The queue position of the first track added is returned, which can then be played using `play_from_queue`. Share links can be of the form:
- `https://open.spotify.com/track/6cpcorzV5cmVjBsuAXq4wD`
- `spotify:album:6wiUBliPe76YAVpNEdidpY`
- `https://tidal.com/browse/album/157273956`
- `https://www.deezer.com/en/playlist/5390258182`
- `https://music.apple.com/dk/album/black-velvet/217502930?i=217503142`
Multiple sharelinks can be supplied in a single action; they are added to the queue in order. An optional queue position can be supplied as the final argument; it applies to the first sharelink, and subsequent sharelinks are appended after it.
**Examples**:
```
sonos Kitchen sharelink "https://open.spotify.com/track/6cpcorzV5cmVjBsuAXq4wD"
5 <-- Returns queue position of first track
sonos Kitchen play_from_queue 5
sonos Kitchen sharelink "https://open.spotify.com/track/AAA" "https://open.spotify.com/album/BBB"
5 <-- Both added; returns queue position of first track
```
## Complete List of Available Actions
### Volume and EQ Control
- **`balance`**: Returns the balance setting of the speaker as a value between -100 and +100, where -100 is left channel only, 0 is left and right set to the same volume, and +100 is right channel only.
- **`balance <balance_setting>`**: Sets the balance of the speaker to a value between -100 and +100, where -100 is left channel only, 0 is left and right set to the same volume, and +100 is right channel only. Intermediate values produce a mix of right/left channels.
- **`bass`**: Returns the bass setting of the speaker, from -10 to 10.
- **`bass <number>`**: Sets the bass setting of the speaker to `<number>`. Values must be between -10 and 10.
- **`dialog_mode`** (or **`dialog`**, **`dialogue_mode`**, **`dialogue`**): Returns the dialog mode setting of the speaker, 'on' or 'off' (if applicable).
- **`dialog_mode <on|off>`** (or **`dialog`**, **`dialogue_mode`**, **`dialogue`**): Sets the dialog mode setting of the speaker to 'on' or 'off' (if applicable).
- **`fixed_volume`**: Returns whether the speaker's Fixed Volume feature is enabled, 'on' or 'off'. (Applies to Sonos Connect and Port devices only.)
- **`fixed_volume <on|off>`**: Sets whether the speaker's Fixed Volume feature is enabled.
- **`group_mute`**: Returns the group mute state of a group of speakers, 'on' or 'off'.
- **`group_mute <on|off>`**: Sets the group mute state of a group of speakers to 'on' or 'off'.
- **`group_relative_volume <adjustment>` (or `group_rel_vol`, `grv`)**: Raises or lowers the group volume by `<adjustment>` which must be a number from -100 to 100.
- **`group_volume` (or `group_vol`)**: Returns the current group volume setting of the speaker's group (0 to 100).
- **`group_volume <volume>` (or `group_vol`)**: Sets the group volume of the speaker's group to `<volume>` (0 to 100). This action operates in the same way as the group volume slider in the Sonos apps. The volume applied to each speaker in the group is scaled by the speaker's current volume.
- **`group_volume_equalize <volume>` (or `group_volume_equalise`, `gve`)**: Sets the volume of all speakers in a group to the same absolute `<volume>` (0-100).
- **`loudness`**: Returns the loudness setting of the speaker, 'on' or 'off'.
- **`loudness <on|off>`**: Sets the loudness setting of the speaker to 'on' or 'off'.
- **`mute`**: Returns the mute setting of the speaker, 'on' or 'off'.
- **`mute <on|off>`**: Sets the mute setting of the speaker to 'on' or 'off'.
- **`night_mode`** (or **`night`**): Returns the night mode setting of the speaker, 'on' or 'off' (if applicable).
- **`night_mode <on|off>`** (or **`night`**): Sets the night mode setting of the speaker to 'on' or 'off' (if applicable).
- **`playing_tv`** (or **`is_playing_tv`**): Returns whether the speaker is currently playing from its TV input source, 'yes' or 'no'.
- **`ramp_to_volume <volume>` (or `ramp`)**: Gently raise or reduce the volume to `<volume>`, which is between 0 and 100. Returns the number of seconds to complete the ramp.
- **`relative_bass <adjustment>` (or `rel_bass`, `rb`)**: Increase or reduce the bass setting by `<adjustment>`, a value between -10 and 10.
- **`relative_sub_gain <adjustment>` (or `rel_sub_gain`, `rsg`)**: Increase or reduce a Sub's gain setting by `<adjustment>`, a value between -15 and 15.
- **`relative_treble <adjustment>` (or `rel_treble`, `rt`)**: Increase or reduce the treble setting by `<adjustment>`, a value between -10 and 10.
- **`relative_volume <adjustment>` (or `rel_vol`, `rv`)**: Raises or lowers the volume by `<adjustment>`, which must be a number from -100 to 100.
- **`sub_enabled`**: Returns `on` if the zone's subwoofer is enabled, otherwise `off`.
- **`sub_enabled <on|off>`**: Enables or disables a zone's subwoofer.
- **`sub_gain`**: Reports the value of a Sub's gain (for speaker groups with a bonded Sub), from `-15` to `+15`.
- **`sub_gain <gain>`**: Sets the value of a Sub's gain, from `-15` to `+15`.
- **`surround_enabled`**: Returns `on` if the zone's surround speakers are enabled, otherwise `off`.
- **`surround_enabled <on|off>`**: Enables or disables a zone's surround speakers.
- **`surround_full_volume_enabled`**: Reports whether surround speakers are in full volume mode (on) or ambient mode (off).
- **`surround_full_volume_enabled <on|off>`**: Sets surround speakers to full volume mode (on) or ambient mode (off).
- **`surround_volume_music`**: Reports the value of the volume level for surround speakers, when playing music sources, from `-15` to `+15`.
- **`surround_volume_music <level>`**: Sets the value of the volume level for surround speakers, when playing music sources, from `-15` to `+15`.
- **`surround_volume_tv`**: Reports the value of the volume level for surround speakers, when playing TV sources, from `-15` to `+15`.
- **`surround_volume_tv <level>`**: Sets the value of the volume level for surround speakers, when playing TV sources, from `-15` to `+15`.
- **`treble`**: Returns the treble setting of the speaker, from -10 to 10.
- **`treble <number>`**: Sets the treble setting of the speaker to `<number>`. Values must be between -10 and 10.
- **`trueplay`**: Returns whether a speaker's Trueplay profile is enabled, 'on' or 'off'.
- **`trueplay <on|off>`**: Sets whether a speaker's Trueplay profile is enabled. Can only be set to 'on' for speakers that have a current Trueplay tuning profile available.
- **`volume` (or `vol`)**: Returns the current volume setting of the speaker (0 to 100).
- **`volume <volume>` (or `vol`)**: Sets the volume of the speaker to `<volume>` (0 to 100).
### Playback Control
- **`album_art`**: Return a URL to the album art for the current stream, if there's one available.
- **`cross_fade`** (or **`crossfade`, `fade`**): Returns the cross fade setting of the speaker, 'on' or 'off'.
- **`cross_fade <on|off>`** (or **`crossfade`, `fade`**): Sets the cross fade setting of the speaker to 'on' or 'off'.
- **`cue_line_in <on | line_in_speaker | left_input, right_input | line_in_speaker right_input>`**: This functions in the same way as the `line_in` action below, but does not automatically start playback (and will stop playback if the speaker is currently playing from the selected Line In input). Can be used without supplying the `on` parameter.
- **`end_session`**: Ends a third-party controlled session, e.g. Spotify Connect.
- **`get_channel`** (or **`channel`**): Get the channel name of the current radio stream, if available.
- **`get_uri`**: Get the URI of the current track or stream. (Note: the output format is subject to change.)
- **`line_in`**: Returns a speaker's Line-In state, 'on' if its input is set to a Line-In source, 'off' otherwise, plus the current playback state (e.g., `on (PAUSED_PLAYBACK)`).
- **`line_in <on | line_in_speaker | left_input, right_input | line_in_speaker right_input>`**: Switch a speaker to a Line-In input. Playback is started automatically. A speaker can be switched to its own Line-In input (`<on>`), **or** the Line-In input of another `<line_in_speaker>` (if applicable). For the case where there is a stereo pair of Play:5 or Five speakers, the left hand speaker's Line-In source is selected using `left_input` (default), and the right-hand speaker's Line-In input is selected using `right_input`. (Complicated example: `sonos Bedroom line_in Lounge right_input`, switches the Bedroom to the right-hand input of the stereo pair in the Lounge, and starts playback.)
- **`next`**: Move to the next track (if applicable for the current audio source).
- **`pause`**: Pause playback (if applicable for the audio source).
- **`pause_all`**: Pause playback on all speakers in the system. (Note: only pauses speakers that are in the same Sonos Household.)
- **`pauseplay`** (or **`playpause`**): Inverts a playing/paused state: if a speaker is currently playing, it will be paused or stopped; if a speaker is paused or stopped, playback will be started.
- **`play`** (or **`start`**): Start playback.
- **`play_directory <directory_name> <options>`** (or **`play_dir`**): Play all of the audio files in the specified local directory. Takes the same `<options>` as the `play_m3u` action. Does not traverse into subdirectories.
- **`play_file <filename> ...`** (or **`play_local_file`**): Play MP3, M4A, MP4, FLAC, OGG, WMA, WAV, or AIFF audio files from your computer. Multiple filenames can be provided and will be played in sequence. To configure termination of the command if playback is paused (as well as stopped), add `_end_on_pause_` to the command, i.e.: `play_file filename _end_on_pause_`.
- **`play_from_queue <track>`** (or **`play_queue`, `pfq`, `pq`**): Play track number `<track>` from the queue. Tracks begin at 1. If `<track>` is omitted, the first item in the queue is played.
- **`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.
- **`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`.
- **`play_mode <mode>` (or `mode`)**: Sets the play mode of the speaker to `<mode>`, which is one of the values above.
- **`play_sharelink <sharelink> [<sharelink2> ...] [<position>]`**: Adds one or more sharelinks to the queue and starts playback from the first one added. An optional position applies to the first sharelink; the rest are appended after it.
- **`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.
- **`previous` (or `prev`)**: Move to the previous track (if applicable for the audio source).
- **`repeat` (or `rpt`)**: Returns the repeat mode state: 'off', 'one', or 'all'.
- **`repeat <off,none|one|all>` (or `rpt`)**: Sets the repeat mode state to one of: 'off' (or 'none'), 'one', or 'all'.
- **`seek <time>` (or `seek_to`)**: Seek to a point within a track (if applicable for the audio source). `<time>` can be expressed using the same formats as used for `sleep_timer` below.
- **`seek_forward <time>` (or `sf`)**: Seek forward within a track (if applicable for the audio source). `<time>` can be expressed using the same formats as used for `sleep_timer` below.
- **`seek_back <time>` (or `sb`)**: Seek backward within a track (if applicable for the audio source). `<time>` can be expressed using the same formats as used for `sleep_timer` below.
- **`shuffle` (or `sh`)**: Returns 'on' if shuffle is enabled, 'off' if not.
- **`shuffle <on|off>` (or `sh`)**: Enables or disables shuffle mode.
- **`sleep_timer` (or `sleep`)**: Returns the current sleep timer remaining time in seconds; 0 if no sleep timer is active.
- **`sleep_timer <duration|off|cancel>` (or `sleep`)**: Set the sleep timer to `<duration>`, which can be **one** of seconds, minutes or hours. Floating point values for the duration are acceptable. Examples: **`10s`, `30m`, `1.5h`**. If the s/m/h is omitted, `s` (seconds) is assumed. The time duration formats HH:MM and HH:MM:SS can also be used. To **cancel** a sleep timer, use `off` or `cancel` instead of a duration.
- **`sleep_at <HH:MM:SS>`**: Sets the sleep timer to sleep at a time up to 24 hours in the future. For example, to set the speaker to sleep at 4pm, use `sleep_at 16:00`.
- **`stop`**: Stop playback.
- **`stop_all`**: Stop playback on all speakers in the system. (Note: only stops speakers that are in the same Sonos Household.)
- **`switch_to_tv`**: Switches to the TV input. Only applicable to soundbars and the Sonos Amp.
- **`track`**: Return information about the currently playing track.
- **`track_follow`** (or **`tf`**): Returns information about the currently playing track, and each subsequent track when the track changes. This action keeps running until cancelled using CTRL-C.
- **`track_follow_compact`** (or **`tfc`**): As `track_follow`, but with a more compact single line representation for each track.
- **`tv_audio_delay`**: Returns the current setting for the audio delay for TV sources, an integer from 0 to 5. (Sonos does not specify the units used for adjustment.) Only applicable to devices with TV inputs.
- **`tv_audio_delay <delay>`**: Sets the audio delay for TV sources; `delay` must be an integer from 0 to 5.
### Queue Actions
When adding items to the queue, the default is to add the items at the end of the queue unless an optional **`<position>`** parameter is supplied, which can take one of the following values:
- An **integer** queue position. If this is <= 0, the position will be set to 1; if greater than the end of the current queue, this will be set to the end of the queue.
- **`start`** or **`first`**: insert at the start of the queue (equivalent to `1`).
- **`play_next`** or **`next`**: Insert at the current queue playback position unless the queue is being used for playback, in which case the item is added directly after the currently playing track, and will play next.
- **`end`** or **`last`**: Insert at the end of the queue -- which is the default if the `position` parameter is not supplied.
Examples:
```shell
sonos lounge queue_search_results all start <-- 'start' is equivalent to '1'
sonos lounge queue_search_results 1-5,9 next
sonos lounge queue_search_results 1,3,4 end <-- Note: 'end' can be omitted
sonos kitchen queue_album zooropa next
```
When items are added to the queue successfully, the queue position of the first added track is returned. Use `play_from_queue` with this track number to commence playback of the added items.
The available actions are:
- **`add_playlist_to_queue <playlist_name> [<position>]`** (or **`queue_playlist`, `add_pl_to_queue`, `apq`**): Add `<playlist_name>` to the queue. Name matching is case-insensitive, and will work on partial matches.
- **`add_library_playlist_to_queue <playlist_name> [<position>]`** (or **`alpq`**): As above, but targets local library imported playlists instead of Sonos playlists.
- **`add_sharelink_to_queue <sharelink> [<sharelink2> ...] [<position>]`** (or **`sharelink`**): Add one or more **Spotify**, **Tidal**, **Deezer**, or **Apple Music** links (for tracks, albums, playlists, etc.) to the queue. Returns the queue position of the first track added. Multiple sharelinks are added in order; an optional position applies to the first, and the rest are appended.
- **`add_uri_to_queue <uri> [<position>]`** Adds a URI to the queue.
- **`clear_queue`** (or **`cq`**): Clears the current queue
- **`list_queue`** (or **`lq`, `q`**): List the tracks in the queue
- **`list_queue <track_number>`** (or **`lq`, `q`**): List the track in the queue at position `<track_number>`
- **`play_from_queue <track_number, or 'current', or 'last_added', or 'last', or 'random'>`** (or **`pfq`, `pq`**): Play `<track_number>` from the queue. Track numbers start from 1. If no `<track_number>` is provided, play starts from the beginning of the queue. If `current` is provided, play starts at the current queue position. If `last_added` is provided, play starts from the queue position of the last added track or set of tracks. If `last` is provided, the last track in the queue is played. If `random` is provided, playback will start at a random queue position.
- **`queue_album <album_name> [<position>]`** (or **`qa`**): Add `<album_name>` from the local library to the queue. If multiple (fuzzy) matches are found for the album name, a random match will be chosen.
- **`queue_length`** (or **`ql`**): Return the length of the current queue.
- **`queue_position`** (or **`qp`**): Return the current queue position.
- **`queue_search_results <search_result_numbers> [<position>]`** (or **`qsr`**): Queue one or more items from the list of items returned by the last search performed, at the end of the queue by default. Search result numbers start from 1, and can be supplied as single integers, sequences (e.g., '4,7,3'), ranges (e.g., '5-10'), or 'all' (for all tracks). Note: do not use spaces either side of the commas and dashes. Sequences and ranges can be mixed, e.g., '1,3-6,10'.
- **`queue_track <track_name> [<position>]`** (or **`qt`**): Add `<track_name>` from the local library to the queue. If multiple (fuzzy) matches are found for the track name, a random match will be chosen. Optionally, `next` or `play_next` can be added to insert the track at the next_play position in the queue. The queue position of the track will be returned.
- **`remove_current_track_from_queue` (or `rctfq`)**: Remove from the queue the track at the current queue position. If the track is playing, this will have the effect of stopping playback and starting to play the next track. (If the last track in the queue is playing, playback will stop and the previous track will start to play.)
- **`remove_last_track_from_queue <count>` (or `rltfq`)**: Removes the last `<count>` tracks from the queue. If `<count>` is omitted, the last track is removed.
- **`remove_from_queue <track_number|sequence|range>`** (or **`rfq`, `rq`**): Remove tracks from the queue. Track numbers start from 1, and can be supplied as single integers, sequences (e.g., '4,7,3'), or ranges (e.g., '5-10'). Note: do not use spaces either side of the commas and dashes. Sequences and ranges can be mixed, e.g., '1,3-6,10'.
- **`save_queue <title>`** (or **`sq`, `create_playlist_from_queue`**): Save the current queue as a Sonos playlist called `<title>`.
- **`set_queue_position <track_number>`** (or **`sqp`**): Set the queue position to `track_number`, without commencing playback. Note that this will stop any current audio being played.
The following has issues and requires further development. For example, it's currently possible to add radio stations to the queue!
- **`add_favourite_to_queue <favourite> [<position>]` (or `add_favorite_to_queue`, `add_fav_to_queue`, `afq`)**: Add a Sonos Favourite to the queue. Optionally, `play_next` or `next` can be added to add the favourite as the next track or playlist to be played. Returns the queue position of the favourite.
### Favourites and Playlists
- **`clear_playlist <playlist>`**: Clear the Sonos playlist named `<playlist>`.
- **`create_playlist <playlist>`**: Create a Sonos playlist named `<playlist>`. (See also `save_queue` above).
- **`cue_favourite <favourite_name>`** (or **`cue_favorite`, `cue_fav`, `cf`**): Cues up a Sonos favourite for playback. This is a convenience action that issues the sequence: `mute, play_favourite, stop, unmute`. It's useful for silently setting a speaker to a state where it's ready to play the nominated favourite. Mute and group mute states are preserved.
- **`delete_playlist <playlist>`** (or **`remove_playlist`**): Delete the Sonos playlist named `<playlist>`.
- **`list_all_playlist_tracks`** (or **`lapt`**): Lists all tracks in all Sonos Playlists.
- **`list_favs`** (or **`list_favorites`, `list_favourites`, `lf`**): Lists all Sonos favourites.
- **`list_library_playlists`** (or **`llp`**): List all local library imported playlists.
- **`list_playlists`** (or **`playlists`, `lp`**): Lists the Sonos playlists.
- **`list_library_playlist_tracks <playlist_name>`** (or **`llpt`**): List the tracks in a local library imported playlist.
- **`list_playlist_tracks <playlist_name>`** (or **`lpt`**): List the tracks in a given Sonos Playlist.
- **`play_favourite <favourite_name>` (or `play_favorite`, `favourite`, `favorite`, `fav`, `pf`, `play_fav`)**: Plays the Sonos favourite identified by `<favourite_name>`. The name is loosely matched; if `<favourite_name>` is a (case insensitive) substring of a Sonos favourite, it will match. In the case of duplicates, the first match encountered will be used. If a queueable item, the favourite will be added to the end of the current queue and played. **Note: this currently works only for certain types of favourite: local library tracks and playlists, radio stations, single Spotify tracks, etc.**
- **`play_favourite_number <number>`** (or **`play_favorite_number`**, **`pfn`**): Play a Sonos favourite by its index number in the list of favourites.
- **`play_favourite_radio_station <station_name>`** (or **`play_favorite_radio_station`, `pfrs`**): Play a favourite radio station in TuneIn's 'My Stations' list.
- **`remove_from_playlist <playlist_name> <track_number>`** (or **`rfp`**): Remove a track from a Sonos playlist.
### TuneIn Radio Station Favourites
The following operate on the stations in TuneIn's 'My Radio Stations' list.
- **`cue_favourite_radio_station`** (or **`cue_favorite_radio_station`**, **`cfrs`**): Cue a favourite radio station for later playback. This is a convenience action that issues the sequence: `mute, play_favourite_radio_station, stop, unmute`. It's useful for silently setting a speaker to a state where it's ready to play the nominated favourite. Mute and group mute states are preserved.
- **`favourite_radio_stations`** (or **`favorite_radio_stations`**, **`lfrs`**, **`frs`**): List the favourite radio stations.
- **`play_favourite_radio_station <station_name>`** (or **`play_favorite_radio_station`, `pfrs`**): Play a favourite radio station.
- **`play_fav_radio_station_no <station_number>`** (or **`pfrsn`**): Play a favourite radio station by its number.
### Grouping, Stereo Pairing, and Surround (Satellite) Speakers
- **`add_satellite_speakers <left_rear_speaker> <right_rear_speaker>`** (or **`add_satellites`**): Bonds `<left_rear_speaker>` and `<right_rear_speaker>` as rear satellite speakers to the target soundbar. The target speaker must be a soundbar. Example: `sonos "Arc" add_satellites "Era 100 L" "Era 100 R"`.
- **`group <master_speaker>`(or `g`)**: Groups the speaker with `<master_speaker>`, which acts as the coordinator.
- **`multi_group <slave_speaker> [<slave_speaker> ...]`**: Groups one or more speakers with the target speaker, which acts as the coordinator.
- **`pair <right_hand_speaker>`**: Creates a stereo pair, where the target speaker becomes the left-hand speaker of the pair and `<right_hand_speaker>` becomes the right-hand of the pair. Can be used to pair dissimilar Sonos devices (e.g., to stereo-pair a Play:1 with a One). The left-hand speaker becomes the coordinator speaker, and the stereo pair will adopt its name.
- **`party_mode` (or `party`)**: Adds all speakers in the system into a single group. The target speaker becomes the group coordinator. Remove speakers individually using `ungroup`, or use `ungroup_all`.
- **`separate_satellite_speakers`** (or **`separate_satellites`**): Removes all bonded satellite speakers from the target soundbar. The target speaker must be a soundbar. Note: this will reset the Trueplay tuning for the device.
- **`transfer_playback <target_speaker>` (or `transfer_to`, `transfer`)**: Transfers playback to <target_speaker>. This is achieved by grouping and ungrouping the speakers, and swapping the group coordinator. It's a convenience shortcut for `speaker1 group speaker2 : speaker1 ungroup`.
- **`ungroup` (or `ug`, `u`)**: Removes the speaker from a group.
- **`ungroup_all`**: Removes all speakers in the target speaker's household from all groups.
- **`ungroup_all_in_group` (or `ugaig`)**: Ungroups all speakers in a group.
- **`unpair`**: Separate a stereo pair. Can be applied to either speaker in the pair.
### Alarms
Some SoCo-CLI alarm actions below require an **alarm specification** (`alarm_spec`), a comma-separated list of exactly **eight** parameters (without spaces next to the commas), which defines all the properties of an alarm. The parameters are as follows:
1. Alarm start time, in hours and minutes using the 24hr clock: HH:MM
2. Alarm duration in hours and minutes: HH:MM
3. Recurrence: A valid recurrence string is `DAILY`, `ONCE`, `WEEKDAYS`,
`WEEKENDS` or of the form `ON_DDDDDD` where `D` is a number from 0-6
representing a day of the week (Sunday is 0, Monday is 1, etc.), e.g., `ON_034` means
Sunday, Wednesday and Thursday
4. Whether the alarm is enabled: `ON` or `OFF` (or `YES`, `NO`)
5. What to play: `CHIME` (or `chime`) for the standard Sonos alarm sound, or a choice from your Sonos Favourites. Sonos Favourite matching will use case-insensitive, partial matches.
6. Play mode: one of `NORMAL`, `SHUFFLE_NOREPEAT`, `SHUFFLE`, `REPEAT_ALL`, `REPEAT_ONE`, `SHUFFLE_REPEAT_ONE` (note that `SHUFFLE` means SHUFFLE *and* REPEAT)
7. The volume to play at: `0`-`100`
8. Whether to include grouped speakers: `ON` or `OFF` (or `YES`, `NO`)
Examples of alarm specifications:
- `07:00,01:30,WEEKDAYS,ON,"Radio 4",NORMAL,50,OFF`
- `06:30,00:01,WEEKDAYS,ON,CHIME,NORMAL,50,OFF`
In actions which **modify** (or copy and modify) an existing alarm, values that are to be left unchanged are denoted by an underscore in the `alarm_spec`. E.g., to change only the duration and volume of an alarm, use an alarm spec such as: `_,01:00,_,_,_,_,60,_`.
The **alarm actions** are as follows:
- **`alarms`** (or **`list_alarms`**): List all of the alarms in the Sonos system. Each alarm has a unique integer `alarm_id` that can be used in the other alarm actions.
- **`alarms_spec`**: List all alarms in `alarm_spec` format, making it easy to copy alarm specs directly into `modify_alarm` or `add_alarm` commands. Specs containing spaces (e.g., in favourite names) are shown pre-quoted for shell use.
- **`alarms_spec_zone`**: As `alarms_spec`, but lists only the alarms for the target zone (speaker).
- **`alarms_zone`**: List the alarms for the target zone (speaker) only.
- **`copy_alarm <alarm_id>`**: Copies the alarm with ID `alarm_id` to the target speaker. Note that alarms cannot be copied back to the same speaker (instead, use `copy_modify_alarm` to do this).
- **`copy_modify_alarm <alarm_id> <alarm_spec>`**: Copies an existing alarm to the target speaker and modifies it according to an alarm specification. Can be used to copy an alarm on the same speaker, but in that case note that the copied alarm **must** have its start time modified.
- **`create_alarm <alarm_spec>`** (or **`add_alarm`**): Creates a new alarm for the target speaker, according to the `alarm_spec`.
- **`disable_alarm <alarm_id,[alarm_id]|all>`** (or **`disable_alarms`**): Disables an existing alarm or alarms. If multiple IDs are supplied, separate them by commas without spaces, e.g., `1,5,6`. To disable all alarms, use `all` as the `alarm_id`.
- **`enable_alarm <alarm_id,[alarm_id]|all>`** (or **`enable_alarms`**): Enables an existing alarm or alarms. If multiple IDs are supplied, separate them by commas without spaces, e.g., `1,5,6`. To enable all alarms, use `all` as the `alarm_id`.
- **`modify_alarm <alarm_id,[alarm_id]|all> <alarm_spec>`** (or **`modify_alarms`**): Modifies an existing alarm or alarms, according to the `alarm_spec` format described above (with underscores for values to be left unchanged). To modify all alarms, use `all` as the `alarm_id`.
- **`move_alarm <alarm_id>`**: Move the alarm with ID `alarm_id` to the target speaker.
- **`remove_alarm <alarm_id[,alarm_id]|all>`** (or **`remove_alarms`**): Removes one or more alarms by their alarm IDs. If multiple IDs are supplied, separate them by commas without spaces, e.g., `1,5,6`. To remove all alarms, use `all` as the `alarm_id`.
- **`snooze_alarm <snooze_duration>`**: Snooze an alarm that's already playing on the target speaker. Snooze duration can be `Nm` to snooze for `N` minutes (or just `N`). Alternatively, the duration can be expressed as `HH:MM:SS`. E.g., `sonos bedroom snooze_alarm 10m`, `sonos bedroom snooze_alarm 90`, or `sonos bedroom snooze_alarm 01:20:00`.
### Music Library Search Functions
The actions below search the Sonos Music library.
- **`list_albums`** (or **`albums`**): Lists all the albums in the music library.
- **`list_artists`** (or **`artists`**): Lists all the artists in the music library.
- **`last_search`** (or **`ls`**): Prints the results of the last album, track or artist search performed, or the last use of `tracks_in_album`, `list_albums`, or `list_playlist_tracks`. Use with `queue_search_results` to add specific items to the queue.
- **`search_albums <album_name> <strict>`** (or **`search_album`**, **`salb`**): Searches the albums in your music library for a fuzzy match with `<album_name>`. Prints out the list of matching albums. Use `strict` to require an exact (case-insensitive) match.
- **`search_artists <artist_name> <strict>`** (or **`search_artist`**, **`sart`**): Searches the artists in your music library for a fuzzy match with `<artist_name>`. Prints out the list of albums featuring any artists that match the search. Use `strict` to require an exact (case-insensitive) match.
- **`search_library <name> <strict>`** (or **`sl`**): Searches the titles in your music library for a fuzzy match with `<name>` against artists, albums and tracks. Prints out the lists of matches. This action is a superset of `search_artists`, `search_albums`, and `search_tracks`, i.e., it searches across all categories. Note: only the last populated search is saved.
- **`search_tracks <track_name> <strict>`** (or **`search_track`**, **`st`**): Searches the tracks in your music library for a fuzzy match with `<track_name>`. Prints out the list of matching tracks. Use `strict` to require an exact (case-insensitive) match.
- **`tracks_in_album <album_name> <strict>`** (or **`tia`**, **`lta`**): Searches the albums in your music library for a fuzzy match with `<album_name>`. Prints out the list of tracks in each matching album. Use `strict` to require an exact (case-insensitive) match.
### Speaker and Sonos System Information
- **`audio_format`**: Soundbars only: report the audio format currently being played.
- **`available_actions`**: List the currently available speaker actions (play, pause, seek, next, etc.).
- **`battery`**: Shows the battery status for Sonos speakers that contain batteries.
- **`buttons`**: Returns whether the speaker's control buttons are enabled, 'on' or 'off'.
- **`buttons <on|off>`**: Sets whether the speaker's control buttons are on or off.
- **`groups`**: Lists all groups in the Sonos system. Also includes single speakers as groups of one, and paired/bonded sets as groups.
- **`groupstatus`**: Indicates whether the speaker is part of a group, and whether it's part of a stereo pair or bonded home theatre configuration. Note that first can override the second: if a paired/bonded coordinator speaker is also part of a group, the group will be reported but not the paired/bonded status.
- **`has_satellites`**: Returns `yes` if the zone/room has satellite (surround) speakers bonded, otherwise `no`.
- **`has_subwoofer`**: Returns `yes` if the zone/room has a subwoofer bonded, otherwise `no`.
- **`info`**: Provides detailed information on the speaker's settings, current state, software version, IP address, etc.
- **`is_indexing`**: Reports on whether the system is currently in the process of reindexing its local libraries: possible responses are `yes` or `no`.
- **`is_satellite`**: Returns `yes` if the target device is a satellite (surround) speaker, otherwise `no`.
- **`is_subwoofer`**: Returns `yes` if the target device is a subwoofer, otherwise `no`.
- **`libraries`** (or **`shares`**): List the local music library shares.
- **`mic_enabled`**: Reports whether a voice-enabled speaker's microphone is active. Returns `True` or `False`. If the speaker does not have a microphone, or if voice services are not enabled, an error will be reported. (Note that the microphone state is not under software control, so this action can only be used to inspect its state, not to change it.)
- **`reindex`**: Start a reindex of the local music libraries. Will not proceed if a reindex is already underway.
- **`reboot_count`**: Returns the number of times a speaker has been rebooted.
- **`rename <new_name>`**: Rename the speaker.
- **`state`** (or **`status`, `playback`**): Returns the current playback state for the speaker, one of: `PAUSED_PLAYBACK`, `PLAYING`, `STOPPED`, or `TRANSITIONING`.
- **`status_light` (or `light`)**: Returns the state of the speaker's status light, 'on' or 'off'.
- **`status_light <on|off>` (or `light`)**: Switch the speaker's status light on or off.
- **`sysinfo`**: Prints a table of information about all speakers in the system.
- **`zones` (or `visible_zones`, `rooms`, `visible_rooms`)**: Prints a simple list of comma separated visible zone/room names, each in double quotes. Use **`all_zones` (or `all_rooms`)** to return all devices including ones not visible in the Sonos controller apps.
## Multiple Sequential Commands
### Chaining Commands Using the `:` Separator
Multiple commands can be run as part of the same `sonos` invocation by using the `:` separator to add multiple `SPEAKER ACTION <parameters>` sequences to the command line. **The `:` separator must be surrounded by spaces** to disambiguate from other uses of `:` in sonos actions.
The benefit of using this approach instead of multiple separate `sonos` commands is that the cost of starting the program is only incurred once. In addition, it allows for the introduction of wait states and loops.
An arbitrary number of commands can be supplied as part of a single `sonos` invocation. If a failure is encountered with any command, `sonos` will report the error, but will generally attempt to execute subsequent commands.
**Example:** `sonos Kitchen volume 25 : Kitchen play`
### Inserting Delays: `wait` and `wait_until`
```
sonos wait <duration>
sonos wait_until <time>
```
The **`wait <duration>`** (or **`wait_for`**) action waits for the specified duration before moving on to the next command. Do not supply a speaker name. This action is useful when, for example, one wants to play audio for a specific period of time, or maintain a speaker grouping for a specific period then ungroup, etc.
`<duration>` can be **one** of seconds, minutes or hours. Floating point values for the duration are acceptable. Examples: `wait 10s`, `wait 30m`, `wait 1.5h`. (If the s/m/h is omitted, `s` (seconds) is assumed.) The time duration formats HH:MM and HH:MM:SS can also be used. Examples are `wait 2:30` (waits 2hrs and 30mins), `wait 0:1:25` (waits 1min 25secs).
The **`wait_until <time>`** action pauses sonos command line execution until the specified time, in 24hr HH:MM or HH:MM:SS format, for example `wait_until 16:30`.
Examples:
- `sonos Bedroom group Study : Study group_volume 50 : Study play : wait 10m : Study stop : Study ungroup`
- `sonos Kitchen play_favourite Jazz24 : wait 30m : Kitchen stop`
- `sonos Bedroom volume 0 : Bedroom play_favourite "Radio 4" : Bedroom ramp 40 : wait 1h : Bedroom ramp 0 : Bedroom stop`
### Waiting Until Playback has Started/Stopped: `wait_start`, `wait_stop` and `wait_end_track`
```
sonos <speaker> wait_start
sonos <speaker> wait_stop
sonos <speaker> wait_stop_not_pause
sonos <speaker> wait_end_track
```
The **`<speaker> wait_start`** and **`<speaker> wait_stop`** actions are used to pause execution of the sequence of `sonos` commands until a speaker has either started or stopped/paused playback. The **`wait_stop_not_pause`** (or **`wsnp`**) action is the same as `wait_stop` but ignores the 'paused' state, i.e., it only resumes when a speaker enters the stopped state.
For example, to reset the volume back to `25` only after the `Bedroom` speaker has stopped playing, use the following command sequence:
`sonos Bedroom wait_stop : Bedroom volume 25`
Note that if a speaker is already playing, `wait_start` will proceed immediately, and if a speaker is already stopped, `wait_stop` will proceed immediately. If the behaviour you want is to continue **after** the **next** piece of audio ends, then you can chain commands as shown in the following example:
`sonos <speaker> wait_start : <speaker> wait_stop : <speaker> vol 50`
The **`wait_end_track`** action will pause execution of `sonos` commands until the current track has ended, or until playback has otherwise paused or stopped. This is useful, for example, when one want to stop playback after the current track has ended:
`sonos <speaker> wait_end_track : <speaker> stop`
### The `wait_stopped_for <duration>` Action
```
sonos <speaker> wait_stopped_for <duration>
sonos <speaker> wait_stopped_for_not_pause <duration>
```
The **`<speaker> wait_stopped_for <duration>`** (or **`wsf`**) action will wait until a speaker has stopped playback for `<duration>` (which uses the same time parameter formats as the `wait` action). If the speaker stops playback, but then restarts (any number of times) during `<duration>`, the timer will be reset to zero each time. Processing continues once the speaker has been stopped for a continuous period equalling the `<duration>`.
The **`<speaker> wait_stopped_for_not_pause <duration>`** (or **`wsfnp`**) action is the same, but ignores the 'paused' state.
This function is useful if one wants to perform an action on a speaker (such as ungrouping it) only once its use has definitely stopped, as opposed to it just being temporarily paused, or stopped while switched to a different audio source. For example:
```
sonos Study wait_stopped_for 5m : Study line_in on : Study play
```
### Repeating Commands: The `loop` Actions
```
loop
loop <iterations>
loop_for <duration>
loop_until <time>
loop_to_start
```
The **`loop`** action loops back to the beginning of a sequence of commands and executes the sequence again. Do not supply a speaker name. In the absence of errors, `loop` will continue indefinitely until manually stopped.
To loop a specific number of times, use **`loop <iterations>`**, giving an integer number of iterations to perform before command processing continues. The number of iterations includes the one just performed, i.e., in the sequence `sonos <speaker> vol 25: wait 1h : loop 2`, the commands preceding the `loop 2` action will be performed twice in total.
To loop for a specific period of time, use **`loop_for <duration>`**, where the format for `<duration>` follows the same rules as `wait`. Note that timer starts from the point when the `loop` statement is reached, not from the overall start of command execution.
To loop until a specific time, use **`loop_until <time>`**, where the format for `<time>` follows the same rules as `wait_until`.
Multiple `loop` statements can be used in `sonos` command sequence. For any given `loop` statement, command execution returns to the command immediately after the most recent `loop`, i.e., the loop executes the commands between the current `loop` action and the previous one. Note that `loop 1` can be considered a null loop action, and can be useful in restricting the scope of a subsequent `loop` action.
The **`loop_to_start`** action will loop back to the very start of a command sequence. It takes no parameters.
Examples:
```
sonos Study wait_start : Study wait_stopped_for 10m : Study volume 25 : loop
sonos wait_until 22:00 : Bedroom play_fav "Radio 4" : Bedroom sleep 30m : loop 3
sonos Bedroom play_fav Jazz24 : Bedroom sleep 30m : wait 1h : loop_for 3h
sonos wait_until 08:00 : Kitchen play_fav "World Service" : Kitchen sleep 10m : wait 1h : loop_until 12:01
```
## Conditional Command Execution
The following modifiers are available that will invoke or suppress an action depending on the state of the target speaker:
```
sonos <speaker> if_stopped <action> <parameters>
sonos <speaker> if_playing <action> <parameters>
sonos <speaker> if_coordinator <action> <parameters>
sonos <speaker> if_not_coordinator <action> <parameters>
sonos <speaker> if_queue <action> <parameters>
sonos <speaker> if_no_queue <action> <parameters>
```
The `if_stopped` modifier will execute the action that follows it only if the speaker is not currently playing. If the speaker is playing, the action will be skipped, and the next command in the sequence (if applicable) will be executed immediately. For example, to set the volume of a speaker back to a default value only if the speaker is not playing, use:
`sonos <speaker> if_stopped volume 25`
No action will be taken if the speaker is playing, and the command will terminate immediately.
Similarly, the `if_playing` modifier will execute the action that follows it only if the speaker is currently playing.
The `if_coordinator` modifier will execute the action that follows only if the target speaker is a coordinator. The `if_not_coordinator` modifier will execute the action that follows only if the target speaker is not a coordinator. Note that many actions are automatically redirected to the coordinator speaker, so this modifier may not be required, depending on your use case.
The `if_queue` modifier will execute the action that follows it only if the speaker's queue has one or more items in it. Similarly, the `if_no_queue` modifier will execute the following action only if the speaker's queue is empty.
Modifiers can be combined and will be evaluated in left to right sequence. All modifiers must be true for the action to be invoked. E.g.:
`sonos <speaker> if_no_queue if_stopped <action> <parameters>`
## Interactive Shell Mode
```
sonos -i <speaker_name>
sonos --interactive <speaker_name>
sonos -i
```
### Description
Interactive shell mode creates a SoCo-CLI command line session for entering `sonos` commands. Compared to using individual `sonos` invocations, using the shell is faster to perform operations, and requires less typing.
Most `sonos` actions are accepted. Multiple actions can be submitted on a single command line using the ` : ` separator. Command **aliases** can be created for commonly used actions and sequences of actions. A **single-keystroke** mode allows action invocations using one touch of the keyboard.
### Usage
Interactive mode is started with the `-i` or `--interactive` command line option. Optionally, a speaker name can be given, in which case all commands will be directed to that speaker (until changed in the shell).
Type `help` or `?` at the sonos command line for more information on using interactive shell mode:
```
$ sonos -i
Entering SoCo-CLI interactive shell.
Type 'help' for available shell commands.
Sonos [] > help
This is SoCo-CLI interactive mode. Interactive commands are as follows:
'1', ... : Set the active speaker. Use the numbers shown by the
'speakers' command. E.g., to set to speaker number 4
in the list, just type '4'.
'0' will unset the active speaker.
'actions' : Show the complete list of SoCo-CLI actions.
'alias' : Add an alias: alias <alias_name> <actions>
Remove an alias: alias <alias_name>
Update an alias by creating a new alias with the same name.
Using 'alias' without parameters shows the current list of
aliases.
Aliases override existing actions and can contain
sequences of actions.
'cd' : Change the working directory of the shell, e.g. 'cd ..'.
Note that on Windows, backslashes must be doubled, e.g.:
'cd C:\\'
'check-for-update'
: Check whether an update is available
'docs' : Print a link to the online documentation.
'exec' : Run a shell command, e.g.: 'exec ls -l'.
'exit' : Exit the shell.
'help' : Show this help message (available shell commands).
'pop' : Restore saved active speaker state.
'push' : Save the current active speaker, and unset the active
speaker.
'rescan' : If your speaker doesn't appear in the 'speakers' list,
use this to perform a more comprehensive scan.
'rescan_max' : Try this if you're still having trouble finding all your
speakers.
'set <spkr>' : Set the active speaker using its name.
Use quotes when needed for the speaker name, e.g.,
'set "Front Reception"'. Unambiguous, partial,
case-insensitive matches are supported, e.g., 'set front'.
To unset the active speaker, omit the speaker name,
or just enter '0'.
'sk' : Enters 'single keystroke' mode. (Also 'single-keystroke'.)
'speakers' : List the names of all available speakers.
'version' : Print the versions of SoCo-CLI, SoCo, and Python in use.
The action syntax is the same as when using 'sonos' from the command line.
If a speaker has been set in the shell, omit the speaker name from the
action.
Use the arrow keys for command history and command editing.
[Not Available on Windows] Use the TAB key for autocompletion of shell
commands, SoCo-CLI actions, aliases, and speaker names.
Sonos [] >
```
### Shell History and Auto-Completion
Commands in the shell history can be scrolled through by using the up/down arrows, and commands can be edited using the left/right arrows to position the cursor.
(*Not available on Windows*) Shell commands can be auto-completed using the TAB key. The shell history is saved between shell sessions in `~/.soco-cli/shell-history.txt`.
### Shell Aliases
Shell aliases allow the creation of shortcuts for individual actions or sequences of actions. Aliases are created using:
`> alias <alias_name> <alias_action> [ : <alias_action>]`
For example, to **create** an alias action sequence `go` that sets the volume of the active speaker to '50', starts playback, and then shows the track name, use:
`> alias go volume 50 : play : track`
Aliases are **run** by using the alias name, e.g.: `> go`.
Aliases are included in **autocompletion** results, and they **override** built-in commands and actions of the same name (so they can be used to remap commands and actions).
- **List** your aliases using the command `alias` without parameters.
- **Remove** an alias by using `alias <alias_name>` without additional parameters.
- **Update** an alias by creating a new action sequence with the same alias name (the previous alias is overwritten).
Aliases are **saved** between sessions, using a file in the `~/.soco-cli` directory.
#### Push and Pop
The **`push`** and **`pop`** commands are useful in alias actions when one wants to target actions at other speakers, but keep the current active speaker when the action sequence ends. `push` saves the current speaker and unsets it, and `pop` reselects the saved speaker, e.g.:
`> alias fv push : set "Front Reception" : volume 50 : pfrs "Jazz 24" : pop`
This command sequence targets the 'Front Reception' speaker, but first saves the current active speaker, restoring it at the end. (This will, of course, still work if the current active speaker is 'Front Reception'.)
#### Alias Subroutines
Aliases can include other aliases in their sequences of actions, e.g.:
```
> alias alias_1 vol 30 : play
> alias alias_2 push : set Kitchen : alias_1 : pop
> alias_2
```
Alias subroutines can be nested to an arbitrary depth. **Loops** are detected and prevented when an alias with a loop is invoked.
#### Alias Arguments
Aliases accept arguments when invoked, which is helpful in remapping existing actions to new alias names. Arguments are specified positionally using `%1`, `%2`, etc. For example:
```
> alias a1 pfq %1
> a1 5 <- Invokes 'pfq 5'
>
> alias a2 push : Kitchen volume %1 : Bathroom volume %1 : pop
>
> a2 30 <- Invokes 'Kitchen volume 30 : Bathroom volume 30'
(surrounded by a push/pop to save the current target
speaker).
```
If positional arguments are not specified, values will not be passed through. Unsatisfied positional arguments are ignored. For example:
```
> alias a1 vol %1 %2
>
> a1 <- Invokes 'vol'
> a1 50 <- Invokes 'vol 50'
> a1 50 50 <- Invokes 'vol 50 50' (and generates an error).
```
Positional arguments can be used multiple times within an action (unlikely to be useful) or within a sequence of actions.
#### Saving and Loading Aliases
```
sonos --save-aliases <filename>
sonos --load-aliases <filename>
sonos --overwrite-aliases <filename>
```
Aliases can be exported to, and loaded from, plain text files using the command line options above. The command will terminate once the file operation is complete. Option `save-aliases` will export the current aliases to the supplied filename; `load-aliases` will load a list of aliases and merge them with the current list (overwriting any duplicate alias names); `overwrite-aliases` will overwrite all current aliases with the list from the file.
The alias file format consists of lines containing `<alias_name> = <alias actions>`, e.g:
```
# This is a comment line
my_alias = vol 30 : play_fav "Radio 4"
p = pauseplay
# Blank lines are OK
m = mute on
```
### Single Keystroke Mode
Single keystroke mode allows the shell to be controlled by single character presses on the keyboard, without requiring the return key to be pressed. This is useful for some headless automation use cases, as well as sometimes being convenient for interactive use. All single character actions are available, including aliases.
Enable by using the action `sk` or `single-keystroke` at the shell prompt. Type `x` to exit back to the normal shell.
To start SoCo-CLI in single keystroke mode, use the command line option `--sk`, along with the interactive (`-i` or `--interactive`) option.
## Cached Discovery
SoCo-CLI uses the full range of speaker discovery mechanisms in SoCo to look up speakers by their names to determine their IP addresses.
First, the native Sonos SSDP multicast discovery process is tried.
If this fails, SoCo-CLI will try scanning every IP address on your local network(s) to find the speaker; it's likely to be doing this some of the time if your network contains multiple Sonos systems (multiple 'households'), or if the network has problems with multicast forwarding. This can be slower than is desirable, so SoCo-CLI also provides an alternative process that scans the complete local network for Sonos devices as a one-off process, and then caches the results in a local file for use in future operations.
It's often faster and more convenient to use the local cached speaker list. The disadvantage of using the cached discovery mechanism is that the speaker list can become stale due to speakers being added/removed/renamed, or IP addresses having changed, meaning the cached list must be refreshed. The `sonos-discover` command, discussed below, is a convenient way of doing this.
### Usage
To use the cached discovery mechanism with `sonos`, use the `--use-local-speaker-list` or `-l` flag. The first time this flag is used, the discovery process will be initiated. This will take a few seconds to complete, after which the `sonos` command will execute. A local speaker list is stored in `<your_home_directory>/.soco-cli/` for use with future invocations of the `sonos` command.
**Example:** `sonos -l "living room" volume 50` uses the local speaker database to look up the "living room" speaker.
When executing a sequence of commands, supply the `-l` option only for the first speaker and it will be used for all speaker lookups, e.g.:
`sonos -l kitchen wait_stop : kitchen vol 25 : study play_favourite "Radio 4"`
### Speaker Naming
Speaker naming does not need to be exact. Matching is case-insensitive, and works on substrings. For example, if you have a speaker named `Front Reception`, then `"front reception"` or just `front` will match, as will any unambiguous substring. If an ambiguously matchable name is supplied then an error will be returned.
Note that if you have speakers with the same names in multiple Sonos systems (Households), SoCo-CLI will fail because it cannot disambiguate the speakers. Speakers should have unique names within a network (or fall back on using IP addresses instead of speaker names).
### Refreshing the Local Speaker List
If your speakers change in some way (e.g., they are renamed, are assigned different IP addresses, or you add/remove speakers), you can refresh the discovery cache using the `--refresh-local-speaker-list` or `-r` option. Note that this option only has an effect when combined with the `-l` option. You can also use the `sonos-discover` command (below).
**Example:** `sonos -lr "living room" volume 50` will refresh the discovery cache before executing the `sonos` command.
### Discovery Options
The following flags can be used to adjust network discovery behaviour if the discovery process is failing:
- **`--network-discovery-threads, -t`**: The number of parallel threads used to scan the local network.
- **`--network-discovery-timeout, -n`**: The timeout used when scanning each host on the local network (how long to wait for a socket connection on port 1400 before giving up).
These options only have an effect when combined with the `-l` **and** `-r` options.
**Example:** `sonos -lr -t 256 -n 1.0 "living room" volume 50`
### The `sonos-discover` Command
**`sonos-discover`** is a separate command for creating/updating the local speaker cache, and for seeing the results of the discovery process. It's an alternative to using the `sonos -r` command. It accepts the same `-t`, `-n` and `-m` options as the `sonos` command.
**Example:** `sonos-discover -t 256 -n 1.0 -m 24` will run `sonos-discover` with a maximum of 256 threads, a network timeout of 1.0s, a minimum netmask of 24 bits, and will print the result.
### Options for the `sonos-discover` Command
Without options, `sonos-discover` will execute the discovery process and print out its results. It will create a speaker cache file, or replace it if already present.
Discovery works by interrogating all network adapters on the device running SoCo-CLI, to build a list of the IP addresses to search for Sonos speakers. If your speakers reside on a subnet that is not directly attached (e.g., they're on a separate VLAN), then use the `--subnets` option to specify manually which networks to search.
**Options**:
- **`--print, -p`**: Print the the current contents of the speaker cache file
- **`--delete-local-speaker-cache, -d`**: Delete the local speaker cache file.
- **`--network-discovery-threads, -t`**: The maximum number of parallel threads used to scan the local network.
- **`--network-discovery-timeout, -n`**: The timeout used when scanning each host on the local network (how long to wait for a socket connection on port 1400 before giving up). Use this if `sonos-discover` is not finding all of your Sonos devices.
- **`--min-netmask, -m`**: The minimum netmask to use when scanning networks. Used to constrain the IP search space. (Note that this option will never **increase** the search space, e.g., if one of the attached networks is 192.168.0.0/24, supplying a `--min-netmask` value of 16 will not increase the search space to 192.168.0.0/16.)
- **`--version, -v`**: Print the versions of SoCo-CLI, SoCo, Python, and exit.
- **`--check-for-update`**: Check for a more recent version of SoCo-CLI.
- **`--docs`**: Print the URL of this README documentation, for the version of SoCo-CLI being used.
- **`--log <level>`**: Turn on logging. Available levels are NONE (default), CRITICAL, ERROR, WARN, INFO, DEBUG, in order of increasing verbosity.
- **`--subnets <subnets_list>`**: Specify which subnet(s) to search, as a comma separated list (without spaces). E.g.: `--subnets 192.168.0.0/24,192.168.1.0/24` or `--subnets 192.168.0.30`. When this option is used, only the specified subnet(s) will be searched, and the `--min-netmask` option (if supplied) is ignored.
## The SoCo-CLI HTTP API Server
(Note that this functionality requires Python 3.7 or above.)
```
sonos-http-api-server
soco-http-api-server
```
SoCo-CLI can be run as a simple HTTP API server, allowing most of its features to be accessed via HTTP requests. It's very simple to run the server and to construct HTTP requests that invoke it.
### Server Usage
The server is started using the `sonos-http-api-server` or `soco-http-api-server` commands:
```
% sonos-http-api-server
SoCo-CLI: Starting SoCo-CLI HTTP API Server v0.4.41
SoCo-CLI: Finding speakers ... ['Bedroom', 'Front Reception', 'Kitchen', 'Study']
SoCo-CLI: Macro: Attempting to (re)load macros from '/Users/pwt/macros.txt'
SoCo-CLI: Macro: Loaded macros:
...
INFO: Started server process [52137]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
```
By default, the server listens for incoming requests on **port 8000**. This can be changed using the **`--port (or -p)`** command line option (e.g., `sonos-http-api-server -p 51000`). The listen port must be available, otherwise the server will terminate with an error.
If accessing the HTTP server from another machine on the network, make sure that your firewall allows incoming TCP requests on your chosen port.
For each request, the server will report the equivalent `sonos` command line (if applicable), execute the sonos command, and report the exit code/error, followed by the URL requested and the HTTP response code, e.g.:
```
SoCo-CLI: Command = 'sonos Study volume', exit code = 0
INFO: 127.0.0.1:51017 - "GET /study/volume HTTP/1.1" 200 OK
SoCo-CLI: Command = 'sonos Study volume 30', exit code = 0
INFO: 127.0.0.1:64948 - "GET /Study/volume/30 HTTP/1.1" 200 OK
```
The server will continue running until stopped using CTRL-C (etc.).
### Using the Local Speaker Cache
To use the local speaker cache file instead of speaker discovery, start the HTTP API server with the `--use-local-speaker-list` or `-l` command line option.
When using this option, it's also possible to supply a `--subnets` command line specification, which will be used in the event of a `/rediscover` operation. The use of this option is described in the usage guide for `sonos-discover`.
The `--subnets` option **only** has an effect when using the local speaker cache, and the results of any `/rediscover` operations will overwrite the existing local speaker cache file.
Example of use with both options:
```
sonos-http-api-server -l --subnets=192.168.0.1/24
```
### HTTP Request Structure
All requests are simple HTTP `GET` requests. Request URLs have the form:
```
http://<server>:<port>/<speaker_name>/<action>[/parameter_1/parameter_2/parameter_3]
```
- **`<server>`** is the IP address or hostname of the server on which the HTTP API server is running.
- **`<port>`** is the TCP port on which the server is listening.
- **`<speaker_name>`** is the target speaker for the action. Use the same naming conventions as for other forms of SoCo-CLI usage; unambiguous partial names can be used, as can IP addresses.
- **`<action>`** is the SoCo-CLI action to perform. Almost all of the main SoCo-CLI actions are available for use.
- **`<parameter_1>`** (etc.) are the parameter(s) required by the action, if any.
Strings with characters that are invalid within URLs should be substituted by their URL-friendly replacements (e.g., space should be replaced by `%20`, comma by `%2C`, apostrophe by `%27`, and colon by `%3A`).
Usage examples:
```
http://192.168.0.100:8000/Study/volume
http://192.168.0.100:8000/Study/volume/50
http://192.168.0.100:8000/Front%20Reception/pause
http://192.168.0.100:8000/Kitchen/group/Hallway
http://192.168.0.100:8000/Kitchen/line_in/Lounge/right_input
```
### Return Values
Return values are supplied in JSON format, and always contain the same fields. A formatted example is shown below:
```
{
"speaker": "Study",
"action": "volume",
"args": [],
"exit_code": 0,
"result": "35",
"error_msg": ""
}
```
The **`speaker`**, **`action`**, and **`args`** fields confirm the data that was sent in the HTTP request, with the speaker's name replaced by its full Sonos name if a shortened version was used in the invocation URL.
The **`exit_code`** field is an integer. This will be zero if the command completed successfully, and non-zero otherwise. (Note that the HTTP response code will indicate success (200) even if the SoCo-CLI action fails, so inspect the `exit_code` for failure detection.)
If the command is successful, the **`result`** field contains the result string, which is exactly the string that would have been printed if the action had been performed on the command line.
If the command is unsuccessful, the **`error_msg`** field contains an error message describing the error.
### Asynchronous Actions (Experimental)
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 have the server respond immediately while the file is played in separate process.
This can be achieved by prefixing the action with `async_`. For example:
```commandline
http://192.168.0.100:8000/Kitchen/async_play_file/my_file.mp3
```
Note that the data returned in this case is probably not useful: it will simply indicate whether the background command was invoked successfully, not whether the command was successful.
Async actions are mutually exclusive for a given speaker: any running async action will be cancelled if a new async action is invoked.
The `async_` prefix also works with macros. See [Asynchronous Macros](#asynchronous-macros) below.
### Macros: Defining Custom HTTP API Server Actions
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.
#### Macro Definition and Usage
Macro definitions take the form of the macro name followed by an equals sign (`=`), then the action(s) to be performed. Comments can be included by using `#` as the first character of a line, and blank lines are ignored. For example, the contents of a macro definition file might be:
```
# SoCo-CLI HTTP API Server Macros file
# Format is:
# macro_name = speaker <action> <parameters> [: speaker <action> <parameters> ...]
# Play the doorbell sound on all speakers
doorbell = Hallway party_mode : Hallway play_file doorbell.mp3 : Hallway ungroup_all
# Group speakers in the morning, and start a favourite radio station
morning = Bathroom group Bedroom : Kitchen group Bedroom : Bedroom play_favourite "Radio 4"
# Set the volume and start playback of a favourite
front_R3 = "Front Reception" volume 50 : "Front Reception" play_favourite "Radio 3"
```
The macros above would be invoked using URLs of the form:
```
http://192.168.0.100:8000/macro/doorbell
http://192.168.0.100:8000/macro/morning
http://192.168.0.100:8000/macro/front_R3
```
**Macro names** are case-sensitive, and should not contain spaces or special characters except for underscores (`_`) and dashes (`-`).
**Speaker names** should ideally use the **exact** speaker name, including capitalisation, and using enclosing quotes where necessary. Shortened names will work, but will be less efficient. (Note: this does not apply when using the local speaker cache option.)
#### Macro Arguments
Macros can be parameterised using up to **twelve** positional arguments, specified in macro definitions by the terms **`%1`** to **`%12`**. The general form for supplying the arguments when the macro is invoked is:
`http://192.168.0.100:8000/macro/<macro_name>/<arg_1>/<arg_2>/<arg_3>/<arg_4>/...` etc.
For example, a macro definition to set all the speakers on one floor to a specified volume could be defined as:
`lower_floor_volume = Kitchen volume %1 : Hallway volume %1 : "Living Room" volume %1`
The macro is then invoked using:
`http://192.168.0.100:8000/macro/lower_floor_volume/30`
Or to use different volumes for each speaker, the macro definition might be:
`lower_floor_volume = Kitchen volume %1 : Hallway volume %2 : "Living Room" volume %3`
and the macro invocation would take the form:
`http://192.168.0.100:8000/macro/lower_floor_volume/30/40/25`
If a macro argument needs to be supplied, but should be ignored during macro processing, then use an underscore `_` as the argument to be ignored. E.g. to ignore `%2` when processing a macro, use a URL of the form:
`http://192.168.0.100:8000/macro/lower_floor_volume/30/_/25`
#### Using the Generic Macro
There's a built-in *generic* macro called `__` that simply maps to `%1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11 %12`. This can be used to create arbitrary command sequences. For example, to run the equivalent of:
```shell
sonos diner volume 30 : diner play_favourite "radio 4"
```
use the following HTTP request (replacing the `:` command sequence separator with `%3A`):
```shell
http://192.168.0.100:8000/macro/__/diner/volume/30/%3A/diner/play_favourite/radio%204
```
#### Troubleshooting
There is comprehensive server-side logging that reports the macro being invoked, the arguments supplied, the substitutions performed, and the `sonos` command line that is assembled and executed. This is helpful for troubleshooting. E.g., processing the following URL:
`http://192.168.0.100:8000/macro/test_1/Study/volume/_/Peter%27s%20Room/volume`
might generate the following server-side output:
```
SoCo-CLI: Macro: Processing macro 'test_1' = '%1 %2 %3 : %4 %5'
SoCo-CLI: Macro: Parameter variables supplied: ['Study', 'volume', '_', "Peter's Room", 'volume']
SoCo-CLI: Macro: Parameter variables used: ['%1', '%2', '%4', '%5'] -> ['Study', 'volume', '"Peter\'s Room"', 'volume']
SoCo-CLI: Macro: Parameter variables ignored or not supplied for: ['%3']
SoCo-CLI: Macro: Parameter variables supplied but ignored or not used: ['%3'] -> ['_']
SoCo-CLI: Macro: Substituting speaker name 'Study' by IP address '192.168.0.39'
SoCo-CLI: Macro: Substituting speaker name 'Peter's Room' by IP address '192.168.0.42'
SoCo-CLI: Macro: Executing: 'sonos 192.168.0.39 volume : 192.168.0.42 volume' in a subprocess
SoCo-CLI: Macro: Exit code = 0
INFO: 192.168.0.100:61548 - "GET /macro/test_1/Study/volume/_/Peter%27s%20Room/volume HTTP/1.1" 200 OK
```
#### Specifying the Macro Definition File
By default, the HTTP API server will look for a file named `macros.txt` in the directory from which it's invoked (the presence of the file is optional). If instead you wish to use a specific macros file, use the `--macros` or `-m` option when starting the server, followed by the name of the macros file, e.g.:
```
sonos-http-api-server --macros my_macros.txt
```
#### Reloading the Macro Definition File
The macro file can be reloaded using the `/macros/reload` endpoint (e.g.: `http://192.168.0.100:8000/macros/reload`). This will reload the macros from the original macros file, and overwrite any that are already installed. The new list of macros will be returned in JSON format.
#### Return Values
Successful invocation of a macro will return the sonos command that was executed, and the result(s) of the actions that were performed (or the error output(s) in the case of a failure), in JSON format, e.g.:
```
{"command": "sonos Kitchen volume", "result": "30"}
```
#### Listing Macros
The `macros/list` endpoint (e.g.: `http://192.168.0.100:8000/macros/list`) will return a JSON list of the macros installed in the server.
#### Asynchronous Macros
Macros can be run asynchronously by prefixing the macro name with `async_` in the URL. The server responds immediately while the macro continues to run in the background. For example:
```
http://192.168.0.100:8000/macro/async_doorbell
http://192.168.0.100:8000/macro/async_lower_floor_volume/30
```
As with asynchronous actions, the data returned is not the macro's output — it will simply indicate whether the background process was successfully invoked. Asynchronous macros are mutually exclusive per macro name: if an async macro with the same name is already running, it will be cancelled before the new one is started.
### Listing Speakers
To list the available speakers, use the `/speakers` endpoint, e.g.: `http://192.168.0.100:8000/speakers`. The returned JSON string will list the speakers.
### Rediscovering Speakers
If the configuration of your speakers changes in some way (e.g., if speakers are renamed or if there are IP address changes), the server can be instructed to reload its speaker data using the `/rediscover` endpoint, e.g.: `http://192.168.0.100:8000/rediscover`. The returned JSON string will list the speakers discovered.
If using the local speaker cache option, the speaker cache file will be overwritten with the new discovery results.
### Inspecting the HTTP API
The HTTP API can be inspected and tested using its OpenAPI live documentation, at the `/docs` endpoint, e.g.:`http://192.168.0.100:8000/docs`
## Using SoCo-CLI as a Python Library
If you'd like to use SoCo-CLI as a high-level library in another Python program, it's simple to do so using its API capability. The goal is to provide the same added value, abstractions, and command structure as when using SoCo-CLI directly from the command line. Essentially, there is a single entry point that accepts exactly the same commands that would be used on the command line.
Using the SoCo-CLI API means that the expense of loading SoCo-CLI is incurred only once during the operation of your program, and speaker discovery results are cached for efficiency.
Note that the native SoCo library can be used alongside the SoCo-CLI API, as needed.
### Importing the API
Import SoCo-CLI in your Python code as follows:
```
from soco_cli import api
```
### Using the API
The API entry point is **`api.run_command(speaker_name, action, *args, use_local_speaker_list)`**, which takes exactly the same parameters as would be provided on the command line:
**Parameters:**
- **`speaker_name (str)`**: The speaker name or speaker IP address supplied as a string. Partial, case-insensitive names will be matched, but the match must be unambiguous. A `SoCo` object may be passed in place of the speaker name.
- **`action (str)`**: The action to perform, supplied as a string. Almost all of the SoCo-CLI actions are available for use, with the exception of the `loop` actions, the `wait_until` and `wait_for` actions, and the `track_follow` action.
- **`*args (tuple)`**: The arguments for the action, supplied as strings. There can be zero or more arguments, depending on the action.
- **`use_local_speaker_list (bool)`**: Whether to use the local speaker cache for speaker discovery. Optional, defaults to `False`.
**Return Values:**
Each `run_command()` invocation returns a three tuple consisting of `exit_code (int)`, `output_string (str)`, and `error_msg (str)`. If the exit code is `0`, the command completed successfully, and the command output (if any) is contained in the `output_string`. If the exit code is non-zero, the command did not complete successfully and `error_msg` will be populated while `output_string` will not.
The `output_string` return value contains exactly what would have been printed to the console if the command had been run from the command line.
The public API function definitions include type annotations, to enable type checking with the utility of your choice (e.g., mypy).
**Examples of use:**
```
exit_code, output, error = api.run_command("Kitchen", "volume")
exit_code, output, error = api.run_command("Study", "mute", "on")
exit_code, output, error = api.run_command("Study", "group", "Kitchen")
exit_code, output, error = api.run_command("Front Reception", "play_favourite", "Radio 6")
```
### Convenience Functions
There are some simple additional convenience functions provided by SoCo-CLI. The use of these functions is optional.
- **`api.set_log_level(log_level)`**: This function sets up Python logging for the whole program. `log_level` is a string which can take one of the following values: `None, Critical, Error, Warn, Info, Debug`. The default value is `None`.
- **`api.handle_sigint()`**: This function sets up a signal handler for SIGINT, providing a tidier exit than a stack trace in the event of a CTRL-C interrupt.
- **`api.get_soco_object(speaker_name, use_local_speaker_list=False)`**: Returns a two-tuple of the SoCo object for a given speaker name (or None), and an error message string. Uses the complete set of SoCo-CLI strategies for speaker discovery.
## Known Issues
Please report any problems you find using GitHub Issues [3].
## Uninstalling
1. Use the normal Pip approach to uninstall the SoCo-CLI package: `pip uninstall soco-cli`.
2. As usual, Pip will not remove dependencies. If you'd like to perform an exhaustive removal, inspect the `requirements.txt` files for `soco-cli`, and for `SoCo`. Take care not to remove packages that may also be required by other installed packages.
3. You may also need to remove the directory `.soco-cli` and its contents from your home directory.
## Acknowledgments
Developed with **[PyCharm](https://www.jetbrains.com/pycharm/)**. Earlier versions benefited from a free Professional licence for open source development from JetBrains.
All trademarks acknowledged. Avantrec Ltd has no connection with Sonos Inc.
## Resources
[1] https://github.com/SoCo/SoCo \
[2] https://pypi.org/project/soco-cli \
[3] https://github.com/avantrec/soco-cli/issues
================================================
FILE: RELEASING.txt
================================================
0. Start on branch 'next_version'; when changes are ready to be released:
Update the CHANGELOG (if required)
make format (if required)
make toc (if required)
git commit -a -m "Ready for release" (if required)
git checkout master
git merge next_version
1. Check __version__ in soco_cli/__init__.py
2. Commit: git commit -a -m "Version X.Y.Z"
3. Push: git push
4. Tag the commit: git tag -a vX.Y.Z -m "Version X.Y.Z"
5. Push the commit tags: git push --tags
6. PyPi push: make pypi_upload
7. git checkout next_version
git merge master
git push
Increment __version__ in __init__.py
Set up CHANGELOG.txt for next version
git commit -a -m "Set up for vX.Y.Z development"
start next set of changes
================================================
FILE: gh-md-toc
================================================
#!/usr/bin/env bash
#
# Steps:
#
# 1. Download corresponding html file for some README.md:
# curl -s $1
#
# 2. Discard rows where no substring 'user-content-' (github's markup):
# awk '/user-content-/ { ...
#
# 3.1 Get last number in each row like ' ... </span></a>sitemap.js</h1'.
# It's a level of the current header:
# substr($0, length($0), 1)
#
# 3.2 Get level from 3.1 and insert corresponding number of spaces before '*':
# sprintf("%*s", (level-1)*'"$nb_spaces"', "")
#
# 4. Find head's text and insert it inside "* [ ... ]":
# substr($0, match($0, /a>.*<\/h/)+2, RLENGTH-5)
#
# 5. Find anchor and insert it inside "(...)":
# substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8)
#
gh_toc_version="0.10.0"
gh_user_agent="gh-md-toc v$gh_toc_version"
#
# Download rendered into html README.md by its url.
#
#
gh_toc_load() {
local gh_url=$1
if type curl &>/dev/null; then
curl --user-agent "$gh_user_agent" -s "$gh_url"
elif type wget &>/dev/null; then
wget --user-agent="$gh_user_agent" -qO- "$gh_url"
else
echo "Please, install 'curl' or 'wget' and try again."
exit 1
fi
}
#
# Converts local md file into html by GitHub
#
# -> curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown
# <p>Hello world github/linguist#1 <strong>cool</strong>, and #1!</p>'"
gh_toc_md2html() {
local gh_file_md=$1
local skip_header=$2
URL=https://api.github.com/markdown/raw
if [ -n "$GH_TOC_TOKEN" ]; then
TOKEN=$GH_TOC_TOKEN
else
TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt"
if [ -f "$TOKEN_FILE" ]; then
TOKEN="$(cat "$TOKEN_FILE")"
fi
fi
if [ -n "${TOKEN}" ]; then
AUTHORIZATION="Authorization: token ${TOKEN}"
fi
local gh_tmp_file_md=$gh_file_md
if [ "$skip_header" = "yes" ]; then
if grep -Fxq "<!--te-->" "$gh_src"; then
# cut everything before the toc
gh_tmp_file_md=$gh_file_md~~
sed '1,/<!--te-->/d' "$gh_file_md" > "$gh_tmp_file_md"
fi
fi
# echo $URL 1>&2
OUTPUT=$(curl -s \
--user-agent "$gh_user_agent" \
--data-binary @"$gh_tmp_file_md" \
-H "Content-Type:text/plain" \
-H "$AUTHORIZATION" \
"$URL")
rm -f "${gh_file_md}~~"
if [ "$?" != "0" ]; then
echo "XXNetworkErrorXX"
fi
if [ "$(echo "${OUTPUT}" | awk '/API rate limit exceeded/')" != "" ]; then
echo "XXRateLimitXX"
else
echo "${OUTPUT}"
fi
}
#
# Is passed string url
#
gh_is_url() {
case $1 in
https* | http*)
echo "yes";;
*)
echo "no";;
esac
}
#
# TOC generator
#
gh_toc(){
local gh_src=$1
local gh_src_copy=$1
local gh_ttl_docs=$2
local need_replace=$3
local no_backup=$4
local no_footer=$5
local indent=$6
local skip_header=$7
if [ "$gh_src" = "" ]; then
echo "Please, enter URL or local path for a README.md"
exit 1
fi
# Show "TOC" string only if working with one document
if [ "$gh_ttl_docs" = "1" ]; then
echo "Table of Contents"
echo "================="
echo ""
gh_src_copy=""
fi
if [ "$(gh_is_url "$gh_src")" == "yes" ]; then
gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy" "$indent"
if [ "${PIPESTATUS[0]}" != "0" ]; then
echo "Could not load remote document."
echo "Please check your url or network connectivity"
exit 1
fi
if [ "$need_replace" = "yes" ]; then
echo
echo "!! '$gh_src' is not a local file"
echo "!! Can't insert the TOC into it."
echo
fi
else
local rawhtml
rawhtml=$(gh_toc_md2html "$gh_src" "$skip_header")
if [ "$rawhtml" == "XXNetworkErrorXX" ]; then
echo "Parsing local markdown file requires access to github API"
echo "Please make sure curl is installed and check your network connectivity"
exit 1
fi
if [ "$rawhtml" == "XXRateLimitXX" ]; then
echo "Parsing local markdown file requires access to github API"
echo "Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting"
TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt"
echo "or place GitHub auth token here: ${TOKEN_FILE}"
exit 1
fi
local toc
toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy" "$indent"`
echo "$toc"
if [ "$need_replace" = "yes" ]; then
if grep -Fxq "<!--ts-->" "$gh_src" && grep -Fxq "<!--te-->" "$gh_src"; then
echo "Found markers"
else
echo "You don't have <!--ts--> or <!--te--> in your file...exiting"
exit 1
fi
local ts="<\!--ts-->"
local te="<\!--te-->"
local dt
dt=$(date +'%F_%H%M%S')
local ext=".orig.${dt}"
local toc_path="${gh_src}.toc.${dt}"
local toc_createdby="<!-- Created by https://github.com/ekalinin/github-markdown-toc -->"
local toc_footer
toc_footer="<!-- Added by: `whoami`, at: `date` -->"
# http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html
# clear old TOC
sed -i"${ext}" "/${ts}/,/${te}/{//!d;}" "$gh_src"
# create toc file
echo "${toc}" > "${toc_path}"
if [ "${no_footer}" != "yes" ]; then
echo -e "\n${toc_createdby}\n${toc_footer}\n" >> "$toc_path"
fi
# insert toc file
if ! sed --version > /dev/null 2>&1; then
sed -i "" "/${ts}/r ${toc_path}" "$gh_src"
else
sed -i "/${ts}/r ${toc_path}" "$gh_src"
fi
echo
if [ "${no_backup}" = "yes" ]; then
rm "$toc_path" "$gh_src$ext"
fi
echo "!! TOC was added into: '$gh_src'"
if [ -z "${no_backup}" ]; then
echo "!! Origin version of the file: '${gh_src}${ext}'"
echo "!! TOC added into a separate file: '${toc_path}'"
fi
echo
fi
fi
}
#
# Grabber of the TOC from rendered html
#
# $1 - a source url of document.
# It's need if TOC is generated for multiple documents.
# $2 - number of spaces used to indent.
#
gh_toc_grab() {
href_regex="/href=\"[^\"]+?\"/"
common_awk_script='
modified_href = ""
split(href, chars, "")
for (i=1;i <= length(href); i++) {
c = chars[i]
res = ""
if (c == "+") {
res = " "
} else {
if (c == "%") {
res = "\\x"
} else {
res = c ""
}
}
modified_href = modified_href res
}
print sprintf("%*s", (level-1)*'"$2"', "") "* [" text "](" gh_url modified_href ")"
'
if [ "`uname -s`" == "OS/390" ]; then
grepcmd="pcregrep -o"
echoargs=""
awkscript='{
level = substr($0, 3, 1)
text = substr($0, match($0, /<\/span><\/a>[^<]*<\/h/)+11, RLENGTH-14)
href = substr($0, match($0, '$href_regex')+6, RLENGTH-7)
'"$common_awk_script"'
}'
else
grepcmd="grep -Eo"
echoargs="-e"
awkscript='{
level = substr($0, 3, 1)
text = substr($0, match($0, /">.*<\/h/)+2, RLENGTH-5)
href = substr($0, match($0, '$href_regex')+6, RLENGTH-7)
'"$common_awk_script"'
}'
fi
# if closed <h[1-6]> is on the new line, then move it on the prev line
# for example:
# was: The command <code>foo1</code>
# </h1>
# became: The command <code>foo1</code></h1>
sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' |
# Sometimes a line can start with <span>. Fix that.
sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<span/<span/g' |
# find strings that corresponds to template
$grepcmd '<h.*class="heading-element".*</a' |
# remove code tags
sed 's/<code>//g' | sed 's/<\/code>//g' |
# remove g-emoji
sed 's/<g-emoji[^>]*[^<]*<\/g-emoji> //g' |
# now all rows are like:
# <h1 class="heading-element">title</h1><a href="..."><span>..</span></a>
# format result line
# * $0 - whole string
# * last element of each row: "</hN" where N in (1,2,3,...)
echo $echoargs "$(awk -v "gh_url=$1" "$awkscript")"
}
# perl -lpE 's/(\[[^\]]*\]\()(.*?)(\))/my ($pre, $in, $post)=($1, $2, $3) ; $in =~ s{\+}{ }g; $in =~ s{%}{\\x}g; $pre.$in.$post/ems')"
#
# Returns filename only from full path or url
#
gh_toc_get_filename() {
echo "${1##*/}"
}
show_version() {
echo "$gh_toc_version"
echo
echo "os: `uname -s`"
echo "arch: `uname -m`"
echo "kernel: `uname -r`"
echo "shell: `$SHELL --version`"
echo
for tool in curl wget grep awk sed; do
printf "%-5s: " $tool
if type $tool &>/dev/null; then
$tool --version | head -n 1
else
echo "not installed"
fi
done
}
show_help() {
local app_name
app_name=$(basename "$0")
echo "GitHub TOC generator ($app_name): $gh_toc_version"
echo ""
echo "Usage:"
echo " $app_name [options] src [src] Create TOC for a README file (url or local path)"
echo " $app_name - Create TOC for markdown from STDIN"
echo " $app_name --help Show help"
echo " $app_name --version Show version"
echo ""
echo "Options:"
echo " --indent <NUM> Set indent size. Default: 3."
echo " --insert Insert new TOC into original file. For local files only. Default: false."
echo " See https://github.com/ekalinin/github-markdown-toc/issues/41 for details."
echo " --no-backup Remove backup file. Set --insert as well. Default: false."
echo " --hide-footer Do not write date & author of the last TOC update. Set --insert as well. Default: false."
echo " --skip-header Hide entry of the topmost headlines. Default: false."
echo " See https://github.com/ekalinin/github-markdown-toc/issues/125 for details."
echo ""
}
#
# Options handlers
#
gh_toc_app() {
local need_replace="no"
local indent=3
if [ "$1" = '--help' ] || [ $# -eq 0 ] ; then
show_help
return
fi
if [ "$1" = '--version' ]; then
show_version
return
fi
if [ "$1" = '--indent' ]; then
indent="$2"
shift 2
fi
if [ "$1" = "-" ]; then
if [ -z "$TMPDIR" ]; then
TMPDIR="/tmp"
elif [ -n "$TMPDIR" ] && [ ! -d "$TMPDIR" ]; then
mkdir -p "$TMPDIR"
fi
local gh_tmp_md
if [ "`uname -s`" == "OS/390" ]; then
local timestamp
timestamp=$(date +%m%d%Y%H%M%S)
gh_tmp_md="$TMPDIR/tmp.$timestamp"
else
gh_tmp_md=$(mktemp "$TMPDIR/tmp.XXXXXX")
fi
while read -r input; do
echo "$input" >> "$gh_tmp_md"
done
gh_toc_md2html "$gh_tmp_md" | gh_toc_grab "" "$indent"
return
fi
if [ "$1" = '--insert' ]; then
need_replace="yes"
shift
fi
if [ "$1" = '--no-backup' ]; then
need_replace="yes"
no_backup="yes"
shift
fi
if [ "$1" = '--hide-footer' ]; then
need_replace="yes"
no_footer="yes"
shift
fi
if [ "$1" = '--skip-header' ]; then
skip_header="yes"
shift
fi
for md in "$@"
do
echo ""
gh_toc "$md" "$#" "$need_replace" "$no_backup" "$no_footer" "$indent" "$skip_header"
done
echo ""
echo "<!-- Created by https://github.com/ekalinin/github-markdown-toc -->"
}
#
# Entry point
#
gh_toc_app "$@"
================================================
FILE: pylintrc
================================================
[MESSAGES CONTROL]
# locally disable too-many-lines is only possible if the disable statement is
# put into the first line, which is not good practice
# see https://github.com/SoCo/SoCo/issues/127
# duplicate code check disabled whilst refactoring of wimp plugin is in
# progress
disable = broad-except, invalid-name, logging-format-interpolation,
global-statement, unused-argument, fixme, too-many-lines,
missing-module-docstring, missing-function-docstring,
bare-except, redefined-outer-name, too-many-branches,
too-many-locals, too-many-nested-blocks
# disable=too-many-lines,locally-disabled,duplicate-code,too-few-public-methods,
# bad-option-value,no-else-return,cyclic-import,too-many-public-methods,
# bad-continuation, broad-except, invalid-name
[REPORTS]
# Tells whether to display a full report or only the messages
reports=no
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=120
[TYPECHECK]
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis
# ignored-modules=pytest
================================================
FILE: pyproject.toml
================================================
[build-system]
requires = [ "setuptools>=61.2",]
build-backend = "setuptools.build_meta"
[project]
name = "soco-cli"
description = "Sonos command line control utility, based on SoCo"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Development Status :: 4 - Beta"
]
requires-python = ">=3.5"
dynamic = ["version", "dependencies"]
[[project.authors]]
name = "Avantrec Ltd"
email = "soco_cli@avantrec.com"
[project.readme]
file = "PYPI_README.md"
content-type = "text/markdown"
[project.urls]
Homepage = "https://github.com/avantrec/soco-cli"
[project.scripts]
sonos = "soco_cli.sonos:main"
soco = "soco_cli.sonos:main"
sonos-discover = "soco_cli.sonos_discover:main"
soco-discover = "soco_cli.sonos_discover:main"
sonos-http-api-server = "soco_cli.http_api:main"
soco-http-api-server = "soco_cli.http_api:main"
[tool.setuptools]
include-package-data = false
packages = ["soco_cli"]
[tool.setuptools.dynamic.version]
attr = "soco_cli.__init__.__version__"
[tool.setuptools.dynamic.dependencies]
file = ["requirements.txt"]
================================================
FILE: requirements-dev.txt
================================================
black
pylint
flake8
wheel
twine
isort
pytest
mypy
build
================================================
FILE: requirements.txt
================================================
soco == 0.27.1; python_version < "3.6"
soco >= 0.31.0; python_version >= "3.6"
ifaddr == 0.1.7; python_version < "3.7"
ifaddr >= 0.2.0; python_version >= "3.7"
tabulate
rangehttpserver
xmltodict
fastapi; python_version >= "3.7"
uvicorn; python_version >= "3.7"
================================================
FILE: setup.cfg
================================================
[flake8]
# E722 = do not use bare 'except'
ignore = E722
max-line-length = 120
# extend-ignore = E203,W503,E231
================================================
FILE: setup.py
================================================
"""
Minimal setup.py for compatibility with legacy builds or build tool versions.
"""
from setuptools import setup
setup()
================================================
FILE: soco_cli/__init__.py
================================================
"""SoCo-CLI is a command line control interface for Sonos systems.
It is a simplified wrapper around the SoCo python library, as well as providing
an extensive range of additional features.
It can be used as a command line program, as an interactive command shell, and
in other programs via its simple API. It can also run as a simple HTTP API
server, to control Sonos via HTTP requests.
For more information, please see: https://github.com/avantrec/soco-cli
"""
__version__ = "0.4.86"
================================================
FILE: soco_cli/__main__.py
================================================
"""The main entry point into the sonos command."""
from soco_cli.sonos import main
if __name__ == "__main__":
main()
================================================
FILE: soco_cli/action_processor.py
================================================
"""The main command processing module.
This module requires refactoring, improvements to its argument handling,
and needs to be converted to a Class.
"""
import logging
import pprint
import time
from collections import OrderedDict
from datetime import datetime, timedelta
from os import get_terminal_size
from random import randint
import soco # type: ignore
import tabulate # type: ignore
from soco.exceptions import NotSupportedException, SoCoUPnPException # type: ignore
from soco.plugins.sharelink import ShareLinkPlugin # type: ignore
from xmltodict import parse # type: ignore
from soco_cli import alarms
from soco_cli.play_local_file import play_local_file
from soco_cli.play_local_file_lists import play_directory_files, play_m3u_file
from soco_cli.speaker_info import print_speaker_table
from soco_cli.utils import (
convert_to_seconds,
create_list_of_items_from_range,
error_report,
event_unsubscribe,
find_by_name,
forget_event_sub,
get_queue_insertion_position,
get_right_hand_speaker,
get_speaker,
one_or_more_parameters,
one_or_two_parameters,
one_parameter,
parameter_type_error,
playback_state,
pretty_print_values,
queue_is_empty,
read_search,
remember_event_sub,
rename_speaker_in_cache,
save_queue_insertion_position,
save_search,
seconds_until,
two_parameters,
unsub_all_remembered_event_subs,
zero_one_or_two_parameters,
zero_or_one_parameter,
zero_parameters,
)
from soco_cli.wait_actions import process_wait
pp = pprint.PrettyPrinter(width=120)
SONOS_MAX_ITEMS = 66000
def filter_track_info(track_info, excluded_fields):
"""Return a capitalised-key dict of track_info entries, excluding specified fields."""
return {
item.capitalize(): track_info[item]
for item in sorted(track_info)
if item not in excluded_fields
}
def _get_track_position_timedelta(speaker):
"""Return the current track position as a timedelta."""
current_position = speaker.get_current_track_info()["position"]
logging.info("Current playback position is '{}'".format(current_position))
h, m, s = [int(x) for x in current_position.split(":")]
return timedelta(hours=h, minutes=m, seconds=s)
def get_playlist(speaker, name, library=False):
"""Returns the playlist object with 'name' otherwise None."""
if library:
playlists = speaker.music_library.get_playlists(complete_result=True)
else:
playlists = speaker.get_sonos_playlists(complete_result=True)
return find_by_name(playlists, name)
def print_list_header(prefix, name):
spacer = " "
title = "{} {}".format(prefix, name)
underline = "=" * len(title)
print(spacer + title)
print(spacer + underline)
def get_current_queue_position(speaker, tracks=None):
"""Find the current queue position and whether a speaker is playing
from the queue.
'is_playing' will be reported correctly in most, but not all, cases.
"""
qp = 0
is_playing = False
track_title = None
try:
track_info = speaker.get_current_track_info()
qp = int(track_info["playlist_position"])
track_title = track_info["title"]
except Exception:
qp = 0
try:
cts = speaker.get_current_transport_info()["current_transport_state"]
if cts == "PLAYING":
if tracks is not None:
try:
if tracks[qp - 1].title == track_title:
is_playing = True
else:
is_playing = False
qp = 1
except (IndexError, AttributeError):
is_playing = False
qp = 1
else:
is_playing = True
else:
is_playing = False
except Exception:
is_playing = False
return qp, is_playing
def print_tracks(tracks, speaker=None, single_track=False, track_number=None):
qp = None
is_playing = None
if speaker:
qp, is_playing = get_current_queue_position(speaker, tracks)
if single_track:
item_number = track_number
else:
item_number = 1
for track in tracks:
# Assemble available track data
info_items = OrderedDict()
try:
info_items["Artist"] = track.creator
except AttributeError:
pass
try:
info_items["Album"] = track.album
except AttributeError:
pass
try:
info_items["Title"] = track.title
except AttributeError:
pass
try:
if track.item_class == "object.item.audioItem.podcast":
info_items["Podcast Episode"] = info_items.pop("Title")
except (AttributeError, KeyError):
pass
# Assemble the info string to be printed
info_string = ""
first = True
for item, info in info_items.items():
if first:
first = False
else:
info_string += " | "
info_string += "{}: {}".format(item, info)
# Print the information; show position and play state if available
prefix = " "
if qp == item_number:
if is_playing:
prefix = " *> "
else:
prefix = " * "
print("{}{:3d}: {}".format(prefix, item_number, info_string))
item_number += 1
return True
def print_albums(albums, omit_first=False):
item_number = 1
for album in albums:
try:
artist = album.creator
except AttributeError:
artist = ""
try:
title = album.title
except AttributeError:
title = ""
if item_number == 1 and omit_first:
omit_first = False
else:
print("{:7d}: Album: {} | Artist: {}".format(item_number, title, artist))
item_number += 1
return True
def print_artists(artists):
item_number = 1
for artist in artists:
artist_name = artist.title
print("{:7d}: {}".format(item_number, artist_name))
item_number += 1
return True
# Action processing functions
@zero_or_one_parameter
def on_off_action(speaker, action, args, soco_function, use_local_speaker_list):
"""Method to deal with actions that have 'on|off semantics"""
if action == "group_mute":
speaker = speaker.group
soco_function = "mute"
np = len(args)
if np == 0:
state = "on" if getattr(speaker, soco_function) else "off"
print(state)
elif np == 1:
arg = args[0].lower()
if arg == "on":
setattr(speaker, soco_function, True)
elif arg == "off":
setattr(speaker, soco_function, False)
else:
parameter_type_error(action, "on|off")
return False
return True
@zero_parameters
def true_false_action(speaker, action, args, soco_function, use_local_speaker_list):
"""Method to deal with status actions that have 'true|false semantics"""
state = "yes" if getattr(speaker, soco_function) else "no"
print(state)
return True
@zero_parameters
def no_args_no_output(speaker, action, args, soco_function, use_local_speaker_list):
getattr(speaker, soco_function)()
return True
@zero_parameters
def no_args_one_output(speaker, action, args, soco_function, use_local_speaker_list):
result = getattr(speaker, soco_function)
if callable(result):
print(getattr(speaker, soco_function)())
else:
print(result)
return True
@zero_or_one_parameter
def list_queue(speaker, action, args, soco_function, use_local_speaker_list):
queue = speaker.get_queue(max_items=SONOS_MAX_ITEMS)
if len(queue) == 0:
# print("Queue is empty")
return True
if len(args) == 1:
try:
track_number = int(args[0])
if not 0 < track_number <= len(queue):
error_report(
"Track number {} is out of queue range".format(track_number)
)
return False
queue = [queue[track_number - 1]]
except ValueError:
parameter_type_error(action, "integer")
return False
print()
if len(args) == 1:
print_tracks(queue, speaker, single_track=True, track_number=track_number)
else:
print_tracks(queue, speaker)
print()
return True
@zero_parameters
def list_numbered_things(speaker, action, args, soco_function, use_local_speaker_list):
if soco_function in [
"get_sonos_favorites",
"get_favorite_radio_stations",
"get_playlists",
# "get_tracks",
]:
things = getattr(speaker.music_library, soco_function)(complete_result=True)
else:
things = getattr(speaker, soco_function)(complete_result=True)
things_list = [thing.title for thing in things]
things_list.sort()
print()
index = 0
for thing in things_list:
index += 1
print("{:5d}: {}".format(index, thing))
print()
return True
@zero_or_one_parameter
def volume_actions(speaker, action, args, soco_function, use_local_speaker_list):
if soco_function == "group_volume":
logging.info("Using speaker group instead of speaker")
speaker = speaker.group
np = len(args)
if np == 0:
print(speaker.volume)
return True
if np == 1:
try:
vol = int(args[0])
if not (0 <= vol <= 100):
raise ValueError
except ValueError:
parameter_type_error(action, "integer 0 to 100")
return False
if soco_function == "ramp_to_volume":
logging.info("Ramping to volume {}".format(vol))
print(speaker.ramp_to_volume(vol))
else:
logging.info("Setting volume to {}".format(vol))
speaker.volume = vol
return True
@one_parameter
def relative_volume(speaker, action, args, soco_function, use_local_speaker_list):
if soco_function == "group_relative_volume":
logging.info("Using speaker group instead of speaker")
speaker = speaker.group
try:
vol = int(args[0])
if not -100 <= vol <= 100:
raise ValueError
except ValueError:
parameter_type_error(action, "integer from -100 to 100")
logging.info("Adjusting relative volume by {}".format(vol))
speaker.set_relative_volume(vol)
return True
@zero_parameters
def print_info(speaker, action, args, soco_function, use_local_speaker_list):
output = getattr(speaker, soco_function)()
for item in sorted(output):
if item not in ["metadata", "uri", "album_art"]:
print(" {}: {}".format(item.capitalize(), output[item]))
return True
@zero_parameters
def track(speaker, action, args, soco_function, use_local_speaker_list):
state = speaker.get_current_transport_info()["current_transport_state"]
if speaker.is_playing_line_in:
print("Using Line In (state: {})".format(state))
return True
def title_not_useful(title):
indicators = ["m3u", "stream", "sonos", "http", "=", "ZPSTR_"]
for indicator in indicators:
if indicator in title:
return True
return False
stream = False
print(" Playback is {}:".format(playback_state(state)))
track_info = speaker.get_current_track_info()
logging.info("Current track info:\n{}".format(track_info))
# Accumulate info elements to be printed
elements = {"Channel": speaker.get_current_media_info()["channel"]}
# Stream
if track_info["duration"] in ["0:00:00", "NOT_IMPLEMENTED"]:
logging.info("Track is a radio stream")
stream = True
elements.update(
filter_track_info(
track_info,
["metadata", "album_art", "duration", "playlist_position", "uri"],
)
)
try:
metadata = parse(track_info["metadata"])
if elements["Artist"] == "":
logging.info("Attempting to find 'Artist' from metadata")
try:
elements["Artist"] = metadata["DIDL-Lite"]["item"]["dc:creator"]
except (KeyError, TypeError):
logging.info("Unable to find 'Artist'")
if elements["Title"] == "":
logging.info("Attempting to find 'Title' from metadata")
try:
elements["Title"] = metadata["DIDL-Lite"]["item"]["dc:title"]
except (KeyError, TypeError):
logging.info("Unable to find 'Title'")
except Exception:
pass
try:
logging.info("Attempting to find 'Radio Show' using events")
sub = speaker.avTransport.subscribe()
remember_event_sub(sub)
event = sub.events.get(timeout=0.5)
elements["Radio Show"] = event.variables[
"current_track_meta_data"
].radio_show.rpartition(",")[0]
event_unsubscribe(sub)
forget_event_sub(sub)
except Exception as e:
logging.info("Unable to find 'Radio Show': {}".format(e))
finally:
unsub_all_remembered_event_subs()
# Podcast, Audio Book, or normal track
else:
logging.info("Track has a non-zero duration")
try:
metadata = parse(track_info["metadata"])
logging.info("Track metadata: {}".format(metadata))
except Exception:
logging.info("No usable metadata available")
metadata = None
# Podcast
if (
metadata
and metadata["DIDL-Lite"]["item"]["upnp:class"]
== "object.item.audioItem.podcast"
):
logging.info("Track is a podcast")
try:
elements["Podcast"] = metadata["DIDL-Lite"]["item"]["r:podcast"]
elements["Release Date"] = metadata["DIDL-Lite"]["item"][
"r:releaseDate"
][:10]
except (KeyError, TypeError):
logging.info("Failed to find 'Podcast' and/or 'Release Date'")
elements.update(
filter_track_info(
track_info, ["metadata", "uri", "album_art", "album", "artist"]
)
)
try:
elements["Episode"] = elements.pop("Title")
except KeyError:
pass
# Audio book
elif (
metadata
and "object.item.audioItem.audioBook"
in metadata["DIDL-Lite"]["item"]["upnp:class"]
):
logging.info("Track is an audio book")
try:
elements["Book Title"] = elements.pop("Channel", "")
elements["Creator(s)"] = track_info["artist"]
elements["Narrator(s)"] = metadata["DIDL-Lite"]["item"]["r:narrator"]
elements["Chapter"] = metadata["DIDL-Lite"]["item"]["dc:title"]
except (KeyError, TypeError):
logging.info("Failed to find book details")
elements.update(
filter_track_info(
track_info,
[
"metadata",
"uri",
"album_art",
"album",
"artist",
"title",
"playlist_position",
],
)
)
# Regular track
else:
logging.info("Track is a normal audio track")
elements.update(
filter_track_info(track_info, ["metadata", "uri", "album_art"])
)
# If there's no title, look in the metadata
try:
if elements["Title"] == "" or title_not_useful(elements["Title"]):
metadata = parse(track_info["metadata"])
elements["Title"] = metadata["DIDL-Lite"]["item"]["dc:title"]
logging.info(
"Found title in metadata: {}".format(elements["Title"])
)
except KeyError:
pass
# Remove blank and 'None' items
elements = {
key: value
for key, value in elements.items()
if value != "" and value is not None and value != "NOT_IMPLEMENTED"
}
# Deduplicate 'Channel' and 'Title'
# Remove 'Title' if it looks unuseful
try:
if (elements["Channel"] == elements["Title"]) or (
stream and title_not_useful(elements["Title"])
):
logging.info("Removing Title: '{}'".format(elements["Title"]))
elements.pop("Title", None)
except KeyError:
pass
# Rename 'Playlist_position' and 'Position'
try:
if int(elements["Playlist_position"]) != 0:
elements["Playlist Position"] = elements["Playlist_position"]
elements.pop("Playlist_position", None)
except KeyError:
pass
try:
elements["Elapsed"] = elements["Position"]
elements.pop("Position", None)
except KeyError:
pass
# Reorder the elements
element_order = [
"Channel",
"Radio Show",
"Podcast",
"Artist",
"Creator(s)",
"Narrator(s)",
"Book Title",
"Chapter",
"Album",
"Title",
"Episode",
"Release Date",
"Playlist Position",
"Duration",
"Elapsed",
]
ordered_elements = OrderedDict()
for element in element_order:
try:
ordered_elements[element] = elements.pop(element)
except KeyError:
pass
# Add any elements we've missed
ordered_elements.update(elements)
logging.info("Items to be printed: {}".format(ordered_elements))
pretty_print_values(ordered_elements, indent=3, spacing=5, sort_by_key=False)
return True
@zero_or_one_parameter
def playback_mode(speaker, action, args, soco_function, use_local_speaker_list):
np = len(args)
possible_args = [
"normal",
"repeat_all",
"repeat_one",
"shuffle",
"shuffle_norepeat",
"shuffle_repeat_one",
]
if np == 0:
print(speaker.play_mode)
elif np == 1:
if args[0].lower() in possible_args:
speaker.play_mode = args[0]
else:
parameter_type_error(action, possible_args)
return True
@zero_or_one_parameter
def shuffle(speaker, action, args, soco_function, use_local_speaker_list):
np = len(args)
if np == 0:
if speaker.shuffle is True:
print("on")
else:
print("off")
elif np == 1:
if args[0].lower() == "on":
speaker.shuffle = True
elif args[0].lower() == "off":
speaker.shuffle = False
else:
error_report("Action '{}' takes parameter 'on' or 'off'".format(action))
return False
return True
@zero_or_one_parameter
def repeat(speaker, action, args, soco_function, use_local_speaker_list):
np = len(args)
if np == 0:
if speaker.repeat is True:
print("all")
elif speaker.repeat is False:
print("off")
else:
print("one")
elif np == 1:
if args[0].lower() in ["off", "none"]:
speaker.repeat = False
elif args[0].lower() == "one":
speaker.repeat = "ONE"
elif args[0].lower() == "all":
speaker.repeat = True
else:
error_report(
"Action '{}' takes parameter 'off', 'one', or 'all'".format(action)
)
return False
return True
@zero_parameters
def transport_state(speaker, action, args, soco_function, use_local_speaker_list):
print(speaker.get_current_transport_info()["current_transport_state"])
return True
def play_favourite_core(speaker, favourite, favourite_number=None):
"""Core of the play_favourite action, but doesn't exit on failure"""
fs = speaker.music_library.get_sonos_favorites(complete_result=True)
if favourite_number:
err_msg = "Favourite number must be integer between 1 and {}".format(len(fs))
try:
favourite_number = int(favourite_number)
except ValueError:
return False, err_msg
if not 0 < favourite_number <= len(fs):
return False, err_msg
# List must be sorted by title to match the output of 'list_favourites'
fs.sort(key=lambda x: x.title)
the_fav = fs[favourite_number - 1]
logging.info(
"Favourite number {} is '{}'".format(favourite_number, the_fav.title)
)
else:
the_fav = find_by_name(fs, favourite)
if the_fav:
# play_uri works for some favourites
# TODO: this is broken and we should test for the
# type of favourite
try:
uri = the_fav.get_uri()
metadata = the_fav.resource_meta_data
logging.info(
"Trying 'play_uri()': URI={}, Metadata={}".format(uri, metadata)
)
speaker.play_uri(uri=uri, meta=metadata)
return True, ""
except Exception as e:
e1 = e
# Other favourites will be added to the queue, then played
try:
# Add to the end of the current queue and play
logging.info("Trying 'add_to_queue()'")
index = speaker.add_to_queue(the_fav, as_next=True)
speaker.play_from_queue(index, start=True)
return True, ""
except Exception as e2:
msg = "1: {} | 2: {}".format(str(e1), str(e2))
return False, msg
msg = "Favourite '{}' not found".format(favourite)
return False, msg
@one_parameter
def play_favourite(speaker, action, args, soco_function, use_local_speaker_list):
result, msg = play_favourite_core(speaker, args[0])
if not result:
error_report(msg)
return False
return True
@one_parameter
def play_favourite_number(speaker, action, args, soco_function, use_local_speaker_list):
logging.info("Playing favourite number {}".format(args[0]))
result, msg = play_favourite_core(speaker, "", args[0])
if not result:
error_report(msg)
return False
return True
@one_or_two_parameters
def add_favourite_to_queue(
speaker, action, args, soco_function, use_local_speaker_list
):
favourite = args[0]
fs = speaker.music_library.get_sonos_favorites()
the_fav = find_by_name(fs, favourite)
if the_fav:
if len(args) == 2:
position = get_queue_insertion_position(speaker, args[1], action)
else:
position = speaker.queue_size + 1
try:
# Print the queue position and return
speaker.add_to_queue(the_fav, position=position)
save_queue_insertion_position(position)
print(position)
return True
except Exception as e:
error_report("{}".format(str(e)))
return False
error_report("Favourite '{}' not found".format(args[0]))
return False
@one_parameter
def play_favourite_radio_number(
speaker, action, args, soco_function, use_local_speaker_list
):
try:
fav_no = int(args[0])
except ValueError:
parameter_type_error(action, "integer")
return False
logging.info("Playing favourite radio station no. {}".format(fav_no))
preset = 0
limit = 99
stations = speaker.music_library.get_favorite_radio_stations(preset, limit)
station_titles = sorted([s.title for s in stations])
logging.info("Sorted station titles are: {}".format(station_titles))
station_title = station_titles[fav_no - 1]
logging.info("Requested station is '{}'".format(station_title))
return play_favourite_radio(
speaker, action, [station_title], soco_function, use_local_speaker_list
)
@one_parameter
def play_favourite_radio(speaker, action, args, soco_function, use_local_speaker_list):
favourite = args[0]
preset = 0
limit = 99
fs = speaker.music_library.get_favorite_radio_stations(preset, limit)
the_fav = find_by_name(fs, favourite)
if the_fav:
uri = the_fav.get_uri()
meta_template = """
<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/"
xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/"
xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/">
<item id="R:0/0/0" parentID="R:0/0" restricted="true">
<dc:title>{title}</dc:title>
<upnp:class>object.item.audioItem.audioBroadcast</upnp:class>
<desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">
{service}
</desc>
</item>
</DIDL-Lite>' """
tunein_service = "SA_RINCON65031_"
uri = uri.replace("&", "&")
metadata = meta_template.format(title=the_fav.title, service=tunein_service)
logging.info("Trying 'play_uri()': URI={}, Metadata={}".format(uri, metadata))
speaker.play_uri(uri=uri, meta=metadata)
return True
error_report("Favourite '{}' not found".format(args[0]))
return False
@one_or_two_parameters
def play_uri(speaker, action, args, soco_function, use_local_speaker_list):
uri = args[0]
title = "" if len(args) == 1 else args[1]
for radio in [False, True]:
try:
speaker.play_uri(uri, title=title, force_radio=radio)
return True
except Exception:
continue
error_report("Failed to play URI: '{}'".format(uri))
return False
@zero_or_one_parameter
def sleep_timer(speaker, action, args, soco_function, use_local_speaker_list):
np = len(args)
if np == 0:
st = speaker.get_sleep_timer()
if st is not None:
time_now = datetime.now()
remaining_seconds = timedelta(0, st)
expiry_time = time_now + remaining_seconds
print(
"Sleep timer expires in {} at {}".format(
remaining_seconds, expiry_time.strftime("%H:%M")
)
)
else:
print("0 (No sleep timer set)")
elif np == 1:
if args[0].lower() in ["off", "cancel"]:
logging.info("Cancelling sleep timer")
speaker.set_sleep_timer(None)
else:
try:
duration = convert_to_seconds(args[0])
except ValueError:
parameter_type_error(
action,
"number of hours, seconds or minutes + 'h/m/s', or off|cancel",
)
return False
if 0 <= duration <= 86399:
logging.info("Setting sleep timer to {}s".format(duration))
speaker.set_sleep_timer(duration)
else:
parameter_type_error(action, "maximum duration is 23.999hrs")
return False
return True
@one_parameter
def sleep_at(speaker, action, args, soco_function, use_local_speaker_list):
try:
duration = seconds_until(args[0])
except ValueError:
parameter_type_error(action, "a time in 24hr 'HH:MM' or 'HH:MM:SS' format")
return False
if 0 <= duration <= 86399:
logging.info("Setting sleep timer to {}s".format(duration))
speaker.set_sleep_timer(duration)
else:
parameter_type_error(action, "maximum duration is 23.999hrs")
return False
return True
@one_parameter
def group_or_pair(speaker, action, args, soco_function, use_local_speaker_list):
speaker2 = get_speaker(args[0], use_local_speaker_list)
if not speaker2:
error_report("Speaker '{}' not found".format(args[0]))
return False
if speaker == speaker2:
error_report("Speakers are the same")
return False
logging.info(
"Executing '{}' on speakers '{}', '{}'".format(
soco_function, speaker.player_name, speaker2.player_name
)
)
getattr(speaker, soco_function)(speaker2)
return True
@two_parameters
def add_satellite_speakers(
speaker, action, args, soco_function, use_local_speaker_list
):
left_rear = get_speaker(args[0], use_local_speaker_list)
if not left_rear:
error_report("Speaker '{}' not found".format(args[0]))
return False
right_rear = get_speaker(args[1], use_local_speaker_list)
if not right_rear:
error_report("Speaker '{}' not found".format(args[1]))
return False
getattr(speaker, soco_function)(left_rear, right_rear)
return True
@one_or_more_parameters
def multi_group(speaker, action, args, soco_function, use_local_speaker_list):
"""
Group one or more speakers with a coordinator speaker. Note: reverses the usual
order; the target speaker is the coordinator, not the speaker to be grouped.
"""
logging.info("Grouping speakers '{}' with '{}'".format(args, speaker.player_name))
for speaker_name in args:
target_speaker = get_speaker(speaker_name, use_local_speaker_list)
if not target_speaker:
error_report("Speaker '{}' not found".format(speaker_name))
continue
logging.info(
"Grouping speaker '{}' with coordinator '{}'".format(
target_speaker.player_name, speaker.player_name
)
)
group_or_pair(
target_speaker,
action,
[speaker.player_name],
soco_function,
use_local_speaker_list,
)
return True
@zero_parameters
def operate_on_all(speaker, action, args, soco_function, use_local_speaker_list):
zones = speaker.all_zones
for zone in zones:
if zone.is_visible:
try:
logging.info(
"Executing '{}' on speaker '{}'".format(
soco_function, zone.player_name
)
)
getattr(zone, soco_function)()
except Exception:
logging.info("Operation failed ... continuing")
# Ignore errors here; don't want to halt on
# a failed pause (e.g., if speaker isn't playing)
continue
return True
@zero_parameters
def zones(speaker, action, args, soco_function, use_local_speaker_list):
zones = speaker.all_zones if "all" in action else speaker.visible_zones
count = 1
for zone in zones:
if 1 < count < len(zones) + 1:
print(", ", end="")
print('"{}"'.format(zone.player_name), end="")
count += 1
print()
return True
@zero_or_one_parameter
def play_from_queue(speaker, action, args, soco_function, use_local_speaker_list):
np = len(args)
if np == 0:
speaker.play_from_queue(0)
return True
if args[0] in ["current", "cp", "current_position"]:
index, _ = get_current_queue_position(speaker)
elif args[0] in ["last", "lp", "last_position"]:
index = len(speaker.get_queue(max_items=SONOS_MAX_ITEMS))
elif args[0] in ["random", "rand", "r"]:
index = randint(1, len(speaker.get_queue(max_items=SONOS_MAX_ITEMS)))
elif args[0] in ["last_added", "la"]:
try:
index = get_queue_insertion_position()
except Exception as e:
error_report("No saved queue position: {}".format(e))
return False
else:
try:
index = int(args[0])
except ValueError:
parameter_type_error(
action,
"integer, 'current', 'last', or 'random'",
)
return False
if 1 <= index <= speaker.queue_size:
speaker.play_from_queue(index - 1)
else:
error_report("Queue index '{}' is out of range".format(index))
return False
return True
@one_parameter
def remove_from_queue(speaker, action, args, soco_function, use_local_speaker_list):
# Generate a list that represents which tracks to remove, denoted by '0'
# Initially mark each track as '1' (retain)
if queue_is_empty(speaker):
return False
queue = []
for _ in range(speaker.queue_size):
queue.append(1)
# Catch exceptions at the end
# Note: this can be refactored using utils.create_list_of_items_from_range()
try:
# Create a list of items to remove based on the input args
# Mark these as '0'
items = args[0].split(",")
for index in items:
# Check for a range ('x-y') instead of a single integer
if "-" in index:
rng = index.split("-")
if len(rng) != 2:
parameter_type_error(
action, "two integers and a '-', e.g., '3-7' when using a range"
)
return False
index_1 = int(rng[0])
index_2 = int(rng[1])
if index_1 < 1 or index_2 < 1:
raise IndexError
if index_1 > index_2:
# Reverse the indices
index_2, index_1 = index_1, index_2
for i in range(index_1 - 1, index_2):
queue[i] = 0
else:
index = int(index)
if index < 1:
raise IndexError
queue[index - 1] = 0
# Exception handling
# Catch any non-integer input values
except ValueError:
parameter_type_error(
action,
"integer, or comma-separated integers without spaces (e.g., 3,7,4)",
)
return False
# Catch any out-of-range values
except IndexError:
error_report(
"Queue index(es) must be between 1 and {} (inclusive)".format(len(queue))
)
return False
# Walk though the list of tracks from position 1, removing items marked '0'
# Account for the queue shift by keeping count of those deleted
logging.info("Created map of queue items to delete (==0) {}".format(queue))
# Note: do not switch the loop below to 'enumerate'. Yield behaviour breaks
# the sequencing of requests to Sonos.
# pylint: disable = consider-using-enumerate
count_removed = 0
for index in range(len(queue)):
if queue[index] == 0:
updated_index = index - count_removed
speaker.remove_from_queue(updated_index)
logging.info(
"Removing queue item at (adjusted) index {}".format(updated_index + 1)
)
count_removed += 1
return True
@zero_parameters
def remove_current_track_from_queue(
speaker, action, args, soco_function, use_local_speaker_list
):
if queue_is_empty(speaker):
return False
current_track = int(speaker.get_current_track_info()["playlist_position"])
logging.info("Removing track {}".format(current_track))
speaker.remove_from_queue(current_track - 1)
return True
@zero_or_one_parameter
def remove_last_track_from_queue(
speaker, action, args, soco_function, use_local_speaker_list
):
queue_size = speaker.queue_size
logging.info("Queue size is {}".format(queue_size))
if queue_is_empty(speaker):
return False
if len(args) == 1:
try:
count = int(args[0])
except ValueError:
parameter_type_error(action, "an integer > 1")
if not 1 <= count <= queue_size:
error_report("parameter must be between 1 and {}".format(queue_size))
return False
else:
count = 1
logging.info("Removing the last {} tracks from the queue".format(count))
while count > 0:
logging.info("Removing track {}".format(queue_size))
speaker.remove_from_queue(queue_size - 1)
queue_size -= 1
count -= 1
return True
@one_parameter
def save_queue(speaker, action, args, soco_function, use_local_speaker_list):
if queue_is_empty(speaker):
return False
speaker.create_sonos_playlist_from_queue(args[0])
return True
@one_parameter
def seek(speaker, action, args, soco_function, use_local_speaker_list):
try:
seconds = convert_to_seconds(args[0])
except ValueError:
parameter_type_error(action, "a valid time format")
return False
if seconds < 0:
parameter_type_error(action, "cannot seek to before start of track")
return False
seek_point = str(timedelta(seconds=seconds))
logging.info("Seek point is {}".format(seek_point))
try:
# seek() will handle out-of-bounds
speaker.seek(seek_point)
except SoCoUPnPException:
parameter_type_error(action, "valid time value on a seekable source")
return False
return True
@one_parameter
def seek_forward(speaker, action, args, soco_function, use_local_speaker_list):
# Calculate the time increment
increment = int(convert_to_seconds(args[0])) # Integer number of seconds
if increment < 0:
parameter_type_error(action, "a positive time increment")
return False
logging.info("Seeking forward by {}s".format(increment))
td_current = _get_track_position_timedelta(speaker)
td_increment = timedelta(seconds=increment)
td_new_str = str(td_current + td_increment)
logging.info(
"Seeking forward to position '{}' ... note: might hit end of track".format(
td_new_str
)
)
try:
speaker.seek(td_new_str)
except SoCoUPnPException:
parameter_type_error(action, "time increment on a seekable source")
return False
return True
@one_parameter
def seek_back(speaker, action, args, soco_function, use_local_speaker_list):
# Calculate the time increment
increment = int(convert_to_seconds(args[0])) # Integer number of seconds
if increment < 0:
parameter_type_error(action, "a positive time increment")
return False
logging.info("Seeking backward by {}s".format(increment))
td_current = _get_track_position_timedelta(speaker)
td_increment = timedelta(seconds=increment)
if td_current - td_increment < timedelta():
logging.info("Cannot seek beyond start of track ... seek to start instead")
td_new_str = "00:00:00"
else:
td_new_str = str(td_current - td_increment)
logging.info("Seeking backward to position '{}'".format(td_new_str))
try:
speaker.seek(td_new_str)
except SoCoUPnPException:
parameter_type_error(action, "time increment on a seekable source")
return False
return True
@one_or_two_parameters
def playlist_operations(speaker, action, args, soco_function, use_local_speaker_list):
name = args[0]
if soco_function == "create_sonos_playlist":
getattr(speaker, soco_function)(name)
return True
if soco_function == "add_uri_to_queue":
getattr(speaker, soco_function)(name)
return True
if soco_function == "remove_sonos_playlist":
try:
playlist = get_playlist(speaker, name)
speaker.remove_sonos_playlist(playlist)
except SoCoUPnPException:
error_report("Playlist '{}' not found".format(name))
return True
playlist = None
if soco_function == "add_to_queue":
playlist = get_playlist(speaker, name)
elif soco_function == "add_library_playlist_to_queue":
playlist = get_playlist(speaker, name, library=True)
if playlist is not None:
if soco_function in ["add_to_queue", "add_library_playlist_to_queue"]:
if len(args) == 2:
position = get_queue_insertion_position(speaker, args[1], action)
else:
position = speaker.queue_size + 1
result = speaker.add_to_queue(playlist, position=position)
save_queue_insertion_position(position)
print(result)
else:
getattr(speaker, soco_function)(playlist)
else:
error_report("Playlist '{}' not found".format(args[0]))
return False
return True
@one_parameter
def list_playlist_tracks(speaker, action, args, soco_function, use_local_speaker_list):
playlist = get_playlist(speaker, args[0])
if playlist:
print()
print_list_header("Sonos Playlist:", playlist.title)
tracks = speaker.music_library.browse_by_idstring(
"sonos_playlists", playlist.item_id, max_items=SONOS_MAX_ITEMS
)
print_tracks(tracks)
print()
save_search(tracks)
return True
error_report("Playlist '{}' not found".format(args[0]))
return False
@one_parameter
def list_library_playlist_tracks(
speaker, action, args, soco_function, use_local_speaker_list
):
playlist = get_playlist(speaker, args[0], library=True)
if playlist:
print()
print_list_header("Library Playlist:", playlist.title)
tracks = speaker.music_library.browse_by_idstring(
"playlists", playlist.item_id, max_items=SONOS_MAX_ITEMS
)
print_tracks(tracks)
print()
save_search(tracks)
return True
error_report("Playlist '{}' not found".format(args[0]))
return False
@two_parameters
def remove_from_playlist(speaker, action, args, soco_function, use_local_speaker_list):
name = args[0]
try:
track_number = int(args[1])
except ValueError:
parameter_type_error(action, "integer (track number)")
return False
playlist = get_playlist(speaker, name)
if playlist:
speaker.remove_from_sonos_playlist(playlist, track_number - 1)
return True
error_report("Playlist '{}' not found".format(args[0]))
return False
@zero_one_or_two_parameters
def line_in(speaker, action, args, soco_function, use_local_speaker_list):
return line_in_core(speaker, action, args, True, use_local_speaker_list)
@zero_one_or_two_parameters
def cue_line_in(speaker, action, args, soco_function, use_local_speaker_list):
if len(args) == 0:
logging.info("'cue_line_in' invoked without parameters; insert 'on'")
new_args = ("on",)
else:
new_args = args
return line_in_core(speaker, action, new_args, False, use_local_speaker_list)
def line_in_core(speaker, action, args, start_playback, use_local_speaker_list):
np = len(args)
if np == 0:
state = "on" if speaker.is_playing_line_in else "off"
state = state + " ({})".format(
speaker.get_current_transport_info()["current_transport_state"]
)
print(state)
else:
source = args[0]
if source.lower() == "off":
logging.info("Stopping playback")
speaker.stop()
elif source.lower() in ["on", "left_input"]:
# Switch to the speaker's own line_in
logging.info("Switching to the speaker's own Line-In")
try:
speaker.switch_to_line_in()
if start_playback:
logging.info("Starting playback")
speaker.play()
else:
logging.info("Stopping playback")
speaker.stop()
except SoCoUPnPException:
error_report("Line In operation failed ... not supported?")
return False
else:
if source.lower() == "right_input":
# We want the right-hand speaker of the stereo pair
logging.info("Looking for right-hand speaker")
line_in_source = get_right_hand_speaker(speaker)
else:
# We want to use another speaker's input
if np == 2: # Want to select the input of a stereo pair
the_input = args[1].lower()
if the_input == "right_input":
logging.info("Using right-hand speaker's input")
logging.info("Looking for right-hand speaker")
left_speaker = get_speaker(source, use_local_speaker_list)
line_in_source = get_right_hand_speaker(left_speaker)
elif the_input == "left_input":
logging.info("Using left-hand speaker's input")
line_in_source = get_speaker(source, use_local_speaker_list)
else:
parameter_type_error(
action,
"second parameter (if present) must be 'left_input' or"
" 'right_input'",
)
gitextract_h5uvuzqn/
├── .gitignore
├── CHANGELOG.txt
├── LICENSE
├── MANIFEST.in
├── Makefile
├── PYPI_README.md
├── README.md
├── RELEASING.txt
├── assets/
│ └── soco-cli-logo.afdesign
├── gh-md-toc
├── pylintrc
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── setup.cfg
├── setup.py
├── soco_cli/
│ ├── __init__.py
│ ├── __main__.py
│ ├── action_processor.py
│ ├── alarms.py
│ ├── aliases.py
│ ├── api.py
│ ├── check_for_update.py
│ ├── cmd_parser.py
│ ├── http_api.py
│ ├── interactive.py
│ ├── keystroke_capture.py
│ ├── m3u_parser.py
│ ├── match_speaker_names.py
│ ├── play_local_file.py
│ ├── play_local_file_lists.py
│ ├── sonos.py
│ ├── sonos_discover.py
│ ├── speaker_info.py
│ ├── speakers.py
│ ├── track_follow.py
│ ├── utils.py
│ └── wait_actions.py
└── tests/
├── test_action_processor.py
├── test_aliases.py
├── test_check_for_update.py
├── test_cli.py
├── test_cmd_parser.py
├── test_comprehensive.py
├── test_http_api.py
├── test_interactive.py
├── test_m3u_parser.py
├── test_match_speaker_names.py
├── test_play_local_file.py
├── test_speakers.py
├── test_utils.py
├── test_utils_extended.py
└── test_wait_actions.py
SYMBOL INDEX (1173 symbols across 35 files)
FILE: soco_cli/action_processor.py
function filter_track_info (line 60) | def filter_track_info(track_info, excluded_fields):
function _get_track_position_timedelta (line 69) | def _get_track_position_timedelta(speaker):
function get_playlist (line 77) | def get_playlist(speaker, name, library=False):
function print_list_header (line 86) | def print_list_header(prefix, name):
function get_current_queue_position (line 94) | def get_current_queue_position(speaker, tracks=None):
function print_tracks (line 134) | def print_tracks(tracks, speaker=None, single_track=False, track_number=...
function print_albums (line 189) | def print_albums(albums, omit_first=False):
function print_artists (line 208) | def print_artists(artists):
function on_off_action (line 219) | def on_off_action(speaker, action, args, soco_function, use_local_speake...
function true_false_action (line 241) | def true_false_action(speaker, action, args, soco_function, use_local_sp...
function no_args_no_output (line 249) | def no_args_no_output(speaker, action, args, soco_function, use_local_sp...
function no_args_one_output (line 255) | def no_args_one_output(speaker, action, args, soco_function, use_local_s...
function list_queue (line 265) | def list_queue(speaker, action, args, soco_function, use_local_speaker_l...
function list_numbered_things (line 292) | def list_numbered_things(speaker, action, args, soco_function, use_local...
function volume_actions (line 314) | def volume_actions(speaker, action, args, soco_function, use_local_speak...
function relative_volume (line 341) | def relative_volume(speaker, action, args, soco_function, use_local_spea...
function print_info (line 358) | def print_info(speaker, action, args, soco_function, use_local_speaker_l...
function track (line 367) | def track(speaker, action, args, soco_function, use_local_speaker_list):
function playback_mode (line 577) | def playback_mode(speaker, action, args, soco_function, use_local_speake...
function shuffle (line 598) | def shuffle(speaker, action, args, soco_function, use_local_speaker_list):
function repeat (line 617) | def repeat(speaker, action, args, soco_function, use_local_speaker_list):
function transport_state (line 642) | def transport_state(speaker, action, args, soco_function, use_local_spea...
function play_favourite_core (line 647) | def play_favourite_core(speaker, favourite, favourite_number=None):
function play_favourite (line 701) | def play_favourite(speaker, action, args, soco_function, use_local_speak...
function play_favourite_number (line 711) | def play_favourite_number(speaker, action, args, soco_function, use_loca...
function add_favourite_to_queue (line 722) | def add_favourite_to_queue(
function play_favourite_radio_number (line 747) | def play_favourite_radio_number(
function play_favourite_radio (line 774) | def play_favourite_radio(speaker, action, args, soco_function, use_local...
function play_uri (line 807) | def play_uri(speaker, action, args, soco_function, use_local_speaker_list):
function sleep_timer (line 822) | def sleep_timer(speaker, action, args, soco_function, use_local_speaker_...
function sleep_at (line 860) | def sleep_at(speaker, action, args, soco_function, use_local_speaker_list):
function group_or_pair (line 876) | def group_or_pair(speaker, action, args, soco_function, use_local_speake...
function add_satellite_speakers (line 894) | def add_satellite_speakers(
function multi_group (line 910) | def multi_group(speaker, action, args, soco_function, use_local_speaker_...
function operate_on_all (line 937) | def operate_on_all(speaker, action, args, soco_function, use_local_speak...
function zones (line 957) | def zones(speaker, action, args, soco_function, use_local_speaker_list):
function play_from_queue (line 970) | def play_from_queue(speaker, action, args, soco_function, use_local_spea...
function remove_from_queue (line 1005) | def remove_from_queue(speaker, action, args, soco_function, use_local_sp...
function remove_current_track_from_queue (line 1075) | def remove_current_track_from_queue(
function remove_last_track_from_queue (line 1087) | def remove_last_track_from_queue(
function save_queue (line 1114) | def save_queue(speaker, action, args, soco_function, use_local_speaker_l...
function seek (line 1122) | def seek(speaker, action, args, soco_function, use_local_speaker_list):
function seek_forward (line 1143) | def seek_forward(speaker, action, args, soco_function, use_local_speaker...
function seek_back (line 1168) | def seek_back(speaker, action, args, soco_function, use_local_speaker_li...
function playlist_operations (line 1193) | def playlist_operations(speaker, action, args, soco_function, use_local_...
function list_playlist_tracks (line 1234) | def list_playlist_tracks(speaker, action, args, soco_function, use_local...
function list_library_playlist_tracks (line 1252) | def list_library_playlist_tracks(
function remove_from_playlist (line 1272) | def remove_from_playlist(speaker, action, args, soco_function, use_local...
function line_in (line 1289) | def line_in(speaker, action, args, soco_function, use_local_speaker_list):
function cue_line_in (line 1294) | def cue_line_in(speaker, action, args, soco_function, use_local_speaker_...
function line_in_core (line 1303) | def line_in_core(speaker, action, args, start_playback, use_local_speake...
function eq (line 1376) | def eq(speaker, action, args, soco_function, use_local_speaker_list):
function eq_relative (line 1395) | def eq_relative(speaker, action, args, soco_function, use_local_speaker_...
function balance (line 1419) | def balance(speaker, action, args, soco_function, use_local_speaker_list):
function reindex (line 1447) | def reindex(speaker, action, args, soco_function, use_local_speaker_list):
function is_indexing (line 1457) | def is_indexing(speaker, action, args, soco_function, use_local_speaker_...
function info (line 1466) | def info(speaker, action, args, soco_function, use_local_speaker_list):
function groups (line 1507) | def groups(speaker, action, args, soco_function, use_local_speaker_list):
function list_libraries (line 1524) | def list_libraries(speaker, action, args, soco_function, use_local_speak...
function system_info (line 1534) | def system_info(speaker, action, args, soco_function, use_local_speaker_...
function list_all_playlist_tracks (line 1540) | def list_all_playlist_tracks(
function wait_stop_core (line 1555) | def wait_stop_core(speaker, not_paused=False):
function wait_stop (line 1585) | def wait_stop(speaker, action, args, soco_function, use_local_speaker_li...
function wait_stop_not_pause (line 1590) | def wait_stop_not_pause(speaker, action, args, soco_function, use_local_...
function wait_stopped_for_core (line 1594) | def wait_stopped_for_core(speaker, action, duration_arg, not_paused=False):
function wait_stopped_for (line 1646) | def wait_stopped_for(speaker, action, args, soco_function, use_local_spe...
function wait_stopped_for_not_pause (line 1651) | def wait_stopped_for_not_pause(
function wait_start (line 1658) | def wait_start(speaker, action, args, soco_function, use_local_speaker_l...
function search_artists (line 1682) | def search_artists(speaker, action, args, soco_function, use_local_speak...
function list_artists (line 1731) | def list_artists(speaker, action, args, soco_function, use_local_speaker...
function list_albums (line 1742) | def list_albums(speaker, action, args, soco_function, use_local_speaker_...
function search_albums (line 1754) | def search_albums(speaker, action, args, soco_function, use_local_speake...
function search_tracks (line 1778) | def search_tracks(speaker, action, args, soco_function, use_local_speake...
function search_library (line 1802) | def search_library(speaker, action, args, soco_function, use_local_speak...
function tracks_in_album (line 1810) | def tracks_in_album(speaker, action, args, soco_function, use_local_spea...
function queue_item_core (line 1839) | def queue_item_core(speaker, action, args, info_type):
function queue_album (line 1861) | def queue_album(speaker, action, args, soco_function, use_local_speaker_...
function queue_track (line 1866) | def queue_track(speaker, action, args, soco_function, use_local_speaker_...
function if_stopped_or_playing (line 1871) | def if_stopped_or_playing(speaker, action, args, soco_function, use_loca...
function if_coordinator (line 1906) | def if_coordinator(speaker, action, args, soco_function, use_local_speak...
function if_queue (line 1928) | def if_queue(speaker, action, args, soco_function, use_local_speaker_list):
function cue_favourite (line 1960) | def cue_favourite(speaker, action, args, soco_function, use_local_speake...
function transfer_playback (line 1994) | def transfer_playback(speaker, action, args, soco_function, use_local_sp...
function queue_position (line 2013) | def queue_position(speaker, action, args, soco_function, use_local_speak...
function last_search (line 2020) | def last_search(speaker, action, args, soco_function, use_local_speaker_...
function get_queue_insertion_position (line 2039) | def get_queue_insertion_position(speaker, insertion_point, action):
function queue_search_results (line 2098) | def queue_search_results(speaker, action, args, soco_function, use_local...
function cue_favourite_radio_station (line 2132) | def cue_favourite_radio_station(
function battery (line 2139) | def battery(speaker, action, args, soco_function, use_local_speaker_list):
function rename (line 2158) | def rename(speaker, action, args, soco_function, use_local_speaker_list):
function album_art (line 2172) | def album_art(speaker, action, args, soco_function, use_local_speaker_li...
function add_uri_to_queue (line 2218) | def add_uri_to_queue(speaker, action, args, soco_function, use_local_spe...
function play_file (line 2232) | def play_file(speaker, action, args, soco_function, use_local_speaker_li...
function play_m3u (line 2244) | def play_m3u(speaker, action, args, soco_function, use_local_speaker_list):
function play_directory (line 2254) | def play_directory(speaker, action, args, soco_function, use_local_speak...
function buttons (line 2264) | def buttons(speaker, action, args, soco_function, use_local_speaker_list):
function fixed_volume (line 2282) | def fixed_volume(speaker, action, args, soco_function, use_local_speaker...
function trueplay (line 2306) | def trueplay(speaker, action, args, soco_function, use_local_speaker_list):
function groupstatus (line 2332) | def groupstatus(speaker, action, args, soco_function, use_local_speaker_...
function pauseplay (line 2387) | def pauseplay(speaker, action, args, soco_function, use_local_speaker_li...
function available_actions (line 2409) | def available_actions(speaker, action, args, soco_function, use_local_sp...
function end_control_session (line 2416) | def end_control_session(speaker, action, args, soco_function, use_local_...
function wait_end_track (line 2427) | def wait_end_track(speaker, action, args, soco_function, use_local_speak...
function get_uri (line 2504) | def get_uri(speaker, action, args, soco_function, use_local_speaker_list):
function get_channel (line 2511) | def get_channel(speaker, action, args, soco_function, use_local_speaker_...
function _is_queue_position (line 2517) | def _is_queue_position(arg):
function add_sharelink_to_queue (line 2527) | def add_sharelink_to_queue(
function play_sharelink (line 2567) | def play_sharelink(speaker, action, args, soco_function, use_local_speak...
function reboot_count (line 2605) | def reboot_count(speaker, action, args, soco_function, use_local_speaker...
function switch_to_tv (line 2611) | def switch_to_tv(speaker, action, args, soco_function, use_local_speaker...
function audio_format (line 2621) | def audio_format(speaker, action, args, soco_function, use_local_speaker...
function mic_enabled (line 2635) | def mic_enabled(
function tv_audio_delay (line 2651) | def tv_audio_delay(speaker, action, args, soco_function, use_local_speak...
function group_volume_equalise (line 2668) | def group_volume_equalise(speaker, action, args, soco_function, use_loca...
function ungroup_all_in_group (line 2687) | def ungroup_all_in_group(speaker, action, args, soco_function, use_local...
function sub_gain (line 2701) | def sub_gain(speaker, action, args, soco_function, use_local_speaker_list):
function set_queue_position (line 2720) | def set_queue_position(speaker, action, args, soco_function, use_local_s...
function surround_volume (line 2740) | def surround_volume(speaker, action, args, soco_function, use_local_spea...
function process_wait_action (line 2761) | def process_wait_action(speaker, action, args, soco_function, use_local_...
function process_action (line 2768) | def process_action(speaker, action, args, use_local_speaker_list=False) ...
class SonosFunction (line 2787) | class SonosFunction:
method __init__ (line 2790) | def __init__(self, function, soco_function=None, switch_to_coordinator...
method processing_function (line 2796) | def processing_function(self):
method soco_function (line 2800) | def soco_function(self):
method switch_to_coordinator (line 2804) | def switch_to_coordinator(self):
function get_actions (line 2808) | def get_actions(
function list_actions (line 2830) | def list_actions(
FILE: soco_cli/alarms.py
function list_alarms (line 29) | def list_alarms(speaker, action, args, soco_function, use_local_speaker_...
function list_alarms_spec (line 95) | def list_alarms_spec(speaker, action, args, soco_function, use_local_spe...
function remove_alarms (line 154) | def remove_alarms(speaker, action, args, soco_function, use_local_speake...
function add_alarm (line 193) | def add_alarm(speaker, action, args, soco_function, use_local_speaker_li...
function modify_alarm (line 208) | def modify_alarm(speaker, action, args, soco_function, use_local_speaker...
function copy_alarm (line 245) | def copy_alarm(speaker, action, args, soco_function, use_local_speaker_l...
function move_alarm (line 251) | def move_alarm(speaker, action, args, soco_function, use_local_speaker_l...
function move_or_copy_alarm (line 256) | def move_or_copy_alarm(speaker, alarm_id, copy=True):
function enable_alarms (line 285) | def enable_alarms(speaker, action, args, soco_function, use_local_speake...
function disable_alarms (line 290) | def disable_alarms(speaker, action, args, soco_function, use_local_speak...
function set_alarms (line 294) | def set_alarms(speaker, alarm_ids, enabled=True):
function snooze_alarm (line 339) | def snooze_alarm(speaker, action, args, soco_function, use_local_speaker...
function copy_modify_alarm (line 394) | def copy_modify_alarm(speaker, action, args, soco_function, use_local_sp...
function _modify_alarm_object (line 434) | def _modify_alarm_object(speaker: SoCo, alarm: Alarm, parms_string: str)...
function set_program_data (line 545) | def set_program_data(speaker: SoCo, alarm: Alarm, fav: str) -> bool:
FILE: soco_cli/aliases.py
class AliasManager (line 12) | class AliasManager:
method __init__ (line 13) | def __init__(self):
method create_alias (line 16) | def create_alias(
method action (line 32) | def action(self, alias_name: str) -> Union[str, None]:
method remove_alias (line 35) | def remove_alias(self, alias_name: str) -> bool:
method alias_names (line 45) | def alias_names(self) -> List[str]:
method save_aliases (line 48) | def save_aliases(self) -> None:
method load_aliases (line 59) | def load_aliases(self) -> None:
method print_aliases (line 67) | def print_aliases(self) -> None:
method save_aliases_to_file (line 74) | def save_aliases_to_file(self, filename: str) -> bool:
method load_aliases_from_file (line 83) | def load_aliases_from_file(self, filename: str) -> bool:
method _aliases_to_text (line 101) | def _aliases_to_text(self, raw: bool = False) -> str:
FILE: soco_cli/api.py
function run_command (line 28) | def run_command(
function set_log_level (line 142) | def set_log_level(log_level: str = "None") -> None:
function handle_sigint (line 151) | def handle_sigint() -> None:
function rescan_speakers (line 156) | def rescan_speakers(timeout: float = None) -> None:
function rediscover_speakers (line 162) | def rediscover_speakers() -> None:
function get_all_speakers (line 168) | def get_all_speakers(use_scan: bool = False) -> list:
function get_all_speaker_names (line 174) | def get_all_speaker_names(use_scan: bool = False) -> list:
function get_soco_object (line 180) | def get_soco_object(
function _get_soco_object (line 210) | def _get_soco_object(speaker_name: str, use_local_speaker_list: bool = F...
function _check_for_speaker_cache (line 221) | def _check_for_speaker_cache() -> None:
function _setup_local_speaker_list (line 230) | def _setup_local_speaker_list() -> None:
FILE: soco_cli/check_for_update.py
function get_latest_version (line 15) | def get_latest_version() -> Union[str, None]:
function print_update_status (line 41) | def print_update_status() -> bool:
function update_available (line 52) | def update_available() -> bool:
FILE: soco_cli/cmd_parser.py
class CLIParser (line 4) | class CLIParser:
method __init__ (line 5) | def __init__(self):
method parse (line 10) | def parse(self, args):
method get_sequences (line 54) | def get_sequences(self):
FILE: soco_cli/http_api.py
class ActiveAsyncOps (line 46) | class ActiveAsyncOps:
method __init__ (line 52) | def __init__(self):
method add_async_pid (line 55) | def add_async_pid(self, speaker_ip: str, pid: int):
method get_async_pid (line 58) | def get_async_pid(self, speaker_ip) -> Optional[int]:
method remove_async_pid (line 61) | def remove_async_pid(self, speaker_ip) -> Optional[int]:
method stop_async_process (line 67) | def stop_async_process(self, speaker_ip: str) -> Optional[int]:
function command_core (line 99) | def command_core(
function root (line 166) | def root() -> Dict:
function speakers (line 171) | def speakers() -> Dict:
function rediscover (line 181) | def rediscover() -> Dict:
function list_audio_files (line 195) | def list_audio_files(directory: str) -> List[str]:
function macros (line 209) | def macros() -> Dict:
function macros_list (line 214) | def macros_list() -> Dict:
function macros_reload (line 219) | def macros_reload() -> Dict:
function run_macro (line 226) | def run_macro(macro_name: str) -> Dict:
function run_macro_1 (line 232) | def run_macro_1(macro_name: str, arg_1: str) -> Dict:
function run_macro_2 (line 238) | def run_macro_2(macro_name: str, arg_1: str, arg_2: str) -> Dict:
function run_macro_3 (line 244) | def run_macro_3(macro_name: str, arg_1: str, arg_2: str, arg_3: str) -> ...
function run_macro_4 (line 250) | def run_macro_4(
function run_macro_5 (line 258) | def run_macro_5(
function run_macro_6 (line 266) | def run_macro_6(
function run_macro_7 (line 284) | def run_macro_7(
function run_macro_8 (line 303) | def run_macro_8(
function run_macro_9 (line 323) | def run_macro_9(
function run_macro_10 (line 345) | def run_macro_10(
function run_macro_11 (line 378) | def run_macro_11(
function run_macro_12 (line 413) | def run_macro_12(
function action_0 (line 447) | def action_0(speaker: str, action: str) -> Dict:
function action_1 (line 452) | def action_1(speaker: str, action: str, arg_1: str) -> Dict:
function action_1_path (line 457) | def action_1_path(speaker: str, action: str, arg_1: str) -> Dict:
function action_2 (line 473) | def action_2(speaker: str, action: str, arg_1: str, arg_2: str) -> Dict:
function action_3 (line 478) | def action_3(speaker: str, action: str, arg_1: str, arg_2: str, arg_3: s...
function args_processor (line 482) | def args_processor() -> None:
function main (line 544) | def main() -> None:
function _process_macro (line 579) | def _process_macro(macro_name: str, *args) -> Tuple[str, str]:
function _lookup_macro (line 634) | def _lookup_macro(macro_name: str) -> str:
function _substitute_variables (line 639) | def _substitute_variables(macro: str, args: Tuple) -> str:
function _substitute_speaker_ips (line 715) | def _substitute_speaker_ips(macro: str, use_local: bool = False) -> str:
function _load_macros (line 737) | def _load_macros(macros: dict, filename: str) -> bool:
function _quote_if_contains_space (line 764) | def _quote_if_contains_space(text: str) -> str:
FILE: soco_cli/interactive.py
function interactive_loop (line 94) | def interactive_loop(
function _completer (line 554) | def _completer(text, context):
function _show_actions (line 560) | def _show_actions():
function _set_actions_and_commands_list (line 576) | def _set_actions_and_commands_list(use_local_speaker_list=False):
function _get_actions_and_commands (line 593) | def _get_actions_and_commands():
function _interactive_help (line 597) | def _interactive_help():
function _get_speaker_names (line 653) | def _get_speaker_names(use_local_speaker_list=False):
function _print_speaker_list (line 669) | def _print_speaker_list(use_local_speaker_list=False):
function _save_readline_history (line 679) | def _save_readline_history():
function _get_readline_history (line 685) | def _get_readline_history():
function _restore_quotes (line 691) | def _restore_quotes(command):
class AliasProcessor (line 697) | class AliasProcessor:
method __init__ (line 701) | def __init__(self):
method process (line 708) | def process(self, command, am, command_list):
method _remove_added_commands (line 803) | def _remove_added_commands(self):
function _rescan (line 809) | def _rescan(use_local_speaker_list=False, max_scan=False):
function _exec (line 826) | def _exec(command_args: List[str]) -> bool:
function _exec_action (line 859) | def _exec_action(speaker_ip: str, action: str, args: List[str]) -> bool:
function _exec_command_line (line 879) | def _exec_command_line(command_line: str) -> None:
function _loop_in_command_sequences (line 895) | def _loop_in_command_sequences(command_sequences: RewindableList) -> bool:
function _exec_loop (line 907) | def _exec_loop(
FILE: soco_cli/keystroke_capture.py
function get_keystroke (line 17) | def get_keystroke() -> str:
FILE: soco_cli/m3u_parser.py
class Track (line 13) | class Track:
method __init__ (line 14) | def __init__(
function parse_m3u (line 29) | def parse_m3u(m3u_file: str) -> List[Track]:
FILE: soco_cli/match_speaker_names.py
function speaker_name_matches (line 6) | def speaker_name_matches(name_supplied, name_stored):
FILE: soco_cli/play_local_file.py
class ThreadedHTTPServer (line 36) | class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
class MyHTTPHandler (line 44) | class MyHTTPHandler(RangeRequestHandler):
method __init__ (line 48) | def __init__(self, *args, filename=None, speaker_ips=None, **kwargs):
method __init__ (line 55) | def __init__(
method do_GET (line 66) | def do_GET(self) -> None:
method log_message (line 91) | def log_message(self, format, *args) -> None:
function http_server (line 96) | def http_server(
function get_server_ip (line 127) | def get_server_ip(speaker: SoCo) -> Optional[str]:
function wait_until_stopped (line 189) | def wait_until_stopped(speaker: SoCo, uri: str, end_on_pause: bool):
function is_supported_type (line 234) | def is_supported_type(filename: str) -> bool:
function play_local_file (line 242) | def play_local_file(speaker: SoCo, pathname: str, end_on_pause: bool = F...
FILE: soco_cli/play_local_file_lists.py
function interaction_manager (line 19) | def interaction_manager(speaker_ip: str) -> None:
function play_file_list (line 66) | def play_file_list(speaker: SoCo, tracks: List[str], options: str = "") ...
function play_m3u_file (line 130) | def play_m3u_file(speaker: SoCo, m3u_file: str, options: str = "") -> bool:
function play_directory_files (line 151) | def play_directory_files(speaker: SoCo, directory: str, options: str = "...
FILE: soco_cli/sonos.py
function main (line 49) | def main():
FILE: soco_cli/sonos_discover.py
function main (line 18) | def main():
FILE: soco_cli/speaker_info.py
function print_speaker_table (line 24) | def print_speaker_table(device):
FILE: soco_cli/speakers.py
class Speakers (line 29) | class Speakers:
method __init__ (line 34) | def __init__(
method remove_deprecated_pickle_files (line 56) | def remove_deprecated_pickle_files(self):
method speaker_cache_loaded (line 66) | def speaker_cache_loaded(self):
method speaker_cache_file_exists (line 70) | def speaker_cache_file_exists(self):
method speakers (line 74) | def speakers(self):
method save_directory (line 78) | def save_directory(self):
method save_directory (line 82) | def save_directory(self, directory):
method save_file (line 86) | def save_file(self):
method save_file (line 90) | def save_file(self, file):
method save_pathname (line 94) | def save_pathname(self):
method network_threads (line 98) | def network_threads(self):
method network_threads (line 102) | def network_threads(self, threads):
method network_timeout (line 106) | def network_timeout(self):
method network_timeout (line 110) | def network_timeout(self, timeout):
method min_netmask (line 114) | def min_netmask(self):
method min_netmask (line 118) | def min_netmask(self, min_netmask):
method subnets (line 122) | def subnets(self):
method subnets (line 126) | def subnets(self, subnets, check_valid=True):
method set_subnets_no_check (line 144) | def set_subnets_no_check(self, subnets):
method save (line 151) | def save(self):
method load (line 161) | def load(self):
method clear (line 172) | def clear(self):
method remove_save_file (line 176) | def remove_save_file(self):
method rename (line 181) | def rename(self, old_name, new_name):
method is_ipv4_address (line 198) | def is_ipv4_address(ip_address):
method get_sonos_device_data (line 207) | def get_sonos_device_data(ip_addr):
method discover (line 228) | def discover(self):
method find (line 252) | def find(self, speaker_name, require_visible=True):
method get_all_speakers (line 283) | def get_all_speakers(self):
method get_all_speaker_names (line 291) | def get_all_speaker_names(self, include_invisible=False):
method print (line 299) | def print(self):
FILE: soco_cli/track_follow.py
function track_follow (line 10) | def track_follow(
FILE: soco_cli/utils.py
function event_unsubscribe (line 25) | def event_unsubscribe(sub):
function set_interactive (line 43) | def set_interactive():
function set_api (line 48) | def set_api():
function set_single_keystroke (line 53) | def set_single_keystroke(sk):
function error_report (line 59) | def error_report(msg):
function parameter_type_error (line 68) | def parameter_type_error(action, required_params):
function parameter_number_error (line 73) | def parameter_number_error(action, parameter_number):
function zero_parameters (line 79) | def zero_parameters(f):
function one_parameter (line 90) | def one_parameter(f):
function zero_or_one_parameter (line 100) | def zero_or_one_parameter(f):
function one_or_two_parameters (line 110) | def one_or_two_parameters(f):
function two_parameters (line 120) | def two_parameters(f):
function zero_one_or_two_parameters (line 130) | def zero_one_or_two_parameters(f):
function one_or_more_parameters (line 140) | def one_or_more_parameters(f):
function seconds_until (line 151) | def seconds_until(time_str):
function create_time_from_str (line 166) | def create_time_from_str(time_str):
function convert_to_seconds (line 186) | def convert_to_seconds(time_str):
function convert_true_false (line 218) | def convert_true_false(true_or_false, conversion="YesOrNo"):
function version (line 226) | def version():
function docs (line 233) | def docs():
function logo (line 242) | def logo():
function set_suspend_sighandling (line 257) | def set_suspend_sighandling(suspend=True):
function set_ctrl_c_interrupted (line 267) | def set_ctrl_c_interrupted(value=True):
function get_ctrl_c_interrupted (line 272) | def get_ctrl_c_interrupted():
function set_speaker_playing_local_file (line 280) | def set_speaker_playing_local_file(speaker):
function sig_handler (line 291) | def sig_handler(signal_received, frame):
class RewindableList (line 344) | class RewindableList(Sequence):
method __init__ (line 349) | def __init__(self, items=[]):
method __iter__ (line 353) | def __iter__(self):
method __getitem__ (line 357) | def __getitem__(self, item):
method __len__ (line 360) | def __len__(self):
method __next__ (line 363) | def __next__(self):
method rewind (line 370) | def rewind(self):
method rewind_to (line 373) | def rewind_to(self, index):
method __str__ (line 381) | def __str__(self):
method index (line 384) | def index(self):
method insert (line 387) | def insert(self, index, element):
method pop_next (line 392) | def pop_next(self):
function configure_logging (line 400) | def configure_logging(log_level: str) -> None:
function set_speaker_list (line 429) | def set_speaker_list(s):
class SpeakerCache (line 434) | class SpeakerCache:
method __init__ (line 435) | def __init__(self, max_threads=256, scan_timeout=0.1, min_netmask=24):
method exists (line 445) | def exists(self):
method cache_speakers (line 448) | def cache_speakers(self, speakers):
method discover (line 453) | def discover(self, reset=False):
method scan (line 469) | def scan(self, reset=False, scan_timeout_override=None):
method add (line 493) | def add(self, speaker):
method find_indirect (line 497) | def find_indirect(self, name):
method find (line 517) | def find(self, name):
method get_all_speakers (line 540) | def get_all_speakers(self, use_scan=False):
method get_all_speaker_names (line 547) | def get_all_speaker_names(self, use_scan=False):
method rename_speaker (line 556) | def rename_speaker(self, old_name, new_name):
function create_speaker_cache (line 571) | def create_speaker_cache(max_threads=256, scan_timeout=1.0, min_netmask=...
function speaker_cache (line 578) | def speaker_cache():
function local_speaker_list (line 583) | def local_speaker_list():
function get_speaker (line 588) | def get_speaker(name, local=False):
function get_right_hand_speaker (line 623) | def get_right_hand_speaker(left_hand_speaker):
function rename_speaker_in_cache (line 649) | def rename_speaker_in_cache(old_name, new_name, use_local_speaker_list=T...
function configure_common_args (line 656) | def configure_common_args(parser):
function check_args (line 717) | def check_args(args):
function save_search (line 738) | def save_search(result):
function read_search (line 747) | def read_search():
function save_queue_insertion_position (line 762) | def save_queue_insertion_position(queue_position: int):
function get_queue_insertion_position (line 771) | def get_queue_insertion_position() -> int:
function save_readline_history (line 791) | def save_readline_history():
function get_readline_history (line 801) | def get_readline_history():
function pretty_print_values (line 814) | def pretty_print_values(items, indent=2, separator=":", spacing=3, sort_...
function playback_state (line 842) | def playback_state(state):
function _confirm_soco_cli_dir (line 867) | def _confirm_soco_cli_dir() -> bool:
function remember_event_sub (line 880) | def remember_event_sub(sub):
function forget_event_sub (line 886) | def forget_event_sub(sub):
function unsub_all_remembered_event_subs (line 895) | def unsub_all_remembered_event_subs():
function find_by_name (line 905) | def find_by_name(items, name):
function queue_is_empty (line 922) | def queue_is_empty(speaker):
function create_list_of_items_from_range (line 930) | def create_list_of_items_from_range(range_definition: str, upper_limit: ...
FILE: soco_cli/wait_actions.py
function process_wait (line 10) | def process_wait(sequence: List):
FILE: tests/test_action_processor.py
function api_mode (line 48) | def api_mode():
function _make_speaker (line 60) | def _make_speaker(**kwargs):
function _make_track (line 67) | def _make_track(title="Track", creator="Artist", album="Album", item_cla...
function _make_favourite (line 79) | def _make_favourite(title, uri="http://example.com/stream", meta="<meta/...
function _call (line 87) | def _call(func, speaker, args, action=None, soco_function="", use_local=...
class TestIsQueuePosition (line 96) | class TestIsQueuePosition:
method test_integer_strings (line 97) | def test_integer_strings(self):
method test_named_keywords_case_insensitive (line 103) | def test_named_keywords_case_insensitive(self):
method test_invalid_strings (line 111) | def test_invalid_strings(self):
method test_close_but_not_valid (line 118) | def test_close_but_not_valid(self):
class TestFilterTrackInfo (line 129) | class TestFilterTrackInfo:
method test_capitalises_keys (line 130) | def test_capitalises_keys(self):
method test_excludes_specified_fields (line 135) | def test_excludes_specified_fields(self):
method test_preserves_values (line 143) | def test_preserves_values(self):
method test_empty_input (line 147) | def test_empty_input(self):
method test_all_excluded (line 150) | def test_all_excluded(self):
method test_output_order_follows_sorted_input_keys (line 156) | def test_output_order_follows_sorted_input_keys(self):
method test_capitalize_only_first_char (line 160) | def test_capitalize_only_first_char(self):
class TestPrintListHeader (line 171) | class TestPrintListHeader:
method test_output_format (line 172) | def test_output_format(self, capsys):
method test_unicode_name (line 181) | def test_unicode_name(self, capsys):
class TestGetCurrentQueuePosition (line 192) | class TestGetCurrentQueuePosition:
method _make_speaker (line 193) | def _make_speaker(self, playlist_position="3", title="A Track", state=...
method test_playing_no_tracks_returns_qp_and_true (line 204) | def test_playing_no_tracks_returns_qp_and_true(self):
method test_paused_returns_qp_and_false (line 210) | def test_paused_returns_qp_and_false(self):
method test_playing_with_matching_track_title (line 216) | def test_playing_with_matching_track_title(self):
method test_playing_with_mismatched_track_title_resets_to_1 (line 226) | def test_playing_with_mismatched_track_title_resets_to_1(self):
method test_track_info_exception_returns_zero (line 236) | def test_track_info_exception_returns_zero(self):
method test_transport_info_exception_returns_false (line 246) | def test_transport_info_exception_returns_false(self):
method test_index_error_on_tracks_access_resets_to_1 (line 257) | def test_index_error_on_tracks_access_resets_to_1(self):
class TestPrintTracks (line 271) | class TestPrintTracks:
method test_prints_track_info (line 272) | def test_prints_track_info(self, capsys):
method test_missing_attributes_are_skipped (line 280) | def test_missing_attributes_are_skipped(self, capsys):
method test_podcast_track_renames_title_field (line 292) | def test_podcast_track_renames_title_field(self, capsys):
method test_numbered_sequentially (line 301) | def test_numbered_sequentially(self, capsys):
method test_current_track_prefix_playing (line 309) | def test_current_track_prefix_playing(self, capsys):
method test_current_track_prefix_paused (line 320) | def test_current_track_prefix_paused(self, capsys):
method test_non_current_track_has_plain_prefix (line 331) | def test_non_current_track_has_plain_prefix(self, capsys):
method test_single_track_mode_uses_given_number (line 342) | def test_single_track_mode_uses_given_number(self, capsys):
method test_returns_true (line 348) | def test_returns_true(self):
class TestPrintAlbums (line 357) | class TestPrintAlbums:
method _make_album (line 358) | def _make_album(self, title, creator="Artist"):
method test_prints_albums (line 364) | def test_prints_albums(self, capsys):
method test_omit_first (line 371) | def test_omit_first(self, capsys):
method test_numbering_with_omit_first_restarts_at_1 (line 378) | def test_numbering_with_omit_first_restarts_at_1(self, capsys):
method test_missing_creator_defaults_to_empty (line 387) | def test_missing_creator_defaults_to_empty(self, capsys):
method test_returns_true (line 395) | def test_returns_true(self):
class TestOnOffAction (line 404) | class TestOnOffAction:
method test_get_state_on (line 405) | def test_get_state_on(self, capsys):
method test_get_state_off (line 411) | def test_get_state_off(self, capsys):
method test_set_on (line 417) | def test_set_on(self):
method test_set_off (line 423) | def test_set_off(self):
method test_set_case_insensitive (line 429) | def test_set_case_insensitive(self):
method test_invalid_arg_returns_false (line 434) | def test_invalid_arg_returns_false(self, capsys):
method test_group_mute_switches_to_group (line 439) | def test_group_mute_switches_to_group(self):
class TestVolumeActions (line 451) | class TestVolumeActions:
method test_get_volume (line 452) | def test_get_volume(self, capsys):
method test_set_volume (line 457) | def test_set_volume(self):
method test_set_volume_boundary_values (line 463) | def test_set_volume_boundary_values(self):
method test_set_volume_out_of_range (line 471) | def test_set_volume_out_of_range(self, capsys):
method test_set_volume_invalid_arg (line 477) | def test_set_volume_invalid_arg(self, capsys):
method test_group_volume_uses_group (line 482) | def test_group_volume_uses_group(self, capsys):
method test_ramp_to_volume (line 488) | def test_ramp_to_volume(self):
class TestShuffle (line 504) | class TestShuffle:
method test_get_shuffle_on (line 505) | def test_get_shuffle_on(self, capsys):
method test_get_shuffle_off (line 510) | def test_get_shuffle_off(self, capsys):
method test_set_shuffle_on (line 515) | def test_set_shuffle_on(self):
method test_set_shuffle_off (line 520) | def test_set_shuffle_off(self):
method test_set_shuffle_case_insensitive (line 525) | def test_set_shuffle_case_insensitive(self):
method test_invalid_arg_returns_false (line 530) | def test_invalid_arg_returns_false(self, capsys):
class TestRepeat (line 541) | class TestRepeat:
method test_get_repeat_all (line 542) | def test_get_repeat_all(self, capsys):
method test_get_repeat_off (line 547) | def test_get_repeat_off(self, capsys):
method test_get_repeat_one (line 552) | def test_get_repeat_one(self, capsys):
method test_set_off (line 557) | def test_set_off(self):
method test_set_none_alias (line 562) | def test_set_none_alias(self):
method test_set_one (line 567) | def test_set_one(self):
method test_set_all (line 572) | def test_set_all(self):
method test_set_case_insensitive (line 577) | def test_set_case_insensitive(self):
method test_invalid_arg_returns_false (line 582) | def test_invalid_arg_returns_false(self, capsys):
class TestPlaybackMode (line 593) | class TestPlaybackMode:
method test_get_mode (line 594) | def test_get_mode(self, capsys):
method test_set_valid_modes (line 599) | def test_set_valid_modes(self):
method test_set_mode_case_insensitive (line 613) | def test_set_mode_case_insensitive(self):
method test_invalid_mode_still_returns_true (line 618) | def test_invalid_mode_still_returns_true(self, capsys):
class TestSleepTimer (line 636) | class TestSleepTimer:
method test_get_no_timer (line 637) | def test_get_no_timer(self, capsys):
method test_get_timer_active (line 643) | def test_get_timer_active(self, capsys):
method test_cancel_timer (line 650) | def test_cancel_timer(self):
method test_cancel_alias (line 656) | def test_cancel_alias(self):
method test_set_timer_seconds (line 661) | def test_set_timer_seconds(self):
method test_set_timer_minutes (line 667) | def test_set_timer_minutes(self):
method test_set_timer_invalid_format (line 673) | def test_set_timer_invalid_format(self, capsys):
method test_set_timer_exceeds_max (line 678) | def test_set_timer_exceeds_max(self, capsys):
class TestSwitchToTv (line 689) | class TestSwitchToTv:
method test_soundbar_switches (line 690) | def test_soundbar_switches(self):
method test_non_soundbar_returns_false (line 696) | def test_non_soundbar_returns_false(self, capsys):
class TestAudioFormat (line 708) | class TestAudioFormat:
method test_soundbar_with_format (line 709) | def test_soundbar_with_format(self, capsys):
method test_soundbar_no_format (line 717) | def test_soundbar_no_format(self, capsys):
method test_non_soundbar_returns_false (line 723) | def test_non_soundbar_returns_false(self, capsys):
class TestMicEnabled (line 735) | class TestMicEnabled:
method test_mic_enabled_true (line 736) | def test_mic_enabled_true(self, capsys):
method test_mic_enabled_false (line 742) | def test_mic_enabled_false(self, capsys):
method test_mic_none_returns_false (line 748) | def test_mic_none_returns_false(self, capsys):
class TestTvAudioDelay (line 760) | class TestTvAudioDelay:
method test_non_soundbar_returns_false (line 761) | def test_non_soundbar_returns_false(self, capsys):
method test_get_delay (line 766) | def test_get_delay(self, capsys):
method test_set_delay (line 771) | def test_set_delay(self):
method test_set_delay_invalid (line 777) | def test_set_delay_invalid(self, capsys):
class TestSetQueuePosition (line 789) | class TestSetQueuePosition:
method test_valid_position (line 790) | def test_valid_position(self):
method test_boundary_first (line 797) | def test_boundary_first(self):
method test_boundary_last (line 803) | def test_boundary_last(self):
method test_out_of_range_low (line 809) | def test_out_of_range_low(self, capsys):
method test_out_of_range_high (line 815) | def test_out_of_range_high(self, capsys):
method test_non_integer_returns_false (line 821) | def test_non_integer_returns_false(self, capsys):
class TestSurroundVolume (line 833) | class TestSurroundVolume:
method test_no_surround_returns_false (line 834) | def test_no_surround_returns_false(self, capsys):
method test_get_gain (line 841) | def test_get_gain(self, capsys):
method test_set_gain (line 848) | def test_set_gain(self):
method test_set_gain_boundary (line 855) | def test_set_gain_boundary(self):
method test_set_gain_out_of_range (line 864) | def test_set_gain_out_of_range(self, capsys):
method test_set_gain_invalid (line 871) | def test_set_gain_invalid(self, capsys):
class TestPlayUri (line 883) | class TestPlayUri:
method test_success_on_first_attempt (line 884) | def test_success_on_first_attempt(self):
method test_falls_back_to_force_radio (line 892) | def test_falls_back_to_force_radio(self):
method test_both_attempts_fail_returns_false (line 900) | def test_both_attempts_fail_returns_false(self, capsys):
method test_passes_title (line 907) | def test_passes_title(self):
method test_title_defaults_to_empty_string (line 912) | def test_title_defaults_to_empty_string(self):
class TestPlayFavouriteCore (line 923) | class TestPlayFavouriteCore:
method _setup_speaker (line 924) | def _setup_speaker(self, favs):
method test_found_by_name_play_uri_succeeds (line 929) | def test_found_by_name_play_uri_succeeds(self):
method test_found_by_fuzzy_name (line 937) | def test_found_by_fuzzy_name(self):
method test_not_found_returns_false_with_message (line 943) | def test_not_found_returns_false_with_message(self):
method test_play_uri_fails_falls_back_to_queue (line 949) | def test_play_uri_fails_falls_back_to_queue(self):
method test_both_strategies_fail_returns_error (line 959) | def test_both_strategies_fail_returns_error(self):
method test_by_number_valid (line 969) | def test_by_number_valid(self):
method test_by_number_out_of_range (line 982) | def test_by_number_out_of_range(self):
method test_by_number_zero_is_out_of_range (line 989) | def test_by_number_zero_is_out_of_range(self):
method test_by_number_non_integer (line 995) | def test_by_number_non_integer(self):
method test_by_number_sorted_by_title (line 1001) | def test_by_number_sorted_by_title(self):
class TestAddFavouriteToQueue (line 1020) | class TestAddFavouriteToQueue:
method _setup (line 1021) | def _setup(self, fav_titles, queue_size=5):
method test_found_appends_to_end (line 1028) | def test_found_appends_to_end(self, capsys):
method test_found_with_position (line 1038) | def test_found_with_position(self):
method test_not_found_returns_false (line 1051) | def test_not_found_returns_false(self, capsys):
method test_add_to_queue_exception_returns_false (line 1059) | def test_add_to_queue_exception_returns_false(self, capsys):
class TestListQueue (line 1075) | class TestListQueue:
method test_empty_queue_returns_true (line 1076) | def test_empty_queue_returns_true(self):
method test_full_queue_calls_print_tracks (line 1082) | def test_full_queue_calls_print_tracks(self):
method test_single_track_by_number (line 1090) | def test_single_track_by_number(self):
method test_track_number_out_of_range (line 1101) | def test_track_number_out_of_range(self, capsys):
method test_track_number_zero_out_of_range (line 1108) | def test_track_number_zero_out_of_range(self, capsys):
method test_non_integer_track_number (line 1114) | def test_non_integer_track_number(self, capsys):
class TestProcessAction (line 1126) | class TestProcessAction:
method test_unknown_action_returns_false (line 1127) | def test_unknown_action_returns_false(self):
method test_known_action_is_dispatched (line 1132) | def test_known_action_is_dispatched(self):
method test_switch_to_coordinator_when_not_coordinator (line 1145) | def test_switch_to_coordinator_when_not_coordinator(self):
method test_switch_to_coordinator_when_already_coordinator (line 1160) | def test_switch_to_coordinator_when_already_coordinator(self):
FILE: tests/test_aliases.py
class TestCreateAndRetrieve (line 16) | class TestCreateAndRetrieve:
method test_create_new_alias (line 17) | def test_create_new_alias(self):
method test_update_existing_alias (line 24) | def test_update_existing_alias(self):
method test_none_action_removes_alias (line 32) | def test_none_action_removes_alias(self):
method test_empty_string_action_removes_alias (line 39) | def test_empty_string_action_removes_alias(self):
method test_alias_name_whitespace_stripped (line 45) | def test_alias_name_whitespace_stripped(self):
method test_action_returns_none_for_unknown_alias (line 50) | def test_action_returns_none_for_unknown_alias(self):
method test_action_value_whitespace_stripped (line 54) | def test_action_value_whitespace_stripped(self):
class TestRemoveAlias (line 60) | class TestRemoveAlias:
method test_remove_existing_alias (line 61) | def test_remove_existing_alias(self):
method test_remove_nonexistent_alias_returns_false (line 68) | def test_remove_nonexistent_alias_returns_false(self):
method test_remove_name_whitespace_stripped (line 73) | def test_remove_name_whitespace_stripped(self):
class TestAliasNames (line 80) | class TestAliasNames:
method test_empty_manager_returns_empty_list (line 81) | def test_empty_manager_returns_empty_list(self):
method test_returns_all_alias_names (line 85) | def test_returns_all_alias_names(self):
method test_removed_alias_not_in_names (line 92) | def test_removed_alias_not_in_names(self):
class TestAliasesToText (line 104) | class TestAliasesToText:
method test_raw_format (line 105) | def test_raw_format(self):
method test_pretty_format_indented (line 113) | def test_pretty_format_indented(self):
method test_multiple_aliases_sorted (line 120) | def test_multiple_aliases_sorted(self):
class TestFileRoundTrip (line 133) | class TestFileRoundTrip:
method test_save_and_load_round_trip (line 134) | def test_save_and_load_round_trip(self):
method test_saved_file_has_header_comment (line 154) | def test_saved_file_has_header_comment(self):
method test_save_to_nonexistent_path_returns_false (line 167) | def test_save_to_nonexistent_path_returns_false(self):
method test_load_comment_lines_ignored (line 173) | def test_load_comment_lines_ignored(self):
method test_load_malformed_line_skipped (line 188) | def test_load_malformed_line_skipped(self, capsys):
method test_load_from_nonexistent_file_returns_false (line 203) | def test_load_from_nonexistent_file_returns_false(self):
method test_load_blank_lines_skipped (line 208) | def test_load_blank_lines_skipped(self):
class TestPrintAliases (line 227) | class TestPrintAliases:
method test_empty_manager_prints_message (line 228) | def test_empty_manager_prints_message(self, capsys):
method test_aliases_printed (line 233) | def test_aliases_printed(self, capsys):
FILE: tests/test_check_for_update.py
function api_mode (line 17) | def api_mode():
function _fake_urlopen (line 24) | def _fake_urlopen(lines):
class TestGetLatestVersion (line 29) | class TestGetLatestVersion:
method test_successful_fetch_returns_version (line 30) | def test_successful_fetch_returns_version(self):
method test_version_string_without_spaces (line 36) | def test_version_string_without_spaces(self):
method test_version_line_with_trailing_newline_stripped (line 42) | def test_version_line_with_trailing_newline_stripped(self):
method test_no_version_line_returns_none (line 48) | def test_no_version_line_returns_none(self):
method test_network_error_returns_none (line 54) | def test_network_error_returns_none(self, capsys):
class TestPrintUpdateStatus (line 63) | class TestPrintUpdateStatus:
method test_up_to_date (line 64) | def test_up_to_date(self, capsys):
method test_update_available (line 75) | def test_update_available(self, capsys):
method test_network_failure_returns_false (line 86) | def test_network_failure_returns_false(self):
class TestUpdateAvailable (line 92) | class TestUpdateAvailable:
method test_same_version_returns_false (line 93) | def test_same_version_returns_false(self):
method test_different_version_returns_true (line 102) | def test_different_version_returns_true(self):
method test_none_version_returns_true (line 111) | def test_none_version_returns_true(self):
FILE: tests/test_cli.py
function test_cli (line 27) | def test_cli(capsys):
function test_api (line 34) | def test_api():
FILE: tests/test_cmd_parser.py
class TestCLIParser (line 6) | class TestCLIParser:
method test_empty_args_produces_no_sequences (line 9) | def test_empty_args_produces_no_sequences(self):
method test_single_token (line 14) | def test_single_token(self):
method test_single_sequence_multiple_tokens (line 19) | def test_single_sequence_multiple_tokens(self):
method test_two_sequences_separated_by_colon (line 26) | def test_two_sequences_separated_by_colon(self):
method test_three_sequences (line 31) | def test_three_sequences(self):
method test_ordering_preserved (line 36) | def test_ordering_preserved(self):
method test_colon_at_start_produces_empty_first_sequence (line 43) | def test_colon_at_start_produces_empty_first_sequence(self):
method test_colon_at_end_has_no_trailing_sequence (line 48) | def test_colon_at_end_has_no_trailing_sequence(self):
method test_consecutive_colons_produce_empty_middle_sequence (line 55) | def test_consecutive_colons_produce_empty_middle_sequence(self):
method test_colon_only_produces_one_empty_sequence (line 60) | def test_colon_only_produces_one_empty_sequence(self):
method test_parse_can_be_called_multiple_times (line 65) | def test_parse_can_be_called_multiple_times(self):
FILE: tests/test_comprehensive.py
function api_mode (line 47) | def api_mode():
class TestConvertToSeconds (line 60) | class TestConvertToSeconds:
method test_hh_mm_ss (line 61) | def test_hh_mm_ss(self):
method test_hh_mm (line 67) | def test_hh_mm(self):
method test_seconds_suffix (line 72) | def test_seconds_suffix(self):
method test_minutes_suffix (line 77) | def test_minutes_suffix(self):
method test_hours_suffix (line 81) | def test_hours_suffix(self):
method test_plain_number_defaults_to_seconds (line 85) | def test_plain_number_defaults_to_seconds(self):
method test_uppercase_suffix_accepted (line 89) | def test_uppercase_suffix_accepted(self):
method test_invalid_raises_value_error (line 95) | def test_invalid_raises_value_error(self):
class TestCreateTimeFromStr (line 109) | class TestCreateTimeFromStr:
method test_hh_mm (line 110) | def test_hh_mm(self):
method test_hh_mm_ss (line 114) | def test_hh_mm_ss(self):
method test_midnight (line 118) | def test_midnight(self):
method test_no_colon_raises (line 122) | def test_no_colon_raises(self):
method test_out_of_range_hour_raises (line 126) | def test_out_of_range_hour_raises(self):
method test_out_of_range_minute_raises (line 130) | def test_out_of_range_minute_raises(self):
method test_out_of_range_second_raises (line 134) | def test_out_of_range_second_raises(self):
method test_too_many_parts_raises (line 138) | def test_too_many_parts_raises(self):
method test_too_few_parts_raises (line 142) | def test_too_few_parts_raises(self):
class TestConvertTrueFalse (line 152) | class TestConvertTrueFalse:
method test_yes_or_no_true (line 153) | def test_yes_or_no_true(self):
method test_yes_or_no_false (line 156) | def test_yes_or_no_false(self):
method test_on_or_off_true (line 159) | def test_on_or_off_true(self):
method test_on_or_off_false (line 162) | def test_on_or_off_false(self):
method test_unknown_conversion_returns_none (line 165) | def test_unknown_conversion_returns_none(self):
class TestPlaybackState (line 175) | class TestPlaybackState:
method test_stopped (line 176) | def test_stopped(self):
method test_paused (line 179) | def test_paused(self):
method test_playing (line 182) | def test_playing(self):
method test_transitioning (line 185) | def test_transitioning(self):
method test_unknown_state (line 188) | def test_unknown_state(self):
class TestCreateListOfItemsFromRange (line 198) | class TestCreateListOfItemsFromRange:
method test_single_item (line 199) | def test_single_item(self):
method test_comma_separated (line 202) | def test_comma_separated(self):
method test_simple_range (line 205) | def test_simple_range(self):
method test_all_keyword (line 208) | def test_all_keyword(self):
method test_all_keyword_case_insensitive (line 211) | def test_all_keyword_case_insensitive(self):
method test_mixed_range_and_singles (line 214) | def test_mixed_range_and_singles(self):
method test_reversed_range_is_sorted (line 218) | def test_reversed_range_is_sorted(self):
method test_duplicates_deduplicated (line 221) | def test_duplicates_deduplicated(self):
method test_result_is_sorted (line 224) | def test_result_is_sorted(self):
method test_item_out_of_range_raises (line 228) | def test_item_out_of_range_raises(self):
method test_zero_raises (line 232) | def test_zero_raises(self):
method test_range_exceeds_limit_raises (line 236) | def test_range_exceeds_limit_raises(self):
method test_malformed_range_raises (line 240) | def test_malformed_range_raises(self):
method test_at_upper_limit (line 244) | def test_at_upper_limit(self):
class TestPrettyPrintValues (line 253) | class TestPrettyPrintValues:
method test_basic_output (line 254) | def test_basic_output(self, capsys):
method test_empty_dict_produces_no_output (line 260) | def test_empty_dict_produces_no_output(self, capsys):
method test_alignment (line 265) | def test_alignment(self, capsys):
method test_custom_separator (line 274) | def test_custom_separator(self, capsys):
method test_sort_by_key (line 279) | def test_sort_by_key(self, capsys):
class TestRewindableList (line 292) | class TestRewindableList:
method test_basic_iteration (line 293) | def test_basic_iteration(self):
method test_rewind_restarts_iteration (line 297) | def test_rewind_restarts_iteration(self):
method test_rewind_mid_iteration (line 303) | def test_rewind_mid_iteration(self):
method test_rewind_to_valid_index (line 310) | def test_rewind_to_valid_index(self):
method test_rewind_to_zero_on_empty (line 318) | def test_rewind_to_zero_on_empty(self):
method test_rewind_to_out_of_bounds_raises (line 323) | def test_rewind_to_out_of_bounds_raises(self):
method test_rewind_to_negative_raises (line 328) | def test_rewind_to_negative_raises(self):
method test_len (line 333) | def test_len(self):
method test_getitem (line 337) | def test_getitem(self):
method test_index_tracks_position (line 342) | def test_index_tracks_position(self):
method test_str (line 351) | def test_str(self):
method test_stop_iteration (line 355) | def test_stop_iteration(self):
method test_insert_before_current_index_adjusts_index (line 362) | def test_insert_before_current_index_adjusts_index(self):
method test_insert_after_current_index_does_not_adjust (line 370) | def test_insert_after_current_index_does_not_adjust(self):
method test_pop_next_removes_first_item (line 377) | def test_pop_next_removes_first_item(self):
method test_pop_next_adjusts_index (line 384) | def test_pop_next_adjusts_index(self):
class TestCLIParser (line 397) | class TestCLIParser:
method test_single_sequence_no_separator (line 398) | def test_single_sequence_no_separator(self):
method test_two_sequences (line 403) | def test_two_sequences(self):
method test_three_sequences (line 408) | def test_three_sequences(self):
method test_empty_args (line 413) | def test_empty_args(self):
method test_trailing_separator_creates_empty_sequence_excluded (line 418) | def test_trailing_separator_creates_empty_sequence_excluded(self):
method test_single_separator_only (line 425) | def test_single_separator_only(self):
method test_colon_in_value_not_treated_as_separator (line 431) | def test_colon_in_value_not_treated_as_separator(self):
class TestSpeakerNameMatches (line 443) | class TestSpeakerNameMatches:
method test_exact_match (line 444) | def test_exact_match(self):
method test_case_insensitive_is_exact (line 449) | def test_case_insensitive_is_exact(self):
method test_apostrophe_normalisation_is_exact (line 454) | def test_apostrophe_normalisation_is_exact(self):
method test_partial_start_of_name (line 460) | def test_partial_start_of_name(self):
method test_partial_any_part_of_name (line 465) | def test_partial_any_part_of_name(self):
method test_no_match (line 470) | def test_no_match(self):
method test_empty_string_matches_any_start (line 475) | def test_empty_string_matches_any_start(self):
method test_longer_name_not_partial_match (line 480) | def test_longer_name_not_partial_match(self):
class TestAliasManager (line 490) | class TestAliasManager:
method test_create_new_alias (line 491) | def test_create_new_alias(self):
method test_create_updates_existing_alias (line 497) | def test_create_updates_existing_alias(self):
method test_action_returns_correct_string (line 504) | def test_action_returns_correct_string(self):
method test_action_returns_none_for_missing (line 509) | def test_action_returns_none_for_missing(self):
method test_create_with_none_actions_removes_alias (line 513) | def test_create_with_none_actions_removes_alias(self):
method test_create_with_empty_string_removes_alias (line 520) | def test_create_with_empty_string_removes_alias(self):
method test_remove_existing_alias (line 527) | def test_remove_existing_alias(self):
method test_remove_nonexistent_alias (line 533) | def test_remove_nonexistent_alias(self):
method test_alias_names_empty (line 537) | def test_alias_names_empty(self):
method test_alias_names_lists_all (line 541) | def test_alias_names_lists_all(self):
method test_alias_names_after_remove (line 547) | def test_alias_names_after_remove(self):
method test_alias_name_stripped (line 554) | def test_alias_name_stripped(self):
method test_print_aliases_empty (line 559) | def test_print_aliases_empty(self, capsys):
method test_print_aliases_shows_names (line 565) | def test_print_aliases_shows_names(self, capsys):
method test_save_and_load_aliases_to_file (line 573) | def test_save_and_load_aliases_to_file(self, tmp_path):
method test_load_aliases_from_file_ignores_comments (line 586) | def test_load_aliases_from_file_ignores_comments(self, tmp_path):
method test_load_aliases_from_nonexistent_file (line 595) | def test_load_aliases_from_nonexistent_file(self):
method test_aliases_to_text_raw (line 599) | def test_aliases_to_text_raw(self):
method test_aliases_to_text_formatted (line 605) | def test_aliases_to_text_formatted(self):
class TestParameterDecorators (line 622) | class TestParameterDecorators:
method _make_call (line 625) | def _make_call(self, decorated_func, params):
method test_zero_parameters_allows_empty (line 629) | def test_zero_parameters_allows_empty(self):
method test_zero_parameters_rejects_one (line 638) | def test_zero_parameters_rejects_one(self, capsys):
method test_one_parameter_allows_one (line 649) | def test_one_parameter_allows_one(self):
method test_one_parameter_rejects_zero (line 658) | def test_one_parameter_rejects_zero(self, capsys):
method test_one_parameter_rejects_two (line 667) | def test_one_parameter_rejects_two(self, capsys):
method test_zero_or_one_parameter (line 676) | def test_zero_or_one_parameter(self):
method test_one_or_two_parameters (line 687) | def test_one_or_two_parameters(self):
method test_two_parameters (line 699) | def test_two_parameters(self):
method test_zero_one_or_two_parameters (line 711) | def test_zero_one_or_two_parameters(self):
method test_one_or_more_parameters (line 723) | def test_one_or_more_parameters(self):
class TestCtrlCInterruptFlag (line 740) | class TestCtrlCInterruptFlag:
method setup_method (line 741) | def setup_method(self):
method teardown_method (line 746) | def teardown_method(self):
method test_default_is_false (line 750) | def test_default_is_false(self):
method test_set_true (line 753) | def test_set_true(self):
method test_set_false (line 757) | def test_set_false(self):
method test_sig_handler_sets_flag_when_suspended (line 762) | def test_sig_handler_sets_flag_when_suspended(self):
method test_sig_handler_does_not_set_flag_for_sigterm_when_suspended (line 770) | def test_sig_handler_does_not_set_flag_for_sigterm_when_suspended(self):
class TestSonosFunction (line 784) | class TestSonosFunction:
method test_properties (line 785) | def test_properties(self):
method test_defaults (line 792) | def test_defaults(self):
method test_switch_to_coordinator_false (line 798) | def test_switch_to_coordinator_false(self):
class TestGetActions (line 808) | class TestGetActions:
method test_returns_list (line 809) | def test_returns_list(self):
method test_is_sorted (line 814) | def test_is_sorted(self):
method test_known_actions_present (line 818) | def test_known_actions_present(self):
method test_satellite_actions_present (line 823) | def test_satellite_actions_present(self):
method test_loop_actions_included_by_default (line 830) | def test_loop_actions_included_by_default(self):
method test_loop_actions_excluded (line 835) | def test_loop_actions_excluded(self):
method test_wait_actions_always_present (line 840) | def test_wait_actions_always_present(self):
method test_track_follow_actions_excluded (line 847) | def test_track_follow_actions_excluded(self):
method test_track_follow_actions_included_by_default (line 852) | def test_track_follow_actions_included_by_default(self):
class TestCheckArgs (line 862) | class TestCheckArgs:
method _args (line 863) | def _args(self, min_netmask=24, timeout=1.0, threads=256):
method test_valid_args_returns_none (line 870) | def test_valid_args_returns_none(self):
method test_boundary_values_valid (line 873) | def test_boundary_values_valid(self):
method test_invalid_min_netmask_low (line 879) | def test_invalid_min_netmask_low(self):
method test_invalid_min_netmask_high (line 884) | def test_invalid_min_netmask_high(self):
method test_invalid_timeout_low (line 889) | def test_invalid_timeout_low(self):
method test_invalid_timeout_high (line 894) | def test_invalid_timeout_high(self):
method test_invalid_threads_low (line 899) | def test_invalid_threads_low(self):
method test_invalid_threads_high (line 904) | def test_invalid_threads_high(self):
method test_multiple_invalid_args_all_reported (line 909) | def test_multiple_invalid_args_all_reported(self):
class TestProcessWait (line 921) | class TestProcessWait:
method test_wait_sleeps_for_correct_duration (line 922) | def test_wait_sleeps_for_correct_duration(self):
method test_wait_for_sleeps_for_correct_duration (line 927) | def test_wait_for_sleeps_for_correct_duration(self):
method test_wait_hh_mm_ss_format (line 932) | def test_wait_hh_mm_ss_format(self):
method test_wait_missing_param_reports_error (line 937) | def test_wait_missing_param_reports_error(self, capsys):
method test_wait_until_calls_sleep (line 944) | def test_wait_until_calls_sleep(self):
method test_wait_until_missing_param_reports_error (line 951) | def test_wait_until_missing_param_reports_error(self, capsys):
method test_wait_invalid_time_format_reports_error (line 958) | def test_wait_invalid_time_format_reports_error(self, capsys):
function _make_speaker (line 976) | def _make_speaker(queue_size=5):
function _make_share_link_plugin (line 982) | def _make_share_link_plugin(valid_uris, add_return_values=None):
class TestAddSharelinkToQueue (line 995) | class TestAddSharelinkToQueue:
method _call (line 996) | def _call(self, speaker, args):
method test_single_uri_appends_and_prints_position (line 1001) | def test_single_uri_appends_and_prints_position(self, capsys):
method test_single_uri_with_position (line 1014) | def test_single_uri_with_position(self):
method test_multiple_uris_appended_in_order (line 1027) | def test_multiple_uris_appended_in_order(self, capsys):
method test_multiple_uris_with_position_first_uses_position (line 1042) | def test_multiple_uris_with_position_first_uses_position(self):
method test_only_first_position_is_saved (line 1060) | def test_only_first_position_is_saved(self):
method test_invalid_single_uri_returns_false (line 1072) | def test_invalid_single_uri_returns_false(self, capsys):
method test_invalid_uri_in_list_prevents_all_adds (line 1081) | def test_invalid_uri_in_list_prevents_all_adds(self):
method test_invalid_last_arg_treated_as_uri_not_position (line 1090) | def test_invalid_last_arg_treated_as_uri_not_position(self, capsys):
method test_upnp_exception_returns_false (line 1101) | def test_upnp_exception_returns_false(self, capsys):
method test_zero_args_rejected_by_decorator (line 1111) | def test_zero_args_rejected_by_decorator(self, capsys):
class TestPlaySharelink (line 1119) | class TestPlaySharelink:
method _call (line 1120) | def _call(self, speaker, args):
method test_single_uri_adds_and_plays (line 1123) | def test_single_uri_adds_and_plays(self):
method test_single_uri_with_position (line 1134) | def test_single_uri_with_position(self):
method test_multiple_uris_plays_from_first_position (line 1147) | def test_multiple_uris_plays_from_first_position(self):
method test_multiple_uris_with_position_first_uses_position (line 1160) | def test_multiple_uris_with_position_first_uses_position(self):
method test_invalid_uri_prevents_any_add_or_play (line 1176) | def test_invalid_uri_prevents_any_add_or_play(self, capsys):
method test_invalid_uri_in_list_prevents_all_adds_and_play (line 1186) | def test_invalid_uri_in_list_prevents_all_adds_and_play(self):
method test_invalid_last_arg_treated_as_uri_not_position (line 1196) | def test_invalid_last_arg_treated_as_uri_not_position(self, capsys):
method test_upnp_exception_does_not_play (line 1208) | def test_upnp_exception_does_not_play(self, capsys):
method test_zero_args_rejected_by_decorator (line 1219) | def test_zero_args_rejected_by_decorator(self):
FILE: tests/test_http_api.py
function reset_globals (line 35) | def reset_globals():
function client (line 50) | def client():
function loaded_macros (line 55) | def loaded_macros():
class TestActiveAsyncOps (line 74) | class TestActiveAsyncOps:
method test_add_and_get (line 75) | def test_add_and_get(self):
method test_get_missing_returns_none (line 80) | def test_get_missing_returns_none(self):
method test_remove_returns_pid_and_clears (line 84) | def test_remove_returns_pid_and_clears(self):
method test_remove_missing_returns_none (line 91) | def test_remove_missing_returns_none(self):
method test_stop_sends_sigint_and_removes (line 95) | def test_stop_sends_sigint_and_removes(self):
method test_stop_missing_returns_none (line 106) | def test_stop_missing_returns_none(self):
method test_stop_already_dead_process_cleans_up_pid (line 110) | def test_stop_already_dead_process_cleans_up_pid(self):
method test_stop_unkillable_process_leaves_pid_tracked (line 118) | def test_stop_unkillable_process_leaves_pid_tracked(self):
method test_overwrite_pid (line 126) | def test_overwrite_pid(self):
method test_multiple_keys_are_independent (line 132) | def test_multiple_keys_are_independent(self):
class TestQuoteIfContainsSpace (line 147) | class TestQuoteIfContainsSpace:
method test_no_space (line 148) | def test_no_space(self):
method test_with_space (line 151) | def test_with_space(self):
method test_empty_string (line 154) | def test_empty_string(self):
method test_multiple_spaces (line 157) | def test_multiple_spaces(self):
class TestSubstituteVariables (line 166) | class TestSubstituteVariables:
method test_no_parameters (line 167) | def test_no_parameters(self):
method test_single_substitution (line 170) | def test_single_substitution(self):
method test_multiple_substitutions (line 174) | def test_multiple_substitutions(self):
method test_unused_args_are_ignored (line 178) | def test_unused_args_are_ignored(self):
method test_unsatisfied_parameter_omitted (line 182) | def test_unsatisfied_parameter_omitted(self):
method test_underscore_arg_is_skipped (line 186) | def test_underscore_arg_is_skipped(self):
method test_arg_with_space_is_quoted (line 191) | def test_arg_with_space_is_quoted(self):
method test_all_twelve_params (line 195) | def test_all_twelve_params(self):
class TestLoadMacros (line 207) | class TestLoadMacros:
method test_loads_valid_file (line 208) | def test_loads_valid_file(self):
method test_ignores_blank_lines_and_comments (line 224) | def test_ignores_blank_lines_and_comments(self):
method test_missing_file_returns_false (line 236) | def test_missing_file_returns_false(self):
method test_malformed_line_is_skipped (line 243) | def test_malformed_line_is_skipped(self):
method test_value_containing_equals_is_accepted (line 256) | def test_value_containing_equals_is_accepted(self):
method test_strips_whitespace_from_name_and_value (line 268) | def test_strips_whitespace_from_name_and_value(self):
class TestLookupMacro (line 286) | class TestLookupMacro:
method test_found (line 287) | def test_found(self, loaded_macros):
method test_not_found_raises_key_error (line 290) | def test_not_found_raises_key_error(self):
class TestProcessMacroSync (line 300) | class TestProcessMacroSync:
method test_unknown_macro_returns_error (line 301) | def test_unknown_macro_returns_error(self):
method test_successful_sync_execution (line 306) | def test_successful_sync_execution(self, loaded_macros):
method test_sync_failure_returns_error_output (line 317) | def test_sync_failure_returns_error_output(self, loaded_macros):
method test_use_local_prepends_flag (line 327) | def test_use_local_prepends_flag(self, loaded_macros):
method test_command_contains_substituted_arg (line 334) | def test_command_contains_substituted_arg(self, loaded_macros):
class TestProcessMacroAsync (line 349) | class TestProcessMacroAsync:
method test_async_runs_popen_not_check_output (line 350) | def test_async_runs_popen_not_check_output(self, loaded_macros):
method test_async_returns_immediately_with_empty_result (line 364) | def test_async_returns_immediately_with_empty_result(self, loaded_macr...
method test_async_strips_prefix_before_macro_lookup (line 376) | def test_async_strips_prefix_before_macro_lookup(self, loaded_macros):
method test_async_unknown_macro_returns_error (line 388) | def test_async_unknown_macro_returns_error(self):
method test_async_pid_is_tracked (line 393) | def test_async_pid_is_tracked(self, loaded_macros):
method test_async_same_name_and_args_cancels_previous (line 404) | def test_async_same_name_and_args_cancels_previous(self, loaded_macros):
method test_async_different_args_run_concurrently (line 428) | def test_async_different_args_run_concurrently(self, loaded_macros):
method test_async_popen_failure_returns_error (line 454) | def test_async_popen_failure_returns_error(self, loaded_macros):
class TestCommandCore (line 469) | class TestCommandCore:
method _make_device (line 470) | def _make_device(self, name="Kitchen", ip="192.168.0.10"):
method test_speaker_not_found_returns_exit_code_1 (line 476) | def test_speaker_not_found_returns_exit_code_1(self):
method test_successful_sync_action (line 481) | def test_successful_sync_action(self):
method test_sync_action_failure (line 493) | def test_sync_action_failure(self):
method test_async_action_uses_popen (line 500) | def test_async_action_uses_popen(self):
method test_async_action_tracks_pid (line 512) | def test_async_action_tracks_pid(self):
method test_speaker_name_with_spaces_is_quoted_in_log (line 521) | def test_speaker_name_with_spaces_is_quoted_in_log(self, capsys):
class TestEndpoints (line 535) | class TestEndpoints:
method test_root (line 536) | def test_root(self, client):
method test_macros_list_empty (line 541) | def test_macros_list_empty(self, client):
method test_macros_list_populated (line 547) | def test_macros_list_populated(self, client, loaded_macros):
method test_macro_not_found (line 554) | def test_macro_not_found(self, client):
method test_macro_sync_success (line 560) | def test_macro_sync_success(self, client, loaded_macros):
method test_macro_async_success (line 570) | def test_macro_async_success(self, client, loaded_macros):
method test_macros_reload (line 582) | def test_macros_reload(self, client):
method test_speaker_action_endpoint (line 588) | def test_speaker_action_endpoint(self, client):
method test_speaker_action_with_arg (line 600) | def test_speaker_action_with_arg(self, client):
method test_end_on_pause_suffix_split_correctly (line 612) | def test_end_on_pause_suffix_split_correctly(self, client):
method test_filename_ending_with_end_on_pause_not_falsely_split (line 630) | def test_filename_ending_with_end_on_pause_not_falsely_split(self, cli...
method test_speakers_endpoint (line 648) | def test_speakers_endpoint(self, client):
FILE: tests/test_interactive.py
function api_mode (line 31) | def api_mode():
class TestRestoreQuotes (line 46) | class TestRestoreQuotes:
method test_single_word_items_unchanged (line 47) | def test_single_word_items_unchanged(self):
method test_multi_word_item_gets_quoted (line 52) | def test_multi_word_item_gets_quoted(self):
method test_multiple_multi_word_items (line 57) | def test_multiple_multi_word_items(self):
method test_three_word_item_gets_quoted (line 62) | def test_three_word_item_gets_quoted(self):
method test_empty_list_unchanged (line 67) | def test_empty_list_unchanged(self):
method test_already_present_quotes_not_doubled (line 72) | def test_already_present_quotes_not_doubled(self):
class TestLoopInCommandSequences (line 85) | class TestLoopInCommandSequences:
method test_no_loop_returns_false (line 86) | def test_no_loop_returns_false(self):
method test_loop_keyword_detected (line 90) | def test_loop_keyword_detected(self):
method test_loop_until_detected (line 94) | def test_loop_until_detected(self):
method test_loop_for_detected (line 98) | def test_loop_for_detected(self):
method test_loop_in_second_sequence (line 102) | def test_loop_in_second_sequence(self):
method test_loop_in_non_first_position_of_sequence (line 106) | def test_loop_in_non_first_position_of_sequence(self):
method test_empty_sequences_returns_false (line 111) | def test_empty_sequences_returns_false(self):
class TestCompleter (line 121) | class TestCompleter:
method test_returns_first_match (line 122) | def test_returns_first_match(self):
method test_returns_second_match_at_context_1 (line 128) | def test_returns_second_match_at_context_1(self):
method test_no_match_raises_index_error (line 134) | def test_no_match_raises_index_error(self):
method test_empty_prefix_matches_all (line 139) | def test_empty_prefix_matches_all(self):
method test_context_beyond_matches_raises_index_error (line 144) | def test_context_beyond_matches_raises_index_error(self):
class TestGetSpeakerNames (line 155) | class TestGetSpeakerNames:
method test_local_uses_local_speaker_list (line 156) | def test_local_uses_local_speaker_list(self):
method test_non_local_uses_speaker_cache (line 163) | def test_non_local_uses_speaker_cache(self):
method test_exception_returns_empty_list (line 170) | def test_exception_returns_empty_list(self, capsys):
class TestPrintSpeakerList (line 184) | class TestPrintSpeakerList:
method test_empty_list_prints_only_blank_line (line 185) | def test_empty_list_prints_only_blank_line(self, capsys):
method test_speakers_listed_with_zero_unset_entry (line 193) | def test_speakers_listed_with_zero_unset_entry(self, capsys):
method test_speakers_numbered_from_one (line 203) | def test_speakers_numbered_from_one(self, capsys):
method test_zero_entry_comes_before_speakers (line 215) | def test_zero_entry_comes_before_speakers(self, capsys):
class TestSetActionsAndCommandsList (line 230) | class TestSetActionsAndCommandsList:
method test_list_contains_actions_with_trailing_space (line 231) | def test_list_contains_actions_with_trailing_space(self):
method test_list_contains_speaker_names_with_trailing_space (line 243) | def test_list_contains_speaker_names_with_trailing_space(self):
method test_list_contains_shell_commands (line 258) | def test_list_contains_shell_commands(self):
method test_list_contains_alias_names (line 270) | def test_list_contains_alias_names(self):
class TestExec (line 288) | class TestExec:
method test_calls_subprocess_run_with_shell_true (line 289) | def test_calls_subprocess_run_with_shell_true(self):
method test_arg_with_space_gets_quoted (line 294) | def test_arg_with_space_gets_quoted(self):
method test_multiple_args_with_spaces_quoted (line 299) | def test_multiple_args_with_spaces_quoted(self):
method test_returns_false_when_no_ctrl_c (line 304) | def test_returns_false_when_no_ctrl_c(self):
method test_returns_true_when_ctrl_c_interrupted (line 312) | def test_returns_true_when_ctrl_c_interrupted(self):
method test_subprocess_exception_does_not_propagate (line 320) | def test_subprocess_exception_does_not_propagate(self, capsys):
class TestExecAction (line 331) | class TestExecAction:
method setup_method (line 332) | def setup_method(self):
method teardown_method (line 336) | def teardown_method(self):
method test_speaker_action_includes_ip_in_command (line 339) | def test_speaker_action_includes_ip_in_command(self):
method test_no_speaker_action_omits_ip (line 354) | def test_no_speaker_action_omits_ip(self):
method test_log_setting_inserted_at_position_1 (line 369) | def test_log_setting_inserted_at_position_1(self):
method test_returns_exec_result_true (line 383) | def test_returns_exec_result_true(self):
method test_returns_exec_result_false (line 389) | def test_returns_exec_result_false(self):
class TestExecLoop (line 401) | class TestExecLoop:
method setup_method (line 402) | def setup_method(self):
method teardown_method (line 406) | def teardown_method(self):
method test_no_loop_returns_false (line 409) | def test_no_loop_returns_false(self):
method test_loop_action_returns_true (line 417) | def test_loop_action_returns_true(self):
method test_loop_calls_exec_command_line_once (line 424) | def test_loop_calls_exec_command_line_once(self):
method test_unix_includes_export_spkr_env (line 431) | def test_unix_includes_export_spkr_env(self):
method test_windows_includes_set_spkr_env (line 443) | def test_windows_includes_set_spkr_env(self):
method test_no_speaker_excludes_spkr_env (line 455) | def test_no_speaker_excludes_spkr_env(self):
method test_use_local_adds_flag_when_no_speaker (line 462) | def test_use_local_adds_flag_when_no_speaker(self):
method test_sequences_joined_with_colon_separator (line 469) | def test_sequences_joined_with_colon_separator(self):
method test_does_not_mutate_remaining_sequences (line 480) | def test_does_not_mutate_remaining_sequences(self):
function _make_am (line 494) | def _make_am(*alias_pairs):
class TestAliasProcessorBasicExpansion (line 507) | class TestAliasProcessorBasicExpansion:
method test_simple_expansion (line 508) | def test_simple_expansion(self):
method test_expansion_inserts_before_existing_commands (line 515) | def test_expansion_inserts_before_existing_commands(self):
method test_multi_token_expansion (line 524) | def test_multi_token_expansion(self):
method test_extra_args_without_placeholders_are_unused (line 531) | def test_extra_args_without_placeholders_are_unused(self):
class TestAliasProcessorArgSubstitution (line 545) | class TestAliasProcessorArgSubstitution:
method test_single_arg_substituted (line 546) | def test_single_arg_substituted(self):
method test_two_args_substituted (line 553) | def test_two_args_substituted(self):
method test_unsatisfied_placeholder_removed (line 560) | def test_unsatisfied_placeholder_removed(self):
method test_no_args_all_placeholders_removed (line 568) | def test_no_args_all_placeholders_removed(self):
method test_same_placeholder_usable_once (line 575) | def test_same_placeholder_usable_once(self):
method test_arg_substitution_in_multi_sequence_alias (line 585) | def test_arg_substitution_in_multi_sequence_alias(self):
class TestAliasProcessorMultiSequence (line 598) | class TestAliasProcessorMultiSequence:
method test_two_sequence_alias_inserted_in_order (line 599) | def test_two_sequence_alias_inserted_in_order(self):
method test_three_sequence_alias (line 606) | def test_three_sequence_alias(self):
method test_multi_sequence_prepended_to_existing_commands (line 613) | def test_multi_sequence_prepended_to_existing_commands(self):
class TestAliasProcessorRecursion (line 629) | class TestAliasProcessorRecursion:
method test_one_level_nested_alias (line 630) | def test_one_level_nested_alias(self):
method test_two_levels_nested (line 637) | def test_two_levels_nested(self):
method test_nested_alias_with_arg_propagation (line 648) | def test_nested_alias_with_arg_propagation(self):
method test_nested_alias_mixed_with_real_action (line 656) | def test_nested_alias_mixed_with_real_action(self):
method test_nested_alias_arg_propagated_two_levels (line 664) | def test_nested_alias_arg_propagated_two_levels(self):
class TestAliasProcessorLoopDetection (line 682) | class TestAliasProcessorLoopDetection:
method test_self_referential_loop_returns_false (line 683) | def test_self_referential_loop_returns_false(self, capsys):
method test_mutual_recursion_ab_returns_false (line 690) | def test_mutual_recursion_ab_returns_false(self, capsys):
method test_three_way_loop_detected (line 697) | def test_three_way_loop_detected(self, capsys):
method test_loop_cleanup_removes_partially_inserted_commands (line 704) | def test_loop_cleanup_removes_partially_inserted_commands(self, capsys):
method test_loop_cleanup_leaves_pre_existing_commands_intact (line 717) | def test_loop_cleanup_leaves_pre_existing_commands_intact(self, capsys):
method test_non_looping_alias_not_misidentified (line 728) | def test_non_looping_alias_not_misidentified(self):
class TestAliasProcessorRemoveAdded (line 743) | class TestAliasProcessorRemoveAdded:
method test_removes_exact_command_count (line 744) | def test_removes_exact_command_count(self):
method test_zero_count_removes_nothing (line 752) | def test_zero_count_removes_nothing(self):
method test_command_count_after_successful_expansion (line 760) | def test_command_count_after_successful_expansion(self):
FILE: tests/test_m3u_parser.py
function api_mode (line 13) | def api_mode():
function _write_tempfile (line 20) | def _write_tempfile(content, suffix=".m3u"):
class TestTrack (line 35) | class TestTrack:
method test_attributes_set (line 36) | def test_attributes_set(self):
method test_none_values_allowed (line 42) | def test_none_values_allowed(self):
class TestParseMp3u (line 54) | class TestParseMp3u:
method test_valid_m3u_with_extinf (line 55) | def test_valid_m3u_with_extinf(self):
method test_multiple_tracks (line 67) | def test_multiple_tracks(self):
method test_missing_extinf_header_still_adds_track (line 84) | def test_missing_extinf_header_still_adds_track(self):
method test_comment_lines_skipped (line 97) | def test_comment_lines_skipped(self):
method test_blank_lines_skipped (line 111) | def test_blank_lines_skipped(self):
method test_missing_extm3u_header_returns_empty (line 120) | def test_missing_extm3u_header_returns_empty(self, capsys):
method test_empty_playlist (line 130) | def test_empty_playlist(self):
method test_title_with_comma_preserved (line 139) | def test_title_with_comma_preserved(self):
method test_path_whitespace_stripped (line 149) | def test_path_whitespace_stripped(self):
method test_non_m3u_extension_skips_header_check (line 160) | def test_non_m3u_extension_skips_header_check(self):
method test_m3u8_extension_requires_header (line 169) | def test_m3u8_extension_requires_header(self, capsys):
FILE: tests/test_match_speaker_names.py
class TestSpeakerNameMatches (line 6) | class TestSpeakerNameMatches:
method test_exact_match (line 9) | def test_exact_match(self):
method test_exact_match_with_spaces (line 14) | def test_exact_match_with_spaces(self):
method test_case_insensitive_match (line 21) | def test_case_insensitive_match(self):
method test_uppercase_supplied (line 26) | def test_uppercase_supplied(self):
method test_mixed_case (line 31) | def test_mixed_case(self):
method test_straight_apostrophe_matches_curly (line 38) | def test_straight_apostrophe_matches_curly(self):
method test_curly_apostrophe_matches_straight (line 44) | def test_curly_apostrophe_matches_straight(self):
method test_partial_start_of_name (line 51) | def test_partial_start_of_name(self):
method test_partial_first_word_of_multi_word_name (line 56) | def test_partial_first_word_of_multi_word_name(self):
method test_partial_middle_of_name (line 63) | def test_partial_middle_of_name(self):
method test_partial_end_of_name (line 68) | def test_partial_end_of_name(self):
method test_partial_substring_match (line 73) | def test_partial_substring_match(self):
method test_no_match (line 80) | def test_no_match(self):
method test_no_match_superset_not_counted (line 85) | def test_no_match_superset_not_counted(self):
method test_empty_supplied_matches_anything_partial (line 93) | def test_empty_supplied_matches_anything_partial(self):
method test_single_character_match (line 99) | def test_single_character_match(self):
method test_identical_single_character (line 104) | def test_identical_single_character(self):
FILE: tests/test_play_local_file.py
class TestIsSupportedType (line 8) | class TestIsSupportedType:
method test_all_supported_types_recognised (line 11) | def test_all_supported_types_recognised(self):
method test_mp3_lowercase (line 16) | def test_mp3_lowercase(self):
method test_flac_uppercase (line 19) | def test_flac_uppercase(self):
method test_wav_mixed_case (line 22) | def test_wav_mixed_case(self):
method test_path_with_directories (line 25) | def test_path_with_directories(self):
method test_path_with_spaces (line 28) | def test_path_with_spaces(self):
method test_txt_not_supported (line 33) | def test_txt_not_supported(self):
method test_mp4_is_supported (line 36) | def test_mp4_is_supported(self):
method test_avi_not_supported (line 39) | def test_avi_not_supported(self):
method test_no_extension (line 42) | def test_no_extension(self):
method test_dot_only (line 45) | def test_dot_only(self):
method test_extension_only_dot (line 48) | def test_extension_only_dot(self):
method test_multiple_dots_uses_last_extension (line 51) | def test_multiple_dots_uses_last_extension(self):
method test_empty_string (line 55) | def test_empty_string(self):
FILE: tests/test_speakers.py
function _make_device (line 12) | def _make_device(name, ip="192.168.1.1", visible=True, household="HH1"):
class TestIsIpv4Address (line 28) | class TestIsIpv4Address:
method test_valid_ip (line 29) | def test_valid_ip(self):
method test_valid_ip_class_a (line 32) | def test_valid_ip_class_a(self):
method test_valid_loopback (line 35) | def test_valid_loopback(self):
method test_cidr_notation (line 38) | def test_cidr_notation(self):
method test_hostname_returns_false (line 42) | def test_hostname_returns_false(self):
method test_ipv6_returns_false (line 45) | def test_ipv6_returns_false(self):
method test_empty_string_returns_false (line 48) | def test_empty_string_returns_false(self):
method test_out_of_range_octet_returns_false (line 51) | def test_out_of_range_octet_returns_false(self):
method test_partial_ip_returns_false (line 54) | def test_partial_ip_returns_false(self):
class TestSaveLoad (line 63) | class TestSaveLoad:
method test_save_returns_false_when_no_speakers (line 64) | def test_save_returns_false_when_no_speakers(self):
method test_save_and_load_round_trip (line 69) | def test_save_and_load_round_trip(self):
method test_load_returns_false_when_no_file (line 81) | def test_load_returns_false_when_no_file(self):
method test_speaker_cache_file_exists_property (line 86) | def test_speaker_cache_file_exists_property(self):
method test_speaker_cache_loaded_property (line 94) | def test_speaker_cache_loaded_property(self):
class TestClearAndRemove (line 107) | class TestClearAndRemove:
method test_clear_empties_speakers_list (line 108) | def test_clear_empties_speakers_list(self):
method test_remove_save_file (line 114) | def test_remove_save_file(self):
class TestRename (line 129) | class TestRename:
method test_rename_existing_speaker (line 130) | def test_rename_existing_speaker(self):
method test_rename_nonexistent_returns_false (line 140) | def test_rename_nonexistent_returns_false(self):
method test_rename_with_apostrophe_normalisation (line 146) | def test_rename_with_apostrophe_normalisation(self):
class TestSubnetsSetter (line 161) | class TestSubnetsSetter:
method test_none_subnets_sets_none (line 162) | def test_none_subnets_sets_none(self):
method test_valid_subnets_kept (line 166) | def test_valid_subnets_kept(self):
method test_invalid_subnets_removed (line 171) | def test_invalid_subnets_removed(self):
method test_all_invalid_subnets_leaves_empty_list (line 176) | def test_all_invalid_subnets_leaves_empty_list(self):
class TestGetAllSpeakerNames (line 186) | class TestGetAllSpeakerNames:
method test_returns_sorted_visible_names (line 187) | def test_returns_sorted_visible_names(self):
method test_invisible_speakers_excluded (line 197) | def test_invisible_speakers_excluded(self):
method test_empty_list_returns_empty (line 207) | def test_empty_list_returns_empty(self):
class TestFind (line 217) | class TestFind:
method test_exact_match_returns_soco_object (line 218) | def test_exact_match_returns_soco_object(self):
method test_partial_match_returns_soco_object (line 226) | def test_partial_match_returns_soco_object(self):
method test_no_match_returns_none (line 234) | def test_no_match_returns_none(self):
method test_invisible_speaker_excluded_by_default (line 240) | def test_invisible_speaker_excluded_by_default(self):
method test_invisible_speaker_found_when_not_requiring_visible (line 246) | def test_invisible_speaker_found_when_not_requiring_visible(self):
method test_ambiguous_partial_match_returns_none (line 254) | def test_ambiguous_partial_match_returns_none(self, capsys):
FILE: tests/test_utils.py
class ConvertToSeconds (line 6) | class ConvertToSeconds(unittest.TestCase):
method test_colon_separated (line 7) | def test_colon_separated(self):
method test_hms (line 14) | def test_hms(self):
FILE: tests/test_utils_extended.py
function api_mode (line 35) | def api_mode():
function reset_subs (line 43) | def reset_subs():
class TestCreateTimeFromStr (line 55) | class TestCreateTimeFromStr:
method test_hh_mm (line 56) | def test_hh_mm(self):
method test_hh_mm_ss (line 60) | def test_hh_mm_ss(self):
method test_midnight (line 64) | def test_midnight(self):
method test_end_of_day (line 68) | def test_end_of_day(self):
method test_no_colon_raises (line 72) | def test_no_colon_raises(self):
method test_too_many_parts_raises (line 76) | def test_too_many_parts_raises(self):
method test_out_of_range_hour_raises (line 80) | def test_out_of_range_hour_raises(self):
method test_out_of_range_minute_raises (line 84) | def test_out_of_range_minute_raises(self):
method test_out_of_range_second_raises (line 88) | def test_out_of_range_second_raises(self):
method test_non_numeric_raises (line 92) | def test_non_numeric_raises(self):
class TestSecondsUntil (line 102) | class TestSecondsUntil:
method _mock_now (line 103) | def _mock_now(self, h, m, s=0):
method test_future_time_returns_positive_seconds (line 109) | def test_future_time_returns_positive_seconds(self):
method test_past_time_wraps_to_next_day (line 117) | def test_past_time_wraps_to_next_day(self):
method test_hh_mm_format (line 126) | def test_hh_mm_format(self):
method test_invalid_format_raises (line 134) | def test_invalid_format_raises(self):
class TestConvertTrueFalse (line 144) | class TestConvertTrueFalse:
method test_yes_or_no_true (line 145) | def test_yes_or_no_true(self):
method test_yes_or_no_false (line 148) | def test_yes_or_no_false(self):
method test_on_or_off_true (line 151) | def test_on_or_off_true(self):
method test_on_or_off_false (line 154) | def test_on_or_off_false(self):
method test_unknown_conversion_returns_none (line 157) | def test_unknown_conversion_returns_none(self):
class TestPlaybackState (line 166) | class TestPlaybackState:
method test_stopped (line 167) | def test_stopped(self):
method test_paused (line 170) | def test_paused(self):
method test_playing (line 173) | def test_playing(self):
method test_transitioning (line 176) | def test_transitioning(self):
method test_unknown_state (line 179) | def test_unknown_state(self):
class TestFindByName (line 188) | class TestFindByName:
method _item (line 189) | def _item(self, title):
method test_strict_match_found (line 194) | def test_strict_match_found(self):
method test_fuzzy_match_case_insensitive (line 199) | def test_fuzzy_match_case_insensitive(self):
method test_strict_match_takes_priority_over_fuzzy (line 204) | def test_strict_match_takes_priority_over_fuzzy(self):
method test_not_found_returns_none (line 210) | def test_not_found_returns_none(self):
method test_empty_list_returns_none (line 214) | def test_empty_list_returns_none(self):
method test_fuzzy_substring_match (line 217) | def test_fuzzy_substring_match(self):
class TestCreateListOfItemsFromRange (line 228) | class TestCreateListOfItemsFromRange:
method test_single_item (line 229) | def test_single_item(self):
method test_range (line 232) | def test_range(self):
method test_reversed_range_normalised (line 235) | def test_reversed_range_normalised(self):
method test_multiple_items_comma_separated (line 238) | def test_multiple_items_comma_separated(self):
method test_mix_of_single_and_range (line 241) | def test_mix_of_single_and_range(self):
method test_all_keyword (line 245) | def test_all_keyword(self):
method test_all_keyword_case_insensitive (line 248) | def test_all_keyword_case_insensitive(self):
method test_duplicates_removed (line 251) | def test_duplicates_removed(self):
method test_item_out_of_range_raises (line 255) | def test_item_out_of_range_raises(self):
method test_zero_out_of_range_raises (line 259) | def test_zero_out_of_range_raises(self):
method test_range_exceeds_limit_raises (line 263) | def test_range_exceeds_limit_raises(self):
method test_result_is_sorted (line 267) | def test_result_is_sorted(self):
class TestPrettyPrintValues (line 277) | class TestPrettyPrintValues:
method test_basic_output (line 278) | def test_basic_output(self, capsys):
method test_empty_dict_prints_nothing (line 286) | def test_empty_dict_prints_nothing(self, capsys):
method test_values_aligned (line 290) | def test_values_aligned(self, capsys):
method test_sort_by_key (line 299) | def test_sort_by_key(self, capsys):
class TestRewindableList (line 310) | class TestRewindableList:
method test_len (line 311) | def test_len(self):
method test_getitem (line 315) | def test_getitem(self):
method test_iteration (line 320) | def test_iteration(self):
method test_iteration_rewinds_on_each_iter_call (line 324) | def test_iteration_rewinds_on_each_iter_call(self):
method test_rewind_resets_index (line 329) | def test_rewind_resets_index(self):
method test_rewind_to_valid_index (line 336) | def test_rewind_to_valid_index(self):
method test_rewind_to_zero_on_empty_list (line 341) | def test_rewind_to_zero_on_empty_list(self):
method test_rewind_to_invalid_raises (line 346) | def test_rewind_to_invalid_raises(self):
method test_index_increments_on_next (line 351) | def test_index_increments_on_next(self):
method test_pop_next_removes_first_element (line 357) | def test_pop_next_removes_first_element(self):
method test_pop_next_on_empty_raises (line 363) | def test_pop_next_on_empty_raises(self):
method test_insert_at_zero_increments_index (line 368) | def test_insert_at_zero_increments_index(self):
method test_insert_at_index_equal_to_current_increments_index (line 375) | def test_insert_at_index_equal_to_current_increments_index(self):
method test_str_representation (line 383) | def test_str_representation(self):
method test_stop_iteration_at_end (line 387) | def test_stop_iteration_at_end(self):
function _make_action (line 399) | def _make_action(decorator):
class TestParameterDecorators (line 409) | class TestParameterDecorators:
method test_zero_parameters_passes_on_empty (line 410) | def test_zero_parameters_passes_on_empty(self):
method test_zero_parameters_fails_on_one (line 414) | def test_zero_parameters_fails_on_one(self):
method test_one_parameter_passes_on_one (line 418) | def test_one_parameter_passes_on_one(self):
method test_one_parameter_fails_on_zero (line 422) | def test_one_parameter_fails_on_zero(self):
method test_one_parameter_fails_on_two (line 426) | def test_one_parameter_fails_on_two(self):
method test_zero_or_one_passes_on_zero (line 430) | def test_zero_or_one_passes_on_zero(self):
method test_zero_or_one_passes_on_one (line 434) | def test_zero_or_one_passes_on_one(self):
method test_zero_or_one_fails_on_two (line 438) | def test_zero_or_one_fails_on_two(self):
method test_one_or_two_passes_on_one (line 442) | def test_one_or_two_passes_on_one(self):
method test_one_or_two_passes_on_two (line 446) | def test_one_or_two_passes_on_two(self):
method test_one_or_two_fails_on_zero (line 450) | def test_one_or_two_fails_on_zero(self):
method test_two_parameters_passes_on_two (line 454) | def test_two_parameters_passes_on_two(self):
method test_two_parameters_fails_on_one (line 458) | def test_two_parameters_fails_on_one(self):
method test_zero_one_or_two_passes_on_zero (line 462) | def test_zero_one_or_two_passes_on_zero(self):
method test_zero_one_or_two_passes_on_two (line 466) | def test_zero_one_or_two_passes_on_two(self):
method test_zero_one_or_two_fails_on_three (line 470) | def test_zero_one_or_two_fails_on_three(self):
method test_one_or_more_passes_on_one (line 474) | def test_one_or_more_passes_on_one(self):
method test_one_or_more_passes_on_many (line 478) | def test_one_or_more_passes_on_many(self):
method test_one_or_more_fails_on_zero (line 482) | def test_one_or_more_fails_on_zero(self):
class TestCheckArgs (line 492) | class TestCheckArgs:
method _make_args (line 493) | def _make_args(self, min_netmask=24, timeout=1.0, threads=256):
method test_valid_args_returns_none (line 500) | def test_valid_args_returns_none(self):
method test_invalid_netmask_low (line 503) | def test_invalid_netmask_low(self):
method test_invalid_netmask_high (line 508) | def test_invalid_netmask_high(self):
method test_boundary_netmask_0 (line 512) | def test_boundary_netmask_0(self):
method test_boundary_netmask_32 (line 515) | def test_boundary_netmask_32(self):
method test_invalid_timeout_negative (line 518) | def test_invalid_timeout_negative(self):
method test_invalid_timeout_too_large (line 522) | def test_invalid_timeout_too_large(self):
method test_invalid_threads_zero (line 526) | def test_invalid_threads_zero(self):
method test_multiple_errors_reported (line 530) | def test_multiple_errors_reported(self):
class TestEventSubscriptions (line 541) | class TestEventSubscriptions:
method test_remember_and_forget (line 542) | def test_remember_and_forget(self):
method test_forget_nonexistent_does_not_raise (line 549) | def test_forget_nonexistent_does_not_raise(self):
method test_unsub_all_calls_unsubscribe (line 553) | def test_unsub_all_calls_unsubscribe(self):
method test_unsub_all_clears_list (line 563) | def test_unsub_all_clears_list(self):
class TestSpeakerCache (line 576) | class TestSpeakerCache:
method test_cache_speakers (line 577) | def test_cache_speakers(self):
method test_add_speaker (line 584) | def test_add_speaker(self):
method test_exists_false_when_empty (line 591) | def test_exists_false_when_empty(self):
method test_exists_true_after_add (line 595) | def test_exists_true_after_add(self):
method test_find_exact_match (line 602) | def test_find_exact_match(self):
method test_find_partial_match (line 608) | def test_find_partial_match(self):
method test_find_no_match_returns_none (line 614) | def test_find_no_match_returns_none(self):
method test_find_ambiguous_returns_none (line 620) | def test_find_ambiguous_returns_none(self, capsys):
method test_rename_speaker (line 628) | def test_rename_speaker(self):
method test_rename_nonexistent_returns_false (line 637) | def test_rename_nonexistent_returns_false(self):
method test_find_indirect_exact_match (line 641) | def test_find_indirect_exact_match(self):
method test_find_indirect_no_match_returns_none (line 650) | def test_find_indirect_no_match_returns_none(self):
FILE: tests/test_wait_actions.py
function api_mode (line 12) | def api_mode():
class TestProcessWaitAndWaitFor (line 19) | class TestProcessWaitAndWaitFor:
method test_waits_for_given_seconds (line 21) | def test_waits_for_given_seconds(self, action):
method test_wait_minutes (line 26) | def test_wait_minutes(self):
method test_wait_hours (line 31) | def test_wait_hours(self):
method test_wait_hh_mm_ss_format (line 36) | def test_wait_hh_mm_ss_format(self):
method test_wait_hh_mm_format (line 41) | def test_wait_hh_mm_format(self):
method test_missing_parameter_skips_sleep (line 46) | def test_missing_parameter_skips_sleep(self, capsys):
method test_too_many_parameters_skips_sleep (line 52) | def test_too_many_parameters_skips_sleep(self, capsys):
method test_invalid_duration_reports_error_then_sleeps_zero (line 58) | def test_invalid_duration_reports_error_then_sleeps_zero(self, capsys):
class TestProcessWaitUntil (line 67) | class TestProcessWaitUntil:
method test_waits_until_given_time (line 68) | def test_waits_until_given_time(self):
method test_missing_parameter_skips_sleep (line 75) | def test_missing_parameter_skips_sleep(self, capsys):
method test_too_many_parameters_skips_sleep (line 81) | def test_too_many_parameters_skips_sleep(self, capsys):
method test_invalid_time_format_reports_error_no_sleep (line 87) | def test_invalid_time_format_reports_error_no_sleep(self, capsys):
Condensed preview — 53 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (688K chars).
[
{
"path": ".gitignore",
"chars": 177,
"preview": "/venv/\n.idea\n/deploy.sh\n__pycache__/\n/build/\n/dist/\n/soco_cli.egg-info/\n*.pyc\n/README.md.orig.*\n/README.md.toc.*\n/explor"
},
{
"path": "CHANGELOG.txt",
"chars": 22613,
"preview": "v0.4.86 - Add 'async_' prefix support for HTTP API Server macros\n - Allow multiple sharelinks in a single 'ad"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "MANIFEST.in",
"chars": 25,
"preview": "include requirements.txt\n"
},
{
"path": "Makefile",
"chars": 1021,
"preview": ".DEFAULT_GOAL := no_op\n\nSRC = setup.py soco_cli/*.py\nTESTS = tests/*.py\nMANIFEST = LICENSE README.md PYPI_README.md MANI"
},
{
"path": "PYPI_README.md",
"chars": 3420,
"preview": "\n# SoCo-CLI: Control Sonos from the Command Line\n\n## Overview\n\nSoCo-CLI is a powerful command line wrapper for the popul"
},
{
"path": "README.md",
"chars": 97114,
"preview": "# SoCo-CLI: Control Sonos from the Command Line\n\n<!--ts-->\n* [SoCo-CLI: Control Sonos from the Command Line](#soco-cli-c"
},
{
"path": "RELEASING.txt",
"chars": 732,
"preview": "0. Start on branch 'next_version'; when changes are ready to be released:\n Update the CHANGELOG (if required)\n make "
},
{
"path": "gh-md-toc",
"chars": 12597,
"preview": "#!/usr/bin/env bash\n\n#\n# Steps:\n#\n# 1. Download corresponding html file for some README.md:\n# curl -s $1\n#\n# 2. "
},
{
"path": "pylintrc",
"chars": 1236,
"preview": "[MESSAGES CONTROL]\n\n# locally disable too-many-lines is only possible if the disable statement is\n# put into the first l"
},
{
"path": "pyproject.toml",
"chars": 1252,
"preview": "[build-system]\n requires = [ \"setuptools>=61.2\",]\n build-backend = \"setuptools.build_meta\"\n\n[project]\n name = \""
},
{
"path": "requirements-dev.txt",
"chars": 56,
"preview": "black\npylint\nflake8\nwheel\ntwine\nisort\npytest\nmypy\nbuild\n"
},
{
"path": "requirements.txt",
"chars": 261,
"preview": "soco == 0.27.1; python_version < \"3.6\"\nsoco >= 0.31.0; python_version >= \"3.6\"\nifaddr == 0.1.7; python_version < \"3.7\"\ni"
},
{
"path": "setup.cfg",
"chars": 112,
"preview": "[flake8]\n# E722 = do not use bare 'except'\nignore = E722\nmax-line-length = 120\n# extend-ignore = E203,W503,E231\n"
},
{
"path": "setup.py",
"chars": 125,
"preview": "\"\"\"\nMinimal setup.py for compatibility with legacy builds or build tool versions.\n\"\"\"\n\nfrom setuptools import setup\n\nset"
},
{
"path": "soco_cli/__init__.py",
"chars": 490,
"preview": "\"\"\"SoCo-CLI is a command line control interface for Sonos systems.\n\nIt is a simplified wrapper around the SoCo python li"
},
{
"path": "soco_cli/__main__.py",
"chars": 123,
"preview": "\"\"\"The main entry point into the sonos command.\"\"\"\n\nfrom soco_cli.sonos import main\n\nif __name__ == \"__main__\":\n main"
},
{
"path": "soco_cli/action_processor.py",
"chars": 115727,
"preview": "\"\"\"The main command processing module.\n\nThis module requires refactoring, improvements to its argument handling,\nand nee"
},
{
"path": "soco_cli/alarms.py",
"chars": 18245,
"preview": "\"\"\"Processing module for alarm actions.\"\"\"\n\nimport logging\nimport time\nfrom copy import copy\nfrom datetime import dateti"
},
{
"path": "soco_cli/aliases.py",
"chars": 3892,
"preview": "\"\"\"Manages aliases for use with the interactive shell.\"\"\"\n\nimport logging\nimport pickle\nfrom os import mkdir, path\nfrom "
},
{
"path": "soco_cli/api.py",
"chars": 7357,
"preview": "\"\"\"The SoCo-CLI API.\n\nProvides a few simple, high-level functions that allow the features of SoCo-CLI\nto be used in othe"
},
{
"path": "soco_cli/check_for_update.py",
"chars": 1516,
"preview": "\"\"\"Checks GitHub for a later version of SoCo-CLI\"\"\"\n\nimport logging\nfrom typing import Union\nfrom urllib.request import "
},
{
"path": "soco_cli/cmd_parser.py",
"chars": 1799,
"preview": "\"\"\"Parse sequential command lines, using ':' as a command separator.\"\"\"\n\n\nclass CLIParser:\n def __init__(self):\n "
},
{
"path": "soco_cli/http_api.py",
"chars": 22516,
"preview": "\"\"\"Implements an HTTP API server for SoCo-CLI commands.\"\"\"\n\nfrom sys import version_info\n\nif version_info.major == 3 and"
},
{
"path": "soco_cli/interactive.py",
"chars": 35336,
"preview": "\"\"\"SoCo-CLI interactive mode handler.\"\"\"\n\nimport logging\nimport subprocess\nimport sys\n\n# Readline is only available on U"
},
{
"path": "soco_cli/keystroke_capture.py",
"chars": 835,
"preview": "\"\"\"Captures single keystrokes.\"\"\"\n\nimport sys\nfrom os import name as os_name\n\nif os_name == \"nt\":\n import msvcrt\n\nels"
},
{
"path": "soco_cli/m3u_parser.py",
"chars": 1863,
"preview": "\"\"\"Parse M3U files.\"\"\"\n\n# Derived from https://github.com/dvndrsn/M3uParser ... thanks!\n#\n# more info on the M3U file fo"
},
{
"path": "soco_cli/match_speaker_names.py",
"chars": 2015,
"preview": "\"\"\"Matches a supplied speaker name to a stored name.\"\"\"\n\nimport logging\n\n\ndef speaker_name_matches(name_supplied, name_s"
},
{
"path": "soco_cli/play_local_file.py",
"chars": 10307,
"preview": "\"\"\"Plays files from the local filesystem.\"\"\"\n\nimport functools\nimport http.client\nimport logging\nimport socket\nimport sy"
},
{
"path": "soco_cli/play_local_file_lists.py",
"chars": 5502,
"preview": "\"\"\"Plays a list of files from the local filesystem, with interactive options.\"\"\"\n\nimport logging\nimport os\nimport sys\nfr"
},
{
"path": "soco_cli/sonos.py",
"chars": 18967,
"preview": "\"\"\"The main entry point into the 'sonos' command.\"\"\"\n\nimport argparse\nimport logging\nimport pprint\nimport sys\nimport tim"
},
{
"path": "soco_cli/sonos_discover.py",
"chars": 2972,
"preview": "\"\"\"The main entry point into the 'sonos-discover' command.\"\"\"\n\nimport argparse\n\nfrom soco_cli.check_for_update import pr"
},
{
"path": "soco_cli/speaker_info.py",
"chars": 3872,
"preview": "\"\"\"Prints a table of information about the Sonos system.\"\"\"\n\nimport datetime\n\nimport tabulate # type: ignore\n\n# Collect"
},
{
"path": "soco_cli/speakers.py",
"chars": 10803,
"preview": "\"\"\"Manages speaker information for Cached Discovery mode.\"\"\"\n\nimport ipaddress\nimport logging\nimport os\nimport pickle\nfr"
},
{
"path": "soco_cli/track_follow.py",
"chars": 5486,
"preview": "import logging\nimport re\nfrom datetime import datetime, timezone\n\nfrom soco import SoCo # type: ignore\n\nfrom soco_cli.a"
},
{
"path": "soco_cli/utils.py",
"chars": 28962,
"preview": "\"\"\"Common utilities used across multiple modules.\"\"\"\n\nimport datetime\nimport logging\nimport os\nimport pickle\nimport sign"
},
{
"path": "soco_cli/wait_actions.py",
"chars": 1498,
"preview": "\"\"\"Process the speaker-independent 'wait' actions.\"\"\"\n\nimport logging\nimport time\nfrom typing import List\n\nfrom soco_cli"
},
{
"path": "tests/test_action_processor.py",
"chars": 42775,
"preview": "\"\"\"Tests for action_processor.py.\n\nCovers pure helpers, output formatters, and action-processing functions\nthat can be e"
},
{
"path": "tests/test_aliases.py",
"chars": 8147,
"preview": "\"\"\"Tests for aliases.py — AliasManager.\"\"\"\n\nimport os\nimport tempfile\nfrom unittest.mock import patch\n\nimport pytest\n\nfr"
},
{
"path": "tests/test_check_for_update.py",
"chars": 4064,
"preview": "\"\"\"Tests for check_for_update.py.\"\"\"\n\nfrom io import BytesIO\nfrom unittest.mock import patch\n\nimport pytest\n\nimport soco"
},
{
"path": "tests/test_cli.py",
"chars": 1315,
"preview": "import unittest\n\nimport soco # type: ignore\n\nfrom soco_cli import action_processor as ap\nfrom soco_cli.api import run_c"
},
{
"path": "tests/test_cmd_parser.py",
"chars": 2207,
"preview": "\"\"\"Tests for cmd_parser.py.\"\"\"\n\nfrom soco_cli.cmd_parser import CLIParser\n\n\nclass TestCLIParser:\n # --- basic parsing"
},
{
"path": "tests/test_comprehensive.py",
"chars": 45794,
"preview": "\"\"\"Comprehensive unit tests for SoCo-CLI utilities.\n\nTests cover all pure-Python logic that does not require a live Sono"
},
{
"path": "tests/test_http_api.py",
"chars": 25486,
"preview": "\"\"\"Tests for the HTTP API server (http_api.py).\n\nCovers: ActiveAsyncOps, pure helper functions, macro loading/substituti"
},
{
"path": "tests/test_interactive.py",
"chars": 29566,
"preview": "\"\"\"Tests for interactive.py.\n\nCovers pure helpers and logic-bearing functions that can be exercised\nwithout a running RE"
},
{
"path": "tests/test_m3u_parser.py",
"chars": 5423,
"preview": "\"\"\"Tests for m3u_parser.py.\"\"\"\n\nimport os\nimport tempfile\n\nimport pytest\n\nimport soco_cli.utils as utils\nfrom soco_cli.m"
},
{
"path": "tests/test_match_speaker_names.py",
"chars": 3489,
"preview": "\"\"\"Tests for match_speaker_names.py.\"\"\"\n\nfrom soco_cli.match_speaker_names import speaker_name_matches\n\n\nclass TestSpeak"
},
{
"path": "tests/test_play_local_file.py",
"chars": 1756,
"preview": "\"\"\"Tests for play_local_file.py — testable portions only.\"\"\"\n\nimport pytest\n\nfrom soco_cli.play_local_file import SUPPOR"
},
{
"path": "tests/test_speakers.py",
"chars": 9479,
"preview": "\"\"\"Tests for speakers.py — Speakers class.\"\"\"\n\nimport os\nimport tempfile\nfrom unittest.mock import MagicMock, patch\n\nimp"
},
{
"path": "tests/test_utils.py",
"chars": 629,
"preview": "import unittest\n\nfrom soco_cli.utils import convert_to_seconds\n\n\nclass ConvertToSeconds(unittest.TestCase):\n def test"
},
{
"path": "tests/test_utils_extended.py",
"chars": 21811,
"preview": "\"\"\"Extended tests for utils.py — covering functions not tested in test_utils.py.\"\"\"\n\nimport argparse\nimport datetime as "
},
{
"path": "tests/test_wait_actions.py",
"chars": 3661,
"preview": "\"\"\"Tests for wait_actions.py.\"\"\"\n\nfrom unittest.mock import patch\n\nimport pytest\n\nimport soco_cli.utils as utils\nfrom so"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the avantrec/soco-cli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 53 files (642.4 KB), approximately 152.6k tokens, and a symbol index with 1173 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.