Peter John Hartman: 1 sonos: control son's 7 files changed, 495 insertions(+), 0 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~anjan/public-inbox/patches/27376/mbox | git am -3Learn more about email & git
A cluster of scripts to control sonos via dmenu. Main script is sonos-menu.sh --- scripts/README.md | 5 + scripts/sonos-artwall.sh | 6 + scripts/sonos-dmenu-artists-albums.sh | 12 + scripts/sonos-dmenu-radio.sh | 10 + scripts/sonos-dmenu-search.sh | 11 + scripts/sonos-dmenu.sh | 95 +++++++ scripts/sonos-pjh.py | 356 ++++++++++++++++++++++++++ 7 files changed, 495 insertions(+) create mode 100755 scripts/sonos-artwall.sh create mode 100755 scripts/sonos-dmenu-artists-albums.sh create mode 100755 scripts/sonos-dmenu-radio.sh create mode 100755 scripts/sonos-dmenu-search.sh create mode 100755 scripts/sonos-dmenu.sh create mode 100755 scripts/sonos-pjh.py diff --git a/scripts/README.md b/scripts/README.md index 47554ff..779d26f 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -9,6 +9,11 @@ Copy the script to `$XDG_CONFIG_HOME/sxmo/userscripts` and make it executable. # Summary of scripts +## sonos-dmenu.sh +- Author: Peter <peterjohnhartman@gmail.com> +- License: MIT +- Description: sonos demnu script + ## cal-convert-to-cron.sh - Author: Peter <peterjohnhartman@gmail.com> - License: MIT diff --git a/scripts/sonos-artwall.sh b/scripts/sonos-artwall.sh new file mode 100755 index 0000000..44c768a --- /dev/null +++ b/scripts/sonos-artwall.sh @@ -0,0 +1,6 @@ +#!/bin/sh +rm -f ~/.wallpaper.jpg +url=$(sonos-pjh.py cur | grep "^Album Art:" | cut -d':' -f2-) +echo "url: $url" +wget $url -O ~/.wallpaper.jpg +feh --bg-fill ~/.wallpaper.jpg diff --git a/scripts/sonos-dmenu-artists-albums.sh b/scripts/sonos-dmenu-artists-albums.sh new file mode 100755 index 0000000..1d8bc3a --- /dev/null +++ b/scripts/sonos-dmenu-artists-albums.sh @@ -0,0 +1,12 @@ +#!/bin/sh -x +dmenucmd="sxmo_dmenu_with_kb.sh -p >>>>>> -i -l 10" +#to repopulate the allartists database: rm -f ~/.sonos-allartists +[ -f ~/.sonos-allartists ] || sonos-pjh.py printallartists | sort -u > ~/.sonos-allartists +artist="$($dmenucmd < ~/.sonos-allartists)" +[ -z "$artist" ] && exit # exit if empty album, i.e., they canceled +album="$(sonos-pjh.py printalbumsfromartist "$artist" | sort -u | $dmenucmd)" +[ -z "$album" ] && exit # exit if empty album, i.e., they canceled +sonos-pjh.py unradio +sonos-pjh.py clearq +sonos-pjh.py search albums "$album" +sonos-pjh.py play | $dmenucmd diff --git a/scripts/sonos-dmenu-radio.sh b/scripts/sonos-dmenu-radio.sh new file mode 100755 index 0000000..5992b26 --- /dev/null +++ b/scripts/sonos-dmenu-radio.sh @@ -0,0 +1,10 @@ +#!/bin/sh -x +dmenucmd="sxmo_dmenu.sh -p >>>>>> -i -l 10" +uri=$(sonos-pjh.py listradio | sort -u | $dmenucmd) +[ -z "$uri" ] && exit +full_uri="$(echo "$uri" | cut -d'*' -f2 | sed -e 's/^[[:space:]]*//')" +sonos-pjh.py clearq +sonos-pjh.py loadradio "$full_uri" +sonos-pjh.py play + + diff --git a/scripts/sonos-dmenu-search.sh b/scripts/sonos-dmenu-search.sh new file mode 100755 index 0000000..db95dce --- /dev/null +++ b/scripts/sonos-dmenu-search.sh @@ -0,0 +1,11 @@ +#!/bin/sh +dmenucmd="sxmo_dmenu_with_kb.sh -p >>>>>> -i -l 10" +search_type="$(printf "tracks\ngenres\nalbums\nartists" | $dmenucmd)" +[ -z "$search_type" ] && exit # exit if empty search +search_value="$(echo "" | $dmenucmd)" +[ -z "$search_value" ] && exit # exit if empty search +echo "st: $search_type sv: $search_value" +sonos-pjh.py clearq +sonos-pjh.py search "$search_type" "$search_value" | $dmenucmd +sonos-pjh.py play + diff --git a/scripts/sonos-dmenu.sh b/scripts/sonos-dmenu.sh new file mode 100755 index 0000000..ec1fdb6 --- /dev/null +++ b/scripts/sonos-dmenu.sh @@ -0,0 +1,95 @@ +#!/bin/sh +function myinfo() { + if [ -n "$DISPLAY" ]; then + notify-send "$*" + elif [ -n "$WAYLAND_DISPLAY" ]; then + notify-send "$*" + else + printf %s "$*" + fi +} + +dmenucmd="sxmo_dmenu.sh -p >>>>>> -i -l 10" + + +function mainloop() { + + res=$(printf "Cancel\nPause\nPlay\nVol Up\nVol Down\nPick Artist-Album\nPick Radio Station\nSearch\nCurrent\nNext\nPrev\nRall\nShuffle\nNormal\nRepeat All\nUpdate\nPrint Q\nClear Q\n" | $dmenucmd) + [ -z "$res" ] && exit + + case "$res" in + "Search") + sonos-dmenu-search.sh + mainloop + ;; + "Pause") + myinfo "$(sonos-pjh.py pause)" + mainloop + ;; + "Play") + myinfo "$(sonos-pjh.py play)" + mainloop + ;; + "Vol Up") + myinfo "$(sonos-pjh.py vol up)" + mainloop + ;; + "Vol Down") + myinfo "$(sonos-pjh.py vol down)" + mainloop + ;; + "Pick Artist-Album") + sonos-dmenu-artists-albums.sh + mainloop + ;; + "Pick Radio Station") + sonos-dmenu-radio.sh + mainloop + ;; + "Current") + myinfo "$(sonos-pjh.py cur)" + mainloop + ;; + "Next") + myinfo "$(sonos-pjh.py next)" + mainloop + ;; + "Prev") + myinfo "$(sonos-pjh.py prev)" + mainloop + ;; + "Rall") + myinfo "$(sonos-pjh.py rall)" + mainloop + ;; + "Shuffle") + myinfo "$(sonos-pjh.py mode shuffle)" + mainloop + ;; + "Normal") + myinfo "$(sonos-pjh.py mode normal)" + mainloop + ;; + "Repeat All") + myinfo "$(sonos-pjh.py mode repeat_all)" + mainloop + ;; + "Update") + myinfo "$(sonos-pjh.py update)" + main loop + ;; + "Print Q") + myinfo "$(sonos-pjh.py printq)" + mainloop + ;; + "Clear Q") + myinfo "$(sonos-pjh.py clearq)" + mainloop + ;; + "Cancel") + exit 0 + ;; + esac +} + +mainloop diff --git a/scripts/sonos-pjh.py b/scripts/sonos-pjh.py new file mode 100755 index 0000000..b79492e --- /dev/null +++ b/scripts/sonos-pjh.py @@ -0,0 +1,356 @@ +#!/usr/bin/python +# Uses python-soco-git aur (custom made by me). +# Original: Christmas Break, 2017 +# Last modified: Mon Dec 13, 2021 06:28PM +# Usage: See below. +# Examples: +# Set Henry's White Noise +# sonos-pjh.py -s Henricus clearq +# sonos-pjh.py -s Henricus search "tracks" "Pink Noise" +# sonos-pjh.py -s Henricus play +# sonos-pjh.py -s Henricus radio + +import soco +import sys +import getopt +import os +# Options here are +# Play1 (= Henry's Room) +# Play3 (= Bedroom) +# Play5 (= Living Room) +# (Note it will AUTOMATICALLY play whatever speaker that speaker is grouped +# with, which is what I want.) +# TODO: If not grouped via another app, I should force the groupings here. +# see partymode and solomode below for grouping all or unjoining + +speaker = "Play5" +file = open(os.path.expanduser('~/.sonos-speaker'),"r") +speaker = file.readline().rstrip() +file.close() + +try: + opts, args = getopt.getopt(sys.argv[1:], "s:h") +except getopt.GetoptError as err: + print(err) + sys.exit(2) +output = None +verbose = False +for o, a in opts: + if o == "-s": + speaker = a + elif o == "-h": + print("sonos-pjh.py -s Play1|Play3|Play5 argument") + print("Examples: sonos-pjh.py -s Play3 radio") + print("Basic Arguments: cur|play|pause|next|prev|vol up|vol down|clearq|printq|rall") + print("Mode Arguments: mode normal|shuffle_norepeat|shuffle|repeat_all") + print("Special Arguments: update|search|printallartists|printalbumsfromartist") + print("Speaker Arguments: list|pick|partymode|solomode") + print("Radio: radio|unradio|listradio|loadradio") + print("Note that it will play whatever other speaker it is grouped with.") + sys.exit(2) + else: + assert False, "unhandled option" + +ip_to_device = {device.ip_address: device + for device in soco.discover()} +ip_addresses = list(ip_to_device.keys()) +ip_addresses.sort() +found_speaker = 0 +for zone_number, ip_address in enumerate(ip_addresses, 1): + name = ip_to_device[ip_address].player_name + if hasattr(name, 'decode'): + name = name.encode('utf-8') + if (name == speaker): + # print("Found Speaker:", speaker) + found_speaker = 1 + file = open(os.path.expanduser('~/.sonos-speaker'),"w+") + file.write(name) + file.close() + player = soco.discovery.by_name(speaker) + +if (found_speaker == 0): + print("Speaker not found:", speaker) + sys.exit() + +# Note that this will be the group too +#print("Speaker:", speaker) + + +if len(args) == 0: + print("See sonos-pjh.py -h") + sys.exit(2) + +cmd=args[0]; + +# +# basic commands +# +if cmd == "play": + player.play() + track_info = player.get_current_track_info() + print("Artist:", track_info['artist']) + print("Album:", track_info['album']) + print("Track:", track_info['title']) + print("Position:", track_info['position']) + print("Duration:", track_info['duration']) + print("Album Art:", track_info['album_art']) + +elif cmd == "pause": + player.pause() + track_info = player.get_current_track_info() + print("Artist:", track_info['artist']) + print("Album:", track_info['album']) + print("Track:", track_info['title']) + print("Position:", track_info['position']) + print("Duration:", track_info['duration']) + print("Album Art:", track_info['album_art']) + +elif cmd == "next": + player.next() + track_info = player.get_current_track_info() + print("Artist:", track_info['artist']) + print("Album:", track_info['album']) + print("Track:", track_info['title']) + print("Position:", track_info['position']) + print("Duration:", track_info['duration']) + print("Album Art:", track_info['album_art']) + +elif cmd == "prev": + player.previous() + track_info = player.get_current_track_info() + print("Artist:", track_info['artist']) + print("Album:", track_info['album']) + print("Track:", track_info['title']) + print("Position:", track_info['position']) + print("Duration:", track_info['duration']) + print("Album Art:", track_info['album_art']) + +# vol [none] or [up|down|#] +elif cmd == "vol": + if (len(args) == 1): + print("Volume:", player.volume) + else: + if args[1] == "down": + player.volume = player.volume - 5 + elif args[1] == "up": + player.volume = player.volume + 5 + else: + player.volume = args[1] + print("Volume:", player.volume) + +elif cmd == "clearq": + player.clear_queue() + +elif cmd == "printq": + queue = player.get_queue() + print("Mode:", player.play_mode) + print("Speaker:", speaker) + for item in queue: + print(item.title) + +# mode [none] or [normal|shuffle_norepeat|shuffle|repeat_all] +elif cmd == 'mode': + if (len(args) == 1): + print("Mode:", player.play_mode) + print("Options are: normal, shuffle_norepeat, shuffle, repeat_all.") + else: + player.play_mode = args[1] + print (player.play_mode) + print("Options are: normal, shuffle_norepeat, shuffle, repeat_all.") + +# set all the speakers in the same group +elif cmd == 'partymode': + player.partymode() + +elif cmd == 'solomode': + player.unjoin() + +elif cmd == "update": + if (len(args) == 1): + if player.music_library.library_updating: + print("Already updating so quitting...") + sys.exit() + else: + print("Telling the library to update...") + player.music_library.start_library_update() + else: + print("Updating status:", player.music_library.library_updating) + +elif cmd == "cur": + track_info = player.get_current_track_info() + print("Mode:", player.play_mode) + print("Speaker:", speaker) + print("Artist:", track_info['artist']) + print("Album:", track_info['album']) + print("Track:", track_info['title']) + print("Position:", track_info['position']) + print("Duration:", track_info['duration']) + print("Album Art:", track_info['album_art']) + +# generic search command - adds to the queue +elif cmd == 'search': + if (len(args) == 1): + print("Need two arguments, e.g., genres|tracks|artists|albums Classical...") + sys.exit() + else: + soutputs = player.music_library.get_music_library_information(args[1], search_term=args[2]) + while soutputs: + soutput = soutputs.pop() + print("Adding to Q:", soutput.title) + player.add_to_queue(soutput) + +# random - all: add all the tracks to the queue and play one randomly +elif cmd == 'rall': + print("Adding each track to the queue and setting it to shuffle...") + player.clear_queue() + player.mode = "shuffle" + genres = player.music_library.get_music_library_information('genres', complete_result=True) + while genres: + genre = genres.pop() + print("Adding to Q:", genre.title) + player.add_to_queue(genre) + +# +# radio: send listradio to dmenu and it spits back a uri which I send to load radio +# + +elif cmd == 'listradio': + stations = player.get_favorite_radio_stations() + for station in stations['favorites']: + print (station['title'], "*", station['uri']) + #stations = player.music_library.get_favorite_radio_stations() + #print(stations) + #for station in stations: + # print(station['KUSC']) + +elif cmd == 'loadradio': # accepts an argument (string) + if (len(args) == 1): + print("You must provide a uri (from listradio output).") + sys.exit() + else: + uri = args[1] + player.add_uri_to_queue(uri) + +# +# dmenu: commands for input into dmenu +# + +elif cmd == 'printallartists': + artists = player.music_library.get_music_library_information('artists', complete_result=True) + while artists: + artist = artists.pop() + print (artist.title) + +elif cmd == 'printalbumsfromartist': + if (len(args) == 1): + print("You must provide an artist.") + sys.exit() + else: + albums = player.music_library.get_music_library_information('artists', subcategories=[args[1]]) + while albums: + album = albums.pop() + print (album.title) + +# +# dmenu: load commands (from dmenu output) +# +#elif cmd == "loadartist": # accepts an argument (string) +# if (len(args) == 1): +# print("Need an argument, e.g., Pixies...") +# sys.exit() +# else: +# print("Looking for Artist:", args[1]) +# artists = player.music_library.get_artists(search_term=args[1]) +# artist = artists[0] +# print('Artist:', artist.title) +# albums = player.music_library.get_music_library_information('artists', subcategories=[artist.title]) +# while albums: +# album = albums.pop() +# if album.title != 'All': +# print('Queuing Album:', album.title) +# player.add_to_queue(album) + +#elif cmd == "loadalbum": # accepts an argument (string) +# if (len(args) == 1): +# print("Need an argument, e.g., Doolittle...") +# sys.exit() +# else: +# print("Looking for Album:", args[1]) +# albums = player.music_library.get_albums(search_term=args[1]) +# album = albums[0] +# print('Queueing Album:', album.title) +# player.add_to_queue(album) + +# +# misc. testing commands +# + +# I don't really use this nor have I tested it +elif cmd == 'uri': # accepts an argument (string) + if (len(args) == 1): + print("Need a uri argument...") + sys.exit() + else: + 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 = args[1] + uri = uri.replace('&', '&') + metadata = meta_template.format(title='foo bar', service=tunein_service) + player.play_uri(uri, metadata) + +# just in case the radio hogs the queue, this releases it +elif cmd == 'unradio': + player.play_from_queue(1) + +elif cmd == 'list': + ip_to_device = {device.ip_address: device + for device in soco.discover()} + ip_addresses = list(ip_to_device.keys()) + ip_addresses.sort() + for zone_number, ip_address in enumerate(ip_addresses, 1): + name = ip_to_device[ip_address].player_name + if hasattr(name, 'decode'): + name = name.encode('utf-8') + print(zone_number, ip_address, name) + +# requires argument with name of speaker. +elif cmd == 'pick': + if (len(args) == 1): + print("Speaker:", player.player_name) + else: + ip_to_device = {device.ip_address: device + for device in soco.discover()} + ip_addresses = list(ip_to_device.keys()) + ip_addresses.sort() + for zone_number, ip_address in enumerate(ip_addresses, 1): + # pylint: disable=no-member + name = ip_to_device[ip_address].player_name + if hasattr(name, 'decode'): + name = name.encode('utf-8') + print("Speaker:", name) + if (name == args[1]): + print("Found Speaker:", args[1]) + file = open(os.expanduser('~/.sonos-speaker'),"w+") + file.write(name) + file.close() + sys.exit() + + print("Speaker not found:", args[1]) +else: + print("Syntax: script -s 'speaker name' play|pause|etc...") + sys.exit() + +# vim: set ts=8 sw=4 tw=0 et : -- 2.34.1
Thanks! Applied: To git.sr.ht:~anjan/sxmo-userscripts dd84589..abf9fd5 master -> master -- w:] www.momi.ca pgp:] https://momi.ca/publickey.txt