diff --git a/agi/Makefile b/agi/Makefile index d9d10dcd3..a2f3bee05 100644 --- a/agi/Makefile +++ b/agi/Makefile @@ -11,7 +11,7 @@ # the GNU General Public License # -AGIS=agi-test.agi eagi-test eagi-sphinx-test +AGIS=agi-test.agi eagi-test eagi-sphinx-test jukebox.agi CFLAGS+= diff --git a/agi/jukebox.agi b/agi/jukebox.agi new file mode 100755 index 000000000..7bd9c10f9 --- /dev/null +++ b/agi/jukebox.agi @@ -0,0 +1,488 @@ +#!/usr/bin/perl +# +# Jukebox 0.2 +# +# A music manager for Asterisk. +# +# Copyright (C) 2005-2006, Justin Tunney +# +# Justin Tunney +# +# This program is free software, distributed under the terms of the +# GNU General Public License v2. +# +# Keep it open source pigs +# +# -------------------------------------------------------------------- +# +# Uses festival to list off all your MP3 music files over a channel in +# a hierarchical fashion. Put this file in your agi-bin folder which +# is located at: /var/lib/asterisk/agi-bin Be sure to chmod +x it! +# +# Invocation Example: +# exten => 68742,1,Answer() +# exten => 68742,2,agi,jukebox.agi|/home/justin/Music +# exten => 68742,3,Hangup() +# +# exten => 68742,1,Answer() +# exten => 68742,2,agi,jukebox.agi|/home/justin/Music|pm +# exten => 68742,3,Hangup() +# +# Options: +# p - Precache text2wave outputs for every possible filename. +# It is much better to set this option because if a caller +# presses a key during a cache operation, it will be ignored. +# m - Go back to menu after playing song +# g - Do not play the greeting message +# +# Usage Instructions: +# - Press '*' to go up a directory. If you are in the root music +# folder you will be exitted from the script. +# - If you have a really long list of files, you can filter the list +# at any time by pressing '#' and spelling out a few letters you +# expect the files to start with. For example, if you wanted to +# know what extension 'Requiem For A Dream' was, you'd type: +# '#737'. Note, phone keypads don't include Q and Z. Q is 7 and +# Z is 9. +# +# Notes: +# - This AGI script uses the MP3Player command which uses the +# mpg123 Program. Grab yourself a copy of this program by +# going to http://www.mpg123.de/cgi-bin/sitexplorer.cgi?/mpg123/ +# Be sure to download mpg123-0.59r.tar.gz because it is known to +# work with Asterisk and hopefully isn't the release with that +# awful security problem. If you're using Fedora Core 3 with +# Alsa like me, make linux-alsa isn't going to work. Do make +# linux-devel and you're peachy keen. +# +# - You won't get nifty STDERR debug messages if you're using a +# remote asterisk shell. +# +# - For some reason, caching certain files will generate the +# error: 'using default diphone ax-ax for y-pau'. Example: +# # echo "Depeche Mode - CUW - 05 - The Meaning of Love" | text2wave -o /var/jukeboxcache/jukeboxcache/Depeche_Mode/Depeche_Mode_-_CUW_-_05_-_The_Meaning_of_Love.mp3.ul -otype ulaw - +# The temporary work around is to just touch these files. +# +# - The background app doesn't like to get more than 2031 chars +# of input. +# + +use strict; + +$|=1; + +# Setup some variables +my %AGI; my $tests = 0; my $fail = 0; my $pass = 0; +my @masterCacheList = (); +my $maxNumber = 10; + +while () { + chomp; + last unless length($_); + if (/^agi_(\w+)\:\s+(.*)$/) { + $AGI{$1} = $2; + } +} + +# setup options +my $SHOWGREET = 1; +my $PRECACHE = 0; +my $MENUAFTERSONG = 0; + +$PRECACHE = 1 if $ARGV[1] =~ /p/; +$MENUAFTERSONG = 1 if $ARGV[1] =~ /m/; +$SHOWGREET = 0 if $ARGV[1] =~ /g/; + +# setup folders +my $MUSIC = $ARGV[0]; +$MUSIC = &rmts($MUSIC); +my $FESTIVALCACHE = "/var/jukeboxcache"; +if (! -e $FESTIVALCACHE) { + `mkdir -p -m0776 $FESTIVALCACHE`; +} + +# make sure we have some essential files +if (! -e "$FESTIVALCACHE/jukebox_greet.ul") { + `echo "Welcome to the Asterisk Jukebox" | text2wave -o $FESTIVALCACHE/jukebox_greet.ul -otype ulaw -`; +} +if (! -e "$FESTIVALCACHE/jukebox_press.ul") { + `echo "Press" | text2wave -o $FESTIVALCACHE/jukebox_press.ul -otype ulaw -`; +} +if (! -e "$FESTIVALCACHE/jukebox_for.ul") { + `echo "For" | text2wave -o $FESTIVALCACHE/jukebox_for.ul -otype ulaw -`; +} +if (! -e "$FESTIVALCACHE/jukebox_toplay.ul") { + `echo "To play" | text2wave -o $FESTIVALCACHE/jukebox_toplay.ul -otype ulaw -`; +} +if (! -e "$FESTIVALCACHE/jukebox_nonefound.ul") { + `echo "There were no music files found in this folder" | text2wave -o $FESTIVALCACHE/jukebox_nonefound.ul -otype ulaw -`; +} +if (! -e "$FESTIVALCACHE/jukebox_percent.ul") { + `echo "Percent" | text2wave -o $FESTIVALCACHE/jukebox_percent.ul -otype ulaw -`; +} +if (! -e "$FESTIVALCACHE/jukebox_generate.ul") { + `echo "Please wait while Astrisk Jukebox cashes the files of your music collection" | text2wave -o $FESTIVALCACHE/jukebox_generate.ul -otype ulaw -`; +} +if (! -e "$FESTIVALCACHE/jukebox_invalid.ul") { + `echo "You have entered an invalid selection" | text2wave -o $FESTIVALCACHE/jukebox_invalid.ul -otype ulaw -`; +} +if (! -e "$FESTIVALCACHE/jukebox_thankyou.ul") { + `echo "Thank you for using Astrisk Jukebox, Goodbye" | text2wave -o $FESTIVALCACHE/jukebox_thankyou.ul -otype ulaw -`; +} + +# greet the user +if ($SHOWGREET) { + print "EXEC Playback \"$FESTIVALCACHE/jukebox_greet\"\n"; + my $result = ; &check_result($result); +} + +# go through the directories +music_dir_cache() if $PRECACHE; +music_dir_menu('/'); + +exit 0; + +########################################################################## + +sub music_dir_menu { + my $dir = shift; + +# generate a list of mp3's and directories and assign each one it's +# own selection number. Then make sure that we've got a sound clip +# for the file name + if (!opendir(THEDIR, rmts($MUSIC.$dir))) { + print STDERR "Failed to open music directory: $dir\n"; + exit 1; + } + my @files = sort readdir THEDIR; + my $cnt = 1; + my @masterBgList = (); + + foreach my $file (@files) { + chomp($file); + if ($file ne '.' && $file ne '..' && $file ne 'festivalcache') { # ignore special files + my $real_version = &rmts($MUSIC.$dir).'/'.$file; + my $cache_version = &rmts($FESTIVALCACHE.$dir).'/'.$file.'.ul'; + my $cache_version2 = &rmts($FESTIVALCACHE.$dir).'/'.$file; + my $cache_version_esc = &clean_file($cache_version); + my $cache_version2_esc = &clean_file($cache_version2); + + if (-d $real_version) { +# 0:id 1:type 2:text2wav-file 3:for-filtering 4:the-directory 5:text2wav echo + push(@masterBgList, [$cnt++, 1, $cache_version2_esc, &remove_special_chars($file), $file, "for the $file folder"]); + } elsif ($real_version =~ /\.mp3$/) { +# 0:id 1:type 2:text2wav-file 3:for-filtering 4:the-mp3 + push(@masterBgList, [$cnt++, 2, $cache_version2_esc, &remove_special_chars($file), $real_version, "to play $file"]); + } + } + } + close(THEDIR); + + my @filterList = @masterBgList; + + if (@filterList == 0) { + print "EXEC Playback \"$FESTIVALCACHE/jukebox_nonefound\"\n"; + my $result = ; &check_result($result); + return 0; + } + + for (;;) { +MYCONTINUE: + +# play bg selections and figure out their selection + my $digit = ''; + my $digitstr = ''; + for (my $n=0; $n<@filterList; $n++) { + &cache_speech(&remove_file_extension($filterList[$n][5]), "$filterList[$n][2].ul") if ! -e "$filterList[$n][2].ul"; + &cache_speech("Press $filterList[$n][0]", "$FESTIVALCACHE/jukebox_$filterList[$n][0].ul") if ! -e "$FESTIVALCACHE/jukebox_$filterList[$n][0].ul"; + print "EXEC Background \"$filterList[$n][2]&$FESTIVALCACHE/jukebox_$filterList[$n][0]\"\n"; + my $result = ; + $digit = &check_result($result); + if ($digit > 0) { + $digitstr .= chr($digit); + last; + } + } + for (;;) { + print "WAIT FOR DIGIT 3000\n"; + my $result = ; + $digit = &check_result($result); + last if $digit <= 0; + $digitstr .= chr($digit); + } + +# see if it's a valid selection + print STDERR "Digits Entered: '$digitstr'\n"; + exit 0 if $digitstr eq ''; + my $found = 0; + goto EXITSUB if $digitstr =~ /\*/; + +# filter the list + if ($digitstr =~ /^\#\d+/) { + my $regexp = ''; + for (my $n=1; $n; &check_result($result); + + `rm $link`; + + if (!$MENUAFTERSONG) { + print "EXEC Playback \"$FESTIVALCACHE/jukebox_thankyou\"\n"; + my $result = ; &check_result($result); + exit 0; + } else { + goto MYCONTINUE; + } + } + } + } + print "EXEC Playback \"$FESTIVALCACHE/jukebox_invalid\"\n"; + my $result = ; &check_result($result); + } + EXITSUB: +} + +sub cache_speech { + my $speech = shift; + my $file = shift; + + my $theDir = extract_file_dir($file); + `mkdir -p -m0776 $theDir`; + + print STDERR "echo \"$speech\" | text2wave -o $file -otype ulaw -\n"; + my $cmdr = `echo "$speech" | text2wave -o $file -otype ulaw -`; + chomp($cmdr); + if ($cmdr =~ /using default diphone/) { +# temporary bug work around.... + `touch $file`; + } elsif ($cmdr ne '') { + print STDERR "Command Failed\n"; + exit 1; + } +} + +sub music_dir_cache { +# generate list of text2speech files to generate + if (!music_dir_cache_genlist('/')) { + print STDERR "Horrible Dreadful Error: No Music Found in $MUSIC!"; + exit 1; + } + +# add to list how many 'number' files we have to generate. We can't +# use the SayNumber app in Asterisk because we want to chain all +# talking in one Background command. We also want a consistent +# voice... + for (my $n=1; $n<=$maxNumber; $n++) { + push(@masterCacheList, [3, "Press $n", "$FESTIVALCACHE/jukebox_$n.ul"]) if ! -e "$FESTIVALCACHE/jukebox_$n.ul"; + } + +# now generate all these darn text2speech files + if (@masterCacheList > 5) { + print "EXEC Playback \"$FESTIVALCACHE/jukebox_generate\"\n"; + my $result = ; &check_result($result); + } + my $theTime = time(); + for (my $n=0; $n < @masterCacheList; $n++) { + my $cmdr = ''; + if ($masterCacheList[$n][0] == 1) { # directory + &cache_speech("for folder $masterCacheList[$n][1]", $masterCacheList[$n][2]); + } elsif ($masterCacheList[$n][0] == 2) { # file + &cache_speech("to play $masterCacheList[$n][1]", $masterCacheList[$n][2]); + } elsif ($masterCacheList[$n][0] == 3) { # number + &cache_speech($masterCacheList[$n][1], $masterCacheList[$n][2]); + } + if (time() >= $theTime + 30) { + my $percent = int($n / @masterCacheList * 100); + print "SAY NUMBER $percent \"\"\n"; + my $result = ; &check_result($result); + print "EXEC Playback \"$FESTIVALCACHE/jukebox_percent\"\n"; + my $result = ; &check_result($result); + $theTime = time(); + } + } +} + +# this function will fill the @masterCacheList of all the files that +# need to have text2speeced ulaw files of their names generated +sub music_dir_cache_genlist { + my $dir = shift; + if (!opendir(THEDIR, rmts($MUSIC.$dir))) { + print STDERR "Failed to open music directory: $dir\n"; + exit 1; + } + my @files = sort readdir THEDIR; + my $foundFiles = 0; + my $tmpMaxNum = 0; + foreach my $file (@files) { + chomp; + if ($file ne '.' && $file ne '..' && $file ne 'festivalcache') { # ignore special files + my $real_version = &rmts($MUSIC.$dir).'/'.$file; + my $cache_version = &rmts($FESTIVALCACHE.$dir).'/'.$file.'.ul'; + my $cache_version2 = &rmts($FESTIVALCACHE.$dir).'/'.$file; + my $cache_version_esc = &clean_file($cache_version); + my $cache_version2_esc = &clean_file($cache_version2); + + if (-d $real_version) { + if (music_dir_cache_genlist(rmts($dir).'/'.$file)) { + $tmpMaxNum++; + $maxNumber = $tmpMaxNum if $tmpMaxNum > $maxNumber; + push(@masterCacheList, [1, $file, $cache_version_esc]) if ! -e $cache_version_esc; + $foundFiles = 1; + } + } elsif ($real_version =~ /\.mp3$/) { + $tmpMaxNum++; + $maxNumber = $tmpMaxNum if $tmpMaxNum > $maxNumber; + push(@masterCacheList, [2, &remove_file_extension($file), $cache_version_esc]) if ! -e $cache_version_esc; + $foundFiles = 1; + } + } + } + close(THEDIR); + return $foundFiles; +} + +sub rmts { # remove trailing slash + my $hog = shift; + $hog =~ s/\/$//; + return $hog; +} + +sub extract_file_name { + my $hog = shift; + $hog =~ /\/?([^\/]+)$/; + return $1; +} + +sub extract_file_dir { + my $hog = shift; + return $hog if ! ($hog =~ /\//); + $hog =~ /(.*)\/[^\/]*$/; + return $1; +} + +sub remove_file_extension { + my $hog = shift; + return $hog if ! ($hog =~ /\./); + $hog =~ /(.*)\.[^.]*$/; + return $1; +} + +sub clean_file { + my $hog = shift; + $hog =~ s/\\/_/g; + $hog =~ s/ /_/g; + $hog =~ s/\t/_/g; + $hog =~ s/\'/_/g; + $hog =~ s/\"/_/g; + $hog =~ s/\(/_/g; + $hog =~ s/\)/_/g; + $hog =~ s/&/_/g; + $hog =~ s/\[/_/g; + $hog =~ s/\]/_/g; + $hog =~ s/\$/_/g; + $hog =~ s/\|/_/g; + $hog =~ s/\^/_/g; + return $hog; +} + +sub remove_special_chars { + my $hog = shift; + $hog =~ s/\\//g; + $hog =~ s/ //g; + $hog =~ s/\t//g; + $hog =~ s/\'//g; + $hog =~ s/\"//g; + $hog =~ s/\(//g; + $hog =~ s/\)//g; + $hog =~ s/&//g; + $hog =~ s/\[//g; + $hog =~ s/\]//g; + $hog =~ s/\$//g; + $hog =~ s/\|//g; + $hog =~ s/\^//g; + return $hog; +} + +sub escape_file { + my $hog = shift; + $hog =~ s/\\/\\\\/g; + $hog =~ s/ /\\ /g; + $hog =~ s/\t/\\\t/g; + $hog =~ s/\'/\\\'/g; + $hog =~ s/\"/\\\"/g; + $hog =~ s/\(/\\\(/g; + $hog =~ s/\)/\\\)/g; + $hog =~ s/&/\\&/g; + $hog =~ s/\[/\\\[/g; + $hog =~ s/\]/\\\]/g; + $hog =~ s/\$/\\\$/g; + $hog =~ s/\|/\\\|/g; + $hog =~ s/\^/\\\^/g; + return $hog; +} + +sub check_result { + my ($res) = @_; + my $retval; + $tests++; + chomp $res; + if ($res =~ /^200/) { + $res =~ /result=(-?\d+)/; + if (!length($1)) { + print STDERR "FAIL ($res)\n"; + $fail++; + exit 1; + } else { + print STDERR "PASS ($1)\n"; + return $1; + } + } else { + print STDERR "FAIL (unexpected result '$res')\n"; + exit 1; + } +}