MP3Tunes Locker から MP3 をアルバム単位でダウンロードするアプリ

MP3Tunes Locker が何やらおもしろそうだったので
MP3 をひたすらアップロードしている。


このサービスにはちゃんとした API が存在している。
ファイルごとにストリーム/ダウンロード用の URL があったりして
結構おもしろいことができそうな予感がしてるけど
何にも思いつかなかったのでとりあえず
アルバム単位で MP3 をダウンロードできるアプリを書いてみた。
VisualRuby の習作だったりする。


アルバム単位のダウンロード自体は LockerSync 3.0 の
「Get Music by Artist, Album or Playlist」からできる。
もちろん、コードを書いたあとに気づいたorz


ちょっと長いけどソースを貼り付けておく。


mp3tunes.rb

#!ruby
require 'net/https'
require 'open-uri'
require 'rexml/document'

EMAIL = "YOUR_EMAIL_ADDRESS"
PASSWORD = "YOUR_PASSWORD"
PARTNER_TOKEN = "9999999999" # get from http://www.mp3tunes.com/partner/cb/tokens/

class MP3Tunes
  def initialize(email = EMAIL, password = PASSWORD, partner_token = PARTNER_TOKEN)
    @email, @password, @partner_token = email, password, partner_token
    self
  end

  def login
    src = REXML::Source.new(opens("https://shop.mp3tunes.com/api/v1/login"+
      "?output=xml&username=#{@email}&password=#{@password}&partner_token=#{@partner_token}"))#, "UTF-8")
    @session_id = REXML::Document.new(src).get_elements('//session_id')[0].text
    puts @session_id
    self
  end

  def artists
    doc = REXML::Document.new(open("http://ws.mp3tunes.com/api/v1/lockerData" +
      "?output=xml&type=artist&sid=#{@session_id}"))
    doc.get_elements("//item").map do |item|
      [item.get_elements("./artistName")[0].text, item.get_elements("./artistId")[0].text]
    end
  end
  
  def albums(artist_id)
    print "artist_id="
    puts artist_id
    doc = REXML::Document.new(open("http://ws.mp3tunes.com/api/v1/lockerData" +
      "?output=xml&type=album&sid=#{@session_id}&artist_id=#{artist_id}"))
    doc.get_elements("//item").map do |item|
      [item.get_elements("./albumTitle")[0].text, item.get_elements("./albumId")[0].text]
    end
  end

  def tracks(album_id)
    print "album_id="
    puts album_id
    doc = REXML::Document.new(open("http://ws.mp3tunes.com/api/v1/lockerData" +
      "?output=xml&type=track&sid=#{@session_id}&album_id=#{album_id}"))
    doc.get_elements("//item").map do |item|
      {:trackId => item.get_elements("./trackId")[0].text,
        :trackTitle => item.get_elements("./trackTitle")[0].text,
        :trackNumber => item.get_elements("./trackNumber")[0].text,
        :trackFileSize => item.get_elements("./trackFileSize")[0].text,
        :downloadURL => item.get_elements("./downloadURL")[0].text + @partner_token,
        :playURL => item.get_elements("./playURL")[0].text + @partner_token,
        :albumTitle => item.get_elements("./albumTitle")[0].text,
        :artistName => item.get_elements("./artistName")[0].text,
      }
    end
  end
  
  private
  def opens(url)
    ar = URI.split(url)
    http = Net::HTTP.new(ar[2], '443')
    http.use_ssl = true
    http.verify_mode = OpenSSL::SSL::VERIFY_NONE
    response = nil
    http.start do |w| response = w.get(ar[5] + "?" + ar[7]) end
    response.body
  end
end

if __FILE__ == $0
  puts MP3Tunes.new.login.artists.size
end


treelist.rb

#!ruby
require 'vr/vrcontrol'
require 'vr/vrcomctl'
require 'vr/vrtwopane'
require 'vr/rscutil'
require 'vr/vrdialog'
require 'kconv'
require 'mp3tunes'

# http://vruby.sourceforge.net/cgi-bin/wiki.cgi?ProgressDialogSampel
class Modaldlg1 < VRModalDialog
  include VRContainersSet
  include VRContainersSet

  def construct
    self.caption = '進捗状況'
    self.move(395,192,247,139)
    addControl(VRStatic,'static1',"static1",24,0,96,24)
    addControl(VRProgressbar,'prgrssBar1',"prgrssBar1",24,32,192,16)
    addControl(VRButton,'button1',"キャンセル",88,72,80,24)
  end 

  include VRUserMessageUseable 
  @@arg = nil
  def self.event(arg)
    @@arg = arg
  end
  
  def self_created
    registerUserMessage(0x11,'progress_state')
    Thread.new do
      loop do
        sleep 0.2
        userMessage('progress_state')
      end
    end
  end
  
  def self_progress_state(wparem,lparam)
    @static1.caption = @@arg.to_s
    @prgrssBar1.position = @@arg.to_i
    close 0 if @@arg.to_i > 99
  end
  
  def button1_clicked
    close 1
  end
end

class TrackListview < VRListview
  def vrinit
    super
    addColumn("No",20)
    addColumn("Name",150)
    addColumn("Size",80)
  end

  def set_tracks(tracks)
    clearItems
    tracks.each do |t|
     addItem([t[:trackNumber], t[:trackTitle].tosjis, t[:trackFileSize]])
    end
  end
end

class ArtistTreeview < VRTreeview
  def set_artists(artists)
    artists.each do |a|
      p = addItem(root, a[0].tosjis, a[1].intern)
      addItem(p, "!", 0) # dummy node
    end
  end

  def add_albums(hitem, albums)
    c = getChildOf(hitem)
    if getItemLParamOf(c) == 0
      deleteItem(c)
      albums.each do |a|
        addItem(hitem, a[0].tosjis, a[1].intern)
      end
    end
  end
end

module MainForm
  include VRMenuUseable
  include VRHorizTwoPane

  def vrinit
    super
    @mp3tunes = MP3Tunes.new.login
  end

  def construct
    self.move(10,10,620,420)
    self.caption = "MP3Tunes Explorer"
    addPanedControl(ArtistTreeview,"tv1","tv")
    addPanedControl(TrackListview,"lv1","lv")
    setMenu newMenu.set( [["&File",[
                           ["&Download","downloadc"],
                           ["&Exit","exitc"]
                          ]]
                        ] )
    @artists = @mp3tunes.artists
    @tv1.set_artists(@artists)
  end

  ## notify ##
  def tv1_selchanged(hitem,lparam)
    album_id = @tv1.getItemLParamOf(hitem).to_sym.to_s
    @lv1.set_tracks(@mp3tunes.tracks(album_id)) if @tv1.getParentOf(hitem)
  end

  def tv1_itemexpanded(hitem,state,lparam)
    album_id = @tv1.getItemLParamOf(hitem).to_sym.to_s
    @tv1.add_albums(hitem, @mp3tunes.albums(album_id))
  end

  ## command ##
  def downloadc_clicked
    if @tv1.selectedItem && @tv1.getParentOf(@tv1.selectedItem) 
      unless @dir
        @dir = selectDirectory
        @dir = @dir.split("\\").join("/") if @dir
      end

      return unless @dir

      album_id = @tv1.getItemLParamOf(@tv1.selectedItem).to_sym.to_s
      tracks = @mp3tunes.tracks(album_id)
      @canceled = 0
      Thread.new do
        i = 0
        tracks.each do |t|
          i += 1
          path = "#{@dir}/#{t[:artistName].tosjis}/#{t[:albumTitle].tosjis}"+
                "/#{t[:trackNumber]}.#{t[:trackTitle].tosjis}.mp3"
          puts t[:downloadURL]
          puts path
          mkdirp(path)
          ar = URI.split(t[:downloadURL])
          dest = open(path, "wb")
          read_size = 0
          Net::HTTP.new(ar[2]).start do |w|
            w.get(ar[5] + "?" + ar[7]) do |buf|
              read_size += buf.size
              dest.write buf
              Modaldlg1.event(100 * i / tracks.size * read_size / t[:trackFileSize].to_i)
              Thread.stop if @canceled == 1
            end
          end
        end
      end
      @canceled = VRLocalScreen.modalform(self, nil, Modaldlg1)
    else
      messageBox "アルバムを選択してください"
    end
  end

  def exitc_clicked
    exit 0
  end

  private
  # http://araistudy.g.hatena.ne.jp/czk-htn/20061024/p2
  def mkdirp(path)
    dir = File::dirname(path)
    dir.scan %r{[^/]+/?} do |d|
      test(?d,d) or Dir.mkdir d
      Dir.chdir d
    end
  end
end

if __FILE__ == $0
  frm = VRLocalScreen.newform
  frm.extend MainForm
  frm.create.show
  VRLocalScreen.messageloop
end


動作イメージ

使い方

mp3tunes.rb のメールアドレスとパスワードを編集

$ ruby treelist.rb

アルバムを選択して File -> Download

所感

  • VisualRuby スゲー
  • PyQt で書かれた LockerSync 3.0 はもっとスゲー
  • MP3Tunes Player がリニューアルして使いにくくなった
  • 登録したときは 25GB まで無料だったのが途中から 50GB まで無料になった
  • すでにあるものを気づかずに作るのが多いをなんとかしたい…