Init-Files/bin/youtubedown
2020-04-30 09:43:23 -07:00

5981 lines
249 KiB
Perl
Executable file

#!/usr/bin/perl -w
# Copyright © 2007-2020 Jamie Zawinski <jwz@jwz.org>
#
# Permission to use, copy, modify, distribute, and sell this software and its
# documentation for any purpose is hereby granted without fee, provided that
# the above copyright notice appear in all copies and that both that
# copyright notice and this permission notice appear in supporting
# documentation. No representations are made about the suitability of this
# software for any purpose. It is provided "as is" without express or
# implied warranty.
#
# Given a YouTube, Vimeo, Instagram or Tumblr video URL,
# downloads the corresponding MP4 file. The name of the file will be derived
# from the title of the video.
#
# --title "STRING" Use this as the title instead.
# --prefix "STRING" Prepend the title with this.
# --suffix Append the video ID to each written file name.
# --out "FILE" Output to this exact file name, ignoring title, suffix.
# --progress Show a textual progress bar for downloads.
# --bwlimit Nkbps Throttle download speed.
#
# --size Instead of downloading it all, print video dimensions.
# This requires "ffmpeg".
#
# --list List the underlying URLs of a playlist.
# --list --list List IDs and titles of a playlist.
# --size --size List the sizes of each video of a playlist.
#
# --no-mux Only download pre-muxed videos, instead of sometimes
# downloading separate audio and video files, then combining
# them afterward with "ffmpeg". If you specify this option,
# you probably can't download anything higher resolution
# than 720p.
#
# --webm Download WebM files if those are higher resolution than
# MP4. Off by default because only VLC can play WebM.
#
# --webm-transcode Download WebM, but convert it to MP4. Off by default
# because it is very slow, however it is the only way to
# get 4K MP4s out of Youtube.
#
# Note: if you have ffmpeg < 2.2, upgrade to something less flaky.
#
# For playlists, it will download each video to its own file.
#
# You can also use this as a bookmarklet, so that you can have a toolbar
# button or bookmark that saves the video you are currently watching to
# your desktop. See https://www.jwz.org/hacks/youtubedown.cgi for instructions
# on how to do that.
#
# Created: 25-Apr-2007.
require 5;
use diagnostics;
use strict;
use IO::Socket;
use IO::Socket::SSL;
use IPC::Open3;
use HTML::Entities;
use Encode;
my $progname0 = $0;
my $progname = $0; $progname =~ s@.*/@@g;
my ($version) = ('$Revision: 1.1549 $' =~ m/\s(\d[.\d]+)\s/s);
# Without this, [:alnum:] doesn't work on non-ASCII.
use locale;
use POSIX qw(locale_h strftime);
setlocale(LC_ALL, "en_US");
my $verbose = 1;
my $append_suffix_p = 0;
my $webm_p = 0;
my $webm_transcode_p = 0;
my $http_proxy = undef;
$ENV{PATH} = "/opt/local/bin:$ENV{PATH}"; # for macports ffmpeg
my @video_extensions = ("mp4", "flv", "webm");
# Anything placed on this list gets unconditionally deleted when this
# script exits, even if abnormally.
#
my %rm_f;
END { rmf(); }
sub rmf() {
foreach my $f (sort keys %rm_f) {
print STDERR "$progname: rm $f\n" if ($verbose > 1);
unlink $f;
}
%rm_f = ();
}
sub signal_cleanup($) {
my ($s) = @_;
print STDERR "$progname: SIG$s\n" if ($verbose > 1);
exit (2); # This causes END{} to run.
# I'd like to propagate the signal to the caller, but I don't see how.
}
$SIG{HUP} = \&signal_cleanup;
$SIG{INT} = \&signal_cleanup;
$SIG{QUIT} = \&signal_cleanup;
$SIG{ABRT} = \&signal_cleanup;
$SIG{KILL} = \&signal_cleanup;
$SIG{TERM} = \&signal_cleanup;
my $noerror = 0;
sub error($) {
my ($err) = @_;
utf8::decode ($err); # Pack multi-byte UTF-8 back into wide chars.
if ($noerror) {
die "$err\n";
} else {
print STDERR "$progname: $err\n";
exit 1;
}
}
# For internal errors.
my $errorI = ("\n" .
"\n\tPlease report this URL to jwz\@jwz.org!" .
"\n\tBut make sure you have the latest version first:" .
"\n\thttps://www.jwz.org/hacks/#youtubedown" .
"\n" .
"\n\tIf this error is happening on *all* videos," .
"\n\tyou can assume that I am already aware of it." .
"\n" .
"\n");
my $error_whiteboard = ''; # for signature diagnostics
sub errorI($) {
my ($err) = @_;
if ($error_whiteboard) {
$error_whiteboard =~ s/^/\t/gm;
$err .= "\n\n" . $error_whiteboard;
$error_whiteboard = '';
}
$err .= $errorI;
error ($err);
}
sub url_quote($) {
my ($u) = @_;
$u =~ s|([^-a-zA-Z0-9.\@/_\r\n])|sprintf("%%%02X", ord($1))|ge;
return $u;
}
sub url_unquote($) {
my ($u) = @_;
$u =~ s/[+]/ /g;
$u =~ s/%([a-z0-9]{2})/chr(hex($1))/ige;
return $u;
}
# Converts &, <, >, " and any UTF8 characters to HTML entities.
# Does not convert '.
#
sub html_quote($) {
my ($s) = @_;
return HTML::Entities::encode_entities ($s,
# Exclude "=042 &=046 <=074 >=076
'^ \t\n\040\041\043-\045\047-\073\075\077-\176');
}
# Convert any HTML entities to Unicode characters.
#
sub html_unquote($) {
my ($s) = @_;
return HTML::Entities::decode_entities ($s);
}
sub fmt_size($) {
my ($size) = @_;
return "unknown size" unless defined ($size);
return ($size > 1024*1024 ? sprintf ("%.0f MB", $size/(1024*1024)) :
$size > 1024 ? sprintf ("%.0f KB", $size/1024) :
"$size bytes");
}
sub fmt_bps($) { # bits per sec, not bytes
my ($bps) = @_;
return ($bps > 1024*1024 ? sprintf ("%.1f Mbps", $bps/(1024*1024)) :
$bps > 1024 ? sprintf ("%.1f Kbps", $bps/1024) :
"$bps bps");
}
my $progress_ticks = 0;
my $progress_time = 0;
my $progress_rubout = '';
my $progress_last = 0;
sub draw_progress($$$) {
my ($ratio, $bps, $eof) = @_; # bits per sec, not bytes
my $cols = 64;
my $ticks = int($cols * $ratio);
my $cursep = (!($verbose > 4) &&
((($ENV{TERM} || 'dumb') ne 'dumb') ||
(($ENV{INSIDE_EMACS} || '') =~ m/comint/)));
my $now = time();
return if ($progress_time == $now && !$eof);
if ($now > $progress_last) {
$progress_last = $now;
my $pct = sprintf("%3d%% %s", 100 * $ratio, fmt_bps ($bps || 0));
$pct =~ s/^ /. /s;
my $L = length($pct);
my $OL = length($progress_rubout);
print STDERR $progress_rubout if ($OL && $cursep); # erase previous pct
$progress_rubout = "\b" x $L;
while ($ticks > $progress_ticks) {
print STDERR ".";
$progress_ticks++;
}
print STDERR $pct;
my $L2 = $OL - $L; # If the current pct is shorter, clear to EOL
print STDERR ((' ' x $L2) . ("\b" x $L2))
if ($L2 > 0 && $cursep);
print STDERR "\n" unless ($cursep);
}
print STDERR "\r" . (' ' x ($cols + 4)) . "\r" # erase line
if ($eof && $cursep);
$progress_time = $now;
$progress_ticks = 0 if ($eof || !$cursep);
$progress_rubout = '' if ($eof);
$progress_last = 0 if ($eof);
}
# Like sysread() but timesout and return undef if no data received in N secs.
# The buffer argument is a reference, not a string.
#
my $timeout_p = 0;
sub sysread_timeout($$$$) {
my ($S, $buf, $bufsiz, $timeout) = @_;
my $read = undef;
my $err = "$progname: $timeout seconds with no data\n";
eval {
local $SIG{ALRM} = sub {
$timeout_p = 1;
print STDERR $err if ($verbose);
die ($err)
};
alarm ($timeout);
$read = sysread ($S, $$buf, $bufsiz);
alarm (0);
};
if ($@) {
die unless ($@ eq $err);
}
return $read;
}
# Using HTTP "Connection: keep-alive" doesn't actually help much, because
# the video segments on the .googlevideo.com URLs explicitly do
# "Connection: close" on us. Still, this saves 3 or 4 connect() calls,
# which can be not-insignificant latency. It just could be a lot better
# if we could grab all of the segments on the same connection.
#
# I tried pipelining the connect() calls by loading the N+1 socket with
# O_NONBLOCK so the the TCP handshake would happen on N+1 while we were
# still reading from N, but that actually made things slightly slower rather
# than faster, since the connect() calls to Youtube's segment servers are
# actually pretty quick (under 20 milliseconds), and we can't pipeline or
# share the SSL handshaking (that's exactly what-the-fuck keep-alive is for!)
#
my $keepalive_p = 1;
my %keepalive; # { $hostname => $socket, ... }
# Loads the given URL, returns: $http, $head, $body,
# $bytes_read, $content_length, $document_length.
# Does not retry or process redirects.
#
sub get_url_1($;$$$$$$$) {
my ($url, $referer, $to_file, $bwlimit, $start_byte, $max_bytes,
$append_p, $progress_p) = @_;
error ("not an HTTP URL, try rtmpdump: $url") if ($url =~ m@^rtmp@i);
error ("not an HTTP URL: $url") unless ($url =~ m@^(https?|feed)://@i);
my $sysread_timeout = 30;
my ($proto, undef, $host, $path) = split(m@/@, $url, 4);
$path = "" unless defined ($path);
$path = "/$path";
my $port = ($host =~ s@:([^:/]*)$@@gs ? $1 : undef);
$port = ($proto eq 'https:' ? 443 : 80) unless $port;
my $oport = $port;
my $ohost = $host;
my $S = $keepalive_p ? $keepalive{"$proto://$host"} : undef;
if ($S) {
print STDERR "$progname: reusing connection: $host\n" if ($verbose > 2);
} else {
# If we were just using LWP::UserAgent, we wouldn't have to do all of this
# proxy crap (that library already handles it) but we use byte-ranges,
# don't always read a URL to completion, and want to display progress bars.
# LWP::UserAgent doesn't provide easily-usable APIs for that case, so, we
# hack the TCP connections more-or-less directly.
if ($http_proxy) {
(undef, undef, $host, undef) = split(m@/@, $http_proxy, 4);
$port = ($host =~ s@:([^:/]*)$@@gs ? $1 : undef);
# RFC7230: Full url "absolute-form" works, but the "origin-form" of
# a path (e.g. "/foo.txt") hides proxy use when using SSL.
$path = $url unless ($proto eq 'https:');
}
# This is the connection to the proxy (if using one) or the target host.
#
$S = IO::Socket::INET->new (PeerAddr => $host,
PeerPort => $port,
Proto => 'tcp',
Type => SOCK_STREAM,
);
error ("connect: $host:$port: $!") unless $S;
# If we are loading https through a proxy, put the proxy into tunnel mode.
#
# Note: this fails if the proxy *itself* is on https. In that case, we
# would need to bring up SSL on the connection to the proxy, then again
# on the interior CONNECT stream.
#
if ($http_proxy && $proto eq 'https:') {
my $hd = "CONNECT $ohost:$oport HTTP/1.0\r\n\r\n";
my @ha = split(/\r?\n/, $hd);
if ($verbose > 2) {
print STDERR " proxy send P $host:$port " . length($hd) ." bytes\n";
foreach (@ha) { print STDERR " ==> $_\n"; }
print STDERR " ==>\n";
}
syswrite ($S, $hd) || error ("syswrite proxy: $url: $!");
my $bufsiz = 1024;
my $buf = '';
$hd = '';
while (! $hd) {
if ($buf =~ m/^(.*?)\r?\n\r?\n(.*)$/s) {
($hd, $buf) = ($1, $2);
last;
}
my $buf2 = '';
my $size = sysread_timeout ($S, \$buf2, $bufsiz, $sysread_timeout);
print STDERR " proxy read P $size bytes\n"
if (defined($size) && $verbose > 2);
last if (!defined($size) || $size <= 0);
$buf .= $buf2;
}
@ha = split (/\r?\n/, $hd);
if ($verbose > 2) {
foreach (@ha) { print STDERR " <== $_\n"; }
print STDERR " <==\n";
}
error ("HTTP proxy error: $ha[0]\n")
unless ($ha[0] =~ m@^HTTP/[0-9.]+ 20\d@si);
}
# Some proxies suck, expect bad behavior like sending a body
$S->flush() || error ("Could not flush proxy socket: $!");
# Now we have a stream to the target host (which may be proxied or direct).
# Put that stream into SSL mode if the target host is https.
#
if ($proto eq 'https:') {
IO::Socket::SSL->start_SSL ($S,
# Ignore certificate errors
verify_hostname => 0,
SSL_verify_mode => 0,
SSL_verifycn_scheme => 'none',
# set hostname for SNI
SSL_hostname => $ohost,
)
|| error ("socket: SSL: $!");
}
$S->autoflush(1);
}
my $user_agent = "$progname/$version";
# Finally we are in straight HTTP land (but $path may be either "absolute"
# or "origin" form, as above.)
# (You'd think this should be HTTP/1.1 since we are using keep-alive,
# but that breaks things for some reason.)
#
my $hdrs = ("GET " . $path . " HTTP/1.0\r\n" .
"Host: $ohost\r\n" .
"User-Agent: $user_agent\r\n");
my @extra_headers = ();
push @extra_headers, "Referer: $referer" if ($referer);
push @extra_headers, "Connection: keep-alive" if ($keepalive_p);
# If we're only reading the first N bytes, don't ask for more.
#
if ($start_byte || $max_bytes) {
#
# 0-0 means return the first byte.
# 0-1 means return the first two bytes.
# 0- is the same as 0-EOF.
# 1- is the same as 1-EOF.
#
$start_byte = 0 unless defined ($start_byte);
my $end_byte = ($max_bytes
? $start_byte + $max_bytes - 1
: "");
push @extra_headers, "Range: bytes=$start_byte-$end_byte";
}
$hdrs .= join ("\r\n", @extra_headers, '') if (@extra_headers);
$hdrs .= "\r\n";
if ($verbose > 3) {
print STDERR "\n";
foreach (split('\r?\n', $hdrs)) {
print STDERR " ==> $_\n";
}
}
syswrite ($S, $hdrs) ||
error ('syswrite: ' . ($! || 'I/O error') . ": $host");
# Using max SSL frame sized (16384) chunks improves performance by
# avoiding SSL frame splitting on sysread() of IO::Socket::SSL.
my $bufsiz = 16384;
my $buf = '';
$bufsiz = int ($bwlimit / 8)
if ($bwlimit && int($bwlimit / 8) < $bufsiz);
# Read network buffers until we have the HTTP response line.
my $http = '';
while (! $http) {
if ($buf =~ m/^(.*?)\r?\n(.*)$/s) {
($http, $buf) = ($1, $2);
last;
}
my $buf2 = '';
my $size = sysread_timeout ($S, \$buf2, $bufsiz, $sysread_timeout);
print STDERR " read A $size\n" if ($verbose > 5);
last if (!defined($size) || $size <= 0);
$buf .= $buf2;
}
$http =~ s/[\r\n]+$//s;
print STDERR " <== $http\n" if ($verbose > 3);
# If the URL isn't there, don't write to the file.
$to_file = undef unless ($http =~ m@^HTTP/[0-9.]+ 20\d@si);
# Read network buffers until we have the response header block.
my $head = '';
while (! $head) {
if ($buf =~ m/^(.*?)\r?\n\r?\n(.*)$/s) {
($head, $buf) = ($1, $2);
last;
}
my $buf2 = '';
my $size = sysread_timeout ($S, \$buf2, $bufsiz, $sysread_timeout);
print STDERR " read B $size\n" if ($verbose > 5);
last if (!defined($size) || $size <= 0);
$buf .= $buf2;
}
if ($verbose > 3) {
foreach (split(/\n/, $head)) {
s/\r$//gs;
print STDERR " <== $_\n";
}
print STDERR " <== \n";
}
# If it's 302, we're going to just return the Location: header after
# reading to the end of the body, if any (to retain the keepalive pipeline).
# Typically 302 responses have Content-Length: 0, but not necessarily?
# And if it's an error, we don't want to write the error body into the
# output file.
#
my $ok_p = ($http =~ m@^HTTP/[0-9.]+ 20\d@si);
# Note that if we requested a byte range, this is the length of the range,
# not the length of the full document.
my ($cl) = ($head =~ m@^Content-Length: \s* (\d+) @mix);
if ($ok_p && ($start_byte || $max_bytes)) {
my ($s, $e, $cl2) = ($head =~ m@^Content-Range:
\s* bytes \s+
(\d+) \s* - \s*
(\d+) \s* / \s*
(\d+) \s* $@mix);
error ("attempting to resume download failed: $url\n$head")
unless defined($cl2);
error ("attempting to resume download failed: wrong start byte: $url")
unless ($s == $start_byte);
# In byte-ranges mode, Content-Length is the length of the chunk being
# returned; the document content-length is in the Content-Range header.
$cl = $cl2;
}
my $document_length = $cl;
$cl = $start_byte + $max_bytes
if ($cl && $max_bytes && $start_byte + $max_bytes < $cl);
$progress_p = 0 if (($cl || 0) <= 0);
my $out;
if ($to_file) {
# No, don't do this.
# utf8::encode($to_file); # Unpack wide chars into multi-byte UTF-8.
if ($to_file eq '-') {
open ($out, ">-");
binmode ($out);
} elsif (! $ok_p) {
# Don't touch the output file on error or redirect.
} elsif ($start_byte) {
$rm_f{$to_file} = 1;
open ($out, '>>:raw', $to_file) || error ("append $to_file: $!");
print STDERR "$progname: open \"$to_file\" @ $start_byte\n"
if ($verbose > 2);
} elsif ($append_p) {
$rm_f{$to_file} = 1;
open ($out, '>>:raw', $to_file) || error ("append $to_file: $!");
print STDERR "$progname: append \"$to_file\"\n"
if ($verbose > 2);
} else {
$rm_f{$to_file} = 1;
open ($out, '>:raw', $to_file) || error ("open $to_file: $!");
print STDERR "$progname: open \"$to_file\"\n" if ($verbose > 2);
}
# If we're proxying a download, also copy the document's headers.
#
if ($to_file eq '-') {
# Maybe if we nuke the Content-Type, that will stop Safari from
# opening the file by default. Answer: nope.
# $head =~ s@^(Content-Type:)[^\r\n]+@$1 application/octet-stream@gmi;
# Ok, maybe if we mark it as an attachment? Answer: still nope.
# $head = "Content-Disposition: attachment\r\n" . $head;
syswrite ($out, $head . "\n\n") || error ("syswrite stdout: $url: $!");
}
}
my $bytes = 0;
my $body = '';
my $start_time = time();
my $actual_bits_per_sec = 0;
if (!defined($cl) || $cl > 0) {
while (1) {
if ($buf eq '') {
my $size = sysread_timeout ($S, \$buf, $bufsiz, $sysread_timeout);
print STDERR " read C " . ($size || 'undef') .
" (" . ($start_byte + $bytes) . ")\n"
if ($verbose > 5);
last if (!defined($size) || $size <= 0);
}
if ($to_file && ($to_file eq '-' || $ok_p)) {
my $n = syswrite ($out, $buf);
error ("file $to_file: $!") if (($n || 0) <= 0);
#print STDERR " wrote $n\n" if ($verbose > 5);
} else {
$body .= $buf;
}
$bytes += length($buf);
$buf = '';
my $now = time();
my $elapsed = $now - $start_time;
$actual_bits_per_sec = $bytes * 8 / ($elapsed <= 0 ? 1 : $elapsed);
draw_progress (($start_byte + $bytes) / $document_length,
$actual_bits_per_sec, 0)
if ($progress_p);
# If we do a read while at EOF, sometimes Youtube hangs for ~30 seconds
# before sending back the EOF, so just stop reading as soon as we have
# reached the Content-Length or $max_bytes. (Oh hey, that's because of
# keep-alive. Duh.)
#
if ($cl && $start_byte + $bytes >= $cl) {
print STDERR " EOF (" . ($start_byte + $bytes) . " >= $cl)\n"
if ($verbose > 5);
last;
}
# If we're throttling our download speed, and we went over, hang back.
#
if ($bwlimit) {
my $tick = 0.1;
my $paused = 0;
while (1) {
last if ($actual_bits_per_sec <= $bwlimit);
select (undef, undef, undef, $tick);
$paused += $tick;
$now = time();
$elapsed = $now - $start_time;
#### It would be better for this to be measured over the last few
#### seconds, rather than measured from the beginning of the download,
#### so that a network drop doesn't cause it to try and "catch up".
$actual_bits_per_sec = $bytes * 8 / ($elapsed <= 0 ? 1 : $elapsed);
print STDERR "$progname: bwlimit: delay $paused\n" if ($verbose > 5);
error ("\"$to_file\" unexpectedly vanished!")
if ($to_file && !-f $to_file);
}
}
}
}
draw_progress (($cl ? ($start_byte + $bytes) / $document_length : 0),
$actual_bits_per_sec, 1)
if ($progress_p);
if ($to_file && !$ok_p) {
error ("\"$to_file\" unexpectedly vanished!") unless (-f $to_file);
print STDERR "$progname: close \"$to_file\"\n" if ($verbose > 2);
close $out || error ("close $to_file: $!");
}
if ($verbose > 3) {
if ($to_file) {
print STDERR " <== [ body ]: $bytes bytes " .
($append_p ? "appended " : "written") .
" to file \"$to_file\"\n";
} else {
print STDERR " <== [ body ]: $bytes bytes\n";
if ($verbose > 4 &&
$head =~ m@^Content-Type: \s* # Safe types to dump to stderr
( text/ |
application/json |
application/x-www- |
video/vnd\.mpeg\.dash\.mpd
)@mix) {
foreach (split(/\n/, $body)) {
s/\r$//gs;
print STDERR " <== $_\n";
}
}
}
}
if ($keepalive_p &&
!$timeout_p &&
$http &&
$head =~ m/^Connection: keep-alive/mi &&
$head =~ m/^Content-Length: /mi) {
print STDERR "$progname: keepalive: $host\n" if ($verbose > 2);
$keepalive{"$proto://$host"} = $S;
} else {
if ($keepalive_p && $verbose > 2) {
my $why = ($head =~ m/^Connection: close/mi ? 'explicit close' :
$head !~ m/^Content-Length:/mi ? 'no length' :
!$http ? 'null response' :
$timeout_p ? 'timed out' :
'implicit close');
print STDERR "$progname: no keepalive: $why: $host\n";
}
delete $keepalive{"$proto://$host"};
close $S;
}
$timeout_p = 0;
$http = 'HTTP/1.1 500 null response' unless $http;
# Check to see if a network failure truncated the file and warn.
# Caller will then resume the download using byte ranges.
#
if ($to_file &&
$cl &&
$start_byte + $bytes < $cl-1) {
my $pct = int (100 * ($start_byte + $bytes) / $cl);
$pct = sprintf ("%.2f", 100 * $bytes / $cl) if ($pct == 100);
print STDERR "$progname: got only $pct% (" .
($start_byte + $bytes) . " / $cl)" .
" of \"$to_file\", resuming...\n"
if ($verbose > 0);
}
if (! ($head =~ m/^Content-Length:/mi)) {
# Sometimes we don't get a length, but since we already read the data,
# we can fake it now, for the benefit of --progress.
$head .= "\nContent-Length: $bytes";
}
return ($http, $head, $body, $bytes, $cl, $document_length);
}
# Loads the given URL, processes redirects; retries dropped connections.
# Returns: $http, $head, $body, $final_redirected_url.
#
sub get_url($;$$$$$$$) {
my ($url, $referer, $to_file, $bwlimit, $max_bytes,
$append_p, $progress_p, $force_ranges_p) = @_;
my $orig_url = $url;
my $redirect_count = 0;
my $error_count = 0;
my $max_redirects = 20;
my $max_errors = 5;
my $total_bytes = 0;
my $start_byte = 0;
errorI ("force_ranges requires output file")
if ($force_ranges_p && !$to_file);
do {
$url =~ s/\#.*$//s; # Remove HTML anchor
# If $force_ranges_p is true, we always make multiple sub-range requests
# for a single document instead of reading the whole document in one
# request. This is because Youtube rate-limits these URLs, but there is
# a full-speed setup burst at the beginning. Empirically, the burst size
# seems to be around 16MB. So if we read a 100MB document with a single
# request, the first 16MB comes in fast, and the remaining 84MB comes in
# slow. If we make 7 different requests instead of 1, it's way faster
# even with the extra connect() latency because we get the setup burst
# on each one.
#
# Update, Apr 2019: burst size seems to be 10MB now.
#
my $burst_size = 1024*1024*10;
my $max_bytes_2 = (($force_ranges_p &&
(!defined($max_bytes) || $max_bytes > $burst_size))
? $burst_size
: $max_bytes);
print STDERR "$progname: GET $url" .
($max_bytes_2
? " $start_byte-" . ($start_byte + $max_bytes_2)
: '') . "\n"
if ($verbose == 3);
my ($http, $head, $body, $bytes, $cl, $cl2) =
get_url_1 ($url, $referer, $to_file, $bwlimit,
$start_byte, $max_bytes_2,
$append_p, $progress_p);
$total_bytes += $bytes;
$max_bytes -= $bytes if defined($max_bytes);
my $target_length = ($force_ranges_p ? $cl2 : $cl);
if ($force_ranges_p && $http =~ m@^HTTP/[0-9.]+ 20\d@si) {
# We are allowed as many force-ranges retries as necessary.
$error_count--;
}
if ($http =~ m@^HTTP/[0-9.]+ 30[123]@si) { # Redirects
my ($location) = ($head =~ m@^Location:[ \t]*([^\r\n]+)@mi);
if (! $location) {
$http = 'HTTP/1.1 500 no location header in 30x';
error ($http);
} elsif ($location =~ m@\bgoogle\.com/sorry/@s) {
# Short circuit Youtube's CAPCHA error instead of retrying
$http = 'HTTP/1.1 403 CAPCHA required: ' . $location;
error ($http);
} else {
print STDERR "$progname: redirect from $url to $location\n"
if ($verbose > 3);
$referer = $url;
$url = $location;
if ($url =~ m@^/@) {
$url = "$1$url" if ($referer =~ m@^(https?://[^/]+)@si);
} elsif (! ($url =~ m@^[a-z]+:@i)) {
$url = "$1$url" if ($referer =~ m@^(https?:)@si);
}
}
error ("too many redirects ($max_redirects) from $orig_url")
if ($redirect_count++ > $max_redirects);
} elsif (! ($http =~ m@^HTTP/[0-9.]+ 20\d@si)) { # Errors
if ($body =~ m@([^<>.:]*verify that you are a human[^<>.:]*)@si) {
# Vimeo: there's no coming back from this, don't retry.
$max_errors = $error_count+1;
}
if ($http =~ m@\b429\b@si) {
# "Too many requests". There's no coming back from this, don't retry.
$max_errors = $error_count+1;
}
return ($http, $head, $body, $url) # Return error to caller.
if (++$error_count >= $max_errors);
print STDERR "$progname: $http: retrying $url\n"
if ($verbose > 3);
} elsif (defined($target_length) && $total_bytes < $target_length) {
# Did not get all of the bytes we wanted; try to get more using
# byte-ranges, next time around the loop.
$start_byte = $total_bytes;
$append_p = 1;
$error_count++ if ($bytes <= 0); # Null response counts as error.
error ("too many retries ($max_errors) attempting to resume $orig_url")
if ($error_count++ > $max_errors);
print STDERR "$progname: got $start_byte of $total_bytes bytes;" .
" resuming $url\n"
if ($verbose > 3);
} else {
return ($http, $head, $body, $url); # 100%, or HTTP error.
}
} while (1);
}
sub check_http_status($$$$) {
my ($id, $url, $http, $err_p) = @_;
return 1 if ($http =~ m@^HTTP/[0-9.]+ 20\d@si);
errorI ("$id: $http: $url") if ($err_p > 1 && $verbose > 0);
error ("$id: $http: $url") if ($err_p);
return 0;
}
# Runs ffmpeg to determine dimensions of the given video file.
# (We only do this in verbose mode, or with --size.)
#
sub video_file_size($) {
my ($file) = @_;
# Sometimes ffmpeg gets stuck in a loop.
# Don't let it run for more than N CPU-seconds.
my $limit = "ulimit -t 10";
my $size = (stat($file))[7];
my @cmd = ("ffmpeg",
"-i", $file,
"-vframes", "0",
"-f", "null",
"/dev/null");
print STDERR "\n$progname: exec: '" . join("' '", @cmd) . "'\n"
if ($verbose > 3);
my $result = '';
{
my ($in, $out, $err);
$err = Symbol::gensym;
my $pid = eval { open3 ($in, $out, $err, @cmd) };
# If ffmpeg doesn't exist, or dumps core, just ignore it.
# There's nothing we can do about it anyway.
if ($pid) {
close ($in);
close ($out);
local $/ = undef; # read entire file
while (<$err>) {
$result .= $_;
}
waitpid ($pid, 0);
}
}
print STDERR "\n$result\n" if ($verbose > 3);
my ($w, $h, $abr) = (0, 0, 0);
($w, $h) = ($1, $2)
if ($result =~ m/^\s*Stream \#.* Video:.* (\d+)x(\d+),? /m);
$abr = $1
if ($result =~ m@^\s*Duration:.* bitrate: ([\d.]+ *[kmb/s]+)@m);
$abr =~ s@/s$@ps@si;
# I don't understand why ffmpeg will say different things for the
# complete file, versus for the first 380 KB of the file, e.g.:
#
# Duration: 00:06:41.75, start: 0.000000, bitrate: 7 kb/s
# Duration: 00:06:41.75, start: 0.000000, bitrate: 133 kb/s
return ($w, $h, $size, $abr);
}
sub which($) {
my ($cmd) = @_;
foreach my $dir (split (/:/, $ENV{PATH})) {
my $cmd2 = "$dir/$cmd";
return $cmd2 if (-x "$cmd2");
}
return undef;
}
# When MacOS web browsers download a file, they write metadata into the
# file's extended attributes saying where and when it was downloaded,
# which can be seen in "Get Info" in the Finder. We do that too, to
# make it easier to figure out the original URL that a video file came
# from.
#
# To extract it:
#
# xattr -px com.apple.metadata:kMDItemWhereFroms FILE |
# xxd -r -p | plutil -convert xml1 - -o -
#
# On Linux systems, freedesktop.org proposes "user.xdg.origin.url".
# That's what "curl --xattr" does. So we write that too.
#
# xattr -p user.xdg.origin.url FILE
#
sub write_file_metadata_url($$$) {
my ($file, $id, $url) = @_;
my $now = time();
my $xattr = which ("xattr");
my $plutil = which ("plutil");
my $mp4tags = which ("mp4tags"); # port install mp4v2
my $added = 0;
my $ok = 1;
if ($xattr) {
my $date = strftime ('%Y-%m-%dT%H:%M:%SZ', gmtime($now));
my $plhead = ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" .
"<!DOCTYPE plist PUBLIC" .
" \"-//Apple//DTD PLIST 1.0//EN\"" .
" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" .
"<plist version=\"1.0\">\n");
my $date_plist = ($plhead .
"<array>\n" .
"\t<date>$date</date>\n" .
"</array>\n" .
"</plist>");
my $url_plist = ($plhead .
"<array>\n" .
"\t<string>" . html_quote($url) . "</string>\n" .
"</array>\n" .
"</plist>");
# Convert the plists to binary form if possible. Probably not strictly
# necessary.
#
if ($plutil) {
foreach my $s ($date_plist, $url_plist) {
my ($in, $out, $err);
$err = Symbol::gensym;
my $pid = eval { open3 ($in, $out, $err,
($plutil,
'-convert', 'binary1',
'-', '-o', '-')) };
# If there are errors converting the plist, just ignore them.
# It's not critical to convert. Though an error would be weird.
if (!$pid) {
print STDERR "$progname: $id: $plutil: $!\n";
} else {
close ($err);
syswrite ($in, $s) || error ("$id: $plutil: $!");
close ($in);
local $/ = undef; # read entire file
my $s2 = '';
while (<$out>) {
$s2 .= $_;
}
$s = $s2 if $s2;
waitpid ($pid, 0);
if ($?) {
my $exit_value = $? >> 8;
my $signal_num = $? & 127;
my $dumped_core = $? & 128;
print STDERR "$progname: $id: $plutil: core dumped!"
if ($dumped_core);
print STDERR "$progname: $id: $plutil: signal $signal_num!"
if ($signal_num);
print STDERR "$progname: $id: $plutil: exited with $exit_value!"
if ($exit_value);
}
}
}
}
# I suppose setting the quarantine flag is also the proper thing to do.
#
my $quarantine = join (';', ('0002', # downloaded but never opened
sprintf("%08x", $now),
$progname,
"org.jwz.$progname"));
# Convert the data to hex, to shield nulls from xattr.
#
my $hexurl = $url;
foreach ($date_plist, $url_plist, $quarantine, $hexurl) {
s/(.)/{ sprintf("%02X ", ord($1)); }/gsex;
}
# Now run xattr for each attribute to dump it into the file.
#
error ("$file does not exist") unless (-f $file);
foreach ([$url_plist, 'com.apple.metadata:kMDItemWhereFroms'],
[$date_plist, 'com.apple.metadata:kMDItemDownloadedDate'],
[$quarantine, 'com.apple.quarantine'],
[$hexurl, 'user.xdg.origin.url']) {
my ($val, $key) = @$_;
my @cmd = ($xattr, "-w", "-x", $key, $val, $file);
print STDERR "\n$progname: exec: '" . join("' '", @cmd) . "'\n"
if ($verbose > 3);
system (@cmd);
$added = 1;
if ($?) {
$ok = 0;
my $exit_value = $? >> 8;
my $signal_num = $? & 127;
my $dumped_core = $? & 128;
print STDERR "$progname: $id: $cmd[0]: core dumped!\n"
if ($dumped_core);
print STDERR "$progname: $id: $cmd[0]: signal $signal_num!\n"
if ($signal_num);
print STDERR "$progname: $id: $cmd[0]: exited with $exit_value!\n"
if ($exit_value);
}
}
} elsif ($verbose > 1) {
print STDERR "$progname: $id: no metadata: xattr not found on \$PATH\n";
}
# If we can, also store the URL inside the file's metadata tags.
# This shows up in the "Video / Description" field in iTunes
# rather than in "Info / Comments".
#
if ($mp4tags && $file =~ m/\.mp4$/si) {
my @cmd = ($mp4tags, "-m", $url, $file);
print STDERR "\n$progname: exec: '" . join("' '", @cmd) . "'\n"
if ($verbose > 3);
my ($in, $out, $err);
$err = Symbol::gensym;
my $pid = eval { open3 ($in, $out, $err, @cmd) };
$added = 1;
if (!$pid) {
print STDERR "$progname: $id: $cmd[0]: $!\n";
$ok = 0;
} else {
close ($in);
close ($out);
close ($err);
waitpid ($pid, 0);
if ($?) {
$ok = 0;
my $exit_value = $? >> 8;
my $signal_num = $? & 127;
my $dumped_core = $? & 128;
if ($verbose > 0) {
# mp4tags fucks up not-infrequently. Be quieter about it.
print STDERR "$progname: $id: $cmd[0]: core dumped!\n"
if ($dumped_core);
print STDERR "$progname: $id: $cmd[0]: signal $signal_num!\n"
if ($signal_num);
print STDERR "$progname: $id: $cmd[0]: exited with $exit_value!\n"
if ($exit_value);
}
}
}
}
print STDERR "$progname: $id: added metadata\n"
if ($added && $ok && $verbose > 1);
}
# Downloads the first 380 KB of the URL, then runs ffmpeg to
# find out the dimensions of the video.
#
sub video_url_size($$;$$$) {
my ($id, $url, $ct, $bwlimit, $noerror) = @_;
my $tmp = $ENV{TMPDIR} || "/tmp";
my $ext = content_type_ext ($ct || '');
my $file = sprintf("$tmp/$progname-%08x.$ext", rand(0xFFFFFFFF));
# Need a lot of data to get size from 1080p.
#
# This used to be 320 KB, but I see 640x360 140 MB videos where we can't
# get the size without 680 KB.
#
# And now I see a 624 x 352, 180 MB, 50 minute video that gets
# "error reading header: -541478725" unless we read 910 KB.
#
my $bytes = 1024 * 1024;
# If it's a segmented URL, only grab data for the first few.
# But HEAD all of them (really, GET of 1 byte) to total up the
# final Content-Length; I don't see another way to find that.
#
my $max = 3;
my $segp = (ref($url) eq 'ARRAY');
my $size = 0;
my ($http, $head, $body);
if ($segp) {
my $i = 0;
my $cl = 0;
my $total = scalar (@$url);
foreach my $u2 (@$url) {
my $append_p = ($i > 0);
my $donep = ($i >= $max);
($http, $head, $body) = get_url ($u2, undef,
($donep ? undef : $file),
$bwlimit,
($donep ? 1 : $bytes),
$append_p);
# internal error if still 403 after retries.
return () unless check_http_status ($id,
"$url segment $i/$total: $u2",
$http,
($noerror ? 0 : 2));
my ($s2) = ($head =~ m@^Content-Range:\s* bytes \s+ [-\d]+ / (\d+) @mix);
($s2) = ($head =~ m@^Content-Length: \s* (\d+) @mix)
unless $s2;
$size += $s2 if defined($s2);
$i++;
}
} else {
($http, $head, $body) = get_url ($url, undef, $file, $bwlimit, $bytes);
# internal error if still 403
return () unless check_http_status ($id, $url, $http, $noerror ? 0 : 2);
}
($ct) = ($head =~ m@^Content-Type: \s* ( [^\s;]+ ) @mix);
($size) = ($head =~ m@^Content-Range: \s* bytes \s+ [-\d]+ / (\d+) @mix)
unless $size;
($size) = ($head =~ m@^Content-Length: \s* (\d+) @mix)
unless $size;
errorI ("$id: expected audio or video, got \"$ct\" in $url")
if ($ct =~ m/text/i);
$size = -1 unless defined($size); # WTF?
my ($w, $h, undef, $abr) = video_file_size ($file);
if (-f $file) {
print STDERR "$progname: rm \"$file\"\n" if ($verbose > 1);
unlink $file;
}
return ($w, $h, $size, $abr);
}
# 24-Jun-2013: When use_cipher_signature=True, the signature must be
# translated from lengths ranging from 82 to 88 back down to the
# original, unciphered length of 81 (40.40).
#
# This is not crypto or a hash, just a character-rearrangement cipher.
# Total security through obscurity. Total dick move.
#
# The implementation of this cipher used by the Youtube HTML5 video
# player lives in a Javascript file with a name like:
# https://s.ytimg.com/yts/jsbin/html5player-VERSION.js
# or https://s.ytimg.com/yts/jsbin/player-VERSION/base.js
# where VERSION changes periodically. Sometimes the algorithm in the
# Javascript changes, also. So we name each algorithm according to
# the VERSION string, and dispatch off of that. Each time Youtube
# rolls out a new html5player file, we will need to update the
# algorithm accordingly. See guess_cipher(), below. Run this
# script with --guess if it has changed. Run --guess --guess from
# cron to have it tell you only when there's a new cipher.
#
# So far, only three commands are used in the ciphers, so we can represent
# them compactly:
#
# - r = reverse the string;
# - sN = slice from character N to the end;
# - wN = swap 0th and Nth character.
#
# The first number is the "sts" parameter from the html5player file,
# which is a timestamp or other ID code corresponding to this algorithm.
# Requesting get_video_info with that number will return URLs using the
# corresponding cipher algorithm. Except sometimes those old 'sts' values
# stop working! See below.
#
# It used to be that the deciphered signature was always of the form:
# <40-chars, dot, 40-chars>, but that seems to no longer be the case as
# of Nov 2018 or so?
#
my %ciphers = (
# 'vflNzKG7n' => '135957536242 s3 r s2 r s1 r w67', # 30 Jan 2013
# 'vfllMCQWM' => '136089118952 s2 w46 r w27 s2 w43 s2 r', # 14 Feb 2013
# 'vflJv8FA8' => '136304655662 s1 w51 w52 r', # 11 Mar 2013
# 'vflR_cX32' => '1580 s2 w64 s3', # 11 Apr 2013
# 'vflveGye9' => '1582 w21 w3 s1 r w44 w36 r w41 s1', # 02 May 2013
# 'vflj7Fxxt' => '1583 r s3 w3 r w17 r w41 r s2', # 14 May 2013
# 'vfltM3odl' => '1584 w60 s1 w49 r s1 w7 r s2 r', # 23 May 2013
# 'vflDG7-a-' => '1586 w52 r s3 w21 r s3 r', # 06 Jun 2013
# 'vfl39KBj1' => '1586 w52 r s3 w21 r s3 r', # 12 Jun 2013
# 'vflmOfVEX' => '1586 w52 r s3 w21 r s3 r', # 21 Jun 2013
# 'vflJwJuHJ' => '1588 r s3 w19 r s2', # 25 Jun 2013
# 'vfl_ymO4Z' => '1588 r s3 w19 r s2', # 26 Jun 2013
# 'vfl26ng3K' => '15888 r s2 r', # 08 Jul 2013
# 'vflcaqGO8' => '15897 w24 w53 s2 w31 w4', # 11 Jul 2013
# 'vflQw-fB4' => '15902 s2 r s3 w9 s3 w43 s3 r w23', # 16 Jul 2013
# 'vflSAFCP9' => '15904 r s2 w17 w61 r s1 w7 s1', # 18 Jul 2013
# 'vflART1Nf' => '15908 s3 r w63 s2 r s1', # 22 Jul 2013
# 'vflLC8JvQ' => '15910 w34 w29 w9 r w39 w24', # 25 Jul 2013
# 'vflm_D8eE' => '15916 s2 r w39 w55 w49 s3 w56 w2', # 30 Jul 2013
# 'vflTWC9KW' => '15917 r s2 w65 r', # 31 Jul 2013
# 'vflRFcHMl' => '15921 s3 w24 r', # 04 Aug 2013
# 'vflM2EmfJ' => '15920 w10 r s1 w45 s2 r s3 w50 r', # 06 Aug 2013
# 'vflz8giW0' => '15919 s2 w18 s3', # 07 Aug 2013
# 'vfl_wGgYV' => '15923 w60 s1 r s1 w9 s3 r s3 r', # 08 Aug 2013
# 'vfl1HXdPb' => '15926 w52 r w18 r s1 w44 w51 r s1', # 12 Aug 2013
# 'vflkn6DAl' => '15932 w39 s2 w57 s2 w23 w35 s2', # 15 Aug 2013
# 'vfl2LOvBh' => '15933 w34 w19 r s1 r s3 w24 r', # 16 Aug 2013
# 'vfl-bxy_m' => '15936 w48 s3 w37 s2', # 20 Aug 2013
# 'vflZK4ZYR' => '15938 w19 w68 s1', # 21 Aug 2013
# 'vflh9ybst' => '15936 w48 s3 w37 s2', # 21 Aug 2013
# 'vflapUV9V' => '15943 s2 w53 r w59 r s2 w41 s3', # 27 Aug 2013
# 'vflg0g8PQ' => '15944 w36 s3 r s2', # 28 Aug 2013
# 'vflHOr_nV' => '15947 w58 r w50 s1 r s1 r w11 s3', # 30 Aug 2013
# 'vfluy6kdb' => '15953 r w12 w32 r w34 s3 w35 w42 s2', # 05 Sep 2013
# 'vflkuzxcs' => '15958 w22 w43 s3 r s1 w43', # 10 Sep 2013
# 'vflGNjMhJ' => '15956 w43 w2 w54 r w8 s1', # 12 Sep 2013
# 'vfldJ8xgI' => '15964 w11 r w29 s1 r s3', # 17 Sep 2013
# 'vfl79wBKW' => '15966 s3 r s1 r s3 r s3 w59 s2', # 19 Sep 2013
# 'vflg3FZfr' => '15969 r s3 w66 w10 w43 s2', # 24 Sep 2013
# 'vflUKrNpT' => '15973 r s2 r w63 r', # 25 Sep 2013
# 'vfldWnjUz' => '15976 r s1 w68', # 30 Sep 2013
# 'vflP7iCEe' => '15981 w7 w37 r s1', # 03 Oct 2013
# 'vflzVne63' => '15982 w59 s2 r', # 07 Oct 2013
# 'vflO-N-9M' => '15986 w9 s1 w67 r s3', # 09 Oct 2013
# 'vflZ4JlpT' => '15988 s3 r s1 r w28 s1', # 11 Oct 2013
# 'vflDgXSDS' => '15988 s3 r s1 r w28 s1', # 15 Oct 2013
# 'vflW444Sr' => '15995 r w9 r s1 w51 w27 r s1 r', # 17 Oct 2013
# 'vflK7RoTQ' => '15996 w44 r w36 r w45', # 21 Oct 2013
# 'vflKOCFq2' => '16 s1 r w41 r w41 s1 w15', # 23 Oct 2013
# 'vflcLL31E' => '16 s1 r w41 r w41 s1 w15', # 28 Oct 2013
# 'vflz9bT3N' => '16 s1 r w41 r w41 s1 w15', # 31 Oct 2013
# 'vfliZsE79' => '16010 r s3 w49 s3 r w58 s2 r s2', # 05 Nov 2013
# 'vfljOFtAt' => '16014 r s3 r s1 r w69 r', # 07 Nov 2013
# 'vflqSl9GX' => '16023 w32 r s2 w65 w26 w45 w24 w40 s2', # 14 Nov 2013
# 'vflFrKymJ' => '16023 w32 r s2 w65 w26 w45 w24 w40 s2', # 15 Nov 2013
# 'vflKz4WoM' => '16027 w50 w17 r w7 w65', # 19 Nov 2013
# 'vflhdWW8S' => '16030 s2 w55 w10 s3 w57 r w25 w41', # 21 Nov 2013
# 'vfl66X2C5' => '16031 r s2 w34 s2 w39', # 26 Nov 2013
# 'vflCXG8Sm' => '16031 r s2 w34 s2 w39', # 02 Dec 2013
# 'vfl_3Uag6' => '16034 w3 w7 r s2 w27 s2 w42 r', # 04 Dec 2013
# 'vflQdXVwM' => '16047 s1 r w66 s2 r w12', # 10 Dec 2013
# 'vflCtc3aO' => '16051 s2 r w11 r s3 w28', # 12 Dec 2013
# 'vflCt6YZX' => '16051 s2 r w11 r s3 w28', # 17 Dec 2013
# 'vflG49soT' => '16057 w32 r s3 r s1 r w19 w24 s3', # 18 Dec 2013
# 'vfl4cHApe' => '16059 w25 s1 r s1 w27 w21 s1 w39', # 06 Jan 2014
# 'vflwMrwdI' => '16058 w3 r w39 r w51 s1 w36 w14', # 06 Jan 2014
# 'vfl4AMHqP' => '16060 r s1 w1 r w43 r s1 r', # 09 Jan 2014
# 'vfln8xPyM' => '16080 w36 w14 s1 r s1 w54', # 10 Jan 2014
# 'vflVSLmnY' => '16081 s3 w56 w10 r s2 r w28 w35', # 13 Jan 2014
# 'vflkLvpg7' => '16084 w4 s3 w53 s2', # 15 Jan 2014
# 'vflbxes4n' => '16084 w4 s3 w53 s2', # 15 Jan 2014
# 'vflmXMtFI' => '16092 w57 s3 w62 w41 s3 r w60 r', # 23 Jan 2014
# 'vflYDqEW1' => '16094 w24 s1 r s2 w31 w4 w11 r', # 24 Jan 2014
# 'vflapGX6Q' => '16093 s3 w2 w59 s2 w68 r s3 r s1', # 28 Jan 2014
# 'vflLCYwkM' => '16093 s3 w2 w59 s2 w68 r s3 r s1', # 29 Jan 2014
# 'vflcY_8N0' => '16100 s2 w36 s1 r w18 r w19 r', # 30 Jan 2014
# 'vfl9qWoOL' => '16104 w68 w64 w28 r', # 03 Feb 2014
# 'vfle-mVwz' => '16103 s3 w7 r s3 r w14 w59 s3 r', # 04 Feb 2014
# 'vfltdb6U3' => '16106 w61 w5 r s2 w69 s2 r', # 05 Feb 2014
# 'vflLjFx3B' => '16107 w40 w62 r s2 w21 s3 r w7 s3', # 10 Feb 2014
# 'vfliqjKfF' => '16107 w40 w62 r s2 w21 s3 r w7 s3', # 13 Feb 2014
# 'ima-vflxBu-5R' => '16107 w40 w62 r s2 w21 s3 r w7 s3', # 13 Feb 2014
# 'ima-vflrGwWV9' => '16119 w36 w45 r s2 r', # 20 Feb 2014
# 'ima-vflCME3y0' => '16128 w8 s2 r w52', # 27 Feb 2014
# 'ima-vfl1LZyZ5' => '16128 w8 s2 r w52', # 27 Feb 2014
# 'ima-vfl4_saJa' => '16130 r s1 w19 w9 w57 w38 s3 r s2', # 01 Mar 2014
# 'ima-en_US-vflP9269H' => '16129 r w63 w37 s3 r w14 r', # 06 Mar 2014
# 'ima-en_US-vflkClbFb' => '16136 s1 w12 w24 s1 w52 w70 s2', # 07 Mar 2014
# 'ima-en_US-vflYhChiG' => '16137 w27 r s3', # 10 Mar 2014
# 'ima-en_US-vflWnCYSF' => '16142 r s1 r s3 w19 r w35 w61 s2', # 13 Mar 2014
# 'en_US-vflbT9-GA' => '16146 w51 w15 s1 w22 s1 w41 r w43 r', # 17 Mar 2014
# 'en_US-vflAYBrl7' => '16144 s2 r w39 w43', # 18 Mar 2014
# 'en_US-vflS1POwl' => '16145 w48 s2 r s1 w4 w35', # 19 Mar 2014
# 'en_US-vflLMtkhg' => '16149 w30 r w30 w39', # 20 Mar 2014
# 'en_US-vflbJnZqE' => '16151 w26 s1 w15 w3 w62 w54 w22', # 24 Mar 2014
# 'en_US-vflgd5txb' => '16151 w26 s1 w15 w3 w62 w54 w22', # 25 Mar 2014
# 'en_US-vflTm330y' => '16151 w26 s1 w15 w3 w62 w54 w22', # 26 Mar 2014
# 'en_US-vflnwMARr' => '16156 s3 r w24 s2', # 27 Mar 2014
# 'en_US-vflTq0XZu' => '16160 r w7 s3 w28 w52 r', # 31 Mar 2014
# 'en_US-vfl8s5-Vs' => '16158 w26 s1 w14 r s3 w8', # 01 Apr 2014
# 'en_US-vfl7i9w86' => '16158 w26 s1 w14 r s3 w8', # 02 Apr 2014
# 'en_US-vflA-1YdP' => '16158 w26 s1 w14 r s3 w8', # 03 Apr 2014
# 'en_US-vflZwcnOf' => '16164 w46 s2 w29 r s2 w51 w20 s1', # 07 Apr 2014
# 'en_US-vflFqBlmB' => '16164 w46 s2 w29 r s2 w51 w20 s1', # 08 Apr 2014
# 'en_US-vflG0UvOo' => '16164 w46 s2 w29 r s2 w51 w20 s1', # 09 Apr 2014
# 'en_US-vflS6PgfC' => '16170 w40 s2 w40 r w56 w26 r s2', # 10 Apr 2014
# 'en_US-vfl6Q1v_C' => '16172 w23 r s2 w55 s2', # 15 Apr 2014
# 'en_US-vflMYwWq8' => '16177 w51 w32 r s1 r s3', # 17 Apr 2014
# 'en_US-vflGC4r8Z' => '16184 w17 w34 w66 s3', # 24 Apr 2014
# 'en_US-vflyEvP6v' => '16189 s1 r w26', # 29 Apr 2014
# 'en_US-vflm397e5' => '16189 s1 r w26', # 01 May 2014
# 'en_US-vfldK8353' => '16192 r s3 w32', # 03 May 2014
# 'en_US-vflPTD6yH' => '16196 w59 s1 w66 s3 w10 r w55 w70 s1', # 06 May 2014
# 'en_US-vfl7KJl0G' => '16196 w59 s1 w66 s3 w10 r w55 w70 s1', # 07 May 2014
# 'en_US-vflhUwbGZ' => '16200 w49 r w60 s2 w61 s3', # 12 May 2014
# 'en_US-vflzEDYyE' => '16200 w49 r w60 s2 w61 s3', # 13 May 2014
# 'en_US-vflimfEzR' => '16205 r s2 w68 w28', # 15 May 2014
# 'en_US-vfl_nbW1R' => '16206 r w8 r s3', # 20 May 2014
# 'en_US-vfll7obaF' => '16212 w48 w17 s2', # 22 May 2014
# 'en_US-vfluBAJ91' => '16216 w13 s1 w39', # 27 May 2014
# 'en_US-vfldOnicU' => '16217 s2 r w7 w21 r', # 28 May 2014
# 'en_US-vflbbaSdm' => '16221 w46 r s3 w19 r s2 w15', # 03 Jun 2014
# 'en_US-vflIpxel5' => '16225 r w16 w35', # 04 Jun 2014
# 'en_US-vfloyxzv5' => '16232 r w30 s3 r s3 r', # 11 Jun 2014
# 'en_US-vflmY-xcZ' => '16230 w25 r s1 w49 w52', # 12 Jun 2014
# 'en_US-vflMVaJmz' => '16236 w12 s3 w56 r s2 r', # 17 Jun 2014
# 'en_US-vflgt97Vg' => '16240 r s1 r', # 19 Jun 2014
# 'en_US-vfl19qQQ_' => '16241 s2 w55 s2 r w39 s2 w5 r s3', # 23 Jun 2014
# 'en_US-vflws3c7_' => '16243 r s1 w52', # 24 Jun 2014
# 'en_US-vflPqsNqq' => '16243 r s1 w52', # 25 Jun 2014
# 'en_US-vflycBCEX' => '16247 w12 s1 r s3 w17 s1 w9 r', # 26 Jun 2014
# 'en_US-vflhZC-Jn' => '16252 w69 w70 s3', # 01 Jul 2014
# 'en_US-vfl9r3Wpv' => '16255 r s3 w57', # 07 Jul 2014
# 'en_US-vfl6UPpbU' => '16259 w37 r s1', # 08 Jul 2014
# 'en_US-vfl_oxbbV' => '16259 w37 r s1', # 09 Jul 2014
# 'en_US-vflXGBaUN' => '16259 w37 r s1', # 10 Jul 2014
# 'en_US-vflM1arS5' => '16262 s1 r w42 r s1 w27 r w54', # 11 Jul 2014
# 'en_US-vfl0Cbn9e' => '16265 w15 w44 r w24 s3 r w2 w50', # 14 Jul 2014
# 'en_US-vfl5aDZwb' => '16265 w15 w44 r w24 s3 r w2 w50', # 15 Jul 2014
# 'en_US-vflqZIm5b' => '16268 w1 w32 s1 r s3 r s3 r', # 17 Jul 2014
# 'en_US-vflBb0OQx' => '16272 w53 r w9 s2 r s1', # 22 Jul 2014
# 'en_US-vflCGk6yw/html5player' => '16275 s2 w28 w44 w26 w40 w64 r s1', # 24 Jul 2014
# 'en_US-vflNUsYw0/html5player' => '16280 r s3 w7', # 30 Jul 2014
# 'en_US-vflId8cpZ/html5player' => '16282 w30 w21 w26 s1 r s1 w30 w11 w20', # 31 Jul 2014
# 'en_US-vflEyBLiy/html5player' => '16283 w44 r w15 s2 w40 r s1', # 01 Aug 2014
# 'en_US-vflHkCS5P/html5player' => '16287 s2 r s3 r w41 s1 r s1 r', # 05 Aug 2014
# 'en_US-vflArxUZc/html5player' => '16289 r w12 r s3 w14 w61 r', # 07 Aug 2014
# 'en_US-vflCsMU2l/html5player' => '16292 r s2 r w64 s1 r s3', # 11 Aug 2014
# 'en_US-vflY5yrKt/html5player' => '16294 w8 r s2 w37 s1 w21 s3', # 12 Aug 2014
# 'en_US-vfl4b4S6W/html5player' => '16295 w40 s1 r w40 s3 r w47 r', # 13 Aug 2014
# 'en_US-vflLKRtyE/html5player' => '16298 w5 r s1 r s2 r', # 18 Aug 2014
# 'en_US-vflrSlC04/html5player' => '16300 w28 w58 w19 r s1 r s1 r', # 19 Aug 2014
# 'en_US-vflC7g_iA/html5player' => '16300 w28 w58 w19 r s1 r s1 r', # 20 Aug 2014
# 'en_US-vfll1XmaE/html5player' => '16303 r w9 w23 w29 w36 s2 r', # 21 Aug 2014
# 'en_US-vflWRK4zF/html5player' => '16307 r w63 r s3', # 26 Aug 2014
# 'en_US-vflQSzMIW/html5player' => '16309 r s1 w40 w70 s2 w28 s1', # 27 Aug 2014
# 'en_US-vfltYLx8B/html5player' => '16310 s3 w19 w24', # 29 Aug 2014
# 'en_US-vflWnljfv/html5player' => '16311 s2 w60 s3 w42 r w40 s2 w68 w20', # 02 Sep 2014
# 'en_US-vflDJ-wUY/html5player' => '16316 s2 w18 s2 w68 w15 s1 w45 s1 r', # 04 Sep 2014
# 'en_US-vfllxLx6Z/html5player' => '16309 r s1 w40 w70 s2 w28 s1', # 04 Sep 2014
# 'en_US-vflI3QYI2/html5player' => '16318 s3 w22 r s3 w19 s1 r', # 08 Sep 2014
# 'en_US-vfl-ZO7j_/html5player' => '16322 s3 w21 s1', # 09 Sep 2014
# 'en_US-vflWGRWFI/html5player' => '16324 r w27 r s1 r', # 12 Sep 2014
# 'en_US-vflJkTW89/html5player' => '16328 w12 s1 w67 r w39 w65 s3 r s1', # 15 Sep 2014
# 'en_US-vflB8RV2U/html5player' => '16329 r w26 r w28 w38 r s3', # 16 Sep 2014
# 'en_US-vflBFNwmh/html5player' => '16329 r w26 r w28 w38 r s3', # 17 Sep 2014
# 'en_US-vflE7vgXe/html5player' => '16331 w46 w22 r w33 r s3 w18 r s3', # 18 Sep 2014
# 'en_US-vflx8EenD/html5player' => '16334 w8 s3 w45 w46 s2 w29 w25 w56 w2', # 23 Sep 2014
# 'en_US-vflfgwjRj/html5player' => '16336 r s2 w56 r s3', # 24 Sep 2014
# 'en_US-vfl15y_l6/html5player' => '16334 w8 s3 w45 w46 s2 w29 w25 w56 w2', # 25 Sep 2014
# 'en_US-vflYqHPcx/html5player' => '16341 s3 r w1 r', # 30 Sep 2014
# 'en_US-vflcoeQIS/html5player' => '16344 s3 r w64 r s3 r w68', # 01 Oct 2014
# 'en_US-vflz7mN60/html5player' => '16345 s2 w16 w39', # 02 Oct 2014
# 'en_US-vfl4mDBLZ/html5player' => '16348 r w54 r s2 w49', # 06 Oct 2014
# 'en_US-vflKzH-7N/html5player' => '16348 r w54 r s2 w49', # 08 Oct 2014
# 'en_US-vflgoB_xN/html5player' => '16345 s2 w16 w39', # 09 Oct 2014
# 'en_US-vflPyRPNk/html5player' => '16353 r w34 w9 w56 r s3 r w30', # 12 Oct 2014
# 'en_US-vflG0qgr5/html5player' => '16345 s2 w16 w39', # 14 Oct 2014
# 'en_US-vflzDhHvc/html5player' => '16358 w26 s1 r w8 w24 w18 r s2 r', # 15 Oct 2014
# 'en_US-vflbeC7Ip/html5player' => '16359 r w21 r s2 r', # 16 Oct 2014
# 'en_US-vflBaDm_Z/html5player' => '16363 s3 w5 s1 w20 r', # 20 Oct 2014
# 'en_US-vflr38Js6/html5player' => '16364 w43 s1 r', # 21 Oct 2014
# 'en_US-vflg1j_O9/html5player' => '16365 s2 r s3 r s3 r w2', # 22 Oct 2014
# 'en_US-vflPOfApl/html5player' => '16371 s2 w38 r s3 r', # 28 Oct 2014
# 'en_US-vflMSJ2iW/html5player' => '16366 s2 r w4 w22 s2 r s2', # 29 Oct 2014
# 'en_US-vflckDNUK/html5player' => '16373 s3 r w66 r s3 w1 w12 r', # 30 Oct 2014
# 'en_US-vflKCJBPS/html5player' => '16374 w15 w2 s1 r s3 r', # 31 Oct 2014
# 'en_US-vflcF0gLP/html5player' => '16375 s3 w10 s1 r w28 s1 w40 w64 r', # 04 Nov 2014
# 'en_US-vflpRHqKc/html5player' => '16377 w39 r w48 r', # 05 Nov 2014
# 'en_US-vflbcuqSZ/html5player' => '16379 r s1 w27 s2 w5 w7 w51 r', # 06 Nov 2014
# 'en_US-vflHf2uUU/html5player' => '16379 r s1 w27 s2 w5 w7 w51 r', # 11 Nov 2014
# 'en_US-vfln6g5Eq/html5player' => '16385 w1 r s3 r s2 w10 s3 r', # 12 Nov 2014
# 'en_US-vflM7pYrM/html5player' => '16387 r s2 r w3 r w11 r', # 15 Nov 2014
# 'en_US-vflP2rJ1-/html5player' => '16387 r s2 r w3 r w11 r', # 18 Nov 2014
# 'en_US-vflXs0FWW/html5player' => '16392 w63 s1 r w46 s2 r s3', # 20 Nov 2014
# 'en_US-vflEhuJxd/html5player' => '16392 w63 s1 r w46 s2 r s3', # 21 Nov 2014
# 'en_US-vflp3wlqE/html5player' => '16396 w22 s3 r', # 24 Nov 2014
# 'en_US-vfl5_7-l5/html5player' => '16396 w22 s3 r', # 25 Nov 2014
# 'en_US-vfljnKokH/html5player' => '16400 s3 w15 s2 w30 w11', # 26 Nov 2014
# 'en_US-vflIlILAX/html5player' => '16407 r w7 w19 w38 s3 w41 s1 r w1', # 04 Dec 2014
# 'en_US-vflEegqdq/html5player' => '16407 r w7 w19 w38 s3 w41 s1 r w1', # 10 Dec 2014
# 'en_US-vflkOb-do/html5player' => '16407 r w7 w19 w38 s3 w41 s1 r w1', # 11 Dec 2014
# 'en_US-vfllt8pl6/html5player' => '16419 r w17 w33 w53', # 16 Dec 2014
# 'en_US-vflsXGZP2/html5player' => '16420 s3 w38 s1 w16 r w20 w69 s2 w15', # 18 Dec 2014
# 'en_US-vflw4H1P-/html5player' => '16427 w8 r s1', # 23 Dec 2014
# 'en_US-vflmgJnmS/html5player' => '16421 s3 w20 r w34 r s1 r', # 06 Jan 2015
# 'en_US-vfl86Quee/html5player' => '16450 s3 r w25 w29 r w17 s2 r', # 15 Jan 2015
# 'en_US-vfl19kCnd/html5player' => '16444 r w29 s1 r s1 r w4 w28', # 17 Jan 2015
# 'en_US-vflbHLA_P/html5player' => '16451 r w20 r w20 s2 r', # 20 Jan 2015
# 'en_US-vfl_ZlzZL/html5player' => '16455 w61 r s1 w31 w36 s1', # 22 Jan 2015
# 'en_US-vflbeV8LH/html5player' => '16455 w61 r s1 w31 w36 s1', # 26 Jan 2015
# 'en_US-vflhJatih/html5player' => '16462 s2 w44 r s3 w17 s1', # 28 Jan 2015
# 'en_US-vflvmwLwg/html5player' => '16462 s2 w44 r s3 w17 s1', # 29 Jan 2015
# 'en_US-vflljBsG4/html5player' => '16462 s2 w44 r s3 w17 s1', # 02 Feb 2015
# 'en_US-vflT5ziDW/html5player' => '16462 s2 w44 r s3 w17 s1', # 03 Feb 2015
# 'en_US-vflwImypH/html5player' => '16471 s3 r w23 s2 w29 r w44', # 05 Feb 2015
# 'en_US-vflQkSGin/html5player' => '16475 w70 r w66 s1 w70 w26 r w48', # 10 Feb 2015
# 'en_US-vflqnkATr/html5player' => '16475 w70 r w66 s1 w70 w26 r w48', # 11 Feb 2015
# 'en_US-vflZvrDTQ/html5player' => '16475 w70 r w66 s1 w70 w26 r w48', # 12 Feb 2015
# 'en_US-vflKjOTVq/html5player' => '16475 w70 r w66 s1 w70 w26 r w48', # 17 Feb 2015
# 'en_US-vfluEf7CP/html5player' => '16475 w70 r w66 s1 w70 w26 r w48', # 18 Feb 2015
# 'en_US-vflF2Mg88/html5player' => '16475 w70 r w66 s1 w70 w26 r w48', # 19 Feb 2015
# 'en_US-vflQTSOsS/html5player' => '16489 s3 r w23 s1 w19 w43 w36', # 24 Feb 2015
# 'en_US-vflbaqfRh/html5player' => '16489 s3 r w23 s1 w19 w43 w36', # 25 Feb 2015
# 'en_US-vflcL_htG/html5player' => '16491 w20 s3 w37 r', # 04 Mar 2015
# 'en_US-vflTbHYa9/html5player' => '16498 s3 w44 s1 r s1 r s3 r s3', # 04 Mar 2015
# 'en_US-vflT9SJ6t/html5player' => '16497 w66 r s3 w60', # 05 Mar 2015
# 'en_US-vfl6xsolJ/html5player' => '16503 s1 w4 s1 w39 s3 r', # 10 Mar 2015
# 'en_US-vflA6e-lH/html5player' => '16503 s1 w4 s1 w39 s3 r', # 13 Mar 2015
# 'en_US-vflu7AB7p/html5player' => '16503 s1 w4 s1 w39 s3 r', # 16 Mar 2015
# 'en_US-vflQb7e_A/html5player' => '16510 w19 w35 r s2 r s1 w64 s2 w53', # 18 Mar 2015
# 'en_US-vflicH9X6/html5player' => '16510 w19 w35 r s2 r s1 w64 s2 w53', # 20 Mar 2015
# 'en_US-vflvDDxpc/html5player' => '16510 w19 w35 r s2 r s1 w64 s2 w53', # 23 Mar 2015
# 'en_US-vflSp2y2y/html5player' => '16510 w19 w35 r s2 r s1 w64 s2 w53', # 24 Mar 2015
# 'en_US-vflFAPa9H/html5player' => '16510 w19 w35 r s2 r s1 w64 s2 w53', # 25 Mar 2015
# 'en_US-vflImsVHZ/html5player' => '16518 r w1 r w17 s2 r', # 30 Mar 2015
# 'en_US-vfllLRozy/html5player' => '16518 r w1 r w17 s2 r', # 31 Mar 2015
# 'en_US-vfldudhuW/html5player' => '16518 r w1 r w17 s2 r', # 02 Apr 2015
# 'en_US-vfl20EdcH/html5player' => '16511 w12 w18 s1 w60', # 06 Apr 2015
# 'en_US-vflCiLqoq/html5player' => '16511 w12 w18 s1 w60', # 07 Apr 2015
# 'en_US-vflOOhwh5/html5player' => '16518 r w1 r w17 s2 r', # 09 Apr 2015
# 'en_US-vflUPVjIh/html5player' => '16511 w12 w18 s1 w60', # 09 Apr 2015
# 'en_US-vfleI-biQ/html5player' => '16519 w39 s3 r s1 w36', # 13 Apr 2015
# 'en_US-vflWLYnud/html5player' => '16538 r w41 w65 w11 r', # 14 Apr 2015
# 'en_US-vflCbhV8k/html5player' => '16538 r w41 w65 w11 r', # 15 Apr 2015
# 'en_US-vflXIPlZ4/html5player' => '16538 r w41 w65 w11 r', # 16 Apr 2015
# 'en_US-vflJ97NhI/html5player' => '16538 r w41 w65 w11 r', # 20 Apr 2015
# 'en_US-vflV9R5dM/html5player' => '16538 r w41 w65 w11 r', # 21 Apr 2015
# 'en_US-vflkH_4LI/html5player' => '16546 w13 s1 w4 s2 r s2 w25', # 22 Apr 2015
# 'en_US-vflfy61br/html5player' => '16546 w13 s1 w4 s2 r s2 w25', # 23 Apr 2015
# 'en_US-vfl1r59NI/html5player' => '16548 r w42 s1 r w29 r w2 s2 r',# 28 Apr 2015
# 'en_US-vfl98hSpx/html5player' => '16548 r w42 s1 r w29 r w2 s2 r',# 29 Apr 2015
# 'en_US-vflheTb7D/html5player' => '16554 r s1 w40 s2 r w6 s3 w60',# 30 Apr 2015
# 'en_US-vflnbdC7j/html5player' => '16555 w52 w25 w62 w51 w2 s2 r s1',# 04 May 2015
# 'new-en_US-vfladkLoo/html5player-new' => '16555 w52 w25 w62 w51 w2 s2 r s1',# 05 May 2015
# 'en_US-vflTjpt_4/html5player' => '16560 w14 r s1 w37 w61 r', # 07 May 2015
# 'en_US-vflN74631/html5player' => '16560 w14 r s1 w37 w61 r', # 08 May 2015
# 'en_US-vflj7H3a2/html5player' => '16560 w14 r s1 w37 w61 r', # 12 May 2015
# 'en_US-vflQbG2p4/html5player' => '16560 w14 r s1 w37 w61 r', # 12 May 2015
# 'en_US-vflHV7Wup/html5player' => '16560 w14 r s1 w37 w61 r', # 13 May 2015
# 'en_US-vflCbZ69_/html5player' => '16574 w3 s3 w45 r w3 w2 r w13 r',# 20 May 2015
# 'en_US-vflugm_Hi/html5player' => '16574 w3 s3 w45 r w3 w2 r w13 r',# 21 May 2015
# 'en_US-vfl3tSKxJ/html5player' => '16577 w37 s3 w57 r w5 r w13 r',# 26 May 2015
# 'en_US-vflE8_7k0/html5player' => '16582 r w41 s3 w69 s1 w66 r w27 s2',# 28 May 2015
# 'en_US-vflmxRINy/html5player' => '16582 r w41 s3 w69 s1 w66 r w27 s2',# 01 Jun 2015
# 'en_US-vflQEtHy6/html5player' => '16582 r w41 s3 w69 s1 w66 r w27 s2',# 02 Jun 2015
# 'en_US-vflRqg76I/html5player' => '16582 r w41 s3 w69 s1 w66 r w27 s2',# 03 Jun 2015
# 'en_US-vfloIm75c/html5player' => '16582 r w41 s3 w69 s1 w66 r w27 s2',# 04 Jun 2015
# 'en_US-vfl0JH6Oo/html5player' => '16582 r w41 s3 w69 s1 w66 r w27 s2',# 08 Jun 2015
# 'en_US-vflHvL0kQ/html5player' => '16582 r w41 s3 w69 s1 w66 r w27 s2',# 09 Jun 2015
# 'new-en_US-vflGBorXT/html5player-new' => '16582 r w41 s3 w69 s1 w66 r w27 s2',# 10 Jun 2015
# 'en_US-vfl4Y6g4o/html5player' => '16582 r w41 s3 w69 s1 w66 r w27 s2',# 11 Jun 2015
# 'en_US-vflKAbZ28/html5player' => '16597 s3 r s2', # 15 Jun 2015
# 'en_US-vflM5YBLT/html5player' => '16602 s2 w25 w14 s1 r', # 17 Jun 2015
# 'en_US-vflnSSUZV/html5player' => '16603 w20 s2 w11 s3 r s1 w2 w15',# 18 Jun 2015
# 'en_US-vfla1HjWj/html5player' => '16603 w20 s2 w11 s3 r s1 w2 w15',# 22 Jun 2015
# 'en_US-vflPcWTEd/html5player' => '16603 w20 s2 w11 s3 r s1 w2 w15',# 23 Jun 2015
# 'en_US-vfljL8ofl/html5player' => '16609 w29 r s1 r w59 r w45', # 25 Jun 2015
# 'en_US-vflUXoyA8/html5player' => '16609 w29 r s1 r w59 r w45', # 29 Jun 2015
# 'en_US-vflzomeEU/html5player' => '16609 w29 r s1 r w59 r w45', # 30 Jun 2015
# 'en_US-vflihzZsw/html5player' => '16617 s3 r s3 w17', # 07 Jul 2015
# 'en_US-vfld2QbH7/html5player' => '16623 w58 w46 s1 w9 r w54 s2 r w55',# 08 Jul 2015
# 'en_US-vflVsMRd_/html5player' => '16623 w58 w46 s1 w9 r w54 s2 r w55',# 09 Jul 2015
# 'en_US-vflp6cSzi/html5player' => '16625 w52 w23 s1 r s2 r s2 r',# 16 Jul 2015
# 'en_US-vflr_ZqiK/html5player' => '16625 w52 w23 s1 r s2 r s2 r',# 20 Jul 2015
# 'en_US-vflDv401v/html5player' => '16636 r w68 w58 r w28 w44 r', # 21 Jul 2015
# 'en_US-vflP7pyW6/html5player' => '16636 r w68 w58 r w28 w44 r', # 22 Jul 2015
# 'en_US-vfly-Z1Od/html5player' => '16636 r w68 w58 r w28 w44 r', # 23 Jul 2015
# 'en_US-vflSxbpbe/html5player' => '16636 r w68 w58 r w28 w44 r', # 27 Jul 2015
# 'en_US-vflGx3XCd/html5player' => '16636 r w68 w58 r w28 w44 r', # 29 Jul 2015
# 'new-en_US-vflIgTSdc/html5player-new' => '16648 r s2 r w43 w41 w8 r w67 r',# 03 Aug 2015
# 'new-en_US-vflnk2PHx/html5player-new' => '16651 r w32 s3 r s1 r',# 06 Aug 2015
# 'new-en_US-vflo_te46/html5player-new' => '16652 r s2 w27 s1', # 06 Aug 2015
# 'new-en_US-vfllZzMNK/html5player-new' => '16657 w11 w29 w63 r w45 w34 s2',# 11 Aug 2015
# 'new-en_US-vflxgfwPf/html5player-new' => '16657 w11 w29 w63 r w45 w34 s2',# 13 Aug 2015
# 'new-en_US-vflTSd4UU/html5player-new' => '16657 w11 w29 w63 r w45 w34 s2',# 14 Aug 2015
# 'new-en_US-vfl2Ys-gC/html5player-new' => '16657 w11 w29 w63 r w45 w34 s2',# 15 Aug 2015
# 'new-en_US-vflRWS2p7/html5player-new' => '16657 w11 w29 w63 r w45 w34 s2',# 19 Aug 2015
# 'new-en_US-vflVBD1Nz/html5player-new' => '16657 w11 w29 w63 r w45 w34 s2',# 20 Aug 2015
# 'new-en_US-vflJVflpM/html5player-new' => '16667 r s1 r w8 r w5 s2 w30 w66',# 24 Aug 2015
# 'en_US-vfleu-UMC/html5player' => '16667 r s1 r w8 r w5 s2 w30 w66',# 26 Aug 2015
# 'new-en_US-vflOWWv0e/html5player-new' => '16667 r s1 r w8 r w5 s2 w30 w66',# 26 Aug 2015
# 'new-en_US-vflyGTTiE/html5player-new' => '16674 w68 s3 w66 s1 r',# 01 Sep 2015
# 'new-en_US-vflCeB3p5/html5player-new' => '16674 w68 s3 w66 s1 r',# 02 Sep 2015
# 'new-en_US-vflhlPTtB/html5player-new' => '16682 w40 s3 w53 w11 s3 r s3 w16 r',# 09 Sep 2015
# 'new-en_US-vflSnomqH/html5player-new' => '16689 w56 w12 r w26 r',# 16 Sep 2015
# 'new-en_US-vflkiOBi0/html5player-new' => '16696 w55 w69 w61 s2 r',# 22 Sep 2015
# 'new-en_US-vflpNjqAo/html5player-new' => '16696 w55 w69 w61 s2 r',# 22 Sep 2015
# 'new-en_US-vflOdTWmK/html5player-new' => '16696 w55 w69 w61 s2 r',# 23 Sep 2015
# 'new-en_US-vfl9jbnCC/html5player-new' => '16703 s1 r w18 w67 r s3 r',# 29 Sep 2015
# 'new-en_US-vflyM0pli/html5player-new' => '16696 w55 w69 w61 s2 r',# 29 Sep 2015
# 'new-en_US-vflJLt_ns/html5player-new' => '16708 w19 s2 r s2 w48 r s2 r',# 30 Sep 2015
# 'new-en_US-vflqLE6s6/html5player-new' => '16708 w19 s2 r s2 w48 r s2 r',# 02 Oct 2015
# 'new-en_US-vflzRMCkZ/html5player-new' => '16711 r s3 r s2 w62 w25 s1 r',# 04 Oct 2015
# 'new-en_US-vflIUNjzZ/html5player-new' => '16711 r s3 r s2 w62 w25 s1 r',# 08 Oct 2015
# 'new-en_US-vflOw5Ej1/html5player-new' => '16711 r s3 r s2 w62 w25 s1 r',# 08 Oct 2015
# 'new-en_US-vflq2mOFv/html5player-new' => '16714 r w37 r w19 r s3 r w5',# 12 Oct 2015
# 'new-en_US-vfl8AWn6F/html5player-new' => '16714 r w37 r w19 r s3 r w5',# 13 Oct 2015
# 'new-en_US-vflEA2BSM/html5player-new' => '16714 r w37 r w19 r s3 r w5',# 14 Oct 2015
# 'new-en_US-vflt2Xpp6/html5player-new' => '16717 r s1 w14', # 15 Oct 2015
# 'new-en_US-vflDpriqR/html5player-new' => '16714 r w37 r w19 r s3 r w5',# 15 Oct 2015
# 'new-en_US-vflptVjJB/html5player-new' => '16723 s2 r s3 w54 w60 w55 w65',# 21 Oct 2015
# 'new-en_US-vflmR8A04/html5player-new' => '16725 w28 s2 r', # 23 Oct 2015
# 'new-en_US-vflx6L8FI/html5player-new' => '16735 r s2 r w65 w1 s1',# 27 Oct 2015
# 'new-en_US-vflYZP7XE/html5player-new' => '16734 s1 r s1 w56 w46 s2 r',# 27 Oct 2015
# 'new-en_US-vflQZZsER/html5player-new' => '16734 s1 r s1 w56 w46 s2 r',# 29 Oct 2015
# 'new-en_US-vflsLAYSi/html5player-new' => '16734 s1 r s1 w56 w46 s2 r',# 29 Oct 2015
# 'new-en_US-vflZWDr6u/html5player-new' => '16734 s1 r s1 w56 w46 s2 r',# 02 Nov 2015
# 'new-en_US-vflJoRj2J/html5player-new' => '16742 w69 w47 r s1 r s1 r w43 s2',# 03 Nov 2015
# 'new-en_US-vflFSFCN-/html5player-new' => '16734 s1 r s1 w56 w46 s2 r',# 04 Nov 2015
# 'new-en_US-vfl6mEKMp/html5player-new' => '16734 s1 r s1 w56 w46 s2 r',# 05 Nov 2015
#'player-en_US-vflJENbn4/base' => '16748 s1 w31 r', # 12 Nov 2015
# 'player-en_US-vfltBCT02/base' => '16756 r s2 r w18 w62 w45 s1', # 17 Nov 2015
# 'player-en_US-vfl0w9xAB/base' => '16756 r s2 r w18 w62 w45 s1', # 17 Nov 2015
# 'player-en_US-vflCIicNM/base' => '16759 w2 s3 r w38 w21 w58', # 20 Nov 2015
# 'player-en_US-vflUpjAy9/base' => '16758 w26 s3 r s3 r s3 w61 s3 r',# 23 Nov 2015
# 'player-en_US-vflFEzfy7/base' => '16758 w26 s3 r s3 r s3 w61 s3 r',# 24 Nov 2015
# 'player-en_US-vfl_RJZIW/base' => '16770 w3 w2 s3 w39 s2 r s2', # 01 Dec 2015
# 'player-en_US-vfln_PDe6/base' => '16770 w3 w2 s3 w39 s2 r s2', # 03 Dec 2015
# 'player-en_US-vflx9OkTA/base' => '16772 s2 w50 r w15 w66 s3', # 07 Dec 2015
# 'player-en_US-vflPRjCOu/base' => '16776 r s1 r w31 s1', # 08 Dec 2015
# 'player-en_US-vflOIF62G/base' => '16776 r s1 r w31 s1', # 10 Dec 2015
# 'player-en_US-vfl2sXoyn/base' => '16777 w13 r s3 w2 r s3 w36', # 10 Dec 2015
# 'player-en_US-vflF6iOW5/base' => '16777 w13 r s3 w2 r s3 w36', # 11 Dec 2015
# 'player-en_US-vfl_a6AWr/base' => '16777 w13 r s3 w2 r s3 w36', # 14 Dec 2015
# 'player-en_US-vflpPblA7/base' => '16777 w13 r s3 w2 r s3 w36', # 15 Dec 2015
# 'player-en_US-vflktcH0f/base' => '16777 w13 r s3 w2 r s3 w36', # 16 Dec 2015
# 'player-en_US-vflXJM_5_/base' => '16777 w13 r s3 w2 r s3 w36', # 17 Dec 2015
# 'player-en_US-vflrSqbyh/base' => '16777 w13 r s3 w2 r s3 w36', # 20 Dec 2015
# 'player-en_US-vflnrstgx/base' => '16777 w13 r s3 w2 r s3 w36', # 22 Dec 2015
# 'player-en_US-vflbZPqYk/base' => '16804 r w50 w8 s2 w40 w64 s1',# 05 Jan 2016
# 'player-en_US-vfl2TFPXm/base' => '16804 r w50 w8 s2 w40 w64 s1',# 06 Jan 2016
# 'player-en_US-vflra1XvP/base' => '16806 s1 r w65 s3 r', # 07 Jan 2016
# 'player-en_US-vfljksafM/base' => '16806 s1 r w65 s3 r', # 11 Jan 2016
# 'player-en_US-vfl844Wcq/base' => '16806 s1 r w65 s3 r', # 12 Jan 2016
# 'player-en_US-vflGR-A-c/base' => '16806 s1 r w65 s3 r', # 14 Jan 2016
# 'player-en_US-vflIfVKII/base' => '16816 s2 w66 r', # 19 Jan 2016
# 'player-en_US-vfl1SLb2X/base' => '16819 s3 r w29 s1 r s1 w54 r w48',# 20 Jan 2016
# 'player-en_US-vfl7CQfyl/base' => '16819 s3 r w29 s1 r s1 w54 r w48',# 22 Jan 2016
# 'player-en_US-vfl0zK-iw/base' => '16819 s3 r w29 s1 r s1 w54 r w48',# 22 Jan 2016
# 'player-en_US-vfl4ZhWmu/base' => '16825 w12 s1 w47 s2 r s1', # 26 Jan 2016
# 'player-en_US-vflYjf147/base' => '16826 s1 r s2 r w50 r', # 27 Jan 2016
# 'player-en_US-vfl66BZ3R/base' => '16826 s1 r s2 r w50 r', # 28 Jan 2016
# 'player-en_US-vflpwz3pO/base' => '16828 w60 w36 w43 r', # 01 Feb 2016
# 'player-en_US-vflwvK3-x/base' => '16832 r w67 w1 r s1 w17', # 03 Feb 2016
# 'player-en_US-vfl93P520/base' => '16832 r w67 w1 r s1 w17', # 04 Feb 2016
# 'player-en_US-vflj1re2B/base' => '16835 s1 r s3 w69 r s3 w53', # 08 Feb 2016
# 'player-en_US-vflpN2vEY/base' => '16836 w16 r s3 r', # 10 Feb 2016
# 'player-en_US-vflCdE8nM/base' => '16841 r w51 s3 r s3 w6 w24 r w21',# 11 Feb 2016
# 'player-en_US-vfl329t6E/base' => '16846 s3 w27 r s2 w29 s2 r s3',# 16 Feb 2016
# 'player-en_US-vflGk0Qy7/base' => '16846 s3 w27 r s2 w29 s2 r s3',# 17 Feb 2016
# 'player-en_US-vfligMRZC/base' => '16849 w4 w3 r w50 r s1 w20 s1',# 18 Feb 2016
# 'player-en_US-vfldIygzk/base' => '16850 w48 r s1 r', # 20 Feb 2016
# 'player-en_US-vflksMPCE/base' => '16853 s2 w61 s2', # 23 Feb 2016
# 'player-en_US-vflEGP5iK/base' => '16849 w4 w3 r w50 r s1 w20 s1',# 23 Feb 2016
# 'player-en_US-vflRVQlNU/base' => '16856 w44 w49 r', # 25 Feb 2016
# 'player-en_US-vflKlzoBL/base' => '16855 w54 r s1 w52 s3 r w16 r',# 28 Feb 2016
# 'player-en_US-vfl_cdzrt/base' => '16855 w54 r s1 w52 s3 r w16 r',# 01 Mar 2016
# 'player-en_US-vflteKQR7/base' => '16861 r w40 s2', # 04 Mar 2016
# 'player-en_US-vfltwl-FJ/base' => '16864 w42 r w14 s3 r s1 r s2',# 08 Mar 2016
# 'player-en_US-vfl6PWeOD/base' => '16864 w42 r w14 s3 r s1 r s2',# 10 Mar 2016
# 'player-en_US-vflcZVscy/base' => '16873 s1 w55 w32 w39 r s3 r w66 s3',# 14 Mar 2016
# 'player-en_US-vflXE5o5C/base' => '16873 s1 w55 w32 w39 r s3 r w66 s3',# 15 Mar 2016
# 'player-en_US-vfl1858es/base' => '16873 s1 w55 w32 w39 r s3 r w66 s3',# 16 Mar 2016
# 'player-en_US-vflKkAVgb/base' => '16873 s1 w55 w32 w39 r s3 r w66 s3',# 17 Mar 2016
# 'player-en_US-vflpmpoFG/base' => '16881 r w70 s2 w53 s1', # 22 Mar 2016
# 'player-en_US-vfl1uoDql/base' => '16881 r w70 s2 w53 s1', # 24 Mar 2016
# 'player-en_US-vfl9rzyi6/base' => '16884 w19 w32 w47 w41 w3 w56 r',# 29 Mar 2016
# 'player-en_US-vflEHWF5a/base' => '16884 w19 w32 w47 w41 w3 w56 r',# 31 Mar 2016
# 'player-en_US-vfl6tDF0R/base' => '16890 s3 r w31 w23 w29', # 31 Mar 2016
# 'player-en_US-vfljAl26P/base' => '16890 s3 r w31 w23 w29', # 01 Apr 2016
# 'player-en_US-vfl9xTY8I/base' => '16892 s1 r s3 w37 w43 w20', # 04 Apr 2016
# 'player-en_US-vfls3wurZ/base' => '16892 s1 r s3 w37 w43 w20', # 05 Apr 2016
# 'player-en_US-vfli5QvRo/base' => '16892 s1 r s3 w37 w43 w20', # 06 Apr 2016
# 'player-en_US-vfllNvdW4/base' => '16897 r w4 s2 w41 r w52 r', # 07 Apr 2016
# 'player-en_US-vfll2CKBY/base' => '16898 w19 r s3', # 12 Apr 2016
# 'player-en_US-vflELI9Sd/base' => '16903 s3 w53 s2 w2', # 13 Apr 2016
# 'player-en_US-vflg4mKgv/base' => '16903 s3 w53 s2 w2', # 14 Apr 2016
# 'player-en_US-vflHZ7KXs/base' => '16903 s3 w53 s2 w2', # 19 Apr 2016
# 'player-en_US-vflnFj56r/base' => '16903 s3 w53 s2 w2', # 20 Apr 2016
# 'player-en_US-vfljFzcWO/base' => '16913 w7 r w13 w69 s3 r w14', # 22 Apr 2016
# 'player-en_US-vflQ6YtHH/base' => '16913 w7 r w13 w69 s3 r w14', # 22 Apr 2016
# 'player-en_US-vflvBNQyW/base' => '16912 s3 w7 w24 s1', # 25 Apr 2016
# 'player-en_US-vflG0wokn/base' => '16916 w62 r w38 s1 r s2 r w13 w12',# 26 Apr 2016
# 'player-en_US-vfll6dEHf/base' => '16916 w62 r w38 s1 r s2 r w13 w12',# 27 Apr 2016
# 'player-en_US-vflA_6ZRP/base' => '16918 w14 s1 r w10', # 29 Apr 2016
# 'player-en_US-vflL5aRF-/base' => '16920 w42 r s1 r w30 r s2', # 02 May 2016
# 'player-en_US-vflKklr93/base' => '16920 w42 r s1 r w30 r s2', # 04 May 2016
# 'player-en_US-vflYi-PAF/base' => '16926 w58 r s3', # 09 May 2016
# 'player-en_US-vflPykJ0g/base' => '16926 w58 r s3', # 10 May 2016
# 'player-en_US-vflw9bxTw/base' => '16926 w58 r s3', # 11 May 2016
# 'player-en_US-vflGdEImZ/base' => '16932 w69 w26 r w8 w22 s1', # 12 May 2016
# 'player-en_US-vflTZ3kuV/base' => '16932 w69 w26 r w8 w22 s1', # 19 May 2016
# 'player-en_US-vfl5u7dIk/base' => '16932 w69 w26 r w8 w22 s1', # 19 May 2016
# 'player-en_US-vflGaNMBw/base' => '16932 w69 w26 r w8 w22 s1', # 21 May 2016
# 'player-en_US-vfl6uEgGV/base' => '16941 r w36 s1 r w26 s1 w60', # 23 May 2016
# 'player-en_US-vflKZdm1L/base' => '16944 w25 s2 r', # 24 May 2016
# 'player-en_US-vflNStq7e/base' => '16944 w25 s2 r', # 25 May 2016
# 'player-en_US-vflAwQJsE/base' => '16945 w53 r w19 s3 w37', # 31 May 2016
# 'player-en_US-vfl7FG-3v/base' => '16944 w25 s2 r', # 02 Jun 2016
# 'player-en_US-vfl7vBziO/base' => '16944 w25 s2 r', # 02 Jun 2016
# 'player-en_US-vflrmwhUy/base' => '16944 w25 s2 r', # 04 Jun 2016
# 'player-en_US-vfljqy_st/base' => '16958 s3 w46 w64 w67 s2 r', # 07 Jun 2016
# 'player-en_US-vflzxAejD/base' => '16959 s1 r w4 w67 s3 r w55 r s3',# 08 Jun 2016
# 'player-en_US-vflqpURrL/base' => '16960 r w65 r', # 09 Jun 2016
# 'player-en_US-vflcUEb1U/base' => '16962 w54 s1 r w9 s1', # 11 Jun 2016
# 'player-en_US-vflBUz8b9/base' => '16965 w1 r s2 w27', # 13 Jun 2016
# 'player-en_US-vfl9bYNJa/base' => '16961 s1 r s1 r w35 r', # 14 Jun 2016
# 'player-en_US-vflruV5iG/base' => '16966 w36 s2 w65 r s2 w11 w31',# 15 Jun 2016
# 'player-en_US-vfldefdPl/base' => '16961 s1 r s1 r w35 r', # 15 Jun 2016
# 'player-en_US-vfl-nPja1/base' => '16968 w21 s1 w60 s2', # 20 Jun 2016
# 'player-en_US-vflLyLvKU/base' => '16974 r w45 r', # 23 Jun 2016
# 'player-en_US-vfl0Cqdyd/base' => '16976 w57 r w57 w38 s3 w47 s2',# 27 Jun 2016
# 'player-en_US-vflOfyD_m/base' => '16976 w57 r w57 w38 s3 w47 s2',# 28 Jun 2016
# 'player-en_US-vflAbrXV8/base' => '16976 w57 r w57 w38 s3 w47 s2',# 30 Jun 2016
# 'player-en_US-vflYIVfbT/base' => '16976 w57 r w57 w38 s3 w47 s2',# 05 Jul 2016
# 'player-en_US-vflL1__zc/base' => '16989 s3 r w58 w34 r', # 07 Jul 2016
# 'player-en_US-vflH9xME5/base' => '16989 s3 r w58 w34 r', # 12 Jul 2016
# 'player-en_US-vflxUWFRm/base' => '16989 s3 r w58 w34 r', # 13 Jul 2016
# 'player-en_US-vflWoKF7f/base' => '16996 r w58 w62 s1 w62 r', # 14 Jul 2016
# 'player-en_US-vflbQww0A/base' => '16989 s3 r w58 w34 r', # 17 Jul 2016
# 'player-en_US-vflIl4-ZN/base' => '16989 s3 r w58 w34 r', # 19 Jul 2016
# 'player-en_US-vfl5RxDNb/base' => '17001 s1 w17 r s3', # 20 Jul 2016
# 'player-en_US-vflIB5TLK/base' => '16989 s3 r w58 w34 r', # 21 Jul 2016
# 'player-en_US-vflVo2R8O/base' => '17007 s1 r w35 r s1 r w36 s3',# 27 Jul 2016
# 'player-en_US-vfld7sVQ3/base' => '17007 s1 r w35 r s1 r w36 s3',# 28 Jul 2016
# 'player-en_US-vflua32tg/base' => '17011 w17 s3 r s3 w26 r w19 s2 w8',# 03 Aug 2016
# 'player-en_US-vflHuW2fm/base' => '17011 w17 s3 r s3 w26 r w19 s2 w8',# 04 Aug 2016
# 'player-en_US-vflI2is8G/base' => '17015 w22 r s2 w24 s2 r', # 08 Aug 2016
# 'player-en_US-vflxMAwM7/base' => '17015 w22 r s2 w24 s2 r', # 09 Aug 2016
# 'player-en_US-vflD53teA/base' => '17015 w22 r s2 w24 s2 r', # 12 Aug 2016
# 'player-en_US-vflduS31F/base' => '17015 w22 r s2 w24 s2 r', # 13 Aug 2016
# 'player-en_US-vflCWknvV/base' => '17015 w22 r s2 w24 s2 r', # 14 Aug 2016
# 'player-en_US-vflsfFMeN/base' => '17015 w22 r s2 w24 s2 r', # 16 Aug 2016
# 'player-en_US-vflYm48JC/base' => '17029 s3 w50 r w46 w5 s2', # 17 Aug 2016
# 'player-en_US-vfl9QlUdu/base' => '17030 r s2 w17 r w1 s1', # 18 Aug 2016
# 'player-en_US-vflIsoTq9/base' => '17031 r s3 w63 r', # 22 Aug 2016
# 'player-en_US-vflB4BK_2/base' => '17031 r s3 w63 r', # 23 Aug 2016
# 'player-en_US-vflrza-6I/base' => '17031 r s3 w63 r', # 25 Aug 2016
# 'player-en_US-vflCFz7Ac/base' => '17039 s3 w2 s2 w46 s1 w31 w27',# 30 Aug 2016
# 'player-en_US-vflYH10GU/base' => '17039 s3 w2 s2 w46 s1 w31 w27',# 31 Aug 2016
# 'player-en_US-vflqMMQzs/base' => '17039 s3 w2 s2 w46 s1 w31 w27',# 01 Sep 2016
# 'player-en_US-vfl3Us3jU/base' => '17046 s2 r s2 w31 w6 r s2', # 06 Sep 2016
# 'player-en_US-vfltdrc9Q/base' => '17050 w19 r s1 r s1 w7 r w38 s3',# 07 Sep 2016
# 'player-en_US-vflwEMtjy/base' => '17056 r s3 r w20 s3 r s2 r', # 13 Sep 2016
# 'player-en_US-vflIb3VDh/base' => '17056 r s3 r w20 s3 r s2 r', # 14 Sep 2016
# 'player-en_US-vflGe_KH9/base' => '17056 r s3 r w20 s3 r s2 r', # 15 Sep 2016
# 'player-en_US-vflOrSoUx/base' => '17060 w35 r s3 r w55 s3 w2', # 20 Sep 2016
# 'player-en_US-vflhmEPlj/base' => '17064 w70 s3 w7 s1 w68 s1 w64',# 21 Sep 2016
# 'player-en_US-vfl-naOSO/base' => '17066 r w30 w40 w48 r s1 w53 s3 r',# 22 Sep 2016
# 'player-en_US-vflHlG7su/base' => '17068 r w35 s2', # 26 Sep 2016
# 'player-en_US-vfl8j0dbL/base' => '17067 w63 s3 w38 s3 w16 w67 s3 r s1',# 26 Sep 2016
# 'player-en_US-vflw2cgEp/base' => '17067 w63 s3 w38 s3 w16 w67 s3 r s1',# 27 Sep 2016
# 'player-en_US-vflhPhaA1/base' => '17071 w15 s3 r s2 w4 s2 r', # 28 Sep 2016
# 'player-en_US-vflK2tmSr/base' => '17072 s3 r s3 r w20', # 29 Sep 2016
# 'player-en_US-vflKBaLr4/base' => '17072 s3 r s3 r w20', # 30 Sep 2016
# 'player-en_US-vflssZQ6P/base' => '17074 r w9 r s3 r s3 w51 r', # 03 Oct 2016
# 'player-en_US-vflXU8Lcz/base' => '17079 r w45 s1 r s2 r s2 r w3',# 05 Oct 2016
# 'player-en_US-vflOj6Vz8/base' => '17079 r w45 s1 r s2 r s2 r w3',# 07 Oct 2016
# 'player-en_US-vflQcYs5w/base' => '17079 r w45 s1 r s2 r s2 r w3',# 07 Oct 2016
# 'player-en_US-vfl-E2vny/base' => '17082 r w9 s1 r s1 w66 w30 r w48',# 11 Oct 2016
# 'player-en_US-vflabgyIE/base' => '17086 s2 w6 r s3 w53 r w46 w56',# 12 Oct 2016
# 'player-en_US-vflkqCvzc/base' => '17086 s2 w6 r s3 w53 r w46 w56',# 13 Oct 2016
# 'player-en_US-vflI-HtJG/base' => '17089 w11 w51 r s2 w32 s1', # 17 Oct 2016
# 'player-en_US-vflMRpBY0/base' => '17092 s1 w68 r w17 w3 s1 w48 r s2',# 18 Oct 2016
# 'player-en_US-vflkGN22k/base' => '17092 s1 w68 r w17 w3 s1 w48 r s2',# 18 Oct 2016
# 'player-en_US-vflEz7zqU/base' => '17093 r s1 w60', # 21 Oct 2016
# 'player-en_US-vflTBNOIW/base' => '17098 s1 r w37 r s1 w53 r s2 r',# 25 Oct 2016
# 'player-en_US-vflx7_SPL/base' => '17098 s1 r w37 r s1 w53 r s2 r',# 26 Oct 2016
# 'player-en_US-vflvtarAT/base' => '17098 s1 r w37 r s1 w53 r s2 r',# 28 Oct 2016
# 'player-en_US-vflG26Hhi/base' => '17102 w32 w26 s1 r w20', # 30 Oct 2016
# 'player-en_US-vfliKSBJe/base' => '17104 s1 r s1 r s3 w29 s2 w24',# 30 Oct 2016
# 'player-en_US-vfl9TjB9H/base' => '17105 w8 r w59 w68', # 01 Nov 2016
# 'player-en_US-vfle0WwUC/base' => '17108 s2 w58 w59', # 07 Nov 2016
# 'player-en_US-vflcQt09B/base' => '17110 w10 r w29 r w46 r w10 s2',# 07 Nov 2016
# 'player-en_US-vfllAQuZd/base' => '17113 s1 r s3 w44 w63 r', # 09 Nov 2016
# 'player-en_US-vflgFv_Kx/base' => '17114 s2 w31 s2 r s3 r w60 s2',# 10 Nov 2016
# 'player-en_US-vflZebs2S/base' => '17120 w48 r w8 w28 s2 w22 w61 s2 w59',# 15 Nov 2016
# 'player-en_US-vflSldmkq/base' => '17121 s3 w13 w41 s1 w51 r w53 r w57',# 18 Nov 2016
# 'player-en_US-vflydz95C/base' => '17133 s2 r w59 r s1 w16 s1', # 29 Nov 2016
# 'player-en_US-vflzQdL0P/base' => '17133 s2 r w59 r s1 w16 s1', # 01 Dec 2016
# 'player-en_US-vflDkHeWE/base' => '17128 r s3 r w26 s1 r w10', # 02 Dec 2016
# 'player-en_US-vflr_3iyV/base' => '17135 r w40 s2 r s1 r w61', # 05 Dec 2016
# 'player-en_US-vflyIX2li/base' => '17141 w58 r s1 w66 r', # 06 Dec 2016
# 'player-en_US-vfl8r3fjW/base' => '17140 w17 s3 w44 w13 r w33 w39',# 07 Dec 2016
# 'player-en_US-vfldNN9oa/base' => '17140 w17 s3 w44 w13 r w33 w39',# 10 Dec 2016
# 'player-en_US-vflK2s6tX/base' => '17141 w58 r s1 w66 r', # 13 Dec 2016
# 'player-en_US-vflFKNtIl/base' => '17147 s2 w21 r s1 r s2 r', # 14 Dec 2016
# 'player-en_US-vfljAVcXG/base' => '17149 s2 w51 r w62 w44 w65', # 15 Dec 2016
# 'player-en_US-vflxP8f0T/base' => '17151 w47 s1 r w21 r w16 r', # 19 Dec 2016
# 'player-en_US-vfla6wgHS/base' => '17151 w47 s1 r w21 r w16 r', # 20 Dec 2016
# 'player-en_US-vflz_1lv2/base' => '17170 s2 r s2 w25 r s3 r', # 05 Jan 2017
# 'player-en_US-vflsagga9/base' => '17175 s3 r w23 r w33 w51 s1 r w26',# 09 Jan 2017
# 'player-en_US-vflC029_L/base' => '17176 s2 r w17 r s2', # 12 Jan 2017
# 'player-en_US-vfl4x5gM8/base' => '17177 r s2 r w5 s1 w7 r', # 13 Jan 2017
# 'player-en_US-vflR62D9G/base' => '17177 r s2 r w5 s1 w7 r', # 15 Jan 2017
# 'player-en_US-vflbh8HdB/base' => '17180 s2 w43 s2 r w35 r', # 17 Jan 2017
# 'player-en_US-vflkZ4r_7/base' => '17180 s2 w43 s2 r w35 r', # 19 Jan 2017
# 'player-en_US-vflamKXEP/base' => '17184 w42 w20 r w4 r s2', # 20 Jan 2017
# 'player-en_US-vflHoC0VQ/base' => '17184 w42 w20 r w4 r s2', # 20 Jan 2017
# 'player-en_US-vfl8Smq8T/base' => '17186 w37 w52 s3 w69 r', # 23 Jan 2017
# 'player-en_US-vflNaXsht/base' => '17190 s1 r s2 r', # 25 Jan 2017
# 'player-en_US-vflQBHHdn/base' => '17190 s1 r s2 r', # 26 Jan 2017
# 'player-en_US-vflp0EuAP/base' => '17192 s2 w64 r s3', # 31 Jan 2017
# 'player-en_US-vflkk7pUE/base' => '17192 s2 w64 r s3', # 02 Feb 2017
# 'player-en_US-vflkRUE82/base' => '17199 w58 w66 s2 w70 r w56', # 09 Feb 2017
# 'player-en_US-vfl8LqiZp/base' => '17199 w58 w66 s2 w70 r w56', # 09 Feb 2017
# 'player-en_US-vflg9Wu9U/base' => '17206 s2 w48 r s2 w40 r w5 r',# 15 Feb 2017
# 'player-en_US-vflqOi6vK/base' => '17217 r s3 w53 s1 r w25 r', # 22 Feb 2017
# 'player-en_US-vflVlxFvV/base' => '17217 r s3 w53 s1 r w25 r', # 24 Feb 2017
# 'player-en_US-vflDQGgxm/base' => '17221 r w12 w69 r w50 r w61 r w10',# 01 Mar 2017
# 'player-en_US-vflOnuOF-/base' => '17229 w31 w23 r w26 r', # 07 Mar 2017
# 'player-en_US-vfl67GkkS/base' => '17240 s1 w30 w63 w26 s3 w8 s2',# 15 Mar 2017
# 'player-en_US-vflk2jRfn/base' => '17240 s1 w30 w63 w26 s3 w8 s2',# 16 Mar 2017
# 'player-en_US-vfl7pRlZI/base' => '17240 s1 w30 w63 w26 s3 w8 s2',# 20 Mar 2017
# 'player-en_US-vfl8dRko7/base' => '17242 w11 s1 r s3', # 21 Mar 2017
# 'player-en_US-vflTlQxIb/base' => '17245 w18 w46 s1 w56 r s3 r w53 s1',# 21 Mar 2017
# 'player-en_US-vflfbDY14/base' => '17246 s2 w55 s1', # 22 Mar 2017
# 'player-en_US-vfl6bNiHm/base' => '17249 s2 r w38 r s3 r', # 25 Mar 2017
# 'player-en_US-vflEzRdnB/base' => '17246 s2 w55 s1', # 25 Mar 2017
# 'player-en_US-vflTzv1GM/base' => '17249 s2 r w38 r s3 r', # 27 Mar 2017
# 'player-en_US-vfl5WC80G/base' => '17251 w37 r s3 w60 r w41', # 28 Mar 2017
# 'player-en_US-vflwkOLTK/base' => '17252 w7 r w27 w34 r w56 w53 s1 r',# 29 Mar 2017
# 'player-en_US-vflgcceTZ/base' => '17252 w7 r w27 w34 r w56 w53 s1 r',# 30 Mar 2017
# 'player-en_US-vflPbFwAK/base' => '17254 s1 r w49 w29 s3 w59 w6 s2',# 30 Mar 2017
# 'player-en_US-vflRjgXJi/base' => '17256 w70 s1 r w63 r w46 w49 s1',# 01 Apr 2017
# 'player-en_US-vfld2g5gM/base' => '17258 w6 r s1 r', # 03 Apr 2017
# 'player-en_US-vfl0d6UIe/base' => '17258 w6 r s1 r', # 04 Apr 2017
# 'player-en_US-vfl-q4dPj/base' => '17258 w6 r s1 r', # 06 Apr 2017
# 'player-en_US-vfl6_PD5A/base' => '17261 w7 s1 r s1 w2 s2 r', # 06 Apr 2017
# 'player-en_US-vfliZaFqy/base' => '17263 w54 s3 w1 w36 s3', # 07 Apr 2017
# 'player-en_US-vflqFHgLE/base' => '17261 w7 s1 r s1 w2 s2 r', # 11 Apr 2017
# 'player-en_US-vflaxXRn1/base' => '17263 w54 s3 w1 w36 s3', # 12 Apr 2017
# 'player-en_US-vfl5-0t5t/base' => '17269 s1 w44 r s1', # 14 Apr 2017
# 'player-en_US-vflchU0AK/base' => '17270 w58 s1 r s2 w8 w21', # 20 Apr 2017
# 'player-en_US-vflNZnmd3/base' => '17277 w66 r w54', # 24 Apr 2017
# 'player-en_US-vflR14qD2/base' => '17277 w66 r w54', # 25 Apr 2017
# 'player-vflppxuSE/en_US/base' => '17277 w66 r w54', # 27 Apr 2017
# 'player-vflp8UEng/en_US/base' => '17291 r s3 r w45', # 05 May 2017
# 'player-vfl3DiVMI/en_US/base' => '17293 w59 s3 w24 r w55 r s2 w38 w19',# 08 May 2017
# 'player-vfljmjb-X/en_US/base' => '17291 r s3 r w45', # 11 May 2017
# 'player-vflxXnk_G/en_US/base' => '17295 r w27 r', # 11 May 2017
# 'player-vfltmLGsd/en_US/base' => '17297 s2 w55 r s3 r', # 16 May 2017
# 'player-vfl8jhACg/en_US/base' => '17303 w67 s3 r s2', # 17 May 2017
# 'player-vfl4Xq3l4/en_US/base' => '17302 s3 r w43', # 19 May 2017
# 'player-vfld8zR1S/en_US/base' => '17305 w16 s1 r s3 w33 s2 r s2',# 22 May 2017
# 'player-vfluaMKo6/en_US/base' => '17305 w16 s1 r s3 w33 s2 r s2',# 23 May 2017
# 'player-vflyC4_W-/en_US/base' => '17316 s1 r w24 s3 r w54 s1', # 30 May 2017
# 'player-vflCqycGh/en_US/base' => '17316 s1 r w24 s3 r w54 s1', # 01 Jun 2017
# 'player-vflZ_L_3c/en_US/base' => '17316 s1 r w24 s3 r w54 s1', # 02 Jun 2017
# 'player-vflQZSd3x/en_US/base' => '17325 s3 r s1', # 12 Jun 2017
# 'player-vflLxaaub/en_US/base' => '17329 s2 r w19 w60 s1 r w15 r s2',# 14 Jun 2017
# 'player-vfle90bgw/en_US/base' => '17329 s2 r w19 w60 s1 r w15 r s2',# 16 Jun 2017
# 'player-vfl2DpwLG/en_US/base' => '17333 w57 r w66', # 19 Jun 2017
# 'player-vfl1Renoe/en_US/base' => '17336 r w38 r w67 w24 r s2', # 20 Jun 2017
# 'player-vflmgXZN3/en_US/base' => '17338 s2 w33 w16 w44 s1 w12 r w19',# 23 Jun 2017
# 'player-vflPHG8dr/en_US/base' => '17342 r w12 r s2 w21 s3 w25 s1 r',# 25 Jun 2017
# 'player-vflAmElk-/en_US/base' => '17343 w4 r s1 w11 s1 w67 r', # 27 Jun 2017
# 'player-vflV4eRc2/en_US/base' => '17344 w46 r w13 r w5 s3 w44 w51',# 28 Jun 2017
# 'player-vflotiWiu/en_US/base' => '17343 w4 r s1 w11 s1 w67 r', # 29 Jun 2017
# 'player-vfl3RjfTG/en_US/base' => '17343 w4 r s1 w11 s1 w67 r', # 05 Jul 2017
# 'player-vfl2U8fxZ/en_US/base' => '17353 r w70 w7 r s2 r s3', # 06 Jul 2017
# 'player-vflDXt52J/en_US/base' => '17354 w39 s3 w70 r s3', # 10 Jul 2017
# 'player-vflZQAwO8/en_US/base' => '17354 w39 s3 w70 r s3', # 11 Jul 2017
# 'player-vflL_WLGI/en_US/base' => '17358 r w42 w32 r', # 13 Jul 2017
# 'player-vflMaap-E/en_US/base' => '17364 r w13 r w28 r s3 r s3', # 19 Jul 2017
# 'player-vflGD0HaZ/en_US/base' => '17364 r w13 r w28 r s3 r s3', # 20 Jul 2017
# 'player-vflC3ZxIh/en_US/base' => '17368 w11 w55 w26', # 24 Jul 2017
# 'player-vflp0IacK/en_US/base' => '17372 w68 s3 w24 s3 w55 r s2',# 27 Jul 2017
# 'player-vflrwQIQw/en_US/base' => '17374 r s2 r w19 s1', # 27 Jul 2017
# 'player-vflRrT_TQ/en_US/base' => '17374 r s2 r w19 s1', # 02 Aug 2017
# 'player-vflN55NZo/en_US/base' => '17379 s2 r s2 w37 s3 w4 w13 w17 s3',# 02 Aug 2017
# 'player-vfl8KhWdC/en_US/base' => '17380 w7 r w33 s2 w51 s2 w46 r s1',# 03 Aug 2017
# 'player-vflIVpVc9/en_US/base' => '17385 r s2 w1 s3 w11 w9 s2', # 07 Aug 2017
# 'player-vflmw6aFG/en_US/base' => '17382 s3 r w36 s1 w48', # 09 Aug 2017
# 'player-vflSyILh9/en_US/base' => '17387 s2 r w4 s1 w6', # 14 Aug 2017
# 'player-vflBXnagy/en_US/base' => '17387 s2 r w4 s1 w6', # 15 Aug 2017
# 'player-vflW7ch5Z/en_US/base' => '17393 r s1 r s2 r s2', # 16 Aug 2017
# 'player-vflAAoWvh/en_US/base' => '17393 r s1 r s2 r s2', # 17 Aug 2017
# 'player-vflTof4g1/en_US/base' => '17399 w25 w9 r', # 23 Aug 2017
# 'player-vflK5H48T/en_US/base' => '17399 w25 w9 r', # 23 Aug 2017
# 'player-vflyJ3OmM/en_US/base' => '17402 w65 s3 r s1 r s1 w58', # 25 Aug 2017
# 'player-vfl2iVoNh/en_US/base' => '17403 s3 w51 w36 s3', # 25 Aug 2017
# 'player-vflyFnz8E/en_US/base' => '17403 s3 w51 w36 s3', # 28 Aug 2017
# 'player-vflWQ9tuM/en_US/base' => '17403 s3 w51 w36 s3', # 30 Aug 2017
# 'player-vflaEZiBp/en_US/base' => '17403 s3 w51 w36 s3', # 05 Sep 2017
# 'player-vflbWGdxe/en_US/base' => '17416 w38 r s1 w52 r w46 w49 r',# 07 Sep 2017
# 'player-vflm9jiGH/en_US/base' => '17416 w38 r s1 w52 r w46 w49 r',# 11 Sep 2017
# 'player-vflUDI8Xm/en_US/base' => '17416 w38 r s1 w52 r w46 w49 r',# 12 Sep 2017
# 'player-vfl8DkB0M/en_US/base' => '17422 s3 r w24 w61 r s3 r', # 13 Sep 2017
# 'player-vflUnLBiU/en_US/base' => '17421 s3 r s1 w45 w25 s3', # 14 Sep 2017
# 'player-vfliXTNRk/en_US/base' => '17423 r s3 r s3 w51 w8 s3 w21',# 18 Sep 2017
# 'player-vflxp5z1z/en_US/base' => '17423 r s3 r s3 w51 w8 s3 w21',# 19 Sep 2017
# 'player-vfl3pBiM5/en_US/base' => '17423 r s3 r s3 w51 w8 s3 w21',# 20 Sep 2017
# 'player-vflR94_oU/en_US/base' => '17423 r s3 r s3 w51 w8 s3 w21',# 22 Sep 2017
# 'player-vfldWu3iC/en_US/base' => '17434 s3 r s1 r w61 r s2 w28',# 26 Sep 2017
# 'player-vfls3Lf3-/en_US/base' => '17434 s3 r s1 r w61 r s2 w28',# 27 Sep 2017
# 'player-vflcAIVzv/en_US/base' => '17437 r w54 r', # 28 Sep 2017
# 'player-vflGRNpAk/en_US/base' => '17436 s1 w50 r s3', # 02 Oct 2017
# 'player-vfl1RKjMF/en_US/base' => '17442 s2 r s2', # 04 Oct 2017
# 'player-vflOdyxa4/en_US/base' => '17444 s2 r w24 s2 w48 s3 r', # 05 Oct 2017
# 'player-vflgfcuiz/en_US/base' => '17444 s2 r w24 s2 w48 s3 r', # 09 Oct 2017
# 'player-vflgH8YLq/en_US/base' => '17448 r w49 s3 w34 s3 w6 s3', # 10 Oct 2017
# 'player-vflwcUIMe/en_US/base' => '17449 w31 r w13 w14 r s1 r w45 r',# 11 Oct 2017
# 'player-vflD3dhYB/en_US/base' => '17452 w41 r w37 w19', # 17 Oct 2017
# 'player-vflHvONov/en_US/base' => '17455 s2 r s2 w20 r s3', # 17 Oct 2017
# 'player-vflcNAJUd/en_US/base' => '17456 w16 s3 w6 r w40 s3 r w49',# 18 Oct 2017
# 'player-vflN-B5oM/en_US/base' => '17463 r w69 w9 s1', # 24 Oct 2017
# 'player-vflC8Yy7I/en_US/base' => '17462 w70 s3 w59 r w46', # 25 Oct 2017
# 'player-vflhIZIgy/en_US/base' => '17462 w70 s3 w59 r w46', # 26 Oct 2017
# 'player-vflSjPnAo/en_US/base' => '17465 r w28 w62 r s1 r s1', # 30 Oct 2017
# 'player-vfl1ElKmp/en_US/base' => '17469 s2 r w62 s2 w5', # 31 Oct 2017
# 'player-vflhqxyp7/en_US/base' => '17469 s2 r w62 s2 w5', # 01 Nov 2017
# 'player-vflg6eF8s/en_US/base' => '17471 w13 w48 r s3 w6', # 02 Nov 2017
# 'player-vflv6AMZr/en_US/base' => '17473 w1 r s2 w16', # 06 Nov 2017
# 'player-vflvYne1z/en_US/base' => '17473 w1 r s2 w16', # 07 Nov 2017
# 'player-vfl8XKJyP/en_US/base' => '17478 s1 r w69 s2 w45 s3 r w64 s2',# 08 Nov 2017
# 'player-vfl97imvj/en_US/base' => '17478 s1 r w69 s2 w45 s3 r w64 s2',# 09 Nov 2017
# 'player-vflXHVFyU/en_US/base' => '17483 r w55 s3 w5 r w36 r w66',# 13 Nov 2017
# 'player-vflg_prv_/en_US/base' => '17486 w58 s3 r s2 w2 s3', # 16 Nov 2017
# 'player-vflPDkkkL/en_US/base' => '17486 w58 s3 r s2 w2 s3', # 16 Nov 2017
# 'player-vflM013co/en_US/base' => '17486 w58 s3 r s2 w2 s3', # 16 Nov 2017
# 'player-vflYXLM5n/en_US/base' => '17488 r s2 w13 s3 w62 r w14', # 20 Nov 2017
# 'player-vflsCMP_E/en_US/base' => '17490 w31 s1 r s3', # 21 Nov 2017
# 'player-vflJtN5rw/en_US/base' => '17494 w45 w69 w2 r s1 r s1 r',# 24 Nov 2017
# 'player-vflnNEucX/en_US/base' => '17492 w61 r s2 r', # 27 Nov 2017
# 'player-vfl8BSHQD/en_US/base' => '17492 w61 r s2 r', # 29 Nov 2017
# 'player-vfl32FIDY/en_US/base' => '17501 w48 r w24 r', # 04 Dec 2017
# 'player-vfl_6lezG/en_US/base' => '17501 w48 r w24 r', # 05 Dec 2017
# 'player-vflvODUt0/en_US/base' => '17501 w48 r w24 r', # 06 Dec 2017
# 'player-vfl4OEYh9/en_US/base' => '17501 w48 r w24 r', # 07 Dec 2017
# 'player-vflebAXY2/en_US/base' => '17508 s2 w60 w51 s3 w52 r w22',# 11 Dec 2017
# 'player-vflu-7yX5/en_US/base' => '17511 r s2 r s2 w69', # 12 Dec 2017
# 'player-vflOQ79Pl/en_US/base' => '17512 w28 w47 r s1 r w6', # 13 Dec 2017
# 'player-vflyoGrhd/en_US/base' => '17512 w28 w47 r s1 r w6', # 14 Dec 2017
# 'player-vflalc4VN/en_US/base' => '17515 w52 w9 s3 r w19 r w44 r',# 18 Dec 2017
# 'player-vflQ3Cu6g/en_US/base' => '17533 w56 s3 w35 r s2 w57 s2',# 03 Jan 2018
# 'player-vflIfz8pB/en_US/base' => '17533 w56 s3 w35 r s2 w57 s2',# 04 Jan 2018
# 'player-vfluepRD8/en_US/base' => '17536 w30 w30 w10 s3', # 08 Jan 2018
# 'player-vflmAXHDE/en_US/base' => '17539 w20 r w35 r s1 w60 r s2',# 09 Jan 2018
# 'player-vflAhnAPk/en_US/base' => '17539 w20 r w35 r s1 w60 r s2',# 10 Jan 2018
# 'player-vflLCGcm0/en_US/base' => '17541 s2 r s3 w27 s2', # 11 Jan 2018
# 'player-vflsh1Hwx/en_US/base' => '17544 r s1 r w52 r s1 r s2', # 16 Jan 2018
# 'player-vflNX6xa_/en_US/base' => '17547 r w14 s1 w66 s1 w9 w65 r',# 17 Jan 2018
# 'player-vfljg_2Dr/en_US/base' => '17549 w52 r s1 w56 s2 r', # 22 Jan 2018
# 'player-vfleux_zG/en_US/base' => '17555 w15 w70 r w10 r w66 s3 w33 w24',# 24 Jan 2018
# 'player-vflX4ueE4/en_US/base' => '17555 w15 w70 r w10 r w66 s3 w33 w24',# 25 Jan 2018
# 'player-vflAZc3qd/en_US/base' => '17555 w15 w70 r w10 r w66 s3 w33 w24',# 29 Jan 2018
# 'player-vflVZNDz1/en_US/base' => '17555 w15 w70 r w10 r w66 s3 w33 w24',# 30 Jan 2018
# 'player-vflxuxnEY/en_US/base' => '17561 s3 r w49', # 31 Jan 2018
# 'player-vflBjp0_H/en_US/base' => '17564 w1 s3 r', # 06 Feb 2018
# 'player-vflG9lb96/en_US/base' => '17570 w26 r w8 w61', # 08 Feb 2018
# 'player-vflNpPGQq/en_US/base' => '17570 w26 r w8 w61', # 12 Feb 2018
# 'player-vflGoYKgz/en_US/base' => '17574 w6 w64 w25 w53 s2 r s3',# 14 Feb 2018
# 'player-vfl8swg2e/en_US/base' => '17574 w6 w64 w25 w53 s2 r s3',# 15 Feb 2018
# 'player-vflLdwQUM/en_US/base' => '17579 s2 w2 w51 w9 s2 r w15 s3',# 20 Feb 2018
# 'player-vflJmXkuH/en_US/base' => '17579 s2 w2 w51 w9 s2 r w15 s3',# 22 Feb 2018
# 'player-vflSVCOgl/en_US/base' => '17579 s2 w2 w51 w9 s2 r w15 s3',# 22 Feb 2018
# 'player-vfldJxavu/en_US/base' => '17579 s2 w2 w51 w9 s2 r w15 s3',# 26 Feb 2018
# 'player-vflC6bTWQ/en_US/base' => '17589 r s3 w35 s1 r w54', # 27 Feb 2018
# 'player-vflGUPF-i/en_US/base' => '17595 r w23 s3 r w45 r w66', # 06 Mar 2018
# 'player-vflCpS7fy/en_US/base' => '17595 r w23 s3 r w45 r w66', # 07 Mar 2018
# 'player-vflpGF_3J/en_US/base' => '17595 r w23 s3 r w45 r w66', # 08 Mar 2018
# 'player-vflqL4Jb8/en_US/base' => '17598 r s1 r w50', # 10 Mar 2018
# 'player-vfllqtOs7/en_US/base' => '17598 r s1 r w50', # 13 Mar 2018
# 'player-vfleo_x3O/en_US/base' => '17598 r s1 r w50', # 14 Mar 2018
# 'player-vflHDhBq1/en_US/base' => '17598 r s1 r w50', # 15 Mar 2018
# 'player-vflHP6k-6/en_US/base' => '17598 r s1 r w50', # 17 Mar 2018
# 'player-vflrObaqJ/en_US/base' => '17606 w12 w18 r s3 r s3 w69 r s3',# 20 Mar 2018
# 'player-vfl33N9QG/en_US/base' => '17606 w12 w18 r s3 r s3 w69 r s3',# 21 Mar 2018
# 'player-vflMfSEyN/en_US/base' => '17606 w12 w18 r s3 r s3 w69 r s3',# 22 Mar 2018
# 'player-vflPBHrby/en_US/base' => '17606 w12 w18 r s3 r s3 w69 r s3',# 24 Mar 2018
# 'player_ias-vfl97oyaf/en_US/base' => '17614 r s2 w67 s3 r s3', # 27 Mar 2018
# 'player-vfl7rrrdV/en_US/base' => '17616 r s3 r s3 w38 s1 w64 r s2',# 28 Mar 2018
# 'player-vflI0cIzU/en_US/base' => '17616 r s3 r s3 w38 s1 w64 r s2',# 29 Mar 2018
# 'player-vflENcx6t/en_US/base' => '17616 r s3 r s3 w38 s1 w64 r s2',# 03 Apr 2018
# 'player-vflE3xFS5/en_US/base' => '17616 r s3 r s3 w38 s1 w64 r s2',# 07 Apr 2018
# 'player-vflSawkIt/en_US/base' => '17616 r s3 r s3 w38 s1 w64 r s2',# 09 Apr 2018
# 'player-vflRhCLRy/en_US/base' => '17616 r s3 r s3 w38 s1 w64 r s2',# 11 Apr 2018
# 'player-vflX7BSrP/en_US/base' => '17632 s1 w70 w15 w3 r s2 r s2 r',# 12 Apr 2018
# 'player-vflUCrh9C/en_US/base' => '17633 s2 w12 w49 s1 r w68 r s3',# 16 Apr 2018
# 'player-vflZnIPED/en_US/base' => '17633 s2 w12 w49 s1 r w68 r s3',# 16 Apr 2018
# 'player-vflFcxzRO/en_US/base' => '17633 s2 w12 w49 s1 r w68 r s3',# 18 Apr 2018
# 'player-vflNLtm2_/en_US/base' => '17638 r w17 w14 s2 r', # 23 Apr 2018
# 'player-vfl5ItJAe/en_US/base' => '17638 r w17 w14 s2 r', # 24 Apr 2018
# 'player-vflPIRcoF/en_US/base' => '17638 r w17 w14 s2 r', # 25 Apr 2018
# 'player-vfluI_BcD/en_US/base' => '17638 r w17 w14 s2 r', # 26 Apr 2018
# 'player-vflp8wBqC/en_US/base' => '17647 s2 r s2 r s3 w60', # 28 Apr 2018
# 'player-vflHFWD7-/en_US/base' => '17647 s2 r s2 r s3 w60', # 01 May 2018
# 'player-vflfv8a8v/en_US/base' => '17647 s2 r s2 r s3 w60', # 03 May 2018
# 'player-vflFw2plq/en_US/base' => '17655 r w51 r w24 s3 w70 r', # 05 May 2018
# 'player-vfl5JMAdU/en_US/base' => '17655 r w51 r w24 s3 w70 r', # 09 May 2018
# 'player-vflUPJQPD/en_US/base' => '17655 r w51 r w24 s3 w70 r', # 09 May 2018
# 'player-vflxk5snu/en_US/base' => '17662 r w50 s2 r s3', # 16 May 2018
# 'player-vflXIriOh/en_US/base' => '17662 r w50 s2 r s3', # 17 May 2018
# 'player-vflBI1oYt/en_US/base' => '17662 r w50 s2 r s3', # 22 May 2018
# 'player-vfllWbVhi/en_US/base' => '17662 r w50 s2 r s3', # 23 May 2018
# 'player-vflqFr_Sb/en_US/base' => '17662 r w50 s2 r s3', # 24 May 2018
# 'player_remote_ux-vflLhtyuT/en_US/base' => '17662 r w50 s2 r s3',# 24 May 2018
# 'player-vflKSi76_/en_US/base' => '17662 r w50 s2 r s3', # 26 May 2018
# 'player-vflmV3Usi/en_US/base' => '17686 w21 s3 w41 r s1 w21 s1',# 05 Jun 2018
# 'player-vfl_RUk0U/en_US/base' => '17686 w21 s3 w41 r s1 w21 s1',# 06 Jun 2018
# 'player-vfl4qvcOS/en_US/base' => '17686 w21 s3 w41 r s1 w21 s1',# 06 Jun 2018
# 'player-vflr_Wq0V/en_US/base' => '17686 w21 s3 w41 r s1 w21 s1',# 09 Jun 2018
# 'player-vflT6zTz3/en_US/base' => '17693 w62 s1 r s3 w16 r', # 12 Jun 2018
# 'player-vflkTAFWp/en_US/base' => '17696 s3 w44 s2 w34', # 14 Jun 2018
# 'player-vfljt23et/en_US/base' => '17701 w60 s2 w31 r w33 s3', # 19 Jun 2018
# 'player-vflT670_e/en_US/base' => '17702 r w7 w5 w6 r w63 w13', # 20 Jun 2018
# 'player-vflpusdz-/en_US/base' => '17703 r w4 w19 s2 r w65 w1 r',# 21 Jun 2018
# 'player-vflWxIE9k/en_US/base' => '17707 w65 s1 r w56 w49 r s1 w60 s1',# 25 Jun 2018
# 'player-vflRPSMdq/en_US/base' => '17708 s3 r w6 r s2 r s2 r', # 26 Jun 2018
# 'player-vfllebDdS/en_US/base' => '17709 s1 w14 w58 r s3 w38 r w14 w23',# 27 Jun 2018
# 'player-vflbyMNJ8/en_US/base' => '17710 w3 w28 s1 r w22', # 28 Jun 2018
# 'player_ias-vflB7iQOt/en_US/base' => '17710 w3 w28 s1 r w22', # 28 Jun 2018
# 'player-vflunOvo8/en_US/base' => '17710 w3 w28 s1 r w22', # 28 Jun 2018
# 'player-vflG40-nw/en_US/base' => '17714 w45 r s2 r', # 02 Jul 2018
# 'player-vfloLF805/en_US/base' => '17715 r w20 r w10 s1 w37 w32 s3',# 03 Jul 2018
# 'player-vflEPlHUY/en_US/base' => '17721 s3 r w58 r s3 w69', # 09 Jul 2018
# 'player-vflAHKVO-/en_US/base' => '17722 w32 r s2 r w69 r s2 r', # 10 Jul 2018
# 'player-vflJjjlWD/en_US/base' => '17723 r s3 w23 w44 r s3', # 12 Jul 2018
# 'player-vfl_lUmSJ/en_US/base' => '17724 w24 r w51 r w60', # 12 Jul 2018
# 'player-vfl9zpg5e/en_US/base' => '17725 w1 r s1', # 13 Jul 2018
# 'player_ias-vfl_mA0rx/en_US/base' => '17728 r s1 w11', # 16 Jul 2018
# 'player-vflzCRPJh/en_US/base' => '17728 r s1 w11', # 16 Jul 2018
# 'player_ias-vfl2WjsTu/en_US/base' => '17731 w51 r w17 r s2 w32 s2',# 19 Jul 2018
# 'player-vfl-Sv0Xf/en_US/base' => '17731 w51 r w17 r s2 w32 s2', # 19 Jul 2018
# 'player-vfllBzgpS/en_US/base' => '17733 w16 w44 s2 r w64 s1 w19',# 21 Jul 2018
# 'player_ias-vflrYD9L0/en_US/base' => '17735 w34 r s1 r w31 s1', # 23 Jul 2018
# 'player-vflo6HcQb/en_US/base' => '17737 s1 r s3 w67 r s2', # 25 Jul 2018
# 'player-vflW8WdD_/en_US/base' => '17737 s1 r s3 w67 r s2', # 26 Jul 2018
# 'player-vflb9tnhu/en_US/base' => '17740 r s1 r s2 w70 s1 r w45 r',# 28 Jul 2018
# 'player-vflmd36GJ/en_US/base' => '17742 s1 w18 r s1 r w60 s3', # 31 Jul 2018
# 'player-vfliKu3Tk/en_US/base' => '17744 w9 r w11 w12 w56 r w1 s3 r',# 01 Aug 2018
# 'player_ias-vflSjBO9f/en_US/base' => '17745 r w48 r', # 02 Aug 2018
# 'player_remote_ux-vflqcuXQQ/en_US/base' => '17745 r w48 r', # 02 Aug 2018
# 'player-vflkrp3z6/en_US/base' => '17745 r w48 r', # 02 Aug 2018
# 'player-vflDNC2vK/en_US/base' => '17749 w52 w13 w27 r s2 w12 r',# 06 Aug 2018
# 'player_ias-vflPDD_hw/en_US/base' => '17750 r s2 r s3 r', # 07 Aug 2018
# 'player_ias-vflWkXN6I/en_US/base' => '17750 r s2 r s3 r', # 08 Aug 2018
# 'player-vflm39o9Z/en_US/base' => '17751 s2 r s3 w54 w9 r s2 w4 s3',# 08 Aug 2018
# 'player-vflM-t6FF/en_US/base' => '17752 w13 s2 w69 r w7 r', # 09 Aug 2018
# 'player-vflCT6NPT/en_US/base' => '17757 r s3 r s1 w42 s2 w45 r',# 14 Aug 2018
# 'player-vflbOM9Vw/en_US/base' => '17757 r s3 r s1 w42 s2 w45 r',# 14 Aug 2018
# 'player-vfl2n6fnF/en_US/base' => '17763 w4 s2 r s2 r s1', # 20 Aug 2018
# 'player-vflvPJ1R-/en_US/base' => '17765 r w17 w23 r w19 s1 r w57 s1',# 22 Aug 2018
# 'player_ias-vflkstHEy/en_US/base' => '17765 r w17 w23 r w19 s1 r w57 s1',# 22 Aug 2018
# 'player-vflGZmBoI/en_US/base' => '17770 s2 w13 s1 r s1 w69 r', # 27 Aug 2018
# 'player-vflEdLQ9n/en_US/base' => '17771 w23 s2 r w6', # 28 Aug 2018
# 'player-vflZ8oBLt/en_US/base' => '17772 s1 w33 s2 r s2 w23 r w43 r',# 29 Aug 2018
# 'player_ias-vflAarKGf/en_US/base' => '17773 w7 w52 s2 w48 r', # 30 Aug 2018
# 'player-vfliK45Zi/en_US/base' => '17773 w7 w52 s2 w48 r', # 30 Aug 2018
# 'player-vflPJRQDm/en_US/base' => '17777 w3 r s1 r s1 r w61 w20 s2',# 03 Sep 2018
# 'player-vflkiBRCU/en_US/base' => '17780 w61 r w46', # 06 Sep 2018
# 'player_ias-vflIVQ4xT/en_US/base' => '17781 r w30 r s1 r w61 s2 w70',# 07 Sep 2018
# 'player-vflvABTsY/en_US/base' => '17781 r w30 r s1 r w61 s2 w70',# 07 Sep 2018
# 'player-vflHei2l6/en_US/base' => '17782 w16 s1 w54', # 08 Sep 2018
# 'player-vflXCnVjq/en_US/base' => '17785 w53 s3 r w4 s3', # 11 Sep 2018
# 'player-vfl6tBysE/en_US/base' => '17786 r w13 s1 w16 r s2', # 12 Sep 2018
# 'player-vflkUTZn2/en_US/base' => '17787 s2 w8 r w43 s2 w11 r', # 13 Sep 2018
# 'player_ias-vflblC9dU/en_US/base' => '17787 s2 w8 r w43 s2 w11 r',# 13 Sep 2018
# 'player-vflxKLgto/en_US/base' => '17791 s1 r w61 r w48 w55 r', # 17 Sep 2018
# 'player-vflvTxtee/en_US/base' => '17792 r w39 s2', # 18 Sep 2018
# 'player-vfl8DfiXg/en_US/base' => '17793 w67 s3 r w4 r s3 r', # 19 Sep 2018
# 'player-vflUpPEZ9/en_US/base' => '17795 s3 r w33 w58 w7 s3 w11 s1',# 21 Sep 2018
# 'player-vfl7GcuOz/en_US/base' => '17798 r w5 r s2 r s2', # 24 Sep 2018
# 'player_ias-vflXs7juB/en_US/base' => '17798 r w5 r s2 r s2', # 24 Sep 2018
# 'player-vfl07ioI6/en_US/base' => '17799 w50 r s2 r', # 26 Sep 2018
# 'player-vflB24EJ3/en_US/base' => '17806 w23 r s2 r w68 w30 r s1',# 02 Oct 2018
# 'player-vfl-vYfC3/en_US/base' => '17807 w49 w25 w4 r w32 s1 w17 w23',# 03 Oct 2018
# 'player_ias-vfleYRcGJ/en_US/base' => '17807 w49 w25 w4 r w32 s1 w17 w23',# 03 Oct 2018
# 'player-vflFV3riw/en_US/base' => '17812 s1 r w45 w56 s3', # 08 Oct 2018
# 'player-vfl8R-b3G/en_US/base' => '17813 s2 r s3 r s2 w32 w35', # 09 Oct 2018
# 'player-vflGzpM1Y/en_US/base' => '17814 r s2 w35 s2 r w4 r', # 10 Oct 2018
# 'player_ias-vflBYAvAP/en_US/base' => '17814 r s2 w35 s2 r w4 r',# 10 Oct 2018
# 'player-vflO1Ey5k/en_US/base' => '17814 r s2 w35 s2 r w4 r', # 10 Oct 2018
# 'player-vflHCRjhV/en_US/base' => '17819 w33 w25 s1 w60 r s1 w70 r',# 15 Oct 2018
# 'player-vflATXXzL/en_US/base' => '17821 w67 s2 w33 w30 r', # 17 Oct 2018
# 'player-vflICk6QU/en_US/base' => '17822 s1 w58 s3 r', # 18 Oct 2018
# 'player_ias-vflOjC-XR/en_US/base' => '17822 s1 w58 s3 r', # 18 Oct 2018
# 'player-vflrZpM9e/en_US/base' => '17824 s1 r w3 r s1', # 20 Oct 2018
# 'player-vflsLe3jn/en_US/base' => '17827 r s3 w36 w9 s3 w31 r s2',# 23 Oct 2018
# 'player-vflQJVaZA/en_US/base' => '17828 w23 s3 w14 s1', # 24 Oct 2018
# 'player_ias-vflTuSS6p/en_US/base' => '17829 r w29 r s2', # 25 Oct 2018
# 'player-vflXM3IU_/en_US/base' => '17829 r w29 r s2', # 25 Oct 2018
# 'player-vflVce_C4/en_US/base' => '17834 r s2 w35 r s2 w15 w48', # 30 Oct 2018
# 'player_ias-vfl4nRobu/en_US/base' => '17836 w2 s2 r w48 w47 s3 r',# 01 Nov 2018
# 'player-vflKOteNp/en_US/base' => '17836 w2 s2 r w48 w47 s3 r', # 01 Nov 2018
# 'player-vfls4aurX/en_US/base' => '17841 w61 w13 w16 s1 r w43 s1 w52 r',# 06 Nov 2018
# 'player_ias_remote_ux-vfl-mZlA8/en_US/base' => '17841 w61 w13 w16 s1 r w43 s1 w52 r',# 06 Nov 2018
# 'player_ias-vfl6LN1Nj/en_US/base' => '17841 w61 w13 w16 s1 r w43 s1 w52 r',# 06 Nov 2018
# 'player_ias-vflxtHgXu/en_US/base' => '17845 r w23 r s2 w43 s1 w17',# 10 Nov 2018
# 'player-vflVKnssA/en_US/base' => '17845 r w23 r s2 w43 s1 w17', # 10 Nov 2018
# 'player_ias-vflplkb5-/en_US/base' => '17849 w31 r w1', # 14 Nov 2018
# 'player_ias-vflof8Kxx/en_US/base' => '17850 w64 w66 s2 w49 s2 r',# 15 Nov 2018
# 'player-vflWnjS_n/en_US/base' => '17850 w64 w66 s2 w49 s2 r', # 15 Nov 2018
# 'player_ias-vflfmGnOV/en_US/base' => '17855 w49 s3 r w44 w32 s3',# 20 Nov 2018
# 'player-vfl718orE/en_US/base' => '17855 w49 s3 r w44 w32 s3', # 20 Nov 2018
# 'player_ias_remote_ux-vflQA1gIN/en_US/base' => '17855 w49 s3 r w44 w32 s3',# 20 Nov 2018
# 'player-vflyUEprh/en_US/base' => '17856 w36 s3 w20 r w70 s3 r w29',# 21 Nov 2018
# 'player-vflX9LQZI/en_US/base' => '17862 s1 r s2 w16 s1 r s2 w26',# 27 Nov 2018
# 'player-vfl-6ni-d/en_US/base' => '17863 w66 r s1 r', # 28 Nov 2018
# 'player-vflBGiA6J/en_US/base' => '17864 w15 s3 w19 s3 r', # 29 Nov 2018
# 'player-vflooFjaN/en_US/base' => '17865 w49 r w24 r', # 30 Nov 2018
# 'player-vflRjqq_w/en_US/base' => '17869 w18 s2 w6 s2 r', # 04 Dec 2018
# 'player-vflrVSewe/en_US/base' => '17871 w33 w18 s1 w34 r s2 r s2',# 06 Dec 2018
# 'player-vflf5K4kk/en_US/base' => '17871 w33 w18 s1 w34 r s2 r s2',# 06 Dec 2018
# 'player_ias-vflA8SWf9/en_US/base' => '17872 s1 w51 r s1 w67 s1 w16 r s1',# 07 Dec 2018
# 'player_ias-vflsBa1u2/en_US/base' => '17876 r w48 r w65', # 11 Dec 2018
# 'player_ias-vflXas3a_/en_US/base' => '17877 s1 r w15 w8 r w12', # 12 Dec 2018
# 'player_ias-vfl4UMq4Z/en_US/base' => '17880 w22 w43 s1 w10 w8 r s3 r s3',# 15 Dec 2018
# 'player_ias-vflNodBFa/en_US/base' => '17882 s3 w57 r s1', # 17 Dec 2018
# 'player_ias-vflztg6e0/en_US/base' => '17884 w59 w12 s2 r s1 w10 r',# 19 Dec 2018
# 'player_ias-vflSzU_20/en_US/base' => '17885 w34 r w68 s1 w38 r',# 20 Dec 2018
# 'player-vflpOZkP0/en_US/base' => '17885 w34 r w68 s1 w38 r', # 20 Dec 2018
# 'player_ias-vflWb9AD2/en_US/base' => '17886 s3 w70 r w54 r w26 w43 r s1',# 21 Dec 2018
# 'player_ias-vflNriX6t/en_US/base' => '17903 w50 r w62 s1 w16 s1',# 07 Jan 2019
# 'player_ias-vfls55OIb/en_US/base' => '17905 w24 w56 r w5', # 09 Jan 2019
# 'player_ias-vfl_235rs/en_US/base' => '17906 w15 w53 s3 r', # 10 Jan 2019
# 'player_ias-vflsx9jEl/en_US/base' => '17908 s2 w63 s1 r s2 r w69 w47 w8',# 12 Jan 2019
# 'player_ias-vflzJWmZN/en_US/base' => '17910 s1 r w44', # 14 Jan 2019
# 'player_ias-vfl-jbnrr/en_US/base' => '17913 w69 r w55 s1 r s2 r s3',# 17 Jan 2019
# 'player_ias-vflH-Ze7P/en_US/base' => '17915 r s2 r w61 s2 w42', # 19 Jan 2019
# 'player_ias-vflSfmvrF/en_US/base' => '17919 r w18 r', # 23 Jan 2019
# 'player_ias-vflLIeur2/en_US/base' => '17920 w62 r w6 w2 w39 w2',# 24 Jan 2019
# 'player_ias-vflok_OV_/en_US/base' => '17921 w59 s3 w41 s1', # 25 Jan 2019
# 'player_ias-vflemibiK/en_US/base' => '17922 w4 w1 w27 r s1 r s1 w11',# 26 Jan 2019
# 'player_ias-vflehrYuM/en_US/base' => '17926 s1 w63 w25', # 30 Jan 2019
# 'player_ias-vfl71PH-c/en_US/base' => '17931 s2 r w55 s1 r w41 w24 r',# 04 Feb 2019
# 'player_ias-vflcS3GOw/en_US/base' => '17932 s1 w55 w49', # 05 Feb 2019
# 'player_ias-vflYv1bWD/en_US/base' => '17933 s3 r w23 s1 w9 r w35 w45 s1',# 06 Feb 2019
# 'player_ias-vflP40QgO/en_US/base' => '17936 r s1 w50 w1', # 09 Feb 2019
# 'player_ias-vflRtzyEV/en_US/base' => '17940 r s2 r s1 r', # 13 Feb 2019
# 'player_ias-vfl9fQPE9/en_US/base' => '17947 w53 w17 r w42 w19 w3',# 20 Feb 2019
# 'player_ias-vflq4d8Te/en_US/base' => '17949 r s3 w22 r w58 s3', # 22 Feb 2019
# 'player_ias-vflkaDufl/en_US/base' => '17952 r w36 r w3 s1 r s2',# 25 Feb 2019
# 'player_ias-vflfI-Uux/en_US/base' => '17953 w12 s3 w15 r s3', # 26 Feb 2019
# 'player_ias-vflX2rhq7/en_US/base' => '17954 w54 w25 s1 r w62 w35 w17',# 27 Feb 2019
# 'player_ias-vflpVg286/en_US/base' => '17957 w27 s1 r s1 r w27 s3 w24',# 02 Mar 2019
# 'player_ias-vflwK9E86/en_US/base' => '17960 w36 s1 r w48 w23 w66 s2',# 05 Mar 2019
# 'player_ias-vfl0Xjrhe/en_US/base' => '17960 w36 s1 r w48 w23 w66 s2',# 05 Mar 2019
# 'player_ias-vflca9-f7/en_US/base' => '17962 s2 r s3 r s1', # 07 Mar 2019
# 'player_ias-vflgQJQnf/en_US/base' => '17967 w18 w39 r w65', # 12 Mar 2019
# 'player_ias-vflkyt12p/en_US/base' => '17968 s2 w12 r w62 s1', # 13 Mar 2019
# 'player_ias-vflhRp6T6/en_US/base' => '17969 w23 r s3 r w16 w61 w48 w47',# 14 Mar 2019
# 'player_ias-vfljLzLcF/en_US/base' => '17971 w8 s3 r s3', # 16 Mar 2019
# 'player_ias-vfl0mwZa0/en_US/base' => '17974 s3 w61 s2 r s1 w48 s2 r',# 19 Mar 2019
# 'player_ias-vflELyWbw/en_US/base' => '17974 s3 w61 s2 r s1 w48 s2 r',# 19 Mar 2019
# 'player_ias-vflGPko2h/en_US/base' => '17976 w23 s1 r s2 r s1 r w23 s2',# 21 Mar 2019
# 'player_ias-vflrQPnxT/en_US/base' => '17981 w41 w33 w28 w18 w31 w23',# 26 Mar 2019
# 'player_ias-vflUi8DdH/en_US/base' => '17984 w53 s2 r s3 r s2 w60 s2 w27',# 29 Mar 2019
# 'player_ias-vflx77j21/en_US/base' => '17984 w53 s2 r s3 r s2 w60 s2 w27',# 29 Mar 2019
# 'player_ias-vflh3Ltot/en_US/base' => '17989 s1 w44 w68 r w59 w35 s3 w7',# 03 Apr 2019
# 'player_ias-vflo38I3N/en_US/base' => '17989 s1 w44 w68 r w59 w35 s3 w7',# 03 Apr 2019
# 'player_ias-vflNoyOhW/en_US/base' => '17990 w19 w16 r s2', # 04 Apr 2019
# 'player_ias-vflLXg1wb/en_US/base' => '17992 w55 s1 w4 r', # 06 Apr 2019
# 'player_ias-vflQXSOCw/en_US/base' => '17995 r w11 s3 r w12 r s3 r s3',# 09 Apr 2019
# 'player_ias-vflptN-I_/en_US/base' => '17995 r w11 s3 r w12 r s3 r s3',# 09 Apr 2019
# 'player_ias_remote_ux-vflu5jbVI/en_US/base' => '17995 r w11 s3 r w12 r s3 r s3',# 09 Apr 2019
# 'player_ias-vflCpof0M/en_US/base' => '18002 w11 r s1 w65', # 16 Apr 2019
# 'player_ias-vfloNowYZ/en_US/base' => '18003 w7 r s2 r s3 r s1 r s3',# 17 Apr 2019
# 'player_ias-vflYEQ3rp/en_US/base' => '18005 r s2 w26 r s2 w70 r',# 19 Apr 2019
# 'player_ias-vflox1iTd/en_US/base' => '18009 w57 w32 s3 r', # 23 Apr 2019
# 'player_ias-vflzZ-uwH/en_US/base' => '18010 w12 w24 r s2 w34 w62 s2 w70 s2',# 24 Apr 2019
# 'player_ias-vflU6l_su/en_US/base' => '18011 s1 r w56 s2 r s2 r',# 25 Apr 2019
# 'player_ias-vfl9qGq_O/en_US/base' => '18012 s3 r w4 r s2 w6 r', # 26 Apr 2019
# 'player_ias-vflXZ59b4/en_US/base' => '18016 w29 s2 r s2 r', # 30 Apr 2019
# 'player_ias-vflOwsp3q/en_US/base' => '18017 s2 r w68 r s3', # 01 May 2019
# 'player_ias-vfl61X81T/en_US/base' => '18019 r w58 s2 r s3 r s1 w70 r',# 03 May 2019
# 'player_ias-vflisCO7O/en_US/base' => '18022 s2 w36 s2', # 06 May 2019
# 'player_ias-vflmRtaf6/en_US/base' => '18023 s3 w30 r s1 r s1 r s3',# 07 May 2019
# 'player_ias-vflQTyJbT/en_US/base' => '18024 w15 r w33 w28 s3 w11 s3 r',# 08 May 2019
# 'player_ias-vflHkKkEW/en_US/base' => '18025 w70 w67 w2 w19 r w45 w56 s3 r',# 09 May 2019
# 'player_ias-vfl5CuSGB/en_US/base' => '18030 r w42 w15 r', # 14 May 2019
# 'player_ias-vflOR94oD/en_US/base' => '18031 s1 w41 s2', # 15 May 2019
# 'player_ias-vflFo4HCs/en_US/base' => '18033 r s3 w4 w65 r w45 w50 s2',# 17 May 2019
# 'player_ias-vflj9IN-5/en_US/base' => '18037 r s1 w41 r w37 s3 r w27 r',# 21 May 2019
# 'player_ias-vfld3bR7p/en_US/base' => '18038 r w45 w33 r s2 r w36 w20 r',# 22 May 2019
# 'player_ias-vflusCuE1/en_US/base' => '18039 w35 s3 r w37 w1 w65',# 23 May 2019
# 'player_ias-vflS2RkAM/en_US/base' => '18040 w56 w48 s3 w64 s1 r s2 w43',# 24 May 2019
# 'player_ias-vfl1T0cVh/en_US/base' => '18041 s2 w62 r s3 w21 r w52',# 25 May 2019
# 'player_ias-vflVstpzG/en_US/base' => '18045 s2 r s2 r s2 r w51 r',# 29 May 2019
# 'player_ias-vfl6QiMWf/en_US/base' => '18045 s2 r s2 r s2 r w51 r',# 29 May 2019
# 'player_ias-vfl5VAqDi/en_US/base' => '18046 r w41 r w62 s2 r', # 30 May 2019
# 'player_ias_remote_ux-vfldk63YK/en_US/base' => '18048 w64 w62 w66 r w29 r s3 r s1',# 01 Jun 2019
# 'player_ias-vfl-SOYuS/en_US/base' => '18051 s3 r s3 w61 r', # 04 Jun 2019
# 'player_ias-vflo4i8HU/en_US/base' => '18052 s1 r s2', # 05 Jun 2019
# 'player_ias-vfl25EWhw/en_US/base' => '18053 w38 r w7 w18 r w21',# 06 Jun 2019
# 'player_ias-vfldhBbts/en_US/base' => '18055 w12 w59 s2 w24 r', # 08 Jun 2019
# 'player_ias-vflzbi_R5/en_US/base' => '18060 w40 w67 r w7 s1', # 13 Jun 2019
# 'player_ias-vfltBCqwT/en_US/base' => '18065 r s3 r w65 r s1 r w20',# 18 Jun 2019
# 'player_ias-vfloOZja_/en_US/base' => '18066 w20 r w17', # 19 Jun 2019
# 'player_ias-vfl49f_g4/en_US/base' => '18066 w20 r w17', # 19 Jun 2019
# 'player_ias-vflnDDQuY/en_US/base' => '18071 w51 r w20 r s1 r s3 r',# 24 Jun 2019
# 'player_ias-vflIucxJp/en_US/base' => '18072 s2 r w54 s1', # 25 Jun 2019
# 'player_ias-vflv00tk0/en_US/base' => '18072 s2 r w54 s1', # 25 Jun 2019
# 'player_ias-vflxACNZ2/en_US/base' => '18074 w8 r w61 r s3 r w47 s2 w11',# 27 Jun 2019
# 'player_ias-vfliSA6ma/en_US/base' => '18079 s2 w5 s2 r', # 02 Jul 2019
# 'player_ias-vfl7A4uZG/en_US/base' => '18081 s1 w24 s3', # 04 Jul 2019
# 'player_ias-vflojvMjn/en_US/base' => '18086 r s3 r s2 r w42 r w14 w4',# 09 Jul 2019
# 'player_ias-vfladvVLE/en_US/base' => '18087 r w60 r s3 r w46', # 10 Jul 2019
# 'player_ias-vflK6rDhN/en_US/base' => '18088 w70 s2 w47 s2 w31 s1',# 11 Jul 2019
# 'player_ias-vfl_2S9FT/en_US/base' => '18092 s2 w58 s3', # 15 Jul 2019
# 'player_ias-vflH7QZGl/en_US/base' => '18092 s2 w58 s3', # 15 Jul 2019
# 'player_ias-vflFxFa3y/en_US/base' => '18095 r s3 r w12 r', # 18 Jul 2019
# 'player_ias-vfl_rJBTq/en_US/base' => '18101 w67 r s1 w61 r', # 24 Jul 2019
# 'player_ias-vfl7A19HM/en_US/base' => '18102 r s1 r s1 r s1', # 25 Jul 2019
# 'player_ias-vflan9mDf/en_US/base' => '18104 s3 w34 s1 w24 r w45',# 27 Jul 2019
# 'player_ias-vflPI0brM/en_US/base' => '18106 s2 w23 w22 r s3 w1 s1 r s2',# 29 Jul 2019
# 'player_ias-vfl3cxFuT/en_US/base' => '18114 s2 r s2 r s1 r', # 06 Aug 2019
# 'player_ias-vfliz8bvh/en_US/base' => '18114 s2 r s2 r s1 r', # 06 Aug 2019
# 'player_ias-vflazKpcG/en_US/base' => '18117 w3 r w59 s3 r w14 w14 s2',# 09 Aug 2019
# 'player_ias-vfl0ft1-Z/en_US/base' => '18117 w3 r w59 s3 r w14 w14 s2',# 09 Aug 2019
# 'player_ias-vflOQSPfo/en_US/base' => '18120 r w24 r w41 r w2', # 12 Aug 2019
# 'player_ias-vfluLgj-p/en_US/base' => '18120 r w24 r w41 r w2', # 12 Aug 2019
# 'player_ias-vfl4ZkW5S/en_US/base' => '18121 s2 r w45 r w23', # 13 Aug 2019
# 'player_ias-vflLAbfAI/en_US/base' => '18122 w9 s3 r s3 r w27 r s1 w64',# 14 Aug 2019
# 'player_ias-vfl4WD7HR/en_US/base' => '18123 r w23 w30 r w40 r w65 w38',# 15 Aug 2019
# 'player_ias-vflubst9M/en_US/base' => '18123 r w23 w30 r w40 r w65 w38',# 15 Aug 2019
# 'player_ias-vflshR-OW/en_US/base' => '18127 r s1 w19', # 19 Aug 2019
# 'player_ias-vflR4bDhL/en_US/base' => '18128 r w34 w50 s1 r w68',# 20 Aug 2019
# 'player_ias-vflRCamp0/en_US/base' => '18128 r w34 w50 s1 r w68',# 20 Aug 2019
# 'player_ias-vfle9vlRm/en_US/base' => '18130 w20 s1 r s3 w35', # 22 Aug 2019
# 'player_ias-vflQ3KR0i/en_US/base' => '18130 w20 s1 r s3 w35', # 22 Aug 2019
# 'player_ias-vflRUnhQH/en_US/base' => '18134 r s2 r w37 s1 w9 s1 r',# 26 Aug 2019
# 'player_ias-vflxmW2zg/en_US/base' => '18135 r s3 w56 r s3 w11 s2 w57',# 27 Aug 2019
# 'player_ias-vflkpoWE6/en_US/base' => '18135 r s3 w56 r s3 w11 s2 w57',# 27 Aug 2019
# 'player_ias-vflB-cY7Z/en_US/base' => '18137 s2 r s2 r w37 s2 r s1',# 29 Aug 2019
# 'player_ias-vflt4leIo/en_US/base' => '18138 w35 w30 r w2 s3 r w8 s2',# 30 Aug 2019
# 'player_ias-vflL8qGmP/en_US/base' => '18142 r s3 r w36 w42', # 03 Sep 2019
# 'player_ias-vfl-_sce4/en_US/base' => '18143 r w52 r s1 w20 r', # 04 Sep 2019
# 'player_ias-vfl9X5OgR/en_US/base' => '18143 r w52 r s1 w20 r', # 04 Sep 2019
# 'player_ias-vflpfvoVH/en_US/base' => '18149 r w52 r w11 s2 r s2',# 10 Sep 2019
# 'player_ias-vfl2pEEGH/en_US/base' => '18151 w25 s3 w11 s1 w25 w29 r w28 s2',# 12 Sep 2019
# 'player_ias-vflbxHFzR/en_US/base' => '18152 r s2 w48 r', # 13 Sep 2019
# 'player_ias-vfl8E5RS_/en_US/base' => '18154 r s2 r w14 w70 w51',# 15 Sep 2019
# 'player_ias-vflBg-eSP/en_US/base' => '18156 w15 r s1 w60 s2 w47 s3 r',# 17 Sep 2019
# 'player_ias-vflFWJS6F/en_US/base' => '18159 w67 s1 r s3', # 20 Sep 2019
# 'player_ias-vfleGIwAA/en_US/base' => '18159 w67 s1 r s3', # 20 Sep 2019
# 'player_ias-vflf1C3PW/en_US/base' => '18163 r w30 s3 w48 w54 s2',# 24 Sep 2019
# 'player_ias-vflSXUoF7/en_US/base' => '18164 w59 s3 w31 s2 w3 s3 r s1 r',# 25 Sep 2019
# 'player_ias-vfl-Yp-48/en_US/base' => '18166 r w69 w9 r w58 s3 r w8 s2',# 27 Sep 2019
# 'player_ias-vflKkjtvu/en_US/base' => '18169 w51 r s2', # 30 Sep 2019
# 'player_ias-vflhIMmpR/en_US/base' => '18170 w24 w7 r w54 s1 r w65',# 01 Oct 2019
# 'player_ias-vflHafnhm/en_US/base' => '18171 s2 w36 w34', # 02 Oct 2019
# 'player_ias-vflgyMPMy/en_US/base' => '18172 w15 r s1 w33 w44 w39 w38 r s1',# 03 Oct 2019
# 'player_ias-vflaagmZn/en_US/base' => '18174 w67 r w21 r s1', # 05 Oct 2019
# 'player_ias-vflLF-qe_/en_US/base' => '18174 w67 r w21 r s1', # 05 Oct 2019
# 'player_ias-vflm8XufX/en_US/base' => '18177 r w22 s3 w42 r', # 08 Oct 2019
# 'player_ias-vflp-7p2p/en_US/base' => '18177 r w22 s3 w42 r', # 08 Oct 2019
# 'player_ias-vflMzJYzW/en_US/base' => '18179 w68 s1 w57 r s1 w23 s1',# 10 Oct 2019
# 'player_ias-vflNSW9LL/en_US/base' => '18180 s2 r s3 w65 s3 r s1',# 11 Oct 2019
# 'player_ias-vflZy72vV/en_US/base' => '18183 w33 w37 w16 r s3', # 14 Oct 2019
# 'player_ias-vflCPQUIL/en_US/base' => '18184 r w17 s1 w36 w37 s2',# 15 Oct 2019
# 'player_ias-vflqs_iv4/en_US/base' => '18185 w8 w43 r s1 w40 s1',# 16 Oct 2019
# 'player_ias-vflzOmLM_/en_US/base' => '18185 w8 w43 r s1 w40 s1',# 16 Oct 2019
# 'player_ias-vflrnurMS/en_US/base' => '18188 w26 s3 w15 w26 s2 r',# 19 Oct 2019
# 'player_ias-vflYUXieR/en_US/base' => '18190 w4 r w31 s1 r s1 r',# 21 Oct 2019
# 'player_ias-vflfmpDLj/en_US/base' => '18192 r w5 r s2 r w34 r w16 r',# 23 Oct 2019
# 'player_ias-vflID-9v_/en_US/base' => '18192 r w5 r s2 r w34 r w16 r',# 23 Oct 2019
# 'player_ias-vflsEMaQv/en_US/base' => '18193 r w18 w7 w12 s3 r', # 24 Oct 2019
# 'player_ias-vflje5zha/en_US/base' => '18194 s2 w31 s3 w32 w2 s1 r',# 25 Oct 2019
# 'player_ias-vflLT_S1E/en_US/base' => '18198 r w65 r', # 29 Oct 2019
# 'player_ias-vflGnuoiU/en_US/base' => '18199 w1 r s1 r', # 30 Oct 2019
# 'player_ias-vflO1GesB/en_US/base' => '18200 w13 w56 s2 r s2', # 31 Oct 2019
# 'player_ias-vflaGJCFN/en_US/base' => '18211 s3 w64 w35 r', # 11 Nov 2019
# 'player_ias-vflu-qpOO/en_US/base' => '18211 s3 w64 w35 r', # 11 Nov 2019
# 'player_ias-vflFlp-mq/en_US/base' => '18214 w70 r w67 w34', # 14 Nov 2019
# 'player_ias-vflhctYB3/en_US/base' => '18216 w6 r s2', # 16 Nov 2019
# 'player_ias-vfl3Ub7Lu/en_US/base' => '18218 r s2 r s3', # 18 Nov 2019
# 'player_ias-vflss95Jx/en_US/base' => '18219 s3 r w58 s1 w18', # 19 Nov 2019
# 'player_ias-vfl8EyRMW/en_US/base' => '18219 s3 r w58 s1 w18', # 19 Nov 2019
# 'player_ias-vflHWPv1o/en_US/base' => '18220 w68 r s1', # 20 Nov 2019
# 'player_ias-vflaU3CuL/en_US/base' => '18222 r s3 r w15 w33 w26 w29 w4 s2',# 22 Nov 2019
# 'player_ias-vfly7X4ko/en_US/base' => '18225 s1 w24 r w23 w32', # 25 Nov 2019
# 'player_ias-vflGkJskG/en_US/base' => '18227 r w24 r w25 s3 r', # 27 Nov 2019
# 'player_ias-vflVQDSr2/en_US/base' => '18229 s1 r s2 r s3 w16 w1',# 29 Nov 2019
# 'player_ias-vfl5RP2xB/en_US/base' => '18233 w28 s3 w15 w58 w54 r s1',# 03 Dec 2019
# 'player_ias-vflvcmhHb/en_US/base' => '18234 w38 s1 w37 s3 r w65 w36 r s1',# 04 Dec 2019
# 'player_ias-vfliASpvT/en_US/base' => '18235 w52 s1 w68 w44 w57 s2 r',# 05 Dec 2019
# 'player_ias-vflYr569U/en_US/base' => '18235 w52 s1 w68 w44 w57 s2 r',# 05 Dec 2019
# 'player_ias-vflhdSMEK/en_US/base' => '18236 r w44 s3', # 06 Dec 2019
# 'player_ias-vflDT4YKf/en_US/base' => '18240 w4 s3 r s3 w47', # 10 Dec 2019
# 'player_ias-vflZn7_Zv/en_US/base' => '18240 w4 s3 r s3 w47', # 10 Dec 2019
# 'player_ias-vfl7Ksmll/en_US/base' => '18242 w70 s2 w19 s3', # 12 Dec 2019
# 'player_ias-vfl22ubNH/en_US/base' => '18249 w26 s1 r s2 r s1 w45 r',# 19 Dec 2019
# 'player_ias-vflMn34bn/en_US/base' => '18268 s1 r s1 w5', # 07 Jan 2020
# 'player_ias-vflY-95hF/en_US/base' => '18269 w58 s2 r s1 r', # 08 Jan 2020
# 'player_ias-vflDLieI-/en_US/base' => '18269 w58 s2 r s1 r', # 08 Jan 2020
# 'player_ias-vflJiqSE7/en_US/base' => '18272 w32 w13 s2 r', # 11 Jan 2020
# 'player_ias-vflO3yVXL/en_US/base' => '18281 w42 w65 s1 r', # 20 Jan 2020
# 'player_ias-vflwruZYD/en_US/base' => '18282 s2 r w36', # 21 Jan 2020
# 'player_ias-vfl7lL1_p/en_US/base' => '18284 w8 r s2 r s1 r w59 s2',# 23 Jan 2020
# 'player_ias-vfl1GpCbm/en_US/base' => '18290 w46 r s2 r', # 29 Jan 2020
'player_ias-vflbwmoEe/en_US/base' => '18295 r w51 r s2 w39 r w6 w58',# 03 Feb 2020
'player_ias-vflZgL1a2/en_US/base' => '18298 s3 r s2 r w67 w11', # 06 Feb 2020
'player_ias-vflrgVy3r/en_US/base' => '18302 r w53 w5', # 10 Feb 2020
'player_ias-vfla5PwTn/en_US/base' => '18302 r w53 w5', # 10 Feb 2020
'player_ias-vfl5eNx6Z/en_US/base' => '18304 s2 r w9 w53 r w52 w68 r',# 12 Feb 2020
'player_ias-vflT0MlXN/en_US/base' => '18305 w8 r w57 w44 s1 w53 r',# 13 Feb 2020
'player_ias-vflp5fPn0/en_US/base' => '18305 w8 r w57 w44 s1 w53 r',# 13 Feb 2020
'player_ias-vfl3Rvzpw/en_US/base' => '18311 w26 r w18 s1 r s2 r w37 s1',# 19 Feb 2020
'player_ias-vfl5Kte8U/en_US/base' => '18317 w30 w63 w15 s3 r w50 s3 r w58',# 25 Feb 2020
'player_ias-vflMJC6WU/en_US/base' => '18323 r s3 w58 r s3 r w16 r w20',# 02 Mar 2020
'player_ias-vfl1Ng2HU/en_US/base' => '18324 w34 s1 w69 s3 r s2 w53',# 03 Mar 2020
'player_ias-vfle4a9aa/en_US/base' => '18324 w34 s1 w69 s3 r s2 w53',# 03 Mar 2020
'player_ias-vfl5C38RC/en_US/base' => '18325 w50 s3 w33 r s2 w6 r w16 r',# 04 Mar 2020
'player_ias-vflQm4drh/en_US/base' => '18330 s1 w21 r w34 w18 w63',# 09 Mar 2020
'player_ias-vflQJ_oH3/en_US/base' => '18332 w61 r s2', # 11 Mar 2020
'player_ias-vflJMXyvH/en_US/base' => '18337 w29 w41 s2 w23 s2 r s2',# 17 Mar 2020
'player_ias-vflSBTliv/en_US/base' => '18338 w15 s1 w43 s3 r s3',# 17 Mar 2020
'player_ias-vflEO2H8R/en_US/base' => '18340 s3 w28 r s3', # 19 Mar 2020
'player_ias-vfl2Bfj4C/en_US/base' => '18341 w70 s1 w62 r w69 r s1',# 20 Mar 2020
'player_ias-vflJalPc2/en_US/base' => '18344 r w23 w65 w53', # 23 Mar 2020
'player_ias-vfl_gAQka/en_US/base' => '18351 w45 s1 w52 r w36 r w2',# 30 Mar 2020
'player_ias-vfluKIiVl/en_US/base' => '18352 r w67 w37 s2 w59', # 31 Mar 2020
'player_ias-vflBAN1y0/en_US/base' => '18352 r w67 w37 s2 w59', # 31 Mar 2020
'player_ias-vfl5cScu9/en_US/base' => '18353 r s3 r', # 01 Apr 2020
'player_ias-vfl6MUxK7/en_US/base' => '18354 w35 r w42 w11 w48 s3 w70 s1',# 02 Apr 2020
'player_ias-vfl_CsZz6/en_US/base' => '18356 w22 w17 s2 r s1 w8',# 04 Apr 2020
'player_ias-vfl6VLxLZ/en_US/base' => '18359 s3 w39 s2 w1 s1 w35 w51 s2 r',# 07 Apr 2020
'4fbb4d5b/player_ias.vflset/en_US/base' => '18359 s3 w39 s2 w1 s1 w35 w51 s2 r',# 07 Apr 2020
'5478d871/player_ias.vflset/en_US/base' => '18366 w38 w7 s2', # 14 Apr 2020
'f676c671/player_ias.vflset/en_US/base' => '18368 s2 w4 w46 s2 w34 w59 r',# 16 Apr 2020
'bfb2a3b4/player_ias.vflset/en_US/base' => '18372 s3 w21 r s3 r w36',# 20 Apr 2020
'45e4d51d/player_ias.vflset/en_US/base' => '18375 w17 w16 r w25 r w50 w35',# 23 Apr 2020
'0374edcb/player_ias.vflset/en_US/base' => '18379 w8 w43 w10 w34',# 27 Apr 2020
);
my $cipher_warning_printed_p = 0;
sub decipher_sig($$$$$) {
my ($url, $id, $cipher, $signature, $via) = @_;
return $signature unless defined ($cipher);
my $orig = $signature;
my @s = split (//, $signature);
my $c = $ciphers{$cipher};
if (! $c) {
print STDERR "$progname: WARNING: $id: unknown cipher $cipher\n"
if ($verbose > 1 && !$cipher_warning_printed_p);
$c = guess_cipher ($cipher, 0, $cipher_warning_printed_p);
$ciphers{$cipher} = $c;
$cipher_warning_printed_p = 1;
}
$c =~ s/([^\s])([a-z])/$1 $2/gs;
my ($sts) = $1 if ($c =~ s/^(\d+)\s*//si);
foreach my $c (split(/\s+/, $c)) {
if ($c eq '') { }
elsif ($c eq 'r') { @s = reverse (@s); }
elsif ($c =~ m/^s(\d+)$/s) { @s = @s[$1 .. $#s]; }
elsif ($c =~ m/^w(\d+)$/s) {
my $a = 0;
my $b = $1 % @s;
($s[$a], $s[$b]) = ($s[$b], $s[$a]);
}
else { errorI ("bogus cipher: $c"); }
}
$signature = join ('', @s);
my $L1 = length($orig);
my $L2 = length($signature);
if ($verbose > 4 && $signature ne $orig) {
print STDERR ("$progname: $id: translated sig, $sts $cipher:\n" .
"$progname: old: $L1: $orig\n" .
"$progname: new: $L2: $signature\n");
}
if (! ($signature =~ m/^[\dA-F]{30,}\.[\dA-F]{30,}$/s)) {
$error_whiteboard .= ("$id: suspicious signature: $sts $cipher:\n" .
"$progname: url: $url\n" .
"$progname: via: $via\n" .
"$progname: old: $L1: $orig\n" .
"$progname: new: $L2: $signature\n");
}
return $signature;
}
sub page_cipher_base_url($$) {
my ($url, $body) = @_;
$body =~ s/\\//gs;
# Sometimes but not always the "ux.js" file comes before "base.js".
# But in the past, the file was not named "base.js"...
# The proper document is the one that starts with "var _yt_player =".
my ($c) = ($body =~ m@/jsbin/((?:html5)?player[-_][^<>\"\']+?/base)\.js@s);
($c) = ($body =~ m@/jsbin/((?:html5)?player[-_][^<>\"\']+?)\.js@s)
unless defined($c);
($c) = ($body =~ m@/player/([^<>\"\']+/player[-_][^<>\"\']+/base)\.js@s)
unless defined($c);
$c =~ s@\\@@gs if defined($c);
errorI ("matched wrong cipher: $c $url\nBody:\n$body")
if (defined($c) && $c !~ m/base$/s);
return $c;
}
# Total kludge that downloads the current html5player, parses the JavaScript,
# and intuits what the current cipher is. Normally we go by the list of
# known ciphers above, but if that fails, we try and do it the hard way.
#
sub guess_cipher($;$$) {
my ($cipher_id, $selftest_p, $nowarn) = @_;
# If we're in cipher-guessing mode, crank up the verbosity to also
# mention the list of formats and which format we ended up choosing.
# $verbose = 2 if ($verbose == 1 && !$selftest_p);
my $url = "https://www.youtube.com/";
my ($http, $head, $body);
my $id = '-';
if (! $cipher_id) {
($http, $head, $body) = get_url ($url); # Get home page
check_http_status ('-', $url, $http, 2);
my @vids = ();
$body =~ s%/watch\?v=([^\"\'<>]+)%{
push @vids, $1;
'';
}%gsex;
errorI ("no videos found on home page $url") unless @vids;
# Get random video -- pick one towards the middle, because sometimes
# the early ones are rental videos.
my $id = @vids[int(@vids / 2)];
$url .= "/watch\?v=$id";
($http, $head, $body) = get_url ($url); # Get random video's info
check_http_status ($id, $url, $http, 2);
($cipher_id) = page_cipher_base_url ($url, $body);
error ("$id: rate limited")
if (!$cipher_id && $body =~ m/large volume of requests/);
errorI ("$id: unparsable cipher url: $url\n\nBody:\n\n$body")
unless $cipher_id;
}
$cipher_id =~ s@\\@@gs;
$url = ($cipher_id =~ m/vflset/
? "https://www.youtube.com/s/player/$cipher_id.js"
: "https://s.ytimg.com/yts/jsbin/$cipher_id.js");
($http, $head, $body) = get_url ($url);
check_http_status ($id, $url, $http, 2);
my ($date) = ($head =~ m/^Last-Modified:\s+(.*)$/mi);
$date =~ s/^[A-Z][a-z][a-z], (\d\d? [A-Z][a-z][a-z] \d{4}).*$/$1/s;
my $v = '[\$a-zA-Z][a-zA-Z\d]*'; # JS variable, 1+ characters
my $v2 = '[\$a-zA-Z][a-zA-Z\d]?'; # JS variable, 2 characters
$v = "$v(?:\.$v)?"; # Also allow "a.b" where "a" would be used as a var.
$v2 = "$v2(?:\.$v2)?";
# First, find the sts parameter:
my ($sts) = ($body =~ m/\bsts:(\d+)\b/si);
if (!$sts) { # New way, 4-Jan-2020
# Find "N" in this: var f=18264; a.fa("ipp_signature_cipher_killswitch")
($sts) = ($body =~
m/$v = (\d{5,}) ; $v \("ipp_signature_cipher_killswitch"\) /sx);
}
errorI ("$cipher_id: no sts parameter: $url") unless $sts;
# Since the script is minimized and obfuscated, we can't search for
# specific function names, since those change. Instead we match the
# code structure.
#
# Note that the obfuscator sometimes does crap like y="split",
# so a[y]("") really means a.split("")
# Find "C" in this: var A = B.sig || C (B.s)
my (undef, $fn) = ($body =~ m/$v = ( $v ) \.sig \|\| ( $v ) \( \1 \.s \)/sx);
# If that didn't work:
# Find "C" in this: A.set ("signature", C (d));
($fn) = ($body =~ m/ $v \. set \s* \( "signature", \s*
( $v ) \s* \( \s* $v \s* \) /sx)
unless $fn;
# If that didn't work:
# Find "C" in this: (A || (A = "signature"), B.set (A, C (d)))
($fn) = ($body =~ m/ "signature" \s* \) \s* , \s*
$v \. set \s* \( \s*
$v \s* , \s*
( $v ) \s* \( \s* $v \s* \)
/sx)
unless $fn;
# Wow, what! Convert (0,window.encodeURIComponent) to just w.eUC
$body =~ s@\(0,($v)\)@ $1 @gs
unless $fn;
# If that didn't work:
# Find "B" in this: A = B(C(A)), D(E,F(A))
# Where "C" is "decodeURIComponent" and "F" is encodeURIComponent
(undef, $fn) = ($body =~ m/ ( $v ) = ( $v ) \( # A = B (
$v \( \1 \) \) , # C ( A )),
$v \( $v , # D ( E,
$v \( $v \) \) # F ( A ))
/sx)
unless $fn;
# If that didn't work:
# Find "C" in this: A.set (B.sp, D (C (E (B.s))))
# where "D" is "encodeURIComponent" and "E" is "decodeURIComponent"
# (Note, this rule is older than the above)
($fn) = ($body =~ m/ $v2 \. set \s* \( \s* # A.set (
$v2 \s* , \s* # B.sp,
$v \s* \( \s* # D (
( $v2 ) \s* \( \s* # C (
$v \s* \( \s* # E (
$v2 \s* # B.s
\) \s* \) \s* \) \s* \) # ))))
/sx)
unless $fn;
# If that didn't work:
# Find "C" in this: A.set (B, C (d))
# or this: A.set (B.sp, C (B.s))
($fn) = ($body =~ m/ $v2 \. set \s* \( \s*
$v2 \s* , \s*
( $v2 ) \s* \( \s* $v2 \s* \) \s* \)
/sx)
unless $fn;
errorI ("$cipher_id: unparsable cipher js: $url") unless $fn;
# Congratulations! If the above error fired, start looking through $url
# for a consecutive series of 2-arg function calls ending with a number.
# The containing function is the decipherer, and its name goes in $fn.
# Find body of function C(D) { ... }
# might be: var C = function(D) { ... }
# might be: , C = function(D) { ... }
my ($fn2) = ($body =~ m@\b function \s+ \Q$fn\E \s* \( $v \)
\s* { ( .*? ) } @sx);
($fn2) = ($body =~ m@(?: \b var \s+ | [,;] \s* )
\Q$fn\E \s* = \s* function \s* \( $v \)
\s* { ( .*? ) } @sx)
unless $fn2;
errorI ("$cipher_id: unparsable fn \"$fn\"") unless $fn2;
$fn = $fn2;
$error_whiteboard .= "fn: $fn2\n";
# They inline the swapper if it's used only once.
# Convert "var b=a[0];a[0]=a[63%a.length];a[63]=b;" to "a=swap(a,63);".
$fn2 =~ s@
var \s ( $v ) = ( $v ) \[ 0 \];
\2 \[ 0 \] = \2 \[ ( \d+ ) % \2 \. length \];
\2 \[ \3 \]= \1 ;
@$2=swap($2,$3);@sx;
my @cipher = ();
foreach my $c (split (/\s*;\s*/, $fn2)) {
# Typically the obfuscator gives member functions names like 'XX.YY',
# but in the case where 'YY' happens to be a reserved word, like 'do',
# it will instead emit 'XX["YY"]'.
#
$c =~ s@ ^ ( $v ) \[\" ( $v ) \"\] @$1.$2@sx;
if ($c =~ m@^ ( $v ) = \1 . $v \(""\) $@sx) { # A=A.split("");
} elsif ($c =~ m@^ ( $v ) = \1 . $v \(\) $@sx) { # A=A.reverse();
$error_whiteboard .= "fn: r: $1\n";
push @cipher, "r";
} elsif ($c =~ m@^ ( $v ) = \1 . $v \( (\d+) \) $@sx) { # A=A.slice(N);
$error_whiteboard .= "fn: s: $1\n";
push @cipher, "s$2";
} elsif ($c =~ m@^ ( $v ) = ( $v ) \( \1 , ( \d+ ) \) $@sx || # A=F(A,N);
$c =~ m@^ ( ) ( $v ) \( $v , ( \d+ ) \) $@sx) { # F(A,N);
my $f = $2;
my $n = $3;
$f =~ s/^.*\.//gs; # C.D => D
# Find function D, of the form: C={ ... D:function(a,b) { ... }, ... }
# Sometimes there will be overlap: X.D and Y.D both exist, and the
# one we want is the second one. So assume the one we want is simple
# enough to not contain any {} inside it.
my ($fn3) = ($body =~ m@ \b \"? \Q$f\E \"? : \s*
function \s* \( [^(){}]*? \) \s*
( \{ [^{}]+ \} )
@sx);
if (!$fn3) {
$fn =~ s/;/;\n\t /gs;
error ("unparsable: function \"$f\" not found\n\tin: $fn");
}
# Look at body of D to decide what it is.
if ($fn3 =~ m@ var \s ( $v ) = ( $v ) \[ 0 \]; @sx) { # swap
$error_whiteboard .= "fn3: w: $f: $fn3\n";
push @cipher, "w$n";
} elsif ($fn3 =~ m@ \b $v \. reverse\( @sx) { # reverse
$error_whiteboard .= "fn3: r: $f: $fn3\n";
push @cipher, "r";
} elsif ($fn3 =~ m@ return \s* $v \. slice @sx || # slice
$fn3 =~ m@ \b $v \. splice @sx) { # splice
$error_whiteboard .= "fn3: s: $f: $fn3\n";
push @cipher, "s$n";
} else {
$fn =~ s/;/;\n\t /gs;
errorI ("unrecognized cipher body $f($n) = $fn3\n\tin: $fn");
}
} elsif ($c =~ m@^ return \s+ $v \. $v \(""\) $@sx) { # return A.join("");
} else {
$fn =~ s/;/;\n\t /gs;
errorI ("$cipher_id: unparsable: $c\n\tin: $fn");
}
}
my $cipher = "$sts " . join(' ', @cipher);
$error_whiteboard .= "cipher: $cipher\n";
if ($selftest_p) {
return $cipher if defined($ciphers{$cipher_id});
$verbose = 2 if ($verbose < 2);
}
if ($verbose > 1 && !$nowarn) {
my $c2 = " '$cipher_id' => '$cipher',";
$c2 = sprintf ("%-66s# %s", $c2, $date);
auto_update($c2) if ($selftest_p && $selftest_p == 2);
print STDERR "$progname: current cipher is:\n$c2\n";
}
return $cipher;
}
# Tired of doing this by hand. Crontabbed self-modifying code!
#
sub auto_update($) {
my ($cipher_line) = @_;
open (my $in, '<:raw', $progname0) || error ("$progname0: $!");
local $/ = undef; # read entire file
my ($body) = <$in>;
close $in;
$body =~ s@(\nmy %ciphers = .*?)(\);)@$1$cipher_line\n$2@s ||
error ("auto-update: unable to splice");
# Since I'm not using CVS any more, also update the version number.
$body =~ s@([\$]Revision:\s+\d+\.)(\d+)(\s+[\$])@
{ $1 . ($2 + 1) . $3 }@sexi ||
error ("auto-update: unable to tick version");
open (my $out, '>:raw', $progname0) || error ("$progname0: $!");
syswrite ($out, $body) || error ("auto-update: $!");
close $out;
print STDERR "$progname: auto-updated $progname0\n";
# This part isn't expected to work for you.
my ($dir) = $ENV{HOME} . '/www/hacks';
system ("cd '$dir'" .
" && git commit -q -m 'cipher auto-update' '$progname'" .
" && git push -q")
if -d $dir;
}
# Replace the signature in the URL, deciphering it first if necessary.
#
sub apply_signature($$$$$$) {
my ($id, $fmt, $url, $cipher, $sig, $via) = @_;
if ($sig) {
if (defined ($cipher)) {
my $o = $sig;
$sig = decipher_sig ($url, $fmt ? "$id/$fmt" : $id, $cipher, $sig, $via);
if ($o ne $sig) {
my $n = $sig;
my ($a, $b) = split(/\./, $o);
my ($c, $d) = split(/\./, $sig);
($a, $b) = ($o, '') unless defined($b);
($c, $d) = ($sig, '') unless defined($d);
my $L1 = sprintf("%d %d.%d", length($o), length($a), length($b));
my $L2 = sprintf("%d %d.%d", length($sig), length($c), length($d));
foreach ($o, $n) { s/\./.\n /gs; }
my $s = "cipher: $cipher\n$L1: $o\n$L2: $n";
# $error_whiteboard .= "\n" if $error_whiteboard;
$fmt = '?' unless defined($fmt);
# $error_whiteboard .= "$fmt: " .
# "https://www.youtube.com/watch?v=$id\n$s";
if ($verbose > 3) {
print STDERR "$progname: $id: deciphered and replaced signature\n";
$s =~ s/^([^ ]+)( )/$2$1/s;
$s =~ s/^/$progname: /gm;
print STDERR "$s\n";
}
}
}
if ($url =~ m@^(.*)/s/[^/]+(.*)$@s) {
$url = "$1/signature/$sig$2"; # DASH /s/ => /signature/
} elsif ($url =~ m/\?/s) {
# Default is to do "signature=SIG" but if there is "sp=XXX"
# in the url_map, that means it goes in "XXX=SIG" instead
# in the video URL.
my ($sig_tag) = ($via =~ m/&sp=([^&]+)/s);
$sig_tag = 'signature' unless defined($sig_tag);
$url =~ s@ & ( signature | sig | \Q$sig_tag\E ) = [^&]+ @@gsx;
$url .= '&' . $sig_tag . '=' . $sig;
} else {
errorI ("unable to splice signature: $url");
}
}
return $url;
}
# Convert the text of a Youtube urlmap field into a structure.
# Apply signatures to enclosed URLs as necessary.
# Returns a hashref, or undef if the signatures could not be applied.
# If $into is provided, inserts the new items there, but does not overwrite
# existing ones.
#
# Returns the number of formats parsed (including redundant ones).
#
sub youtube_parse_urlmap($$$;$) {
my ($id, $urlmap, $cipher, $into) = @_;
my $cipher_printed_p = 0;
if ($urlmap =~ m/^\{"/s) { # Ugh, sometimes it is JSON
$urlmap =~ s/^\{//s;
$urlmap =~ s/"(.*?)" : ([^,]+) [,\}]* /{ # "a":x, => a=x&
"$1=" . url_quote($2) . "&";
}/gsexi;
$urlmap =~ s/&\{/,/gs;
}
my $count = 0;
foreach my $mapelt (split (/,/, $urlmap)) {
# Format used to be: "N|url,N|url,N|url"
# Now it is: "url=...&quality=hd720&fallback_host=...&type=...&itag=N"
my ($k, $v, $e, $sig, $sig2, $sig3, $w, $h, $size);
my $sig_via = $mapelt;
if ($mapelt =~ m/^\d+\|/s) {
($k, $v) = m/^(.*?)\|(.*)$/s;
} elsif ($mapelt =~ m/^[a-z][a-z\d_]*=/s) {
($sig) = ($mapelt =~ m/\bsig=([^&]+)/s); # sig= when un-ciphered.
($sig2) = ($mapelt =~ m/\bs=([^&]+)/s); # s= when enciphered.
($sig3) = ($mapelt =~ m@/s/([^/?&]+)@s); # /s/XXX/ in DASH.
($k) = ($mapelt =~ m/\bitag=(\d+)/s);
($v) = ($mapelt =~ m/\burl=([^&]+)/s);
$v = '' unless $v;
# In JSON, "cipher":"sp=sig&s=...&url=..."
if ($mapelt =~ m@\bcipher=([^&\"]+)@s) {
my $sig4 = url_unquote ($1);
$sig4 =~ s/^.*"(.*?)".*$/$1/s;
my ($s2) = ($sig4 =~ m/\bs=([^&]+)/s);
my ($u2) = ($sig4 =~ m/\burl=([^&]+)/s);
if ($u2) {
$sig = undef;
$sig2 = $s2;
$v = $u2;
$sig_via = $sig4; # so that apply_signature can find sp=
}
}
$v =~ s@\\u0026@&@gs;
$v = url_unquote($v);
$v =~ s/^\"|\"$//gs;
($size) = ($v =~ m/\bclen=([^&]+)/s);
($w, $h) = ($v =~ m/\bsize=(\d+)x(\d+)/s);
# JSON
($size) = ($mapelt =~ m/\bcontentLength=\"?(\d+)/s) unless $size;
($w) = ($mapelt =~ m/\bwidth=\"?(\d+)/s) unless $w;
($h) = ($mapelt =~ m/\bheight=\"?(\d+)/s) unless $h;
my ($q) = ($mapelt =~ m/\bquality=([^&]+)/s);
my ($t) = ($mapelt =~ m/\b(?:type|mimeType)=([^&]+)/s);
$q = url_unquote($q) if ($q);
$t = url_unquote($t) if ($t);
if ($q && $t) {
$e = "\t$q, $t";
} elsif ($t) {
$e = $t;
}
$e = url_unquote($e) if ($e);
}
# error ("$id: can't download RTMPE DRM videos")
# # There was no indiciation in get_video_info that this is an RTMPE
# # stream, so it took us several retries to fail here.
# if (!$v && $urlmap =~ m/\bconn=rtmpe%3A/s);
errorI ("$id: unparsable urlmap entry: no itag: $mapelt") unless ($k);
errorI ("$id: unparsable urlmap entry: no url: $mapelt") unless ($v);
my ($ct) = ($e =~ m@\b((audio|video|text|application)/[-_a-z\d]+)\b@si);
$v =~ s@^.*?\|@@s; # VEVO
errorI ("$id: enciphered URL but no cipher found: $v")
if (($sig2 || $sig3) && !$cipher);
if ($verbose > 1 && !$cipher_printed_p) {
print STDERR "$progname: $id: " .
(($sig2 || $sig3) ? "enciphered" : "non-enciphered") .
(($sig2 || $sig3) ? " (" . ($cipher || 'NONE') . ")" :
($cipher ? " ($cipher)" : "")) .
"\n";
$cipher_printed_p = 1;
}
# Apply the signature to the URL, deciphering it if necessary.
#
# The "use_cipher_signature" parameter is as lie: it is sometimes true
# even when the signatures are not enciphered. The only way to tell
# is if the URLs in the map contain "s=" instead of "sig=".
#
# If we loaded get_video_info with the "sts" parameter, meaning we told
# it what cipher to use, then the returned URLs have that cipher, and
# all is good. However, if we had omitted the "sts" parameter, then
# the URLs come back with some unknown cipher (it's not the last cipher
# in the list, for example) so we can't decode it.
#
# So in the bad old days, we didn't use "sts", and when we got an
# enciphered video, we had to scrape the HTML to find the real cipher.
# This had the shitty side effect that when a video was both enciphered
# and was "content warning", we couldn't download it at all.
#
# But now that we always pass "sts" to get_video_info, this isn't a
# problem any more. I think that in this modern world, we never actually
# need to scrape HTML any more, because we should always know a working
# cipher ahead of time.
#
# Aug 2018: Nope, we now scrape HTML every time because that's the only
# way to reliably get dashmpd URLs that work.
#
$v = apply_signature ($id, $k, $v,
($sig2 || $sig3) ? $cipher : undef,
url_unquote ($sig || $sig2 || $sig3 || ''),
$sig_via);
# Finally! The "ratebypass" parameter turns off rate limiting!
# But we can't add it to a URL that signs the "ratebypass" parameter.
#
if (! ($v =~ m@sparams=[^?&]*ratebypass@ ||
$v =~ m@sparams/[^/]*ratebypass@)) {
if ($v =~ m@\?@s) {
$v .= '&ratebypass=yes';
} elsif ($v =~ m@/itag/@s) { # dashmpd-style.
$v .= ($v =~ m@/$@s ? '' : '/') . 'ratebypass/yes/';
}
}
print STDERR "\t\t$k\t$v\t$e\n" if ($verbose > 3);
if ($v =~ m/&live=(1|yes)\b/gs) {
# We need to get the segments from the DASH manifest instead:
# this URL is only the first segment, a few seconds long.
print STDERR "$progname: $id: skipping fmt $k\n" if ($verbose > 2);
next;
}
my %v = ( fmt => $k,
url => $v,
content_type => $ct,
w => $w,
h => $h,
size => $size,
);
if (! defined ($into->{$k})) {
$into->{$k} = \%v;
print STDERR "$progname: $id: found fmt $k\n" if ($verbose > 2);
}
$count++;
}
return $count;
}
# There are two ways of getting the underlying video formats from youtube:
# parse it out of the HTML, or call get_video_info.
#
# We have to do both, because they all fail in different ways at different
# times, so we try a bunch of things and append together any results we find.
# The randomness leading to this crazy approach includes but is not limited
# to:
#
# - The DASH URL in the HTML always works, but sometimes the DASH URL in
# get_video_info does not -- the latter appears to use a different cipher.
#
# - There are 4 different ways of invoking get_video_info, and sometimes
# only one of them works. E.g., sometimes the "el=" option is needed to
# retrieve info, but sometimes it *prevents* you from retrieving info.
# E.g., "info" and "embedded" sometimes give geolocation errors, and
# yet are the only way to bypass the age gate.
#
# - Sometimes all formats are present in get_video_info, but sometimes only
# the 720p and lower resolutions are there, and higher resolutions are
# only listed in the DASH URL.
#
# - Sometimes the DASH URL pointed to from the HTML and the DASH URL pointed
# to by get_video_info have different sets of formats in them.
#
# - And sometimes there are no DASH URLs.
# Parses a dashmpd URL and inserts the contents into $fmts
# as per youtube_parse_urlmap.
#
sub youtube_parse_dashmpd($$$$) {
my ($id, $url, $cipher, $into) = @_;
# I don't think this is needed.
# $url .= '?disable_polymer=true';
# Some dashmpd URLs have /s/NNNNN.NNNNN enciphered signatures in them.
# We have to replace them with /signature/MMMMM.MMMMM or we can't read
# the manifest file. The URLs *within* the manifest will also have
# signatures on them, but ones that (I think?) do not need to be
# deciphered.
#
if ($url =~ m@/s/([^/]+)@s) {
my $sig = $1;
print STDERR "$id: DASH manifest enciphered\n" if ($verbose > 1);
$url = apply_signature ($id, undef, $url, $cipher, $sig, '');
print STDERR "$id: DASH manifest deciphered\n" if ($verbose > 1);
} else {
print STDERR "$id: DASH manifest non-enciphered\n" if ($verbose > 1);
}
my $count = 0;
my ($http2, $head2, $body2) = get_url ($url);
check_http_status ($id, $url, $http2, 2);
# Nuke the subtitles: the Representations inside them aren't useful.
$body2 =~ s@<AdaptationSet mimeType=[\"\']text/.*?</AdaptationSet>@@gs;
my @reps = split(/<Representation\b/si, $body2);
shift @reps;
foreach my $rep (@reps) {
my ($k) = ($rep =~ m@id=[\'\"](\d+)@si);
my ($url) = ($rep =~ m@<BaseURL\b[^<>]*>([^<>]+)@si);
my ($type) = ($rep =~ m@\bcodecs="(.*?)"@si);
my ($w) = ($rep =~ m@\bwidth="(\d+)"@si);
my ($h) = ($rep =~ m@\bheight="(\d+)"@si);
my ($segs) = ($rep =~ m@<SegmentList[^<>]*>(.*?)</SegmentList>@si);
my $size;
$type = ($w && $h ? "video/mp4" : "audio/mp4") . ";+codecs=\"$type\"";
if ($segs) {
my ($url0) = ($segs =~ m@<Initialization\s+sourceURL="(.*?)"@si);
my @urls = ($segs =~ m@<SegmentURL\s+media="(.*?)"@gsi);
unshift @urls, $url0 if defined($url0);
foreach (@urls) { $_ = $url . $_; };
$url = \@urls;
($size) = ($url0 =~ m@/clen/(\d+)/@si) # Not always present
if ($url0);
}
my %v = ( fmt => $k,
url => $url,
content_type => $type,
dashp => 1,
w => $w,
h => $h,
size => $size,
# abr => undef,
);
# Sometimes the DASH URL for a format works but the non-DASH URL is 404.
my $prefer_dash_p = 1;
my $old = $into->{$k};
$old = undef if ($prefer_dash_p && $old && !$old->{dashp});
if (!$old) {
$into->{$k} = \%v;
print STDERR "$progname: $id: found fmt $k" .
(ref($url) eq 'ARRAY' ? " (" . scalar(@$url) . " segs)" : "") .
"\n"
if ($verbose > 2);
}
$count++;
}
return $count;
}
# For some errors, we know there's no point in retrying.
#
my $blocked_re = join ('|',
('(available|blocked it) in your country',
'copyright (claim|grounds)',
'removed by the user',
'account.*has been terminated',
'has been removed',
'has not made this video available',
'has closed their YouTube account',
'is not available',
'is unavailable',
'is not embeddable',
'can\'t download rental videos',
'livestream videos',
'invalid parameters',
'RTMPE DRM',
'Private video\?',
'video is private',
'piece of shit',
'you are a human',
'\bCAPCHA required',
'\b429 Too Many Requests',
'Premieres in \d+ (min|hour|day|week|month)',
'video has not yet premiered',
'live event will begin in',
'^[^:]+: exists: ',
));
# Scrape the HTML page to extract the video formats.
# Populates $fmts and returns $error_message.
#
sub load_youtube_formats_html($$$) {
my ($id, $url, $fmts) = @_;
my $oerror = '';
my $err = '';
my ($http, $head, $body) = get_url ($url);
my ($title) = ($body =~ m@<title>\s*(.*?)\s*</title>@si);
$title = '' unless $title;
utf8::decode ($title); # Pack multi-byte UTF-8 back into wide chars.
$title = munge_title (html_unquote ($title));
# Do this after we determine whether we have any video info.
# sanity_check_title ($title, $url, $body, 'load_youtube_formats_html');
get_youtube_year ($id, $body); # Populate cache so we don't load twice.
my $unquote_p = 1;
my ($args) = ($body =~ m@'SWF_ARGS' *: *{(.*?)}@s);
if (! $args) { # Sigh, new way as of Apr 2010...
($args) = ($body =~ m@var swfHTML = [^\"]*\"(.*?)\";@si);
$args =~ s@\\@@gs if $args;
($args) = ($args =~ m@<param name="flashvars" value="(.*?)">@si) if $args;
($args) = ($args =~ m@fmt_url_map=([^&]+)@si) if $args;
$args = "\"fmt_url_map\": \"$args\"" if $args;
}
if (! $args) { # Sigh, new way as of Aug 2011...
($args) = ($body =~ m@'PLAYER_CONFIG':\s*{(.*?)}@s);
$args =~ s@\\u0026@&@gs if $args;
$unquote_p = 0;
}
if (! $args) { # Sigh, new way as of Jun 2013...
($args) = ($body =~ m@ytplayer\.config\s*=\s*{(.*?)};@s);
$args =~ s@\\u0026@&@gs if $args;
$unquote_p = 1;
}
$args = '' unless defined $args;
if (! $args) {
# Try to find a better error message
(undef, $err) = ($body =~ m@<( div | h1 ) \s+
(?: id | class ) =
"(?: error-box |
unavailable-message )"
[^<>]* > \s*
( .+? ) \s*
</ \1 > @six);
if ($err) {
$err =~ s@^.*="yt-uix-button-content"[^<>]*>([^<>]+).*@$1@si;
$err =~ s/<[^<>]*>//gs;
}
$err = "Rate limited: CAPCHA required"
if (!$err && $body =~ m/large volume of requests/);
if ($err) {
my ($err2) = ($body =~ m@<div class="submessage">(.*?)</div>@si);
if ($err2) {
$err2 =~ s@<button.*$@@s;
$err2 =~ s/<[^<>]*>//gs;
$err .= ": $err2";
}
$err =~ s/^"[^\"\n]+"\n//s;
$err =~ s/^&quot;[^\"\n]+?&quot;\n//s;
$err =~ s/\s+/ /gs;
$err =~ s/^\s+|\s+$//s;
$err =~ s/\.(: )/$1/gs;
$err =~ s/\.$//gs;
#$err = "$err ($title)" if ($title);
$oerror = $err;
$http = 'HTTP/1.0 404';
}
}
# Sometimes we have <TITLE>YouTube</TITLE> but the real title is
# buried inside some JSON.
#
if (!$title || $title =~ m/^untitled$/si) {
if ($body =~ m/\\?"title\\?":\\?"(.*?)\\?",/si) {
$title = $1;
$title =~ s/\\//gs;
$title = munge_title (html_unquote ($title));
}
}
$oerror =~ s@<.*?>@@gs if $oerror;
$oerror =~ s/ \(YouTube\)$//s if $oerror;
# Sometimes Youtube returns HTTP 404 pages that have real messages in them,
# so we have to check the HTTP status late. But sometimes it doesn't return
# 404 for pages that no longer exist. Hooray.
$http = 'HTTP/1.0 404'
if ($oerror && $oerror =~ m/$blocked_re/sio);
$err = "$http: $oerror"
unless (check_http_status ($id, $url, $http, 0));
$err = "no ytplayer.config$oerror"
if (!$args && !$err);
my ($cipher) = page_cipher_base_url ($url, $body);
my ($kind, $kind2, $urlmap, $urlmap2);
#### hlsvp are m3u8u files, but that data always seems to also be present
#### in dash, so I haven't bothered parsing those.
my $count = 0;
foreach my $key (#'hlsvp',
'fmt_url_map',
'fmt_stream_map', # VEVO
'url_encoded_fmt_stream_map', # Aug 2011
'adaptive_fmts',
'dashmpd',
'player_response',
) {
my ($v) = ($args =~ m@"$key": *"(.*?[^\\])"@s);
$v = '' if (!defined($v) || $v eq '",');
$v =~ s@\\@@gs;
next unless $v;
print STDERR "$progname: $id HTML: found $key\n" if ($verbose > 2);
# source%3Dyt_premiere_broadcast%26 or /source/yt_premiere_broadcast/
if ($v =~ m/yt_premiere_broadcast/) {
$err = "video has not yet premiered";
undef %$fmts; # The fmts point to a countdown video.
last;
}
if ($v =~ m@&live_playback=([^&]+)@si ||
$v =~ m@&live=(1)@si ||
$v =~ m@&source=(yt_live_broadcast)@si) {
$err = "can't download live videos";
# The fmts point to an M3U8 that is currently of unbounded length.
undef %$fmts;
last;
}
if ($key eq 'dashmpd' || $key eq 'hlsvp') {
$count += youtube_parse_dashmpd ("$id HTML", $v, $cipher, $fmts);
} elsif ($key eq 'player_response') {
my $ov = $v;
($v) = ($ov =~ m@"dashManifestUrl": *"(.*?[^\\])"@s);
# This manifest sometimes works when the one in get_video_info doesn't.
$count += youtube_parse_dashmpd ("$id HTML", $v, $cipher, $fmts) if $v;
# Nov 2019: Saw this on an old fmt 133 video, and it was the only
# list of formats available in the HTML.
($v) = ($ov =~ m@"adaptiveFormats": *\[(.*?)\]@s);
$count += youtube_parse_urlmap ("$id HTML", $v, $cipher, $fmts) if $v;
} else {
$count += youtube_parse_urlmap ("$id HTML", $v, $cipher, $fmts);
}
}
# Do this after we determine whether we have any video info.
sanity_check_title ($title, $url,
"ERR: \"$err\"\n\n$body", ####
'load_youtube_formats_html')
if ($count);
$fmts->{title} = $title unless defined($fmts->{title});
$fmts->{cipher} = $cipher unless defined($fmts->{cipher});
return $err;
}
# Loads various versions of get_video_info to extract the video formats.
# Populates $fmts and returns $error_message.
#
sub load_youtube_formats_video_info($$$) {
my ($id, $url, $fmts) = @_;
my $cipher = $fmts->{cipher};
my $sts = undef;
if ($cipher) {
my $c = $ciphers{$cipher};
if (! $c) {
print STDERR "$progname: WARNING: $id: unknown cipher $cipher\n"
if ($verbose > 1 && !$cipher_warning_printed_p);
$c = guess_cipher ($cipher, 0, $cipher_warning_printed_p);
$ciphers{$cipher} = $c;
}
$sts = $1 if ($c =~ m/^\s*(\d+)\s/si);
errorI ("$id: $cipher: no sts") unless $sts;
}
my $info_url_1 = ('https://www.youtube.com/get_video_info' .
"?video_id=$id" .
($sts ? '&sts=' . $sts : '') .
# I don't think any of these are needed.
# '&ps=default' .
# '&hl=en' .
# '&disable_polymer=true' .
# '&gl=US' .
# Avoid "playback restricted" or "content warning".
# They sniff this referer for embedding.
'&eurl=' .
url_quote ('https://youtube.googleapis.com/v/' . $id)
);
# Sometimes the 'el' arg is needed to avoid "blocked it from display
# on this website or application". But sometimes, including it *causes*
# "sign in to confirm your age". So try it with various options.
#
# Note that each of these can return a different error message for the
# same unloadable video, so arrange them with better error last:
#
# "": "This video is unavailable"
# embedded: "This video is unavailable"
# info: "Invalid parameters"
# detailpage: "This video has been removed by the user"
#
my @extra_parameters = ('', '&el=embedded', '&el=info', '&el=detailpage');
my ($title, $body, $embed_p, $rental, $live_p, $premiere_p);
my $err = undef;
my $done = 0;
# The retries here are because sometimes we get HTTP 200, but the body
# of the document contains fewer parameters than it should; reloading
# sometimes fixes it.
# my $retries = 5;
my $retries = 1;
while ($retries--) {
foreach my $extra (@extra_parameters) {
my $info_url = $info_url_1 . $extra;
my ($http, $head);
($http, $head, $body) = get_url ($info_url);
my $err2 = (check_http_status ($id, $url, $http, 0) ? undef : $http);
$err = $err2 unless $err;
my $body2 = $body; # FFS
$body2 =~ s/%5C/\\/gs;
$body2 =~ s/\\u0026/&/gs;
$body2 =~ s/%3D/=/gs;
($title) = ($body2 =~ m@&title=([^&]+)@si) unless $title;
($rental) = ($body2 =~ m@&ypc_vid=([^&]+)@si);
($live_p) = ($body2 =~ m@&live_playback=([^&]+)@si ||
$body2 =~ m@&live=(1)@si ||
$body2 =~ m@&source=(yt_live_broadcast)@si);
$embed_p = $1 if ($body =~ m@&allow_embed=([^&]+)@si);
$embed_p = 0 if (!defined($embed_p) &&
$body =~ m/on[+\s+]other[+\s+]websites/s);
# Sigh, %2526source%253Dyt_premiere_broadcast%2526
$premiere_p = 1 if ($body =~ m@yt_premiere_broadcast@s);
$err = "can't download livestream videos" if ($live_p);
# "player_response" contains JSON:
# "playabilityStatus":{
# "status":"LIVE_STREAM_OFFLINE",
# "reason":"Premieres in 10 hours",
my $count = 0;
foreach my $key (#'hlsvp',
'fmt_url_map',
'fmt_stream_map', # VEVO
'url_encoded_fmt_stream_map', # Aug 2011
'adaptive_fmts',
'dashmpd',
'player_response',
) {
my ($v) = ($body =~ m@[?&]$key=([^&?]+)@si);
next unless defined ($v);
$v = url_unquote ($v);
$v =~ s@\\u0026@&@gs;
$v =~ s@\\@@gs;
print STDERR "$progname: $id VI: found $key" .
(defined($embed_p)
? ($embed_p ? " (embeddable)" : " (non-embeddable)")
: "") .
"\n"
if ($verbose > 1);
if ($key eq 'dashmpd' || $key eq 'hlsvp') {
$count += youtube_parse_dashmpd ("$id VI-1", $v, $cipher, $fmts);
} elsif ($key eq 'player_response') {
($v) = ($v =~ m@"adaptiveFormats":\[(.*?)\]@s);
$count += youtube_parse_urlmap ("$id VI-2", $v, $cipher, $fmts)
if ($v);
} else {
$count += youtube_parse_urlmap ("$id VI-3", $v, $cipher, $fmts);
}
}
$done = ($count >= 3 && $title);
# Don't let "Invalid parameters" override "This video is private".
$err = url_unquote ($1)
if ((!$err || $err =~ m/invalid param/si) &&
$body =~ m/\bstatus=fail\b/si &&
$body =~ m/\breason=([^?&]+)/si);
# This gets us "This video is private" instead of "Invalid parameters".
if ($body =~ m/player_response=([^&]+)/s) {
my $s = url_unquote ($1);
$s =~ s@\\u0026@&@gs;
$s =~ s@\\u003c@<@gs;
$s =~ s@\\u003e@>@gs;
$s =~ s@\\n@ @gs;
if ($s =~ m/"reason":"(.*?)"[,\}]/si) {
$err = $1;
$err =~ s/<[^<>]*>//gs;
}
}
}
last if $done;
sleep (1);
}
if ($err) {
$err =~ s/<[^<>]+>//gs;
$err =~ s/\n/ /gs;
$err =~ s/\s*Watch on YouTube\.?//gs; # FU
}
$err = "video is not embeddable"
if ($err && (defined($embed_p) && !$embed_p));
if ($premiere_p) {
$err = "video has not yet premiered";
undef %$fmts if ($premiere_p); # The fmts point to a countdown video.
}
$body = '' unless $body;
($title) = ($body =~ m@&title=([^&]+)@si) unless $title;
errorI ("$id: no title in $info_url_1") if (!$title && !$err);
$title = url_unquote($title) if $title;
if (!$err) {
($err) = ($body =~ m@reason=([^&]+)@s);
$err = '' unless $err;
if ($err) {
$err = url_unquote($err);
$err =~ s/^"[^\"\n]+"\n//s;
$err =~ s/\s+/ /gs;
$err =~ s/^\s+|\s+$//s;
$err = " (\"$err\")";
}
}
$err =~ s/ \(YouTube\)$//s if $err;
$err .= ': rental video' if ($err && $rental);
if ($err && $rental) {
error ("can't download rental videos, but the preview can be\n" .
"$progname: downloaded at " .
"https://www.youtube.com/watch?v=$rental");
}
utf8::decode ($title) if $title;
$fmts->{title} = $title unless defined($fmts->{title});
$fmts->{cipher} = $cipher unless defined($fmts->{cipher});
return $err;
}
# Returns a hash of:
# [ title => "T",
# N => [ ...video info... ],
# M => [ ...video info... ], ... ]
#
sub load_youtube_formats($$$) {
my ($id, $url, $size_p) = @_;
my %fmts;
# Scrape the HTML page before loading get_video_info because the
# DASH URL in the HTML page is more likely to work than the one
# returned by get_video_info.
# I don't think any of these are needed.
# $url .= join('&',
# 'has_verified=1',
# 'bpctr=9999999999',
# 'hl=en',
# 'disable_polymer=true');
my $err1 = load_youtube_formats_html ($id, $url, \%fmts);
my $err2 = load_youtube_formats_video_info ($id, $url, \%fmts);
# Which error sucks less? Hard to say.
# my $err = $err2 || $err;
my $err = $err2 || $err1;
my $both = ($err1 || '') . ' ' . ($err2 || '');
$err = 'age-restricted video is not embeddable'
if ($both =~ m/content warning/si &&
$both =~ m/not embeddable/si);
# It's rare, but there can be only one format available.
# Keys: 18, cipher, title.
if (scalar (keys %fmts) < 3) {
error ("$id: $err") if $err;
errorI ("$id: no formats available: $err");
}
$fmts{thumb} = "https://img.youtube.com/vi/" . $id . "/0.jpg"
unless ($fmts{thumb});
return \%fmts;
}
# Returns a hash of:
# [ title: "T",
# N: [ ...video info... ],
# M: [ ...video info... ], ... ]
#
sub load_vimeo_formats($$$) {
my ($id, $url, $size_p) = @_;
# Vimeo's new way, 3-Mar-2015.
# The "/NNNN?action=download" page no longer exists. There is JSON now.
# This URL is *often* all that we need:
#
my $info_url = ("https://player.vimeo.com/video/$id/config" .
"?bypass_privacy=1"); # Not sure if this does anything
# But if we scrape the HTML page for the version of the config URL
# that has "&s=XXXXX" on it (some kind of signature, I presume) then
# we *sometimes* get HD when we would not have gotten it with the
# other URL:
#
my ($http, $head, $body) = get_url ($url);
# Don't check status: sometimes the info URL is on the 404 page!
# Maybe this happens if embedding is disabled.
if ($body =~ m@([^<>.:]*verify that you are a human[^<>.:]*)@si) {
error ("$id: $http $1"); # Bail early: $info_url will not succeed.
}
my $obody = $body; # Might be a better error message in here.
$body =~ s/\\//gs;
if ($body =~ m@(\bhttps?://[^/]+/video/\d+/config\?[^\s\"\'<>]+)@si) {
$info_url = html_unquote($1);
} else {
print STDERR "$progname: $id: no info URL\n" if ($verbose > 1);
}
my $referer = $url;
# Test cases:
#
# https://vimeo.com/120401488
# Has a Download link on the page that lists 270p, 360p, 720p, 1080p
# The config url only lists 270p, 360p, 1080p
# https://vimeo.com/70949607
# No download link on the page
# The config URL gives us 270p, 360p, 1080p
# https://vimeo.com/104323624
# No download link
# Simple info URL gives us only one size, 360p
# Signed info URL gives us 720p and 360p
# https://vimeo.com/117166426
# A private video
# https://vimeo.com/88309465
# "HTTP/1.1 451 Unavailable For Legal Reasons"
# "removed as a result of a third-party notification"
# https://vimeo.com/121870373
# A private video that isn't 404 for some reason
# https://vimeo.com/83711059
# The HTML page is 404, but the simple info URL works,
# and the video is downloadable anyway!
# https://vimeo.com/209
# Yes, this is a real video. No "h264" in "files" metadata,
# only .flv as "vp6".
# https://www.vimeo.com/142574658
# Only has "progressive" formats, not h264. Downloads fine though.
($http, $head, $body) = get_url ($info_url, $referer);
my $err = undef;
if (!check_http_status ($id, $info_url, $http, 0)) {
($err) = ($body =~ m@ \{ "message" : \s* " ( .+? ) " , @six);
$err = "Private video" if ($err && $err =~ m/privacy setting/si);
$err = $1 if ($body =~ m@([^<>.:]*verify that you are a human[^<>.:]*)@si);
$err = $http . ($err ? ": $err" : "");
} else {
$http = ''; # 200
}
my ($title) = ($body =~ m@ "title" : \s* " (.+?) ", @six);
my ($files0) = ($body =~ m@ \{ "h264" : \s* \{ ( .+? \} ) \} , @six);
my ($files1) = ($body =~ m@ \{ "vp6" : \s* \{ ( .+? \} ) \} , @six);
my ($files2) = ($body =~ m@ "progressive" : \s* \[ ( .+? \] ) \} @six);
my $files = ($files0 || '') . ($files1 || '') . ($files2 || '');
my ($thumb) = ($body =~ m/"thumbs":\{"\d+":"(.*?)"/s);
# Sometimes we get empty-ish data for "Private Video", but HTTP 200.
$err = "No video info (Private video?)"
if (!$err && !$title && !$files);
if ($err) {
if ($obody) {
# The HTML page might provide an explanation for the error.
my ($err2) = ($obody =~
m@ exception_data \s* = \s* { [^{}]*
"notification" \s* : \s* " (.*?) ",@six);
if ($err2) {
$err2 =~ s/\\n/\n/gs; # JSON
$err2 =~ s/\\//gs;
$err2 =~ s/<[^<>]*>//gs; # Lose tags
$err2 =~ s/^\s+//gs;
$err2 =~ s/\n.*$//gs; # Keep first para only.
$err .= " $err2" if $err2;
}
}
error ("$id: $err") if ($http || $err =~ m/Private/s);
errorI ("$id: $err");
}
my %fmts;
if ($files) {
errorI ("$id: no title") unless $title;
$fmts{title} = $title;
my $i = 0;
my %seen;
foreach my $f (split (/\},?\s*/, $files)) {
next unless (length($f) > 50);
# my ($fmt) = ($f =~ m@^ \" (.+?) \": @six);
# ($fmt) = ($f =~ m@^ \{ "profile": (\d+) @six) unless $fmt;
my ($fmt) = ($f =~ m@^ \{ "profile": (\d+) @six);
next unless $fmt;
next if ($seen{$fmt});
my ($url2) = ($f =~ m@ "url" : \s* " (.*?) " @six);
my ($w) = ($f =~ m@ "width" : \s* (\d+) @six);
my ($h) = ($f =~ m@ "height" : \s* (\d+) @six);
next unless $url2;
errorI ("$id: unparsable vimeo video formats: $f")
unless ($fmt && $url2 && $w && $h);
print STDERR "$progname: $fmt: ${w}x$h: $url2\n"
if ($verbose > 2);
my ($ext) = ($url2 =~ m@ ^ [^?&]+ \. ( [^./?&]+ ) ( [?&] | $ ) @sx);
$ext = 'mp4' unless $ext;
my $ct = ($ext =~ m/^(flv|webm|3gpp?)$/s ? "video/$ext" :
$ext =~ m/^(mov)$/s ? 'video/quicktime' :
'video/mpeg');
$seen{$fmt} = 1;
my %v = ( fmt => $i,
url => $url2,
content_type => $ct,
w => $w,
h => $h,
# size => undef,
# abr => undef,
);
$fmts{$i} = \%v;
$i++;
}
}
$fmts{thumb} = $thumb if ($thumb);
return \%fmts;
}
# Returns a hash of:
# [ title: "T",
# year: "Y",
# N: [ ...video info... ],
# M: [ ...video info... ], ... ]
#
sub load_tumblr_formats($$$) {
my ($id, $url, $size_p) = @_;
# The old code doesn't work any more: I guess they locked down the
# video info URL to require an API key. So we can just grab the
# "400" version, I guess...
{
my ($http, $head, $body) = get_url ($url);
check_http_status ($id, $url, $http, 1);
# Incestuous
if ($body =~ m@ <IFRAME [^<>]*? \b SRC="
( https?:// [^<>\"/]*? \b
( vimeo\.com | youtube\.com |
instagram\.com )
[^<>\"]+ )
@six) {
return load_formats ($1, $size_p);
}
my ($title) = ($body =~ m@<title>\s*(.*?)</title>@six);
if (! ($body =~ m@<meta \s+ property="og:type" \s+
content="[^\"<>]*?video@six)) {
exit (1) if ($verbose <= 0); # Skip silently if --quiet.
error ("not a Tumblr video URL: $url $verbose");
}
my ($img) = ($body =~ m@<meta \s+ property="og:image" \s+
content="([^<>]*?)"@six);
error ("no title: $url\n$body") unless $title;
error ("no og:image: $url") unless $img;
$img =~ s@_[^/._]+\.[a-z]+$@_480.mp4@si;
error ("couldn't find video URL: $url")
unless ($img =~ m/\.mp4$/s);
$img =~ s@^https?://[^/]+@https://vt.tumblr.com@si;
$title = munge_title (html_unquote ($title || ''));
sanity_check_title ($title, $url, $body, 'load_tumblr_formats');
my $fmts = {};
my $i = 0;
my ($w, $h) = (0, 0);
my %v = ( fmt => $i,
url => $img,
content_type => 'video/mp4',
w => $w,
h => $h,
# size => undef,
# abr => undef,
);
$fmts->{$i} = \%v;
$fmts->{title} = $title;
# $fmts->{year} = $year;
return $fmts;
}
# The following no longer works.
my ($host) = ($url =~ m@^https?://([^/]+)@si);
my $info_url = "https://api.tumblr.com/v2/blog/$host/posts/video?id=$id";
my ($http, $head, $body) = get_url ($info_url);
check_http_status ($id, $url, $http, 1);
$body =~ s/^.* "posts" : \[ //six;
my ($title) = ($body =~ m@ "slug" : \s* \" (.+?) \" @six);
my ($year) = ($body =~ m@ "date" : \s* \" (\d{4})- @six);
$title = munge_title (html_unquote ($title || ''));
sanity_check_title ($title, $url, $body, 'load_tumblr_formats 2');
my $fmts = {};
$body =~ s/^.* "player" : \[ //six;
my $i = 0;
foreach my $chunk (split (/\},/, $body)) {
my ($e) = ($chunk =~ m@ "embed_code" : \s* " (.*?) " @six);
$e =~ s/\\n/\n/gs;
$e =~ s/ \\[ux] \{ ([a-z0-9]+) \} / unihex($1) /gsexi; # \u{XXXXXX}
$e =~ s/ \\[ux] ([a-z0-9]{4}) / unihex($1) /gsexi; # \uXXXX
$e =~ s/\\//gs;
my ($w) = ($e =~ m@width=['"]?(\d+)@si);
my ($h) = ($e =~ m@height=['"]?(\d+)@si);
my ($src) = ($e =~ m@<source\b(.*?)>@si);
my ($v) = ($src =~ m@src=['"](.*?)['"]@si);
my ($ct) = ($src =~ m@type=['"](.*?)['"]@si);
my %v = ( fmt => $i,
url => $v,
content_type => $ct,
w => $w,
h => $h,
# size => undef,
# abr => undef,
);
$fmts->{$i} = \%v;
$i++;
}
$fmts->{title} = $title;
$fmts->{year} = $year;
return $fmts;
}
# Returns a hash of:
# [ title: "T",
# year: "Y",
# 0: [ ...video info... ],
# Since Instagram only offers one resolution.
#
sub load_instagram_formats($$$) {
my ($id, $url, $size_p) = @_;
my ($http, $head, $body) = get_url ($url);
check_http_status ($id, $url, $http, 1);
my ($title) = ($body =~ m@<meta \s+ property="og:title" \s+
content="([^<>]*?)"@six);
my ($src) = ($body =~ m@<meta \s+ property="og:video:secure_url" \s+
content="([^<>]*?)"@six);
my ($w) = ($body =~ m@<meta \s+ property="og:video:width" \s+
content="([^<>]*?)"@six);
my ($h) = ($body =~ m@<meta \s+ property="og:video:height" \s+
content="([^<>]*?)"@six);
my ($ct) = ($body =~ m@<meta \s+ property="og:video:type" \s+
content="([^<>]*n?)"@six);
my ($year) = ($body =~ m@\bdatetime="(\d{4})-@six);
my ($thumb) = ($body =~ m@<meta \\s+ property="og:image" \s+
content="([^<>]*n?)"@six);
error ("$id: no video in $url")
unless ($src && $w && $h && $ct && $title);
$title = munge_title (html_unquote ($title || ''));
sanity_check_title ($title, $url, $body, 'load_instagram_formats');
$ct =~ s/;.*//s;
my $fmts = {};
my $i = 0;
my %v = ( fmt => $i,
url => $src,
content_type => $ct,
w => $w,
h => $h,
# size => undef,
# abr => undef,
);
$fmts->{$i} = \%v;
$fmts->{title} = $title;
$fmts->{year} = $year;
$fmts->{thumb} = $thumb if $thumb;
return $fmts;
}
# Returns a hash of:
# [ title: "T",
# year: "Y",
# 0: [ ...video info... ],
# Since Twitter only offers one resolution.
#
sub load_twitter_formats($$$) {
my ($id, $url, $size_p) = @_;
my ($http, $head, $body) = get_url ($url);
check_http_status ($id, $url, $http, 1);
my ($title) = ($body =~ m@<meta \s+ property="og:title" \s+
content="([^<>]*?)"@six);
$url = "https://twitter.com/i/videos/tweet/$id";
($http, $head, $body) = get_url ($url);
check_http_status ($id, $url, $http, 1);
my ($id2) = ($body =~ m@/tweet_video\\?/([^<>&?.]+)@s);
# ($id2) = ($body =~ m@/web-video-player/([^<>&?/\\]+)@s) unless $id2;
# ($id2) = ($body =~ m@/ext_tw_video\\?/([^<>&?/\\]+)@s) unless $id2;
# errorI ("$id: video ID not found\n$body") unless ($id2);
my $src;
if ($id2) {
$src = "https://pbs.twimg.com/tweet_video/$id2.mp4";
} else {
my $url2 = "https://twitter.com/i/videos/$id";
($http, $head, $body) = get_url ($url2);
check_http_status ($id, $url2, $http, 1);
($id2) = ($body =~ m@/ext_tw_video\\?/([^<>&?/\\]+)@s);
$body = html_unquote($body);
($src) = ($body =~ m@"video_url":"([^\"]+)"@si);
error ("Twitter is a piece of shit, none of this works any more")
unless ($src);
errorI ("$id: video_url not found") unless ($src);
$src =~ s/\\//gs;
# Now Twitter is giving us an ".m3u8u" chunked file instead of an .mp4
# because fuck you that's why.
#
if ($src =~ m@\.m3u[^/]+$@s) {
# ($http, $head, $body) = get_url ($src);
#### ...
error ("Twitter is a piece of shit, we can't handle .m3u8u video");
}
}
$title =~ s/ on Twitter$//s;
$title = munge_title (html_unquote ($title || ''));
sanity_check_title ($title, $url, $body, 'load_twitter_formats');
my $ct = 'image/mp4';
my ($w, $h) = (0, 0);
my $year = undef;
my $fmts = {};
my $i = 0;
my %v = ( fmt => $i,
url => $src,
content_type => $ct,
w => $w,
h => $h,
# size => undef,
# abr => undef,
);
$fmts->{$i} = \%v;
$fmts->{title} = $title;
$fmts->{year} = $year;
return $fmts;
}
# Return the year at which this video was uploaded.
#
my %youtube_year_cache;
sub get_youtube_year($;$) {
my ($id, $body) = @_;
# Avoid loading the page twice.
my $year = $youtube_year_cache{$id};
return $year if $year;
# 13-May-2015: https://www.youtube.com/watch?v=99lDR6jZ8yE (Lamb)
# HTML says this:
# <strong class="watch-time-text">Uploaded on Oct 28, 2011</strong>
# But /feeds/api/videos/99lDR6jZ8yE?v=2 says:
# <updated> 2015-05-13T21:13:28.000Z
# <published> 2015-04-17T15:23:22.000Z
# <yt:uploaded> 2015-04-17T15:23:22.000Z
#
# And one of my own: https://www.youtube.com/watch?v=HbN4wBJMOuE
# <strong class="watch-time-text">Published on Sep 20, 2014</strong>
# <published> 2015-04-17T15:23:22.000Z
# <updated> 2015-05-16T18:48:26.000Z
# <yt:uploaded> 2015-04-17T15:23:22.000Z
#
# In fact, I uploaded that on Sep 20, 2014, and when I did I set the
# Advanced Settings / Recording Date to Sep 14, 2014. Some time in
# 2015, I edited the description text. I have no theory for why the
# "published" and "updated" dates are different and are both 2015.
#
# So, let's scrape the HTML isntead of using the API.
#
# (Actually, we don't have a choice now anyway, since they turned off
# the v2 API in June 2015, and the v3 API requires authentication.)
# my $data_url = ("https://gdata.youtube.com/feeds/api/videos/$id?v=2" .
# "&fields=published" .
# "&safeSearch=none" .
# "&strict=true");
my $data_url = "https://www.youtube.com/watch?v=$id";
my ($http, $head);
if (! $body) {
($http, $head, $body) = get_url ($data_url);
return undef unless check_http_status ($id, $data_url, $http, 0);
}
# my ($year, $mon, $dotm, $hh, $mm, $ss) =
# ($body =~ m@<published>(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)@si);
($year) = ($body =~ m@\bclass="watch-time-text">[^<>]+\b(\d{4})</@s);
$youtube_year_cache{$id} = $year;
return $year;
}
# Return the year at which this video was uploaded.
#
sub get_vimeo_year($) {
my ($id) = @_;
my $data_url = "https://vimeo.com/api/v2/video/$id.xml";
my ($http, $head, $body) = get_url ($data_url);
return undef unless check_http_status ($id, $data_url, $http, 0);
my ($year, $mon, $dotm, $hh, $mm, $ss) =
($body =~ m@<upload_date>(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)@si);
return $year;
}
my $ffmpeg_warned_p = 0;
# Given a list of available underlying videos, pick the ones we want.
#
sub pick_download_format($$$$$) {
my ($id, $site, $url, $force_fmt, $fmts) = @_;
foreach my $k (keys %$fmts) {
next if ($k eq 'title' || $k eq 'year' || $k eq 'cipher' || $k eq 'thumb');
my $fmt = $fmts->{$k};
my $ct = $fmt->{content_type} || '';
# Youtube has started returning "video/mp4" content types that are
# actually VP9 / WebM rather than H.264. Those must be transcoded,
# not copied, to play on H.264-only devices.
#
if ($ct =~ m@^video/mp4.*vp9@si) { # video/mp4;+codecs="vp9"
$fmt->{content_type} = 'video/webm';
} elsif ($ct =~ m@^audio/mp4.*opus@si) { # audio/mp4;+codecs="opus"
$fmt->{content_type} = 'audio/opus';
}
}
if (defined($force_fmt) && $force_fmt eq 'all') {
my @all = ();
foreach my $k (keys %$fmts) {
next if ($k eq 'title' || $k eq 'year' || $k eq 'cipher' ||
$k eq 'thumb');
push @all, $k;
}
return sort { $a <=> $b } @all;
}
if ($site eq 'vimeo' ||
$site eq 'tumblr' ||
$site eq 'instagram' ||
$site eq 'twitter') {
# On these sites, just pick the entry with the largest size
# and/or resolution.
# No muxing needed on Vimeo
$force_fmt = undef if ($force_fmt && $force_fmt eq 'mux');
if (defined($force_fmt)) {
error ("$site --fmt must be digits: $force_fmt")
unless ($force_fmt =~ m/^\d+$/s);
foreach my $k (keys %$fmts) {
if ($k eq $force_fmt) {
print STDERR "$progname: $id: forced #$k (" .
$fmts->{$k}->{w} . " x " .
$fmts->{$k}->{h} . ")\n"
if ($verbose > 1);
return $k;
}
}
error ("$id: format $force_fmt does not exist");
}
my $best = undef;
foreach my $k (keys %$fmts) {
next if ($k eq 'title' || $k eq 'year' || $k eq 'cipher' ||
$k eq 'thumb');
$best = $k
if (!defined($best) ||
(($fmts->{$k}->{size} || 0) > ($fmts->{$best}->{size} || 0) ||
($fmts->{$k}->{w} * $fmts->{$k}->{h} >
$fmts->{$best}->{w} * $fmts->{$best}->{h})));
}
print STDERR "$progname: $id: picked #$best (" .
$fmts->{$best}->{w} . " x " .
$fmts->{$best}->{h} . ")\n"
if ($verbose > 1);
return $best;
} elsif ($site ne 'youtube') {
errorI ("unknown site $site");
}
errorI ("$id: unrecognized site: $url") unless ($site eq 'youtube');
my %known_formats = (
#
# v=undef means it's an audio-only format.
# a=undef means it's a video-only format.
# Codecs "mp4S" and "webmS" are 3d video (left/right stereo).
#
# ID video container video size audio codec bitrate
#
0 => { v => 'flv', w => 320, h => 180, a => 'mp3', abr => 64 },
5 => { v => 'flv', w => 320, h => 180, a => 'mp3', abr => 64 },
6 => { v => 'flv', w => 480, h => 270, a => 'mp3', abr => 96 },
13 => { v => '3gp', w => 176, h => 144, a => 'amr', abr => 13 },
17 => { v => '3gp', w => 176, h => 144, a => 'aac', abr => 24 },
18 => { v => 'mp4', w => 480, h => 360, a => 'aac', abr => 125 },
22 => { v => 'mp4', w => 1280, h => 720, a => 'aac', abr => 198 },
34 => { v => 'flv', w => 640, h => 360, a => 'aac', abr => 52 },
35 => { v => 'flv', w => 854, h => 480, a => 'aac', abr => 107 },
36 => { v => '3gp', w => 320, h => 240, a => 'aac', abr => 37 },
37 => { v => 'mp4', w => 1920, h => 1080, a => 'aac', abr => 128 },
38 => { v => 'mp4', w => 4096, h => 2304, a => 'aac', abr => 128 },
43 => { v => 'webm', w => 640, h => 360, a => 'vor', abr => 128 },
44 => { v => 'webm', w => 854, h => 480, a => 'vor', abr => 128 },
45 => { v => 'webm', w => 1280, h => 720, a => 'vor', abr => 128 },
46 => { v => 'webmS',w => 1920, h => 1080, a => 'vor', abr => 128 },
59 => { v => 'mp4', w => 854, h => 480, a => 'aac', abr => 128 },
78 => { v => 'mp4', w => 720, h => 406, a => 'aac', abr => 128 },
82 => { v => 'mp4S', w => 640, h => 360, a => 'aac', abr => 128 },
83 => { v => 'mp4S', w => 854, h => 240, a => 'aac', abr => 128 },
84 => { v => 'mp4S', w => 1280, h => 720, a => 'aac', abr => 198 },
85 => { v => 'mp4S', w => 1920, h => 520, a => 'aac', abr => 198 },
92 => { v => 'mp4', w => 320, h => 240, a => undef },
93 => { v => 'mp4', w => 640, h => 360, a => undef },
94 => { v => 'mp4', w => 854, h => 480, a => undef },
95 => { v => 'mp4', w => 1280, h => 720, a => undef },
96 => { v => 'mp4', w => 1920, h => 1080, a => undef },
100 => { v => 'webmS',w => 640, h => 360, a => 'vor', abr => 128 },
101 => { v => 'webmS',w => 854, h => 480, a => 'vor', abr => 128 },
102 => { v => 'webmS',w => 1280, h => 720, a => 'vor', abr => 128 },
120 => { v => 'flv', w => 1280, h => 720, a => 'aac', abr => 128 },
132 => { v => 'mp4', w => 320, h => 240, a => undef },
133 => { v => 'mp4', w => 426, h => 240, a => undef },
134 => { v => 'mp4', w => 640, h => 360, a => undef },
135 => { v => 'mp4', w => 854, h => 480, a => undef },
136 => { v => 'mp4', w => 1280, h => 720, a => undef },
137 => { v => 'mp4', w => 1920, h => 1080, a => undef },
138 => { v => 'mp4', w => 3840, h => 2160, a => undef },
139 => { v => undef, a => 'm4a', abr => 48 },
140 => { v => undef, a => 'm4a', abr => 128 },
141 => { v => undef, a => 'm4a', abr => 256 },
142 => { v => 'mp4', w => 426, h => 240, a => undef },
143 => { v => 'mp4', w => 640, h => 360, a => undef },
144 => { v => 'mp4', w => 854, h => 480, a => undef },
145 => { v => 'mp4', w => 1280, h => 720, a => undef },
146 => { v => 'mp4', w => 1920, h => 1080, a => undef },
148 => { v => undef, a => 'aac', abr => 51 },
149 => { v => undef, a => 'aac', abr => 132 },
150 => { v => undef, a => 'aac', abr => 260 },
151 => { v => 'mp4', w => 72, h => 32, a => undef },
160 => { v => 'mp4', w => 256, h => 144, a => undef },
161 => { v => 'mp4', w => 256, h => 144, a => undef },
167 => { v => 'webm', w => 640, h => 360, a => undef },
168 => { v => 'webm', w => 854, h => 480, a => undef },
169 => { v => 'webm', w => 1280, h => 720, a => undef },
170 => { v => 'webm', w => 1920, h => 1080, a => undef },
171 => { v => undef, a => 'vor', abr => 128 },
172 => { v => undef, a => 'vor', abr => 256 },
218 => { v => 'webm', w => 854, h => 480, a => undef },
219 => { v => 'webm', w => 854, h => 480, a => undef },
222 => { v => 'mp4', w => 854, h => 480, a => undef },
223 => { v => 'mp4', w => 854, h => 480, a => undef },
224 => { v => 'mp4', w => 1280, h => 720, a => undef },
225 => { v => 'mp4', w => 1280, h => 720, a => undef },
226 => { v => 'mp4', w => 1920, h => 1080, a => undef },
227 => { v => 'mp4', w => 1920, h => 1080, a => undef },
242 => { v => 'webm', w => 426, h => 240, a => undef },
243 => { v => 'webm', w => 640, h => 360, a => undef },
244 => { v => 'webm', w => 854, h => 480, a => undef },
245 => { v => 'webm', w => 854, h => 480, a => undef },
246 => { v => 'webm', w => 854, h => 480, a => undef },
247 => { v => 'webm', w => 1280, h => 720, a => undef },
248 => { v => 'webm', w => 1920, h => 1080, a => undef },
249 => { v => undef, a => 'vor', abr => 50 },
250 => { v => undef, a => 'vor', abr => 70 },
251 => { v => undef, a => 'vor', abr => 160 },
256 => { v => undef, a => 'm4a', abr => 97, c=>5.1},
258 => { v => undef, a => 'm4a', abr => 191, c=>5.1},
264 => { v => 'mp4', w => 2560, h => 1440, a => undef },
266 => { v => 'mp4', w => 3840, h => 2160, a => undef },
271 => { v => 'webm', w => 2560, h => 1440, a => undef },
272 => { v => 'webm', w => 3840, h => 2160, a => undef },
273 => { v => 'webm', w => 854, h => 480, a => undef },
274 => { v => 'webm', w => 1280, h => 720, a => undef },
275 => { v => 'webm', w => 1920, h => 1080, a => undef },
278 => { v => 'webm', w => 256, h => 144, a => undef },
279 => { v => 'webm', w => 426, h => 240, a => undef },
280 => { v => 'webm', w => 640, h => 360, a => undef },
298 => { v => 'mp4', w => 1280, h => 720, a => undef },
299 => { v => 'mp4', w => 1920, h => 1080, a => undef },
302 => { v => 'webm', w => 1280, h => 720, a => undef },
303 => { v => 'webm', w => 1920, h => 1080, a => undef },
304 => { v => 'mp4', w => 2560, h => 1440, a => undef },
305 => { v => 'mp4', w => 3840, h => 1920, a => undef },
# 308 => { v => 'mp4', w => 2560, h => 1440, a => undef },
308 => { v => 'webm', w => 2560, h => 1440, a => undef },
313 => { v => 'webm', w => 3840, h => 2160, a => undef },
315 => { v => 'webm', w => 3840, h => 2160, a => undef },
317 => { v => 'webm', w => 854, h => 480, a => undef },
318 => { v => 'webm', w => 854, h => 480, a => undef },
327 => { v => undef, a => 'm4a', abr => 128, c=>5.1 },
330 => { v => 'webm', w => 256, h => 144, a => undef },
331 => { v => 'webm', w => 426, h => 240, a => undef },
332 => { v => 'webm', w => 640, h => 360, a => undef },
333 => { v => 'webm', w => 854, h => 480, a => undef },
334 => { v => 'webm', w => 1280, h => 720, a => undef },
335 => { v => 'webm', w => 1920, h => 1080, a => undef },
336 => { v => 'webm', w => 2560, h => 1440, a => undef },
337 => { v => 'webm', w => 3840, h => 2160, a => undef },
338 => { v => undef, a => 'vor', abr => 4 },
339 => { v => undef, a => 'vor', abr => 170, c=>5.1 },
350 => { v => undef, a => 'vor', abr => 50 },
351 => { v => undef, a => 'vor', abr => 49 },
352 => { v => undef, a => 'vor', abr => 3 },
357 => { v => 'webm', w => 1280, h => 720, a => undef },
358 => { v => 'webm', w => 1280, h => 720, a => undef },
359 => { v => 'webm', w => 1920, h => 1080, a => undef },
360 => { v => 'webm', w => 1920, h => 1080, a => undef },
394 => { v => 'av1', w => 256, h => 144, a => undef },
395 => { v => 'av1', w => 426, h => 240, a => undef },
396 => { v => 'av1', w => 640, h => 360, a => undef },
397 => { v => 'av1', w => 854, h => 480, a => undef },
398 => { v => 'av1', w => 1280, h => 720, a => undef },
399 => { v => 'av1', w => 1920, h => 1080, a => undef },
400 => { v => 'av1', w => 2560, h => 1440, a => undef },
401 => { v => 'av1', w => 3840, h => 2160, a => undef },
402 => { v => 'av1', w => 3840, h => 2160, a => undef },
403 => { v => 'av1', w => 5888, h => 2160, a => undef },
597 => { v => 'mp4', w => 256, h => 144, a => undef },
598 => { v => 'webm', w => 256, h => 144, a => undef },
599 => { v => undef, a => 'aac', abr => 30 },
600 => { v => undef, a => 'opus', abr => 35 },
);
#
# The table on https://en.wikipedia.org/wiki/YouTube#Quality_and_formats
# disagrees with the above to some extent. Which is more accurate?
# (Oh great, they deleted that table from Wikipedia. Lovely.)
# (Ah great, they added the table back to Wikipedia Mar 2016.)
# (Aaaand it's gone again, some time before Mar 2019.)
#
# fmt=38/37/22 are only available if upload was that exact resolution.
#
# For things uploaded in 2009 and earlier, fmt=18 was higher resolution
# than fmt=34. But for things uploaded later, fmt=34 is higher resolution.
# This code assumes that 34 is the better of the two.
#
# The WebM formats 43, 44 and 45 began showing up around Jul 2011.
# The MP4 versions are higher resolution (e.g. 37=1080p but 45=720p).
#
# The stereo/3D formats 46, 82-84, 100-102 first spotted in Sep/Nov 2011.
#
# As of Jan 2015, Youtube seems to have stopped serving format 37 (1080p),
# but is instead serving 137 (1080p, video only). To download anything of
# 1080p or higher, you are expected to download a video-only and an
# audio-only stream and mux them on the client side. This is insane.
# It seems that "urlmap" contains the muxed videos and "adaptive_fmts"
# contains the unmuxed ones.
#
# For debugging this stuff, use "--fmt N" to force downloading of a
# particular format or "--fmt all" to grab them all.
#
#
# Test cases and examples:
#
# https://www.youtube.com/watch?v=wjzyv2Q_hdM
# 5-Aug-2011: 38=flv/1080p but 45=webm/720p.
# 6-Aug-2011: 38 no longer offered.
#
# https://www.youtube.com/watch?v=ms1C5WeSocY
# 6-Aug-2011: embedding disabled, but get_video_info works.
#
# https://www.youtube.com/watch?v=g40K0dFi9Bo
# 10-Sep-2011: 3D, fmts 82 and 84.
#
# https://www.youtube.com/watch?v=KZaVq1tFC9I
# 14-Nov-2011: 3D, fmts 100 and 102. This one has 2D images in most
# formats but left/right images in the 3D formats.
#
# https://www.youtube.com/watch?v=SlbpRviBVXA
# 15-Nov-2011: 3D, fmts 46, 83, 85, 101. This one has left/right images
# in all of the formats, even the 2D formats.
#
# https://www.youtube.com/watch?v=711bZ_pLusQ
# 30-May-2012: First sighting of fmt 36, 3gpp/240p.
#
# https://www.youtube.com/watch?v=0yyorhl6IjM
# 30-May-2013: Here's one that's more than an hour long.
#
# https://www.youtube.com/watch?v=pc4ANivCCgs
# 15-Nov-2013: First sighting of formats 59 and 78.
#
# https://www.youtube.com/watch?v=WQzVhOZnku8
# 3-Sep-2014: First sighting of a 24/7 realtime stream.
#
# https://www.youtube.com/watch?v=gTIK2XawLDA
# 22-Jan-2015: DNA Lounge 24/7 live stream, 640x360.
#
# https://www.youtube.com/watch?v=hHKJ5eE7I1k
# 22-Jan-2015: 2K video. Formats 36, 136, 137, 138.
#
# https://www.youtube.com/watch?v=udAL48P5NJU
# 22-Jan-2015: 4K video. Formats 36, 136, 137, 138, 266, 313.
#
# https://www.youtube.com/watch?v=OEhRucEVzH8
# 20-Feb-2015: best formats 18 (640 x 360) and 135 (854 x 480)
# First sighting of a video where we must mux to get the best
# non-HD version.
#
# https://www.youtube.com/watch?v=Ol61WOSzLF8
# 10-Mar-2015: formerly RTMPE but 14-Apr-2015 no longer
#
# https://www.youtube.com/watch?v=1ltcDfZMA3U Maps
# 29-Mar-2015: formerly playable in US region, but no longer
#
# https://www.youtube.com/watch?v=ttqMGYHhFFA Metric
# 29-Mar-2015: Formerly enciphered, but no longer
#
# https://www.youtube.com/watch?v=7wL9NUZRZ4I Bowie
# 29-Mar-2015: Formerly enciphered and content warning; no longer CW.
#
# https://www.youtube.com/watch?v=07FYdnEawAQ Timberlake
# 29-Mar-2015: enciphered and "content warning" (HTML scraping fails)
#
# https://youtube.com/watch?v=HtVdAasjOgU
# 29-Mar-2015: content warning, but non-enciphered
#
# https://www.youtube.com/watch?v=__2ABJjxzNo
# 29-Mar-2015: has url_encoded_fmt_stream_map but not adaptive_fmts
#
# https://www.youtube.com/watch?v=lqQg6PlCWgI
# 29-Mar-2015: finite-length archive of a formerly livestreamed video.
# We currently can't download this, but it's doable.
# See dna/backstage/src/slideshow/slideshow-youtube-frame.pl
# Update, 7-Aug-2016: this one works now; it seems to have been
# converted to a normal video with a url map.
#
# Enciphered:
# https://www.youtube.com/watch?v=ktoaj1IpTbw Chvrches
# https://www.youtube.com/watch?v=28Vu8c9fDG4 Emika
# https://www.youtube.com/watch?v=_mDxcDjg9P4 Vampire Weekend
# https://www.youtube.com/watch?v=8UVNT4wvIGY Gotye
# https://www.youtube.com/watch?v=OhhOU5FUPBE Black Sabbath
# https://www.youtube.com/watch?v=UxxajLWwzqY Icona Pop
#
# https://www.youtube.com/watch?v=g_uoH6hJilc
# 28-Mar-2015: enciphered Vevo (Years & Years) on which CTF was failing
#
# https://www.youtube.com/watch?v=ccyE1Kz8AgM
# 28-Mar-2015: not viewable in US (US is not on the include list)
#
# https://www.youtube.com/watch?v=ccyE1Kz8AgM
# 28-Mar-2015: blocked in US (US is on the exclude list)
#
# https://www.youtube.com/watch?v=GjxOqc5hhqA
# 28-Mar-2015: says "please sign in", but when signed in, it's private
#
# https://www.youtube.com/watch?v=UlS_Rnb5WM4
# 28-Mar-2015: non-embeddable (Pogo)
#
# https://www.youtube.com/watch?v=JYEfJhkPK7o
# 14-Apr-2015: RTMPE DRM
# get_video_info fails with "This video contains content from Mosfilm,
# who has blocked it from display on this website. Watch on Youtube."
# There's a generic rtmpe: URL in "conn" and a bunch of options in
# "stream", but I don't know how to put those together into an
# invocation of "rtmpdump" that does anything at all.
#
# https://www.youtube.com/watch?v=UXMG102kSvk
# 17-Aug-2015: WebM higher rez than MP4:
# 299 (1920 x 1080 mp4 v/o)
# 308 (2560 x 1440 webm v/o) <-- webm, not mp4
# 315 (3840 x 2160 webm v/o)
#
# https://www.youtube.com/watch?v=dC_nFgJAcuQ
# 2-Dec-2015: First sighting of 5.1 stereo formats 256 and 258.
#
# https://www.youtube.com/watch?v=vBtlUl-Xh5w
# 30-Jun-2016: First sighting of 5.1 stereo formats 327 and 339.
#
# https://www.youtube.com/watch?v=uTnO1ITQWr0
# 6-Aug-2016: finite-length archive of a formerly livestreamed video.
# This is Flash-player only because it has embedding disabled.
# We currently can't download this, but it's doable.
# See dna/backstage/src/slideshow/slideshow-youtube-frame.pl
#
# https://www.youtube.com/watch?v=oVjMF_TfY6M
# 17-Aug-2018: Content warning and not embeddable
#
# https://www.youtube.com/watch?v=HtfKRdRJIEs
# 17-Dec-2018: fmt 135 from dashmpd works, but fmt 135 HTML is 404.
#
# https://www.youtube.com/watch?v=I_MkW0CW4QM
# 17-Dec-2018: both dashmpd and non, fmt 137
#
# https://www.youtube.com/watch?v=6od76UNHt-M
# 13-Jan-2019, only one format, 18
#
# https://www.youtube.com/watch?v=jy0Q75xCwDU
# 26-May-2019, no pre-muxed formats, can't be downloaded without ffmpeg.
# Divide %known_formats into muxed, video-only and audio-only lists.
#
my (@pref_muxed, @pref_vo, @pref_ao);
foreach my $id (keys (%known_formats)) {
my $fmt = $known_formats{$id};
my $v = $fmt->{v};
my $a = $fmt->{a};
my $b = $fmt->{abr};
my $c = $fmt->{c}; # channels (e.g. 5.1)
my $w = $fmt->{w};
my $h = $fmt->{h};
$known_formats{$id}->{desc} = (($w && $h ? "$w x $h $v" :
$b ? "$b kbps $a" :
"?x?") .
($c ? " $c" : '') .
($w && $h && $b ? '' :
$w ? ' v/o' : ' a/o'));
error ("W and H flipped: $id") if ($w && $h && $w < $h);
# Ignore 3d video or other weirdo vcodecs.
next if ($v && !($v =~ m/^(mp4|flv|3gp|webm)$/));
if (! $webm_p) {
# Skip WebM and Vorbis if desired.
next if ($a && !$v && $a =~ m/^(vor)$/);
next if (!$a && $v && $v =~ m/^(webm)$/);
}
if ($v && $a) {
push @pref_muxed, $id;
} elsif ($v) {
push @pref_vo, $id;
} else {
push @pref_ao, $id;
}
}
# Sort each of those lists in order of download preference.
#
foreach my $S (\@pref_muxed, \@pref_vo, \@pref_ao) {
@$S = sort {
my $A = $known_formats{$a};
my $B = $known_formats{$b};
my $aa = $A->{h} || 0; # Prefer taller video.
my $bb = $B->{h} || 0;
return ($bb - $aa) unless ($aa == $bb);
$aa = (($A->{v} || '') eq 'mp4'); # Prefer MP4 over WebM.
$bb = (($B->{v} || '') eq 'mp4');
return ($bb - $aa) unless ($aa == $bb);
$aa = $A->{c} || 0; # Prefer 5.1 over stereo.
$bb = $B->{c} || 0;
return ($bb - $aa) unless ($aa == $bb);
$aa = $A->{abr} || 0; # Prefer higher audio rate.
$bb = $B->{abr} || 0;
return ($bb - $aa) unless ($aa == $bb);
$aa = (($A->{a} || '') eq 'aac'); # Prefer AAC over MP3.
$bb = (($B->{a} || '') eq 'aac');
return ($bb - $aa) unless ($aa == $bb);
$aa = (($A->{a} || '') eq 'mp3'); # Prefer MP3 over Vorbis.
$bb = (($B->{a} || '') eq 'mp3');
return ($bb - $aa) unless ($aa == $bb);
return 0;
} @$S;
}
my $vfmt = undef;
my $afmt = undef;
my $mfmt = undef;
# Find the best pre-muxed format.
#
foreach my $target (@pref_muxed) {
if ($fmts->{$target}) {
$mfmt = $target;
last;
}
}
# If muxing is allowed, find the best un-muxed pair of formats, if
# such a pair exists that is higher resolution than the best
# pre-muxed format.
#
if (defined($force_fmt) && $force_fmt eq 'mux') {
foreach my $target (@pref_vo) {
if ($fmts->{$target}) {
$vfmt = $target;
last;
}
}
# WebM must always be paired with Vorbis audio.
# MP4 must always be paired with MP3, M4A or AAC audio.
my $want_vorbis_p = ($vfmt && $known_formats{$vfmt}->{v} =~ m/^webm/si);
foreach my $target (@pref_ao) {
next unless $fmts->{$target};
my $is_vorbis_p = (($known_formats{$target}->{a} || '') =~ m/^vor/si);
if (!!$want_vorbis_p == !!$is_vorbis_p) {
$afmt = $target;
last;
}
}
# If we got one of the formats and not the other, this isn't going to
# work. Fall back on pre-muxed.
#
if (($vfmt || $afmt) && !($vfmt && $afmt)) {
print STDERR "$progname: $id: found " .
($vfmt ? 'video-only' : 'audio-only') . ' but no ' .
($afmt ? 'video-only' : 'audio-only') . " formats.\n"
if ($verbose > 1);
$vfmt = undef;
$afmt = undef;
}
# If the best unmuxed format is not better resolution than the best
# pre-muxed format, just use the pre-muxed version.
#
if ($mfmt &&
$vfmt &&
$known_formats{$vfmt}->{h} <= $known_formats{$mfmt}->{h}) {
print STDERR "$progname: $id: rejecting $vfmt + $afmt (" .
$known_formats{$vfmt}->{w} . " x " .
$known_formats{$vfmt}->{h} . ") for $mfmt (" .
$known_formats{$mfmt}->{w} . " x " .
$known_formats{$mfmt}->{h} . ")\n"
if ($verbose > 1);
$vfmt = undef;
$afmt = undef;
}
# At this point, we're definitely intending to mux.
# But maybe we can't because there's no ffmpeg -- if so, print
# a warning, then fall back to a lower resolution stream, if possible.
#
if ($vfmt && $afmt && !which ("ffmpeg")) {
if (!$ffmpeg_warned_p) {
print STDERR "$progname: WARNING: $id: \"ffmpeg\" not installed.\n";
print STDERR "$progname: $id: downloading lower resolution.\n"
if ($mfmt);
$ffmpeg_warned_p = 1;
}
$vfmt = undef;
$afmt = undef;
}
}
# If there is a format in the list that we don't know about, warn.
# This is the only way I have of knowing when new ones turn up...
#
{
my @unk = ();
foreach my $k (sort keys %$fmts) {
next if ($k eq 'title' || $k eq 'year' || $k eq 'cipher' ||
$k eq 'thumb');
push @unk, $k if (!$known_formats{$k});
}
print STDERR "$progname: $id: unknown format " . join(', ', @unk) .
"$errorI\n"
if (@unk);
}
if ($verbose > 1) {
print STDERR "$progname: $id: available formats:\n";
foreach my $k (sort { ($a =~ m/^\d+$/s ? $a : 0) <=>
($b =~ m/^\d+$/s ? $b : 0) }
keys(%$fmts)) {
next if ($k eq 'title' || $k eq 'year' || $k eq 'cipher' ||
$k eq 'thumb');
print STDERR sprintf("%s: %3d (%s)\n",
$progname, $k,
($known_formats{$k}->{desc} || '?') .
($fmts->{$k}->{dashp}
? (' dash' .
((ref ($fmts->{$k}->{url}) eq 'ARRAY')
? ' ' . scalar(@{$fmts->{$k}->{url}}) .
' segments'
: ''))
: ''));
}
}
if ($vfmt && $afmt) {
if ($verbose > 1) {
my $d1 = $known_formats{$vfmt}->{desc};
my $d2 = $known_formats{$afmt}->{desc};
foreach ($d1, $d2) { s@ [av]/?o$@@si; }
$d1 .= ' dash' if ($fmts->{$vfmt}->{dashp});
$d2 .= ' dash' if ($fmts->{$afmt}->{dashp});
print STDERR "$progname: $id: picked $vfmt + $afmt ($d1 + $d2)\n";
}
return ($vfmt, $afmt);
} elsif ($mfmt) {
# Either not muxing, or muxing not available/necessary.
my $why = 'picked';
if (defined($force_fmt) && $force_fmt ne 'mux') {
error ("$id: format $force_fmt does not exist")
unless ($fmts->{$force_fmt});
$why = 'forced';
$mfmt = $force_fmt;
}
print STDERR "$progname: $id: $why $mfmt (" .
($known_formats{$mfmt}->{desc} || '???') . ")\n"
if ($verbose > 1);
return ($mfmt);
} elsif ($force_fmt) {
return $force_fmt;
} else {
error ("$id: No pre-muxed formats; \"ffmpeg\" required for download");
}
}
# This is all completely horrible: try to convert the random crap people
# throw into Youtube video titles into something more consistent.
#
# - Aims for "Artist -- Title" instead of various other ways of spelling that.
# - Omits noise phrases like "official music video" and "high quality".
# - Downcases things that appear to be gratuitously in all-caps.
#
# This likely does stupid things on Youtube things that aren't music videos.
#
sub munge_title($) {
my ($title) = @_;
return 'Untitled' unless defined($title);
sub unihex($;) {
my ($c) = @_;
$c = hex($c);
return '' if ($c >= 0xD800 && $c <= 0xDFFF); # UTF-16 surrogate
my $s = chr($c);
# If this is a single-byte non-ASCII character, chr() created a
# single-byte non-Unicode string. Assume that byte is Latin1 and
# expand it to the corresponding unicode character.
#
# Test cases:
# https://www.vimeo.com/82503761 é as \u00e9\u00a0
# https://www.vimeo.com/123397581 û– as \u00fb\u2013
# https://www.youtube.com/watch?v=z9ScJBmEdQw ä as UTF8 (2 bytes)
# https://www.youtube.com/watch?v=eAXmgId3NTQ ø as UTF8 (2 bytes)
# https://www.youtube.com/watch?v=FszEaxrHGTs ∆ as UTF8 (3 bytes)
# https://www.youtube.com/watch?v=4ViwSeuWVfE JP as UTF8 (3 bytes)
# https://vimeo.com/118261420 Snowman emoji, etc.
#
# If this is still a Latin1 string, upgrade it to wide chars.
if (! utf8::is_utf8($s)) {
utf8::encode ($s); # Unpack Latin1 into multi-byte UTF-8.
utf8::decode ($s); # Pack multi-byte UTF-8 into wide chars.
}
return $s;
}
utf8::decode ($title); # Pack multi-byte UTF-8 back into wide chars.
# Decode \u and \x syntax.
$title =~ s/ \\[ux] \{ ([a-z0-9]+) \} / unihex($1) /gsexi; # \u{XXXXXX}
$title =~ s/ \\[ux] ([a-z0-9]{4}) / unihex($1) /gsexi; # \uXXXX
$title =~ s/[\x{2012}-\x{2013}]+/-/gs; # various dashes
$title =~ s/[\x{2014}-\x{2015}]+/--/gs; # various long dashes
$title =~ s/\x{2018}+/\`/gs; # backquote
$title =~ s/\x{2019}+/\'/gs; # quote
$title =~ s/[\x{201c}\x{201d}]+/\"/gs; # ldquo, rdquo
$title =~ s/\`/\'/gs;
$title =~ s/\s*(\|\s*)+/ - /gs; # | to -
$title =~ s/\\//gs; # I think we can just omit other backslashes entirely.
# spacing, punctuation cleanups
$title =~ s/^\s+|\s+$//gs;
$title =~ s/\s+/ /gs;
$title =~ s/\s+,/,/gs;
$title =~ s@\s+w/\s+@ with @gs; # convert w/ to with
$title =~ s@(\d)/(?=\d)@$1.@gs; # convert / to . in dates
$title =~ s@/@ - @gs; # remaining / to delimiter
$title =~ s/^Youtube -+ //si;
$title =~ s/ -+ Youtube$//si;
$title =~ s/^Youtube$//si;
$title =~ s/ on Vimeo\s*$//si;
$title =~ s/Broadcast Yourself\.?$//si;
$title =~ s/\b ( ( (in \s*)?
(
HD | TV | HDTV | HQ | 720\s*p? | 1080\s*p? | 4K |
High [-\s]* Qual (ity)?
) |
FM(\'s)? |
EP s? (?>[\s\.\#]*) (?!\d+) | # allow "episode" usage
MV | performance |
SXSW ( \s* Music )? ( \s* \d{4} )? |
Showcasing \s Artist |
Presents |
(DVD|CD)? \s+ (out \s+ now | on \s+ (iTunes|Amazon)) |
fan \s* made |
( FULL|COMPLETE ) \s+ ( set|concert|album ) |
FREE \s+ ( download|D\s*[[:punct:]-]\s*L ) |
Live \s+ \@ \s .*
)
\b \s* )+ //gsix;
$title =~ s/\b (The\s*)? (Un)?Off?ici[ae]le?
( [-\s]*
( Video | Clip | Studio | Music | Audio | Stereo | Lyric )s?
)+
\b//gsix;
$title =~ s/\b Music ( [-\s]* ( Video | Clip )s?)+ \b//gsix;
$title =~ s/\.(mp[34]|m4[auv]|mov|mqv|flv|wmv)\b//si;
$title =~ s/\b(on\s*)? [A-Za-z-0-9.]+\.com $//gsix; # kill trailing urls
$title =~ s/\b(brought to you|made possible) by .*$//gsi; # herp derpidy derp
$title =~ s/\bour interview with\b/ interviews /gsi; # re-handled below
$title =~ s/\b(perform|performs|performing)\b/ - /gsi; # other delimiters
$title =~ s/\b(play |plays |playing )\b/ - /gsi; # other delimiters
$title =~ s/\s+ [\|+]+ \s+ / - /gsi; # other delimiters
$title =~ s/!+/!/gsi; # yes, I'm excited too
$title =~ s/\s+-+[\s-]*\s/ - /gsi; # condense multiple delimiters into one
$title =~ s/\s+/ /gs;
# Lose now-empty parens.
# 1 while ($title =~ s/\(\s*\)//gs);
# 1 while ($title =~ s/\[\s*\]//gs);
# 1 while ($title =~ s/\{\s*\}//gs);
# Lose now-empty parens.
#
# Any combination of these words is an empty phrase.
my ($empty_phrase)
= q/
\s*(
the | new | free | amazing | (un)?off?ici[ae]le? |
on | iTunes | Amazon | [\s[:punct:]]+ | version |
cc | song | video | audio | band | source | field |
extended | mix | remix | edit | stream | uncut | single |
track | to be | released? | out | now |
teaser | trailer | videoclip
)?\s*
/
;
# Jesus fuck, youtube, how much more diarrhea can there be??
# None. None more diarrhea.
1 while ($title =~ s/\(($empty_phrase)*\)//gsix); # Check all
1 while ($title =~ s/\[($empty_phrase)*\]//gsix); # three
1 while ($title =~ s/\{($empty_phrase)*\}//gsix); # paren styles.
$title =~ s/[-;:,\s]+$//gs; # trailing crap
$title =~ s/\bDirected by\b/Dir./gsi; # "Directed By" is not "A by B"
$title =~ s/\bProduced by\b/Prod./gsi; # "Produced By" is not "A by B"
# Guess the title and artist by applying a series of regexes, in order,
# Starting with the most sensitive attempts,
# slowly moving to the most stable attempts,
# and ending with the most desperate attempts.
my $obrack = '[\(\[\{]'; # for readability; matches the 3 major brackets.
my $cbrack = '[\)\]\}]'; # /$obrack $cbrack/ matches "[ }". close enough.
my ($artist, $track, $junk) = (undef, undef, '');
($title, $junk) = ($1, $2) # TITLE (JUNK)
if ($title =~ m/^(.*)\s+$obrack+ (.*) $cbrack+ $/six);
($title, $junk) = ($1, "$3 $junk") # TITLE (Dir. by D) .*
if ($title =~ m/^ ( .+? )
($obrack+|\s)\s* ((Dir|Prod)\. .*)$/six);
($track, $artist) = ($1, $2) # TRACK performed by ARTIST
if (!$artist && # TRACK by ARTIST
$title =~ m/^ ( .+? ) \b
(?: performed \s+ )? by \b ( .+ )$/six);
($artist, $track) = ($1, $2) # ARTIST performing TRACK
if (!$artist &&
$title =~ m/^ ( .+? ) \b (?: plays | playing | performs? |
performing )
\b ( .+ )$/six);
($artist, $track) = ($1, "\L$2\E $3") # ARTIST talks about HIMSELF
if (!$artist && # ^^^^^^^^^^^^^^^^^^^ = TRACK
$title =~ m/^ ( .+? ) \b
\(? \s* (interview|talks about) \s* \)?
\b \s* ( .+ ) $/six);
($artist, $track) = ($2, "interview by $1") # IDIOT interviews ARTIST
if (!$artist && # TRACK = interview by IDIOT
$title =~ m/^ ( .+? ) \b
(?: interviews | interviewing )
\b ( .+ )$/six);
($track, $artist) = ($1, $2) # "TRACK" ARTIST
if (!$artist &&
$title =~ m/^ \" ( .+? ) \" [,\s]+ ( .+ )$/six);
($artist, $track, $junk) = ($1, $2, "$3 $junk") # ARTIST "TRACK" JUNK
if (!$artist &&
$title =~ m/^ ( .+? ) [,\s]+ \" ( .+ ) \" ( .*? ) $/six);
($track, $artist) = ($1, $2) # 'TRACK' ARTIST
if (!$artist &&
$title =~ m/^ \' ( .+? ) \' [,\s]+ ( .+ )$/six);
($artist, $track, $junk) = ($1, $2, "$3 $junk") # ARTIST 'TRACK' JUNK
if (!$artist &&
$title =~ m/^ ( .+? ) [,\s]+ \' ( .+ ) \' ( .*? ) $/six);
($artist, $track) = ($1, $2) # ARTIST -- TRACK
if (!$artist &&
$title =~ m/^ ( .+? ) \s* --+ \s* ( .+ )$/six);
($artist, $track) = ($1, $2) # ARTIST: TRACK
if (!$artist &&
$title =~ m/^ ( .+? ) \s* :+ \s* ( .+ )$/six);
($artist, $track) = ($1, $2) # ARTIST-- TRACK
if (!$artist &&
$title =~ m/^ ( .+? ) --+ \s* ( .+ )$/six);
($artist, $track) = ($1, $2) # ARTIST - TRACK
if (!$artist &&
$title =~ m/^ ( .+? ) \s+ - \s+ ( .+ )$/six);
($artist, $track) = ($1, $2) # ARTIST- TRACK
if (!$artist &&
$title =~ m/^ ( .+? ) -+ \s* ( .+ )$/six);
($artist, $track) = ($1, $2) # ARTIST live at LOCATION
if (!$artist && # ^^^^^^^^^^^^^^^^ = TITLE
$title =~ m/^ ( .+? ) (live \s* (at|@) .+ )$/six);
($artist, $junk) = ($1, "$2 $junk") # more JUNK in $artist?
if ($artist &&
$artist =~ m/^ ( .+? ) \s+ -+ \s+ ( .+? ) $/six);
($track, $junk) = ($1, "$2 $junk") # live at LOCATION in $track?
if ($artist && $track &&
$track =~ m/^ ( .+? ) \s+ $obrack? ( live \s* (at|@) .* )$/six);
# ^^^^^^^---closing paren to be chopped below
# You will find my junk requires extra scrubbing today.
if ($junk) {
$junk =~ s/^\s+|\s+$//gs;
# disallow junk consisting of all punctuation,
# but allow junk consisting of all digits or foreign chars.
$junk = '' if $junk =~ m/^[[:punct:]\s]+$/i;
# de-parenthesize
$junk =~ s/^ [\(\[\{\s]+ (.+?) [\)\]\}\s]+ $/$1/six;
# Stahhhhhp...
$junk = '' if $junk =~ m/ ^ \s* ( (un)?off?ici[ae]le? | video ) \s* $/six;
}
# Thoroughly wash fruits and vegetables before eating.
foreach my $s ($artist, $track, $junk, $title) {
next unless $s;
# Allow leading and trailing "." here.
# Otherwise, it messes up
# Seasons -- ...Of Our Discontent
# Jordin Sparks -- S.O.S. (Let The Music Play)
# R.E.M. -- Automatic for the People
$s =~ s/^ [-\s\"\'\`\|,;:]+ |
[-\s\"\'\`\|,;:]+ $ //gsx;
# Remove easily-found unbalanced parens.
#
# "TRACK (by ARTIST)" becomes "ARTIST) - TRACK (".
# Cleaning unbalanced parens as below fixes that,
# but messes up the band name "Sunn O)))". Oh well.
next if $s =~ m/^Sunn [0O]\)\)\)?$/;
# I use defined() and /e to avoid undef warning for $1 replacement.
1 while ($s =~ s/^ ([^\(]*?) \) / defined($1)?$1:"" /gsex); # Leading
1 while ($s =~ s/^ ([^\[]*?) \] / defined($1)?$1:"" /gsex); # close
1 while ($s =~ s/^ ([^\{]*?) \} / defined($1)?$1:"" /gsex); # brackets.
1 while ($s =~ s/ \( ([^\)]*) $/ defined($1)?$1:"" /gsex); # Trailing
1 while ($s =~ s/ \[ ([^\]]*) $/ defined($1)?$1:"" /gsex); # open
1 while ($s =~ s/ \{ ([^\}]*) $/ defined($1)?$1:"" /gsex); # brackets.
# The above does NOT correct, for instance, "ARTIST - TRACK (2014) )".
# Maybe I'll fix that later; I do love the burden of inhuman toil.
# If there are no lower case letters,
# capitalize all fully-upper-case words (with some allowances).
my $okupper =
# There're fewer good all-caps artists than diarrhea words.
# Just list them.
# Ironically, the story of the band ALL CAPS
# is too stupid to warrant including them.
'NIN|MS\s?MR|RJD2|HNN|' # ARTISTS
.'MF\|?\s?MB\|?|'
.'STRFKR|EMA|UDG|BDRG|HOTT MT|'
.'RAW|MNDR|HTRK|SPC ECO|RTX|2NE1|'
.'BT|INXS|THX|SNL|CTRL|NSFW|DNA|'
.'POB|JPL|LNX|' # are these DJs? abbreviations?
.'YKWYR|MFN|TV|ICHRU|AAA|OK|MJ|' # TRACKS
.'I\s?L\s?U|TKO|SWAG|'
.'LAX|ADHD|BTR'
;
$s =~ s/\b([[:upper:]])([[:upper:]\d]+)\b/$1\L$2/gsi # Capitalize,
unless ($s =~ m/[a-z]/s || # unless lowercase or
$s =~ m/^($okupper)$/ # specifically okayed.
)
;
}
# THIS IS IT!
$title = "$artist - $track" if $artist;
$title .= " ($junk)" if $junk;
# Final cleanups, to prevent bad filenames
$title =~ s@\s*[/:]+\s*@ - @gs; # no colons or slashes
$title =~ s/^ - | - $//gs; # leading, trailing delimeters
$title =~ s/^\s+|\s+$//gs; # leading, trailing space
$title =~ s/\s+/ /gs; # multiple spaces
# Don't allow the title to begin with "." or it writes a hidden file.
# And dash causes a stdout dump.
$title =~ s/^[-.,\s]+//gs;
# Oh FFS. I don't know what's going on here, but on MacOS 10.13.6 we
# sometimes get "Illegal byte sequence" from open() when the UTF-8
# in the file name still has Latin1 in it somehow, e.g. when it
# somehow still has &oslash; as \370 instead of \303\270.
#
# This maybe has to do with Perl thinking "UTF-8" means "standard"
# and "utf8" means "anything goes"?
#
# The following round trip seems to clean it up, maybe.
#
$title = Encode::encode('UTF-8', $title);
$title = Encode::decode('UTF-8', $title);
return $title || "Untitled";
}
sub sanity_check_title($$$$) {
my ($title, $url, $body, $where) = @_;
errorI ("no title: $where, $url\n\n$body")
if (!$title || $title =~ m/^untitled$/si);
}
# Does any version of the file exist with the usual video suffixes?
# Returns the one that exists.
#
sub file_exists_with_suffix($;) {
my ($f) = @_;
foreach my $ext (@video_extensions) {
my $ff = "$f.$ext";
# No, don't do this.
# utf8::encode($ff); # Unpack wide chars into multi-byte UTF-8.
return ($ff) if -f ($ff);
}
return undef;
}
# There are so many ways to specify URLs of videos... Turn them all into
# something sane and parsable.
#
# Duplicated in youtubefeed.
sub canonical_url($;) {
my ($url) = @_;
# Forgive pinheaddery.
$url =~ s@&amp;@&@gs;
$url =~ s@&amp;@&@gs;
# Add missing "https:"
$url = "https://$url" unless ($url =~ m@^https?://@si);
# Rewrite youtu.be URL shortener.
$url =~ s@^https?://([a-z]+\.)?youtu\.be/@https://youtube.com/v/@si;
# Youtube's "attribution links" don't encode the second URL:
# there are two question marks. FFS.
# https://www.youtube.com/attribution_link?u=/watch?v=...&feature=...
$url =~ s@^(https?://[^/]*\byoutube\.com/)attribution_link\?u=/@$1@gsi;
# Rewrite Vimeo URLs so that we get a page with the proper video title:
# "/...#NNNNN" => "/NNNNN"
$url =~ s@^(https?://([a-z]+\.)?vimeo\.com/)[^\d].*\#(\d+)$@$1$3@s;
$url =~ s@^http:@https:@s; # Always https.
my ($id, $site, $playlist_p);
# Youtube /view_play_list?p= or /p/ URLs.
if ($url =~ m@^https?://(?:[a-z]+\.)?(youtube) (?:-nocookie)? \.com/
(?: view_play_list\?p= |
p/ |
embed/p/ |
.*? [?&] list=(?:PL)? |
embed/videoseries\?list=(?:PL)?
)
([^<>?&,]+) ($|&) @sx) {
($site, $id) = ($1, $2);
$url = "https://www.$site.com/view_play_list?p=$id";
$playlist_p = 1;
# Youtube "/verify_age" URLs.
} elsif ($url =~
m@^https?://(?:[a-z]+\.)?(youtube) (?:-nocookie)? \.com/+
.* next_url=([^&]+)@sx ||
$url =~ m@^https?://(?:[a-z]+\.)?google\.com/
.* service = (youtube)
.* continue = ( http%3A [^?&]+)@sx ||
$url =~ m@^https?://(?:[a-z]+\.)?google\.com/
.* service = (youtube)
.* next = ( [^?&]+)@sx
) {
$site = $1;
$url = url_unquote($2);
if ($url =~ m@&next=([^&]+)@s) {
$url = url_unquote($1);
$url =~ s@&.*$@@s;
}
$url = "https://www.$site.com$url" if ($url =~ m@^/@s);
# Youtube /watch/?v= or /watch#!v= or /v/ URLs.
} elsif ($url =~ m@^https?:// (?:[a-z]+\.)?
(youtube) (?:-nocookie)? (?:\.googleapis)? \.com/+
(?: (?: watch/? )? (?: \? | \#! ) v= |
v/ |
embed/ |
.*? &v= |
[^/\#?&]+ \#p(?: /[a-zA-Z\d] )* /
)
([^<>?&,\'\"]+) ($|[?&]) @sx) {
($site, $id) = ($1, $2);
$url = "https://www.$site.com/watch?v=$id";
# Youtube "/user" and "/profile" URLs.
} elsif ($url =~ m@^https?://(?:[a-z]+\.)?(youtube) (?:-nocookie)? \.com/
(?:user|profile).*\#.*/([^&/]+)@sx) {
$site = $1;
$id = url_unquote($2);
$url = "https://www.$site.com/watch?v=$id";
error ("unparsable user next_url: $url") unless $id;
# Vimeo /NNNNNN URLs
# and player.vimeo.com/video/NNNNNN
# and vimeo.com/m/NNNNNN
} elsif ($url =~
m@^https?://(?:[a-z]+\.)?(vimeo)\.com/(?:video/|m/)?(\d+)@s) {
($site, $id) = ($1, $2);
$url = "https://$site.com/$id";
# Vimeo /videos/NNNNNN URLs.
} elsif ($url =~ m@^https?://(?:[a-z]+\.)?(vimeo)\.com/.*/videos/(\d+)@s) {
($site, $id) = ($1, $2);
$url = "https://$site.com/$id";
# Vimeo /channels/name/NNNNNN URLs.
# Vimeo /ondemand/name/NNNNNN URLs.
} elsif ($url =~
m@^https?://(?:[a-z]+\.)?(vimeo)\.com/[^/]+/[^/]+/(\d+)@s) {
($site, $id) = ($1, $2);
$url = "https://$site.com/$id";
# Vimeo /album/NNNNNN/video/MMMMMM
} elsif ($url =~
m@^https?://(?:[a-z]+\.)?(vimeo)\.com/album/\d+/video/(\d+)@s) {
($site, $id) = ($1, $2);
$url = "https://$site.com/$id";
# Vimeo /moogaloop.swf?clip_id=NNNNN
} elsif ($url =~ m@^https?://(?:[a-z]+\.)?(vimeo)\.com/.*clip_id=(\d+)@s) {
($site, $id) = ($1, $2);
$url = "https://$site.com/$id";
# Tumblr /video/UUU/NNNNN
} elsif ($url =~
m@^https?://[-_a-z\d]+\.(tumblr)\.com/video/([^/]+)/(\d{8,})/@si) {
my $user;
($site, $user, $id) = ($1, $2, $3);
$site = lc($site);
$url = "https://$user.$site.com/post/$id";
# Tumblr /post/NNNNN
} elsif ($url =~ m@^https?://([-_a-z\d]+)\.(tumblr)\.com
/.*?/(\d{8,})(/|$)@six) {
my $user;
($user, $site, $id) = ($1, $2, $3);
$site = lc($site);
$url = "https://$user.$site.com/post/$id";
# Instagram /p/NNNNN
} elsif ($url =~ m@^https?://([-_a-z\d]+\.)?(instagram)\.com/p/([^/?&]+)@si) {
(undef, $site, $id) = ($1, $2, $3);
$site = lc($site);
$url = "https://www.$site.com/p/$id";
# Twitter /USER/status/NNNNN
} elsif ($url =~ m@^https?://([-_a-z\d]+\.)?(twitter)\.com/([^/?&]+)
/status/([^/?&]+)@six) {
my $user;
(undef, $site, $user, $id) = ($1, $2, $3, $4);
$site = lc($site);
$url = "https://$site.com/$user/status/$id";
} else {
error ("unparsable URL: $url");
}
error ("bogus URL: $url") if ($id =~ m@[/:?]@s);
return ($url, $id, $site);
}
# Having downloaded a video file and an audio file, combine them and delete
# the two originals.
#
sub mux_downloaded_files($$$$$$$) {
my ($id, $url, $title, $v1, $v2, $muxed_file, $progress_p) = @_;
my $video_file = $v1->{file};
my $audio_file = $v2->{file};
if (! defined($muxed_file)) {
$muxed_file = $video_file;
$muxed_file =~ s@\.(audio-only|video-only)\.@.@gs;
$muxed_file =~ s@ [^\s\[\]]+(\].)@$1@gs;
}
error ("$id: mismunged filename $muxed_file")
if ($muxed_file eq $audio_file || $muxed_file eq $video_file);
error ("$id: exists: $muxed_file (1)") if (-f $muxed_file);
error ("$video_file does not exist") unless (-f $video_file);
error ("$audio_file does not exist") unless (-f $audio_file);
my @cmd = ('ffmpeg',
# "-hide_banner", # not present in 0.6.5
# "-loglevel", "panic",
'-i', $video_file,
'-i', $audio_file,
'-map', '0:v:0', # from file 0, video track 0
'-map', '1:a:0', # from file 1, audio track 0
'-shortest'); # they should be the same length already
my $desc = 'merging';
my $expect_same_size_p = 1;
if ($webm_transcode_p &&
($v1->{content_type} =~ m/webm$/si ||
$v2->{content_type} =~ m/webm$/si)) {
# We are transcoding from WebM/Vorbis to MP4/AAC. It's slow.
$desc = 'transcoding';
$muxed_file =~ s@\.[^./]+$@.mp4@gsi;
$expect_same_size_p = 0;
push @cmd, ('-c:v', 'libx264', # video codec
'-profile:v', 'high',
'-crf', '22', # h.264 quality (18 is high,
'-pix_fmt', 'yuv420p', # 22 seems to match WebM)
'-acodec', 'aac',
'-ab', '128k', # audio bitrate
'-movflags', 'faststart'); # Move index to front
} else {
push @cmd, ('-vcodec', 'copy', # no re-encoding
'-acodec', 'copy');
}
# If there's no extension on the "--out" file, default to MP4.
push @cmd, ('-f', 'mp4') unless ($muxed_file =~ m@\.[^/]+$@s);
push @cmd, $muxed_file;
$rm_f{$muxed_file} = 1;
if ($verbose == 1) {
print STDERR "$progname: $desc audio and video...\n";
} elsif ($verbose > 1) {
print STDERR "$progname: $id: exec: $desc: '" . join("' '", @cmd) . "'\n";
}
{
my $result = '';
my ($in, $out, $err);
$err = Symbol::gensym;
my $pid = eval { open3 ($in, $out, $err, @cmd) };
if (!$pid) {
$err = "exec: $cmd[0]: $!";
} else {
close ($in);
close ($out);
my ($dur, $input_bytes);
my $start_time = time();
if ($progress_p) {
my $s1 = (stat($audio_file))[7] || 0;
my $s2 = (stat($video_file))[7] || 0;
$input_bytes = $s1 + $s2;
}
# The stderr output from ffmpeg sometimes uses \n and sometimes \r
# so we have to use sysread here and split manually instead of
# while (<$err>) or the whole stderr buffers.
#
my $bufsiz = 16384;
while (1) {
my ($rin, $win, $ein, $rout, $wout, $eout);
$rin = $win = $ein = '';
vec ($rin, fileno($err), 1) = 1;
$ein = $rin | $win;
my $nfound = select ($rout = $rin, $wout = $win, $eout = $ein, undef);
my $chunk = '';
my $size = sysread ($err, $chunk, $bufsiz);
last if ($nfound && !$size); # closed
$result .= $chunk;
if ($progress_p || $verbose > 2) {
# Let's just assume ffmpeg never splits writes mid-line.
# (Actually "rarely" is as good as "never")
$chunk =~ s/\r\n?/\n/gs;
foreach my $line (split(/\n/, $chunk)) {
print STDERR " <== $line\n" if ($verbose > 2);
# ffmpeg doesn't provide the total number of frames anywhere,
# so we have to go by timestamp instead:
#
# Input #0, ...
# Duration: 00:03:26.99, start: 0.000000, bitrate: 3005 kb/s
# ...
# frame= 3378 fps= 88 q=30.0 size= 7680kB time=00:00:55.21 ...
#
if (!$dur &&
$line =~ m/^\s*Duration:\s+(\d+):(\d\d):(\d\d(\.\d+)?)\b/s) {
$dur = $1*60*60 + $2*60 + $3;
} elsif ($dur && $progress_p &&
$line =~ m/^\s* frame= .* \b
time= \s* (\d+):(\d\d):(\d\d(\.\d+)?)/sx) {
my $cur = $1*60*60 + $2*60 + $3;
my $elapsed = time() - $start_time;
my $bps = $elapsed ? ($input_bytes * 8 / $elapsed) : 0;
draw_progress ($cur / $dur, $bps, 0);
}
}
}
}
draw_progress (1, 0, 1) if ($dur && $progress_p);
# The stderr from the subprocess has hit EOF, so the pid should be
# dead momentarily.
waitpid ($pid, 0);
my $exit_value = $? >> 8;
my $signal_num = $? & 127;
my $dumped_core = $? & 128;
$err = undef;
$err = "$id: $cmd[0]: core dumped!" if ($dumped_core);
$err = "$id: $cmd[0]: signal $signal_num!" if ($signal_num);
$err = "$id: $cmd[0]: exited with $exit_value!" if ($exit_value);
}
if ($err) {
if (-f $muxed_file) {
print STDERR "$progname: rm \"$muxed_file\"\n" if ($verbose > 1);
unlink ($muxed_file); # It's not a download, and it's broken.
}
my @L = split(/(?:\r?\n)+/, $result);
$result = join ("\n", @L[-5 .. -1]) # only last 5 lines
if (@L > 5);
if ($result) {
$result =~ s/^/$cmd[0]: /gm;
$err .= "\n\n$result\n";
}
error ($err);
}
}
my $s1 = (stat($audio_file))[7] || 0;
my $s2 = (stat($video_file))[7] || 0;
my $s3 = (stat($muxed_file))[7] || 0;
$s1 = $s1 + $s2;
my $diff = $s1 * 0.05; # 5% of audio+video seems safe & sane
if ($s3 > 8*1024*1024 && # File is non-tiny
($expect_same_size_p &&
(($s3 < ($s1 - $diff)) || # muxed is less than audio+video - N%
($s3 > ($s1 + $diff))))) { # muxed is more than audio+video + N%
my $s1b = fmt_size ($s1);
my $s3b = fmt_size ($s3);
print STDERR "$progname: WARNING: " .
"$id: $cmd[0] wrote a short file! Got $s3b, expected $s1b" .
" ($s1 - $s3 = $diff)\n";
}
if ($verbose < 3) {
foreach my $f ($audio_file, $video_file) {
if (-f $f) {
print STDERR "$progname: rm \"$f\"\n" if ($verbose > 1);
unlink $f;
}
}
}
delete $rm_f{$muxed_file}; # Succeeded, keep file.
write_file_metadata_url ($muxed_file, $id, $url);
if ($verbose > 0) {
my ($w, $h, $size, $abr) = video_file_size ($muxed_file);
$size = -1 unless $size;
my $ss = fmt_size ($size);
$ss .= ", $w x $h" if ($w && $h);
print STDERR "$progname: wrote \"$muxed_file\"\n";
print STDERR "$progname: $ss\n";
}
}
sub content_type_ext($;) {
my ($ct) = @_;
if ($ct =~ m@/(x-)?flv$@si) { return 'flv'; }
elsif ($ct =~ m@/(x-)?webm$@si) { return 'webm'; }
elsif ($ct =~ m@/(x-)?3gpp$@si) { return '3gpp'; }
elsif ($ct =~ m@/quicktime$@si) { return 'mov'; }
elsif ($ct =~ m@^audio/mp4$@si) { return 'm4a'; }
else { return 'mp4'; }
}
sub load_formats($$) {
my ($url, $size_p) = @_;
my ($url2, $id, $site) = canonical_url ($url);
return ($site eq 'youtube' ? load_youtube_formats ($id, $url, $size_p):
$site eq 'vimeo' ? load_vimeo_formats ($id, $url, $size_p) :
$site eq 'tumblr' ? load_tumblr_formats ($id, $url, $size_p) :
$site eq 'instagram' ? load_instagram_formats ($id, $url, $size_p) :
$site eq 'twitter' ? load_twitter_formats ($id, $url, $size_p) :
error ("$id: unknown site: $site"));
}
sub download_video_url($$$$$$$$$$$);
sub download_video_url($$$$$$$$$$$) {
my ($url, $title, $prefix, $outfile, $size_p,
$list_p, $list_idx, $list_count,
$bwlimit, $progress_p, $force_fmt) = @_;
$error_whiteboard = ''; # reset per-URL diagnostics
$progress_ticks = 0; # reset progress-bar counters
$progress_time = 0;
# Pack multi-byte UTF-8 back into wide chars.
utf8::decode ($title) if defined($title);
utf8::decode ($prefix) if defined($prefix);
foreach ($title, $prefix) {
s@\s*[/:]+\s*@ - @gs if $_; # no colons or slashes
s/^\s+|\s+$//gs if $_;
}
my ($id, $site);
($url, $id, $site) = canonical_url ($url);
# If downloading a playlist, recurse.
#
if ($url =~ m@view_play_list@s) {
error ("--out does not work with playlists") if ($outfile);
return download_youtube_playlist ($id, $url, $title, $prefix, $size_p,
$list_p, $bwlimit, $progress_p,
$force_fmt);
}
# Fuck you, Twitter. Handle links to Youtube inside twits.
# If there is both a Youtube link and Twitter-hosted video,
# we ignore the latter.
#
if ($site eq 'twitter') {
my ($http, $head, $body) = get_url ($url);
check_http_status ($id, $url, $http, 1);
if ($body =~ m@\b ( https?://( youtu\.be | [^a-z/]+\.youtube\.com )
/ [^\s\"\'<>]+ ) @six) {
($url, $id, $site) = canonical_url ($1);
}
}
# Handle --list for playlists.
#
if ($list_p) {
if ($list_p > 1) {
my $t2 = ($prefix ? "$prefix $title" : $title);
print STDOUT "$id\t$t2\n";
} else {
print STDOUT "https://www.$site.com/watch?v=$id\n";
}
return;
}
# Though Tumblr and Twitter can host their own videos, much of the time
# there is just an embedded Youtube video instead.
#
if ($site eq 'tumblr' || $site eq 'twitter') {
my ($http, $head, $body) = get_url ($url);
check_http_status ($id, $url, $http, 1);
if ($body =~ m@ \b ( https?:// (?: [a-z]+\. )?
youtube\.com/
[^\"\'<>]*? (?: embed | \?v= )
[^\"\'<>]+ )@six) {
($url, $id, $site) = canonical_url (html_unquote ($1));
}
}
my $suf = (" [" . $id .
($force_fmt && $force_fmt ne 'mux' ? " $force_fmt" : "") .
"]");
if (! ($size_p || $list_p)) {
# If we're writing with --suffix, we can check for an existing
# file before knowing the title of the video. Check for a file
# with "[this-ID]" in it. (The quoting rules of perl's "glob"
# function are ridiculous and confusing, so let's do it the hard
# way instead.)
#
opendir (my $dir, '.') || error ("readdir: $!");
foreach my $f (readdir ($dir)) {
if ($f =~ m/\Q$suf\E/s) {
exit (1) if ($verbose <= 0); # Skip silently if --quiet.
error ("$id: exists: $f (2)");
}
}
closedir $dir;
if (defined($outfile)) {
error ("$id: exists: $outfile (3)") if (-f $outfile);
} elsif (defined($title)) {
# If we already have a --title, we can check for the existence of the
# file before hitting the network. Otherwise, we need to download the
# video info to find out the title and thus the file name.
#
my $t2 = ($prefix ? "$prefix $title" : $title);
my $o = (file_exists_with_suffix ("$t2") ||
file_exists_with_suffix ("$t2$suf") ||
file_exists_with_suffix ("$title") ||
file_exists_with_suffix ("$title$suf"));
if ($o) {
exit (1) if ($verbose <= 0); # Skip silently if --quiet.
error ("$id: exists: $o (4)");
}
}
}
# Videos can come in multiple resolutions, and sometimes with audio and
# video in separate URLs. Get the list of all possible downloadable video
# formats.
#
my $fmts = load_formats ($url, $size_p);
# Set the title unless it was specified on the command line with --title.
#
if (!defined($title) && defined($fmts)) {
$title = munge_title ($fmts->{title});
#sanity_check_title ($title, $url, '[fmts]', 'download_video_url');
# Add the year to the title unless there's a year there already.
#
if ($title !~ m@ \(\d{4}\)@si) { # skip if already contains " (NNNN)"
my $year = ($fmts->{year} ? $fmts->{year} :
$site eq 'youtube' ? get_youtube_year ($id) :
$site eq 'vimeo' ? get_vimeo_year ($id) : undef);
if ($year &&
$year != (localtime())[5]+1900 && # Omit this year
$title !~ m@\b$year\b@s) { # Already in the title
$title .= " ($year)";
}
}
# Now that we've hit the network and determined the real title, we can
# check for existing files on disk.
#
if (!defined($outfile) &&
(! ($size_p || $list_p))) {
my $t2 = ($prefix ? "$prefix $title" : $title);
my $o = (file_exists_with_suffix ("$t2") ||
file_exists_with_suffix ("$title") ||
file_exists_with_suffix ("$title") ||
file_exists_with_suffix ("$title$suf"));
if ($o) {
exit (1) if ($verbose <= 0); # Skip silently if --quiet.
error ("$id: exists: $o (5)");
}
}
}
# Now that we have the video info, decide what to download.
# If we're doing --fmt all, this is all of them.
# Otherwise, it's either one URL or two (audio + video mux).
#
my @targets = pick_download_format ($id, $site, $url, $force_fmt, $fmts)
if (defined ($fmts));
my @pair = (@targets == 2 && $force_fmt ne 'all' ? @targets : ());
if ($size_p && @pair) {
# With --size, we only need to examine the first pair of the mux.
@targets = ($pair[0]) if ($pair[0]);
@pair = ();
}
$append_suffix_p = 1
if (!$size_p && defined($force_fmt) && $force_fmt eq 'all');
my @outfiles = ();
if (defined($outfile) && @pair) {
foreach (@pair) {
my $f = sprintf("%s-%08x", $outfile, rand(0xFFFFFFFF));
push @outfiles, $f;
}
}
foreach my $target (@targets) {
my $fmt = $fmts->{$target};
my $ct = $fmt->{content_type};
my $w = $fmt->{width};
my $h = $fmt->{height};
my $abr = $fmt->{abr};
my $size = $fmt->{size};
my $url2 = $fmt->{url};
my $dashp = $fmt->{dashp};
$error_whiteboard .= "fmt $target: $url2\n\n";
if ($size_p) {
if (! (($w && $h) || $abr)) {
# On a non-playlist, --size --size means guess the size from the
# format rather than downloading the first part of the video to
# get the exact resolution.
# #### Actually we can't guess that because we don't have access
# to $known_formats here. Oh well.
#
if ($size_p == 1) {
my ($w2, $h2, $s2, $a2) =
video_url_size ($id, $url2, $ct, $bwlimit,
(defined($force_fmt) && $force_fmt eq 'all'
? 1 : 0));
$w = $w2 if $w2;
$h = $h2 if $h2;
$size = $s2 if $s2;
$abr = $a2 if $a2;
}
}
my $ii = $id . (@targets == 1 ? '' : ":$target");
my $ss = fmt_size ($size);
my $wh = ($w && $h
? "${w} x ${h}"
: ($abr ? "$abr " : ' ?x?'));
my $t2 = ($prefix ? "$prefix $title" : $title);
print STDOUT "$ii\t$wh\t$ss\t$t2\n";
} else {
$suf = ($append_suffix_p
? (" [" . $id .
((@targets == 1 &&
!(defined($force_fmt) && $force_fmt eq 'all'))
? '' : " $target") .
"]")
: (@pair
? ($target == $pair[0] ? '.video-only' : '.audio-only')
: ''));
my $file = ($prefix ? "$prefix $title" : $title) . $suf;
$ct =~ s/;.*$//s;
$file .= '.' . content_type_ext($ct);
my $ftitle = $file;
$file = (@pair
? ($target == $pair[0] ? $outfiles[0] : $outfiles[1])
: $outfile)
if (defined($outfile));
$fmt->{file} = $file;
if (-f $file) {
if (($force_fmt || '') eq 'mux') {
# Allow the temporary files used in muxing to be overwritten.
} else {
exit (1) if ($verbose <= 0); # Skip silently if --quiet.
error ("$id: exists: $file (6)")
unless (($force_fmt || '') eq 'all');
# Nobody uses --fmt all except for debugging; allow partial files.
print STDERR "$progname: $id: exists: $file (6)\n";
next;
}
}
print STDERR "$progname: reading \"$ftitle\"\n" if ($verbose > 0);
my $start_time = time();
if (ref($url2) eq 'ARRAY') {
my $start = time();
my $total = scalar (@$url2);
my $bytes = 0;
my $bps = 0;
my $i = 0;
foreach my $url3 (@$url2) {
my $append_p = ($i > 0);
print STDERR "\n" if ($i && $progress_p && $verbose > 2);
my ($http, $head, $body);
print STDERR "$progname: downloading segment $i/$total\n"
if ($verbose > 2);
($http, $head, $body) = get_url ($url3, undef, $file,
$bwlimit, undef,
$append_p, 0);
# internal error if still 403 after retries.
check_http_status ($id,
"$url segment $i/$total: $url3",
$http, 2);
# When loading segmented URLs we only update the progress marker
# when each segment is fully downloaded... but they're small,
# that's kind of their whole point.
#
if ($progress_p) {
my ($size) = ($head =~
m@^Content-Range: \s* bytes \s+ [-\d]+ / (\d+) @mix);
($size) = ($head =~ m@^Content-Length: \s* (\d+) @mix)
unless $size;
$bytes += $size if defined($size); # Sometimes missing!
my $elapsed = time() - $start;
$bps = 8 * ($elapsed ? $bytes / $elapsed : 0);
draw_progress ($i / $total, $bps, 0);
}
$i++;
}
draw_progress (1, $bps, 1) if ($progress_p);
} else {
my $force_ranges_p = 1;
my ($http, $head, $body) = get_url ($url2, undef, $file,
$bwlimit, undef, 0, $progress_p,
$force_ranges_p);
# internal error if still 403
check_http_status ($id, $url2, $http, 2);
}
my $download_time = time() - $start_time;
if (! -s $file) {
print STDERR "$progname: rm \"$file\"\n" if ($verbose > 1 && -f $file);
unlink ($file);
error ("$file: failed: $url");
}
# The metadata tags seem to confuse ffmpeg.
write_file_metadata_url ($file, $id, $url) if (!@pair);
if ($verbose > 0) {
# Now that we've written the file, get the real numbers from it,
# in case the server metadata lied to us.
my $abr = 0;
($w, $h, $size, $abr) = video_file_size ($file);
$size = -1 unless $size;
my $ss = fmt_size ($size);
if ($w && $h) {
$ss .= ", $w x $h";
} elsif ($abr) {
$ss .= ", $abr";
}
if ($download_time && $size > 0) {
# Let's see how badly youtube is rate-limiting our downloads.
my $t = sprintf("%d:%02d:%02d",
int($download_time/(60*60)),
int($download_time/(60))%60,
int($download_time)%60);
$ss .= " downloaded in $t";
my $bps = fmt_bps ($size * 8 / $download_time);
$ss .= ", $bps";
}
print STDERR "$progname: wrote \"$file\"\n";
print STDERR "$progname: $ss\n";
}
# #### I'm trying to work out how to identify the short placeholder videos
# #### used when a video's "premier time" has not yet been reached.
# #### The placeholder video is a countdown that is sometimes 36MB,
# #### sometimes 501KB ??
# ####
# if ($verbose == 0 && $file && $file =~ m/\.mp4$/si) {
# my $size = (stat($file))[7] || 0;
# if (($size < (40*1024*1024)) ||
# $title =~ m/poppy/si) {
# print STDERR "$progname: ##### DEBUG: weirdly small file: " .
# int($size/(1024*1024)) . "M;\n" .
# "$url\n\"$title\"\n\n";
# system ("youtubedown", "--size", "-vvvv", $url);
# }
# }
# If we're not muxing, this is the final file.
delete $rm_f{$file} unless @pair;
}
}
if (@pair) {
mux_downloaded_files ($id, $url, $title,
$fmts->{$pair[0]},
$fmts->{$pair[1]},
$outfile,
$progress_p);
} elsif ($size_p && !@targets) {
print STDERR "$id\tsize unknown (live stream?)\n";
}
}
# Sometimes we get 403 and "suspicious signature" and I don't know why.
# But retrying often works.
# Also sometimes the underlying video segments are 404.
#
sub download_video_url_retry($$$$$$$$$$$) {
my ($url, $title, $prefix, $outfile, $size_p,
$list_p, $list_idx, $list_count,
$bwlimit, $progress_p, $force_fmt) = @_;
($url) = canonical_url ($url);
my $retries = 10;
my $i = 0;
for ($i = 0; $i < $retries; $i++) {
my $ono = $noerror;
eval {
$noerror = 1;
download_video_url ($url, $title, $prefix, $outfile, $size_p,
$list_p, $list_idx, $list_count,
$bwlimit, $progress_p, $force_fmt);
};
$noerror = $ono;
last unless ($@); # Done if no error.
#last unless ($@ =~ m@\b403 Forbidden@); # Done if error but not 403.
last if ($@ =~ m/$blocked_re/sio); # These errors don't go away.
print STDERR "$progname: failed, retrying $url\n" if ($verbose == 2);
last if ($verbose > 2);
print STDERR "$progname: RETRYING $url\n\n$@\n\n" if ($verbose > 2);
$error_whiteboard .= "retrying $url\n";
rmf();
sleep (1);
}
if ($@) {
my $err = $@;
$err =~ s/\s+$//s;
$err .= " (after $i retries)" if ($i);
if ($err && $verbose <= 0 &&
($err =~ m/$blocked_re/sio ||
$err =~ m@\b403 Forbidden@s ||
$err =~ m@\b404 Not Found@s ||
$err =~ m@\bno video in @s ||
$err =~ m@\bI/O error:@s ||
$err =~ m@broken pipe@s)) {
# With --quiet, just silently ignore private videos and 404s
# for "youtubefeed".
exit (1);
}
error ($err);
}
}
# Returns the title and URLs of every video in the playlist.
#
sub youtube_playlist_urls($$;$) {
my ($id, $url, $first_only_p) = @_;
my @playlist = ();
my $start = 0;
my ($http, $head, $body) = get_url ($url);
check_http_status ($id, $url, $http, 1);
my ($title) = ($body =~ m@<title>\s*([^<>]+?)\s*</title>@si);
$title = munge_title($title);
sanity_check_title ($title, $url, $body, 'youtube_playlist_urls');
$title = 'Untitled Playlist' unless $title;
($body =~ s/^.*?<div \s+ id="pl-video-list"//six) ||
($body =~ s/^.*?<\/ytd-playlist-sidebar-renderer//six) ||
errorI ("unparsable playlist HTML: $url");
my ($more) = ($body =~ m@href=[\"\']([^\"\']*/browse_ajax[^\"\']+)@si);
if ($first_only_p) {
@playlist = ( $playlist[0] );
$more = undef;
}
my $i = 0;
$body =~ s@<A \b (.*?) > \s* ([^<>]*?) \s* </A> @{
my ($href, $t2) = ($1, $2);
(undef, $href) = ($href =~ m% \b href \s* = \s* ([\"\'])(.*?)\1%six);
if ($href && $t2) {
$href = html_unquote($href);
if ($href =~ m%[?&]v=([^?&]+)%si) {
push \@playlist, [ $t2, 'https://www.youtube.com/watch?v=' . $1 ];
}
}
"";
}@gsexi;
errorI ("$id: no playlist entries?") unless @playlist;
# Scraping the HTML only gives us the first hundred videos if the
# playlist has more than that. To get the rest requires more work.
#
my $page = 2;
while ($more) {
$more = html_unquote ($more);
$more = 'https:' if ($more =~ m@^//@s);
$more = 'https://www.youtube.com' . $more if ($more =~ m@^/@s);
print STDERR "$progname: loading playlist page $page...\n"
if ($verbose > 1);
($http, $head, $body) = get_url ($more);
check_http_status ($id, $more, $http, 1);
$body =~ s/ \\[ux] ([a-z0-9]{4}) / chr(hex($1)) /gsexi; # \uXXXX
$body =~ s@\\@@gs;
foreach my $tag ($body =~ m@<[^<>]+>@gsi) {
my (undef, $t2) = ($tag =~ m@data-title=([\"\"])(.*?)\1@si);
my (undef, $id) = ($tag =~ m@data-video-id=([\"\"])(.*?)\1@si);
push @playlist, [ $t2, 'https://www.youtube.com/watch?v=' . $id ]
if ($id);
}
($more) = ($body =~ m@href=[\"\']([^\"\']*/browse_ajax[^\"\']+)@si);
$page++;
}
# Prefix each video's title with the playlist's title and its index.
#
$i = 0;
my $count = @playlist;
foreach my $P (@playlist) {
$i++;
my $t2 = $P->[0];
$t2 = munge_title (html_unquote ($t2));
sanity_check_title ($t2, $url, $body, 'youtube_playlist_urls 2');
my $ii = ($count > 999 ? sprintf("%04d", $i) :
$count > 99 ? sprintf("%03d", $i) :
sprintf("%02d", $i));
$t2 = "$title: $ii: $t2";
$P->[0] = $t2;
}
return ($title, @playlist);
}
sub download_youtube_playlist($$$$$$$$$) {
my ($id, $url, $title, $prefix, $size_p, $list_p,
$bwlimit, $progress_p, $force_fmt) = @_;
# With "--size", only get the size of the first video.
# With "--size --size", get them all.
my ($title2, @playlist) = youtube_playlist_urls($id, $url, ($size_p == 1));
$title = $title2 unless $title;
print STDERR "$progname: playlist \"$title\" (" . scalar (@playlist) .
" entries)\n"
if ($verbose > 1);
my $list_count = scalar @playlist;
my $list_idx = 0;
foreach my $P (@playlist) {
my ($t2, $u2) = @$P;
my $ono = $noerror;
eval {
$noerror = 1;
utf8::encode ($t2) if defined($t2);
download_video_url_retry ($u2, $t2, $prefix, undef, $size_p, $list_p,
$list_idx, $list_count,
$bwlimit, $progress_p, $force_fmt);
$noerror = $ono;
};
print STDERR "$progname: $@" if $@;
last if ($size_p == 1);
$list_idx++;
}
}
sub usage() {
print STDERR "usage: $progname" .
" [--verbose] [--quiet] [--progress] [--size]\n" .
"\t\t [--title txt] [--prefix txt] [--suffix] [--out file]\n" .
"\t\t [--fmt N] [--no-mux] [--bwlimit N [kb | KB | mb | MB]]\n" .
"\t\t [--webm] [--webm-transcode]\n" .
"\t\t youtube-or-vimeo-urls ...\n";
exit 1;
}
sub main() {
binmode (STDOUT, ':utf8'); # video titles in messages
binmode (STDERR, ':utf8');
# historical suckage: the environment variable name is lower case.
$http_proxy = ($ENV{http_proxy} || $ENV{HTTP_PROXY} ||
$ENV{https_proxy} || $ENV{HTTPS_PROXY});
delete $ENV{http_proxy};
delete $ENV{HTTP_PROXY};
delete $ENV{https_proxy};
delete $ENV{HTTPS_PROXY};
if ($http_proxy && $http_proxy !~ m/^http/si) {
# historical suckage: allow "host:port" as well as "http://host:port".
$http_proxy = "http://$http_proxy";
}
my @urls = ();
my $title = undef;
my $prefix = undef;
my $out = undef;
my $size_p = 0;
my $list_p = 0;
my $progress_p = 0;
my $fmt = undef;
my $expect = undef;
my $guessp = 0;
my $muxp = 1;
my $bwlimit = undef;
while ($#ARGV >= 0) {
$_ = shift @ARGV;
if (m/^--?verbose$/) { $verbose++; }
elsif (m/^-v+$/) { $verbose += length($_)-1; }
elsif (m/^--?q(uiet)?$/) { $verbose--; }
elsif (m/^--?progress$/) { $progress_p++; }
elsif (m/^--?no-progress$/) { $progress_p = 0; }
elsif (m/^--?suffix$/) { $append_suffix_p = 1; }
elsif (m/^--?no-suffix$/) { $append_suffix_p = 0; }
elsif (m/^--?prefix$/) { $expect = $_; $prefix = shift @ARGV; }
elsif (m/^--?title$/) { $expect = $_; $title = shift @ARGV; }
elsif (m/^--?out$/) { $expect = $_; $out = shift @ARGV; }
elsif (m/^--?size$/) { $expect = $_; $size_p++; }
elsif (m/^--?list$/) { $expect = $_; $list_p++; }
elsif (m/^--?fmt$/) { $expect = $_; $fmt = shift @ARGV; }
elsif (m/^--?mux$/) { $expect = $_; $muxp = 1; }
elsif (m/^--?no-?mux$/) { $expect = $_; $muxp = 0; }
elsif (m/^--?webm$/) { $expect = $_; $webm_p = 1; }
elsif (m/^--?no-?webm$/) { $expect = $_; $webm_p = 0; }
elsif (m/^--?webm-trans(code)?$/) { $webm_p = 1;
$webm_transcode_p = 1; }
elsif (m/^--?no-?webm-trans(code)?$/) { $webm_transcode_p = 0; }
elsif (m/^--?guess$/) { $guessp++; }
elsif (m/^--?bwlimit$/) {
#
# Many variant spellings are allowed:
#
# bits: k, kb, kbps, kps, kb/s, k/s;
# bytes: K, Kb, Kbps, Kps, Kb/s, K/s,
# KB, KBps, KBPS, KPS, KB/s, KB/S, K/S.
#
my $bit_suf = '(b|bps|ps|b/s|/s)?$';
my $byte_suf = '(b|bps|ps|b/s|/s|B|Bps|BPS|PS|B/s|B/S|/S)?$';
$bwlimit = shift @ARGV;
if ($bwlimit =~ s@ \s* k $bit_suf @@sx) { # k bits
$bwlimit *= 1024;
} elsif ($bwlimit =~ s@ \s* K $byte_suf @@sx) { # K bytes
$bwlimit *= 1024 * 8;
} elsif ($bwlimit =~ s@ \s* m $bit_suf @@sx) { # m bits
$bwlimit *= 1024 * 1024;
} elsif ($bwlimit =~ s@ \s* M $byte_suf @@sx) { # M bytes
$bwlimit *= 1024 * 1024 * 8;
} elsif ($bwlimit =~ s@ \s* g $bit_suf @@sx) { # g bits
$bwlimit *= 1024 * 1024 * 1024;
} elsif ($bwlimit =~ s@ \s* G $byte_suf @@sx) { # G bytes
$bwlimit *= 1024 * 1024 * 1024 * 8;
} elsif ($bwlimit =~ s@ \s* $bit_suf @@sx) { # bits
$bwlimit += 0;
} elsif ($bwlimit =~ s@ \s* $byte_suf @@sx) { # Bytes
$bwlimit /= 8;
} elsif ($bwlimit =~ m@^ \d+ ( \.\d+ )? $ @sx) { # no units: k bits
$bwlimit *= 1024;
} else {
error ("unparsable units: $bwlimit");
}
} elsif (m/^-./) { usage; }
else {
s@^//@https://@s;
error ("not a Youtube, Vimeo, Instagram, Tumblr," .
" or Twitter URL: $_")
unless (m@^(https?://)?
([a-z]+\.)?
( youtube(-nocookie)?\.com/ |
youtu\.be/ |
vimeo\.com/ |
google\.com/ .* service=youtube |
youtube\.googleapis\.com
tumblr\.com/ |
instagram\.com/ |
twitter\.com/ |
)@six);
$fmt = 'mux' if ($muxp && !defined($fmt));
usage if (defined($fmt) && $fmt !~ m/^\d+|all|mux$/s);
my @P = ($title, $fmt, $out, $_);
push @urls, \@P;
$title = undef;
$out = undef;
$expect = undef;
}
}
error ("$expect applies to the following URLs, so it must come first")
if ($expect);
if ($guessp) {
guess_cipher (undef, $guessp - 1);
exit (0);
}
usage unless ($#urls >= 0);
foreach (@urls) {
my ($title, $fmt, $out, $url) = @$_;
download_video_url_retry ($url, $title, $prefix, $out, $size_p,
$list_p, 0, 0,
$bwlimit, $progress_p, $fmt);
}
}
main();
exit 0;