Tuesday 15 October 2013

Copying music to In Car Entertainment (ICE) system with limited Memory Card capacity

My car has a decent In Car Entertainment (ICE) system but unfortunately the only way of getting music on to the Hard Disc is via a Memory Card with maximum 2Gb capacity.

I therefore have to transfer the music files piece meal. I have therefore written a bit of perl that supports the copying of files but maintains a Tag file of what files have been transferred - ie its a simple mirror program that:

  • Keeps a record of files that already been copied;
  • Restricts the files transferred to those with a given suffix (currently .mp3);
  • Fails carefully when out of space on target directory;
  • Will recopy files that have changed since original copy.
It is designed for my NAS which is an ARM based Linux box with perl installed. Anybody who wishes to use it would need to ensure they have perl installed and change the "#!" header to their perl location.

Usage is (mount/umount will probably have be done as root):

  1. Mount memory card on your linux computer
    Eg: if you're card is /dev/sdb and you have created a directory /tmp/ext, as root:
    mount -t auto /dev/sdb /tmp/ext
  2. Make sure memory card is empty:
    rm -rf /tmp/ext/*
  3. Run perl copy script calling out:
    - The tags file you want to use
    - The Source directory
    - The Target directory on your memory card:
    Eg:
    ./cpto.pl ~/carmusic.tags /home/music /tmp/ext/music
  4. This will either complete fully or will fail with "No space left on device"
  5. Unmount the card on your linux computer:
    umount /tmp/ext
  6. Put memory card in your ICE and copy to Hard Disk selecting Replace files
  7. If step 4 failed with "No space left on device" then repeat from step 1.
The tags file looks like this:
/home/music/la_roux/la_roux/armour_love.mp3^1251554384^C /home/music/la_roux/la_roux/reflections_are_protection.mp3^1251554386^C /home/music/la_roux/la_roux/growing_pains.mp3^1251554686^C /home/music/Green Day/International Superhits/Basket Case.mp3^1235913966^C /home/music/Green Day/International Superhits/Brainstew.mp3^1235914182^C /home/music/Green Day/International Superhits/Geek Stink Breath.mp3^1235914132^C /home/music/Green Day/International Superhits/Good Riddance Time of Your Life.mp3^1235914370^C /home/music/Green Day/International Superhits/Hitchin A Ride.mp3^1235914324^C /home/music/Green Day/International Superhits/JAR Jason Andrew Relva.mp3^1235914098^C /home/music/Green Day/International Superhits/Jaded.mp3^1235914206^.
Note that the file format is:
FromFileName^ModificationTime^Status

Where Status is "." if copy is in progress and "C" is completed copy. Source: cpto.pl

#!/opt/bin/perl

#       This perl script is used to mirro files from one dir to another
#       It keeps a tag file of files that have been copied and the modification time
#       of the source file when copy was done so that copies
#       are not repeated.
#
#       It is designed to fill up a device with files to be copied until its out of space
#       You can then do what you want with the files on the device (eg copy them to another device)
#       clear the device nd run the copy again. The tags file ensures that only as yet uncopied
#       files are placed on the device.
#
#       I wrote this so that I can transfer my music files to my car's enterntainment system using
#       a 2Gb memory card - the maximum card size supported by the ICE.
#
#       Usage:
#       cpto.pl tagFileName /home/music /mnt/extcard/music

#       Author: Tony.Jewell@Cregganna.Com
#       See: http://www.tonyjewell.com/2013/10/copying-music-to-in-car-entertainment.html
#       Disclaimer: I take not responsibility for any damage or other nepharious effects this
#               script will have on your PC - use at yoru own risk.

use strict;
use warnings;
use diagnostics;

use File::stat;
use File::Basename;
use File::Path;

if ( $#ARGV < 2 ) {
        print "Usage: cpto TagFile Source Destination\n";
        exit 1;
}

my @suffixes = ( ".mp3" );
my $buffSize = 8 * 2**10;
my $tagsFile = "${ARGV[0]}";
# tags is keyed by file name and value is date/time of cp from file
my %tags;

sub loadTags {
        # Check that we can write to it
        open (TAGS, ">>${tagsFile}") || die "Unable to open ${tagsFile} for writing: $!";
        close(TAGS);

        if ( -f "${tagsFile}" ) {
                open( my $TAGS, "<${tagsFile}") || die "Unable to open ${tagsFile}: $!";
                while (my $tagLine = <$TAGS>) {
                        chomp $tagLine;
                        my @tag = split('\^', $tagLine);
                        if ($#tag >= 2 && $tag[2] =~ "C") {
                                $tags{$tag[0]} = $tag[1];
                        }
                }
                close(TAGS);
        }
}

# We write a start Copy to make sure there is room to write to the tags file
# This writes a pending line with status "."
sub tagStartCopy {
        my $name = shift;
        my $mtime = shift;
        open (TAGS, "+<${tagsFile}") || die "Unable to open ${tagsFile} for writing: $!";

        # Seek to end
        sysseek(TAGS, 0, 2 ) || die "Unable to seek file $tagsFile: $!";

        my $tagPos = tell(TAGS);
        syswrite(TAGS, "$name^$mtime^.\n");     # Mark as pending

        close(TAGS) || die "Unable to close $tagsFile after writing: $!";

        return $tagPos;
}

# Rewrite the tag line with the completed copy status of "C".
sub tagEndCopy {
        my $tagPos = shift;
        my $name = shift;
        my $mtime = shift;

        open (TAGS, "+<${tagsFile}") || die "Unable to open ${tagsFile} for writing: $!";

        sysseek(TAGS, $tagPos, 0 ) || die "Unable to seek file $tagsFile: $!";
        syswrite(TAGS, "$name^$mtime^C\n");

        close(TAGS) || die "Unable to close $tagsFile after writing: $!";

        $tags{$name} = $mtime;
}

sub chkDirExists {
        my $dir = dirname(shift);

        if (! -d $dir) {
                mkpath($dir) || die "Unable to create Directory: $dir: $!";
        }
}


sub cpFile {
        my $fromName = shift;
        my $toName = shift;

        my($filename, $directories, $suffix) = fileparse($fromName, @suffixes);
        if ( ! $suffix ) {
                return;
        }

        my $from = stat($fromName) || die "Unable to stat $fromName: $!";
        my $tag = $tags{$fromName};

        if ( $tag && $tag >= $from->mtime ) {
                # Already done this one
                return;
        }

        print "$fromName => $toName....";

        &chkDirExists($toName);
        my $tagPos = &tagStartCopy($fromName, $from->mtime);

        open( IN, "< $fromName") || die "Unable to open for Reading: $fromName: $!";
        open( OUT, "> $toName") || die "Unable to open for Writing: $toName: $!";
        binmode(IN);
        binmode(OUT);
        my $buff;
        while (read(IN, $buff, $buffSize)) {
                print OUT $buff;
        }
        close( IN ) || die "Unable to close for Reading: $fromName: $!";
        close( OUT ) || die "Unable to close for Writing: $toName: $!";

        &tagEndCopy($tagPos, $fromName, $from->mtime);

        print "\n";
}

sub cpDir {
        my $fromName = shift;
        my $toName = shift;

        opendir(my $DIR, $fromName) || die "Unable to read directory: $fromName: $!";

        while(my $file = readdir $DIR) {
                if ( ! ($file =~ /^[.]+$/ ) ) {
                        &cp($fromName . '/' . $file, $toName . '/' . $file);
                }
        }

        closedir $DIR;
}

sub cp {
        my $fromName = shift;
        my $toName = shift;

        if (-d $fromName) {
                &cpDir($fromName, $toName);
        } elsif (-f $fromName) {
                &cpFile($fromName, $toName);
        }
}



&loadTags();
&cp($ARGV[1], $ARGV[2]);