Merge "manager: Add AMI event Load/Unload"
[asterisk/asterisk.git] / agi / jukebox.agi
1 #!/usr/bin/perl
2 #
3 # Jukebox 0.2
4 #
5 # A music manager for Asterisk.
6 #
7 # Copyright (C) 2005-2006, Justin Tunney
8 #
9 # Justin Tunney <jesuscyborg@gmail.com>
10 #
11 # This program is free software, distributed under the terms of the
12 # GNU General Public License v2.
13 #
14 # Keep it open source pigs
15 #
16 # --------------------------------------------------------------------
17 #
18 # Uses festival to list off all your MP3 music files over a channel in
19 # a hierarchical fashion.  Put this file in your agi-bin folder which
20 # is located at: /var/lib/asterisk/agi-bin  Be sure to chmod +x it!
21 #
22 # Invocation Example:
23 #   exten => 68742,1,Answer()
24 #   exten => 68742,2,agi,jukebox.agi|/home/justin/Music
25 #   exten => 68742,3,Hangup()
26 #
27 #   exten => 68742,1,Answer()
28 #   exten => 68742,2,agi,jukebox.agi|/home/justin/Music|pm
29 #   exten => 68742,3,Hangup()
30 #
31 # Options:
32 #   p - Precache text2wave outputs for every possible filename.
33 #       It is much better to set this option because if a caller
34 #       presses a key during a cache operation, it will be ignored.
35 #   m - Go back to menu after playing song
36 #   g - Do not play the greeting message
37 #
38 # Usage Instructions:
39 #   - Press '*' to go up a directory.  If you are in the root music
40 #     folder you will be exitted from the script.
41 #   - If you have a really long list of files, you can filter the list
42 #     at any time by pressing '#' and spelling out a few letters you
43 #     expect the files to start with.  For example, if you wanted to
44 #     know what extension 'Requiem For A Dream' was, you'd type:
45 #     '#737'.  Note, phone keypads don't include Q and Z.  Q is 7 and
46 #     Z is 9.
47 #
48 # Notes:
49 # - This AGI script uses the MP3Player command which uses the
50 #   mpg123 Program.  Grab yourself a copy of this program by
51 #   going to http://www.mpg123.de/cgi-bin/sitexplorer.cgi?/mpg123/
52 #   Be sure to download mpg123-0.59r.tar.gz because it is known to
53 #   work with Asterisk and hopefully isn't the release with that
54 #   awful security problem.  If you're using Fedora Core 3 with
55 #   Alsa like me, make linux-alsa isn't going to work.  Do make
56 #   linux-devel and you're peachy keen.
57 #
58 # - You won't get nifty STDERR debug messages if you're using a
59 #   remote asterisk shell.
60 #
61 # - For some reason, caching certain files will generate the
62 #   error: 'using default diphone ax-ax for y-pau'.  Example:
63 #   # 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 -
64 #   The temporary work around is to just touch these files.
65 #
66 # - The background app doesn't like to get more than 2031 chars
67 #   of input.
68 #
69
70 use strict;
71
72 $|=1;
73
74 # Setup some variables
75 my %AGI; my $tests = 0; my $fail = 0; my $pass = 0;
76 my @masterCacheList = ();
77 my $maxNumber = 10;
78
79 while (<STDIN>) {
80         chomp;
81         last unless length($_);
82         if (/^agi_(\w+)\:\s+(.*)$/) {
83                 $AGI{$1} = $2;
84         }
85 }
86
87 # setup options
88 my $SHOWGREET = 1;
89 my $PRECACHE = 0;
90 my $MENUAFTERSONG = 0;
91
92 $PRECACHE = 1 if $ARGV[1] =~ /p/;
93 $MENUAFTERSONG = 1 if $ARGV[1] =~ /m/;
94 $SHOWGREET = 0 if $ARGV[1] =~ /g/;
95
96 # setup folders
97 my $MUSIC = $ARGV[0];
98 $MUSIC = &rmts($MUSIC);
99 my $FESTIVALCACHE = "/var/jukeboxcache";
100 if (! -e $FESTIVALCACHE) {
101         `mkdir -p -m0776 $FESTIVALCACHE`;
102 }
103
104 # make sure we have some essential files
105 if (! -e "$FESTIVALCACHE/jukebox_greet.ul") {
106         `echo "Welcome to the Asterisk Jukebox" | text2wave -o $FESTIVALCACHE/jukebox_greet.ul -otype ulaw -`;
107 }
108 if (! -e "$FESTIVALCACHE/jukebox_press.ul") {
109         `echo "Press" | text2wave -o $FESTIVALCACHE/jukebox_press.ul -otype ulaw -`;
110 }
111 if (! -e "$FESTIVALCACHE/jukebox_for.ul") {
112         `echo "For" | text2wave -o $FESTIVALCACHE/jukebox_for.ul -otype ulaw -`;
113 }
114 if (! -e "$FESTIVALCACHE/jukebox_toplay.ul") {
115         `echo "To play" | text2wave -o $FESTIVALCACHE/jukebox_toplay.ul -otype ulaw -`;
116 }
117 if (! -e "$FESTIVALCACHE/jukebox_nonefound.ul") {
118         `echo "There were no music files found in this folder" | text2wave -o $FESTIVALCACHE/jukebox_nonefound.ul -otype ulaw -`;
119 }
120 if (! -e "$FESTIVALCACHE/jukebox_percent.ul") {
121         `echo "Percent" | text2wave -o $FESTIVALCACHE/jukebox_percent.ul -otype ulaw -`;
122 }
123 if (! -e "$FESTIVALCACHE/jukebox_generate.ul") {
124         `echo "Please wait while Astrisk Jukebox cashes the files of your music collection" | text2wave -o $FESTIVALCACHE/jukebox_generate.ul -otype ulaw -`;
125 }
126 if (! -e "$FESTIVALCACHE/jukebox_invalid.ul") {
127         `echo "You have entered an invalid selection" | text2wave -o $FESTIVALCACHE/jukebox_invalid.ul -otype ulaw -`;
128 }
129 if (! -e "$FESTIVALCACHE/jukebox_thankyou.ul") {
130         `echo "Thank you for using Astrisk Jukebox, Goodbye" | text2wave -o $FESTIVALCACHE/jukebox_thankyou.ul -otype ulaw -`;
131 }
132
133 # greet the user
134 if ($SHOWGREET) {
135         print "EXEC Playback \"$FESTIVALCACHE/jukebox_greet\"\n";
136         my $result = <STDIN>; &check_result($result);
137 }
138
139 # go through the directories
140 music_dir_cache() if $PRECACHE;
141 music_dir_menu('/');
142
143 exit 0;
144
145 ##########################################################################
146
147 sub music_dir_menu {
148         my $dir = shift;
149
150 # generate a list of mp3's and directories and assign each one it's
151 # own selection number.  Then make sure that we've got a sound clip
152 # for the file name
153         if (!opendir(THEDIR, rmts($MUSIC.$dir))) {
154                 print STDERR "Failed to open music directory: $dir\n";
155                 exit 1;
156         }
157         my @files = sort readdir THEDIR;
158         my $cnt = 1;
159         my @masterBgList = ();
160
161         foreach my $file (@files) {
162                 chomp($file);
163                 if ($file ne '.' && $file ne '..' && $file ne 'festivalcache') { # ignore special files
164                         my $real_version = &rmts($MUSIC.$dir).'/'.$file;
165                         my $cache_version = &rmts($FESTIVALCACHE.$dir).'/'.$file.'.ul';
166                         my $cache_version2 = &rmts($FESTIVALCACHE.$dir).'/'.$file;
167                         my $cache_version_esc = &clean_file($cache_version);
168                         my $cache_version2_esc = &clean_file($cache_version2);
169
170                         if (-d $real_version) {
171 #                                                    0:id    1:type 2:text2wav-file      3:for-filtering             4:the-directory 5:text2wav echo
172                                 push(@masterBgList, [$cnt++, 1,     $cache_version2_esc, &remove_special_chars($file), $file,          "for the $file folder"]);
173                         } elsif ($real_version =~ /\.mp3$/) {
174 #                                                    0:id    1:type 2:text2wav-file      3:for-filtering             4:the-mp3
175                                 push(@masterBgList, [$cnt++, 2,     $cache_version2_esc, &remove_special_chars($file), $real_version,  "to play $file"]);
176                         }
177                 }
178         }
179         close(THEDIR);
180
181         my @filterList = @masterBgList;
182
183         if (@filterList == 0) {
184                 print "EXEC Playback \"$FESTIVALCACHE/jukebox_nonefound\"\n";
185                 my $result = <STDIN>; &check_result($result);
186                 return 0;
187         }
188
189         for (;;) {
190 MYCONTINUE:
191
192 # play bg selections and figure out their selection
193                 my $digit = '';
194                 my $digitstr = '';
195                 for (my $n=0; $n<@filterList; $n++) {
196                         &cache_speech(&remove_file_extension($filterList[$n][5]), "$filterList[$n][2].ul") if ! -e "$filterList[$n][2].ul";
197                         &cache_speech("Press $filterList[$n][0]", "$FESTIVALCACHE/jukebox_$filterList[$n][0].ul") if ! -e "$FESTIVALCACHE/jukebox_$filterList[$n][0].ul";
198                         print "EXEC Background \"$filterList[$n][2]&$FESTIVALCACHE/jukebox_$filterList[$n][0]\"\n";
199                         my $result = <STDIN>;
200                         $digit = &check_result($result);
201                         if ($digit > 0) {
202                                 $digitstr .= chr($digit);
203                                 last;
204                         }
205                 }
206                 for (;;) {
207                         print "WAIT FOR DIGIT 3000\n";
208                         my $result = <STDIN>;
209                         $digit = &check_result($result);
210                         last if $digit <= 0;
211                         $digitstr .= chr($digit);
212                 }
213
214 # see if it's a valid selection
215                 print STDERR "Digits Entered: '$digitstr'\n";
216                 exit 0 if $digitstr eq '';
217                 my $found = 0;
218                 goto EXITSUB if $digitstr =~ /\*/;
219
220 # filter the list
221                 if ($digitstr =~ /^\#\d+/) {
222                         my $regexp = '';
223                         for (my $n=1; $n<length($digitstr); $n++) {
224                                 my $d = substr($digitstr, $n, 1);
225                                 if ($d == 2) {
226                                         $regexp .= '[abc]';
227                                 } elsif ($d == 3) {
228                                         $regexp .= '[def]';
229                                 } elsif ($d == 4) {
230                                         $regexp .= '[ghi]';
231                                 } elsif ($d == 5) {
232                                         $regexp .= '[jkl]';
233                                 } elsif ($d == 6) {
234                                         $regexp .= '[mno]';
235                                 } elsif ($d == 7) {
236                                         $regexp .= '[pqrs]';
237                                 } elsif ($d == 8) {
238                                         $regexp .= '[tuv]';
239                                 } elsif ($d == 9) {
240                                         $regexp .= '[wxyz]';
241                                 }
242                         }
243                         @filterList = ();
244                         for (my $n=1; $n<@masterBgList; $n++) {
245                                 push(@filterList, $masterBgList[$n]) if $masterBgList[$n][3] =~ /^$regexp/i;
246                         }
247                         goto MYCONTINUE;
248                 }
249
250                 for (my $n=0; $n<@masterBgList; $n++) {
251                         if ($digitstr == $masterBgList[$n][0]) {
252                                 if ($masterBgList[$n][1] == 1) { # a folder
253                                         &music_dir_menu(rmts($dir).'/'.$masterBgList[$n][4]);
254                                         @filterList = @masterBgList;
255                                         goto MYCONTINUE;
256                                 } elsif ($masterBgList[$n][1] == 2) { # a file
257 # because *'s scripting language is crunk and won't allow us to escape
258 # funny filenames, we need to create a temporary symlink to the mp3
259 # file
260                                         my $mp3 = &escape_file($masterBgList[$n][4]);
261                                         my $link = `mktemp`;
262                                         chomp($link);
263                                         $link .= '.mp3';
264                                         print STDERR "ln -s $mp3 $link\n";
265                                         my $cmdr = `ln -s $mp3 $link`;
266                                         chomp($cmdr);
267                                         print "Failed to create symlink to mp3: $cmdr\n" if $cmdr ne '';
268
269                                         print "EXEC MP3Player \"$link\"\n";
270                                         my $result = <STDIN>; &check_result($result);
271
272                                         `rm $link`;
273
274                                         if (!$MENUAFTERSONG) {
275                                                 print "EXEC Playback \"$FESTIVALCACHE/jukebox_thankyou\"\n";
276                                                 my $result = <STDIN>; &check_result($result);
277                                                 exit 0;
278                                         } else {
279                                                 goto MYCONTINUE;
280                                         }
281                                 }
282                         }
283                 }
284                 print "EXEC Playback \"$FESTIVALCACHE/jukebox_invalid\"\n";
285                 my $result = <STDIN>; &check_result($result);
286         }
287       EXITSUB:
288 }
289
290 sub cache_speech {
291         my $speech = shift;
292         my $file = shift;
293
294         my $theDir = extract_file_dir($file);
295         `mkdir -p -m0776 $theDir`;
296
297         print STDERR "echo \"$speech\" | text2wave -o $file -otype ulaw -\n";
298         my $cmdr = `echo "$speech" | text2wave -o $file -otype ulaw -`;
299         chomp($cmdr);
300         if ($cmdr =~ /using default diphone/) {
301 # temporary bug work around....
302                 `touch $file`;
303         } elsif ($cmdr ne '') {
304                 print STDERR "Command Failed\n";
305                 exit 1;
306         }
307 }
308
309 sub music_dir_cache {
310 # generate list of text2speech files to generate
311         if (!music_dir_cache_genlist('/')) {
312                 print STDERR "Horrible Dreadful Error: No Music Found in $MUSIC!";
313                 exit 1;
314         }
315
316 # add to list how many 'number' files we have to generate.  We can't
317 # use the SayNumber app in Asterisk because we want to chain all
318 # talking in one Background command.  We also want a consistent
319 # voice...
320         for (my $n=1; $n<=$maxNumber; $n++) {
321                 push(@masterCacheList, [3, "Press $n", "$FESTIVALCACHE/jukebox_$n.ul"]) if ! -e "$FESTIVALCACHE/jukebox_$n.ul";
322         }
323
324 # now generate all these darn text2speech files
325         if (@masterCacheList > 5) {
326                 print "EXEC Playback \"$FESTIVALCACHE/jukebox_generate\"\n";
327                 my $result = <STDIN>; &check_result($result);
328         }
329         my $theTime = time();
330         for (my $n=0; $n < @masterCacheList; $n++) {
331                 my $cmdr = '';
332                 if ($masterCacheList[$n][0] == 1) { # directory
333                         &cache_speech("for folder $masterCacheList[$n][1]", $masterCacheList[$n][2]);
334                 } elsif ($masterCacheList[$n][0] == 2) { # file
335                         &cache_speech("to play $masterCacheList[$n][1]", $masterCacheList[$n][2]);
336                 } elsif ($masterCacheList[$n][0] == 3) { # number
337                         &cache_speech($masterCacheList[$n][1], $masterCacheList[$n][2]);
338                 }
339                 if (time() >= $theTime + 30) {
340                         my $percent = int($n / @masterCacheList * 100);
341                         print "SAY NUMBER $percent \"\"\n";
342                         my $result = <STDIN>; &check_result($result);
343                         print "EXEC Playback \"$FESTIVALCACHE/jukebox_percent\"\n";
344                         my $result = <STDIN>; &check_result($result);
345                         $theTime = time();
346                 }
347         }
348 }
349
350 # this function will fill the @masterCacheList of all the files that
351 # need to have text2speeced ulaw files of their names generated
352 sub music_dir_cache_genlist {
353         my $dir = shift;
354         if (!opendir(THEDIR, rmts($MUSIC.$dir))) {
355                 print STDERR "Failed to open music directory: $dir\n";
356                 exit 1;
357         }
358         my @files = sort readdir THEDIR;
359         my $foundFiles = 0;
360         my $tmpMaxNum = 0;
361         foreach my $file (@files) {
362                 chomp;
363                 if ($file ne '.' && $file ne '..' && $file ne 'festivalcache') { # ignore special files
364                         my $real_version = &rmts($MUSIC.$dir).'/'.$file;
365                         my $cache_version = &rmts($FESTIVALCACHE.$dir).'/'.$file.'.ul';
366                         my $cache_version2 = &rmts($FESTIVALCACHE.$dir).'/'.$file;
367                         my $cache_version_esc = &clean_file($cache_version);
368                         my $cache_version2_esc = &clean_file($cache_version2);
369
370                         if (-d $real_version) {
371                                 if (music_dir_cache_genlist(rmts($dir).'/'.$file)) {
372                                         $tmpMaxNum++;
373                                         $maxNumber = $tmpMaxNum if $tmpMaxNum > $maxNumber;
374                                         push(@masterCacheList, [1, $file, $cache_version_esc]) if ! -e $cache_version_esc;
375                                         $foundFiles = 1;
376                                 }
377                         } elsif ($real_version =~ /\.mp3$/) {
378                                 $tmpMaxNum++;
379                                 $maxNumber = $tmpMaxNum if $tmpMaxNum > $maxNumber;
380                                 push(@masterCacheList, [2, &remove_file_extension($file), $cache_version_esc]) if ! -e $cache_version_esc;
381                                 $foundFiles = 1;
382                         }
383                 }
384         }
385         close(THEDIR);
386         return $foundFiles;
387 }
388
389 sub rmts { # remove trailing slash
390         my $hog = shift;
391         $hog =~ s/\/$//;
392         return $hog;
393 }
394
395 sub extract_file_name {
396         my $hog = shift;
397         $hog =~ /\/?([^\/]+)$/;
398         return $1;
399 }
400
401 sub extract_file_dir {
402         my $hog = shift;
403         return $hog if ! ($hog =~ /\//);
404         $hog =~ /(.*)\/[^\/]*$/;
405         return $1;
406 }
407
408 sub remove_file_extension {
409         my $hog = shift;
410         return $hog if ! ($hog =~ /\./);
411         $hog =~ /(.*)\.[^.]*$/;
412         return $1;
413 }
414
415 sub clean_file {
416         my $hog = shift;
417         $hog =~ s/\\/_/g;
418         $hog =~ s/ /_/g;
419         $hog =~ s/\t/_/g;
420         $hog =~ s/\'/_/g;
421         $hog =~ s/\"/_/g;
422         $hog =~ s/\(/_/g;
423         $hog =~ s/\)/_/g;
424         $hog =~ s/&/_/g;
425         $hog =~ s/\[/_/g;
426         $hog =~ s/\]/_/g;
427         $hog =~ s/\$/_/g;
428         $hog =~ s/\|/_/g;
429         $hog =~ s/\^/_/g;
430         return $hog;
431 }
432
433 sub remove_special_chars {
434         my $hog = shift;
435         $hog =~ s/\\//g;
436         $hog =~ s/ //g;
437         $hog =~ s/\t//g;
438         $hog =~ s/\'//g;
439         $hog =~ s/\"//g;
440         $hog =~ s/\(//g;
441         $hog =~ s/\)//g;
442         $hog =~ s/&//g;
443         $hog =~ s/\[//g;
444         $hog =~ s/\]//g;
445         $hog =~ s/\$//g;
446         $hog =~ s/\|//g;
447         $hog =~ s/\^//g;
448         return $hog;
449 }
450
451 sub escape_file {
452         my $hog = shift;
453         $hog =~ s/\\/\\\\/g;
454         $hog =~ s/ /\\ /g;
455         $hog =~ s/\t/\\\t/g;
456         $hog =~ s/\'/\\\'/g;
457         $hog =~ s/\"/\\\"/g;
458         $hog =~ s/\(/\\\(/g;
459         $hog =~ s/\)/\\\)/g;
460         $hog =~ s/&/\\&/g;
461         $hog =~ s/\[/\\\[/g;
462         $hog =~ s/\]/\\\]/g;
463         $hog =~ s/\$/\\\$/g;
464         $hog =~ s/\|/\\\|/g;
465         $hog =~ s/\^/\\\^/g;
466         return $hog;
467 }
468
469 sub check_result {
470         my ($res) = @_;
471         my $retval;
472         $tests++;
473         chomp $res;
474         if ($res =~ /^200/) {
475                 $res =~ /result=(-?\d+)/;
476                 if (!length($1)) {
477                         print STDERR "FAIL ($res)\n";
478                         $fail++;
479                         exit 1;
480                 } else {
481                         print STDERR "PASS ($1)\n";
482                         return $1;
483                 }
484         } else {
485                 print STDERR "FAIL (unexpected result '$res')\n";
486                 exit 1;
487         }
488 }