Skip to content

Instantly share code, notes, and snippets.

@yamaimo
Created May 28, 2015 04:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yamaimo/5c3e46d97a92f716f0af to your computer and use it in GitHub Desktop.
Save yamaimo/5c3e46d97a92f716f0af to your computer and use it in GitHub Desktop.
NicovideoDownloader with Ruby (improved)
#!/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'
require 'ffi'
module NicovideoDL
Version = "0.2.0"
def exit_with_error(message)
$stderr.puts "Error: #{message}"
exit(1)
end
module_function :exit_with_error
# attach 'librtmp' interface
module LibRTMP
extend FFI::Library
ffi_lib 'librtmp'
### log.h ###
# typedef enum
# { RTMP_LOGCRIT=0, RTMP_LOGERROR, RTMP_LOGWARNING, RTMP_LOGINFO,
# RTMP_LOGDEBUG, RTMP_LOGDEBUG2, RTMP_LOGALL
# } RTMP_LogLevel;
# extern RTMP_LogLevel RTMP_debuglevel;
RTMP_LogLevel = enum [
:RTMP_LOGCRIT, 0, :RTMP_LOGERROR, :RTMP_LOGWARNING, :RTMP_LOGINFO,
:RTMP_LOGDEBUG, :RTMP_LOGDEBUG2, :RTMP_LOGALL
]
attach_variable :RTMP_debuglevel, RTMP_LogLevel
# set debug level to 'quiet'
LibRTMP.RTMP_debuglevel = RTMP_LogLevel[:RTMP_LOGCRIT]
### amf.h ###
# typedef enum
# { AMF_NUMBER = 0, AMF_BOOLEAN, AMF_STRING, AMF_OBJECT,
# AMF_MOVIECLIP, /* reserved, not used */
# AMF_NULL, AMF_UNDEFINED, AMF_REFERENCE, AMF_ECMA_ARRAY, AMF_OBJECT_END,
# AMF_STRICT_ARRAY, AMF_DATE, AMF_LONG_STRING, AMF_UNSUPPORTED,
# AMF_RECORDSET, /* reserved, not used */
# AMF_XML_DOC, AMF_TYPED_OBJECT,
# AMF_AVMPLUS, /* switch to AMF3 */
# AMF_INVALID = 0xff
# } AMFDataType;
AMFDataType = enum [
:AMF_NUMBER, 0, :AMF_BOOLEAN, :AMF_STRING, :AMF_OBJECT, :AMF_MOVIECLIP,
:AMF_NULL, :AMF_UNDEFINED, :AMF_REFERENCE, :AMF_ECMA_ARRAY, :AMF_OBJECT_END,
:AMF_STRICT_ARRAY, :AMF_DATE, :AMF_LONG_STRING, :AMF_UNSUPPORTED,
:AMF_RECORDSET, :AMF_XML_DOC, :AMF_TYPED_OBJECT, :AMF_AVMPLUS, :AMF_INVALID, 0xff
]
# typedef struct AVal
# {
# char *av_val;
# int av_len;
# } AVal;
class AVal < FFI::Struct
layout(
:av_val, :pointer,
:av_len, :int)
end
# struct AMFObjectProperty;
class AMFObjectProperty < FFI::Struct; end
# typedef struct AMFObject
# {
# int o_num;
# struct AMFObjectProperty *o_props;
# } AMFObject;
class AMFObject < FFI::Struct
layout(
:o_num, :int,
:o_props, AMFObjectProperty.ptr)
end
# typedef struct AMFObjectProperty
# {
# AVal p_name;
# AMFDataType p_type;
# union
# {
# double p_number;
# AVal p_aval;
# AMFObject p_object;
# } p_vu;
# int16_t p_UTCoffset;
# } AMFObjectProperty;
class AMFObjectProperty < FFI::Struct
class PropData < FFI::Union
layout(
:p_number, :double,
:p_aval, AVal,
:p_object, AMFObject)
end
layout(
:p_name, AVal,
:p_type, AMFDataType,
:p_vu, PropData,
:p_UTCoffset, :int16)
end
# unsigned int AMF_DecodeInt24(const char *data);
# unsigned int AMF_DecodeInt32(const char *data);
attach_function :AMF_DecodeInt24, [:pointer], :uint
attach_function :AMF_DecodeInt32, [:pointer], :uint
# int AMF_Decode(AMFObject * obj, const char *pBuffer, int nSize, int bDecodeName);
# void AMF_Dump(AMFObject * obj);
attach_function :AMF_Decode, [AMFObject.ptr, :pointer, :int, :int], :int
attach_function :AMF_Dump, [AMFObject.ptr], :void
# AMFObjectProperty *AMF_GetProp(AMFObject * obj, const AVal * name, int nIndex);
attach_function :AMF_GetProp, [AMFObject.ptr, AVal.ptr, :int], AMFObjectProperty.ptr
# double AMFProp_GetNumber(AMFObjectProperty * prop);
# void AMFProp_GetString(AMFObjectProperty * prop, AVal * str);
attach_function :AMFProp_GetNumber, [AMFObjectProperty.ptr], :double
attach_function :AMFProp_GetString, [AMFObjectProperty.ptr, AVal.ptr], :void
### rtmp.h ###
# #define RTMP_LIB_VERSION 0x020300 /* 2.3 */
RTMP_LIB_VERSION = 0x020300 # 2.3
# #define RTMP_FEATURE_HTTP 0x01
# #define RTMP_FEATURE_ENC 0x02
# #define RTMP_FEATURE_SSL 0x04
# #define RTMP_FEATURE_MFP 0x08 /* not yet supported */
# #define RTMP_FEATURE_WRITE 0x10 /* publish, not play */
# #define RTMP_FEATURE_HTTP2 0x20 /* server-side rtmpt */
RTMP_FEATURE_HTTP = 0x01
RTMP_FEATURE_ENC = 0x02
RTMP_FEATURE_SSL = 0x04
RTMP_FEATURE_MFP = 0x08
RTMP_FEATURE_WRITE = 0x10
RTMP_FEATURE_HTTP2 = 0x20
# #define RTMP_PROTOCOL_UNDEFINED -1
# #define RTMP_PROTOCOL_RTMP 0
# #define RTMP_PROTOCOL_RTMPE RTMP_FEATURE_ENC
# #define RTMP_PROTOCOL_RTMPT RTMP_FEATURE_HTTP
# #define RTMP_PROTOCOL_RTMPS RTMP_FEATURE_SSL
# #define RTMP_PROTOCOL_RTMPTE (RTMP_FEATURE_HTTP|RTMP_FEATURE_ENC)
# #define RTMP_PROTOCOL_RTMPTS (RTMP_FEATURE_HTTP|RTMP_FEATURE_SSL)
# #define RTMP_PROTOCOL_RTMFP RTMP_FEATURE_MFP
RTMP_PROTOCOL_UNDEFINED = -1
RTMP_PROTOCOL_RTMP = 0
RTMP_PROTOCOL_RTMPE = RTMP_FEATURE_ENC
RTMP_PROTOCOL_RTMPT = RTMP_FEATURE_HTTP
RTMP_PROTOCOL_RTMPS = RTMP_FEATURE_SSL
RTMP_PROTOCOL_RTMPTE = (RTMP_FEATURE_HTTP|RTMP_FEATURE_ENC)
RTMP_PROTOCOL_RTMPTS = (RTMP_FEATURE_HTTP|RTMP_FEATURE_SSL)
RTMP_PROTOCOL_RTMFP = RTMP_FEATURE_MFP
# #define RTMP_READ_HEADER 0x01
# #define RTMP_READ_RESUME 0x02
# #define RTMP_READ_NO_IGNORE 0x04
# #define RTMP_READ_GOTKF 0x08
# #define RTMP_READ_GOTFLVK 0x10
# #define RTMP_READ_SEEKING 0x20
RTMP_READ_HEADER = 0x01
RTMP_READ_RESUME = 0x02
RTMP_READ_NO_IGNORE = 0x04
RTMP_READ_GOTKF = 0x08
RTMP_READ_GOTFLVK = 0x10
RTMP_READ_SEEKING = 0x20
# #define RTMP_READ_COMPLETE -3
# #define RTMP_READ_ERROR -2
# #define RTMP_READ_EOF -1
# #define RTMP_READ_IGNORE 0
RTMP_READ_COMPLETE = -3
RTMP_READ_ERROR = -2
RTMP_READ_EOF = -1
RTMP_READ_IGNORE = 0
# typedef struct RTMP_READ
# {
# char *buf;
# char *bufpos;
# unsigned int buflen;
# uint32_t timestamp;
# uint8_t dataType;
# uint8_t flags;
# int8_t status;
# uint8_t initialFrameType;
# uint32_t nResumeTS;
# char *metaHeader;
# char *initialFrame;
# uint32_t nMetaHeaderSize;
# uint32_t nInitialFrameSize;
# uint32_t nIgnoredFrameCounter;
# uint32_t nIgnoredFlvFrameCounter;
# } RTMP_READ;
class RTMP_READ < FFI::Struct
layout(
:buf, :pointer,
:bufpos, :pointer,
:buflen, :uint,
:timestamp, :uint32,
:dataType, :uint8,
:flags, :uint8,
:status, :int8,
:initialFrameType, :uint8,
:nResumeTS, :uint32,
:metaHeader, :pointer,
:initialFrame, :pointer,
:nMetaHeaderSize, :uint32,
:nInitialFrameSize, :uint32,
:nIgnoredFrameCounter, :uint32,
:nIgnoredFlvFrameCounter, :uint32)
end
# typedef struct RTMPPacket
# {
# uint8_t m_headerType;
# uint8_t m_packetType;
# uint8_t m_hasAbsTimestamp; /* timestamp absolute or relative? */
# int m_nChannel;
# uint32_t m_nTimeStamp; /* timestamp */
# int32_t m_nInfoField2; /* last 4 bytes in a long header */
# uint32_t m_nBodySize;
# uint32_t m_nBytesRead;
# RTMPChunk *m_chunk;
# char *m_body;
# } RTMPPacket;
class RTMPPacket < FFI::Struct
layout(
:m_headerType, :uint8,
:m_packetType, :uint8,
:m_hasAbsTimestamp, :uint8,
:m_nChannel, :int,
:m_nTimeStamp, :uint32,
:m_nInfoField2, :int32,
:m_nBodySize, :uint32,
:m_nBytesRead, :uint32,
:m_chunk, :pointer,
:m_body, :pointer)
end
# #define RTMP_BUFFER_CACHE_SIZE (16*1024)
RTMP_BUFFER_CACHE_SIZE = (16 * 1024)
# typedef struct RTMPSockBuf
# {
# int sb_socket;
# int sb_size; /* number of unprocessed bytes in buffer */
# char *sb_start; /* pointer into sb_pBuffer of next byte to process */
# char sb_buf[RTMP_BUFFER_CACHE_SIZE]; /* data read from socket */
# int sb_timedout;
# void *sb_ssl;
# } RTMPSockBuf;
class RTMPSockBuf < FFI::Struct
layout(
:sb_socket, :int,
:sb_size, :int,
:sb_start, :pointer,
:sb_buf, [:char, RTMP_BUFFER_CACHE_SIZE],
:sb_timedout, :int,
:sb_ssl, :pointer)
end
# #define RTMP_LF_AUTH 0x0001 /* using auth param */
# #define RTMP_LF_LIVE 0x0002 /* stream is live */
# #define RTMP_LF_SWFV 0x0004 /* do SWF verification */
# #define RTMP_LF_PLST 0x0008 /* send playlist before play */
# #define RTMP_LF_BUFX 0x0010 /* toggle stream on BufferEmpty msg */
# #define RTMP_LF_FTCU 0x0020 /* free tcUrl on close */
# #define RTMP_LF_FAPU 0x0040 /* free app on close */
RTMP_LF_AUTH = 0x0001
RTMP_LF_LIVE = 0x0002
RTMP_LF_SWFV = 0x0004
RTMP_LF_PLST = 0x0008
RTMP_LF_BUFX = 0x0010
RTMP_LF_FTCU = 0x0020
RTMP_LF_FAPU = 0x0040
# #define RTMP_SWF_HASHLEN 32
RTMP_SWF_HASHLEN = 32
# typedef struct RTMP_LNK
# {
# AVal hostname;
# AVal sockshost;
# AVal playpath0; /* parsed from URL */
# AVal playpath; /* passed in explicitly */
# AVal tcUrl;
# AVal swfUrl;
# AVal pageUrl;
# AVal app;
# AVal auth;
# AVal flashVer;
# AVal subscribepath;
# AVal usherToken;
# AVal token;
# AVal pubUser;
# AVal pubPasswd;
# AMFObject extras;
# int edepth;
# int seekTime;
# int stopTime;
# int lFlags;
# int swfAge;
# int protocol;
# int timeout; /* connection timeout in seconds */
# int pFlags; /* unused, but kept to avoid breaking ABI */
# unsigned short socksport;
# unsigned short port;
# void *dh; /* for encryption */
# void *rc4keyIn;
# void *rc4keyOut;
# uint32_t SWFSize;
# uint8_t SWFHash[RTMP_SWF_HASHLEN];
# char SWFVerificationResponse[RTMP_SWF_HASHLEN+10];
# } RTMP_LNK;
class RTMP_LNK < FFI::Struct
layout(
:hostname, AVal,
:sockshost, AVal,
:playpath0, AVal,
:playpath, AVal,
:tcUrl, AVal,
:swfUrl, AVal,
:pageUrl, AVal,
:app, AVal,
:auth, AVal,
:flashVer, AVal,
:subscribepath, AVal,
:usherToken, AVal,
:token, AVal,
:pubUser, AVal,
:pubPasswd, AVal,
:extras, AMFObject,
:edepth, :int,
:seekTime, :int,
:stopTime, :int,
:lFlags, :int,
:swfAge, :int,
:protocol, :int,
:timeout, :int,
:pFlags, :int,
:socksport, :ushort,
:port, :ushort,
:dh, :pointer,
:rc4keyIn, :pointer,
:rc4keyOut, :pointer,
:SWFSize, :uint32,
:SWFHash, [:uint8, RTMP_SWF_HASHLEN],
:SWFVerificationResponse, [:char, RTMP_SWF_HASHLEN+10])
end
# typedef struct RTMP
# {
# int m_inChunkSize;
# int m_outChunkSize;
# int m_nBWCheckCounter;
# int m_nBytesIn;
# int m_nBytesInSent;
# int m_nBufferMS;
# int m_stream_id; /* returned in _result from createStream */
# int m_mediaChannel;
# uint32_t m_mediaStamp;
# uint32_t m_pauseStamp;
# int m_pausing;
# int m_nServerBW;
# int m_nClientBW;
# uint8_t m_nClientBW2;
# uint8_t m_bPlaying;
# uint8_t m_bSendEncoding;
# uint8_t m_bSendCounter;
# int m_numInvokes;
# int m_numCalls;
# RTMP_METHOD *m_methodCalls; /* remote method calls queue */
# int m_channelsAllocatedIn;
# int m_channelsAllocatedOut;
# RTMPPacket **m_vecChannelsIn;
# RTMPPacket **m_vecChannelsOut;
# int *m_channelTimestamp; /* abs timestamp of last packet */
# double m_fAudioCodecs; /* audioCodecs for the connect packet */
# double m_fVideoCodecs; /* videoCodecs for the connect packet */
# double m_fEncoding; /* AMF0 or AMF3 */
# double m_fDuration; /* duration of stream in seconds */
# int m_msgCounter; /* RTMPT stuff */
# int m_polling;
# int m_resplen;
# int m_unackd;
# AVal m_clientID;
# RTMP_READ m_read;
# RTMPPacket m_write;
# RTMPSockBuf m_sb;
# RTMP_LNK Link;
# } RTMP;
class RTMP < FFI::Struct
layout(
:m_inChunkSize, :int,
:m_outChunkSize, :int,
:m_nBWCheckCounter, :int,
:m_nBytesIn, :int,
:m_nBytesInSent, :int,
:m_nBufferMS, :int,
:m_stream_id, :int,
:m_mediaChannel, :int,
:m_mediaStamp, :uint32,
:m_pauseStamp, :uint32,
:m_pausing, :int,
:m_nServerBW, :int,
:m_nClientBW, :int,
:m_nClientBW2, :uint8,
:m_bPlaying, :uint8,
:m_bSendEncoding, :uint8,
:m_bSendCounter, :uint8,
:m_numInvokes, :int,
:m_numCalls, :int,
:m_methodCalls, :pointer,
:m_channelsAllocatedIn, :int,
:m_channelsAllocatedOut, :int,
:m_vecChannelsIn, :pointer,
:m_vecChannelsOut, :pointer,
:m_channelTimestamp, :pointer,
:m_fAudioCodecs, :double,
:m_fVideoCodecs, :double,
:m_fEncoding, :double,
:m_fDuration, :double,
:m_msgCounter, :int,
:m_polling, :int,
:m_resplen, :int,
:m_unackd, :int,
:m_clientID, AVal,
:m_read, RTMP_READ,
:m_write, RTMPPacket,
:m_sb, RTMPSockBuf,
:Link, RTMP_LNK)
end
# int RTMP_ParseURL(const char *url, int *protocol,
# AVal *host, unsigned int *port, AVal *playpath, AVal *app);
attach_function :RTMP_ParseURL, [
:string, :pointer, AVal.ptr, :pointer, AVal.ptr, AVal.ptr], :int
# void RTMP_SetBufferMS(RTMP *r, int size);
# void RTMP_UpdateBufferMS(RTMP *r);
attach_function :RTMP_SetBufferMS, [RTMP.ptr, :int], :void
attach_function :RTMP_UpdateBufferMS, [RTMP.ptr], :void
# int RTMP_SetOpt(RTMP *r, const AVal *opt, AVal *arg);
# void RTMP_SetupStream(RTMP *r, int protocol,
# AVal *hostname, unsigned int port, AVal *sockshost,
# AVal *playpath, AVal *tcUrl, AVal *swfUrl,
# AVal *pageUrl, AVal *app, AVal *auth,
# AVal *swfSHA256Hash, uint32_t swfSize,
# AVal *flashVer, AVal *subscribepath, AVal *usherToken,
# int dStart, int dStop, int bLiveStream, long int timeout);
attach_function :RTMP_SetOpt, [RTMP.ptr, AVal.ptr, AVal.ptr], :int
attach_function :RTMP_SetupStream, [
RTMP.ptr, :int, AVal.ptr, :uint, AVal.ptr,
AVal.ptr, AVal.ptr, AVal.ptr,
AVal.ptr, AVal.ptr, AVal.ptr,
AVal.ptr, :uint32,
AVal.ptr, AVal.ptr, AVal.ptr,
:int, :int, :int, :long], :void
# int RTMP_Connect(RTMP *r, RTMPPacket *cp);
attach_function :RTMP_Connect, [RTMP.ptr, RTMPPacket.ptr], :int
# int RTMP_IsConnected(RTMP *r);
# int RTMP_IsTimedout(RTMP *r);
# double RTMP_GetDuration(RTMP *r);
# int RTMP_ToggleStream(RTMP *r);
attach_function :RTMP_IsConnected, [RTMP.ptr], :int
attach_function :RTMP_IsTimedout, [RTMP.ptr], :int
attach_function :RTMP_GetDuration, [RTMP.ptr], :double
attach_function :RTMP_ToggleStream, [RTMP.ptr], :int
# int RTMP_ConnectStream(RTMP *r, int seekTime);
# int RTMP_ReconnectStream(RTMP *r, int seekTime);
attach_function :RTMP_ConnectStream, [RTMP.ptr, :int], :int
attach_function :RTMP_ReconnectStream, [RTMP.ptr, :int], :int
# void RTMP_Init(RTMP *r);
# void RTMP_Close(RTMP *r);
attach_function :RTMP_Init, [RTMP.ptr], :void
attach_function :RTMP_Close, [RTMP.ptr], :void
# int RTMP_LibVersion(void);
attach_function :RTMP_LibVersion, [], :int
# int RTMP_FindFirstMatchingProperty(
# AMFObject *obj, const AVal *name, AMFObjectProperty * p);
attach_function :RTMP_FindFirstMatchingProperty, [
AMFObject.ptr, AVal.ptr, AMFObjectProperty.ptr], :int
# int RTMP_Read(RTMP *r, char *buf, int size);
attach_function :RTMP_Read, [RTMP.ptr, :pointer, :int], :int
### rtmpdump.c ###
# #define RD_SUCCESS 0
# #define RD_FAILED 1
# #define RD_INCOMPLETE 2
# #define RD_NO_CONNECT 3
RD_SUCCESS = 0
RD_FAILED = 1
RD_INCOMPLETE = 2
RD_NO_CONNECT = 3
# #define DEF_BUFTIME (10 * 60 * 60 * 1000) /* 10 hours default */
DEF_BUFTIME = (10 * 60 * 60 * 1000)
end
# define wrap methods
module LibRTMP
class AVal
def self.from_string(str)
aval = self.new
aval[:av_val] = FFI::MemoryPointer.from_string(str)
aval[:av_len] = str.bytesize
aval
end
def to_s
self[:av_val].read_string(self[:av_len])
rescue
""
end
def ==(other)
self.to_s == other.to_s
end
end
def decode_int24(str)
buffer = FFI::MemoryPointer.new :char, str.bytesize
buffer.put_bytes(0, str, 0, str.bytesize)
return LibRTMP.AMF_DecodeInt24(buffer)
end
def decode_int32(str)
buffer = FFI::MemoryPointer.new :char, str.bytesize
buffer.put_bytes(0, str, 0, str.bytesize)
return LibRTMP.AMF_DecodeInt32(buffer)
end
def parse_url(url)
protocol_p = FFI::MemoryPointer.new :int
host_aval = AVal.new
port_p = FFI::MemoryPointer.new :uint
playpath_aval = AVal.new
app_aval = AVal.new
if LibRTMP.RTMP_ParseURL(url, protocol_p, host_aval, port_p, playpath_aval, app_aval) <= 0
raise "failed to parse url. [url: #{url}]"
end
return {
protocol: protocol_p.read_int,
host: host_aval.to_s,
port: port_p.read_uint,
playpath: playpath_aval.to_s,
app: app_aval.to_s}
end
module_function :decode_int24, :decode_int32, :parse_url
class AMFObject
def decode(buffer, buffer_size, decode_name)
LibRTMP.AMF_Decode(self, buffer, buffer_size, decode_name ? 1 : 0)
end
def dump
LibRTMP.AMF_Dump(self)
end
def get_property(name, index)
name_aval = name.nil? ? nil : AVal.from_string(name)
LibRTMP.AMF_GetProp(self, name_aval, index)
end
def find_first_matching_property(name)
name_aval = AVal.from_string(name)
prop = AMFObjectProperty.new
if LibRTMP.RTMP_FindFirstMatchingProperty(self, name_aval, prop) > 0
prop
else
nil
end
end
end
class AMFObjectProperty
def get_number
LibRTMP.AMFProp_GetNumber(self)
end
def get_string
str_aval = AVal.new
LibRTMP.AMFProp_GetString(self, str_aval)
str_aval.to_s
end
end
class RTMP
def set_buffer_ms(buffer_ms)
LibRTMP.RTMP_SetBufferMS(self, buffer_ms)
end
def update_buffer_ms
LibRTMP.RTMP_UpdateBufferMS(self)
end
def set_option(option, value)
option_aval = AVal.from_string(option)
value_aval = AVal.from_string(value)
LibRTMP.RTMP_SetOpt(self, option_aval, value_aval)
end
def setup_stream(protocol, host_name, port, socks_host,
playpath, tc_url, swf_url,
page_url, app, auth,
swf_sha256_hash, swf_size,
flash_ver, subscribe_path, usher_token,
start, stop, is_live_stream, timeout)
host_name_aval = AVal.from_string(host_name)
socks_host_aval = AVal.from_string(socks_host)
playpath_aval = AVal.from_string(playpath)
tc_url_aval = AVal.from_string(tc_url)
swf_url_aval = AVal.from_string(swf_url)
page_url_aval = AVal.from_string(page_url)
app_aval = AVal.from_string(app)
auth_aval = AVal.from_string(auth)
swf_sha256_hash_aval = AVal.from_string(swf_sha256_hash)
flash_ver_aval = AVal.from_string(flash_ver)
subscribe_path_aval = AVal.from_string(subscribe_path)
usher_token_aval = AVal.from_string(usher_token)
LibRTMP.RTMP_SetupStream(self, protocol, host_name_aval, port, socks_host_aval,
playpath_aval, tc_url_aval, swf_url_aval,
page_url_aval, app_aval, auth_aval,
swf_sha256_hash_aval, swf_size,
flash_ver_aval, subscribe_path_aval, usher_token_aval,
start, stop, is_live_stream ? 1 : 0, timeout)
end
def connect(packet)
LibRTMP.RTMP_Connect(self, packet)
end
def connected?
LibRTMP.RTMP_IsConnected(self) > 0
end
def timedout?
LibRTMP.RTMP_IsTimedout(self) > 0
end
def get_duration
LibRTMP.RTMP_GetDuration(self)
end
def connect_stream(seek_time)
LibRTMP.RTMP_ConnectStream(self, seek_time)
end
def init
LibRTMP.RTMP_Init(self)
end
def close
LibRTMP.RTMP_Close(self)
end
def read(buffer, buffer_size)
LibRTMP.RTMP_Read(self, buffer, buffer_size)
end
def add_connect_option(option)
if self.set_option("conn", option) <= 0
raise "faild to set connect option. [option: #{option}]"
end
end
end
end
# application code for RTMP
module LibRTMP
ResumeInfo = Struct.new(:meta_header, :meta_header_size,
:last_key_frame, :last_key_frame_type, :last_key_frame_size,
:seek, :duration, :last_file_position)
class ResumeInfo
# Workaround to exit with 0 if the file is fully (> 99.9%) downloaded
def fully_downloaded?
seek = self.seek
duration = self.duration
(duration > 0) && (seek >= (duration * 999))
end
end
class FLVFile < File
def get_resume_info
resume_info = ResumeInfo.new
file_size = self.size
# read file header
self.pos = 0
header = self.read(13)
if (header.byteslice(0) != 'F') ||
(header.byteslice(1) != 'L') ||
(header.byteslice(2) != 'V') ||
(header.getbyte(3) != 0x01)
raise "invalid FLV file."
end
file_type = header.getbyte(4)
if (file_type & 0x05) == 0
raise "FLV file contains neither video nor audio."
end
audio_only = ((file_type & 0x04) > 0) && ((file_type & 0x01) == 0x00)
data_offset = LibRTMP.decode_int32(header.byteslice(5, 4))
self.pos = data_offset
prev_tag_size_data = self.read(4)
prev_tag_size = LibRTMP.decode_int32(prev_tag_size_data)
if prev_tag_size != 0
raise "first prev tag size is not 0."
end
# find meta data
block_offset = data_offset + 4
found = false
while (block_offset < (file_size - 4)) && (!found)
self.pos = block_offset
block_header = self.read(4)
data_size = LibRTMP.decode_int24(block_header.byteslice(1, 3))
if block_header.getbyte(0) == 0x12
self.pos = block_offset + 11
data = self.read(data_size)
data_buf = FFI::MemoryPointer.new :char, data_size
data_buf.write_bytes(data, 0, data_size)
meta_obj = AMFObject.new
if meta_obj.decode(data_buf, data_size, false) < 0
raise "failed to decode meta data packet."
end
property = meta_obj.get_property(nil, 0)
meta_string = property.get_string
if meta_string == "onMetaData"
meta_obj.dump
resume_info.meta_header_size = data_size
resume_info.meta_header = data_buf
prop_duration = meta_obj.find_first_matching_property("duration")
if prop_duration
resume_info.duration = prop_duration.get_number
else
resume_info.duration = 0
end
found = true
break
end
end
block_offset += 11 + data_size + 4
end
unless found
raise "failed to find meta data."
end
# find last key frame
block_offset = file_size
block_header = nil
prev_tag_size = 0
loop do
if block_offset < 13
raise "unexpected start of file."
end
self.pos = block_offset - 4
prev_tag_size_data = self.read(4)
prev_tag_size = LibRTMP.decode_int32(prev_tag_size_data)
if prev_tag_size == 0
raise "faild to find last key frame."
end
if (prev_tag_size < 0) || (prev_tag_size > (block_offset - 4 - 13))
raise "invalid prev tag size."
end
block_offset -= (prev_tag_size + 4)
self.pos = block_offset
block_header = self.read(11)
data_start = self.read(1)
if audio_only && (block_header.getbyte(0) == 0x08)
break
end
if (!audio_only) && (block_header.getbyte(0) == 0x09) && ((data_start.getbyte(0) & 0xf0) == 0x10)
break
end
end
resume_info.last_key_frame_type = block_header.getbyte(0)
last_key_frame_size = prev_tag_size - 11
resume_info.last_key_frame_size = last_key_frame_size
self.pos = block_offset + 11
last_key_frame_data = self.read(last_key_frame_size)
last_key_frame = FFI::MemoryPointer.new :char, last_key_frame_size
last_key_frame.write_bytes(last_key_frame_data, 0, last_key_frame_size)
resume_info.last_key_frame = last_key_frame
resume_info.seek = LibRTMP.decode_int24(block_header.byteslice(4, 3))
resume_info.seek |= (block_header.getbyte(7) << 24)
resume_info.last_file_position = block_offset + prev_tag_size + 4
return resume_info
end
end
class RTMP
def self.create_from_uri(uri, page_uri)
rtmp = self.new
rtmp.clear
rtmp.init
rtmp.setup_uri(uri, page_uri)
rtmp
end
def setup_uri(uri, page_uri)
url_without_query = URI::Generic.build(scheme: uri.scheme, host: uri.host, path: uri.path).to_s
@tc_url = url_without_query
@page_url = page_uri.to_s
@playpath = uri.query.split('=')[1]
url_info = LibRTMP.parse_url(url_without_query)
@protocol = url_info[:protocol]
@host = url_info[:host]
@port = url_info[:port]
@playpath = url_info[:playpath] unless url_info[:playpath].empty?
@app = url_info[:app]
if @port == 0
if (@port & RTMP_FEATURE_SSL) > 0
@port = 443
elsif (@port & RTMP_FEATURE_HTTP) > 0
@port = 80
else
@port = 1935
end
end
end
def open(resume_info, &block)
seek = resume_info ? resume_info.seek : 0
self.setup_stream(
@protocol, @host, @port, "",
@playpath, @tc_url, "",
@page_url, @app, "",
"", 0,
"", "", "",
0, 0, false, 30)
self[:Link][:lFlags] |= RTMP_LF_BUFX
self.set_buffer_ms(DEF_BUFTIME)
if self.connect(nil) <= 0
raise "failed to connect."
end
if self.connect_stream(seek) <= 0
raise "failed to connect stream."
end
if resume_info
self[:m_read][:timestamp] = seek
self[:m_read][:flags] |= RTMP_READ_RESUME
self[:m_read][:initialFrameType] = resume_info.last_key_frame_type
self[:m_read][:nResumeTS] = seek
self[:m_read][:metaHeader] = resume_info.meta_header
self[:m_read][:nMetaHeaderSize] = resume_info.meta_header_size
self[:m_read][:initialFrame] = resume_info.last_key_frame
self[:m_read][:nInitialFrameSize] = resume_info.last_key_frame_size
end
block.call
self.close
end
def read_data(&block)
status = RD_SUCCESS
buffer_size = 64 * 1024
buffer = FFI::MemoryPointer.new(:char, buffer_size)
buffer_time = DEF_BUFTIME
duration = self.get_duration
# download
loop do
size = self.read(buffer, buffer_size)
if size > 0
data = buffer.read_bytes(size)
if duration <= 0
duration = self.get_duration
end
if (duration > 0) && (buffer_time < (duration * 1000))
buffer_time = (duration * 1000 + 5000).to_i
self.set_buffer_ms(buffer_time)
self.update_buffer_ms
end
timestamp = self[:m_read][:timestamp]
block.call(data, timestamp / 1000.0, duration)
else
if self[:m_read][:status] == RTMP_READ_EOF
break
elsif self[:m_read][:status] == RTMP_READ_COMPLETE
break
end
end
if (size < 0) || (!self.connected?) || self.timedout?
status = RD_INCOMPLETE
break
end
end
timestamp = self[:m_read][:timestamp]
if (duration > 0) && (timestamp < (duration * 999))
status = RD_INCOMPLETE
end
status
end
end
end
class DownloadStatusPrinter
Size1K = 1024
Epsilon = 0.0001
def initialize(total_size=0, unit=:byte)
@start_time = Time.now
@total_size = total_size
@unit = unit
end
attr_accessor :total_size
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(current_size)
total_size_str = format(@total_size)
$stdout.printf("\rDownloading 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)
"#{format(speed)}/sec"
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(value)
if @unit == :byte
format_byte(value)
elsif @unit == :sec
format_sec(value)
else
"#{value}"
end
end
def format_byte(value)
if value
exp = (value > 0) ? Math.log(value, Size1K).to_i : 0
suffix = "bKMGTPEZY"[exp]
if exp == 0
"#{value}#{suffix}"
else
converted = value.to_f / (Size1K ** exp)
sprintf "%.2f%s", converted, suffix
end
else
"N/A b"
end
end
def format_sec(value)
if value
if value > 60
converted = value.to_f / 60
sprintf "%.1fmin", converted
else
sprintf "%.1fsec", value.to_f
end
else
"N/A sec"
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, :byte)
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"]
playpath = uri.query.split('=')[1]
fmst1 = video_info["fmst1"]
fmst2 = video_info["fmst2"]
status = LibRTMP::RD_SUCCESS
resume_info = nil
download_status_printer = DownloadStatusPrinter.new(0, :sec)
LibRTMP::FLVFile.open(output_filename, "w+b") do |file|
loop do
if resume_info && resume_info.fully_downloaded?
status = LibRTMP::RD_SUCCESS
break
end
rtmp = LibRTMP::RTMP.create_from_uri(uri, original_uri)
rtmp.add_connect_option("S:#{video_info['fmst1']}")
rtmp.add_connect_option("S:#{video_info['fmst2']}")
rtmp.add_connect_option("S:#{playpath}")
rtmp.open(resume_info) do
status = rtmp.read_data do |data, current_sec, total_sec|
file.write(data)
if (download_status_printer.total_size == 0) && (total_sec > 0)
download_status_printer.total_size = total_sec
end
download_status_printer.print(current_sec)
end
end
if status == LibRTMP::RD_INCOMPLETE
resume_info = file.get_resume_info
file.pos = resume_info.last_file_position
else
break
end
end
end
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