#!/usr/bin/perl ############################################################################### # # fysh # o # Copyright (C) 2007 Vincent Stehlé ° # (",>{ # This is free software under GPL. See documentation and license in the # embedded doc. at the end of this file. # ############################################################################### ############################################################################### # # Debug - Package for debug messages. # ############################################################################### package Debug; use warnings; use strict; BEGIN { use Exporter (); our @ISA = qw(Exporter); our @EXPORT = qw(debug DEBUG); } ############################################################################### # # DEBUG # ############################################################################### our $DEBUG = 0; ############################################################################### # # debug # ############################################################################### sub debug($) { my ($m) = @_; return if !$DEBUG; my ($package, $filename, $line) = caller; print "[$$]$package/$line: $m"; } ############################################################################### # # Cache - Package for caching. # ############################################################################### package Cache; use warnings; use strict; use File::Copy; use Carp; use Fatal qw(:void opendir readdir closedir copy); BEGIN { Debug::->import } ############################################################################### # # read_dir_to_hash # ############################################################################### sub read_dir_to_hash($) { my ($dirname) = @_; my %r; # local *D; opendir(D, $dirname); while(my $e = readdir(D)){ next if $e eq '.'; next if $e eq '..'; $r{$e} = 1; debug("In cache: $e\n"); } closedir(D); return %r; } ############################################################################### # # new # ############################################################################### sub new($$) { my ($class, $cache_dir) = @_; defined $cache_dir or confess; my $r = { # Hash, to list cached entries. # Keys are the entries names. m_entries => { read_dir_to_hash($cache_dir) }, # Path to the cache directory. m_cache_dir => $cache_dir, }; bless $r; return $r; } ############################################################################### # # add # # Description: # Add a file into the cache as 'entry'. # ############################################################################### sub add($$$) { my ($self, $file, $entry) = @_; debug("Adding $file to cache as $entry\n"); copy($file, "$self->{m_cache_dir}/$entry"); $self->{m_entries}{$entry} = 1; } ############################################################################### # # query # # Description: # Ask if a file is cached. # ############################################################################### sub query($$) { my ($self, $entry) = @_; debug("Query $entry..."); my $r; $r = "$self->{m_cache_dir}/$entry" if exists $self->{m_entries}{$entry}; debug((defined $r ? $r : '') ."\n"); return $r; } ############################################################################### # # Jobs - Package for parallel jobs handling. # ############################################################################### package Jobs; use warnings; use strict; BEGIN { Debug::->import } ############################################################################### # # new # ############################################################################### sub new($$) { my ($class, $max_jobs) = @_; my $r = { # Temporary hash, to track the running jobs. # Keys are the jobs pids, values are the data passed by the caller. m_jobs => {}, # Maximum number of simultaneous jobs running. m_max_jobs => $max_jobs, # Jobs data and return codes. m_jobs_rc => [], }; bless $r; return $r; } ############################################################################### # # wait_jobs # # Description: # Wait for children until there are n running jobs # or less. # ############################################################################### sub wait_jobs($$) { my($self, $n) = @_; while(1){ my $running = scalar(keys %{$self->{m_jobs}}); debug "$running job(s) currently running.\n"; last if $running <= $n; # Wait for a job to finish. debug "Waiting...\n"; my $c = wait(); my $rc = $? >> 8; debug "-> $c ($rc)\n"; # Sanity checks. if($c < 0){ warn "wait() returned $c. Strange!\n"; return; } if(!exists $self->{m_jobs}{$c}){ warn "wait() returned $c, but job is not in" ." jobs hash. Strange!\n"; return; } # Store data and return code for later use by the caller. my $data = $self->{m_jobs}{$c}; push(@{$self->{m_jobs_rc}}, {data => $data, 'return-code' => $rc}); # Forget job. delete $self->{m_jobs}{$c}; } } ############################################################################### # # shift_rc # ############################################################################### sub shift_rc($){ shift(@{$_[0]->{m_jobs_rc}}) } ############################################################################### # # wait_for_all_jobs # # Description: # Wait for all children until there are no more running. # ############################################################################### sub wait_for_all_jobs($) { my ($self) = @_; $self->wait_jobs(0); } ############################################################################### # # wait_for_free_slot # ############################################################################### sub wait_for_free_slot($) { my ($self) = @_; $self->wait_jobs($self->{m_max_jobs} - 1); } ############################################################################### # # launch # # Description: # Run function in parallel. # # Arguments: # Function to launch. May be a perl block ({ ... }). # Id, that you get back at wait time. # # Notes: # We ensure not to exceed the maximum number of slots # allowed. # ############################################################################### sub launch($&$) { my ($self, $function, $data) = @_; # Wait for a free slot first. $self->wait_for_free_slot; # Ok, fork. debug "Forking.\n"; my $r = fork(); if(!$r){ # We are in the child. debug "Child $$ starting.\n"; # Install signal handlers. sub jobs_sig_handler() { exit 1; } $SIG{INT} = \&jobs_sig_handler; $SIG{TERM} = \&jobs_sig_handler; # Do the job. my $rc = &$function; # And exit. debug "Child $$ exiting ($rc).\n"; exit($rc); } else { # We are in the father. # Remember the child as well as its data. debug "Launched $r.\n"; $self->{m_jobs}{$r} = $data; } } ############################################################################### # # kill_all # # Description: # Kill all running jobs. # # Notes: # We use a sig term. # ############################################################################### sub kill_all($) { my ($self) = @_; kill 'TERM', keys %{$self->{m_jobs}}; } ############################################################################### # # Estimator - Package to estimate the compressed size of a file. # ############################################################################### package Estimator; use warnings; use strict; use File::stat; BEGIN { Debug::->import } ############################################################################### # # new # ############################################################################### sub new($$) { my ($class, $cache_dir) = @_; my $r = { # Accumulator. This is an accumulation of the in/compressed ratio. m_acc => 2, }; bless $r; return $r; } ############################################################################### # # get_file_size # ############################################################################### sub get_file_size($) { my ($filename) = @_; my $st = stat($filename) or die; my $s = $st->size; debug "$filename: $s\n"; return $s; } ############################################################################### # # update # ############################################################################### sub update($$) { my ($self, $in_file, $compressed_file) = @_; # Stat sizes. my $in_size = get_file_size($in_file); my $compressed_size = get_file_size($compressed_file); # Compute ratio. my $ratio = $in_size / $compressed_size; # Accumulate it. my $a = 0.9; $self->{m_acc} = $a * $self->{m_acc} + (1 - $a) * $ratio; # TODO: Better predictor. } ############################################################################### # # predicted_compressed_value # ############################################################################### sub predicted_compressed_value($$) { my ($self, $in_file) = @_; my $in_size = get_file_size($in_file); # Predict based on accumulated ratio. return $in_size / $self->{m_acc}; } ############################################################################### # # main - Main package. # ############################################################################### package main; use warnings; use strict; use File::Copy; use Fatal qw(:void open close read seek mkdir copy unlink); use Getopt::Long; use Pod::Usage; use Fcntl qw(:seek); use Carp; BEGIN { Debug::->import } ############################################################################### # # VERSION # ############################################################################### my $VERSION = '0.6'; ############################################################################### # # CONFIG # ############################################################################### my $CONFIG = '.fyshrc'; ############################################################################### # # EXECUTABLES # ############################################################################### my @EXECUTABLES = qw(find python mount umount df grep sed cut wget); ############################################################################### # # ITUNESSD # # Description: # iTunesSD path, relative to iPod root. # ############################################################################### my $ITUNESSD = "iPod_Control/iTunes/iTunesSD"; ############################################################################### # # ITUNESSTATS # # Description: # iTunesStats path, relative to iPod root. # ############################################################################### my $ITUNESSTATS = "iPod_Control/iTunes/iTunesStats"; ############################################################################### # # REBUILD_DB # # Description: # Name of the rebuild_db script, as we expect it in the tgz and on the iPod. # ############################################################################### my $REBUILD_DB = "rebuild_db.py"; ############################################################################### # # REBUILD_DB_URL # ############################################################################### my $REBUILD_DB_URL = "http://mesh.dl.sourceforge.net/sourceforge/shuffle-db" ."/rebuild_db-1.0-rc1.tar.gz"; ############################################################################### # # REBUILD_DB_TGZ_DIR # # Description: # Name of the directory in the tgz. # ############################################################################### my $REBUILD_DB_TGZ_DIR = "rebuild_db"; ############################################################################### # # my_system # ############################################################################### sub my_system($) { my($c) = @_; debug "$c\n"; system($c); } ############################################################################### # # safe_system # ############################################################################### sub safe_system($) { my($c) = @_; my $r = my_system($c); confess $c if ($r >> 8); } ############################################################################### # # flac_to_m4a # # Returned value: # True in case of error. # # Notes: # Shuffle cuts @20KHz. # Note that we need to use a unix process like api (0: ok, else: error) for # the father to be informed in case we die (and return non-zero). # ############################################################################### sub flac_to_m4a($$\%) { my($flac, $m4a, $opt) = @_; # Decode. my $tmp1 = "/tmp/tmp1-$$.wav"; my $flac_opts = "-d"; $flac_opts .= " --silent" if !$DEBUG; my $r = my_system("$opt->{flac} $flac_opts -o $tmp1 \"$flac\""); goto error if ($r >> 8); # Normalize. my $tmp2; if(! exists $opt->{'dont-normalize'}){ $tmp2 = "/tmp/tmp2-$$.wav"; debug "Computing optimal volume... "; my $v = `$opt->{sox} $tmp1 -t raw /dev/null stat -v 2>&1`; chomp $v; debug "$v\n"; if($v < $opt->{'normalize-threshold'}){ # Normalization is not worth it. debug "Below threshold: not normalizing.\n"; $tmp2 = $tmp1; } else { debug "Normalizing... "; my $sox_redirect = $DEBUG ? "" : ">/dev/null 2>&1"; $r = my_system("$opt->{sox} -v $v $tmp1 $tmp2 $sox_redirect"); goto error if ($r >> 8); debug "done\n"; unlink $tmp1; } } else { # User forbid normalization. $tmp2 = $tmp1; } # Encode. my $faac_opts = $opt->{'faac-opts'}; $faac_opts =~ s/^['"](.*)['"]$/$1/; my $faac_redirect = $DEBUG ? "" : ">/dev/null 2>&1"; $r = my_system("$opt->{faac} -o \"$m4a\" $faac_opts $tmp2 $faac_redirect"); goto error if ($r >> 8); unlink $tmp2; # Ok, exit. return 0; # In case of error, we arrive here. error: unlink $tmp1 if -e $tmp1; unlink $tmp2 if -e $tmp2; unlink $m4a if -e $m4a; return 1; } ############################################################################### # # read_bytes # # Arguments: # size: in bytes. # ############################################################################### sub read_bytes($$) { my ($F, $size) = @_; my $x; read($F, $x, $size); return $x; } ############################################################################### # # read_n_bytes # ############################################################################### sub read_n_bytes($$;$) { my ($F, $n, $little_endian) = @_; # Defaults. $little_endian = 0 if !defined $little_endian; my $x = read_bytes($F, $n); my @t = unpack("C$n", $x); # Endianess. @t = reverse @t if !$little_endian; my $r = 0; my $cnt = 0; foreach my $v (@t){ $r += ($v << (8 * $cnt++)) } return $r; } ############################################################################### # # read_string # ############################################################################### sub read_string($$) { my ($F, $size) = @_; my $x = read_bytes($F, $size); # Cope with unicode's zeroes. my @t = unpack("S*", $x); $x = pack("C*", @t); return unpack("Z*", $x); } ############################################################################### # # read_itunessd # # Notes: # We return an empty array in case of error. # ############################################################################### sub read_itunessd(\%) { my ($opt) = @_; local *F; my $filename = "$opt->{'ipod-root'}/$ITUNESSD"; open(F, $filename) or return (); binmode F; # Header. my $num_songs = read_n_bytes(*F, 3); debug "num_songs: $num_songs\n"; seek(F, 3, SEEK_CUR); # Skip 3. my $header_size = read_n_bytes(*F, 3); debug "header_size: $header_size\n"; # Each entry. my @r; for my $song (0 .. ($num_songs - 1)){ debug "song $song\n"; seek(F, ($header_size + ($song * 558) + 33), SEEK_SET); my $filename = read_string(*F, 522); debug "filename $filename\n"; push(@r, $filename); } close(F); return @r; } ############################################################################### # # read_itunesstats # # Notes: # This one is little endian. # We return an empty array in case of error. # ############################################################################### sub read_itunesstats(\%) { my ($opt) = @_; local *F; my $filename = "$opt->{'ipod-root'}/$ITUNESSTATS"; open(F, $filename) or return (); binmode F; # Header. my $num_songs = read_n_bytes(*F, 3, 1); debug "num_songs: $num_songs\n"; # Each entry. my @r; for my $song (0 .. ($num_songs - 1)){ debug "song $song\n"; seek(F, (6 + ($song * 18) + 12), SEEK_SET); my $playcount = read_n_bytes(*F, 3, 1); debug "playcount $playcount\n"; my $skippedcount = read_n_bytes(*F, 3, 1); debug "skippedcount $skippedcount\n"; push(@r, {playcount => $playcount, skippedcount => $skippedcount}); } close(F); return @r; } ############################################################################### # # remove_previous_music # # Returned value: # Hash with keys being the basenames of the removed tracks. # ############################################################################### sub remove_previous_music(\%) { my ($opt) = @_; my @sd = read_itunessd(%{$opt}); my @stats = read_itunesstats(%{$opt}); my %r; # Remove. for my $i (0 .. $#stats){ debug "$sd[$i] played=$stats[$i]->{playcount}" ." skipped=$stats[$i]->{skippedcount}\n"; if($stats[$i]->{playcount} || $stats[$i]->{skippedcount}){ my $f = "$opt->{'ipod-root'}$sd[$i]"; next if ! -e $f; debug "Removing $f\n"; unlink $f; # Remember for later re-fill, to avoid re-selecting the ones # we just removed. my $bn = $sd[$i]; $bn =~ /([^\/]+)$/; $bn = $1; $r{$bn} = 1; } } return %r; } ############################################################################### # # install_rebuild_db_if_needed # ############################################################################### sub install_rebuild_db_if_needed(\%) { my ($opt) = @_; # Check rebuild db presence. my $rdb = "$opt->{'ipod-root'}/$REBUILD_DB"; return if -e $rdb; # If we arrive here, we need to install it. print "Did not find $REBUILD_DB on iPod: installing it.\n"; # Download. my $tmp = "/tmp/tmp-$$-rebuild.tar.gz"; my $tmp_dir = "/tmp/tmp-$$-dir"; safe_system("wget -O $tmp $REBUILD_DB_URL && mkdir $tmp_dir" ."&& cd $tmp_dir && tar zxvf $tmp"); unlink $tmp; # Install. my $arc_rdb = "$tmp_dir/$REBUILD_DB_TGZ_DIR/$REBUILD_DB"; debug "Installing to $rdb\n"; copy($arc_rdb, $rdb); chmod 0755, $rdb; # Cleanup. safe_system("rm -fR $tmp_dir"); } ############################################################################### # # check_executables # ############################################################################### sub check_executables(\%) { my($opt) = @_; my $stop = 0; foreach my $e ($opt->{faac}, $opt->{flac}, $opt->{sox}, @EXECUTABLES){ my $w = `which $e`; chomp $w; if($w eq ''){ warn "$e not in path.\n"; $stop = 1; } } if($stop){ warn "Stop."; exit 1; } } ############################################################################### # # get_env_vars # ############################################################################### sub get_env_vars(\%) { my($opt) = @_; my @t = ( {var => 'FYSH_FAAC', opt => 'faac'}, {var => 'FYSH_FLAC', opt => 'flac'}, {var => 'FYSH_SOX', opt => 'sox'}, ); foreach my $r (@t){ my $v = $r->{var}; my $o = $r->{opt}; $opt->{$o} = $ENV{$v} if (exists $ENV{$v} && defined $ENV{$v}); } } ############################################################################### # # read_config_file # ############################################################################### sub read_config_file() { my $c = "$ENV{HOME}/$CONFIG"; return if ! -e $c; local *F; open(F, $c); my @r; while(my $l = ){ chomp $l; $l =~ s/#.*$//; next if $l =~ /^\s*$/; push(@r, $l); } close(F); return @r; } ############################################################################### # # detect_num_cpu # ############################################################################### sub detect_num_cpu() { # Detect the number of cpu. # TODO: This is Linux only for now, and this # could be more elegant. my $r = `grep processor /proc/cpuinfo |wc -l`; chomp $r; debug "Detected $r cpu.\n"; return $r; } ############################################################################### # # query_free_space # ############################################################################### sub query_free_space($) { my($path) = @_; my $r; return $r; } ############################################################################### # # progress # # TODO: A complete package when more than just a message (ETA, space left...) # ############################################################################### sub progress($) { my ($m) = @_; # Limit size. my $max_size = 79; # TODO: ask terminal! my $len = length($m); if($len > $max_size){ # Shorten to fit in line. my $ls = $max_size / 2 - 1; my $rs = $max_size - $ls - 2; my $n = substr($m, 0, $ls) .".." .substr($m, ($len - $rs), $rs); $m = $n; } else { # Complete untill end of line. $m .= (" " x ($max_size - $len)); } # Display message. if($DEBUG) { print "$m\n" } else { print "\r$m" } } ############################################################################### # # Main. # ############################################################################### # Autoflush. $| = 1; # Command line. # Shamelessly adapted from pod2usage man page. # We have default values. my %opt = ( 'faac' => 'faac', 'faac-opts' => '', 'flac' => 'flac', 'cache-dir' => "$ENV{HOME}/.fysh-cache", 'ipod-music-dir' => 'music', 'ipod-root' => '/media/ipod', 'normalize-threshold' => 1.05, 'rebuild-db-opts' => '', 'sox' => 'sox', ); # Take environment variables into account. get_env_vars(%opt); # Take config file into account. my @config = read_config_file(); @ARGV = (@config, @ARGV); ## Parse options and print usage if there is a syntax error, ## or if usage was explicitly requested. GetOptions(\%opt, 'cache-dir=s', 'debug', 'faac=s', 'faac-opts=s', 'flac=s', 'help', 'ipod-music-dir=s', 'ipod-root=s', 'man', 'max-jobs=i', 'mount', 'music-dir=s', 'normalize-threshold=f', 'no-normalize', 'no-random', 'no-rebuild-db', 'no-remove', 'rebuild-db-opts=s', 'sox=s', 'version') or pod2usage(2); pod2usage(1) if exists $opt{help}; pod2usage(-verbose => 2) if exists $opt{man}; # Complain on unknown arguments. pod2usage(-exitval => 2, -message => "Unknown arguments: ". join(" ", @ARGV)) if scalar(@ARGV); # Debug printing. $DEBUG = 1 if exists $opt{debug}; # Detect num cpu if no jobs limit given. $opt{'max-jobs'} = detect_num_cpu() if !exists $opt{'max-jobs'}; # Handle version. if(exists $opt{'version'}){ print < ° This is free software under GPL (see --man). (",>{ END exit; } # Initialize random seed. srand(time); # Sanity. check_executables(%opt); # Handle mount. if(exists $opt{'mount'}){ progress "Mounting $opt{'ipod-root'}..."; my_system("mount $opt{'ipod-root'}"); progress "Done with mount."; } # Make room. my %removed; progress "Removing played and skipped tracks..."; %removed = remove_previous_music(%opt) if !exists $opt{'no-remove'}; my $num_removed = scalar keys %removed; progress "Removed $num_removed track". ($num_removed > 1 ? "s" : "") ."."; # List flac. my @l; progress "Listing source tracks..."; if(exists $opt{'music-dir'}){ @l = `find $opt{'music-dir'} -type f -name \*.flac`; chomp @l; # We sort the files, as we will use random selection thereafter. @l = sort(@l); } my $num_listed = scalar @l; progress "Listed $num_listed track". ($num_listed > 1 ? "s" : "") ."."; # Ensure directory exists on the iPod. my $music_dir = "$opt{'ipod-root'}/$opt{'ipod-music-dir'}"; if(! -e $music_dir){ progress "Creating $music_dir..."; mkdir($music_dir); progress "Done creating dir."; } # Prepare to launch jobs. my $jobs = Jobs::->new($opt{'max-jobs'}); # Register a signal handler for clean exit. sub sig_handler() { $jobs->kill_all; exit 1; } $SIG{INT} = \&sig_handler; $SIG{TERM} = \&sig_handler; # Prepare to caching. my $cache = Cache::->new($opt{'cache-dir'}); # Prepare predictor. my $est = Estimator::->new; # Perform selection. my $cnt = 0; # Helper, to handle finished jobs, if there are any. # Returned value: false in case of error. sub handle_finished_jobs() { my $s = 1; # Check finished jobs if there are any. while(1){ # Get finished job. my $r = $jobs->shift_rc; # No more finished jobs? last if !defined $r; # Ok, we have a job, which just finished ok. Retrieve the # informations we passed at launch time and the return code. my $rc = $r->{'return-code'}; my $i = $r->{data}; # Debug. debug(" bn: $i->{bn}\n"); debug(" src: $i->{src}\n"); debug(" tmp: $i->{tmp}\n"); debug("cache-entry: $i->{'cache-entry'}\n"); debug(" dst: $i->{dst}\n"); debug("return-code: $rc\n"); # Error? if($rc || !-f $i->{tmp}){ # Progress. progress "Error coding $i->{bn}!"; $s = 0; next; } # Progress. progress "Done coding $i->{bn}."; # Add to cache. $cache->add($i->{tmp}, $i->{'cache-entry'}) ; # Update size estimate. $est->update($i->{src}, $i->{tmp}); # Move encoded file to ipod. # In case the move fails this is very likely # due to no space left on ipod. # TODO: more silent when not in debug. if(!move($i->{tmp}, $i->{dst})){ debug "Cannot move $i->{tmp} to $i->{dst}: $!\n"; progress "iPod is full."; $s = 0; next; } } return $s; } TRACK: while(1){ # No more music? if(!@l){ progress "Ran out of music!"; last; } # Handle finished jobs if there are any. my $r = handle_finished_jobs(); # In case of error, finish. if(!$r){ debug "Killing remaining jobs...\n"; $jobs->kill_all; last TRACK; } # TODO: # o Do not rely on error to detect disk full. # o Better error handling. # Select one flac randomly. my $i = exists $opt{'no-random'} ? $cnt : int(rand(@l)); # Remove it from list. my $flac = splice(@l, $i, 1); debug "Selecting $flac\n"; # Compute bn, tmp and dst name. my $bn = $flac; $bn =~ s/\.flac$/\.m4a/; $bn =~ s%$opt{'music-dir'}/%%; $bn =~ s%[^a-zA-Z0-9\-\.]+%_%g; my $tmp = "/tmp/tmp-$$-$cnt-$bn"; my $dst = "$music_dir/$bn"; # Skip if just removed. if(exists $removed{$bn}){ debug "Just removed: skipping.\n"; next; } # Skip if already exists. if(-f $dst){ debug "Already exists: skipping.\n"; next; } # Try cache lookup first. my $cached = $cache->query($bn); if(defined $cached){ # We have a cached copy of the compressed file. # No need to re-compress. progress "Cache hit for $bn."; if(!copy($cached, $dst)){ # When the copy fails, for us it means the iPod is full. progress "iPod is full."; last TRACK; } # Also, use it for the size estimations. $est->update($flac, $cached); } else { # Predict compressed size and see if it is worth encoding. # TODO! debug "predicted: ". $est->predicted_compressed_value($flac) ."\n"; # Launch encoding. # Notes: # o This is blocking when the number of running jobs exceeds # a given limit. # o We supply some info as the 'data'. progress "Encoding $bn..."; $jobs->launch(sub { flac_to_m4a($flac, $tmp, %opt) }, {src => $flac, tmp => $tmp, dst => $dst, bn => $bn, 'cache-entry' => $bn}); } # TODO: allow "hit-under-miss" when blocked, but how? $cnt++; } # When we arrive here in case of "normal" exit (as opposed to error cases) # we may have the last jobs dispatched but still running. Wait for them. $jobs->wait_for_all_jobs; # Again, handle finished jobs if there are any. handle_finished_jobs(); # TODO: error handling? progress "Put $cnt track". ($cnt > 1 ? "s" : "") ." on iPod."; # Rebuild db if not forbidden to. if(!exists $opt{'no-rebuild-db'}){ install_rebuild_db_if_needed(%opt); my $o = $opt{'rebuild-db-opts'}; $o =~ s/^['"](.*)['"]$/$1/; $o .= " >/dev/null 2>&1" if !$DEBUG; progress "Rebuilding db with $REBUILD_DB..."; my_system("cd $opt{'ipod-root'} && python $REBUILD_DB $o"); progress "Done rebuilding db."; } # Handle umount. if(exists $opt{'mount'}){ progress "Unmounting $opt{'ipod-root'}..."; my_system("umount $opt{'ipod-root'}"); progress "Done unmounting."; } # End. # TODO: more coherent return code? progress "All done."; print "\n"; ############################################################################### # # Embedded documentation. # ############################################################################### __END__ =pod =head1 NAME fysh - Fill your shuffle =head1 SYNOPSIS fysh [options] Options are: =over =item B<-c, --cache-dir> Specify cache dir. (Default: $HOME/.fysh-cache) =item B<-d, --debug> Turn on debug printing. (Default: Off) =item B<--faac=> Specify faac binary. (Default: 'faac') =item B<--faac-opts=> Pass additional options to faac. You may enclose the string between ' or ". =item B<--flac=> Specify flac binary. (Default: 'flac') =item B<-h, --help> Print a brief help message and exits. =item B<--ipod-music-dir=> Specify where to put music on the iPod shuffle. This path is relative to the iPod's root. (Default: 'music') =item B<--ipod-root=> Specify where the iPod shuffle is mounted. (Default: '/media/ipod') =item B<--man> Prints the manual page and exits. =item B<--max-jobs=> Limit the maximum number of encoding jobs to launch. (Default: Number of cpu) =item B<--mount> Let fysh handle mount/umount. (Default: Do not handle mounts) =item B<--music-dir=> Specify music dir to search for flac files. =item B<--normalize-threshold=> Do not normalize sound amplitude prior to encoding if the scale factor is below the given threshold (in other words: if normalizing is not worth the effort). (Default: 1.05) =item B<--no-normalize> Do not normalize sound amplitude prior to encoding. Just leave it as it is. (Default: Normalize) =item B<--no-random> Do not pick tracks at random. This is for debug, mostly. (Default: Pick at random) =item B<--no-rebuild-db> Do not rebuild shuffle's database with rebuild_db.py. (Default: Rebuild) =item B<--no-remove> Do not remove played or skipped tracks. (Default: Remove) =item B<-r, --rebuild-db-opts=> Pass additional options to rebuild_db.py. You may enclose the string between ' or ". =item B<-s, --sox=> Specify sox binary. (Default: 'sox') =item B<-v, --version> Prints version information and exits. =back =head1 DESCRIPTION fysh is meant to fill an iPod shuffle with music. It chooses, encodes, transfers and registers the music into the iPod automatically. fysh is pronounced like "fish". o ° (",>{ The first step done by fysh after its invocation is to make some room on the shuffle. To do so, the play and skip statistics are examined. All files that have been played at least once or skipped at least once are removed. The music directory tree you pass to fysh is searched for .flac files. One file is picked at random and encoded to .m4a (AAC LP). It is then copied to your iPod shuffle under the /music/ directory. The process is repeated until there are no more song available or the iPod is full. =head2 iPod shuffle directories Here is a sample iPod shuffle directory tree: / +- iPod_Control/ | `- iTunes/ +- music/ +- rebuild_db.py `- rebuild_db.log.txt Under iPod_Control/iTunes are all iTunes files, including playlist and statistics. Under music are the encoded songs. rebuild_db.py is the python script used to generate the playlist and rebuild_db.log.txt is its execution log. =head2 Configuration fysh configuration is determined in the following order. First, fysh has default values for several of its settings. Then environment variables are taken into account (see the section "ENVIRONMENT VARIABLES" below). Then fysh tries to read a .fyshrc in the user's home directory (see the section "CONFIGURATION FILE" below). And finally fysh will obey its command line arguments. =head1 EXAMPLES Invoke fysh, passing additional options to faac: $ fysh --faac-opts='-q 90 -c 19000' Invoke fysh, with a low nice value to limit cpu usage: $ nice -n 19 fysh Invoke fysh, with three encoding jobs in parallel at most: $ fysh --max-jobs=3 =head1 CONFIGURATION FILE fysh configuration file is $HOME/.fyshrc. The .fyshrc config file can contain all options that can be passed on the command line. Blank lines are ignored. Comments are introduced by a '#' character and last until the end of the line (shell style comments). A sample .fyshrc may read like: # .fyshrc - fysh configuration file --ipod-root=/my/ipod/mount/point --music-dir=/my/music/collection # We want fysh to automount the iPod: --mount # Adjust faac settings (iPod's passed band is 20kHz): --faac-opts='-q 90 -c 19000' This configuration file has precedence over the environment variables, but is overriden by command lines arguments. =head1 ENVIRONMENT VARIABLES fysh takes the following environment variables into account. Note that the .fyshrc config file as well as the command line options have precedence over those variables: =over =item FYSH_FAAC Specify faac binary. See option '--faac'. =item FYSH_FLAC Specify flac binary. See option '--flac'. =item FYSH_SOX Specify sox binary. See option '--sox'. =back =head1 REFERENCES There is a webpage for fysh: http://vincent.stehle.free.fr/fysh/. File formats were taken from: http://ipodlinux.org/ITunesDB. This script relies on the python database builder of http://shuffle-db.sourceforge.net/ =head1 AUTHOR Vincent Stehlé . =head1 COPYRIGHT Copyright (C) 2007 Vincent Stehlé. 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =head1 TODO Some enhancements remain to be done to this program: =over =item Blacklist undesired input files. =item Find latest rebuild_db.py version when downloading. =item Use GNU queue. Even better: allow arbitrary wrapper for commands. =item Able to fill cache speculatively. Able to limit cache size. Allow "hit-under-miss". Cache statistics. =item Better error handling. =item Able to limit fill size. =item Depend on fewer executables. =item Be able to specify removal criteria (played and/or skipped threshold). =item Be able to accumulate iTunes statistics on the host. Use them to decide which file to pick next time. =item Handle optional executables and mandatory executables in different ways during start check. =item Find a way to estimate when it is worth encoding according to the remaining size on the iPod. Today we encode, then we check if it fits. This is one encode too many. One way could be: estimate intermediate uncompressed wav size, then estimate final compressed size. then decide if it is worth trying to encode. This would necessitate a dont-estimate option too. =item Destination names computation may not suit everyone. Change this to something more flexible. =item Handle first time shuffle too. Install this dump: http://shuffle-db.sourceforge.net/ipod_root.zip =item Read bookmark from iTunes stats and use it to decide if we need to remove a file. Use a threshold? =item Handle more in and out formats. Be able to deal with other inputs than .flac: .mp3, .m4b, .aa... Some should be just copied, not encoded. =item Leave less files in the tmp when exiting abnormally. Group tmp files under the same temporary dir? =item Normalize sound power rather than range as it is done yet? =item Be silent when more than one job? =item Progress indicator. =item More portable cpu number detection. =item Cleanup ~/ hierarchy. Go from: ~/ +- .fysh-cache/ ` - .fyshrc ...to: ~/ ` - .fysh/ +- cache/ `- fyshrc (or config, or something) =item BUG! Handle gracefully "no space left" on ipod (as before). Check remaining size in that case, and make sure rebuild_db.py can complete. Fixed room to leave? Delete last entry? =item Subdivide cache directories. Use md5 hash? =back =cut