~anjan/public-inbox

sxmo-userscripts: sonos: control son's v1 APPLIED

Peter John Hartman: 1
 sonos: control son's

 7 files changed, 495 insertions(+), 0 deletions(-)
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH sxmo-userscripts] sonos: control son's Export this patch

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('&', '&amp;')
        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