|
@@ -11,15 +11,21 @@ use JSON;
|
|
|
use XML::LibXML;
|
|
use XML::LibXML;
|
|
|
use IPC::System::Simple qw( system );
|
|
use IPC::System::Simple qw( system );
|
|
|
use WWW::YouTube::Download;
|
|
use WWW::YouTube::Download;
|
|
|
|
|
+use PrettyPrint;
|
|
|
|
|
|
|
|
# Paramters
|
|
# Paramters
|
|
|
-my $CURL_BIN = 'curl';
|
|
|
|
|
-my @CURL_ARGS = ('-4', '--insecure', '-C', '-');
|
|
|
|
|
-my $BATCH_SIZE = 50;
|
|
|
|
|
-my $MAX_INDEX = 500;
|
|
|
|
|
-my $URL_PREFIX = 'http://gdata.youtube.com/feeds/api/users/';
|
|
|
|
|
-my $URL_SUFFIX = '/uploads';
|
|
|
|
|
-my %API_PARAMS = (
|
|
|
|
|
|
|
+my $CURL_BIN = 'curl';
|
|
|
|
|
+my @CURL_ARGS = ('-4', '--insecure', '-C', '-');
|
|
|
|
|
+my $BATCH_SIZE = 50;
|
|
|
|
|
+my $MAX_INDEX = 500;
|
|
|
|
|
+my $URL_PREFIX = 'http://gdata.youtube.com/feeds/api/users/';
|
|
|
|
|
+my $URL_SUFFIX = '/uploads';
|
|
|
|
|
+my %CHAN_PARAMS = (
|
|
|
|
|
+ 'strict' => 1,
|
|
|
|
|
+ 'v' => 2,
|
|
|
|
|
+ 'alt' => 'json',
|
|
|
|
|
+);
|
|
|
|
|
+my %LIST_PARAMS = (
|
|
|
'strict' => 1,
|
|
'strict' => 1,
|
|
|
'v' => 2,
|
|
'v' => 2,
|
|
|
'alt' => 'jsonc',
|
|
'alt' => 'jsonc',
|
|
@@ -31,6 +37,11 @@ sub findVideos($);
|
|
|
sub findFiles($);
|
|
sub findFiles($);
|
|
|
sub ytURL($);
|
|
sub ytURL($);
|
|
|
sub buildNFO($);
|
|
sub buildNFO($);
|
|
|
|
|
+sub buildSeriesNFO($);
|
|
|
|
|
+sub getChannel($);
|
|
|
|
|
+sub fetchParse($);
|
|
|
|
|
+sub buildURL($$);
|
|
|
|
|
+sub saveString($$);
|
|
|
|
|
|
|
|
# Sanity check
|
|
# Sanity check
|
|
|
if (scalar(@ARGV) < 1) {
|
|
if (scalar(@ARGV) < 1) {
|
|
@@ -69,11 +80,28 @@ if ($ENV{'BATCH_SIZE'} && $ENV{'BATCH_SIZE'} =~ /(\d+)/) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
# Construct globals
|
|
# Construct globals
|
|
|
-$API_PARAMS{'max-results'} = $BATCH_SIZE;
|
|
|
|
|
|
|
+$LIST_PARAMS{'max-results'} = $BATCH_SIZE;
|
|
|
if (!$DEBUG) {
|
|
if (!$DEBUG) {
|
|
|
push(@CURL_ARGS, '--silent');
|
|
push(@CURL_ARGS, '--silent');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+# Grab the channel data
|
|
|
|
|
+my $channel = getChannel($user);
|
|
|
|
|
+
|
|
|
|
|
+# Create the channel NFO, if needed
|
|
|
|
|
+my $nfo = $dir . '/tvshow.nfo';
|
|
|
|
|
+if (!-e $nfo) {
|
|
|
|
|
+ if ($DEBUG) {
|
|
|
|
|
+ print STDERR 'Creating series NFO: ' . $channel->{'title'} . "\n";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ my $xml = buildSeriesNFO($channel);
|
|
|
|
|
+ if ($DEBUG > 1) {
|
|
|
|
|
+ print STDERR 'Saving NFO: ' . $xml . "\n";
|
|
|
|
|
+ }
|
|
|
|
|
+ saveString($nfo, $xml);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
# Find all the user's videos on YT
|
|
# Find all the user's videos on YT
|
|
|
my $videos = findVideos($user);
|
|
my $videos = findVideos($user);
|
|
|
|
|
|
|
@@ -83,7 +111,7 @@ my $files = findFiles($dir);
|
|
|
# Fill in missing videos and NFOs
|
|
# Fill in missing videos and NFOs
|
|
|
foreach my $id (keys(%{$videos})) {
|
|
foreach my $id (keys(%{$videos})) {
|
|
|
my $basePath = $dir . '/' . sprintf('%02d', $videos->{$id}->{'number'}) . ' - ' . $id . '.';
|
|
my $basePath = $dir . '/' . sprintf('%02d', $videos->{$id}->{'number'}) . ' - ' . $id . '.';
|
|
|
- my $nfo = $basePath . 'nfo';
|
|
|
|
|
|
|
+ $nfo = $basePath . 'nfo';
|
|
|
|
|
|
|
|
# If we haven't heard of the file, or don't have an NFO for it
|
|
# If we haven't heard of the file, or don't have an NFO for it
|
|
|
# Checking for the NFO allows use to resume failed downloads
|
|
# Checking for the NFO allows use to resume failed downloads
|
|
@@ -115,59 +143,70 @@ foreach my $id (keys(%{$videos})) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- # Build the XML document
|
|
|
|
|
|
|
+ # Build and save the XML document
|
|
|
my $xml = buildNFO($videos->{$id});
|
|
my $xml = buildNFO($videos->{$id});
|
|
|
if ($DEBUG > 1) {
|
|
if ($DEBUG > 1) {
|
|
|
print STDERR 'Saving NFO: ' . $xml . "\n";
|
|
print STDERR 'Saving NFO: ' . $xml . "\n";
|
|
|
}
|
|
}
|
|
|
|
|
+ saveString($nfo, $xml);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- # Save to disk
|
|
|
|
|
- my $fh = undef();
|
|
|
|
|
- if (!open($fh, '>', $nfo)) {
|
|
|
|
|
- warn('Cannot open NFO: ' . $! . "\n");
|
|
|
|
|
- next;
|
|
|
|
|
- }
|
|
|
|
|
- print $fh $xml;
|
|
|
|
|
- close($fh);
|
|
|
|
|
|
|
+sub saveString($$) {
|
|
|
|
|
+ my ($path, $str) = @_;
|
|
|
|
|
+
|
|
|
|
|
+ my $fh = undef();
|
|
|
|
|
+ if (!open($fh, '>', $path)) {
|
|
|
|
|
+ warn('Cannot open file for writing: ' . $! . "\n");
|
|
|
|
|
+ return undef();
|
|
|
}
|
|
}
|
|
|
|
|
+ print $fh $str;
|
|
|
|
|
+ close($fh);
|
|
|
|
|
+ return 1;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-sub buildNFO($) {
|
|
|
|
|
- my ($video) = @_;
|
|
|
|
|
|
|
+sub buildSeriesNFO($) {
|
|
|
|
|
+ my ($channel) = @_;
|
|
|
|
|
|
|
|
# Create an XML tree
|
|
# Create an XML tree
|
|
|
my $doc = XML::LibXML::Document->new('1.0', 'UTF-8');
|
|
my $doc = XML::LibXML::Document->new('1.0', 'UTF-8');
|
|
|
$doc->setStandalone(1);
|
|
$doc->setStandalone(1);
|
|
|
- my $show = $doc->createElement('episodedetails');
|
|
|
|
|
|
|
+ my $show = $doc->createElement('tvshow');
|
|
|
$doc->setDocumentElement($show);
|
|
$doc->setDocumentElement($show);
|
|
|
my $elm;
|
|
my $elm;
|
|
|
|
|
|
|
|
- # Generic elements (in case we get excited later)
|
|
|
|
|
- my $fileinfo = $doc->createElement('fileinfo');
|
|
|
|
|
- $show->appendChild($fileinfo);
|
|
|
|
|
- my $streamdetails = $doc->createElement('streamdetails');
|
|
|
|
|
- $fileinfo->appendChild($streamdetails);
|
|
|
|
|
- $elm = $doc->createElement('video');
|
|
|
|
|
- $streamdetails->appendChild($elm);
|
|
|
|
|
|
|
+ # Add data
|
|
|
|
|
+ $elm = $doc->createElement('title');
|
|
|
|
|
+ $elm->appendText($channel->{'title'});
|
|
|
|
|
+ $show->appendChild($elm);
|
|
|
|
|
|
|
|
- $elm = $doc->createElement('playcount');
|
|
|
|
|
|
|
+ $elm = $doc->createElement('premiered');
|
|
|
|
|
+ $elm->appendText(time2str('%Y-%m-%d', $channel->{'date'}));
|
|
|
$show->appendChild($elm);
|
|
$show->appendChild($elm);
|
|
|
- $elm = $doc->createElement('credits');
|
|
|
|
|
|
|
+
|
|
|
|
|
+ $elm = $doc->createElement('plot');
|
|
|
|
|
+ $elm->appendText($channel->{'description'});
|
|
|
$show->appendChild($elm);
|
|
$show->appendChild($elm);
|
|
|
|
|
|
|
|
- my $actor = $doc->createElement('actor');
|
|
|
|
|
- $show->appendChild($actor);
|
|
|
|
|
- $elm = $doc->createElement('name');
|
|
|
|
|
- $actor->appendChild($elm);
|
|
|
|
|
- $elm = $doc->createElement('role');
|
|
|
|
|
- $actor->appendChild($elm);
|
|
|
|
|
|
|
+ # Return the string
|
|
|
|
|
+ return $doc->toString();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+sub buildNFO($) {
|
|
|
|
|
+ my ($video) = @_;
|
|
|
|
|
|
|
|
- # Static elements (per the nature of this process)
|
|
|
|
|
|
|
+ # Create an XML tree
|
|
|
|
|
+ my $doc = XML::LibXML::Document->new('1.0', 'UTF-8');
|
|
|
|
|
+ $doc->setStandalone(1);
|
|
|
|
|
+ my $show = $doc->createElement('episodedetails');
|
|
|
|
|
+ $doc->setDocumentElement($show);
|
|
|
|
|
+ my $elm;
|
|
|
|
|
+
|
|
|
|
|
+ # Add data
|
|
|
$elm = $doc->createElement('season');
|
|
$elm = $doc->createElement('season');
|
|
|
$elm->appendText('1');
|
|
$elm->appendText('1');
|
|
|
$show->appendChild($elm);
|
|
$show->appendChild($elm);
|
|
|
|
|
|
|
|
- # Dynamic data
|
|
|
|
|
$elm = $doc->createElement('episode');
|
|
$elm = $doc->createElement('episode');
|
|
|
$elm->appendText($video->{'number'});
|
|
$elm->appendText($video->{'number'});
|
|
|
$show->appendChild($elm);
|
|
$show->appendChild($elm);
|
|
@@ -219,7 +258,7 @@ sub ytURL($) {
|
|
|
my $stream = $meta->{'video_url_map'}->{$streamID};
|
|
my $stream = $meta->{'video_url_map'}->{$streamID};
|
|
|
my ($res) = $stream->{'resolution'} =~ /^\s*(\d+)/;
|
|
my ($res) = $stream->{'resolution'} =~ /^\s*(\d+)/;
|
|
|
if ($DEBUG > 1) {
|
|
if ($DEBUG > 1) {
|
|
|
- print STDERR $streamID . ' (' . $stream->{'suffix'} . ')' . ' => ' . $stream->{'resolution'} . ' : ' . $stream->{'url'} . "\n\n";
|
|
|
|
|
|
|
+ print STDERR $streamID . ' (' . $stream->{'suffix'} . ')' . ' => ' . $stream->{'resolution'} . ' : ' . $stream->{'url'} . "\n";
|
|
|
}
|
|
}
|
|
|
if ( ($res > $bestResolution)
|
|
if ( ($res > $bestResolution)
|
|
|
|| ($res == $bestResolution && defined($bestStream) && $bestStream->{'suffix'} ne 'mp4'))
|
|
|| ($res == $bestResolution && defined($bestStream) && $bestStream->{'suffix'} ne 'mp4'))
|
|
@@ -264,9 +303,8 @@ sub findFiles($) {
|
|
|
close($fh);
|
|
close($fh);
|
|
|
|
|
|
|
|
if ($DEBUG) {
|
|
if ($DEBUG) {
|
|
|
- print STDERR "\n\nFound " . scalar(keys(%files)) . " files:\n\t" . join("\n\t", keys(%files)) . "\n\n";
|
|
|
|
|
|
|
+ print STDERR "\nFound " . scalar(keys(%files)) . " files:\n\t" . join("\n\t", keys(%files)) . "\n";
|
|
|
if ($DEBUG > 1) {
|
|
if ($DEBUG > 1) {
|
|
|
- use PrettyPrint;
|
|
|
|
|
print STDERR prettyPrint(\%files, "\t") . "\n";
|
|
print STDERR prettyPrint(\%files, "\t") . "\n";
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -274,54 +312,92 @@ sub findFiles($) {
|
|
|
return \%files;
|
|
return \%files;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+sub buildURL($$) {
|
|
|
|
|
+ my ($base, $params) = @_;
|
|
|
|
|
+ my $url = $base . '?';
|
|
|
|
|
+ foreach my $name (keys(%{$params})) {
|
|
|
|
|
+ $url .= '&' . uri_escape($name) . '=' . uri_escape($params->{$name});
|
|
|
|
|
+ }
|
|
|
|
|
+ return $url;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+sub fetchParse($) {
|
|
|
|
|
+ my ($url) = @_;
|
|
|
|
|
+
|
|
|
|
|
+ # Fetch
|
|
|
|
|
+ if ($DEBUG) {
|
|
|
|
|
+ print STDERR 'Fetching list URL: ' . $url . "\n";
|
|
|
|
|
+ }
|
|
|
|
|
+ my $content = get($url);
|
|
|
|
|
+ if (!defined($content) || length($content) < 10) {
|
|
|
|
|
+ die('Invalid content from URL: ' . $url . "\n");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ # Parse
|
|
|
|
|
+ my $data = decode_json($content);
|
|
|
|
|
+ if (!defined($data) || ref($data) ne 'HASH') {
|
|
|
|
|
+ die('Invalid JSON: ' . $content . "\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ if ($DEBUG > 2) {
|
|
|
|
|
+ print STDERR "Raw JSON data:\n" . prettyPrint($data, ' ') . "\n";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return $data;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+sub getChannel($) {
|
|
|
|
|
+ my ($user) = @_;
|
|
|
|
|
+
|
|
|
|
|
+ # Build, fetch, parse
|
|
|
|
|
+ my $url = buildURL($URL_PREFIX . $user, \%CHAN_PARAMS);
|
|
|
|
|
+ my $data = fetchParse($url);
|
|
|
|
|
+
|
|
|
|
|
+ if (!exists($data->{'entry'}) || ref($data->{'entry'}) ne 'HASH') {
|
|
|
|
|
+ die("Invalid channel data\n");
|
|
|
|
|
+ }
|
|
|
|
|
+ $data = $data->{'entry'};
|
|
|
|
|
+
|
|
|
|
|
+ # Extract the data we want
|
|
|
|
|
+ my %channel = (
|
|
|
|
|
+ 'id' => $data->{'yt$channelId'}->{'$t'},
|
|
|
|
|
+ 'title' => $data->{'author'}[0]->{'name'}->{'$t'},
|
|
|
|
|
+ 'date' => str2time($data->{'published'}->{'$t'}),
|
|
|
|
|
+ 'description' => $data->{'summary'}->{'$t'},
|
|
|
|
|
+ );
|
|
|
|
|
+ return \%channel;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
sub findVideos($) {
|
|
sub findVideos($) {
|
|
|
my ($user) = @_;
|
|
my ($user) = @_;
|
|
|
my %videos = ();
|
|
my %videos = ();
|
|
|
|
|
|
|
|
- # Build the base URL
|
|
|
|
|
|
|
+ # Prepare the static URL elements
|
|
|
my $baseURL = $URL_PREFIX . $user . $URL_SUFFIX;
|
|
my $baseURL = $URL_PREFIX . $user . $URL_SUFFIX;
|
|
|
-
|
|
|
|
|
- # And the static URL parameters
|
|
|
|
|
- my %params = %API_PARAMS;
|
|
|
|
|
|
|
+ my %params = %LIST_PARAMS;
|
|
|
|
|
|
|
|
# Loop through until we have all the entries
|
|
# Loop through until we have all the entries
|
|
|
my $index = 1;
|
|
my $index = 1;
|
|
|
my $itemCount = undef();
|
|
my $itemCount = undef();
|
|
|
LOOP:
|
|
LOOP:
|
|
|
{
|
|
{
|
|
|
- # Build the actual URL
|
|
|
|
|
- my $url = $baseURL . '?start-index=' . $index;
|
|
|
|
|
- foreach my $name (keys(%params)) {
|
|
|
|
|
- $url .= '&' . uri_escape($name) . '=' . uri_escape($params{$name});
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- # Fetch
|
|
|
|
|
- if ($DEBUG) {
|
|
|
|
|
- print STDERR 'Fetching list URL: ' . $url . "\n";
|
|
|
|
|
- }
|
|
|
|
|
- my $content = get($url);
|
|
|
|
|
- if (!defined($content) || length($content) < 10) {
|
|
|
|
|
- die('Invalid content from URL: ' . $url . "\n");
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ # Build, fetch, parse
|
|
|
|
|
+ $params{'start-index'} = $index;
|
|
|
|
|
+ my $url = buildURL($baseURL, \%params);
|
|
|
|
|
+ my $data = fetchParse($url);
|
|
|
|
|
|
|
|
- # Parse
|
|
|
|
|
- my $data = decode_json($content);
|
|
|
|
|
- if (!defined($data) || ref($data) ne 'HASH') {
|
|
|
|
|
- die('Invalid JSON: ' . $content . "\n");
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ # Grab the total count, so we know when to stop
|
|
|
if (!exists($data->{'data'})) {
|
|
if (!exists($data->{'data'})) {
|
|
|
- die('Invalid data set: ' . $content . "\n");
|
|
|
|
|
|
|
+ die("Invalid video list\n");
|
|
|
}
|
|
}
|
|
|
$data = $data->{'data'};
|
|
$data = $data->{'data'};
|
|
|
-
|
|
|
|
|
- # Grab the total count, so we know when to stop
|
|
|
|
|
if (!defined($itemCount) && exists($data->{'totalItems'})) {
|
|
if (!defined($itemCount) && exists($data->{'totalItems'})) {
|
|
|
$itemCount = $data->{'totalItems'};
|
|
$itemCount = $data->{'totalItems'};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
# Process each item
|
|
# Process each item
|
|
|
if (!exists($data->{'items'}) || ref($data->{'items'}) ne 'ARRAY') {
|
|
if (!exists($data->{'items'}) || ref($data->{'items'}) ne 'ARRAY') {
|
|
|
- die('Invalid item list: ' . $content . "\n");
|
|
|
|
|
|
|
+ die("Invalid video list\n");
|
|
|
}
|
|
}
|
|
|
my $items = $data->{'items'};
|
|
my $items = $data->{'items'};
|
|
|
my $offset = 0;
|
|
my $offset = 0;
|
|
@@ -351,9 +427,8 @@ sub findVideos($) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if ($DEBUG) {
|
|
if ($DEBUG) {
|
|
|
- print STDERR "\n\nFound " . scalar(keys(%videos)) . ' videos for YouTube user ' . $user . ":\n\t" . join("\n\t", keys(%videos)) . "\n\n";
|
|
|
|
|
|
|
+ print STDERR "\nFound " . scalar(keys(%videos)) . ' videos for YouTube user ' . $user . ":\n\t" . join("\n\t", keys(%videos)) . "\n";
|
|
|
if ($DEBUG > 1) {
|
|
if ($DEBUG > 1) {
|
|
|
- use PrettyPrint;
|
|
|
|
|
print STDERR prettyPrint(\%videos, "\t") . "\n";
|
|
print STDERR prettyPrint(\%videos, "\t") . "\n";
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|