Last active
August 29, 2015 14:20
-
-
Save yamaimo/0edc93ca45a29d082f20 to your computer and use it in GitHub Desktop.
NicovideoDownloader with Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
#---- | |
# ruby-nicovideo-dl.rb | |
#---- | |
# [Set up] | |
# 1. Install Netrc gem | |
# Use gem command. | |
# | |
# $ gem install netrc | |
# | |
# 2. Put the '.netrc' file | |
# Put the following file '.netrc' in your home directory. | |
# | |
# ==> ${HOME}/.netrc <== | |
# machine nicovideo | |
# login (e-mail address for nicovideo) | |
# password (password for nicovideo) | |
# | |
# 3. Install RTMPDump | |
# Install RTMPDump from "https://rtmpdump.mplayerhq.hu/". | |
# | |
# [Usage] | |
# $ ./ruby-nicovideo-dl.rb (video URL(s) of nicovideo) | |
#---- | |
require 'netrc' | |
require 'net/http' | |
require 'net/https' | |
require 'cgi' | |
module NicovideoDL | |
Version = "0.1.0" | |
def exit_with_error(message) | |
$stderr.puts "Error: #{message}" | |
exit(1) | |
end | |
module_function :exit_with_error | |
class DownloadStatusPrinter | |
Size1K = 1024 | |
Epsilon = 0.0001 | |
def initialize(total_size=0) | |
@start_time = Time.now | |
@total_size = total_size | |
end | |
def print(current_size) | |
if @total_size != 0 | |
percent = (100.0 * current_size) / @total_size | |
percent_str = sprintf("%.1f", percent) | |
eta_str = get_eta_str(current_size) | |
else | |
percent_str = "---.-" | |
eta_str = "--:--" | |
end | |
speed_str = get_speed_str(current_size) | |
current_size_str = format_bytes(current_size) | |
total_size_str = format_bytes(@total_size) | |
$stdout.printf("\rRetrieving video data: %5s%% (%8s of %s) at %12s ETA %s ", | |
percent_str, current_size_str, total_size_str, speed_str, eta_str) | |
$stdout.flush | |
end | |
private | |
def get_eta_str(current_size) | |
speed = calculate_speed(current_size) | |
if speed | |
rest_size = @total_size - current_size | |
eta = (rest_size / speed).to_i | |
eta_min, eta_sec = eta.divmod(60) | |
if eta_min > 99 | |
"--:--" | |
else | |
sprintf "%02d:%02d", eta_min, eta_sec | |
end | |
else | |
"--:--" | |
end | |
end | |
def get_speed_str(current_size) | |
speed = calculate_speed(current_size) | |
if speed | |
"#{format_bytes(speed)}/sec" | |
else | |
"N/A b/sec" | |
end | |
end | |
def calculate_speed(current_size) | |
elapsed = Time.now - @start_time | |
if (current_size == 0) || (elapsed < Epsilon) | |
nil | |
else | |
current_size.to_f / elapsed | |
end | |
end | |
def format_bytes(bytes) | |
exp = (bytes > 0) ? Math.log(bytes, Size1K).to_i : 0 | |
suffix = "bKMGTPEZY"[exp] | |
if exp == 0 | |
"#{bytes}#{suffix}" | |
else | |
converted = bytes.to_f / (Size1K ** exp) | |
sprintf "%.2f%s", converted, suffix | |
end | |
end | |
end | |
class Downloader | |
LoginURI = URI.parse("https://secure.nicovideo.jp/secure/login?site=niconico") | |
LoginPostFormat = "current_form=login&mail=%s&password=%s&login_submit=Log+In" | |
VideoHost = "www.nicovideo.jp" | |
VideoPathFormat = "/watch/%s" | |
VideoURLRegexp = %r{^(?:(?:http://)?(?:\w+\.)?(?:nicovideo\.jp/(?:v/|(?:watch(?:\.php)?))?/)?(\w+))} | |
VideoInfoPathFormat = "/api/getflv?v=%s&as3=1" | |
VideoTypeRegexp = %r{^http://.*\.nicovideo\.jp/smile\?(.*?)=.*} | |
def initialize | |
login | |
end | |
def download(url) | |
puts "Download #{url}" | |
video_id = get_video_id(url) | |
video_cookies = get_video_cookies(video_id) | |
video_info = get_video_info(video_id, video_cookies) | |
video_extension = get_video_extension(video_info["uri"]) | |
output_filename = "#{video_id}#{video_extension}" | |
puts "- video URL : #{video_info['uri'].to_s}" | |
puts "- output file: #{output_filename}" | |
if video_info["uri"].scheme == "http" | |
download_with_http(video_info, video_cookies, output_filename) | |
elsif video_info["uri"].scheme == "rtmpe" | |
download_with_rtmpe(video_info, video_cookies, output_filename) | |
else | |
NicovideoDL.exit_with_error("Unsupported scheme. [scheme=#{video_info['uri'].scheme}]") | |
end | |
puts "done." | |
end | |
private | |
def login | |
unless @user_session | |
$stdout.print("Login...") | |
$stdout.flush | |
netrc_info = Netrc.read | |
user, password = netrc_info["nicovideo"] | |
if user.nil? || user.empty? || password.nil? || password.empty? | |
NicovideoDL.exit_with_error("Netrc is invalid.") | |
end | |
https = Net::HTTP.new(LoginURI.host, LoginURI.port) | |
https.use_ssl = true | |
https.verify_mode = OpenSSL::SSL::VERIFY_NONE | |
postdata = sprintf(LoginPostFormat, user, password) | |
response = https.post(LoginURI.request_uri, postdata) | |
response.get_fields('set-cookie').each do |cookie| | |
key, value = cookie.split(';').first.split('=') | |
if (key == 'user_session') && (value != 'deleted') | |
@user_session = value | |
break | |
end | |
end | |
if @user_session.nil? | |
NicovideoDL.exit_with_error("Failed to login.") | |
end | |
puts "done." | |
end | |
end | |
def get_video_id(url) | |
if match_data = VideoURLRegexp.match(url) | |
match_data[1] | |
else | |
NicovideoDL.exit_with_error("URL is invalid. [url=#{url}]") | |
end | |
end | |
def get_video_cookies(video_id) | |
video_cookies = Hash.new | |
video_cookies['user_session'] = @user_session | |
http = Net::HTTP.new(VideoHost) | |
video_path = sprintf(VideoPathFormat, video_id) | |
response = http.get(video_path, make_http_header(video_cookies)) | |
response.get_fields('set-cookie').each do |cookie| | |
key, value = cookie.split(';').first.split('=') | |
if key == 'nicohistory' | |
video_cookies[key] = value | |
break | |
end | |
end | |
video_cookies | |
end | |
def get_video_info(video_id, video_cookies) | |
video_path = sprintf(VideoPathFormat, video_id) | |
original_uri = URI::HTTP.build(host: VideoHost, path: video_path) | |
http = Net::HTTP.new(VideoHost) | |
video_info_path = sprintf(VideoInfoPathFormat, video_id) | |
response = http.get(video_info_path, make_http_header(video_cookies)) | |
while response.is_a?(Net::HTTPRedirection) | |
redirect_uri = URI.parse(response.get_fields('location').first) | |
http = Net::HTTP.new(redirect_uri.host) | |
response = http.get(redirect_uri.request_uri, make_http_header(video_cookies)) | |
end | |
begin | |
info = CGI.parse(response.body) | |
video_url = info['url'].first | |
video_uri = URI.parse(video_url) | |
if video_uri.scheme == "http" | |
{"uri" => video_uri} | |
else | |
fmst2, fmst1 = info['fmst'].first.split(':') | |
{"uri" => video_uri, "original_uri" => original_uri, | |
"fmst1" => fmst1, "fmst2" => fmst2} | |
end | |
rescue | |
NicovideoDL.exit_with_error("Failed to access video information.") | |
end | |
end | |
def get_video_extension(video_uri) | |
if match_data = VideoTypeRegexp.match(video_uri.to_s) | |
if match_data[1] == "s" | |
".swf" | |
elsif match_data[1] == "m" | |
".mp4" | |
else | |
".flv" | |
end | |
else | |
".flv" | |
end | |
end | |
def download_with_http(video_info, video_cookies, output_filename) | |
http = Net::HTTP.new(video_info["uri"].host) | |
http.request_get(video_info["uri"].request_uri, make_http_header(video_cookies)) do |response| | |
total_size = response.get_fields('Content-length').first.to_i rescue 0 | |
File.open(output_filename, "wb") do |file| | |
current_size = 0 | |
download_status_printer = DownloadStatusPrinter.new(total_size) | |
response.read_body do |video_block| | |
download_status_printer.print(current_size) | |
current_size += video_block.bytesize | |
file.write(video_block) | |
end | |
download_status_printer.print(current_size) | |
end | |
end | |
end | |
def download_with_rtmpe(video_info, video_cookies, output_filename) | |
original_uri = video_info["original_uri"] | |
uri = video_info["uri"] | |
uri_without_query = URI::Generic.build(scheme: uri.scheme, host: uri.host, path: uri.path) | |
playpath = uri.query.split('=')[1] | |
fmst1 = video_info["fmst1"] | |
fmst2 = video_info["fmst2"] | |
download_status_printer = DownloadStatusPrinter.new | |
done = false | |
until done | |
current_size = File.size(output_filename) rescue 0 | |
download_status_printer.print(current_size) | |
done = system <<END_OF_COMMAND | |
rtmpdump \\ | |
-q \\ | |
-e \\ | |
-r #{uri_without_query.to_s} \\ | |
-t #{uri_without_query.to_s} \\ | |
-p #{original_uri.to_s} \\ | |
-y #{playpath} \\ | |
-C S:#{fmst1} \\ | |
-C S:#{fmst2} \\ | |
-C S:#{playpath} \\ | |
-o #{output_filename} | |
END_OF_COMMAND | |
end | |
current_size = File.size(output_filename) | |
download_status_printer.print(current_size) | |
end | |
def make_http_header(cookies) | |
cookie_str = cookies.map do |key, value| | |
"#{key}=#{value};" | |
end.join(' ') | |
{'Cookie' => cookie_str} | |
end | |
end | |
end | |
if __FILE__ == $PROGRAM_NAME | |
if ARGV.size < 1 | |
raise "URL is not specified." | |
end | |
downloader = NicovideoDL::Downloader.new | |
ARGV.each do |url| | |
downloader.download(url) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment