Friday, August 17, 2007

Automating torrent downloads with AppleTV + RSS

If you regularly subscribe to a few podcasts or video programs that are available via bittorrent with an RSS feed, and you watch them on your appletv, you might be wondering if you can automate the process so everything happens automatically without the need for any external devices (ie. no need to use iTunes), well you can.

Firstly, you can use Yahoo Pipes to aggregate your chosen subscription's RSS feeds into a single feed and only include the programming you want to download, then you setup your (hacked) AppleTV with atvTorrents, simple enough. Lastly you need something that will download the new items from the aggregate RSS feed and start them downloading.

I couldn't find anything that would do this last bit for the AppleTV, so I wrote something myself in perl, which is already installed on the AppleTV, after seeing a couple of simple examples in ruby and python.

Below is the code, is has no external dependencies other than /usr/bin/curl, which doesn't come with the AppleTV, but you can get it from the Weather plugin and copy it into /usr/bin

#! /usr/bin/perl

use warnings;
use strict;

# Fetch a URL with curl
sub fetchurl {
my $url = shift;
my $cmd = qq{/usr/bin/curl -s "$url"};
return `$cmd`;
}

# Return a list as an arrayref
sub as_arrayref {
my $arg0 = shift;
return \@_ if ! defined $arg0;
return [ @$arg0, @_ ] if ref $arg0 eq 'ARRAY';
return [ $arg0, @_ ];
}

# Convert feed XML data into a nested data structure
sub parse {
my ($markup) = @_;

# Split on matching start end tags
# - On match returns sets of 4 elements: [ '', tag, tag_values, content ]
# - On no match return 1 element: [ content ]
my @markup = split(/<([^\ >]+)\ *([^>]*)>(.*?)<\/\1>/, $markup);

my $result = ();

while ($#markup >= 0) {
if ($markup[0] eq '') {
my $element = {};
shift(@markup);
my $tag = shift(@markup);
my %values = split(/[\ =]/,shift(@markup));
$element->{$tag} = parse(shift(@markup));
$element->{$tag}->{_values} = \%values if %values;
foreach my $key (keys %$element) {
if (defined $result->{$key}) {
$result->{$key} = as_arrayref $result->{$key}, $element->{$key};
} else {
$result->{$key} = $element->{$key};
}
}
} else {
# This should only get called once on no matches from split
$result = shift(@markup);
}
}
return $result;
}

# Read RSS feed and return parsed data structure
sub rssread {
my $feedurl = shift;
my @result;
foreach my $item (split("\n", fetchurl($feedurl))) {
my $data = parse($item);
ref $data eq 'HASH' and push(@result, $data);
}
return @result;
}

my $downloadpath = "/Users/frontrow/Torrents/";
my $downloadlog = "/Users/frontrow/.tvdownloads";
my $feedurl = "http://pipes.yahoo.com/pipes/pipe.run?_id=SOMEUNIQUEID&_render=rss";

sub is_downloaded {
my $link = shift;
open(LOG, $downloadlog) or return 0;
while() { chomp(); return 1 if $_ eq $link }
close(LOG);
return 0;
}

sub write_log {
my $link = shift;
open(LOG, ">>$downloadlog");
printf LOG "%s\n", $link;
close(LOG);
}

sub get_tvtorrents {
my @result = rssread($feedurl);
foreach my $it (@result) {
my $items = as_arrayref $it->{rss}->{channel}->{item};
foreach my $item (@{$items}) {
if (! is_downloaded $item->{link} ) {
(my $torrentfile = $downloadpath . $item->{title} . '.torrent') =~ s/ /\_/g;
my $torrentdata = fetchurl($item->{link});

open(TORRENT, ">$torrentfile");
print TORRENT $torrentdata;
write_log($item->{link});
close(TORRENT);

print $torrentfile . "\n";
}
}
}
}

get_tvtorrents();


Once you have this working, you will need to have it run regularly. To do that we need to use launchd by creating a tvtorrents.plist file in /Users/frontrow/Library/LaunchAgents that looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>appletv.tvtorrents</string>
<key>LowPriorityIO</key>
<true/>
<key>Nice</key>
<integer>1</integer>
<key>ProgramArguments</key>
<array>
<string>/Users/frontrow/bin/tvtorrents.pl</string>
</array>
<key>StartInterval</key>
<integer>300</integer>
</dict>
</plist>


This will run the script /Users/frontrow/bin/tvtorrents.pl every 5 minutes (300 seconds). And that's all there is to it.

1 comment:

Jody said...

Thanks but this script doesn't seem to run. I get errors on the use of strict when it hits the title tag from my xml and on each of the regex split takes because of "Use of uninitialized value"