#!/usr/bin/perl

$|++;

use warnings;
use strict;

our $VERSION = '0.026';
our ( $config, $cfgdir );

init();
load_credentials( File::Spec->catfile( $cfgdir, "userid" ) );
load_config( File::Spec->catfile( $cfgdir,      "config" ) );

if ( $config->{from_udev} ) {
    $config->{mountpoint}
        = File::Spec->catdir( '/media', $ENV{'mount_point'} )
        unless ( $config->{mountpoint} );
    sync_podcasts_to_player( );
    exit;
}

if ( $config->{purge_podcasts_after} ) {
    purge_files( $config->{podcast_dir}, $config->{purge_podcasts_after} );
}

sync_podcasts_to_disk();

#END MAIN

sub init {

    use WebService::Google::Reader;
    use File::Spec;
    use File::Path;
    use LWP::Simple;
    use POSIX qw(setsid);
    use File::Copy;
    use constant SEP => "\0\0";

    $cfgdir = File::Spec->catdir( $ENV{HOME}, '.GReaderSync' );

    #Setup our folders
    check_dirs($cfgdir);
}

sub check_dirs {
    foreach my $d (@_) {
        if ( !-d $d ) {
            mkpath $d || die "Couldn't make directory $d!";
        }
    }
}

sub load_credentials {

    #Get the Google reader credentials
    my $userid_file = shift;
    if ( -r $userid_file ) {
        open( F, $userid_file )
            || die "Couldn't open $userid_file for reading!";
        my $sep = SEP;
        chomp( my $line = <F> );
        ( $config->{gid}, $config->{gpw} ) = split( /$sep/, $line );
        close(F);
    }
}

sub usage {
    my $exitcode = shift || 0;
    $exitcode = 0 if ( $exitcode eq 'help' );

    print <<"EOD";
NAME    
    GReaderSync.pl - Sync podcasts from feeds stored on Google Reader to your 
                     MP3  Player

VERSION
    $VERSION
    
SYNOPSIS
    /usr/local/bin/GReaderSync.pl [OPTION]...
    
DESCRIPTION
    You must run with --user-id and --password once to store the credentials.
    If not already present, GReaderSync.pl will create a self-documented 
    config file at ~/.GReaderSync/config.
    
    --install-help
            Display installation information.
    --user-id=mygoogleuserid
            Set the userid to use to log into Google Reader.
    --password=mygooglepassord
            Set the password to use to log into Google Reader.
    --verbose
            Enable verbose logging.  Default is silent.
    --make-rule='NAME OF PLAYER'
            Generate a UDEV rule using "NAME OF PLAYER" as the model.
    --mountpoint=/path/to/mounted/mp3player
            Specify the path to the mounted mp3player manually.
    --help
            This output.
    --from-udev
            Triggers the mp3 player sync mode.  The process forks to the 
            background, redirects output to \$HOME/.GReaderSync/player_sync.log,
            and begins the synchronization process.

            

EOD
    exit $exitcode;
}

sub install_help {
    print <<"EOD";
Google Reader is cool - it lets you keep up on your feeds from anywhere.  
Your phone, your MAC, your PC, and on and on.  Every podcast I listen to has
an RSS or an Atom feed that you can add to Google Reader, but I really 
didn't like Google's interface to actually listen to the podcast.  Also, my
MP3 player doesn't have a browser, so I'd have to manually download the 
podcast from Google Reader to my PC, then copy the podcast to my MP3 player.
This aggravated me to the point of doing something about it.

GReaderSync.pl serves two functions - 1) To download podcasts tagged with a
user-configurable term to your PC, more than likely via a cron job; 2) Sync 
your GReaderSync.pl downloads to your MP3 player automatically upon 
connection.

Step 0: Install the Prerequisites
To install necessary libs on Ubuntu:
    sudo apt-get install libxml-atom-perl libcrypt-ssleay-perl \
        libjson-any-perl libclass-accessor-perl
    sudo perl -MCPAN -e 'install WebService::Google::Reader'

Step 1: Setup Google Reader
Log into Google Reader, and tag some of your podcast entries with a custom
tag - I use the hostname of the PC that will be doing the downloading.

Step 2: Configure GReaderSync.pl
Run 
    /usr/local/bin/GReaderSync.pl --user-id=mygoogleuserid --password=mygooglepassord

This will cache your username and password, as well as create a sample config
file for you at ~/.GReaderSync/config.  Open that file in a text editor.  The
file is self-documented, but make sure you set 'podcast_dir' to wherever you
want your podcasts stored, and set 'tags' to the tag you used in Step 1.

Step 3: Run GReaderSync.pl
Run
    /usr/local/bin/GReaderSync.pl --verbose
    
This will download all the podcasts linked to from the Reader entries you 
previously tagged.  Once it downloads the podcast, the script removes the tag
from Google Reader, so it won't download it again unless you retag the entry.

Step 4: Configure UDEV for automatic syncing to your MP3 Player
    
You need to determine the model name of your player.  If your player gets 
automatically mounted to /media/NAME OF PLAYER, then your model is 'NAME OF 
PLAYER'.  Otherwise, follow this howto and use the value from ATTRS{model}:
 http://www.reactivated.net/writing_udev_rules.html

Once you have the model, then run the following to have the script generate 
the rule for you.  DO NOT RUN THIS AS ROOT, RUN IT AS THE USER THAT WILL BE
RUNNING THE SYNC!!!
    /usr/local/bin/GReaderSync.pl --make-rule='NAME OF PLAYER'
  
Now take the output, and put it in a udev rule (you do have to be root to do 
this).

Step 5: Try it out
At this point, you should have podcasts on your PC, so go ahead and plug the
MP3 player in and with any luck at all you will end up with a sync.  There is
a logfile created at ~/.GReaderSync/player_sync.log

Step 6: Setup your crontab
Fire off 'crontab -e', and add a job.  Something like this should do nicely:
0 */2 * * * /home/justintime/GReaderSync.pl

This runs the sync once every two hours.

Step 6: Enjoy!

EOD
}

sub create_config {
    my $configfile = shift;
    open( CFG, ">$configfile" )
        || die "Couldn't open $configfile for writing!";
    print CFG <<"EOF";
# This is a comment, starting with a pound sign.
# Listed here are all possible options:
#
# verbose = 1 # Turn on verbose logging, 0 == off == default.
#
# purge_podcasts_after = 30  # deletes podcasts from the source machine after 
#                            # 30 days, 0 means never delete.  0 is default.
#
# podcast_dir = /home/justintime/podcasts # podcast dir on source machine
#
# mount_point = /media/Sansa e260 # mount point of MP3 player - must be
#                                 # mounted automatically before the script
#                                 # fires.
#
# tags = m170, m170-2 # comma delimited list of tags to pull down from 
#                     # Google Reader
#
# dest_dir = MUSIC/podcasts # relative path to podcast dir on mp3 player, 
#                           # defaults to MUSIC/GReader
#
# tags and podcast_dir are required
    
EOF
    close(CFG);
}

sub load_config {

    #Parse our file
    my $configfile = shift;
    if ( -e $configfile ) {
        open( CFG, $configfile )
            || die "Couldn't open $configfile for reading!";
        while ( my $line = <CFG> ) {
            chomp($line);
            $line =~ s/^\s+//;
            $line =~ s/\s+$//;
            next if ( $line =~ /^#/ );
            next if ( $line =~ /^\s*$/ );
            my ( $key, $value ) = split( /\s*=\s*/, $line );
            if ( $key eq 'tags' ) {
                $config->{tags} = [ split( /\s*,\s*/, $value ) ];
            }
            else {
                $config->{$key} = $value;
            }
        }
        close(CFG);
    }
    else {
        print "Config file not present at $configfile, generating sample.\n";
        create_config($configfile);
    }

    # Command line trumps all
    my ( $gid, $gpw );
    use Getopt::Long;
    GetOptions(
        'user-id=s'    => \$gid,
        'password=s'   => \$gpw,
        'mountpoint=s' => \$config->{mountpoint},
        'help'         => \&usage,
        'from-udev'    => \$config->{from_udev},
        'make-rule=s'  => \&generate_udev_rule,
        'install-help' => \&install_help,
        'verbose'      => \$config->{verbose},
    );
    if ( $gid || $gpw ) {
        if (   ( !$config->{gid} )
            || ( !$config->{gpw} )
            || ( $gid ne $config->{gid} )
            || ( $gpw ne $config->{gpw} ) )
        {
            $config->{gid} = $gid;
            $config->{gpw} = $gpw;
            save_credentials( File::Spec->catfile( $cfgdir, "userid" ) );
        }
    }
    $config->{dest_dir} = File::Spec->catdir( "MUSIC", "GReader" )
        unless $config->{dest_dir};
    validate_config();
    check_dirs( $config->{podcast_dir} );
}

sub validate_config {
    my @required = qw(podcast_dir tags dest_dir);
    foreach (@required) {
        if ( !$config->{$_} ) {
            print "Please set $_ in config file at "
                . File::Spec->catfile( $cfgdir, 'config' ) . "\n\n";
            usage(1);
        }
    }
}

sub save_credentials {

    #Save credentials to the file
    my $userid_file = shift;
    print "Saving credentials to $userid_file.\n" if ( $config->{verbose} );
    if ( $config->{gid} ) {
        open( F, ">$userid_file" )
            || die "Couldn't open $userid_file for writing!";
        print F $config->{gid} . SEP . $config->{gpw} . "\n";
        close(F);
        chmod 0600, $userid_file
            || die "Couldn't set $userid_file permissions to 0600: $!";
    }
}

sub sync_files {
    my ( $from, $to ) = @_;
    if ( !-d $to ) {
        mkpath $to || die "Couldn't make directory $to!";
    }
    use File::Find;

    my $last_synced_at = -M File::Spec->catfile( $to, ".last_synced" );
    $last_synced_at = 9999999999999999 unless $last_synced_at;

    # Traverse desired filesystems
    File::Find::find(
        {   wanted => sub {
                ( my ( $dev, $ino, $mode, $nlink, $uid, $gid ) = lstat($_) );
                if ( -f $_ && -M $_ < $last_synced_at ) {
                    print "Syncing " . $File::Find::name . "\n";
                    copy( $File::Find::name, $to );
                }
                }
        },
        $from
    );
}

sub purge_files {
    my ( $dir, $days ) = @_;
    if ( !-d $dir ) {
        warn "purge_files called on non-existant dir $dir.";
    }
    use File::Find;

    # Traverse desired filesystems
    File::Find::find(
        {   wanted => sub {
                ( my ( $dev, $ino, $mode, $nlink, $uid, $gid ) = lstat($_) );
                if ( -f $_ && -M $_ > $days ) {
                    print "Purging " . $File::Find::name . "\n";
                    unlink($File::Find::name)
                        || warn "Unable to remove file $File::Find::name: $!";
                }
                }
        },
        $dir
    );
}

sub daemonize {
    close(STDOUT);
    close(STDIN);
    close(STDERR);
    my $pid = fork;
    exit if ($pid);

    #select our logfile if verbose
    if ( $config->{verbose} ) {
        open( LOG, ">$cfgdir/player_sync.log" );
        select LOG;
        $|++;
        print "Syncing "
            . $config->{podcast_dir} . " to "
            . File::Spec->catdir( $config->{mountpoint},
            $config->{dest_dir} )
            . "\n";
    }
    die "Couldn't fork: $!" unless defined($pid);
    setsid() || die "Can't start a new session!: $!";
}

sub sync_podcasts_to_player {

    #fork to the background
    daemonize();

    #Make sure everything is mounted
    my $i;
    for ( $i = 1; $i <= 20; $i++ ) {
        if ( scalar( glob( $config->{mountpoint} . "/*" ) ) > 0 ) {
            last;
        }
        sleep 1;
    }
    if ( $i == 20 ) {
        die "Never saw any files under "
            . $config->{mountpoint}
            . " in 20 seconds, is your automounter working?";
    }

    my $to = File::Spec->catdir($config->{mountpoint}, $config->{dest_dir});

    sync_files( $config->{podcast_dir}, $to );

    if ( $config->{verbose} ) {
        print "Exiting.\n";
        close(LOG);
    }

    #Create our timestamp file
    open( OUT,
        ">" . File::Spec->catfile($config->{mountpoint},$config->{dest_dir},".last_synced") );
    close(OUT);

}

sub sync_podcasts_to_disk {

    #Fetch/Parse the feed
    if ( !$config->{gid} || !$config->{gpw} ) {
        die
            "Couldn't determine Google Reader credentials.  Pass it in with --user-id and --password, and I'll save it for later.";
    }
    my $reader = WebService::Google::Reader->new(
        username => $config->{gid},
        password => $config->{gpw},
        https    => 1,
    );

    foreach my $tag ( @{ $config->{tags} } ) {
        my $feed = $reader->tag($tag);
        if ( my $error = $reader->error ) {
            die "Google Reader returned an error: $error\n";
        }
        print "Fetching entries tagged " . $feed->title . "\n"
            if $config->{verbose};
        my $count = 0;
        for my $entry ( $feed->entries ) {
            $count++;
            print "  Looking for podcasts to download from ", $entry->title,
                "\n"
                if $config->{verbose};
            foreach my $link ( $entry->link ) {
                if ( $link->rel eq "enclosure" ) {
                    my $uri  = URI->new( $link->href );
                    my $file = File::Spec->catfile( $config->{podcast_dir},
                        ( File::Spec->splitpath( $uri->path ) )[2] );
                    print "  Found podcast at $uri - downloading it..."
                        if $config->{verbose};
                    my $status_code = mirror( $uri, $file );
                    print " done.\n" if $config->{verbose};
                    if ( $status_code == 304 ) {
                        print "    Local file $file up to date, skipping.\n"
                            if $config->{verbose};
                        $reader->untag_entry( $entry, $tag )
                            || warn "Couldn't untag " . $entry->title . ".\n";
                    }
                    elsif ( is_success($status_code) ) {
                        print "    Downloaded $uri to $file!\n"
                            if $config->{verbose};
                        $reader->untag_entry( $entry, $tag )
                            || warn "Couldn't untag " . $entry->title . ".\n";
                        warn $reader->error if $reader->error;
                    }
                    else {
                        print
                            "    HTTP Status code == $status_code - Something's wrong, I'll try again later.\n"
                            if $config->{verbose};
                    }
                }
            }
            print "\n" if $config->{verbose};
        }
        print "Synchronized $count entries tagged as $tag to disk\n"
            if $config->{verbose};
    }
}

sub generate_udev_rule {
    my $model = $_[1];

# ACTION=="add", ATTRS{model}=="Sansa e260", ENV{mount_point}="Sansa e260", ENV{HOME}="/home/justintime/", RUN+="/usr/local/bin/GReaderSync.pl --verbose --from-udev"
    my $output
        = 'ACTION=="add", ATTRS{model}=="' 
        . $model
        . '", ENV{mount_point}="'
        . $model
        . '", ENV{HOME}="'
        . $ENV{HOME}
        . '", RUN+="';
    my $script = $0;
    my ( $vol, $dirs, $file ) = File::Spec->splitpath($script);
    my $abspath = File::Spec->rel2abs($dirs);
    my $fullpath = File::Spec->catfile( $abspath, $file );
    $output .= "$fullpath --verbose --from-udev\"\n";
    print
        "Put the following in a udev rules file (I use /etc/udev/rules.d/50-mp3player-sync.rules)\n\n";
    print $output;
    exit;
}

