jukebox.agi 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  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. use strict;
  70. $|=1;
  71. # Setup some variables
  72. my %AGI; my $tests = 0; my $fail = 0; my $pass = 0;
  73. my @masterCacheList = ();
  74. my $maxNumber = 10;
  75. while (<STDIN>) {
  76. chomp;
  77. last unless length($_);
  78. if (/^agi_(\w+)\:\s+(.*)$/) {
  79. $AGI{$1} = $2;
  80. }
  81. }
  82. # setup options
  83. my $SHOWGREET = 1;
  84. my $PRECACHE = 0;
  85. my $MENUAFTERSONG = 0;
  86. $PRECACHE = 1 if $ARGV[1] =~ /p/;
  87. $MENUAFTERSONG = 1 if $ARGV[1] =~ /m/;
  88. $SHOWGREET = 0 if $ARGV[1] =~ /g/;
  89. # setup folders
  90. my $MUSIC = $ARGV[0];
  91. $MUSIC = &rmts($MUSIC);
  92. my $FESTIVALCACHE = "/var/jukeboxcache";
  93. if (! -e $FESTIVALCACHE) {
  94. `mkdir -p -m0776 $FESTIVALCACHE`;
  95. }
  96. # make sure we have some essential files
  97. if (! -e "$FESTIVALCACHE/jukebox_greet.ul") {
  98. `echo "Welcome to the Asterisk Jukebox" | text2wave -o $FESTIVALCACHE/jukebox_greet.ul -otype ulaw -`;
  99. }
  100. if (! -e "$FESTIVALCACHE/jukebox_press.ul") {
  101. `echo "Press" | text2wave -o $FESTIVALCACHE/jukebox_press.ul -otype ulaw -`;
  102. }
  103. if (! -e "$FESTIVALCACHE/jukebox_for.ul") {
  104. `echo "For" | text2wave -o $FESTIVALCACHE/jukebox_for.ul -otype ulaw -`;
  105. }
  106. if (! -e "$FESTIVALCACHE/jukebox_toplay.ul") {
  107. `echo "To play" | text2wave -o $FESTIVALCACHE/jukebox_toplay.ul -otype ulaw -`;
  108. }
  109. if (! -e "$FESTIVALCACHE/jukebox_nonefound.ul") {
  110. `echo "There were no music files found in this folder" | text2wave -o $FESTIVALCACHE/jukebox_nonefound.ul -otype ulaw -`;
  111. }
  112. if (! -e "$FESTIVALCACHE/jukebox_percent.ul") {
  113. `echo "Percent" | text2wave -o $FESTIVALCACHE/jukebox_percent.ul -otype ulaw -`;
  114. }
  115. if (! -e "$FESTIVALCACHE/jukebox_generate.ul") {
  116. `echo "Please wait while Astrisk Jukebox cashes the files of your music collection" | text2wave -o $FESTIVALCACHE/jukebox_generate.ul -otype ulaw -`;
  117. }
  118. if (! -e "$FESTIVALCACHE/jukebox_invalid.ul") {
  119. `echo "You have entered an invalid selection" | text2wave -o $FESTIVALCACHE/jukebox_invalid.ul -otype ulaw -`;
  120. }
  121. if (! -e "$FESTIVALCACHE/jukebox_thankyou.ul") {
  122. `echo "Thank you for using Astrisk Jukebox, Goodbye" | text2wave -o $FESTIVALCACHE/jukebox_thankyou.ul -otype ulaw -`;
  123. }
  124. # greet the user
  125. if ($SHOWGREET) {
  126. print "EXEC Playback \"$FESTIVALCACHE/jukebox_greet\"\n";
  127. my $result = <STDIN>; &check_result($result);
  128. }
  129. # go through the directories
  130. music_dir_cache() if $PRECACHE;
  131. music_dir_menu('/');
  132. exit 0;
  133. ##########################################################################
  134. sub music_dir_menu {
  135. my $dir = shift;
  136. # generate a list of mp3's and directories and assign each one it's
  137. # own selection number. Then make sure that we've got a sound clip
  138. # for the file name
  139. if (!opendir(THEDIR, rmts($MUSIC.$dir))) {
  140. print STDERR "Failed to open music directory: $dir\n";
  141. exit 1;
  142. }
  143. my @files = sort readdir THEDIR;
  144. my $cnt = 1;
  145. my @masterBgList = ();
  146. foreach my $file (@files) {
  147. chomp($file);
  148. if ($file ne '.' && $file ne '..' && $file ne 'festivalcache') { # ignore special files
  149. my $real_version = &rmts($MUSIC.$dir).'/'.$file;
  150. my $cache_version = &rmts($FESTIVALCACHE.$dir).'/'.$file.'.ul';
  151. my $cache_version2 = &rmts($FESTIVALCACHE.$dir).'/'.$file;
  152. my $cache_version_esc = &clean_file($cache_version);
  153. my $cache_version2_esc = &clean_file($cache_version2);
  154. if (-d $real_version) {
  155. # 0:id 1:type 2:text2wav-file 3:for-filtering 4:the-directory 5:text2wav echo
  156. push(@masterBgList, [$cnt++, 1, $cache_version2_esc, &remove_special_chars($file), $file, "for the $file folder"]);
  157. } elsif ($real_version =~ /\.mp3$/) {
  158. # 0:id 1:type 2:text2wav-file 3:for-filtering 4:the-mp3
  159. push(@masterBgList, [$cnt++, 2, $cache_version2_esc, &remove_special_chars($file), $real_version, "to play $file"]);
  160. }
  161. }
  162. }
  163. close(THEDIR);
  164. my @filterList = @masterBgList;
  165. if (@filterList == 0) {
  166. print "EXEC Playback \"$FESTIVALCACHE/jukebox_nonefound\"\n";
  167. my $result = <STDIN>; &check_result($result);
  168. return 0;
  169. }
  170. for (;;) {
  171. MYCONTINUE:
  172. # play bg selections and figure out their selection
  173. my $digit = '';
  174. my $digitstr = '';
  175. for (my $n=0; $n<@filterList; $n++) {
  176. &cache_speech(&remove_file_extension($filterList[$n][5]), "$filterList[$n][2].ul") if ! -e "$filterList[$n][2].ul";
  177. &cache_speech("Press $filterList[$n][0]", "$FESTIVALCACHE/jukebox_$filterList[$n][0].ul") if ! -e "$FESTIVALCACHE/jukebox_$filterList[$n][0].ul";
  178. print "EXEC Background \"$filterList[$n][2]&$FESTIVALCACHE/jukebox_$filterList[$n][0]\"\n";
  179. my $result = <STDIN>;
  180. $digit = &check_result($result);
  181. if ($digit > 0) {
  182. $digitstr .= chr($digit);
  183. last;
  184. }
  185. }
  186. for (;;) {
  187. print "WAIT FOR DIGIT 3000\n";
  188. my $result = <STDIN>;
  189. $digit = &check_result($result);
  190. last if $digit <= 0;
  191. $digitstr .= chr($digit);
  192. }
  193. # see if it's a valid selection
  194. print STDERR "Digits Entered: '$digitstr'\n";
  195. exit 0 if $digitstr eq '';
  196. my $found = 0;
  197. goto EXITSUB if $digitstr =~ /\*/;
  198. # filter the list
  199. if ($digitstr =~ /^\#\d+/) {
  200. my $regexp = '';
  201. for (my $n=1; $n<length($digitstr); $n++) {
  202. my $d = substr($digitstr, $n, 1);
  203. if ($d == 2) {
  204. $regexp .= '[abc]';
  205. } elsif ($d == 3) {
  206. $regexp .= '[def]';
  207. } elsif ($d == 4) {
  208. $regexp .= '[ghi]';
  209. } elsif ($d == 5) {
  210. $regexp .= '[jkl]';
  211. } elsif ($d == 6) {
  212. $regexp .= '[mno]';
  213. } elsif ($d == 7) {
  214. $regexp .= '[pqrs]';
  215. } elsif ($d == 8) {
  216. $regexp .= '[tuv]';
  217. } elsif ($d == 9) {
  218. $regexp .= '[wxyz]';
  219. }
  220. }
  221. @filterList = ();
  222. for (my $n=1; $n<@masterBgList; $n++) {
  223. push(@filterList, $masterBgList[$n]) if $masterBgList[$n][3] =~ /^$regexp/i;
  224. }
  225. goto MYCONTINUE;
  226. }
  227. for (my $n=0; $n<@masterBgList; $n++) {
  228. if ($digitstr == $masterBgList[$n][0]) {
  229. if ($masterBgList[$n][1] == 1) { # a folder
  230. &music_dir_menu(rmts($dir).'/'.$masterBgList[$n][4]);
  231. @filterList = @masterBgList;
  232. goto MYCONTINUE;
  233. } elsif ($masterBgList[$n][1] == 2) { # a file
  234. # because *'s scripting language is crunk and won't allow us to escape
  235. # funny filenames, we need to create a temporary symlink to the mp3
  236. # file
  237. my $mp3 = &escape_file($masterBgList[$n][4]);
  238. my $link = `mktemp`;
  239. chomp($link);
  240. $link .= '.mp3';
  241. print STDERR "ln -s $mp3 $link\n";
  242. my $cmdr = `ln -s $mp3 $link`;
  243. chomp($cmdr);
  244. print "Failed to create symlink to mp3: $cmdr\n" if $cmdr ne '';
  245. print "EXEC MP3Player \"$link\"\n";
  246. my $result = <STDIN>; &check_result($result);
  247. `rm $link`;
  248. if (!$MENUAFTERSONG) {
  249. print "EXEC Playback \"$FESTIVALCACHE/jukebox_thankyou\"\n";
  250. my $result = <STDIN>; &check_result($result);
  251. exit 0;
  252. } else {
  253. goto MYCONTINUE;
  254. }
  255. }
  256. }
  257. }
  258. print "EXEC Playback \"$FESTIVALCACHE/jukebox_invalid\"\n";
  259. my $result = <STDIN>; &check_result($result);
  260. }
  261. EXITSUB:
  262. }
  263. sub cache_speech {
  264. my $speech = shift;
  265. my $file = shift;
  266. my $theDir = extract_file_dir($file);
  267. `mkdir -p -m0776 $theDir`;
  268. print STDERR "echo \"$speech\" | text2wave -o $file -otype ulaw -\n";
  269. my $cmdr = `echo "$speech" | text2wave -o $file -otype ulaw -`;
  270. chomp($cmdr);
  271. if ($cmdr =~ /using default diphone/) {
  272. # temporary bug work around....
  273. `touch $file`;
  274. } elsif ($cmdr ne '') {
  275. print STDERR "Command Failed\n";
  276. exit 1;
  277. }
  278. }
  279. sub music_dir_cache {
  280. # generate list of text2speech files to generate
  281. if (!music_dir_cache_genlist('/')) {
  282. print STDERR "Horrible Dreadful Error: No Music Found in $MUSIC!";
  283. exit 1;
  284. }
  285. # add to list how many 'number' files we have to generate. We can't
  286. # use the SayNumber app in Asterisk because we want to chain all
  287. # talking in one Background command. We also want a consistent
  288. # voice...
  289. for (my $n=1; $n<=$maxNumber; $n++) {
  290. push(@masterCacheList, [3, "Press $n", "$FESTIVALCACHE/jukebox_$n.ul"]) if ! -e "$FESTIVALCACHE/jukebox_$n.ul";
  291. }
  292. # now generate all these darn text2speech files
  293. if (@masterCacheList > 5) {
  294. print "EXEC Playback \"$FESTIVALCACHE/jukebox_generate\"\n";
  295. my $result = <STDIN>; &check_result($result);
  296. }
  297. my $theTime = time();
  298. for (my $n=0; $n < @masterCacheList; $n++) {
  299. my $cmdr = '';
  300. if ($masterCacheList[$n][0] == 1) { # directory
  301. &cache_speech("for folder $masterCacheList[$n][1]", $masterCacheList[$n][2]);
  302. } elsif ($masterCacheList[$n][0] == 2) { # file
  303. &cache_speech("to play $masterCacheList[$n][1]", $masterCacheList[$n][2]);
  304. } elsif ($masterCacheList[$n][0] == 3) { # number
  305. &cache_speech($masterCacheList[$n][1], $masterCacheList[$n][2]);
  306. }
  307. if (time() >= $theTime + 30) {
  308. my $percent = int($n / @masterCacheList * 100);
  309. print "SAY NUMBER $percent \"\"\n";
  310. my $result = <STDIN>; &check_result($result);
  311. print "EXEC Playback \"$FESTIVALCACHE/jukebox_percent\"\n";
  312. my $result = <STDIN>; &check_result($result);
  313. $theTime = time();
  314. }
  315. }
  316. }
  317. # this function will fill the @masterCacheList of all the files that
  318. # need to have text2speeced ulaw files of their names generated
  319. sub music_dir_cache_genlist {
  320. my $dir = shift;
  321. if (!opendir(THEDIR, rmts($MUSIC.$dir))) {
  322. print STDERR "Failed to open music directory: $dir\n";
  323. exit 1;
  324. }
  325. my @files = sort readdir THEDIR;
  326. my $foundFiles = 0;
  327. my $tmpMaxNum = 0;
  328. foreach my $file (@files) {
  329. chomp;
  330. if ($file ne '.' && $file ne '..' && $file ne 'festivalcache') { # ignore special files
  331. my $real_version = &rmts($MUSIC.$dir).'/'.$file;
  332. my $cache_version = &rmts($FESTIVALCACHE.$dir).'/'.$file.'.ul';
  333. my $cache_version2 = &rmts($FESTIVALCACHE.$dir).'/'.$file;
  334. my $cache_version_esc = &clean_file($cache_version);
  335. my $cache_version2_esc = &clean_file($cache_version2);
  336. if (-d $real_version) {
  337. if (music_dir_cache_genlist(rmts($dir).'/'.$file)) {
  338. $tmpMaxNum++;
  339. $maxNumber = $tmpMaxNum if $tmpMaxNum > $maxNumber;
  340. push(@masterCacheList, [1, $file, $cache_version_esc]) if ! -e $cache_version_esc;
  341. $foundFiles = 1;
  342. }
  343. } elsif ($real_version =~ /\.mp3$/) {
  344. $tmpMaxNum++;
  345. $maxNumber = $tmpMaxNum if $tmpMaxNum > $maxNumber;
  346. push(@masterCacheList, [2, &remove_file_extension($file), $cache_version_esc]) if ! -e $cache_version_esc;
  347. $foundFiles = 1;
  348. }
  349. }
  350. }
  351. close(THEDIR);
  352. return $foundFiles;
  353. }
  354. sub rmts { # remove trailing slash
  355. my $hog = shift;
  356. $hog =~ s/\/$//;
  357. return $hog;
  358. }
  359. sub extract_file_name {
  360. my $hog = shift;
  361. $hog =~ /\/?([^\/]+)$/;
  362. return $1;
  363. }
  364. sub extract_file_dir {
  365. my $hog = shift;
  366. return $hog if ! ($hog =~ /\//);
  367. $hog =~ /(.*)\/[^\/]*$/;
  368. return $1;
  369. }
  370. sub remove_file_extension {
  371. my $hog = shift;
  372. return $hog if ! ($hog =~ /\./);
  373. $hog =~ /(.*)\.[^.]*$/;
  374. return $1;
  375. }
  376. sub clean_file {
  377. my $hog = shift;
  378. $hog =~ s/\\/_/g;
  379. $hog =~ s/ /_/g;
  380. $hog =~ s/\t/_/g;
  381. $hog =~ s/\'/_/g;
  382. $hog =~ s/\"/_/g;
  383. $hog =~ s/\(/_/g;
  384. $hog =~ s/\)/_/g;
  385. $hog =~ s/&/_/g;
  386. $hog =~ s/\[/_/g;
  387. $hog =~ s/\]/_/g;
  388. $hog =~ s/\$/_/g;
  389. $hog =~ s/\|/_/g;
  390. $hog =~ s/\^/_/g;
  391. return $hog;
  392. }
  393. sub remove_special_chars {
  394. my $hog = shift;
  395. $hog =~ s/\\//g;
  396. $hog =~ s/ //g;
  397. $hog =~ s/\t//g;
  398. $hog =~ s/\'//g;
  399. $hog =~ s/\"//g;
  400. $hog =~ s/\(//g;
  401. $hog =~ s/\)//g;
  402. $hog =~ s/&//g;
  403. $hog =~ s/\[//g;
  404. $hog =~ s/\]//g;
  405. $hog =~ s/\$//g;
  406. $hog =~ s/\|//g;
  407. $hog =~ s/\^//g;
  408. return $hog;
  409. }
  410. sub escape_file {
  411. my $hog = shift;
  412. $hog =~ s/\\/\\\\/g;
  413. $hog =~ s/ /\\ /g;
  414. $hog =~ s/\t/\\\t/g;
  415. $hog =~ s/\'/\\\'/g;
  416. $hog =~ s/\"/\\\"/g;
  417. $hog =~ s/\(/\\\(/g;
  418. $hog =~ s/\)/\\\)/g;
  419. $hog =~ s/&/\\&/g;
  420. $hog =~ s/\[/\\\[/g;
  421. $hog =~ s/\]/\\\]/g;
  422. $hog =~ s/\$/\\\$/g;
  423. $hog =~ s/\|/\\\|/g;
  424. $hog =~ s/\^/\\\^/g;
  425. return $hog;
  426. }
  427. sub check_result {
  428. my ($res) = @_;
  429. my $retval;
  430. $tests++;
  431. chomp $res;
  432. if ($res =~ /^200/) {
  433. $res =~ /result=(-?\d+)/;
  434. if (!length($1)) {
  435. print STDERR "FAIL ($res)\n";
  436. $fail++;
  437. exit 1;
  438. } else {
  439. print STDERR "PASS ($1)\n";
  440. return $1;
  441. }
  442. } else {
  443. print STDERR "FAIL (unexpected result '$res')\n";
  444. exit 1;
  445. }
  446. }