Skip to content

Instantly share code, notes, and snippets.

@yamaimo
Last active August 29, 2015 14:20
Show Gist options
  • Save yamaimo/0edc93ca45a29d082f20 to your computer and use it in GitHub Desktop.
Save yamaimo/0edc93ca45a29d082f20 to your computer and use it in GitHub Desktop.
NicovideoDownloader with Ruby
#!/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