ザキンコのブログ

ザキンコの日記のはてなブログ版です。

RDシリーズのネットdeダビングで動画データを受信できるRubyスクリプト

よく使っていた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ダビングソフトと同様にプロテクトがかかっていないアナログで録画した動画しか転送できません。
参考にしたサイト

(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