1286 lines
43 KiB
Perl
Executable File
1286 lines
43 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
|
|
# Cantata-Dynamic
|
|
#
|
|
# Copyright (c) 2011-2013 Craig Drummond <craig.p.drummond@gmail.com>
|
|
#
|
|
# ----
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; see the file COPYING. If not, write to
|
|
# the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
# Boston, MA 02110-1301, USA.
|
|
|
|
use IO::Socket::INET;
|
|
use POSIX;
|
|
use File::stat;
|
|
use File::Basename;
|
|
use Cwd 'abs_path';
|
|
# use LWP::Simple;
|
|
use Thread;
|
|
use Socket;
|
|
use IO::Socket;
|
|
use threads;
|
|
use threads::shared;
|
|
use URI::Escape;
|
|
use Encode;
|
|
use Sys::Hostname;
|
|
use Socket qw(:all);
|
|
|
|
# UDP multicast message sender - used to communicate with Cantata when cantata-dynamic is run in server mode!
|
|
my $mcastGroup : shared = '239.123.123.123';
|
|
my $mcastPort : shared = 6602;
|
|
my $mcastTtl : shared = 1;
|
|
|
|
my $isServerMode : shared =0;
|
|
my $dynamicIsActive : shared =1;
|
|
my $currentStatus : shared ="IDLE";
|
|
|
|
$testMode=0;
|
|
$PLAY_QUEUE_DESIRED_LENGTH=10;
|
|
$PLAY_QUEUE_CURRENT_POS=5;
|
|
|
|
my $mpdHost : shared ="localhost";
|
|
my $mpdPort : shared ="6600";
|
|
my $mpdPasswd : shared ="";
|
|
my $idString : shared = hostname();
|
|
|
|
my $currentStatusTime = time;
|
|
|
|
# Read MPDs host, port, and password details from env - if set
|
|
sub readConnectionDetails() {
|
|
my $hostEnv=$ENV{'MPD_HOST'};
|
|
my $portEnv=$ENV{'MPD_PORT'};
|
|
if (length($portEnv)>2) {
|
|
$mpdPort=$portEnv;
|
|
}
|
|
|
|
if (length($hostEnv)>2) {
|
|
my $sep = index($hostEnv, '@');
|
|
|
|
if ($sep>0) {
|
|
$mpdPasswd=substr($hostEnv, 0, $sep);
|
|
$mpdHost=substr($hostEnv, $sep+1, length($hostEnv)-$sep);
|
|
} else {
|
|
$mpdHost=$hostEnv;
|
|
}
|
|
}
|
|
}
|
|
|
|
sub readReply() {
|
|
my $sock=shift;
|
|
local $data;
|
|
my $socketData;
|
|
while ($sock->connected()) {
|
|
$sock->recv($data, 1024);
|
|
if (! $data) {
|
|
return '';
|
|
}
|
|
$socketData="${socketData}${data}";
|
|
$data="";
|
|
|
|
if (($socketData=~ m/(OK)$/) || ($socketData=~ m/^(OK)/)) {
|
|
return $socketData;
|
|
} elsif ($socketData=~ m/^(ACK)/) {
|
|
return '';
|
|
}
|
|
}
|
|
}
|
|
|
|
# Connect to MPD
|
|
sub connectToMpd() {
|
|
my $connDetails="";
|
|
my $sock;
|
|
if ($mpdHost=~ m/^(\/)/) {
|
|
$sock = new IO::Socket::UNIX(Peer => $mpdHost, Type => 0);
|
|
$connDetails=$mpdHost;
|
|
} else {
|
|
$sock = new IO::Socket::INET(PeerAddr => $mpdHost, PeerPort => $mpdPort, Proto => 'tcp');
|
|
$connDetails="${mpdHost}:${mpdPort}";
|
|
}
|
|
if ($sock && $sock->connected()) {
|
|
if (&readReply($sock)) {
|
|
if ($mpdPasswd) {
|
|
$sock->send("password ${mpdPasswd} \n");
|
|
if (! &readReply($sock)) {
|
|
print "ERROR: Invalid password\n";
|
|
eval { close $sock; };undef $sock;
|
|
}
|
|
}
|
|
} else {
|
|
print "ERROR: Failed to read connection reply fom MPD (${connDetails})\n";
|
|
close($sock);
|
|
}
|
|
} else {
|
|
print "ERROR: Failed to connect to MPD (${connDetails})\n";
|
|
}
|
|
return $sock;
|
|
}
|
|
|
|
sub sendCommand() {
|
|
my $cmd = shift;
|
|
my $status = 0;
|
|
my $sock=&connectToMpd();
|
|
my $sockData;
|
|
$cmd="${cmd}\n";
|
|
if ($sock && $sock->connected()) {
|
|
print $sock encode('utf-8' => $cmd);
|
|
$sockData=&readReply($sock);
|
|
eval { close $sock; }; undef $sock;
|
|
}
|
|
if ($sockData ne '') {
|
|
return decode_utf8($sockData);
|
|
}
|
|
return $sockData;
|
|
}
|
|
|
|
sub waitForEvent() {
|
|
my $sock=&connectToMpd();
|
|
if ($sock && $sock->connected()) {
|
|
$sock->send("idle player playlist\n");
|
|
&readReply($sock);
|
|
eval { close $sock; };undef $sock;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub baseDir() {
|
|
my $cacheDir=$ENV{'XDG_CACHE_HOME'};
|
|
if (!$cacheDir) {
|
|
$cacheDir="$ENV{'HOME'}/.cache";
|
|
}
|
|
$cacheDir="${cacheDir}/cantata/dynamic";
|
|
return $cacheDir
|
|
}
|
|
|
|
sub lockFile() {
|
|
my $fileName=&baseDir();
|
|
$fileName="${fileName}/lock";
|
|
return $fileName;
|
|
}
|
|
|
|
$lastArtistSearch="";
|
|
@artistSearchResults=();
|
|
|
|
# Query LastFM for artists similar to supplied artist
|
|
sub querySimilarArtists() {
|
|
my $artist=uri_escape(shift);
|
|
if ($artist ne $lastArtistSearch) {
|
|
@artistSearchResults=();
|
|
# my $text = get 'http://ws.audioscrobbler.com/1.0/artist/'.$artist.'/similar.txt';
|
|
# my $artistNum=0;
|
|
# open(my $fileHandle, '<', \$text);
|
|
# if (tell($fileHandle) != -1) {
|
|
# my @lines = <$fileHandle>; # Read into an array...
|
|
@lines=`wget \'http://ws.audioscrobbler.com/1.0/artist/${artist}/similar.txt\' -O -`;
|
|
foreach my $line (@lines) {
|
|
@parts = split(/,/, $line);
|
|
if (3==scalar(@parts)) {
|
|
my $artist=$parts[2];
|
|
$artist =~ s/&/&/g;
|
|
$artist =~ s/\n//g;
|
|
$artistSearchResults[$artistNum]=$artist;
|
|
$artistNum++;
|
|
}
|
|
}
|
|
# }
|
|
# close($fileHandle);
|
|
}
|
|
}
|
|
|
|
$mpdDbUpdated=0;
|
|
$rulesChanged=1;
|
|
$includeRules;
|
|
$excludeRules;
|
|
$lastIncludeRules;
|
|
$lastExcludeRules;
|
|
$initialRead=1;
|
|
$rulesTimestamp=0;
|
|
|
|
# Determine if rules file has been updated
|
|
sub checkRulesChanged() {
|
|
if ($initialRead==1) { # Always changed on first run...
|
|
$rulesChanged=1;
|
|
$initialRead=0;
|
|
} elsif ( scalar(@lastIncludeRules)!=scalar(@includeRules) ||
|
|
scalar(@lastExcludeRules)!=scalar(@excludeRules)) { # Different number of rules
|
|
$rulesChanged=1;
|
|
} else { # Same number of rules, so need to check if the rules themselves have changed or not...
|
|
$rulesChanged=0;
|
|
for (my $i=0; $i<scalar(@includeRules) && $rulesChanged==0; $i++) {
|
|
if ($includeRules[$i] ne $lastIncludeRules[$i]) {
|
|
$rulesChanged=1;
|
|
}
|
|
}
|
|
for (my $i=0; $i<scalar(@excludeRules) && $rulesChanged==0; $i++) {
|
|
if ($excludeRules[$i] ne $lastExcludeRules[$i]) {
|
|
$rulesChanged=1;
|
|
}
|
|
}
|
|
}
|
|
@lastIncludeRules=@includeRules;
|
|
@lastExcludeRules=@excludeRules;
|
|
}
|
|
|
|
# Add a rule to the list of rules that will be used to query MPD
|
|
sub saveRule() {
|
|
my $rule=$_[0];
|
|
my @dates=@{ $_[1] };
|
|
my @artistList=@{ $_[2] };
|
|
my @genreList=@{ $_[3] };
|
|
my $ruleMatch=$_[4];
|
|
my $isInclude=$_[5];
|
|
my @type=();
|
|
|
|
if ($isInclude == 1) {
|
|
@type=@includeRules;
|
|
} else {
|
|
@type=@excludeRules;
|
|
}
|
|
|
|
# We iterate through the list of artists - so if this is empty, add a blank artist.
|
|
# artistList will only be set if we have been told to find tracks by similar artists...
|
|
if (scalar(@artistList)==0) {
|
|
$artistList[0]="";
|
|
}
|
|
if (scalar(@genreList)==0) {
|
|
$genreList[0]="";
|
|
}
|
|
my $ruleNum=scalar(@type);
|
|
for my $genre (@genreList) {
|
|
for my $artist (@artistList) {
|
|
$line =~ s/\"//g;
|
|
if (scalar(@dates)>0) { # Create rule for each date (as MPDs search does not take ranges)
|
|
my $baseRule=$rule;
|
|
foreach my $date (@dates) {
|
|
$type[$ruleNum]="${ruleMatch} ${baseRule} Date \"${date}\"";
|
|
if ($artist ne "") {
|
|
$type[$ruleNum]=$type[$ruleNum]." Artist \"${artist}\"";
|
|
}
|
|
if ($genre ne "") {
|
|
$type[$ruleNum]=$type[$ruleNum]." Genre \"${genre}\"";
|
|
}
|
|
$ruleNum++;
|
|
}
|
|
} elsif ($artist ne "" || $genre ne "" || $rule ne "") {
|
|
$type[$ruleNum]="${ruleMatch} $rule";
|
|
if ($artist ne "") {
|
|
$type[$ruleNum]=$type[$ruleNum]." Artist \"${artist}\"";
|
|
}
|
|
if ($genre ne "") {
|
|
$type[$ruleNum]=$type[$ruleNum]." Genre \"${genre}\"";
|
|
}
|
|
$ruleNum++;
|
|
}
|
|
}
|
|
}
|
|
if ($isInclude == 1) {
|
|
@includeRules=@type;
|
|
} else {
|
|
@excludeRules=@type;
|
|
}
|
|
}
|
|
|
|
# Read rules from ~/.cache/cantata/dynamic/rules
|
|
# (or from ${filesDir}/rules in HTTP mode)
|
|
#
|
|
# File format:
|
|
#
|
|
# Rule
|
|
# <Tag>:<Value>
|
|
# <Tag>:<Value>
|
|
# Rule
|
|
#
|
|
# e.g.
|
|
#
|
|
# Rule
|
|
# AlbumArtist:Various Artists
|
|
# Genre:Dance
|
|
# Rule
|
|
# AlbumArtist:Wibble
|
|
# Date:1980-1989
|
|
# Exact:false
|
|
# Exclude:true
|
|
#
|
|
$activeFile="";
|
|
$activeLinksTo="";
|
|
sub readRules() {
|
|
if ($activeFile eq "") {
|
|
$activeFile=&baseDir();
|
|
$activeFile="${activeFile}/rules";
|
|
}
|
|
|
|
unless (-e $activeFile) {
|
|
$rulesChanged=0;
|
|
return;
|
|
}
|
|
|
|
# Check if rules (well, the file it points to), has changed since the last read...
|
|
my $currentActiveLink=abs_path($activeFile);
|
|
$fileTime = stat($currentActiveLink)->mtime;
|
|
if ($initialRead!=1 && $fileTime==$rulesTimestamp && $activeLinksTo eq $currentActiveLink) {
|
|
# No change, so no need to read it again!
|
|
$rulesChanged=0;
|
|
return;
|
|
}
|
|
$activeLinksTo=$currentActiveLink;
|
|
$rulesTimestamp=$fileTime;
|
|
|
|
for(my $i=0; $i<10; $i++) {
|
|
open(my $fileHandle, "<:encoding(utf8)", $activeFile);
|
|
if (tell($fileHandle) != -1) {
|
|
my @lines = <$fileHandle>; # Read into an array...
|
|
my $ruleMatch="find";
|
|
my @dates=();
|
|
my @similarArtists=();
|
|
my $isInclude=1;
|
|
my $currentRule="";
|
|
@includeRules=();
|
|
@excludeRules=();
|
|
close($fileHandle);
|
|
foreach my $line (@lines) {
|
|
if (! ($line=~ m/^(#)/)) {
|
|
$line =~ s/\n//g;
|
|
my $sep = index($line, ':');
|
|
|
|
if ($sep>0) {
|
|
$key=substr($line, 0, $sep);
|
|
$val=substr($line, $sep+1, length($line)-$sep);
|
|
} else {
|
|
$key=$line;
|
|
$val="";
|
|
}
|
|
if ($key=~ m/^(Rule)/) { # New rule...
|
|
if (length($currentRule)>1 || scalar(@similarArtists)>0 || scalar(@dates)>0 || scalar(@genres)>0) {
|
|
&saveRule($currentRule, \@dates, \@similarArtists, \@genres, $ruleMatch, $isInclude);
|
|
}
|
|
$currentRule="";
|
|
@dates=();
|
|
@similarArtists=();
|
|
@genres=();
|
|
} else {
|
|
if ($key eq "Date") {
|
|
my @dateVals = split("-", $val);
|
|
if (scalar(@dateVals)==2) {
|
|
my $fromDate=scalar($dateVals[0]);
|
|
my $toDate=scalar($dateVals[1]);
|
|
if ($fromDate > $toDate) { # Fix dates if from>to!!!
|
|
my $tmp=$fromDate;
|
|
$fromDate=$toDate;
|
|
$toDate=$tmp;
|
|
}
|
|
my $pos=0;
|
|
for(my $d=$fromDate; $d<=$toDate; $d++) {
|
|
$dates[$pos]=$d;
|
|
$pos++;
|
|
}
|
|
} else {
|
|
@dates=($val)
|
|
}
|
|
} elsif ($key eq "Genre" && $val =~ m{\*}) {
|
|
# Wildcard genre - get list of genres from MPD, and find the ones that contain the genre string.
|
|
$val =~ s/\*//g;
|
|
my $socketData=&sendCommand("list genre");
|
|
my @mpdGenres = split("\n", $socketData);
|
|
my $pos=0;
|
|
foreach my $genre (@mpdGenres) {
|
|
$genre =~ s/Genre: //g;
|
|
if ($genre ne "OK" && $genre ne "" && $genre =~/$val/i) {
|
|
$genres[$pos]=$genre;
|
|
$pos++;
|
|
}
|
|
}
|
|
} elsif ($key eq "Artist" || $key eq "Album" || $key eq "AlbumArtist" || $key eq "Composer" || $key eq "Title" || $key eq "Genre") {
|
|
$currentRule="${currentRule} ${key} \"${val}\"";
|
|
} elsif ($key eq "SimilarArtists") {
|
|
&querySimilarArtists($val); # Perform a last.fm query to find similar artists
|
|
@artistSearchResults; # Save results of query
|
|
@artistSearchResults=uniq(@artistSearchResults); # Ensure we only have unique entries...
|
|
if (scalar(@artistSearchResults)>1) {
|
|
my @mpdArtists = ();
|
|
my $pos=0;
|
|
# Get MPD artists...
|
|
my $socketData=&sendCommand("list artist");
|
|
my @mpdResponse=split("\n", $socketData);
|
|
foreach my $artist (@mpdResponse) {
|
|
$artist =~ s/Artist: //g;
|
|
if ($artist ne "OK" && $artist ne "" && $artist ne $val) {
|
|
$mpdArtists[$pos]=$artist;
|
|
$pos++;
|
|
}
|
|
}
|
|
|
|
## Get MPD album-artists...
|
|
#$socketData=&sendCommand("list albumartist");
|
|
#@mpdResponse=split("\n", $socketData);
|
|
#foreach my $artist (@mpdResponse) {
|
|
# $artist =~ s/AlbumArtist: //g;
|
|
# if ($artist ne "OK" && $artist ne "" && $artist ne $val) {
|
|
# $mpdArtists[$pos]=$artist;
|
|
# $pos++;
|
|
# }
|
|
#}
|
|
@mpdArtists=uniq(@mpdArtists);
|
|
|
|
# Now chec which last.fm artists MPD actually has...
|
|
my $pos=0;
|
|
foreach my $artist (@artistSearchResults) {
|
|
my @match = grep(/^$artist/i, @mpdArtists);
|
|
if (scalar(@match)>0) {
|
|
$similarArtists[$pos]=$artist;
|
|
$pos++;
|
|
}
|
|
}
|
|
}
|
|
$similarArtists[scalar(@similarArtists)]=$val; # Add ourselves!!!
|
|
} elsif ($key eq "Exact" && $val eq "false") {
|
|
$ruleMatch="search";
|
|
} elsif ($key eq "Exclude" && $val eq "true") {
|
|
$isInclude=0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (length($currentRule)>1 || scalar(@similarArtists)>0 || scalar(@dates)>0 || scalar(@genres)>0) {
|
|
&saveRule($currentRule, \@dates, \@similarArtists, \@genres, $ruleMatch, $isInclude);
|
|
}
|
|
|
|
if (1==$testMode) {
|
|
print "INCLUDE--------------\n";
|
|
foreach my $rule (@includeRules) {
|
|
print "${rule}\n";
|
|
}
|
|
print "EXCLUDE--------------\n";
|
|
foreach my $rule (@excludeRules) {
|
|
print "${rule}\n";
|
|
}
|
|
print "---------------------\n"
|
|
}
|
|
|
|
&checkRulesChanged();
|
|
return 1;
|
|
}
|
|
if (0==$isServerMode) {
|
|
sleep 1;
|
|
}
|
|
}
|
|
&checkRulesChanged();
|
|
return 0;
|
|
}
|
|
|
|
# Remove duplicate entries from an array...
|
|
sub uniq {
|
|
return keys %{{ map { $_ => 1 } @_ }};
|
|
}
|
|
|
|
# Send message to Cantata application - ued when run in server mode
|
|
sub sendServerMessage() {
|
|
my $message=shift;
|
|
if (length($message)<=0) {
|
|
$message="STATUS:STATE:${currentStatus}\nRULES:${activeRules}\nTIME:${currentStatusTime}";
|
|
}
|
|
my $sock = IO::Socket::INET->new(Proto=>'udp', PeerAddr=>${mcastGroup}, PeerPort=>${mcastPort});
|
|
if ($sock && $sock->connected()) {
|
|
setsockopt($sock, IPPROTO_IP, IP_TTL, $mcastTtl);
|
|
$sock->send("{CANTATA/${idString}}${message}");
|
|
eval { close $sock; }; undef $sock;
|
|
}
|
|
}
|
|
|
|
# Send message to Cantata application...
|
|
sub sendMessage() {
|
|
my $method=shift;
|
|
my $argument=shift;
|
|
system("qdbus com.googlecode.cantata /cantata ${method} ${argument}");
|
|
if ( $? == -1 ) {
|
|
# Maybe qdbus is not installed? Try dbus-send...
|
|
system("dbus-send --type=method_call --session --dest=com.googlecode.cantata /cantata com.googlecode.cantata.${method} string:${argument}");
|
|
}
|
|
}
|
|
|
|
# Use rules to obtain a list of songs from MPD...
|
|
sub getSongs() {
|
|
# If we have no current songs, or rules have changed, or MPD has been updated - then we need to run the rules against MPD to get song list...
|
|
if (scalar(@mpdSongs)<1 || $rulesChanged==1 || $mpdDbUpdated==1) {
|
|
my @excludeSongs=();
|
|
if (scalar(@excludeRules)>0) {
|
|
# Get list of songs that should be removed from the song list...
|
|
my $mpdSong=0;
|
|
foreach my $rule (@excludeRules) {
|
|
my $socketData=&sendCommand($rule);
|
|
if (defined($socketData)) {
|
|
my @lines = split('\n', $socketData);
|
|
foreach my $line (@lines) {
|
|
if ($line=~ m/^(file\:)/) {
|
|
my $sep = index($line, ':');
|
|
if ($sep>0) {
|
|
$excludeSongs[$mpdSong]=substr($line, $sep+2, length($line)-($sep+1));
|
|
$mpdSong++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@excludeSongs=uniq(@excludeSongs);
|
|
}
|
|
}
|
|
|
|
my %excludeSongSet = map { $_ => 1 } @excludeSongs;
|
|
|
|
@mpdSongs=();
|
|
my $mpdSong=0;
|
|
if (scalar(@includeRules)>0) {
|
|
foreach my $rule (@includeRules) {
|
|
my $socketData=&sendCommand($rule);
|
|
if (defined($socketData)) {
|
|
my @lines = split('\n', $socketData);
|
|
foreach my $line (@lines) {
|
|
if ($line=~ m/^(file\:)/) {
|
|
my $sep = index($line, ':');
|
|
if ($sep>0) {
|
|
my $song=substr($line, $sep+2, length($line)-($sep+1));
|
|
if (! $excludeSongSet{$song}) {
|
|
$mpdSongs[$mpdSong]=$song;
|
|
$mpdSong++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@mpdSongs=uniq(@mpdSongs);
|
|
}
|
|
} else {
|
|
# No 'include' rules => get all songs!
|
|
my $socketData=&sendCommand("listall");
|
|
if (defined($socketData)) {
|
|
my @lines = split('\n', $socketData);
|
|
foreach my $line (@lines) {
|
|
if ($line=~ m/^(file\:)/) {
|
|
my $sep = index($line, ':');
|
|
if ($sep>0) {
|
|
my $song=substr($line, $sep+2, length($line)-($sep+1));
|
|
if (! $excludeSongSet{$song}) {
|
|
$mpdSongs[$mpdSong]=$song;
|
|
$mpdSong++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (scalar(@mpdSongs)<1) {
|
|
if (1==$isServerMode) {
|
|
$currentStatus="NO_SONGS";
|
|
&sendServerMessage();
|
|
} else {
|
|
&sendMessage("showError", "NO_SONGS");
|
|
exit(0);
|
|
}
|
|
} elsif (1==$isServerMode) {
|
|
$currentStatus="HAVE_SONGS";
|
|
&sendServerMessage();
|
|
}
|
|
|
|
if (1==$testMode) {
|
|
print "SONGS--------------\n";
|
|
foreach my $song (@mpdSongs) {
|
|
print "${song}\n";
|
|
}
|
|
print "---------------------\n"
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# Following canAdd/storeSong are used to remember songs that have been added to the playqueue, so that
|
|
# we don't re-add them too soon!
|
|
#
|
|
@playQueueHistory=();
|
|
$playQueueHistoryLimit=0;
|
|
$playQueueHistoryPos=0;
|
|
sub canAdd() {
|
|
my $file=shift;
|
|
my $numSongs=shift;
|
|
my $pqLimit=0;
|
|
|
|
# Calculate a reasonable level for the history...
|
|
if (1==$numSongs) {
|
|
return 1;
|
|
} elsif ($numSongs<5) {
|
|
$pqLimit=int(($numSongs/2)+0.5);
|
|
} else {
|
|
$pqLimit=int(($numSongs*0.75)+0.5);
|
|
if ($pqLimit>200) {
|
|
$pqLimit=200;
|
|
}
|
|
}
|
|
|
|
# If the history level has changed, then so must have the rules/mpd/whatever, so add this song anyway...
|
|
if ($pqLimit != $playQueueHistoryLimit) {
|
|
$playQueueHistoryLimit=$pqLimit;
|
|
@playQueueHistory=();
|
|
return 1;
|
|
}
|
|
|
|
my $size=scalar(@playQueueHistory);
|
|
if ($size>$playQueueHistoryLimit) {
|
|
$size=$playQueueHistoryLimit;
|
|
}
|
|
|
|
for (my $i=0; $i<$size; ++$i) {
|
|
if ($playQueueHistory[$i] eq $file) {
|
|
return 0;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub storeSong() {
|
|
my $file=shift;
|
|
if ($playQueueHistoryLimit<=0) {
|
|
$playQueueHistoryLimit=5;
|
|
}
|
|
|
|
if ($playQueueHistoryPos>=$playQueueHistoryLimit) {
|
|
$playQueueHistoryPos=0;
|
|
}
|
|
$playQueueHistory[$playQueueHistoryPos]=$file;
|
|
$playQueueHistoryPos++;
|
|
}
|
|
|
|
#
|
|
# This is the 'main' function of the dynamizer
|
|
#
|
|
sub populatePlayQueue() {
|
|
&readConnectionDetails();
|
|
my $lastMpdDbUpdate=-1;
|
|
while (1) {
|
|
if (0==$dynamicIsActive && 1==$isServerMode) {
|
|
while (0==$dynamicIsActive) {
|
|
if (0==&waitForEvent()) {
|
|
# TODO: Could not connect to MPD, and dynamic is not active, so wait??? Not sure aobut this...
|
|
sleep(2);
|
|
}
|
|
}
|
|
}
|
|
|
|
my $socketData='';
|
|
if (1==$dynamicIsActive) {
|
|
# Use status to obtain the current song pos, and to check that MPD is running...
|
|
$socketData=&sendCommand("status");
|
|
}
|
|
if (defined($socketData)) {
|
|
my @lines = split('\n', $socketData);
|
|
my $playQueueLength=0;
|
|
my $playQueueCurrentTrackPos=0;
|
|
my $isPlaying=0;
|
|
foreach my $val (@lines) {
|
|
if ($val=~ m/^(song\:)/) {
|
|
my @vals = split(": ", $val);
|
|
if (scalar(@vals)==2) {
|
|
$playQueueCurrentTrackPos=scalar($vals[1]);
|
|
}
|
|
} elsif ($val=~ m/^(state\:)/) {
|
|
my @vals = split(": ", $val);
|
|
if (scalar(@vals)==2 && $vals[1]=~ m/^(play)/) {
|
|
$isPlaying=1;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Call stats, so that we can obtain the last time MPD was updated.
|
|
# We use this to determine when we need to refresh the searched set of songs
|
|
$mpdDbUpdated=0;
|
|
$socketData=&sendCommand("stats");
|
|
if (defined($socketData)) {
|
|
my @lines = split('\n', $socketData);
|
|
foreach my $val (@lines) {
|
|
if ($val=~ m/^(db_update\:)/) {
|
|
my @vals = split(": ", $val);
|
|
if (scalar(@vals)==2) {
|
|
my $mpdDbUpdate=scalar($vals[1]);
|
|
if ($mpdDbUpdate!=$lastMpdDbUpdate) {
|
|
$lastMpdDbUpdate=$mpdDbUpdate;
|
|
$mpdDbUpdated=1;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Get current playlist info
|
|
$socketData=&sendCommand("playlist");
|
|
if (defined($socketData)) {
|
|
my @lines = split('\n', $socketData);
|
|
my $playQueueLength=scalar(@lines);
|
|
if ($playQueueLength>0 && $lines[$playQueueLength-1]=~ m/^(OK)/) {
|
|
$playQueueLength--;
|
|
}
|
|
|
|
# trim playlist start so that current becomes <=$PLAY_QUEUE_CURRENT_POS
|
|
for (my $i=0; $i < $playQueueCurrentTrackPos - ($PLAY_QUEUE_CURRENT_POS-1); $i++) {
|
|
&sendCommand("delete 0");
|
|
$playQueueLength--;
|
|
}
|
|
if ($playQueueLength<0) {
|
|
$playQueueLength=0;
|
|
}
|
|
|
|
&readRules();
|
|
&getSongs();
|
|
my $numMpdSongs=scalar(@mpdSongs);
|
|
if ($numMpdSongs>0) {
|
|
# fill up playlist to 10 random tunes
|
|
my $failues=0;
|
|
my $added=0;
|
|
while ($playQueueLength < $PLAY_QUEUE_DESIRED_LENGTH) {
|
|
my $pos=int(rand($numMpdSongs));
|
|
if ($failues > 100 || &canAdd(${mpdSongs[$pos]}, $numMpdSongs)) {
|
|
my $file=${mpdSongs[$pos]};
|
|
$file =~ s/\\/\\\\/g;
|
|
$file =~ s/\"/\\\"/g;
|
|
if (&sendCommand("add \"${file}\"") ne '') {
|
|
&storeSong(${mpdSongs[$pos]});
|
|
$playQueueLength++;
|
|
$failues=0;
|
|
$added++;
|
|
}
|
|
} else { # Song is already in playqueue history...
|
|
$failues++;
|
|
}
|
|
}
|
|
# If we are not currently playing and we filled playqueue - then play first!
|
|
if ($isPlaying==0 && $added==$PLAY_QUEUE_DESIRED_LENGTH) {
|
|
&sendCommand("play 0")
|
|
}
|
|
}
|
|
&waitForEvent();
|
|
} elsif (0==$isServerMode) {
|
|
sleep 2;
|
|
}
|
|
} elsif (0==$isServerMode) {
|
|
sleep 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
sub readPid() {
|
|
my $fileName=shift;
|
|
|
|
if (-e $fileName) {
|
|
open(my $fileHandle, $fileName);
|
|
my @lines = <$fileHandle>;
|
|
close($fileHandle);
|
|
if (scalar(@lines)>0) {
|
|
my $pid=$lines[0];
|
|
return scalar($pid);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub daemonize() {
|
|
my $fileName=shift;
|
|
# daemonize process...
|
|
chdir '/';
|
|
umask 0;
|
|
open STDIN, '/dev/null' or die "Can't read /dev/null: $!";
|
|
open STDOUT, '>>/dev/null' or die "Can't write to /dev/null: $!";
|
|
open STDERR, '>>/dev/null' or die "Can't write to /dev/null: $!";
|
|
defined( my $pid = fork ) or die "Can't fork: $!";
|
|
exit if $pid;
|
|
|
|
# dissociate this process from the controlling terminal that started it and stop being part
|
|
# of whatever process group this process was a part of.
|
|
POSIX::setsid() or die "Can't start a new session.";
|
|
|
|
# callback signal handler for signals.
|
|
$SIG{INT} = $SIG{TERM} = $SIG{HUP} = \&signalHandler;
|
|
$SIG{PIPE} = 'ignore';
|
|
|
|
# Write our PID the lock file, so that 'stop' knows which PID to kill...
|
|
open(my $fileHandle, ">${fileName}");
|
|
print $fileHandle $$;
|
|
close $fileHandle;
|
|
}
|
|
|
|
sub start() {
|
|
my $pidFile=&lockFile();
|
|
my $pid=&readPid($pidFile);
|
|
if ($pid>0) {
|
|
$exists = kill 0, $pid;
|
|
if ($exists) {
|
|
print "PROCESS $pid is running!\n";
|
|
return;
|
|
}
|
|
}
|
|
|
|
my $fileName=&lockFile();
|
|
&daemonize($fileName);
|
|
&sendMessage("dynamicStatus", "running");
|
|
&populatePlayQueue();
|
|
}
|
|
|
|
sub signalHandler {
|
|
if (0==$isServerMode) {
|
|
unlink(&lockFile());
|
|
&sendMessage("dynamicStatus", "stopped");
|
|
} else {
|
|
unlink($pidFile);
|
|
}
|
|
exit(0);
|
|
}
|
|
|
|
sub stop() {
|
|
my $pidFile=&lockFile();
|
|
my $pid=&readPid($pidFile);
|
|
if ($pid>0) {
|
|
system("kill", $pid);
|
|
system("pkill", "-P", $pid);
|
|
}
|
|
}
|
|
|
|
# #####################################
|
|
# HTTP SERVER MODE
|
|
#
|
|
# GetID: GET http://host:port/id
|
|
# GetStatus: GET http://host:port/status
|
|
# ListPlaylists: GET http://host:port/list http://host:port/list?withDetails={1/0}
|
|
# GetPlaylist: GET http://host:port/${playlist}
|
|
# DeletePlaylist: DELETE http://host:port/${playlist}
|
|
# SavePlaylist: POST http://host:port/save?name=${playlist} [BODY:Content]
|
|
# SetActive: POST http://host:port/setActive?name=${playlist}&start={1/0}
|
|
# ControlDynamizer: POST http://host:port/control?state={start/stop}
|
|
#
|
|
# #####################################
|
|
$filesDir="/var/lib/mpd/dynamic";
|
|
$httpPort=6601;
|
|
$httpControlPage=0;
|
|
$pidFile="/var/run/cantata-dynamic/pid";
|
|
|
|
# Attempt to load a config file that will specify MPD connection settings and dynamic folder location
|
|
sub loadConfig() {
|
|
my $config=shift;
|
|
if (!$config || ($config=~ m/^(default)/)) {
|
|
$config="/etc/cantata-dynamic.conf";
|
|
}
|
|
open(my $fileHandle, $config) || die "ERROR: Failed to load config $config - $!\n";
|
|
$activeFile="/var/run/cantata-dynamic/rules";
|
|
if (tell($fileHandle) != -1) {
|
|
my @lines = <$fileHandle>; # Read into an array...
|
|
close($fileHandle);
|
|
foreach my $line (@lines) {
|
|
if (! ($line=~ m/^(#)/)) {
|
|
$line =~ s/\n//g;
|
|
my $sep = index($line, '=');
|
|
|
|
if ($sep>0) {
|
|
$key=substr($line, 0, $sep);
|
|
$val=substr($line, $sep+1, length($line)-$sep);
|
|
if ($key=~ m/^(filesDir)/) {
|
|
$filesDir=$val;
|
|
} elsif ($key=~ m/^(activeFile)/) {
|
|
$activeFile=$val;
|
|
} elsif ($key=~ m/^(mpdHost)/) {
|
|
$mpdHost=$val;
|
|
} elsif ($key=~ m/^(mpdPort)/) {
|
|
$mpdPort=$val;
|
|
} elsif ($key=~ m/^(mpdPassword)/) {
|
|
$mpdPasswd=$val;
|
|
} elsif ($key=~ m/^(httpPort)/) {
|
|
$httpPort=$val;
|
|
} elsif ($key=~ m/^(httpControlPage)/) {
|
|
$httpControlPage=$val;
|
|
} elsif ($key=~ m/^(pidFile)/) {
|
|
$pidFile=$val;
|
|
} elsif ($key=~ m/^(id)/ && length($val)>0) {
|
|
$idString=$val;
|
|
} elsif ($key=~ m/^(msgPort)/) {
|
|
$mcastPort=$val;
|
|
} elsif ($key=~ m/^(msgGroup)/) {
|
|
$mcastGroup=$val;
|
|
} elsif ($key=~ m/^(msgTtl)/ && length($val)>0 && $val>0 && $val<128) {
|
|
$mcastTtl=$val;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Create folders, if these do not already exist...
|
|
my $pidDir=dirname($pidFile);
|
|
my $activeFileDir=dirname($activeFile);
|
|
unless (-d $pidDir) {
|
|
mkdir $pidDir or die;
|
|
}
|
|
unless (-d $activeFileDir) {
|
|
mkdir $activeFileDir or die;
|
|
}
|
|
unless (-d $filesDir) {
|
|
mkdir $filesDir or die;
|
|
}
|
|
}
|
|
|
|
sub readRuleFile() {
|
|
my @result=();
|
|
my $fileName=uri_unescape(shift);
|
|
open(my $fileHandle, $fileName);
|
|
if (tell($fileHandle) != -1) {
|
|
my @lines = <$fileHandle>; # Read into an array...
|
|
close($fileHandle);
|
|
foreach my $line (@lines) {
|
|
if ($line =~ /\n$/) {
|
|
push(@result, $line);
|
|
} else {
|
|
push(@result, $line."\n");
|
|
}
|
|
}
|
|
}
|
|
return @result;
|
|
}
|
|
|
|
sub listRules() {
|
|
my @result=();
|
|
my $showContents=shift;
|
|
opendir(D, "$filesDir");
|
|
while (my $f = readdir(D)) {
|
|
if ($f=~m/.rules$/) {
|
|
push(@result, "FILENAME:${f}\n");
|
|
if ($showContents>0) {
|
|
push(@result, &readRuleFile($filesDir."/".$f));
|
|
}
|
|
}
|
|
}
|
|
closedir(D);
|
|
push(@result, "\nTIME:${currentStatusTime}");
|
|
return @result;
|
|
}
|
|
|
|
sub determineActiveRules() {
|
|
local $fileName="";
|
|
if (-f $activeFile && -l $activeFile) {
|
|
$fileName=basename abs_path($activeFile);
|
|
$fileName =~ s/.rules//g;
|
|
}
|
|
return $fileName;
|
|
}
|
|
|
|
sub saveRulesToFile() {
|
|
my $name=uri_unescape(shift);
|
|
if (! $name) {
|
|
return "ERROR: No name specified";
|
|
}
|
|
if ($name =~ m/\.rules/ || $name =~ m/\//) {
|
|
return "ERROR: Invalid name";
|
|
}
|
|
my $content=shift;
|
|
# TODO: Parse content!!!
|
|
my $rulesName=$name;
|
|
$rulesName="${filesDir}/${rulesName}.rules";
|
|
|
|
open (my $fileHandle, '>'.$rulesName);
|
|
if (tell($fileHandle) != -1) {
|
|
print $fileHandle $content;
|
|
close($fileHandle);
|
|
$currentStatusTime = time;
|
|
&sendServerMessage();
|
|
return "OK";
|
|
} else {
|
|
return "ERROR: Failed to create file";
|
|
}
|
|
}
|
|
|
|
sub deleteRules() {
|
|
my $name=uri_unescape(shift);
|
|
$name =~ s/\///g;
|
|
my $active=&determineActiveRules();
|
|
my $rulesName=$name;
|
|
$rulesName="${filesDir}/${rulesName}.rules";
|
|
if (!unlink($rulesName)) {
|
|
return "ERROR: Failed to remove file";
|
|
}
|
|
$currentStatusTime = time;
|
|
if ($name eq $active) {
|
|
&control("stop");
|
|
}
|
|
&sendServerMessage();
|
|
return "OK";
|
|
}
|
|
|
|
sub control() {
|
|
my $command=shift;
|
|
if ($command eq "start") {
|
|
$dynamicIsActive=1;
|
|
$currentStatus="STARTING";
|
|
&sendCommand("clear");
|
|
&sendServerMessage();
|
|
return "OK";
|
|
} elsif ($command eq "stop") {
|
|
my $doClear=shift;
|
|
$dynamicIsActive=0;
|
|
$currentStatus="IDLE";
|
|
if ($doClear eq "true" || $doClear eq "1") {
|
|
&sendCommand("clear");
|
|
}
|
|
&sendServerMessage();
|
|
return "OK";
|
|
}
|
|
return "ERROR: Invalid command";
|
|
}
|
|
|
|
sub setActiveRules() {
|
|
my $name=uri_unescape(shift);
|
|
if ($name eq "") {
|
|
return "ERROR: No name supplied";
|
|
}
|
|
my $rulesName=$name;
|
|
my $active=&determineActiveRules();
|
|
if ($rulesName eq $active) {
|
|
return "OK";
|
|
}
|
|
|
|
$rulesName="${filesDir}/${rulesName}.rules";
|
|
if (-f $rulesName) {
|
|
if (-l $activeFile) {
|
|
if (!unlink($activeFile)) {
|
|
return "ERROR: Failed to remove link";
|
|
}
|
|
} elsif (-f $activeFile) {
|
|
return "ERROR: 'rules' is not a link";
|
|
}
|
|
system("ln -s \"${rulesName}\" \"${activeFile}\"");
|
|
if (0!=$?) {
|
|
return "ERROR: Failed to create 'rules' symlink";
|
|
}
|
|
&sendServerMessage();
|
|
return "OK";
|
|
} else {
|
|
return "ERROR: Could not file ${name}";
|
|
}
|
|
}
|
|
|
|
sub buildControlPage() {
|
|
my $body="<html><head><title>Dynamic Playlists</title></head><body><h2>Dynamic Playlists</h2>"
|
|
. "<p><i>Click on a playlist name to load</i></p>";
|
|
|
|
my @rules=&listRules(0);
|
|
my $active=&determineActiveRules();
|
|
$body = $body . "<p><ul>";
|
|
my $num=1;
|
|
|
|
foreach my $rule (@rules) {
|
|
$rule =~ s/FILENAME://;
|
|
$rule =~ s/.rules//;
|
|
$rule =~ s/\n//;
|
|
if ($rule=~ m/^(TIME:)/) {
|
|
} else {
|
|
$body = $body . "<li>";
|
|
if ($rule eq $active) {
|
|
$body = $body . "<b>";
|
|
}
|
|
$body = $body . "<form name=\"ruleForm". ${num} ."\" method=\"post\" action=\"/setActive?name=" . $rule . "&start=1&showStartPage=1\">"
|
|
. "<a href=\"javascript: loadRule" .${num} ."()\">" .$rule ."</a></form>"
|
|
. "<script type=\"text/javascript\">function loadRule" .${num} ."() { document.ruleForm". ${num} .".submit(); }</script>";
|
|
if ($rule eq $active) {
|
|
$body = $body . "</b>";
|
|
}
|
|
$body = $body ."</li>";
|
|
$num=$num+1;
|
|
}
|
|
}
|
|
$body = $body . "</ul></p>";
|
|
if (1==$dynamicIsActive) {
|
|
$body = $body . "<br/><p><form method=post enctype=\"text/plain\" action=\"/control?state=stop&showStartPage=1\">"
|
|
. "<input type=\"submit\" name=\"submit\" value=\"Stop Dynamizer\"></form></p>";
|
|
}
|
|
$body = $body . "</body></html>";
|
|
return $body;
|
|
}
|
|
|
|
sub writeToClient() {
|
|
my $client=shift;
|
|
my $message=shift;
|
|
my $addCrlf=shift;
|
|
if ($client->connected()) {
|
|
if (1==$addCrlf) {
|
|
print $client $message, Socket::CRLF;
|
|
} else {
|
|
print $client $message;
|
|
}
|
|
}
|
|
}
|
|
|
|
sub httpServer() {
|
|
my $server = new IO::Socket::INET(Proto => 'tcp', LocalPort => $httpPort, Listen => SOMAXCONN, Reuse => 1);
|
|
$server or die "ERROR: Unable to create HTTP socket (${httpPort}): $!";
|
|
|
|
print "Starting HTTP server on ${httpPort}\n";
|
|
while (my $client = $server->accept()) {
|
|
$client->autoflush(1);
|
|
my $prevStatusTime=$currentStatusTime;
|
|
my %request = ();
|
|
my %data;
|
|
local $/ = Socket::CRLF;
|
|
while (<$client>) {
|
|
chomp; # Main http request
|
|
if (/\s*(\w+)\s*([^\s]+)\s*HTTP\/(\d.\d)/) {
|
|
$request{METHOD} = uc $1;
|
|
$request{URL} = $2;
|
|
$request{HTTP_VERSION} = $3;
|
|
} # Standard headers
|
|
elsif (/:/) {
|
|
(my $type, my $val) = split /:/, $_, 2;
|
|
$type =~ s/^\s+//;
|
|
foreach ($type, $val) {
|
|
s/^\s+//;
|
|
s/\s+$//;
|
|
}
|
|
$request{lc $type} = $val;
|
|
} # POST data
|
|
elsif (/^$/) {
|
|
read($client, $request{CONTENT}, $request{'content-length'})
|
|
if defined $request{'content-length'};
|
|
last;
|
|
}
|
|
}
|
|
local $response="";
|
|
local $responseType="text/plain";
|
|
$queryItems{statusTime}=0;
|
|
|
|
if ($request{URL} =~ /(.*)\?(.*)/) {
|
|
$request{URL} = $1;
|
|
$request{QUERY} = $2;
|
|
my @args=split("&", $request{QUERY});
|
|
for my $arg (@args) {
|
|
(my $type, my $val) = split /=/, $arg, 2;
|
|
$queryItems{$type}=$val;
|
|
}
|
|
}
|
|
# print "${request{METHOD}} URL:${request{URL}} QUERY:${request{QUERY}}\n";
|
|
if ($request{METHOD} eq 'GET') {
|
|
if ($request{URL} eq '/status') {
|
|
local $activeRules=&determineActiveRules();
|
|
$response="STATE:${currentStatus}\nRULES:${activeRules}\nTIME:${currentStatusTime}\n";
|
|
} elsif ($request{URL} eq '/id') {
|
|
$response="ID:${idString}\nGROUP:${mcastGroup}\nPORT:${mcastPort}";
|
|
} elsif ($request{URL} eq '/list') {
|
|
$response = join('', &listRules($queryItems{withDetails}));
|
|
} elsif ($request{URL} eq '/' && 1==$httpControlPage) {
|
|
$responseType="text/html";
|
|
$response = &buildControlPage();
|
|
} else {
|
|
$response="ERROR: Invalid URL";
|
|
}
|
|
} elsif ($request{METHOD} eq 'POST') {
|
|
if ($request{URL} eq '/setActive') {
|
|
my @args=split("&", $request{QUERY});
|
|
|
|
$response=&setActiveRules($queryItems{name});
|
|
if ($response eq "OK" && ($queryItems{start} eq "true" || $queryItems{start} eq "1")) {
|
|
$response=&control("start");
|
|
}
|
|
} elsif ($request{URL} eq '/save') {
|
|
$response=&saveRulesToFile($queryItems{name}, $request{CONTENT});
|
|
} elsif ($request{URL} eq '/control') {
|
|
$response=&control($queryItems{state}, $queryItems{clear});
|
|
}
|
|
} elsif ($request{METHOD} eq 'DELETE') {
|
|
$response=&deleteRules($request{URL});
|
|
} elsif (1==$httpControlPage) {
|
|
$responseType="text/html";
|
|
$response = &buildControlPage();
|
|
}
|
|
|
|
if ($response eq "") {
|
|
&writeToClient($client, "HTTP/1.0 404 Not Found", 1);
|
|
&writeToClient($client, Socket::CRLF, 0);
|
|
&writeToClient($client, "<html><body>404 Not Found</body></html>", 0);
|
|
} elsif ($response =~ m/^ERROR/) {
|
|
&writeToClient($client, "HTTP/1.0 404 Not Found", 1);
|
|
&writeToClient($client, Socket::CRLF);
|
|
&writeToClient($client, "<html><body>${response}</body></html>", 0);
|
|
} elsif ($request{METHOD} eq 'POST') {
|
|
&writeToClient($client, "HTTP/1.0 201 Created", 1);
|
|
if ($queryItems{statusTime}!=0) {
|
|
&writeToClient($client, "Content-type: ${responseType}", 1);
|
|
&writeToClient($client, Socket::CRLF);
|
|
if ($prevStatusTime > $queryItems{statusTime}) {
|
|
&writeToClient($client, "UPDATE_REQUIRED", 1);
|
|
}
|
|
&writeToClient($client, "TIME:".$currentStatusTime, 0);
|
|
} elsif ($queryItems{showStartPage} eq "1") {
|
|
# Reload start page :-)
|
|
$response = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">"
|
|
. "<html><head><meta http-equiv=\"REFRESH\" content=\"0;url=http://" . $request{host} . "\"></head></body></html>";
|
|
&writeToClient($client, "Content-type: text/html", 1);
|
|
&writeToClient($client, Socket::CRLF);
|
|
&writeToClient($client, $response, 1);
|
|
}
|
|
} else {
|
|
&writeToClient($client, "HTTP/1.0 200 OK", 1);
|
|
&writeToClient($client, "Content-type: ${responseType}", 1);
|
|
&writeToClient($client, Socket::CRLF);
|
|
if ($queryItems{statusTime}!=0 && $response eq "OK") {
|
|
&writeToClient($client, "OK", 1);
|
|
if ($prevStatusTime > $queryItems{statusTime}) {
|
|
&writeToClient($client, "UPDATE_REQUIRED", 1);
|
|
}
|
|
&writeToClient($client, "TIME:".$currentStatusTime, 0);
|
|
} else {
|
|
&writeToClient($client, $response);
|
|
}
|
|
}
|
|
close $client;
|
|
}
|
|
}
|
|
|
|
sub serverMode() {
|
|
&loadConfig(shift);
|
|
&daemonize($pidFile);
|
|
$isServerMode=1;
|
|
$dynamicIsActive=0;
|
|
threads->create(\&populatePlayQueue, 1);
|
|
&httpServer();
|
|
&sendServerMessage();
|
|
}
|
|
|
|
sub stopServer() {
|
|
&loadConfig(shift);
|
|
my $pid=&readPid($pidFile);
|
|
if ($pid>0) {
|
|
system("kill", $pid);
|
|
system("pkill", "-P", $pid);
|
|
}
|
|
$currentStatus="TERMINATED";
|
|
&sendServerMessage();
|
|
}
|
|
|
|
if ($ARGV[0] eq "start") {
|
|
&start();
|
|
} elsif ($ARGV[0] eq "stop") {
|
|
&stop();
|
|
} elsif ($ARGV[0] eq "server") {
|
|
&serverMode($ARGV[1]);
|
|
} elsif ($ARGV[0] eq "stopserver") {
|
|
&stopServer($ARGV[1]);
|
|
} elsif ($ARGV[0] eq "test") {
|
|
$testMode=1;
|
|
&populatePlayQueue();
|
|
} else {
|
|
print "Cantata MPD Dynamizer script\n";
|
|
print "\n";
|
|
print "Usage: $0 start|stop|server|stopserver\n";
|
|
}
|