Created
May 28, 2015 04:10
-
-
Save yamaimo/5c3e46d97a92f716f0af to your computer and use it in GitHub Desktop.
NicovideoDownloader with Ruby (improved)
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' | |
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