#!/usr/bin/perl -w use strict; # jukebox - http://raf.org/jukebox/ # # Copyright (C) 2002 raf # # 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; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # or visit http://www.gnu.org/copyleft/gpl.html # jukebox - play selected music continuously in random order # # 20021208 raf # Set defaults and load configuration my ($name) = $0 =~ /([^\/]+)$/; my $default_root = '/mnt/music'; my $default_tocglob = '00*.toc'; my $default_gap = 2; my $default_player = 'wav=play,mp3=mpg321 -q,ogg=ogg123 -q,flac=flac -sdc % | play -t wav -'; my $default_opts = 'short'; load('/etc/jukebox.conf'); load("$ENV{HOME}/.jukeboxrc"); sub help { print << "ENDHELP"; NAME $name - play selected music continuously in random order SYNOPSIS $name [options] [--] [selector...] options: -h, --help - Show the help message then exit -V, --version - Show the version message then exit -d, --debug - Print debug messages -a, --all - Select everything (not just your favourites) -j, --strict - Search for selectors only in "Jukebox" tags -m, --mandatory - Search treating all selectors as mandatory -n, --list - Just list matching tracks (don't play them) -x, --extra - Show artist/album/title as well (implies -n) -e, --elide - Suppress duplicate artist/albums (implies -x) -v, --withtags - Include tags when listing tracks (implies -x) -s, --sequential - Play tracks sequentially, then stop -c, --continuous - Play continuously (even if -s) -i, --nouser - No user interaction between tracks -g, --gap seconds - Inter track gap (in seconds) -f, --playlist file - Play tracks from a playlist file -r, --root dir[:dir]* - Override default music directory ($default_root) -t, --tocglob tocglob - Override default tocfile glob ($default_tocglob) -p, --player player - Override default music players ($default_player) -u, --unlink - Unlink playlist (see -f) after reading it -T, --tagstats - List known selectors/categories/tags -P, --perl - Perl junkie (perl re in search criteria) DESCRIPTION $name plays selected music continuously in a random order. By default, $name only plays your favourite tracks (see below). If the -a option is supplied, any tracks might be played. If selectors are supplied, $name plays matching tracks. For a track to match, it must: - match all of the selectors that start with '+', - match none of the selectors that start with '-', and - match at least one of the ordinary selectors. Searches are case and accent insensitive. Phrases can be quoted with single or double quotes (courtesy of the shell). $name searches tocfiles for selectors in: - "Artist:", "Title:" and "Jukebox:" (and any other) headers, - track titles, and - track specific "[Jukebox: ...]" tags. Your favourite tracks are defined to be those that are tagged with your login name. The default selector is your login name. The -j option causes $name to ignore artist names and album and track titles. The search for selectors only looks at "Jukebox:" headers and track specific "Jukebox:" tags. This searches for tracks by category rather than by name. The -m option causes all selectors to be treated as mandatory. This has the same effect as adding a '+' character to the start of every selector that does not already start with a '+' or '-' character. The -f option plays tracks listed in a playlist file rather than using selectors to search for tracks. The format of the playlist is one filename per line (just like the output of the -n option). The -u option causes this file to be deleted after it is read. The -s option plays the selected tracks in order, and then stops. This can be used to play a single album. If only a single track is selected, then the -s option is assumed. Adding the -c option repeats the in order selection continuously rather than stopping. This can be used to drive the neighbours mad while you're on holiday. After each track is played, the user is asked whether or not they wish to continue. If there is no answer within two seconds, jukebox continues with the next track. If the user enters 'n' quickly enough, jukebox will terminate. The -i option causes this interaction to be suppressed, so jukebox will continue until it is killed by external means. The -g option causes a pause between tracks. The -n option just prints the selected tracks without playing them. The -x option adds the artist, album and track titles to this output. The -e option suppresses duplicate artists and album titles from this output (the fields are present but empty). This reduces network traffic. The -v option adds a field containing each track's jukebox tags. This is to aid remote robot clients that need more information for compiling playlists. The -e and -v options imply the -x option and the -x option implies the -n option. The -t, -r and -p options override various default values. The argument to the -r option can contain a colon separated list of directories to select tracks from multiple places. The -P option allows perl "regular expressions" in the search criteria. The -T option lists all categories used in all tocfiles (after the "Jukebox:" header or in track specific "[Jukebox: ...]" tags). After each category there are four numbers: - the number of albums that match the category, - the number of individual tracks that explicitly match the category, - the total number of tracks that match (inherited or explicit), and - the number of selected tracks that match the criteria. Here's an example set of jukebox tags but feel free to classify tracks however you see fit. rock rockabilly glam punk black metal thrash surf garage pub grunge seattle jazz ragtime bebop bigband swing boogiewoogie dixieland free acid funk jive blues delta stlouis louisiana chicago downhome gutbucket stomp prewar dirty reggae ska dub ragamuffin rap gansta pranks techno rave hiphop triphop trance club house dance eurotechno eurohouse eurodance tribal urban gogo gabber hi-nrg newbeat freestyle anime gfunk industrial breakbeat hardcore drill grindcore ghettotech biphop idm country western bluegrass americana texmex christian rodeo yodeling square honkytonk bakersfield outlaw queercore riotgrrl doowop pop britpop boyband girlband rnb bubblegum jpop synthpop skiffle anti twee alternative indie newage newwave darkwave fusion crossover nova gothic classical symphonies operas operettas arias cantatas fugues canons concertos chamber sonatas musique-concrete divas counter tenors oratorios madrigals motets serenades preludes waltzes ballet suites folk shanties filk ballad torchsong gospel ccm avantgarde primus booty chanson jam terror negerpunk polskpunk space cult psychedelic madchester shoegazing disco soul motown muzak crooners lounge barbershop karaoke childrens nursery rhymes lullabies holiday military march tattoo ballroom world european african asian indian australian pacific american usa english british canadian oz aboriginal celtic irish welsh french spanish portuguese italian german tibetan chinese japanese hawaiian cajun latin carribean andean brazillian cuban arabic egyptian pakistani hindustani continental goa algerian tuvan gypsy armenian azerbaijani greek iranian iraqi israeli kurdistani kyrgyztani lebanese turkish bossanova calypso candombe carnatic chacha conjunto corrido fado flamenco junkanoo mariachi merengue norteno ranchero rhumba salsa samba tango tejano vallenato zydeco taiko afrobeat afropop hi-life mquanga juju soukous township qawwali rai polka rebetiko gamelan zarzeula klezmer chazzanut male female vocal instrumental spoken poetry acappella chant choral acoustic electric electronic power ambient microtonal noise fractal tv radio movie theatre showtunes dancehall musical cabaret vaudeville broadway soundtracks themes string guitar piano brass woodwind drum bass percussion gregorian medieval romantic renaissance baroque contemporary traditional modern early late recent new progressive neo mod post retro revival solo duet trio quartet band choir orchestra north south east west central live studio 20s 30s 40s 50s 60s 70s 80s 90s 00s awesome excellent good fair bad crap weird comedy novelty happy depressing disturbing death relaxing political protest class gender race war peace hard soft easy loud quiet fast slow hot cool smooth explicit driving cooking gardening meditating housework washingup party SIGNALS When jukebox receives a SIGTERM signal, it sends a SIGTERM signal to any player processes and then exits. This stops the jukebox. When jukebox receives a SIGUSR1 signal, it sends a SIGSTOP or SIGCONT signal to any player processes. This pauses and continues the current track. When jukebox receives a SIGUSR2 signal, it sends a SIGTERM signal to any player processes. This skips to the next track. EXAMPLES Play your favourite tracks jukebox List your favourite tracks jukebox -n Create a playlist file of Eric Clapton tracks jukebox -n 'eric clapton' > eric.play Play tracks from a playlist file jukebox -f - < eric.play List all known tags and a histogram of their use jukebox -T -a Play a single album in order, then stop jukebox -s +"janis joplin" +'cheap thrills' Play a single song over and over and over again jukebox -c 'night and day' Play female jazz singers of the 1940's (two versions) jukebox -j +female +vocal +jazz +40s jukebox -jm female vocal jazz 40s Play anything but the Wiggles jukebox -- -wiggles NOTE If there is a jukebox network server running, then it is impolite to use jukebox directly. Use jukeboxc (in command line or graphical mode) instead. NOTE The long options are only available if the value of the juke_opts variable in /etc/jukebox.conf is "long". This is because the standard perl module, Getopt::Long, requires the presence of a "--" argument before any arguments that begin with "+" because it thinks they are options (unless \$POSIXLY_CORRECT is set in the environment). Since it is expected that this program will receive many command line arguments that begin with "+", the default is to only handle single letter options via the Getopt::Std module. Note that, with either short or long options, "--" must be used whenever there are search terms that begin with "-" because these really do look like options. FILES /etc/jukebox.conf - System wide configuration file ~/.jukeboxrc - User specific configuration file SEE ALSO rip(1), riptrack(1), mktoc(1), toc2names(1), toc2tags(1), cdr(1), cdrw(1), burn(1), burnw(1), cdbackup(1), mp3backup(1), jukebox(1), jukeboxc(1), jukeboxc.jar(1), jukeboxd(8), jukeboxd-init.d(8), jukebox.conf(5), http://raf.org/jukebox/Jukebox-HOWTO AUTHOR raf ENDHELP exit; } sub version() { print "jukebox-0.1\n"; exit; } # Check the arguments my %opt; help() unless do_opts(); help() if exists $opt{h}; version() if exists $opt{V}; my $debug = exists $opt{d}; my $all = exists $opt{a}; my $strict = exists $opt{j}; my $mandatory = exists $opt{m}; my $withtags = exists $opt{v}; my $elide = exists $opt{e}; my $extra = exists $opt{x} || $elide || $withtags; my $list = exists $opt{n} || $extra; my $seq = exists $opt{s}; my $cont = exists $opt{c}; my $user = !exists $opt{i}; my $gap = $opt{g} || 0; my $playlist = $opt{f} if exists $opt{f}; my (@root) = split /[,: ]+/, $opt{r} || $default_root; my $tocglob = $opt{t} || $default_tocglob; my %player = decode_player($opt{p} || $default_player); my $unlink = exists $opt{u}; my $tagstats = exists $opt{T}; my $perl = exists $opt{P}; my (@all, @none, @any); my @states = (SIGSTOP(), SIGCONT()); my $state = 0; die "Abort: Selectors and the -a option are mutually exclusive.\n" if $all && $#ARGV != -1; die "Abort: Selectors and the -f option are mutually exclusive.\n" if defined $playlist && $#ARGV != -1; die "Abort: The -a and -j options are mutually exclusive.\n" if $all && $strict; die "Abort: The -a and -m options are mutually exclusive.\n" if $all && $mandatory; die "Abort: The -a and -f options are mutually exclusive.\n" if $all && defined $playlist; die "Abort: The -f and -j options are mutually exclusive.\n" if defined $playlist && $strict; die "Abort: The -f and -m options are mutually exclusive.\n" if defined $playlist && $mandatory; die "Abort: The -f and -nxev options are mutually exclusive.\n" if defined $playlist && $list; die "Abort: The -f and -T options are mutually exclusive.\n" if defined $playlist && $tagstats; die "Abort: The -g option's argument must be numeric.\n" unless $gap =~ /^[\d.]+$/; die "Abort: The -f option's argument must be \"-\" or a filename.\n" if defined $playlist && $playlist ne '-' && ! -f $playlist; die "Abort: The -r option's argument (or juke_root) must contain at least one directory.\n" if $#root == -1; die "Abort: The -p option's argument (or juke_player) must contain at least one player.\n" if $#{[keys %player]} == -1; die "Abort: The -u option requires the -f option.\n" if $unlink && !defined $playlist; die "Abort: The -T and -nxev options are mutually exclusive.\n" if $tagstats && $list; for (@root) { die "Abort: The -r option's argument must contain only directories.\n" unless -d; } for (@ARGV) { push(@all, $1), next if /^\+(.+)$/; push(@none, $1), next if /^-(.+)$/; push(@all, $_), next if $mandatory; push(@any, $_); } $_ = normalise($_, !$perl) for (@all); $_ = normalise($_, !$perl) for (@none); $_ = normalise($_, !$perl) for (@any); my $any = qr/@{[join '|', @any]}/i if $#any != -1; $any = qr/$ENV{LOGNAME}/i, $strict = 1 unless have_selectors() or $all or defined $playlist; if ($debug) { print "Options:\n"; print "debug=$debug all=$all strict=$strict mandatory=$mandatory list=$list extra=$extra elide=$elide withtags=$withtags seq=$seq cont=$cont user=$user gap=$gap unlink=$unlink tagstats=$tagstats perl=$perl\n"; print "playlist=", (defined $playlist) ? $playlist : '', "\n"; print "root=@root tocglob=$tocglob player=@{[encode_player(%player)]}\n"; print "all: ", ($#all != -1) ? @all : '', "\n"; print "none: ", ($#none != -1) ? @none : '', "\n"; print "any: ", (defined $any) ? $any : '', "\n"; print "\n"; } # Parse all toc files looking for suitable tracks # Toc files are of the form: # # Artist: ............ # required - not really # Title: ............. # required - not really # Other: ............. # optional - tags here apply to all tracks # Jukebox: tags # optional - tags here apply to all tracks # # required blank line # 01 Track Name [Jukebox: tags] # 02 ... my @toc; my @tracks; my @artists; my @albums; my @tags; my @titles; my %album_categories; my %track_categories; my %total_categories; my %selected_categories; my $num_albums = 0; my $num_tracks = 0; if (defined $playlist) { open(PLAYLIST, $playlist) || die "Failed to open '$playlist' for reading: $!\n"; while () { chop; next if /'/; next unless /\.(@{[join '|', keys %player]})$/i; next unless -f; push(@tracks, $_); } close(PLAYLIST); unlink($playlist) if $unlink && $playlist ne '-'; } else { push(@toc, split /\n/, `find "$_" -name '$tocglob'`) for (@root); for my $tocfile (sort @toc) { if (open(TOC, $tocfile)) { my $tocpattern = $tocglob; $tocpattern =~ s/\*/.*/; my ($dir) = $tocfile =~ /^(.*\/)$tocpattern$/; ++$num_albums if $tagstats; my $artist; my $album; my $title; # Artist, Title, etc. apply to all tracks. my $cd = ''; my $jheader = undef; while () { last if /^$/; chop; if (/^([A-Za-z]+): (.*)$/) { $cd .= ' ' . $2; $artist = $2 if $list && $extra && $1 eq 'Artist'; $album = $2 if $list && $extra && $1 eq 'Title'; $jheader = (defined $jheader) ? "$jheader $2" : $2 if $1 eq 'Jukebox'; if ($tagstats && $1 eq 'Jukebox') { ++$album_categories{$_} for (split /[\s,]+/, $2); } } } # Gather tracks that match while () { chomp; next unless my ($index, $track) = $_ =~ /^(\d{2}) (.*)$/; my ($jtags) = $track =~ /\[[Jj]ukebox: ([^\]]+)\]/; my $search = ($strict) ? (defined $jheader ? $jheader : '') . (defined $jtags ? ' ' . $jtags : '') : $cd . ' ' . $track; ++$num_tracks if $tagstats; if ($all || (have_selectors() && match_selectors($search))) { print "check: $search\n" if $debug; my @glob = grep { /\.(@{[join '|', keys %player]})$/i } glob("$dir$index*"); next unless defined $glob[0]; next unless $glob[0] =~ /\.(@{[join '|', keys %player]})$/i; next if $glob[0] =~ /'/; push(@tracks, $glob[0]); print "match: $glob[0]\n" if $debug; if ($list && $extra) { $track =~ s/\s*(\[[Jj]ukebox: [^\]]*\])\s*//; push(@artists, $artist); push(@albums, $album); push(@titles, $track); if ($withtags) { my $tags = $jheader if defined $jheader; $tags = (defined $tags) ? "$tags $jtags" : $jtags if defined $jtags; $tags =~ s/^\s+//; $tags =~ s/\s+$//; $tags =~ s/\s+/ /; push(@tags, $tags); } } if ($tagstats && defined $jtags) { ++$selected_categories{$_} for (split /[\s,]+/, $jtags); } if ($tagstats && defined $jheader) { ++$selected_categories{$_} for (split /[\s,]+/, $jheader); } } if ($tagstats && defined $jtags) { ++$track_categories{$_}, ++$total_categories{$_} for (split /[\s,]+/, $jtags); } if ($tagstats && defined $jheader) { ++$total_categories{$_} for (split /[\s,]+/, $jheader); } } close(TOC); } } } # Just list selected tracks? if ($list) { if ($extra) { for (my $i = 0; $i <= $#tracks; ++$i) { my $artist = ($elide && $i && $artists[$i] eq $artists[$i - 1]) ? '' : $artists[$i]; my $album = ($elide && $i && $albums[$i] eq $albums[$i - 1]) ? '' : $albums[$i]; my $tags = ($withtags) ? "\t" . $tags[$i] : ''; print "$artist\t$album\t$titles[$i]\t$tracks[$i]$tags\n"; } } else { print "$_\n" for (@tracks); } print "No matching tracks\n" if $#tracks == -1; exit; } # Just list known categories? if ($tagstats) { print "Categories Albums Tracks Total Selected\n"; print "--------------- ------ ------ ------ --------\n"; print sprintf("%-15s %6d %6d %6d %8d\n", $_, $album_categories{$_} || 0, $track_categories{$_} || 0, $total_categories{$_}, $selected_categories{$_} || 0) for (grep { $_ !~ /\b\d{4}\b/ } sort keys %total_categories); print "--------------- ------ ------ ------ --------\n"; print sprintf("%-15s %6d %6d %6d %8d\n", 'Total', $num_albums, $num_tracks, $num_tracks, scalar @tracks); exit; } # Play selected music in random or sequential order die "No matching tracks\n" if $#tracks == -1; use POSIX; my $last = $#tracks; $seq = 1 if $#tracks == 0; my $pid; $| = 1; $SIG{TERM} = \&term; $SIG{USR1} = \&usr1; $SIG{USR2} = \&usr2; $SIG{INT} = 'IGNORE'; for (my $i = 0;; ++$i) { $i = ($seq) ? (($i > $last) ? 0 : $i) : floor(rand($last + 1)); print '[', $i + 1, '/', $last + 1, "] $tracks[$i]\n"; # we need to sleep here otherwise the following errors can occur: # ogg123: Could not load default driver and no driver specified in config file. Exiting. # mpg321: Can't find a suitable libao driver. (Is device in use?) # maybe the sound device takes a while to get released. select(undef, undef, undef, $default_gap) if $default_gap && !$gap && $seq && !$i; select(undef, undef, undef, $gap) if $gap; my ($fmt) = $tracks[$i] =~ /\.(@{[join '|', keys %player]})$/i; die "Unknown format: '$tracks[$i]'\n" unless defined $fmt && defined $player{lc($fmt)}; $tracks[$i] =~ s/%/%%/g; my $player = $player{lc($fmt)}; $player .= ' %' unless $player =~ /(? =~ /[nN]/; print "\n"; } } exit; # Execute the player command sub run_cmd { my ($cmd) = @_; my $pid = fork; die "Failed to fork: $!\n" if $pid == -1; return $pid if $pid; print "exec $cmd\n" if $debug; setpgrp; exec $cmd; die "Failed to exec: $!\n"; } # Handle SIGTERM (kill player process(es) if any, then exit) sub term { kill(-(SIGTERM), $pid) if defined $pid && $pid > 0; exit; } # Handle SIGUSR1 (pause/continue player process(es) if any) sub usr1 { kill(-$states[$state], $pid), $state = !$state if defined $pid && $pid > 0; } # Handle SIGUSR2 (kill player process(es) but continue) sub usr2 { kill(-(SIGTERM), $pid) if defined $pid && $pid > 0; } # Are there any selectors? sub have_selectors { return $#all != -1 || $#none != -1 || defined $any; } # Match a string against selectors sub match_selectors { my ($str) = @_; $str = normalise($str, 0); for my $all (@all) { return 0 unless ($strict) ? $str =~ /\b$all\b/i : $str =~ /$all/i; } for my $none (@none) { return 0 if ($strict) ? $str =~ /\b$none\b/i : $str =~ /$none/i; } return 1 unless defined $any; return ($strict) ? $str =~ /\b$any\b/ : $str =~ /$any/; } # Remove accents and disable simple regular expressions sub normalise { my ($str, $re) = @_; $str =~ tr/¿ÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåçèéêëìíîïðñòóôõöøùúûüýþÿ/?AAAAAACEEEEIIIIDNOOOOOxOUUUUYPBaaaaaaceeeeiiiidnoooooouuuuypy/; $str =~ s/Æ/AE/g; $str =~ s/æ/ae/g; $str = quotemeta($str) if $re; return $str; } # Decode the juke_player variable sub decode_player { my ($spec) = @_; my %player; # If only a single player is given, assume mp3 format if ($spec =~ /^([^:=,]+)$/) { $player{mp3} = $1; return %player; } for my $player (split /,/, $spec) { $player{lc($1)} = $2 if $player =~ /^([^=:]+)[=:](.+)$/; } return %player; } # Encode the juke_player variable sub encode_player { my (%player) = @_; return join(',', map { "$_=$player{$_}" } sort keys %player); } # Locate a command in $PATH sub locate { my ($cmd) = @_; for (split /:/, $ENV{PATH}) { return "$_/$cmd" if -x "$_/$cmd"; } return undef; } # Load the configuration file sub load { my ($config) = @_; return unless -r $config; return unless open(CONFIG, $config); while () { s/#.*$//; s/^\s+//; s/\s+$//; s/\s+/ /g; next if /^$/; $default_root = $1 if /^juke_root=['"]?([^'"]+)['"]?$/; $default_tocglob = $1 if /^juke_toc=['"]?([^'"]+)['"]?$/; $default_gap = $1 if /^juke_gap=['"]?([^'"]+)['"]?$/; $default_player = $1 if /^juke_player=['"]?([^'"]+)['"]?$/; $default_opts = $1 if /^juke_opts=['"]?([^'"]+)['"]?$/; } close(CONFIG); } # Process the command line options sub do_opts { if ($default_opts eq 'long') { use Getopt::Long; #qw(:config gnu_getopt); Getopt::Long::Configure qw(no_getopt_compat permute bundling); GetOptions \%opt, qw(h|help V|version d|debug a|all j|strict m|mandatory n|list x|extra e|elide v|withtags s|sequential c|continuous i|nouser g|gap=f f|playlist=s r|root=s t|tocglob=s p|player=s u|unlink T|tagstats P|perl); } else { use Getopt::Std; getopts 'hVdajmnxevscig:f:r:t:p:uTP', \%opt; } } # vi:set ts=4 sw=4