├── README.md ├── .gitignore ├── _squeezy.bash ├── _squeezy ├── LICENSE ├── squeezy.conf ├── squeezy.README.txt ├── squeezy.HELP.txt └── squeezy /README.md: -------------------------------------------------------------------------------- 1 | squeezy 2 | ======= 3 | 4 | A command-line utility for controlling squeezebox network audio players via their squeezeserver. 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | blib/ 2 | .build/ 3 | _build/ 4 | cover_db/ 5 | inc/ 6 | Build 7 | !Build/ 8 | Build.bat 9 | .last_cover_stats 10 | Makefile 11 | Makefile.old 12 | MANIFEST.bak 13 | META.yml 14 | MYMETA.yml 15 | nytprof.out 16 | pm_to_blib 17 | -------------------------------------------------------------------------------- /_squeezy.bash: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | _squeezy() 4 | { 5 | local cur prev opts 6 | COMPREPLY=() 7 | cur="${COMP_WORDS[COMP_CWORD]}" 8 | prev="${COMP_WORDS[COMP_CWORD-1]}" 9 | 10 | if [[ ${cur} == -* ]] ; then 11 | opts=`squeezy -options` 12 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 13 | return 0 14 | fi 15 | 16 | if [[ ${prev} == -sync ]] ; then 17 | opts=`squeezy -players` 18 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 19 | return 0 20 | fi 21 | 22 | if [[ ${prev} == -play ]] ; then 23 | _filedir 24 | return 0 25 | fi 26 | 27 | } 28 | 29 | complete -F _pkg_delete $dirnames pi piu pm portmaster 30 | 31 | for player in squeezy $(squeezy -players) 32 | do 33 | player_lc=$(echo $player | tr 'A-Z' 'a-z') 34 | complete -F _squeezy $player_lc 35 | alias $player_lc="$player_lc" 36 | done 37 | 38 | -------------------------------------------------------------------------------- /_squeezy: -------------------------------------------------------------------------------- 1 | #compdef squeezy kitchen living bedroom nico chrissy 2 | 3 | # 4 | # this is the squeezy completion configuration file for zsh; it goes in the 5 | # right place for you -- the right place for me being 6 | # ~/.zsh_completion/_squeezy 7 | # 8 | # you will need to edit the list of commands above to match the pseudonyms you 9 | # have for squeezy, if any; a minimal confguration would be just: 10 | #compdef squeezy 11 | # 12 | # to avoid having to fire up several processes (one of which being a big perl 13 | # process) this completer caches the available options in $cache (see below) 14 | # 15 | 16 | local squeezy=`whence -p squeezy` 17 | local cache="$HOME/.squeezy_zsh_completion_cache" 18 | 19 | [ -z "$squeezy" ] && return 1 20 | [ -x "$squeezy" ] || return 2 21 | 22 | if ! [ -r $cache -a -s $cache ] || [ "$squeezy" -nt $cache ] 23 | then 24 | command squeezy -options | tr ' ' '\n' | grep '^-[a-zA-Z0-9]' > $cache 25 | fi 26 | 27 | [ -r $cache -a -s $cache ] || return 4 28 | 29 | _arguments `cat $cache` 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Phill (pssc) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /squeezy.conf: -------------------------------------------------------------------------------- 1 | 2 | # absent some extraordinary coincidence, you will have to change the 3 | # settings in this file to get anything useful out of squeezy 4 | 5 | # ############################################### 6 | # options: 7 | # server 8 | # port 9 | # username 10 | # password 11 | # player 12 | # player 13 | # shortcut 14 | # small_volume 15 | # large_volume 16 | # prefixpre 17 | # prefixpos 18 | # 19 | # whitespace is not allowed within tokens (although extra whitespace 20 | # between tokens is ok) 21 | # 22 | 23 | # ############################################### 24 | # server 25 | # 26 | # server [| | 27 | # aws ] 28 | # 29 | # - the server defaults to localhost, if no value is specified here 30 | # - the port defaults to 9090, if no value is specified here 31 | # 32 | # - AWS Communication via Simple Queue service is now available. 33 | # - - AWS Access Id need send message, FIFO queue 34 | # - - AWS Access Id needs revice and delete message, FIFO queue 35 | # 36 | # - autodiscovery will work in additon to this. 37 | #server 192.168.4.72 38 | #server 192.168.4.72 8080 39 | #server 192.168.5.73 8080 8000 40 | #server erebor aws 12345678901234567890 0123456789012345678901234567890123456789 https://sqs.us-west-2.amazonaws.com/123456789012/server-recv.fifo https://sqs.us-west-2.amazonaws.com/124742781706/squeezy1-recv.fifo 41 | #port 9090 42 | #verbose 43 | #debug 44 | 45 | # autodiscover servers (default yes) 46 | autodiscover yes 47 | 48 | # autodiscover players (default yes) 49 | playerdiscover yes 50 | 51 | 52 | # ############################################### 53 | # authentication 54 | # - if username is unset here, then no authentication takes place 55 | # - if username is set here, then password must be set here too 56 | # 57 | # if you don't use authentication on your server, then just remove (or 58 | # comment out) these two lines 59 | # 60 | 61 | #username blott 62 | password YOUR_PASSWORD 63 | 64 | 65 | # ############################################### 66 | # players 67 | # 68 | # there are two formats for specifying players: 69 | # - player 70 | # - player [] 71 | # 72 | # - if is omitted, then group 0 is assumed 73 | # - the first player in group 0 is the default player; so, if you don't 74 | # use groups at all, then that would be the first player listed here 75 | # - should be the human-readable name of your player, as 76 | # configured on your squeezeserver, and may not contain any whitespace 77 | # - for each player, a new option is added to squeezy; for example, 78 | # with the configuration below, you can invoke: 79 | # squeezy -kitchen 80 | # squeezy -bedroom 81 | # - at least one player must be in 0 for squeezy to do anything 82 | # much useful at all 83 | # 84 | 85 | player Hungry 86 | player Living 87 | player squeezepi-iq-pi2 88 | player 1 squeezepi-test-pi2 89 | player 1 squeezepi-test-piB 90 | player 2 Sleepy 192.168.1.1 91 | player 3 blackpi3 erebor macid 92 | 93 | 94 | # also, if squeezy is executed from an executable whose names matches 95 | # one of these players, than that player is used as the default 96 | # 97 | # configure some players and then try 'squeezy -printlinks' 98 | 99 | # ############################################### 100 | # the maginitude of small and large changes to volume levels: 101 | # see -louder, -quieter, -Louder, -Quieter, -, +, --, ++ 102 | # 103 | 104 | small_volume 4 105 | large_volume 13 106 | 107 | # ############################################### 108 | # prefixes 109 | # 110 | # this is an oddity to handle file systems being mounted at different places on 111 | # the client and the server; if you're running squeezy on the same host as the 112 | # server then you can ignore (remove or comment out) these two lines 113 | # 114 | # whenever the '-play' and '-add' options are used, squeezy checks 115 | # whether the thing to be played is a file or a directory 116 | # 117 | # if it is, then realpath is called, then squeezy premoves the prefix 118 | # 'prefixpre' from the start of the path, and replaces it with 'prefixpos' 119 | # 120 | # so 'prefixpre' should be the path to the root of your music collection 121 | # on the client, and 'prefixpos' should be the corresponding path on the 122 | # server 123 | # 124 | # clear? 125 | # 126 | # either or both of these can be left empty; if provided, they should probably 127 | # be absolute paths; they SHOULD NOT include trailling backslashes 128 | # 129 | # here are my own settings: 130 | # 131 | # prefixpre /mnt 132 | # prefixpos 133 | 134 | prefixpre 135 | prefixpos 136 | 137 | # ############################################### 138 | # shortcuts 139 | # - playlist/filename/directory name/url may not contain whitespace 140 | # - for each shortcut, a new option is added to squeezy; for example, 141 | # with the configuration below, you can invoke: 142 | # squeezy -dvb 143 | # squeezy -r4, etc. 144 | # to play the first shortcut, or the fifth 145 | # - "squeezy -default" plays the first shortcut 146 | # 147 | 148 | shortcut dvb http://localhost:7654/dvb-last.mp3 149 | shortcut last http://localhost:7654/dvb-last.mp3 150 | shortcut r1 http://localhost:7654/dvb-r1.mp3 151 | shortcut r3 http://localhost:7654/dvb-r3.mp3 152 | shortcut r4 http://localhost:7654/dvb-r4.mp3 153 | shortcut r5 http://localhost:7654/dvb-r5.mp3 154 | shortcut r5x http://localhost:7654/dvb-r5x.mp3 155 | shortcut ws http://localhost:7654/dvb-ws.mp3 156 | 157 | 158 | -------------------------------------------------------------------------------- /squeezy.README.txt: -------------------------------------------------------------------------------- 1 | 2 | Copyright: 3 | Stephen Blott 4 | Phillip Camp 5 | 6 | License: 7 | MIT license 8 | http://opensource.org/licenses/mit-license.php 9 | 10 | Project home: 11 | https://github.com/pssc/squeezy 12 | 13 | Introduction: 14 | squeezy is a command-line wrapper around squeezeserver's telnet/CLI 15 | interface. It can be used from the command line, in scripts or from 16 | cron. 17 | 18 | Examples: 19 | squeezy -on 20 | squeezy -off 21 | squeezy -playlist 22 | squeezy -jump 2 # play the second song on the playlist 23 | squeezy -play . # play all songs in the current directory 24 | squeezy -play 25 | squeezy -play 26 | squeezy -next 27 | squeezy -previous 28 | squeezy -sleep 60 29 | squeezy -kitchen -volume 14 30 | squeezy -kitchen -volume +5 31 | squeezy -kitchen - 32 | squeezy -button 33 | squeezy -pause 34 | ... etc 35 | 36 | ("kitchen" and "bedroom" here would be the names of your players) 37 | 38 | Why? 39 | - I've had my players since 2005 and the buttons on the remotes are 40 | packing up; I listen to squeezeboxes all the time, but with squeezy 41 | I rarely have to touch the remotes 42 | 43 | - the web interface is too slow 44 | 45 | - you're a geek, and you get things done faster at the keyboard 46 | 47 | - it's scriptable, and completion scripts are provided for 48 | interactive use (bash and zsh completion) 49 | 50 | - squeezy integrates well with desktop menuing systems or with cron; 51 | here are some cron examples: 52 | 53 | 30 20 * * * chronic squeezy -bedroom -exit_if_playing -volume 22 -dvb 54 | 00 23 * * * chronic squeezy -bedroom -volume -1 55 | 30 23 * * * chronic squeezy -bedroom -volume -2 56 | 00 00 * * * chronic squeezy -bedroom -volume -1 57 | 30 00 * * * chronic squeezy -bedroom -volume -1 58 | 00 01 * * * chronic squeezy -bedroom -volume -1 59 | 30 02 * * * chronic squeezy -all -off 60 | 30 06 * * * chronic squeezy -bedroom -exit_if_playing -volume 17 -dvb 61 | 62 | chronic is part of of the moreutils package 63 | - http://kitenet.net/~joey/code/moreutils/ 64 | 65 | turn on the squeezebox in my bedroom a 8.30 playing my favourite 66 | radio station and at volume 22; however, the -exit_if_playing 67 | options tells squeezy NOT to do this if the player happens already 68 | to be on 69 | 70 | from 11.00 onwards, the volume is gradually reduced and then at 71 | 2.30 this (and all my other players) are turned off 72 | 73 | turn the player on again a 6.30 in the morning, but at a lower 74 | volume level; again, do this only if the player is not already on 75 | 76 | the '-dvb' is a shortcut in my configuration file; it plays my 77 | favourite radio station 78 | 79 | - another cron example: 80 | 81 | */8 1-6 * * * chronic squeezy -bedroom -exit_if_sleeping -sleep 30 82 | 83 | if I wake up and turn a player on during the night, it'll 84 | automatically go to sleep after about half an hour 85 | 86 | - and another cron example: 87 | 88 | 30 05 * * sun chronic squeezy -all -die_if_playing || chronic SOME_COMMAND_TO_SET_THE_RADIO_STATION 89 | 90 | I listen to various radio stations, but want a particular one 91 | playing in the morning; this changes to that station, but only if 92 | I'm not actually listening to any of my squeezeboxes already 93 | 94 | SOME_COMMAND_TO_SET_THE_RADIO_STATION is actually a command to set 95 | the tuner on my DVB card 96 | 97 | Dependencies: 98 | - squeezy has been written and runs on FreeBSD and Ubuntu; it 99 | shouldn't be too hard to get it to work on other Unix and Unix-like 100 | systems (including Macs); but it may be more difficult to get it to 101 | work on other popular operating systems 102 | 103 | - squeezy uses the telnet/CLI interface of the squeezebox server; 104 | this hasn't changed significantly in many years, so it should work 105 | with pretty much any server more recent than about five years old 106 | 107 | - squeezy is written in "perl", any reasonably-recent perl version 108 | should do 109 | 110 | - Non-software: 111 | if you haven't done so already, then use your squeezeserver's web 112 | interface to give your players suitable human-readable names (such 113 | as "Kitchen", or "Bedroom"); these names must not contain 114 | whitespace characters. 115 | 116 | Installation: 117 | 1. put the file "squeezy" into some suitable place (such as 118 | ~/bin/, /usr/local/bin/, or /usr/bin/) and make it executable 119 | 120 | 2. edit "squeezy.conf" to match your own setup. You MUST do this; 121 | squeezy will not do much without a suitable configuration file 122 | 123 | 3. install "squeezy.conf" in some suitable location; squeezy looks 124 | for its configuration file in the following locations: 125 | 126 | ./squeezy.conf 127 | $HOME/.squeezy.conf 128 | $HOME/.config/squeezy/squeezy.conf 129 | $HOME/.config/squeezy.conf 130 | /usr/local/etc/squeezy.conf 131 | /etc/squeezy.conf 132 | 133 | it chooses the first (readable) file it finds. 134 | 135 | Usage: 136 | try "squeezy -help"; "squeezy -otions" might be helpful too 137 | 138 | Tips: 139 | - try "squeezy -print_links"; this outputs a list of commands to 140 | install an alias for squeezy for each of your players; for example, 141 | for me its output is: 142 | 143 | ln -vf /usr/local/bin/squeezy /usr/local/bin/kitchen 144 | ln -vf /usr/local/bin/squeezy /usr/local/bin/living 145 | ln -vf /usr/local/bin/squeezy /usr/local/bin/bedroom 146 | ln -vf /usr/local/bin/squeezy /usr/local/bin/nico 147 | ln -vf /usr/local/bin/squeezy /usr/local/bin/chrissy 148 | 149 | then use: 150 | 151 | squeezy -print-links | sudo sh 152 | 153 | to install the links (you may need 'sudo') 154 | 155 | WARNING: make sure your player names do not clash with existing command names 156 | before doing this 157 | 158 | you can then do things like: 159 | 160 | kitchen -off 161 | bedroom -volume 12 162 | 163 | where 'kitchen' and 'bedroom' are the names of your players. 164 | 165 | - take a look at 'chronic' from the moreutils package, it helps 166 | when writing nice clean crontab files 167 | 168 | also, the -exit_if_playing option is particularly useful in 169 | crontab files 170 | 171 | - files are included in the distribution for TAB-completion in both 172 | zsh and bash 173 | -------------------------------------------------------------------------------- /squeezy.HELP.txt: -------------------------------------------------------------------------------- 1 | command-line options: 2 | players: Kitchen, Living, Bedroom, SqueezePlay-Pi 3 | configuration file: 4 | ./squeezy.conf 5 | 6 | configuration file search locations: 7 | ./squeezy.conf 8 | /home/pc188/.squeezy.conf 9 | /home/pc188/.config/squeezy/squeezy.conf 10 | /home/pc188/.config/squeezy.conf 11 | /usr/local/etc/squeezy.conf 12 | /etc/squeezy.conf 13 | 14 | command-line options: 15 | + : increase the volume slightly (by 4%) 16 | ++ : increase the volume significantly (by 13%) 17 | - : decrease the volume slightly (by 4%) 18 | -- : reduce the volume significantly (by 13%) 19 | --help : show this help message 20 | -Bedroom : select player Bedroom (automatically added option) 21 | -Chrissy : select player Chrissy (automatically added option) 22 | -Kitchen : select player Kitchen (automatically added option) 23 | -Living : select player Living (automatically added option) 24 | -Louder : increase the volume significantly (by 13%) 25 | -Nico : select player Nico (automatically added option) 26 | -Quieter : reduce the volume significantly (by 13%) 27 | -Sleepy : select player Sleepy (automatically added option) 28 | -SqueezePlay-Pi : select player SqueezePlay-Pi (automatically added option) 29 | -add : add the indicated file, directory (contents), playlist or url to the playlist 30 | -alarms : enabled alarms count (can only sound if not generally disabled) 31 | -alarms_enabled : alarms generally enabled? 32 | -all : apply subsequent commands to all players (eg. squeezy -all -off) 33 | -b : select player Bedroom (automatically added option) 34 | -bedroom : select player Bedroom (automatically added option) 35 | -button : send a button key command to the player (see the server's "Default.map" file) 36 | -c : select player Chrissy (automatically added option) 37 | -chrissy : select player Chrissy (automatically added option) 38 | -cliport : query cli port VIA JSON 39 | -connected : is player connected? 40 | -currenttitle : show the currently-playing track 41 | -default : play the default shortcut (the first one in the configuration file, currently http://localhost:7654/dvb-last.mp3) 42 | -dvb : play shortcut dvb (http://localhost:7654/dvb-last.mp3, automatically added option) 43 | -elapsed : show the elapsed time in currently-playing track/url 44 | -exit : set up action for -if_XXXX to exit with specificed code 45 | -goto : jump to a track on the current playlist (relative or absolute, eg. 3, +2, -4) 46 | -group : apply subsequent command to all players in this group (eg. squeezy -group 1) 47 | -groups : show the configured player groups 48 | -help : show this help message 49 | -if_alarm : if alarms are disabled generally or no alarm is enabled 50 | -if_connected : if this player is not connected 51 | -if_playing : if this player is on and playing 52 | -if_sleeping : if this player is on and is timing down to sleep 53 | -json : send command via JSON interface quoted as one argument 54 | -jump : jump to a track on the current playlist (relative or absolute, eg. 3, +2, -4) 55 | -k : select player Kitchen (automatically added option) 56 | -kitchen : select player Kitchen (automatically added option) 57 | -l : select player Living (automatically added option) 58 | -last : play shortcut last (http://localhost:7654/dvb-last.mp3, automatically added option) 59 | -listen : listen to squeezeserver activity (on standard output, see also '-tick') 60 | -living : select player Living (automatically added option) 61 | -louder : increase the volume slightly (by 4%) 62 | -mode : show the player's mode ("play", "stop" or "pause") 63 | -mute : set volume to 0 64 | -n : select player Nico (automatically added option) 65 | -next : jump to the next track on the playlist 66 | -nico : select player Nico (automatically added option) 67 | -not : -if_XXXX can be prefixed with not 68 | -off : turn player off 69 | -on : turn player on and start playing the current playlist/track 70 | -options : list all options to standard output (useful for configuring shell completion) 71 | -pause : toggle pause 72 | -play : play the indicated file, directory (contents), playlist or url 73 | -player_count : show the number of connected players 74 | -player_id : show the id of the player 75 | -player_ip : show the ip address of the player 76 | -player_model : show the model of the player 77 | -players : show all configured players 78 | -playing : show the currently-playing track 79 | -playlist : show the current playlist 80 | -playlist_delete : delete (or remove) an item from the current playlist (eg. '-playlist_delete 3') 81 | -playlist_index : show the index of the current song in the playlist 82 | -playlist_length : show the length of the current playlist 83 | -playlist_remove : delete (or remove) an item from the current playlist (eg. '-playlist_delete 3') 84 | -power : show whether power is on or off 85 | -prev : jump to the previous track on the playlist 86 | -previous : jump to the previous track on the playlist 87 | -print_links : show a list of commands suitable for creating player pseudonyms for the squeezy command 88 | -quieter : decrease the volume slightly (by 4%) 89 | -r1 : play shortcut r1 (http://localhost:7654/dvb-r1.mp3, automatically added option) 90 | -r3 : play shortcut r3 (http://localhost:7654/dvb-r3.mp3, automatically added option) 91 | -r4 : play shortcut r4 (http://localhost:7654/dvb-r4.mp3, automatically added option) 92 | -r5 : play shortcut r5 (http://localhost:7654/dvb-r5.mp3, automatically added option) 93 | -r5x : play shortcut r5x (http://localhost:7654/dvb-r5x.mp3, automatically added option) 94 | -rescan : rescan changes|playlists|clean 95 | -s : select player Sleepy (automatically added option) 96 | -shortcuts : show the configured shortcuts 97 | -show : show a message on the player's display (for 7 seconds) 98 | -silent : redirect standard output to '/dev/null' 99 | -sleep : make the player sleep in minutes (use '0' or '-sleep_clear' to cancel sleep) 100 | -sleep_clear : cancel sleep 101 | -sleeping : report on a player's sleep status (indicated in seconds) 102 | -sleepy : select player Sleepy (automatically added option) 103 | -squeezeplay-pi : select player SqueezeLite-Pi (automatically added option) 104 | -sync : synchronise player with (another player; unreliable) 105 | -syncgroups : show synchronisation groups 106 | -tick : make -listen request all players' playback time every second 107 | -time : jump to an absolute or relative position in the current track (measured in seconds; eg. 10, +30, -30) 108 | -title : show the currently-playing track 109 | -unsync : unsynchronise player (unreliable) 110 | -volume : set the player's volume (absolute or relative, use '?' to query the current volume) 111 | -ws : play shortcut ws (http://localhost:7654/dvb-ws.mp3, automatically added option) 112 | -------------------------------------------------------------------------------- /squeezy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | my $version="0.27"; 4 | my $blerb=" 5 | Squeezy: 6 | A command-line utility for controlling squeezebox network audio players via their squeezeserver. 7 | 8 | Copyright: 9 | Stephen Blott (smblott@gmail.com) 10 | Phillip Camp (phillipcamp@gmail.com) 11 | 12 | License: 13 | MIT license 14 | http://opensource.org/licenses/mit-license.php 15 | 16 | Project home: 17 | - https://github.com/pssc/squeezy 18 | 19 | See also: 20 | squeezy.README.txt 21 | squeezy.conf 22 | squeezy --help 23 | 24 | "; 25 | use strict; 26 | 27 | # 28 | # imports 29 | # 30 | use IO::Socket; 31 | use IO::Select; 32 | use Cwd 'realpath'; 33 | use Encode; 34 | #use Data::Dumper; 35 | 36 | # 37 | # find a configuration file 38 | # 39 | my $conf_base = 'squeezy.conf'; 40 | my $conf_locs = qq( ./$conf_base 41 | $ENV{HOME}/.$conf_base 42 | $ENV{HOME}/.config/squeezy/$conf_base 43 | $ENV{HOME}/.config/$conf_base 44 | /usr/local/etc/$conf_base 45 | /etc/$conf_base ); 46 | 47 | my @conf_file = map { $_ && -r $_ ? $_ : () } split /\s+/, $conf_locs; 48 | my $conf_file = $conf_file[0]; 49 | 50 | # die "squeezy error: could not find configuration file:\n", map { "\t$_\n" } @conf_file 51 | # unless $conf_file; 52 | 53 | # 54 | # some globals 55 | # 56 | my $json_module = "JSON::RPC::Client"; 57 | my $my_name = "/$0"; 58 | $my_name =~ s:^.*/::; 59 | my $server = 'localhost'; 60 | my $port = 9090; 61 | my $port_def = 9090; 62 | my $json = 9000; 63 | my $username = undef; 64 | my $password = undef; 65 | my $group = undef; 66 | my @group; 67 | my %shortcut; 68 | my $default = undef; 69 | my $silent = undef; 70 | my $little = '3'; 71 | my $lot = '10'; 72 | my $prefixpre = ''; 73 | my $prefixpos = ''; 74 | my $tick = undef; 75 | my $player = undef; 76 | my $cpo = undef; 77 | my $def_player = undef; 78 | my $autodiscover = 1; 79 | my $playerdiscover = 1; 80 | my %command; 81 | my @player; 82 | my @servers = (); 83 | my @udp_ports = (3483); 84 | my $DISCOVERY_PERIOD = 1; # discovery period 85 | my $showtime = 7; 86 | my $timeout = $showtime +1; 87 | my $timeout_aws = $timeout+3; 88 | my $delay_aws = 3; 89 | my $retry_aws = 3; 90 | my $json_api_uri_post = "/jsonrpc.js"; 91 | my $json_api_uri_pre = "http://"; 92 | my $socket = undef; 93 | my $action_def = sub{ }; 94 | my $action = $action_def; 95 | my $not = 0; 96 | my $serverss =''; 97 | my $playerss =''; 98 | my $verbose = 0; 99 | my $debug = 0; 100 | my $skip = {}; 101 | 102 | # 103 | # utilities 104 | # 105 | 106 | sub aws_support { 107 | my $soft = shift; 108 | 109 | return 1 if (defined $INC{'Amazon/SQS/Simple.pm'}); 110 | 111 | eval { 112 | require Amazon::SQS::Simple; 113 | Amazon::SQS::Simple->import(); 114 | }; 115 | 116 | if ($@) { 117 | if ( -r "/etc/debian_version" ) { 118 | warn "Install AWS support: sudo apt-get install libamazon-sqs-simple-perl" if ( -r "/etc/debian_version" ); 119 | } else { 120 | warn "Install AWS support: cpan App::cpanminus; cpanm Amazon::SQS::Simple"; 121 | } 122 | die "AWS features not supported: Amazon::SQS::Simple not available" if not $soft; 123 | warn "AWS features not supported: Amazon::SQS::Simple not available... Trying to cope." if $soft > 1; 124 | return 0; 125 | } 126 | return 1; 127 | } 128 | 129 | 130 | sub json_support { 131 | my $soft = shift; 132 | 133 | # check whether module is already loaded 134 | return 1 if (defined $INC{'JSON/RPC/Client.pm'} or defined $INC{'JSON/RPC/Legacy/Client.pm'}); 135 | 136 | eval { 137 | require JSON; 138 | JSON->import(); 139 | }; 140 | 141 | eval { 142 | require JSON::RPC::Client; 143 | JSON::RPC::Client->import(); 144 | }; 145 | if ($@) { 146 | eval { 147 | $json_module = 'JSON::RPC::Legacy::Client'; 148 | require JSON::RPC::Legacy::Client; 149 | JSON::RPC::Legacy::Client->import(); 150 | }; 151 | } 152 | 153 | if ($@) { 154 | if ( -r "/etc/debian_version" ) { 155 | warn "Install JSON support: sudo apt-get install libjson-rpc-perl" if ( -r "/etc/debian_version" ); 156 | } else { 157 | warn "Install JSON support: cpan App::cpanminus; cpanm JSON::RPC::Legacy::Client"; 158 | } 159 | die "JSON features not supported: JSON::RPC::(Legacy::)Client not available" if not $soft; 160 | warn "JSON features not supported: JSON::RPC::(Legacy::)Client not available... Trying to cope." if $soft > 1; 161 | return 0; 162 | } 163 | return 1; 164 | } 165 | 166 | sub urlencode { 167 | my $str = shift; 168 | $str =~ s/([^A-Za-z0-9])/sprintf('%%%02X', ord($1))/seg; 169 | return $str; 170 | } 171 | 172 | sub urldecode { 173 | my $str = shift; 174 | $str =~ s/%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg; 175 | return $str; 176 | } 177 | 178 | sub pick_field 179 | { 180 | return urldecode((split /\s+/, shift)[shift]); 181 | } 182 | 183 | sub notted { 184 | if ($not) { 185 | return not shift 186 | } else { 187 | return shift 188 | } 189 | } 190 | 191 | sub report 192 | { 193 | my $mess = shift; 194 | my $value = shift; 195 | print join(' ', $cpo->{name}|| $player, $mess, $value), "\n"; 196 | return $value; 197 | } 198 | 199 | sub jsonr { 200 | my ($key , $r) = @_; 201 | $r = $r->result; 202 | if (ref($r) eq "HASH") { 203 | print $$r{$key}."\n"; 204 | } else { 205 | print STERR $r; 206 | } 207 | } 208 | 209 | sub jsonp { 210 | my ($key , $r) = @_; 211 | $r = $r->result; 212 | if (ref($r) eq "HASH" and defined $$r{$key}) { 213 | return $$r{$key}; 214 | } else { 215 | print STERR $r; 216 | return undef; 217 | } 218 | } 219 | 220 | sub terminal_cols 221 | { 222 | -t STDOUT || return undef; 223 | return (map { chomp; $_ } `tput cols`)[0]; 224 | } 225 | 226 | # 227 | # server socket commands 228 | # 229 | sub send_command { 230 | # this a syncronous command/response interaction, one line is sent to the 231 | # server and one line is received in response (and returned) 232 | # if hash? FIXME 233 | if (ref($server) eq "HASH" && $server->{HOST} eq 'aws') { 234 | return send_command_aws(@_); 235 | } else { 236 | return send_command_cli(@_); 237 | } 238 | # FIXME json also? 239 | } 240 | 241 | sub aws_recv { 242 | my $recv = shift; 243 | my $timeout = shift; 244 | # Retrieve responce 245 | my @msgs; 246 | foreach my $try (1..$retry_aws) { 247 | eval { 248 | @msgs = $recv->ReceiveMessageBatch(WaitTimeSeconds=>$timeout); 249 | }; 250 | if ($@ && $@ !~ / Forbidden 403/) { 251 | #FIXME do we need to save url for this 252 | sleep $delay_aws; 253 | warn("Recv ",$try," to ",$recv," ",$@); 254 | next; 255 | } 256 | last; 257 | } 258 | if ($#msgs < 0) { 259 | return; 260 | } 261 | my $body = $msgs[$#msgs]->MessageBody(); 262 | $recv->DeleteMessageBatch(\@msgs); 263 | return $body; 264 | } 265 | 266 | sub send_command_aws { 267 | use Time::HiRes; 268 | 269 | # Create an SQS object 270 | print "AWS Access ID $server->{'ACCESS'}\n" if $verbose; 271 | my $sqs = new Amazon::SQS::Simple($server->{'ACCESS'}, $server->{'SECRET'}); 272 | 273 | # Get a queues 274 | my $send = $sqs->GetQueue($server->{'SEND'}); 275 | my $recv = $sqs->GetQueue($server->{'RECV'}); 276 | 277 | # clear rev queue 278 | aws_recv($recv,0); 279 | 280 | # Send command 281 | my $id = 'squeezy.'.$server->{'ACCESS'}.'.'.$$; 282 | my $time = Time::HiRes::time(); 283 | my $response; 284 | foreach my $try (1..$retry_aws) { 285 | eval { 286 | $response = $send->SendMessage(join(" ", @_). "\n",MessageGroupId => $id, MessageDeduplicationId => $id.'.'.$time); 287 | }; 288 | if ($@ && $@ !~ / Forbidden 403/) { 289 | #FIXME do we need to save url for this 290 | sleep $delay_aws; 291 | warn("Send Try ",$try," for ",$send," ",$@); 292 | next; 293 | } 294 | last; 295 | } 296 | if (not defined $response) { 297 | return; 298 | } 299 | 300 | my $body = aws_recv($recv,$timeout_aws); 301 | chomp $body; 302 | return $body; 303 | } 304 | 305 | 306 | sub send_command_cli { 307 | if ( ! $socket ) 308 | { 309 | $socket = IO::Socket::INET -> new ( PeerAddr => $server, 310 | PeerPort => $port, 311 | Timeout => $timeout, 312 | Proto => 'tcp' ) 313 | or 314 | (warn "squeezy error: could not connect to '$server' port '$port'\n" and return -1); 315 | 316 | if ( $username ) 317 | { 318 | if ( ! $password ) 319 | { die "squeezy error: username provided, but no password provided (username='$username')\n"; } 320 | print $socket "login $username $password\n"; 321 | # ignore the following response; even with an incorrect 322 | # username/password the server happily echos it back 323 | my $response = <$socket>; 324 | } 325 | # force some unnecessary client/server interaction in order to detect 326 | # an authentication failure 327 | print $socket "player count ?\n"; 328 | if ( ! $socket->connected() || ! <$socket> ) 329 | { die "squeezy error: authentication failed (username='$username', password='$password')\n"; } 330 | } 331 | 332 | if ( ! $socket->connected() ) 333 | { die "squeezy error: server dropped socket connection\n"; } 334 | 335 | print $socket join(" ", @_), "\n"; 336 | 337 | ## FIXME some commands produce no responce... so timoeut needed 338 | my $response =""; 339 | my $wait = IO::Select->new($socket); 340 | if (my ($found) = $wait->can_read($timeout)) { 341 | $response = <$socket>; 342 | } 343 | 344 | if ( ! $socket->connected() ) 345 | { die "squeezy error: server dropped socket connection\n"; } 346 | 347 | chomp $response; 348 | return $response; 349 | } 350 | 351 | =head2 silmrequest ( $server, $player, @command_and_ags ) 352 | 353 | query using json slim request interface. 354 | 355 | =cut 356 | 357 | sub slimrequest { 358 | my ($server, $player, $command_and_ags) = @_; 359 | json_support(); 360 | 361 | my $client = $server->{JSONC} ? $server->{JSONC} : new $json_module; 362 | my $uri = $json_api_uri_pre.($server->{IP}||$server->{HOST}).":".($server->{JSON}||$json).$json_api_uri_post; 363 | my $o = { 364 | method => 'slim.request', 365 | version => '1.0', 366 | id => "squeezy.$version.$$", 367 | params => [ ($player->{pid} || $player->{name} || "") ,$command_and_ags ], 368 | }; 369 | 370 | if ( $server->{HOST} eq 'aws' ) { 371 | use if ($server->{HOST} eq 'aws'), "JSON"; 372 | use if ($server->{HOST} eq 'aws'), "Class::Struct"; 373 | struct( DUMMY_RO => { jsontext => '$', result => '*%' }); 374 | my $c = JSON::encode_json($o); 375 | print $c,"\n" if ($debug); 376 | my $resp = send_command_aws(JSON::encode_json($o)); 377 | print $resp,"\n" if ($debug); 378 | return new DUMMY_RO(jsontext => $resp, result => { error => 'No json object returned' }) if ($resp !~ /^{.*}$/);#die? 379 | my $ro = JSON::decode_json($resp); 380 | return new DUMMY_RO(jsontext => $resp, result => $ro->{result}); 381 | } else { 382 | $client->version($o->{version}); # RPC version 383 | $client->id($o->{id}); 384 | my $res = $client->call($uri, $o); 385 | # FIXME error warn or die? carp? 386 | if ($res) { 387 | if ($res->is_error) { 388 | return "Error : ". $res->error_message; 389 | } else { 390 | return $res; 391 | } 392 | } else { 393 | return $client->status_line."\n"; 394 | } 395 | } 396 | } 397 | 398 | # 399 | # read configuration file 400 | # 401 | 402 | if ( -r $conf_file ) 403 | { 404 | open CONFIG, "<$conf_file" 405 | or die "squeezy: open of configuration file failed ($conf_file)\n"; 406 | 407 | foreach ( ) 408 | { 409 | s/#.*//; # strip comments 410 | s/^\s*//; # strip leading whitespace 411 | s/\s*$//; # strip trailing whitespace 412 | $_ || do next; # skip empty lines 413 | 414 | my @arg = split /\s+/; 415 | 416 | $arg[0] eq 'debug' && do { $debug = 1; next; }; 417 | $arg[0] eq 'verbose' && do { $verbose = 1; next; }; 418 | $arg[0] eq 'server' && $#arg == 1 && do { $server = $arg[1]; push @servers, { HOST => $arg[1] }; next; }; 419 | $arg[0] eq 'server' && $#arg == 2 && do { $server = $arg[1]; push @servers, { HOST => $arg[1], PORT => $arg[2]}; next; }; 420 | $arg[0] eq 'server' && $#arg == 3 && do { $server = $arg[1]; push @servers, { HOST => $arg[1], PORT => $arg[2], JSON => $arg[3]}; next; }; 421 | $arg[0] eq 'server' && $#arg == 6 && do { $server = $arg[1]; push @servers, { NAME => $arg[1], HOST => $arg[2], ACCESS=>$arg[3], SECRET=>$arg[4], SEND=>$arg[5], RECV=>$arg[6]}; aws_support(); next; }; 422 | $arg[0] eq 'port' && $#arg == 1 && do { $port = $arg[1]; next; }; 423 | $arg[0] eq 'json' && $#arg == 1 && do { $json = $arg[1]; next; }; 424 | $arg[0] eq 'username' && $#arg == 1 && do { $username = $arg[1]; next; }; 425 | $arg[0] eq 'password' && $#arg == 1 && do { $password = $arg[1]; next; }; 426 | 427 | $arg[0] eq 'player' 428 | && do 429 | { if ( $#arg == 1 ) { push @{$group[0]}, { name=>$arg[1]}; next; } 430 | if ( $#arg == 2 ) { push @{$group[$arg[1]]}, { name=>$arg[2]}; next; } 431 | if ( $#arg == 3 ) { push @{$group[$arg[1]]}, { name=>$arg[2], server=>{HOST=>$arg[3]}}; next; } 432 | if ( $#arg == 4 ) { push @{$group[$arg[1]]}, { name=>$arg[2], server=>{HOST=>$arg[3]}, pid=>$arg[4]}; next; }; }; 433 | 434 | $arg[0] eq 'shortcut' && $#arg == 2 435 | && do 436 | { $default = $arg[2] unless $default; 437 | $shortcut{$arg[1]} = $arg[2]; 438 | next; }; 439 | 440 | $arg[0] eq 'small_volume' && $#arg == 1 && $arg[1] =~ m/^[0-9]+$/ && do { $little = $arg[1]; next; }; 441 | $arg[0] eq 'large_volume' && $#arg == 1 && $arg[1] =~ m/^[0-9]+$/ && do { $lot = $arg[1]; next; }; 442 | 443 | $arg[0] eq 'prefixpre' 444 | && do 445 | { if ( $#arg == 0 ) { next; } 446 | if ( $#arg == 1 ) { $prefixpre = $arg[1]; next; } }; 447 | 448 | $arg[0] eq 'prefixpos' 449 | && do 450 | { if ( $#arg == 0 ) { next; } 451 | if ( $#arg == 1 ) { $prefixpos = $arg[1]; next; } }; 452 | 453 | $arg[0] eq 'autodiscover' 454 | && do 455 | { if ( $#arg == 0 ) { next; } 456 | if ( $#arg == 1 ) { $autodiscover = (lc($arg[1]) eq 'yes') ? 1 : 0; next; } }; 457 | 458 | $arg[0] eq 'playerdiscover' 459 | && do 460 | { if ( $#arg == 0 ) { next; } 461 | if ( $#arg == 1 ) { $playerdiscover = (lc($arg[1]) eq 'yes') ? 1 : 0; next; } }; 462 | 463 | $arg[0] eq 'discoverperiod' 464 | && do 465 | { if ( $#arg == 0 ) { next; } 466 | if ( $#arg == 1 ) { $DISCOVERY_PERIOD = $arg[1]; next; } }; 467 | 468 | die "error in $conf_file: $_\n" 469 | } 470 | 471 | close CONFIG; 472 | } 473 | 474 | 475 | 476 | if ( $autodiscover ) { 477 | foreach my $port (@udp_ports) { 478 | socket(my $socket, AF_INET, SOCK_DGRAM, getprotobyname('udp')); 479 | setsockopt($socket, SOL_SOCKET, SO_BROADCAST, 1); 480 | my $destpaddr = sockaddr_in($port, INADDR_BROADCAST); 481 | send($socket, "eIPAD\0NAME\0JSON\0VERS\0UUID\0CLIP\0", 0, $destpaddr); 482 | my $wait = IO::Select->new($socket); 483 | 484 | while (my ($found) = $wait->can_read($DISCOVERY_PERIOD)) { 485 | my $clientpaddr = recv($socket, my $msg, 1600, 0); 486 | # chop of leading character 487 | $msg = substr($msg, 1); 488 | 489 | my $len = length($msg); 490 | my ($tag, $len2, $val); 491 | 492 | my $bag = {}; 493 | 494 | while ($len > 0) { 495 | $tag = substr($msg, 0, 4); 496 | $len2 = unpack("xxxxC", $msg); 497 | $val = $len2 ? substr($msg, 5, $len2) : undef; 498 | 499 | print(" TLV: $tag len: $len2, $val\n") if $debug; 500 | 501 | $bag->{$tag} = $val; 502 | 503 | $msg = substr($msg, $len2 + 5); 504 | $len = $len - $len2 - 5; 505 | } 506 | 507 | # get server's IP address 508 | if ($clientpaddr) { 509 | my ($portno, $ipaddr) = sockaddr_in($clientpaddr); 510 | $bag->{IP} = inet_ntoa($ipaddr); 511 | $bag->{HOST} = gethostbyaddr($ipaddr, AF_INET); 512 | $bag->{PORT} = $bag->{CLIP} if (defined $bag->{CLIP}); 513 | if ((not defined $bag->{PORT}) && json_support(2)) { 514 | my $cliport = jsonp('_p2',slimrequest($bag,undef,["pref" ,"plugin.cli:cliport","?" ])); 515 | $bag->{PORT} = $cliport if $cliport; 516 | } 517 | push @servers, $bag; 518 | } 519 | } 520 | close $socket; 521 | } 522 | } 523 | 524 | if (scalar(@servers) == 0 ) { 525 | push @servers, { HOST=>$server, PORT=>$port}; 526 | } elsif (scalar(@servers) > 1 ) { 527 | $serverss = 'servers: '. join(', ', map { $_->{NAME}.":".($_->{HOST} || "uknown").":".($_->{IP} ||$_->{SEND}|| "unkown") } @servers) ."\n"; 528 | } 529 | 530 | if ($playerdiscover) { 531 | # ask squeezeservers for the names of players 532 | foreach my $host (@servers) { 533 | if ($host->{HOST} eq 'aws' and $host->{SEND}) { 534 | $server = $host 535 | } else { 536 | $server = $host->{IP} || $host->{HOST}; 537 | } 538 | $port = $host->{PORT} || $port_def; 539 | 540 | my $count = pick_field 2, send_command 'player count ?'; 541 | if ( $count ) 542 | { 543 | for (my $i=0; $i<$count; $i+=1) 544 | { 545 | my $pn = pick_field 3, send_command "player name $i ?"; 546 | my $pid = pick_field 3, send_command "player id $i ?"; 547 | if ( $pn && $pn ne '?' ) { 548 | push @{$group[0]}, { name=>$pn, pid=>$pid , server=>$host } 549 | } else { 550 | if ( $pid && $pid ne '?' ) 551 | { push @{$group[0]}, { name=>$pid, server=>$host} } 552 | warn "Player discovery playname issue $i:$pn:$pid" 553 | } 554 | } 555 | if ( $group[0][0] ) 556 | { 557 | my $name = $host->{NAME} || $host->{HOST} || $host->{IP}; 558 | if (scalar(@servers) eq 1 ) { 559 | $name = ''; 560 | } else { 561 | $name = "(".$name.")"; 562 | } 563 | $playerss.= 'players auto discovered: '. join(', ', map { $_->{name}.$name } @{$group[0]}). "\n"; 564 | } 565 | } 566 | $host->{SOCKET} = $socket; 567 | $socket = undef; 568 | } 569 | if ( $group[0][0] ) { 570 | $playerss = 'players auto discovered: '. join(', ', map { $_->{name}.(scalar(@servers) eq 1? '':"(".($_->{server}->{NAME} || $_->{server}->{HOST} || $_->{server}->{IP}).")") } @{$group[0]}). "\n"; 571 | } 572 | } 573 | 574 | 575 | 576 | if ( $username && ! $password ) 577 | { die "squeezy configuration error: username is set ($username) but password is unset\n"; } 578 | 579 | 580 | # find a default player 581 | while ( ! $def_player ) 582 | { 583 | @player = map { @$_ } @group; 584 | $def_player = $player[0]; 585 | if ( !$def_player ) 586 | { 587 | #print STDERR "squeezy warning: no players specified or detected\n"; 588 | #$group[0][0] = 'NO_PLAYERS_THIS_WILL_NOT_WORK'; 589 | last; 590 | } 591 | } 592 | 593 | # push a group containing all players 594 | push @group, [ @player ]; 595 | 596 | # 597 | # strip any trailing '/'s from $prefixpre and $prefixpos 598 | # 599 | 600 | if ( $prefixpre ) { $prefixpre =~ s:/+$::; } 601 | if ( $prefixpos ) { $prefixpos =~ s:/+$::; } 602 | 603 | # 604 | # listen output wrapper functions 605 | # 606 | 607 | sub listen_output 608 | { 609 | # this is just an output hook, it's somewhere to put post-processing 610 | # triggers for the -listen output 611 | # 612 | # if we see a "new song", then request full status for that player 613 | # 614 | if ( $_[1] eq 'playlist' && $_[2] eq 'newsong' ) 615 | { 616 | print $socket "$_[0] status - 1\n"; 617 | } 618 | 619 | print STDOUT join(' ', @_), "\n"; 620 | } 621 | 622 | sub listen 623 | { 624 | my $count = pick_field 2, send_command 'player count ?'; 625 | listen_output 'player', 'count', $count; 626 | 627 | my @ids; 628 | for (my $i=0; $i<$count; $i+=1) 629 | { 630 | my $id = pick_field 3, send_command "player id $i ?"; 631 | push @ids, $id; 632 | listen_output 'player', 'id', $i, $id; 633 | } 634 | 635 | # the &send_command() calls above force $socket to be open by here 636 | my $select = IO::Select->new($socket); 637 | STDOUT->autoflush(1); 638 | 639 | sub socket_input 640 | { 641 | my $wait = shift; 642 | 643 | while ( $select->can_read($wait) ) 644 | { 645 | my $line = <$socket>; 646 | chomp $line; 647 | my @line = split(/\s+/, $line, 3); 648 | 649 | if ( $line[1] eq 'status' ) 650 | { 651 | # 'status' lines contain many fields, so parse them here to make 652 | # them easier to deal with down stream 653 | my @prefix = (urldecode($line[0]), 'status'); 654 | listen_output @prefix, 'start', 'status'; 655 | foreach ( map { urldecode($_) } split(/\s+/, $line) ) 656 | { listen_output @prefix, split(/:/, $_, 2); } 657 | listen_output @prefix, 'end', 'status'; 658 | next; 659 | } 660 | 661 | listen_output map { urldecode($_) } split(/\s+/, $line); 662 | } 663 | }; 664 | 665 | # from here on we print() directly to the socket and sprinkle in 666 | # socket_input() calls to catch the ouput; syncronous send_command() calls 667 | # will no loger work 668 | 669 | for (my $i=0; $i<$count; $i+=1) 670 | { 671 | print $socket "player name $ids[$i] ?\n"; 672 | socket_input(0); # 0 here means do not block 673 | } 674 | 675 | for (my $i=0; $i<$count; $i+=1) 676 | { 677 | print $socket "$ids[$i] status - 1\n"; 678 | socket_input(0); # 0 here means do not block 679 | } 680 | 681 | print $socket "listen 1\n"; 682 | while ( $socket->connected() ) 683 | { 684 | socket_input(1); # 1 here means block for 1 second 685 | if ( $tick ) 686 | { 687 | for (my $i=0; $i<$count; $i+=1) 688 | { 689 | print $socket "$ids[$i] time ?\n"; 690 | socket_input(0); # 0 here means do not block 691 | } 692 | } 693 | } 694 | 695 | print STDERR "squeezy error: lost socket connection\n"; 696 | $socket = undef; 697 | } 698 | 699 | sub idFromName { 700 | my $name = shift; 701 | 702 | foreach my $p (@{$group[ $#group ] }) { 703 | if ( ($p->{name} eq $name && $p->{pid} ) || $name eq $p->{pid} ) { 704 | return $p->{pid}; 705 | } 706 | } 707 | return undef; 708 | } 709 | 710 | # ###################################################################### 711 | # commands 712 | # 713 | 714 | sub command 715 | { 716 | my $command = shift; 717 | if ( $command{$command}->{deprecated} ) { 718 | warn "Commmand ",$command," deprecated.\n"; 719 | } 720 | if ( $command{$command}->{json} ) { 721 | $command{$command}->{command}(shift,shift,@_); 722 | } else { 723 | $command{$command}->{command}(@_); 724 | } 725 | } 726 | 727 | ## Warning CLI API supports Using the name insead of the ID but only!! 728 | # if its the first arg IE 0 729 | %command = 730 | ( 731 | 732 | '-add' => 733 | { 734 | help => 'add the indicated file, directory (contents), playlist or url to the playlist', 735 | do_shift => 1, 736 | command => sub { command -play, shift, 'add'; } 737 | }, 738 | 739 | '-alarms' => 740 | { 741 | help => 'enabled alarms count (can only sound if not generally disabled)', 742 | command => sub { report 'enabled alarms', pick_field 6, send_command urlencode($player)." alarms 0 99 filter:enabled"; } 743 | }, 744 | 745 | '-alarms_enabled' => 746 | { 747 | help => 'alarms generally enabled?', 748 | command => sub { report 'alarms enabled', pick_field 3, send_command urlencode($player)." playerpref alarmsEnabled ?"; } 749 | }, 750 | 751 | '-all' => 752 | { 753 | help => 'apply subsequent commands to all players (eg. squeezy -all -off)', 754 | command => sub { $group = $#group; } 755 | }, 756 | 757 | '-button' => 758 | { 759 | help => 'send a button key command to the player (see the server\'s "Default.map" file)', 760 | do_shift => 1, 761 | command => sub { send_command urlencode($player), 'button', $_[0]; } 762 | }, 763 | 764 | '-cliport' => 765 | { 766 | help => "query cli port VIA JSON", 767 | json => 1, 768 | server => 1, 769 | command => sub { jsonr("_p2",slimrequest(shift,undef,["pref" ,"plugin.cli:cliport","?" ])); }, 770 | }, 771 | 772 | '-connected' => 773 | { 774 | help => 'is player connected?', 775 | command => sub { report 'connected', pick_field( 2, send_command urlencode($player)." connected ?"); } 776 | }, 777 | 778 | '-default' => 779 | { 780 | help => "play the default shortcut (the first one in the configuration file, currently $default)", 781 | command => 782 | sub 783 | { 784 | if ( ! $default ) 785 | { die 'squeezy -default: no shortcuts in configuration file, so no default available\n'; } 786 | command -play, $default; 787 | } 788 | }, 789 | 790 | '-if_alarm' => 791 | { 792 | help => 'if alarms are enabled generally and an alarm is enabled', 793 | command => 794 | sub { if (notted( my $r = (command('-alarms_enabled') eq "1" && command('-alarms') ne 'count:0'))) 795 | { print $cpo->{name}," has ",$r ? "" : "no ","active alarm \n"; &$action; } } 796 | }, 797 | 798 | '-die_if_playing' => 799 | { 800 | help => 'if this player is on and playing, then die immediately; exit code 1', 801 | deprecated => 1, 802 | command => 803 | sub { if ( command('-power') && command('-mode') eq 'play' ) 804 | { print $cpo->{name}," is on and playing, exiting immediately\n"; exit(1); } } 805 | }, 806 | 807 | '-elapsed' => 808 | { 809 | help => 'show the elapsed time in currently-playing track/url', 810 | command => sub { report 'time', pick_field 2, send_command urlencode($player)." time ?"; } 811 | }, 812 | 813 | '-exit' => 814 | { 815 | help => 'set up action for -if_XXXX to exit with specified code', 816 | do_shift => 1, 817 | command => sub { my $ecode = shift ;$action = sub { print "Exiting $ecode\n" if $verbose; exit($ecode); }}, 818 | }, 819 | 820 | '-skip'=> 821 | { 822 | help => 'set up action for -if_XXXX to skip to next player', 823 | command => sub { $action = sub { print "Skipping ",$cpo->{name},"\n" if $verbose; $skip->{$player}=1; $action = $action_def; &$action; }}, 824 | }, 825 | 826 | '-not' => 827 | { 828 | help => '-if_XXXX can be prefixed with not', 829 | command => sub { $not = 1 }, 830 | }, 831 | 832 | '-if_connected' => 833 | { 834 | help => 'if this player is not connected', 835 | command => sub { if (notted( my $r = command('-connected') eq 1) ) 836 | { print $cpo->{name}," is ",$r ? "" : "not " ,"connected\n"; &$action; } } 837 | }, 838 | 839 | '-if_playing' => 840 | { 841 | help => 'if this player is on and playing', 842 | command => sub { if (notted( my $r = (command('-power') eq "1" && command('-mode') eq 'play')) ) 843 | { print $cpo->{name}," is ",$r ? "on and" : "not" ," playing,\n"; &$action; } } 844 | }, 845 | 846 | '-if_sleeping' => 847 | { 848 | help => 'if this player is on and is timing down to sleep', 849 | command => 850 | sub { if ( notted( my $r = (command('-power') eq "1" && command('-sleeping') ) ) ) 851 | { print $cpo->{name}," is ",$r ? "on and" : "not" ," timing down to sleep\n"; &$action; } } 852 | }, 853 | 854 | '-exit_if_playing' => 855 | { 856 | help => 'if this player is on and playing, then exit immediately; exit code 0', 857 | deprecated => 1, 858 | command => 859 | sub { if ( command('-power') && command('-mode') eq 'play') 860 | { print $cpo->{name}," is on and playing, exiting immediately\n"; exit(0); } } 861 | }, 862 | 863 | '-exit_if_sleeping' => 864 | { 865 | help => 'if this player is on and is timing down to sleep, then exit immediately; exit code 0', 866 | deprecated => 1, 867 | command => 868 | sub { 869 | if ( command('-power') && command('-sleeping') ) 870 | { print $cpo->{name}," is on and timing down to sleep, exiting immediately\n"; exit(0); } } 871 | }, 872 | 873 | '-group' => 874 | { 875 | help => 'apply subsequent command to all players in this group (eg. squeezy -group 1)', 876 | do_shift => 1, 877 | command => 878 | sub 879 | { 880 | if ( ! ($_[0] =~ m/^[0-9]+$/ ) ) 881 | { die "squeezy -group: not a number ($_[0])\n"; } 882 | if ( $#group < $_[0] ) 883 | { die "squeezy -group: invalid group number ($_[0], max is $#group)\n"; } 884 | $group = shift; 885 | } 886 | }, 887 | 888 | '-groups' => 889 | { 890 | help => 'show the configured player groups', 891 | command => 892 | sub 893 | { 894 | for (my $i=0; $i<=$#group; $i+=1) 895 | { 896 | print "group $i:\n"; 897 | foreach my $g ( @{$group[$i]} ) 898 | { print " ".$g->{name}."\n"; } 899 | } 900 | } 901 | }, 902 | 903 | '-help' => 904 | { 905 | help => 'show this help message', 906 | server => 1, 907 | command => 908 | sub 909 | { 910 | print "squeezy $version\n",$blerb; 911 | print "configuration file:\n $conf_file\n"; 912 | print "\nconfiguration file search locations:\n"; 913 | foreach ( map { $_ ? $_ : () } split /\s+/, $conf_locs ) 914 | { print " $_\n"; } 915 | print "\ncommand-line options:\n"; 916 | foreach my $command ( sort keys %command ) 917 | { 918 | if ( $command{$command}->{help} and not $command{$command}->{deprecated}) 919 | { 920 | my $argument = $command{$command}->{do_shift} ? ' ' : ''; 921 | print sprintf "%-18s: %s\n", "$command$argument", $command{$command}->{help}; 922 | } 923 | } 924 | } 925 | }, 926 | 927 | '-json' => 928 | { 929 | help => "send command via JSON interface quoted as one argument, eg 'player count ?' returns JSON result text", 930 | json => 1, 931 | server => 1, 932 | do_shift => 1, 933 | command => sub { 934 | my $r = slimrequest(shift,shift,[split(/\s+/,shift,-1)]); 935 | if ( $verbose ) { 936 | # full request + responce 937 | print $r->jsontext,"\n"; 938 | } else { 939 | print JSON::encode_json($r->result),"\n"; 940 | } 941 | } 942 | }, 943 | 944 | '-jump' => 945 | { 946 | help => 'jump to a track on the current playlist (relative or absolute, eg. 3, +2, -4)', 947 | do_shift => 1, 948 | command => 949 | sub 950 | { 951 | my $target = shift; 952 | if ( ! ( $target =~ m/^[+-]?[0-9]{1,}$/ ) ) 953 | { die "squeezy -jump: invalid jump specifier ($target)\n"; } 954 | my $count = command -playlist_length; 955 | my $index = command -playlist_index; 956 | report 'new track', pick_field 3, send_command urlencode($player)." playlist index $target"; 957 | command -playing; 958 | } 959 | }, 960 | 961 | '-listen' => 962 | { 963 | help => 'listen to squeezeserver activity (on standard output, see also \'-tick\')', 964 | command => sub { &listen(); } 965 | }, 966 | 967 | '-louder' => 968 | { 969 | help => "increase the volume slightly (by $little%)", 970 | command => sub { command -volume, "+$little"; } 971 | }, 972 | 973 | '-Louder' => 974 | { 975 | help => "increase the volume significantly (by $lot%)", 976 | command => sub { command -volume, "+$lot" } 977 | }, 978 | 979 | '-mode' => 980 | { 981 | help => 'show the player\'s mode ("play", "stop" or "pause")', 982 | command => 983 | sub 984 | { 985 | command -power; # FIXME needed? 986 | report 'mode', pick_field 2, send_command urlencode($player)." mode ?"; 987 | } 988 | }, 989 | 990 | '-mute' => 991 | { 992 | # this is not real "muting"; it should be possible subsequently to 993 | # unmute, returning to the original volume; this doesn't work on 994 | # my players (perhps because they're old), so I took the brute 995 | # force approach here --> just set the volume to 0 996 | help => 'set volume to 0', 997 | command => sub { command -volume, '0'; } 998 | }, 999 | 1000 | '-next' => 1001 | { 1002 | help => 'jump to the next track on the playlist', 1003 | command => 1004 | sub 1005 | { 1006 | send_command urlencode($player)." playlist index +1"; 1007 | command -playing; 1008 | } 1009 | }, 1010 | 1011 | '-on' => 1012 | { 1013 | help => 'turn player on and start playing the current playlist/track', 1014 | command => 1015 | sub 1016 | { 1017 | send_command urlencode($player)." power 1"; 1018 | send_command urlencode($player)." pause 0"; 1019 | command -power; 1020 | command -mode; 1021 | command -playing; 1022 | } 1023 | }, 1024 | 1025 | '-off' => 1026 | { 1027 | help => 'turn player off', 1028 | command => 1029 | sub 1030 | { 1031 | send_command urlencode($player)." power 0"; 1032 | command -power; 1033 | } 1034 | }, 1035 | 1036 | '-options' => 1037 | { 1038 | help => 'list all options to standard output (useful for configuring shell completion)', 1039 | server => 1, 1040 | command => sub { 1041 | foreach my $command ( sort keys %command ) 1042 | { 1043 | if ( not $command{$command}->{deprecated}) 1044 | { 1045 | print $command," "; 1046 | } 1047 | } 1048 | print "\n"; 1049 | } 1050 | }, 1051 | 1052 | '-pause' => 1053 | { 1054 | help => 'toggle pause', 1055 | command => 1056 | sub 1057 | { 1058 | send_command urlencode($player)." pause"; 1059 | command -mode; 1060 | } 1061 | }, 1062 | 1063 | '-play' => 1064 | { 1065 | help => 'play the indicated file, directory (contents), playlist or url', 1066 | do_shift => 1, 1067 | command => 1068 | sub 1069 | { 1070 | my $play = shift; 1071 | my $oper = shift || 'play'; # can also be 'add' 1072 | if ( -r $play ) 1073 | { 1074 | $play = realpath($play); 1075 | $play =~ s:^$prefixpre::; # $play should still have a leading '/' here 1076 | $play = "$prefixpos$play"; 1077 | } 1078 | $play = urlencode $play; 1079 | send_command urlencode($player)." playlist $oper $play"; 1080 | command -playing; 1081 | } 1082 | }, 1083 | 1084 | '-player_count' => 1085 | { 1086 | help => 'show the number of connected players', 1087 | server => 1, 1088 | command => sub { report 'player count', pick_field 2, send_command 'player count ?'; } 1089 | }, 1090 | 1091 | '-player_name' => 1092 | { 1093 | help => 'show the name of the player', 1094 | do_shift => 1, 1095 | command => sub { 1096 | print 'player name ', pick_field 3,send_command "player name ".urlencode(shift)." ?"; 1097 | print "\n" 1098 | } 1099 | }, 1100 | 1101 | '-player_id' => 1102 | { 1103 | ## not the real player id command as this would need a index or ID we get the name back on failure ie no id. 1104 | help => 'show the id of the player', 1105 | command => sub { 1106 | my $pid = $cpo->{pid} || pick_field 0,send_command urlencode($player); 1107 | if ($pid eq $cpo->{name}) { 1108 | report 'player id', pick_field 0, "None($cpo->{pid})"; 1109 | } else { 1110 | report 'player id', pick_field 0, $pid; 1111 | } 1112 | } 1113 | }, 1114 | 1115 | '-player_ip' => 1116 | { 1117 | ## Needs to be id or index not name if not vaild id we just get the fist players IP back... so check vaild id? 1118 | help => 'show the ip address of the player', 1119 | command => sub { 1120 | my $pid = $cpo->{pid} || pick_field 0,send_command urlencode($player); 1121 | if ($pid eq $cpo->{name} ) { 1122 | report 'player IP', pick_field 0, "None"; 1123 | } else { 1124 | report 'player IP', pick_field 3, send_command "player ip ".urlencode($pid)." ?"; 1125 | } 1126 | } 1127 | }, 1128 | 1129 | '-playlist' => 1130 | { 1131 | help => 'show the current playlist', 1132 | command => 1133 | sub 1134 | { 1135 | my $count = command -playlist_length; 1136 | my $index = command -playlist_index; 1137 | my $cols = terminal_cols(); 1138 | for (my $i=0; $i<$count; $i+= 1) 1139 | { 1140 | my $album = pick_field 4, send_command urlencode($player)." playlist album $i ?"; 1141 | my $artist = pick_field 4, send_command urlencode($player)." playlist artist $i ?"; 1142 | my $title = pick_field 4, send_command urlencode($player)." playlist title $i ?"; 1143 | if ( $cols ) 1144 | { 1145 | my $left = sprintf "%s %-3d %s", $i == $index ? '>' : ' ', $i, $title; 1146 | my $right = sprintf "%${cols}s", "$album - $artist"; 1147 | my $line = sprintf "%s %s", $left, substr($right, length($left)+1); 1148 | print substr($line,0,$cols), "\n"; 1149 | } 1150 | else 1151 | { 1152 | print $i == $index ? '> ' : ' '; 1153 | print sprintf "%-3d ", $i; 1154 | print "$title; $album - $artist\n"; 1155 | } 1156 | } 1157 | } 1158 | }, 1159 | 1160 | '-playlist_delete' => 1161 | { 1162 | help => 'delete (or remove) an item from the current playlist (eg. \'-playlist_delete 3\')', 1163 | do_shift => 1, 1164 | command => 1165 | sub 1166 | { 1167 | my $target = shift; 1168 | if ( ! ( $target =~ m/^[0-9]{1,}$/ ) ) 1169 | { die "squeezy -playlist_delete: invalid playlist specifier ($target)\n"; } 1170 | my $count = command -playlist_length; 1171 | if ( $count <= $target ) 1172 | { die "squeezy -playlist_delete: invalid playlist specifier, too big ($target)\n"; } 1173 | report 'playlist delete', pick_field 3, send_command urlencode($player)." playlist delete $target"; 1174 | command -playing; 1175 | } 1176 | }, 1177 | 1178 | '-playlist_index' => 1179 | { 1180 | help => 'show the index of the current song in the playlist', 1181 | command => sub { report 'playlist index', pick_field 3, send_command urlencode($player)." playlist index ?"; } 1182 | }, 1183 | 1184 | '-playlist_length' => 1185 | { 1186 | help => 'show the length of the current playlist', 1187 | command => sub { report 'playlist length', pick_field 3, send_command urlencode($player)." playlist tracks ?"; } 1188 | }, 1189 | 1190 | '-playlist_repeat' => 1191 | { 1192 | help => 'playlist repeat <0|1|2|?|none|song|all|query|toggle>', 1193 | do_shift => 1 , 1194 | command => sub { my $arg = shift; 1195 | if ( not($arg eq '?' or $arg eq 0 or $arg eq 1 or $arg eq 2 ) ) { 1196 | if ( $arg eq 'none' ) { 1197 | $arg = 0 1198 | } elsif ( $arg eq 'query' ) { 1199 | $arg = '?'; 1200 | } elsif ( $arg eq 'song' ) { 1201 | $arg = 1 1202 | } elsif ( $arg eq 'all' ) { 1203 | $arg = 2 1204 | } else { 1205 | $arg = ''; 1206 | } 1207 | } 1208 | report 'playlist repeat', pick_field 3, send_command urlencode($player)." playlist repeat $arg"; 1209 | if ($arg eq '') { 1210 | command -playlist_repeat, "?"; 1211 | } 1212 | } 1213 | }, 1214 | 1215 | '-playlist_shuffle' => 1216 | { 1217 | help => 'playlist shuffle <0|1|2|?|none|song|album|query|toggle>', 1218 | do_shift => 1, 1219 | command => sub { my $arg = shift; 1220 | if ( not($arg eq '?' or $arg eq 0 or $arg eq 1 or $arg eq 2 ) ) { 1221 | if ( $arg eq 'none' ) { 1222 | $arg = 0 1223 | } elsif ( $arg eq 'query' ) { 1224 | $arg = '?'; 1225 | } elsif ( $arg eq 'song' ) { 1226 | $arg = 1 1227 | } elsif ( $arg eq 'album' ) { 1228 | $arg = 2 1229 | } else { 1230 | $arg = ''; 1231 | } 1232 | } 1233 | report 'playlist shuffle', pick_field 3, send_command urlencode($player)." playlist shuffle $arg"; 1234 | if ($arg eq '') { 1235 | command -playlist_shuffle, "?"; 1236 | } 1237 | } 1238 | }, 1239 | 1240 | '-player_model' => 1241 | { 1242 | ## Needs to be id or index not name 1243 | help => 'show the model of the player', 1244 | command => sub { report 'player model', pick_field 3, send_command "player model ".urlencode($cpo->{pid})." ?"; } 1245 | }, 1246 | 1247 | '-players' => 1248 | { 1249 | help => 'show all configured players', 1250 | command => sub { print map { $_->{name}."\n" } @player; } 1251 | }, 1252 | 1253 | '-playing' => 1254 | { 1255 | help => 'show the currently-playing track', 1256 | command => sub { 1257 | report 'genre', pick_field 2, send_command urlencode($player)." genre ?"; 1258 | report 'artist', pick_field 2, send_command urlencode($player)." artist ?"; 1259 | report 'album', pick_field 2, send_command urlencode($player)." album ?"; 1260 | command -title; 1261 | report 'duration', pick_field 2, send_command urlencode($player)." duration ?"; 1262 | command '-time', '?'; 1263 | report 'remote', pick_field 2, send_command urlencode($player)." remote ?"; 1264 | command -current_title; 1265 | report 'path', pick_field 2, send_command urlencode($player)." path ?"; 1266 | } 1267 | }, 1268 | 1269 | '-title' => 1270 | { 1271 | help => 'show the track title ', 1272 | command => sub { report 'title', pick_field 2, send_command urlencode($player)." title ?"; } 1273 | }, 1274 | 1275 | '-current_title' => 1276 | { 1277 | help => 'show the current title', 1278 | command => sub { report 'current_title', pick_field 2, send_command urlencode($player)." current_title ?"; } 1279 | }, 1280 | 1281 | '-power' => 1282 | { 1283 | help => 'show whether power is on or off', 1284 | command => sub { report 'power', pick_field 2, send_command urlencode($player)." power ?"; } 1285 | }, 1286 | 1287 | '-previous' => 1288 | { 1289 | help => 'jump to the previous track on the playlist', 1290 | command => 1291 | sub 1292 | { 1293 | send_command urlencode($player)." playlist index -1"; 1294 | command -playing; 1295 | } 1296 | }, 1297 | 1298 | '-print_links' => 1299 | { 1300 | help => 'show a list of commands suitable for creating player pseudonyms for the squeezy command', 1301 | command => 1302 | sub 1303 | { 1304 | my $squeezy = $0; # not $my_name 1305 | my $dir = $squeezy; 1306 | $dir =~ s:/[^/]+$::; 1307 | foreach ( map { lc $_->{name} } @player ) 1308 | { print "ln -vf \"$squeezy\" \"$dir/$_\"\n"; } 1309 | } 1310 | }, 1311 | 1312 | '-quieter' => 1313 | { 1314 | help => "decrease the volume slightly (by $little%)", 1315 | command => sub { command -volume, "-$little"; } 1316 | }, 1317 | 1318 | '-Quieter' => 1319 | { 1320 | help => "reduce the volume significantly (by $lot%)", 1321 | command => sub { command -volume, "-$lot"; } 1322 | }, 1323 | 1324 | '-rescan' => 1325 | { 1326 | help => "rescan changes|playlists|clean", 1327 | server => 1, 1328 | do_shift => 1, 1329 | command => sub { my $st = urlencode shift; 1330 | if ($st eq "changes" ) { $st ="rescan"; } 1331 | elsif ($st eq "playlists" ) { $st ="rescan playlists"; } 1332 | elsif ($st eq "clean" ) { $st ="wipecache"; } 1333 | elsif ($st eq "wipe" ) { $st ="wipecache"; } 1334 | elsif ($st eq "all" ) { $st ="wipecache"; } 1335 | else { $st= "rescan ".urlencode($st); } 1336 | send_command $st; 1337 | } 1338 | }, 1339 | 1340 | '-shortcuts' => 1341 | { 1342 | help => 'show the configured shortcuts', 1343 | command => sub { foreach ( keys %shortcut ) 1344 | { print sprintf "%-8s %s\n", $_, $shortcut{$_}; } 1345 | } 1346 | }, 1347 | 1348 | '-show' => 1349 | { 1350 | help => "show a message on the player's display (for $showtime seconds)", 1351 | do_shift => 1, 1352 | command => sub { my $show = urlencode shift; 1353 | send_command urlencode($player)." show font:huge duration:".$showtime." centered:1 line2:$show"; } 1354 | }, 1355 | 1356 | '-silent' => 1357 | { 1358 | help => 'redirect standard output to \'/dev/null\'', 1359 | command => sub { open STDOUT, '>', '/dev/null'; } 1360 | }, 1361 | 1362 | '-sleep' => 1363 | { 1364 | help => 'make the player sleep in minutes (use \'0\' or \'-sleep_clear\' to cancel sleep)', 1365 | do_shift => 1, 1366 | command => 1367 | sub 1368 | { 1369 | my $time = shift; 1370 | if ( ! ( $time =~ m/^[0-9]{1,}$/ ) ) 1371 | { die "squeezy -sleep: invalid argument ($time)\n"; } 1372 | $time = 60 * $time; 1373 | send_command urlencode($player)." sleep $time"; 1374 | command -sleeping; 1375 | } 1376 | }, 1377 | 1378 | '-sleep_clear' => 1379 | { 1380 | help => 'cancel sleep', 1381 | command => 1382 | sub 1383 | { 1384 | send_command urlencode($player)." sleep 0"; 1385 | command -sleeping; 1386 | } 1387 | }, 1388 | 1389 | '-sleeping' => 1390 | { 1391 | help => 'report on a player\'s sleep status (indicated in seconds)', 1392 | command => 1393 | sub 1394 | { 1395 | my $time = pick_field 2, send_command urlencode($player)." sleep ?"; 1396 | my @time = split('\.', $time); 1397 | report 'sleep', $time[0]; 1398 | } 1399 | }, 1400 | 1401 | '-start' => 1402 | { 1403 | help => 'start playing from the current playlist', 1404 | command => sub { 1405 | send_command urlencode($player)." play"; 1406 | command -playing; 1407 | } 1408 | }, 1409 | 1410 | '-sync' => 1411 | { 1412 | help => 'synchronise player with (another player id )', 1413 | do_shift => 1, 1414 | command => 1415 | sub 1416 | { 1417 | # arg needs to be a real player id not name. 1418 | my $arg = idFromName(shift); 1419 | send_command urlencode($player)." sync ".urlencode($arg); 1420 | report 'sync', pick_field 2, send_command urlencode($player)." sync ?"; 1421 | } 1422 | }, 1423 | 1424 | '-syncgroups' => 1425 | { 1426 | help => 'show synchronisation groups', 1427 | server => 1, 1428 | command => sub { 1429 | my @r = split /\s+/, send_command 'syncgroups ?'; 1430 | if (shift @r eq 'syncgroups') { 1431 | while ($#r > 0) { 1432 | print urldecode(shift @r)," ",urldecode(shift @r),"\n"; 1433 | } 1434 | } 1435 | } 1436 | }, 1437 | 1438 | '-tick' => 1439 | { 1440 | help => 'make -listen request all players\' playback time every second', 1441 | command => sub { $tick = 1; } 1442 | }, 1443 | 1444 | '-time' => 1445 | { 1446 | help => 'jump to an absolute or relative position in the current track (measured in seconds; eg. 10, +30, -30)', 1447 | do_shift => 1, 1448 | command => 1449 | sub 1450 | { 1451 | my $time = shift; 1452 | if ( ! ( $time =~ m/^[+-]?[?0-9]{1,}$/ ) ) 1453 | { die "squeezy -time: invalid time specifier ($time)\n"; } 1454 | report 'time', pick_field 2, send_command urlencode($player)." time $time"; 1455 | } 1456 | }, 1457 | 1458 | '-unsync' => 1459 | { 1460 | help => 'unsynchronise player', 1461 | command => 1462 | sub 1463 | { 1464 | send_command urlencode($player)." sync -"; 1465 | command -syncgroups; 1466 | report 'sync', pick_field 2, send_command urlencode($player)." sync ?"; 1467 | } 1468 | }, 1469 | 1470 | '-volume' => 1471 | { 1472 | help => "set the player's volume (absolute or relative, use \'?\' to query the current volume)", 1473 | do_shift => 1, 1474 | command => 1475 | sub 1476 | { 1477 | my $vol = shift; 1478 | if ( $vol ne '?' ) 1479 | { 1480 | if ( ! ( $vol =~ m/^[+-]?[0-9]{1,}$/ ) ) 1481 | { die "squeezy -volume: invalid volume specifier ($vol)\n"; } 1482 | if ( 100 < $vol || $vol < -100) 1483 | { die "squeezy -volume: invalid volume specifier ($vol, should be min/max 100)\n"; } 1484 | send_command urlencode($player)." mixer volume $vol"; 1485 | } 1486 | report 'volume', pick_field 3, send_command urlencode($player)." mixer volume ?"; 1487 | } 1488 | }, 1489 | 1490 | '-verbose' => 1491 | { 1492 | help => 'show serves and players amongst other info', 1493 | server => 1, 1494 | command => sub { 1495 | $verbose = 1; 1496 | print $serverss if ($serverss); 1497 | print $playerss if ($playerss); 1498 | } 1499 | }, 1500 | ); 1501 | 1502 | # 1503 | # add options to select each player; for example, for a player 'Kitchen' this 1504 | # will add options: 1505 | 1506 | # -Kitchen 1507 | # -kitchen 1508 | # -k 1509 | # 1510 | # (options are not added if they would overwrite existing options; so, if you 1511 | # have a player named 'Help', then an option '-help' will not be added) 1512 | # 1513 | foreach ( @player ) 1514 | { 1515 | my $po = $_; 1516 | my $p = $_->{name}; # player name 1517 | my $s = $_->{server}; 1518 | my $ns = $p; 1519 | $ns =~ s/ /_/g; # remove spaces 1520 | my $lc = lc $ns; # same, but lower case 1521 | # Deal with c hars rather than bytes as all lms output is utf8 encoded... 1522 | my $fc = substr( Encode::decode_utf8($lc),0,1); # just the first character 1523 | $fc = Encode::encode_utf8($fc); 1524 | foreach ( $ns, $lc, $fc ) 1525 | { 1526 | if ( ! $command{$_} ) 1527 | { 1528 | $command{"-$_"} = { is_player => 1, 1529 | help => "select player $p (automatically added option)", 1530 | command => sub { $group = undef; $def_player = $po; $player = $p; $cpo = $po; $server = $s->{IP} || $s->{HOST} || $server; $socket = $s->{SOCKET} || undef; $port = $s->{PORT} || $port; print "$p selected\n"; }, }; 1531 | } 1532 | } 1533 | } 1534 | 1535 | # 1536 | # add a command to directly play each shortcut; again, only if it doesn't 1537 | # overwrite an existing command 1538 | # 1539 | 1540 | foreach ( keys %shortcut ) 1541 | { 1542 | if ( ! $command{-$_} ) 1543 | { 1544 | my $sc = $_; 1545 | $command{-$sc} = 1546 | { 1547 | help => "play shortcut $sc ($shortcut{$sc}, automatically added option)", 1548 | command => sub { command -play, $shortcut{$sc}; } 1549 | }; 1550 | } 1551 | } 1552 | 1553 | # 1554 | # some abbreviated/alternative option names 1555 | # 1556 | 1557 | $command{'-'} = $command{-quieter} if ! $command{'-'}; 1558 | $command{'--'} = $command{-Quieter} if ! $command{'--'}; 1559 | $command{'+'} = $command{-louder} if ! $command{'+'}; 1560 | $command{'++'} = $command{-Louder} if ! $command{'++'}; 1561 | $command{-currenttitle} = $command{-current_title} if ! $command{-currenttitle}; 1562 | $command{-playlist_remove} = $command{-playlist_delete} if ! $command{-playlist_remove}; 1563 | $command{-prev} = $command{-previous} if ! $command{-prev}; 1564 | $command{-goto} = $command{-jump} if ! $command{-goto}; 1565 | $command{'--help'} = $command{-help} if ! $command{'--help'}; 1566 | $command{'--verbose'} = $command{-verbose} if ! $command{'--verbose'}; 1567 | 1568 | # 1569 | # if the name of this "executable" is also the name of a player, then select 1570 | # that player as the default player 1571 | # 1572 | # with a player named 'Kitchen' (say) and an appropriate hard link: 1573 | # 1574 | # ln -v /usr/local/bin/squeezy /usr/local/bin/kitchen 1575 | # 1576 | # this allows you to say things like "kitchen -quieter"; see also the 1577 | # -print_links option 1578 | # 1579 | 1580 | if ( $command{-$my_name} && $command{-$my_name}{is_player} ) 1581 | { command -$my_name; } 1582 | 1583 | # 1584 | # argument processing (the main loop) 1585 | # 1586 | 1587 | while ( 0 <= $#ARGV ) 1588 | { 1589 | my $arg = shift @ARGV; 1590 | my @whom = defined $group ? @{$group[$group]} : ( $def_player ); 1591 | ## FIXME some commands operate on server only... 1592 | 1593 | if ( $command{$arg} ) 1594 | { 1595 | # 1596 | # argument checking (to the extent that that's possible here) 1597 | # 1598 | 1599 | if ( $command{$arg}->{do_shift} ) 1600 | { 1601 | my $argument = $ARGV[0]; 1602 | if ( ! defined $argument ) 1603 | { die "squeezy $arg: no argument where argument required\n"; } 1604 | if ( $command{$argument} ) 1605 | { die "squeezy $arg: invalid argument ($argument, which rather looks like an option)\n"; } 1606 | } 1607 | 1608 | if ( !$command{$arg}->{server} && !$def_player ) { 1609 | print STDERR "squeezy warning: no players specified or detected\n"; 1610 | print STDERR " you likely won't be able to do much without them\n"; 1611 | } 1612 | 1613 | # 1614 | # command processing 1615 | # 1616 | foreach my $p ( @whom ) 1617 | { 1618 | if ( exists $p->{name} || $command{$arg}->{server} ) { 1619 | $socket = $p->{SOCKET} || undef; 1620 | $player = $p->{pid} || $p->{name} || undef; 1621 | $cpo = $p; # Needed for groups as def player not set correctly. 1622 | $server = $p->{server} ? $p->{server}{IP} || $p->{server}{HOST} : $server; 1623 | $server = ($p->{server} && $p->{server}{SEND}) ? $p->{server} : $server; 1624 | if ($skip->{$player}) { 1625 | $skip->{$player} = 0; 1626 | next; 1627 | } 1628 | $port = $p->{PORT} || $port; 1629 | if ( $command{$arg}->{json} ) { 1630 | ## FIXME some commands operate on server only... select server? rather than player... 1631 | command $arg, ($p->{server} ||$servers[0]), $p, $ARGV[0]; 1632 | } else { 1633 | command $arg, $ARGV[0]; 1634 | } 1635 | 1636 | } else { 1637 | die "squeezy invalid player object $p\n"; 1638 | } 1639 | } 1640 | 1641 | if ( $command{$arg}->{do_shift} ) 1642 | { shift @ARGV; } 1643 | 1644 | if ( $arg =~ /-if_.*/ ) 1645 | { $not = 0; } 1646 | 1647 | next; 1648 | } 1649 | 1650 | die "squeezy error: unknown argument ($arg)\n"; 1651 | } 1652 | 1653 | exit(0); 1654 | --------------------------------------------------------------------------------