よく使っていたLANDE-RDが64bit環境で動かない(オープンソース版はWinPcapが必要で面倒)ので、LAN経由で受信するためのRubyスクリプトを書きました。Rubyだと非常に短く書けます。Rubyは「ruby 1.9.3p194 (2012-04-20) [i386-mingw32]」。テスト環境はWindows 7(64bit)、使ったデッキはRD-R100。
このスクリプトを実行するとNetBIOSのポート(137)をオープンしてRDシリーズのリクエストに応答するようになります。ダビングでLANを選ぶと「RDCapture」という機器が見えるのでそれに転送してください。カレントディレクトリに転送されたファイルを置きます。LANDE-RDと同じくtxtファイルも同時に作ります。あと、他のネットdeダビングソフトと同様にプロテクトがかかっていないアナログで録画した動画しか転送できません。
参考にしたサイト
- http://tools.ietf.org/html/rfc1002
- http://tools.ietf.org/html/rfc1001
- http://en.wikipedia.org/wiki/NetBIOS_over_TCP/IP
- http://www.usefullcode.net/2007/03/netbios.html
- http://www.usefullcode.net/2007/03/toshiba_rdde.html
- http://kawara.homelinux.net/pukiwiki/pukiwiki.php?RD-XS34
- http://www.hdbench.net/ja/olanderd/
- http://homepage.mac.com/raktajino/RDService/RDService.html
(2013.10.20追記)
9時間超の動画(ただし、DVD-Video形式の動画。VR形式の動画は未確認)はダビングできません。これはネットdeダビングかデッキの仕様のようです。うまくチャプターを切って9時間未満にしてください。RD-W301で確認しました。
RDCapture.rb (Rubyファイルの文字コードはWindows-31J、CP932、いわゆるWindows用の文字コードで)
# coding: Windows-31J require 'fileutils' require 'tempfile' require 'socket' Socket.do_not_reverse_lookup = true Thread.abort_on_exception = true def createTCPServer(host, startport) lastport = 65535 begin TCPServer.open(host, startport) rescue Errno::EADDRINUSE startport += 1 retry end end def topath(title) title.each_byte.map{|b| case b # Not available this characters in NTFS. http://support.microsoft.com/kb/100108/ja # ? " / \ < > * | : when 0x3f # ? [0x8148].pack('n') when 0x22 # " [0x814A].pack('n') when 0x2F # / [0x815E].pack('n') when 0x5C # \ [0x818F].pack('n') when 0x3C # < [0x8171].pack('n') when 0x3E # > [0x8172].pack('n') when 0x2A # * [0x8196].pack('n') when 0x7C # | [0x8162].pack('n') when 0x3A # : [0x8146].pack('n') else [b].pack('C') end }.join.force_encoding(Encoding::Windows_31J) end def getrdtitle(header) header[0x84, 64].delete("\0").force_encoding(Encoding::Windows_31J) end def getrddetail(header) header[0xCC, 800].delete("\0").force_encoding(Encoding::Windows_31J) end def getrdch(header) header[0x434, 132].delete("\0").force_encoding(Encoding::Windows_31J) end def getrdheader(path) IO.binread(path, 0x1800) end def getrdtitlefromfile(path) getrdtitle(getrdheader(path)) end def getrddetailfromfile(path) getrddetail(getrdheader(path)) end def getrdrecdatefromfile(path) "#{getrdheader(path)[0x62, 2]}/#{getrdheader(path)[0x64, 2]}/#{getrdheader(path)[0x66, 2]} #{getrdheader(path)[0x6c, 2]}:#{getrdheader(path)[0x6e, 2]}-#{getrdheader(path)[0x70, 2]}:#{getrdheader(path)[0x72, 2]}" end def getrdchfromfile(path) getrdch(getrdheader(path)) end def createrdtxt(title, detail, date, ch) str = <<"EOS" 番組名: #{title} 番組詳細: #{detail} 録画日時: #{date} チャンネル名: #{ch} EOS end def handle_data(path) FileUtils.cp(path, topath(getrdtitlefromfile(path)) + '.mpg', {:verbose => true}) File.open(topath(getrdtitlefromfile(path)) + '.mpg.txt', 'w') do |f| f.write createrdtxt(getrdtitlefromfile(path), getrddetailfromfile(path), getrdrecdatefromfile(path), getrdchfromfile(path)) end end def handle_datareceive(ds, path) File.open(path, 'wb') do |f| while input = ds.gets f.write(input) end end ds.close puts "data socket close" end def handle_datasend(ds) str = <<EOS <?xml version=\"1.0\" encoding=\"shift_jis\"?> <status> <dubbing>ready</dubbing> <accept_dubbing>ready</accept_dubbing> <device0_remain>167772160</device0_remain> <device0_recorded>0</device0_recorded> <device0_title_remain>350</device0_title_remain> <device1_remain>167772160</device1_remain> <device1_recorded>0</device1_recorded> <device1_title_remain>350</device1_title_remain> </status> EOS ds.puts str ds.close puts "data socket close" end def handle_client(c) dserver = nil # TCPServer c.puts "220 RDCapture v0.01 running on #{Socket.gethostname}" puts "Accepted connection from #{c.peeraddr[2]}" while true input = c.gets if !input p "Client on #{c.peeraddr[2]} disconnected." break end puts input = input.chomp command_a = input.split(' ') case command_a[0] when 'USER' c.puts('331 ') when 'PASS' c.puts('230 ') when 'TYPE' c.puts('200 ') when 'PASV' dserver = createTCPServer('', 5000) puts "Open port #{dserver.addr[1].to_s}" ip_port_str = Socket.ip_address_list[0].ip_address.tr('.', ',') + ',' + (dserver.addr[1] >> 8).to_s + ',' + (dserver.addr[1] & 0xff).to_s c.puts("227 Entering Passive Mode (#{ip_port_str}).") when 'STOR' if command_a[1] == '$netdubbing$dev0.dat' || command_a[1] == '$netdubbing$dubbinginfo.xml' c.puts('150 ') ds = dserver.accept # to temp t = Tempfile.open('') handle_datareceive(ds, t.path) handle_data(t.path) if command_a[1] == '$netdubbing$dev0.dat' c.puts('226 ') else c.puts('550 ') break end when 'RETR' c.puts('150 ') ds = dserver.accept handle_datasend(ds) c.puts('226 ') when 'QUIT' c.puts('221 ') break else c.puts('502 ') end end puts "Closing connection to #{c.peeraddr[2]}" c.close loop do break if c.closed? end end class NSPacket NAME_QUERY_REQUEST = :NAME_QUERY_REQUEST NAME_REGISTRATION_REQUEST = :NAME_REGISTRATION_REQUEST NAME_RELEASE_REQUEST_DEMAND = :NAME_RELEASE_REQUEST_DEMAND UNKNOWN_REQUEST = :UNKNOWN_REQUEST POSITIVE_NAME_QUERY_RESPONSE = :POSITIVE_NAME_QUERY_RESPONSE attr_reader :name_trn_id, :type, :rd_series def self.create_pnqr_packet(name_trn_id, name, addrinfo) buf = '' # Device Flag buf << [1].pack('C') # Transaction ID buf << [name_trn_id & 0xff].pack('C') # Flag OPCODE NM_FLAGS buf << [0x85].pack('C') # flag NM_FLAGS RCODE buf << [0x00].pack('C') # Quesion buf << [0x0000].pack('n') # Answer buf << [0x0001].pack('n') # Authority buf << [0x0000].pack('n') # Additional buf << [0x0000].pack('n') # Answer # Name.length buf << [32].pack('C') # Name # encode buf << ("%-15s\0"%name + [0].pack('C')).each_byte.map{|c|[(c >> 4) + 0x41, (c & 0xf) + 0x41].pack("CC")}.join # Name end buf << [0].pack('C') # Type buf << [0x0020].pack('n') # Class buf << [0x0001].pack('n') # TTL buf << [0x00000000].pack('N') # Length buf << [0x0006].pack('n') # Answer Flag buf << [0x0000].pack('n') # IP buf << addrinfo.to_sockaddr[4] buf << addrinfo.to_sockaddr[5] buf << addrinfo.to_sockaddr[6] buf << addrinfo.to_sockaddr[7] return buf end def parse(packet) unless packet p @type = UNKNOWN_REQUEST return end if packet.size < 12 p @type = UNKNOWN_REQUEST return end @header = packet[0, 12] @body = packet[12, packet.size - 12] # NetBIOS Header # NAME_TRN_ID 16 bits(unsigned short) # OPCODE 5 bits # R 0 # OPCODE 1-4 # NM_FLAGS 7 bits # RCODE 4 bits # QDCOUNT 16 bits # ANCOUNT 16 bits # NSCOUNT 16 bits # ARCOUNT 16 bits header_a = @header.unpack('n6') @name_trn_id = header_a[0] @response_flag = header_a[1][15] @opcode = (header_a[1] >> 11) & 0xf @authoritative_answer_flag = header_a[1][10] @truncation_flag = header_a[1][9] @recursion_desired_flag = header_a[1][8] @recursion_available_flag = header_a[1][7] @broadcast_flag = header_a[1][4] @rcode = header_a[1] & 0xf @qdcount = header_a[2] @ancount = header_a[3] @nscount = header_a[4] @arcount = header_a[5] if @response_flag == 0 && \ @opcode == 0 && \ @authoritative_answer_flag == 0 && \ @truncation_flag == 0 && \ @recursion_desired_flag == 1 && \ @recursion_available_flag == 0 && \ @broadcast_flag == 1 && \ @rcode == 0 && \ @qdcount == 1 && \ @ancount == 0 && \ @nscount == 0 && \ @arcount == 0 @type = NAME_QUERY_REQUEST elsif @response_flag == 0 && \ @opcode == 5 && \ @authoritative_answer_flag == 0 && \ @truncation_flag == 0 && \ @recursion_desired_flag == 1 && \ @recursion_available_flag == 0 && \ @broadcast_flag == 1 && \ @rcode == 0 && \ @qdcount == 1 && \ @ancount == 0 && \ @nscount == 0 && \ @arcount == 1 @type = NAME_REGISTRATION_REQUEST elsif @response_flag == 0 && \ @opcode == 6 && \ @authoritative_answer_flag == 0 && \ @truncation_flag == 0 && \ @recursion_desired_flag == 0 && \ @recursion_available_flag == 0 && \ @broadcast_flag == 1 && \ @rcode == 0 && \ @qdcount == 1 && \ @ancount == 0 && \ @nscount == 0 && \ @arcount == 1 @type = NAME_RELEASE_REQUEST_DEMAND else @type = UNKNOWN_REQUEST return end eoq = [32,1].pack('n2') @question_section = @body[0, @body.index(eoq) + 4] l = @question_section.unpack('C1') # size 0x20 cname = @question_section[1, l[0]] # decode dname = cname.each_byte.each_slice(2).map{|c1, c2| ((c1 - 0x41) << 4 | (c2 - 0x41)).chr}.join name = dname if dname[-1] == [0].pack('C') && dname[0, 3] == 'GR-' # RD Series? @rd_series = true name = dname.chop puts "Request from RD Series NET DE DUBBING :" << name else # NetBIOS @rd_series = false puts "Request from :" << name.chop end end end # main myname = 'RDCapture' # max 15 char puts "Server Name => #{myname}" puts "Current directory => #{FileUtils.pwd}" UDPSocket.do_not_reverse_lookup = true Thread.start(UDPSocket.open()) do |s| port = 137 s.bind('', port) puts 'Server start. Open port ' + port.to_s loop do msg, addr = s.recvfrom(1024) nsp = NSPacket.new nsp.parse(msg) puts nsp.type next unless nsp.type == NSPacket::NAME_QUERY_REQUEST && nsp.rd_series packet_res = NSPacket.create_pnqr_packet(nsp.name_trn_id, myname, Addrinfo.new(addr)) sock_res = UDPSocket.open() sock_res.send(packet_res, 0, Addrinfo.new(addr).ip_address, port) puts 'response send to:' << Addrinfo.new(addr).ip_address << ' port:' << port.to_s sock_res.close end end Thread.start(TCPServer.open('', 21)) do |s| puts 'Server start. Open port ' + s.addr[1].to_s loop do client = s.accept Thread.start(client) do |c| handle_client(c) end end end begin (ThreadGroup::Default.list - [Thread.current]).each {|th| th.join} puts 'all threads finished' rescue Interrupt puts 'Interrrupt' (ThreadGroup::Default.list - [Thread.current]).each {|th| th.kill} puts 'all threads kill' (ThreadGroup::Default.list - [Thread.current]).each {|th| th.join} puts 'all threads finished' end