April 26, 2005

Share Your EyeTV Archive - v1.2

When I posted my original shell script to make a folder of links to your EyeTV recordings with human-readable names, it caught on faster then I expected. I made no real effort to promote it, but soon my referrer logs showed it had been posted on the CenterStage message board and the AV Science Forum. The only explanation is someone found my comment here. Since at least a few people found the script useful too, I was motivated to rewrite it in perl. I had been meaning to do this for a while, and I knew I should have done it with perl in the first place. Plus, I fixed a bug where two recordings of the same show on the same day would only link the latter. (Thanks Jan!)

    EyeTV Archive folder:


    Folder of links created by the script:

OVERVIEW
If you have an Elgato EyeTV personal video recorder (PVR)—like a TiVo for the Mac—and you want to share the recorded programs on a local network there's a small problem. EyeTV stores your programs in a directory on your hard drive but its far from as user friendly as the system iTunes uses. I like the EyeTV interface fine but I don't like being forced to use it to access my content.

With the perl script below you can have a better way to access your content through the file system locally or on a network. This is especially helpful if you want stream it to a Home Theater system or to share it with other users on your LAN. Plus, you can easily drag and drop an mpg to copy or burn it, without exporting it first from EyeTV. The script below will create links to the files in your EyeTV Archive in another folder that you choose—with human-readable names. You can quickly grab the file from the link with out launching EyeTV to export the file.

DISCLAIMER
Use of this software is at your own risk. You agree not hold me liable for any unexpected results or lost data.

INSTALLATION
Download the script here. If you copy and paste the code below you need to make sure you save the file with unix line breaks. Save the script to /usr/local/bin and chmod +x it. You'll need to edit the first few lines to set up your preferences. Enter the full path of your EyeTV Archive ($archive_dir) and the full path to the folder you want to put the links ($link_dir). The latter should be an empty folder because the script will delete any files there that end in .mpg.

HOW TO RUN IT
The script can either create hard links or symbolic links to the mpgs in your archive. Symbolic links are more like aliases and the finder will treat them as such. The disadvantage is they wont necessarily work for file sharing (AFP & Samba). Hard links are more like a pointer to the same location on your hard drive. As such, you can only use hard links if the file you are linking to is on the same physical drive as the link.

The script can take three switches when executed: -p will print a report; -l with make hard links; and -s will make symbolic links. If for some reason you entered both -sl it would default to symbolic links. Here's and example of how to execute the command to create symbolic links:

joshua% eyetvlinks.pl -s

I set up a cron job to fire off the script a couple times a day so the links get updated as the content in the archive changes. I recommend Cronnix for that. In my configuration I have the location of the links on the same drive as the archive, so I use hard links. I also print the report and pipe it to logger to make an entry into the system log. The command I use in Cronnix/cron is:

/usr/local/bin/eyetvlinks.pl -pl | logger

SOURCE CODE

#!/usr/bin/perl -w

# EyeTvLinks 1.2
#
# Search recursively through the EyeTV Archive and make some
# friendly links in my shared video directory for the roommates.
#
# Copyright 2005 Joshua McFarren. Some rights reserved.
# This work is licensed under a Creative Commons License:
# http://creativecommons.org/licenses/by-sa/2.0/

my $archive_dir = "/Volumes/Seagate 200/EyeTV Archive";
my $link_dir    = "/Volumes/Seagate 200/Video/EyeTV";
my $separator   = "  ";

### SCRIPT ###

use strict;
use Getopt::Std;
my $script_name = &pluck($0);
my $version     = '1.2';
my %opts;
&print_usage() && exit unless getopts('slp', \%opts);

opendir(ARCHIVE, $archive_dir) or die "ERROR! Couldn't open $archive_dir: $!\n";
my @dirs = grep { !/^(f{16}|\.)/ } grep { -d "$archive_dir/$_" } readdir(ARCHIVE);
closedir(ARCHIVE);

my %shows = ();
foreach my $show_code (@dirs) {

    my $title = '';
    my $subdir = "$archive_dir/$show_code";

    opendir(SUBDIR, $subdir ) or die "ERROR! Couldn't open $subdir: $!\n";
    my @files = grep { s/.mpg// } grep { /.mpg/ } readdir(SUBDIR);
    closedir(SUBDIR);

    if (@files) {
        my $slurp = &slurp_file("$subdir/$show_code" . ".eyetvp");
        if ( $slurp =~ m#<key>title</key>\s+<string>([^<]+)</string>#i ) {
            $title = $1;
            $title =~ s#\s*(/|:)\s*# - #g;
        } else { print STDERR "ERROR! Bad XML found in $subdir/$show_code", ".eyetvp\n"; }
    } else { next; } # no mpgs recorded yet move on

    my %recordings = (); # reset list of recordings for each show
    foreach my $recording_code (@files) {
        my $slurp = &slurp_file("$subdir/$recording_code" . ".eyetvr");
        if ( $slurp =~ m#<key>start</key>\s+<date>(\d{4}-\d{2}-\d{2})[^<]*</date>#i ) {
            # for readablity, we could push $1 instead
            my $date = $1;
            # hash of recordings for active show
            $recordings{$recording_code} = $date;
        } else { next; }
    } # next recording

    # store a reference to it in the hash of shows
    $shows{$show_code} = { 'title' => $title, 'recordings' => \%recordings };

} # next show

if (! %opts) {
    &print_usage("$script_name version $version");
} else {
    if ( $opts{'l'} || $opts{'s'} ) {
        &clear_dir();
        &make_links();
    }
    if ( $opts{'p'} ) { &print_info(); }
}

### SUBS ###

sub clear_dir {
    opendir(LINKDIR, $link_dir) or die "ERROR! Couldn't open $link_dir: $!\n";
    my @links = map { "$link_dir/$_" } grep { /.mpg/ } readdir(LINKDIR);
    closedir(LINKDIR);
    foreach my $link (@links) {
        unlink ($link) or warn "ERROR! Couldn't unlink file: $!\n$link\n";
    }
}

sub make_links {
    foreach my $show_code (keys %shows) {
        # pull out the reference we stored earlier
        my $showref = $shows{$show_code}->{'recordings'};
        foreach my $recording_code (keys %$showref){
            my $source = "$archive_dir/$show_code/$recording_code" . ".mpg";
            my $target  = "$link_dir/" . $shows{$show_code}->{'title'} . " - ";
               $target .=  $shows{$show_code}->{'recordings'}->{$recording_code} . ".mpg";
            if ($opts{'s'}) {
                my $episode_num = 2;
                while (-e $target ) {
                    $target =~ s#(\d{4}-\d{2}-\d{2})( - \d+)?(.mpg)#$1 - $episode_num$3#i;
                    $episode_num++;
                }
                symlink $source, $target or warn "\nERROR! $!\n$source\n$target\n";
            } else {
                my $episode_num = 2;
                while (-e $target ) {
                    $target =~ s#(\d{4}-\d{2}-\d{2})( - \d+)?(.mpg)#$1 - $episode_num$3#i;
                    $episode_num++;
                }
                link $source, $target or warn "\nERROR! $!\n$source\n$target\n";
            }
        }
    }
}

sub print_info {
    foreach my $show_code (keys %shows) {
        print "\n";
        print $shows{$show_code}->{'title'}, "\n";
        # pull out the reference we stored earlier
        my $showref = $shows{$show_code}->{'recordings'};
        foreach my $recording_code (keys %$showref){
            print $separator, $shows{$show_code}->{'recordings'}->{$recording_code};
            print $separator, "$show_code/$recording_code", ".mpg\n";
        }
    }
}

sub slurp_file {
    my $path = shift;
    my $contents = '';
    open (INFILE, "< $path") or die "ERROR! Can't open $path: $!\n";
    while (<INFILE>) { $contents .= $_; }
    close INFILE;
    return $contents;
}

sub pluck {
    my @path = split ('/', "$_[0]");
    return pop @path;
}

sub print_usage {
if ($_[0]){ print STDERR "$_[0]\n"; }
print STDERR <<EOF;
Usage: $script_name [OPTIONS]
    options:
    -s  create symbolic links
    -l  create hard links
    -p  print information about recordings
EOF
}

__END__

HISTORY:

1.2   April 22, 2005 - Ported script to perl. No more backticks, no more
      sed or awk. Huge performance increase. Rewritten from the ground up
      with nested hash data structure. Fixed bug where two recordings of
      the same program on the same day replace each other. (Thanks Jan!)
      Supports command line switches. Better reporting with -p switch.

1.1b  February 15, 2005 - Added support for multiple recordings of same
      program on different days by adding date to the link name. Removed
      useless uses of echo and cat.

1.0b  May 5, 2004 - Rough cut.

TO DO:

   *  Put description in the finders get info field?

   *  Add support for locating the EyeTV archive automatically?
      Chris Nandor did this, but you need to install modules for it to
      work. I would like to keep this simple to install and run for
      people who have little cli experience. Perhaps just check for the
      prefs in the obvious places.

   *  Add support for specifying the link_dir with a switch

DISTRIBUTION:

This script and it's documentation can be found here:
http://www.mcfarren.org/archives/000030.html

This work is licensed under a Creative Commons License:
http://creativecommons.org/licenses/by-sa/2.0/

Creative Commons License   This work is licensed under a Creative Commons License
Technorati tags: , , , , , , , , , , , , ,
Posted by joshua at April 26, 2005 9:38 PM
Comments

Regarding your second todo item:

use warnings;
use strict;
my $a = `osascript -e 'tell application "EyeTV" to set bla to repository url\nreturn POSIX path of bla'`;
print "The EyeTV Repository is located at $a\n";

That, of course, launches the EyeTV application. IMO a minor inconvenience, if any at all.

BTW: Thanks for your Script! But calling subs using ampersand... shakes his head ;-)

Posted by: anjoschu at April 27, 2005 1:25 AM

You can easily store preferences (ie the paths) using he default command. Combined with my first suggestion and a "choose folder" dialog, the users need not edit the perl script at all.

# Check for an already existing setting, set $link_dir to "" if -r is given
my $link_dir = $opts{'r'} ? "" : `defaults read org.mcfarren.eyetvlinks link_dir`;

# Check for an already existing setting, set $archive_dir to "" if -r is given
my $archive_dir = $opts{'r'} ? "" : `defaults read org.mcfarren.eyetvlinks archive_dir`;

# Read in & ask for paths if no settings available or -r given
unless ($archive_dir) {
    $archive_dir = `osascript -e 'tell application "EyeTV" to set bla to repository url\nreturn POSIX path of bla'` or die("cannot get repository url from eyetv\n");
    chomp $archive_dir;

    # store this in the preferences
    system 'defaults write org.mcfarren.eyetvlinks archive_dir -string "'.$archive_dir.'"'
}
unless ($link_dir) {
    $link_dir = `osascript -e 'tell application "EyeTV" to set bla to choose folder\nreturn POSIX path of bla'` or die("dialog cancelled by user\n");
    chomp $link_dir;
    # store this in the preferences
    system 'defaults write org.mcfarren.eyetvlinks link_dir -string "'.$link_dir.'"'
}

# those shell return values end in \n
chomp $archive_dir;
chomp $link_dir;

There. On its first execution, the script asks for the link_dir and fetches the repository path form eyetv. Those values are then stored & recalled on each launch so cron should work fine. It only asks again when called with the -r option.

There's still a minor bug. The script does not exit promptly when called without options. Plus, it should also exit after resetting everything when called only with the -r option. But it runs through. Not sure why.

I added another feature as well, it bothered me that episode names from the .eyetvr files were not represented in the file name, so I used to see "Simpsons - 2005-03-21" instead of "Simpsons - Moaning Lisa - 2005-03-21". I changed this. If there is no episode name given, it also suppresses the extra hyphen. The extended foreach loop looks thus:

foreach my $recording_code (@files) {
    $slurp = &slurp_file("$subdir/$recording_code" . ".eyetvr");
    if ( $slurp =~ m#&lt;key&gt;start&lt;/key&gt;\s+&lt;date&gt;(\d{4}-\d{2}-\d{2})[^&lt;]*&lt;/date&gt;#i ) {

        # for readablity, we could push $1 instead
        my $date = $1;

        # get episode name, set $episode to empty string of none
        if ( $slurp =~ m#&lt;key&gt;title&lt;/key&gt;\s+&lt;string&gt;([^&lt;]*)&lt;&lt;/string&gt;&gt;#i ) {
            $episode = $1 ? $1." - " : "";
            $episode =~ s#\s*(/|:)\s*# - #g;
        } else { warn "ERROR! Bad XML found in $subdir/$show_code", ".eyetvr\n"; }

        # hash of recordings for active show
        $recordings{$recording_code} = $episode.$date;
    } else { next; }
} # next recording

Here is the complete altered file

Posted by: anjoschu at April 27, 2005 3:31 AM

Editors note: Andreas has given me permission to include his code above in a future version of this script. I plan to address the -r bug he noted and include more robust error checking on the paths. Also I plan to add another switch (-e) to optionally include the episode title in the link name. If you're impatient you can download his version with the URL he listed above.

Posted by: Joshua McFarren at April 27, 2005 1:52 PM


Post a comment









Remember personal info?