rubymtp 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +340 -0
- data/ChangeLog +2 -0
- data/README +57 -0
- data/bin/mtpfs +10 -0
- data/bin/podcast +10 -0
- data/lib/mtp.rb +285 -0
- data/lib/mtp/association.rb +49 -0
- data/lib/mtp/container.rb +121 -0
- data/lib/mtp/datacode.rb +485 -0
- data/lib/mtp/datatypes.rb +51 -0
- data/lib/mtp/device.rb +270 -0
- data/lib/mtp/fuse.rb +368 -0
- data/lib/mtp/object.rb +122 -0
- data/lib/mtp/playlist.rb +60 -0
- data/lib/mtp/podcast.rb +162 -0
- data/lib/mtp/properties.rb +226 -0
- data/lib/mtp/protocol.rb +298 -0
- data/lib/mtp/storage.rb +39 -0
- data/lib/mtp/track.rb +74 -0
- data/patches/ruby-mp3info-0.5.diff +80 -0
- data/patches/syndication-0.6.1.diff +41 -0
- data/test/container_test.rb +13 -0
- metadata +87 -0
    
        data/lib/mtp/object.rb
    ADDED
    
    | @@ -0,0 +1,122 @@ | |
| 1 | 
            +
            module MTP
         | 
| 2 | 
            +
              class Object
         | 
| 3 | 
            +
                
         | 
| 4 | 
            +
                attr_reader(:ph, :properties, :id, :format, :protection_status, :compressed_size, :thumb_format, :thumb_compressed_size,
         | 
| 5 | 
            +
                            :thumb_pix_width, :thumb_pix_height, :image_pix_with, :image_pix_height, :image_bit_depth,
         | 
| 6 | 
            +
                            :sequence_number)
         | 
| 7 | 
            +
                attr_accessor :compressed_size, :filename, :date_created, :date_modified, :keywords, :association, :data
         | 
| 8 | 
            +
                
         | 
| 9 | 
            +
                PROTECTION_STATUS = { 0x0000 => "No protection", 0x0001 => "Read-only", 0x8002 => "Read-only Data", 0x8003 => "Non-transferable data"}
         | 
| 10 | 
            +
                  
         | 
| 11 | 
            +
                def initialize
         | 
| 12 | 
            +
                  @properties = Properties.new(self)
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def self.property_writer(*properties)
         | 
| 16 | 
            +
                  properties.each do |property|
         | 
| 17 | 
            +
                    Object.class_eval %{
         | 
| 18 | 
            +
                      def #{property}=(prop)
         | 
| 19 | 
            +
                        properties.#{property} = prop
         | 
| 20 | 
            +
                      end
         | 
| 21 | 
            +
                    }
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def self.property_reader(*properties)
         | 
| 26 | 
            +
                  properties.each do |property|
         | 
| 27 | 
            +
                    Object.class_eval %{
         | 
| 28 | 
            +
                      def #{property}
         | 
| 29 | 
            +
                        properties.#{property}
         | 
| 30 | 
            +
                      end
         | 
| 31 | 
            +
                    }
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def self.property_accessor(*properties)
         | 
| 36 | 
            +
                  Object.property_reader(*properties)
         | 
| 37 | 
            +
                  Object.property_writer(*properties)
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def self.register_format(klass, code)
         | 
| 41 | 
            +
                  @@format_classes ||= Hash.new { |h,k| h[k] = Object }
         | 
| 42 | 
            +
                  @@format_classes[code.to_i] = klass
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                
         | 
| 46 | 
            +
                def self.load(ph, id, storage = nil)
         | 
| 47 | 
            +
                  object = nil
         | 
| 48 | 
            +
                  t = ph.transaction.get_object_info(id)
         | 
| 49 | 
            +
                  t.expect("OK")
         | 
| 50 | 
            +
                  storage_id, format = t.data.payload.unpack("VK")
         | 
| 51 | 
            +
                  object = @@format_classes[format.to_i].new
         | 
| 52 | 
            +
                  object.load(ph, id, t.data.payload)
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
                
         | 
| 55 | 
            +
                def load(ph, id, payload, storage = nil)
         | 
| 56 | 
            +
                  @id = id
         | 
| 57 | 
            +
                  @ph = ph
         | 
| 58 | 
            +
                  @storage = storage
         | 
| 59 | 
            +
                  @sent = true
         | 
| 60 | 
            +
                  storage_id, @format, @protection_status, @compressed_size, @thumb_format,
         | 
| 61 | 
            +
                  @thumb_compressed_size, @thumb_pix_width, @thumb_pix_height, @image_pix_width, @image_pix_height, @image_bit_depth,
         | 
| 62 | 
            +
                  @parent_id, association_code, association_desc, @sequence_number, @filename, @date_created, @date_modified, @keywords =
         | 
| 63 | 
            +
                    payload.unpack("VKSIKIIIIIIISIIJDDJ")
         | 
| 64 | 
            +
                  @protection_status = Datacode.new(@protection_status, PROTECTION_STATUS)
         | 
| 65 | 
            +
                  @association = Association.load(association_code, association_desc)
         | 
| 66 | 
            +
                  self
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
                
         | 
| 69 | 
            +
                def inspect
         | 
| 70 | 
            +
                  "#<MTP::Object:0x%08x @format=#{@format.name.inspect} @filename=#{@filename.inspect} @parent_id=0x%08x>" % [ @id, @parent_id ]
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def dump_properties
         | 
| 74 | 
            +
                  puts "0x%04x:%16s: %64s %s" % [ 0, "id", @id, "ro" ]
         | 
| 75 | 
            +
                  properties.supported.each do |property|
         | 
| 76 | 
            +
                    p = properties.get_value(property)
         | 
| 77 | 
            +
                    puts "0x%04x:%16s: %64s %s" % [ p.code.to_i, property, p.value, (p.writable? ? 'rw' : 'ro') ]
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
                  
         | 
| 81 | 
            +
                def format=(code)
         | 
| 82 | 
            +
                  @format = (code.is_a?(Datacode) ? code : Datacode.new(code, nil, MTP::Datacode::Masks::OBJECT_FORMAT))
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
                
         | 
| 85 | 
            +
                def association
         | 
| 86 | 
            +
                  @association ||= Association.load(0, 0)
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
                
         | 
| 89 | 
            +
                def valid?
         | 
| 90 | 
            +
                  !@format.nil? and !@compressed_size.nil?
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
                
         | 
| 93 | 
            +
                
         | 
| 94 | 
            +
                def pack
         | 
| 95 | 
            +
                  [ 0, @format, @protection_status || 0, @compressed_size || 0, @thumb_format || 0, # VKSIK
         | 
| 96 | 
            +
                    @thumb_compressed_size || 0, @thumb_pix_width || 0, @thumb_pix_height || 0,     #III
         | 
| 97 | 
            +
                    @image_pix_width || 0, @image_pix_height || 0, @image_bit_depth || 0,           #III
         | 
| 98 | 
            +
                    @parent_id || 0, association.code, association.desc, @sequence_number || 0,     #IKII
         | 
| 99 | 
            +
                    @filename, @date_created, @date_modified, @keywords || "" ].pack("VKSIKIIIIIIIKIIJDDJ")
         | 
| 100 | 
            +
                end
         | 
| 101 | 
            +
                
         | 
| 102 | 
            +
                def new_object?
         | 
| 103 | 
            +
                  !@sent
         | 
| 104 | 
            +
                end
         | 
| 105 | 
            +
                
         | 
| 106 | 
            +
                def before_sending(ph)
         | 
| 107 | 
            +
                  @ph = ph
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
                
         | 
| 110 | 
            +
                def after_sending
         | 
| 111 | 
            +
                  @sent = true
         | 
| 112 | 
            +
                end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                def <=>(object)
         | 
| 115 | 
            +
                  @id <=> object.id 
         | 
| 116 | 
            +
                end
         | 
| 117 | 
            +
                
         | 
| 118 | 
            +
                def data
         | 
| 119 | 
            +
                  @data || Data.new("\0\0", @compressed_size)
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
              end
         | 
| 122 | 
            +
            end
         | 
    
        data/lib/mtp/playlist.rb
    ADDED
    
    | @@ -0,0 +1,60 @@ | |
| 1 | 
            +
            module MTP
         | 
| 2 | 
            +
              class Playlist < Object
         | 
| 3 | 
            +
                include Enumerable
         | 
| 4 | 
            +
                
         | 
| 5 | 
            +
                register_format self, Datacode.new("AbstractAudioVideoPlaylist")
         | 
| 6 | 
            +
                property_accessor :name
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def initialize
         | 
| 9 | 
            +
                  super
         | 
| 10 | 
            +
                  self.format = "AbstractAudioVideoPlaylist"
         | 
| 11 | 
            +
                  self.compressed_size = 0
         | 
| 12 | 
            +
                  @data = Data.new("\0\0")
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
                
         | 
| 15 | 
            +
                def load(ph, id, payload)
         | 
| 16 | 
            +
                  super(ph, id, payload)
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def each
         | 
| 20 | 
            +
                  MTP.logger.info("retrieving playlist reference")
         | 
| 21 | 
            +
                  t = @ph.transaction.get_object_references(id)
         | 
| 22 | 
            +
                  t.expect("OK")
         | 
| 23 | 
            +
                  t.data.payload.unpack("V+").first.each do |object_id|
         | 
| 24 | 
            +
                    yield @ph.device[object_id] unless @ph.device[object_id].nil?
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
                
         | 
| 28 | 
            +
                def <<(object)
         | 
| 29 | 
            +
                  current_ids = map { |o| o.id }
         | 
| 30 | 
            +
                  current_ids << object.id
         | 
| 31 | 
            +
                  store_references(current_ids)
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def delete(object)
         | 
| 35 | 
            +
                  current_ids = select { |o| o.id != object.id }
         | 
| 36 | 
            +
                  current_ids.map! { |o| o.id }
         | 
| 37 | 
            +
                  store_references(current_ids)
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                def sort!(&block)
         | 
| 41 | 
            +
                  current_ids = sort(&block).map { |o| o.id }
         | 
| 42 | 
            +
                  store_references(current_ids)
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def sort_by!(&block)
         | 
| 46 | 
            +
                  current_ids = sort_by(&block).map { |o| o.id }
         | 
| 47 | 
            +
                  store_references(current_ids)
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                private 
         | 
| 51 | 
            +
                def store_references(ids)
         | 
| 52 | 
            +
                  t = @ph.transaction
         | 
| 53 | 
            +
                  t.data = Data.new([ ids ].pack("V+"))
         | 
| 54 | 
            +
                  t.set_object_references(id)
         | 
| 55 | 
            +
                  t.expect("OK")
         | 
| 56 | 
            +
                  self
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
                  
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
            end
         | 
    
        data/lib/mtp/podcast.rb
    ADDED
    
    | @@ -0,0 +1,162 @@ | |
| 1 | 
            +
            require 'mtp'
         | 
| 2 | 
            +
            require 'yaml'
         | 
| 3 | 
            +
            require 'syndication/rss'
         | 
| 4 | 
            +
            require 'syndication/podcast'
         | 
| 5 | 
            +
            require 'open-uri'
         | 
| 6 | 
            +
            require 'fileutils'
         | 
| 7 | 
            +
            require 'facets/more/times'
         | 
| 8 | 
            +
            require 'facet/date/to_time'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            class Syndication::RSS::Item
         | 
| 11 | 
            +
              attr_accessor :keep
         | 
| 12 | 
            +
            end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
             | 
| 15 | 
            +
            module MTP
         | 
| 16 | 
            +
              class Podcast
         | 
| 17 | 
            +
                @@logger = Logger.new(STDERR)
         | 
| 18 | 
            +
                def self.logger
         | 
| 19 | 
            +
                  @@logger
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
                
         | 
| 22 | 
            +
                def logger
         | 
| 23 | 
            +
                  @@logger
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
                
         | 
| 26 | 
            +
                class Feed
         | 
| 27 | 
            +
                  attr_accessor :title, :url, :keep
         | 
| 28 | 
            +
                  def initialize(title, options = {})
         | 
| 29 | 
            +
                    @title = title
         | 
| 30 | 
            +
                    @url = options["url"] || options[:url]
         | 
| 31 | 
            +
                    @keep = options["keep"] || options[:keep]
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                 
         | 
| 34 | 
            +
                  def items
         | 
| 35 | 
            +
                    content = nil
         | 
| 36 | 
            +
                    open(@url) { |s| content = s.read }
         | 
| 37 | 
            +
                    parser = Syndication::RSS::Parser.new
         | 
| 38 | 
            +
                    rss = parser.parse(content)
         | 
| 39 | 
            +
                    rss.items.select { |i| i.keep = @keep; i.pubdate.to_time > @keep.days.ago }
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
                
         | 
| 43 | 
            +
                def initialize(device, options = {})
         | 
| 44 | 
            +
                  @device = device
         | 
| 45 | 
            +
                  @playlist_name = options[:playlist] || "podcasts"
         | 
| 46 | 
            +
                  @configuration_dir = File.join(ENV['HOME'], '.mtp-podcast')
         | 
| 47 | 
            +
                  @configuration_file = options[:configuration] || File.join(@configuration_dir, 'config.yml')
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
                
         | 
| 50 | 
            +
                def playlist
         | 
| 51 | 
            +
                  return @playlist unless @playlist.nil?
         | 
| 52 | 
            +
                  
         | 
| 53 | 
            +
                  @playlist = @device.playlists.detect { |p| p.name == @playlist_name }
         | 
| 54 | 
            +
                  
         | 
| 55 | 
            +
                  if @playlist.nil?
         | 
| 56 | 
            +
                    @playlist = Playlist.new
         | 
| 57 | 
            +
                    @device.send(@playlist)
         | 
| 58 | 
            +
                    @playlist.name = @playlist_name
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
                    
         | 
| 61 | 
            +
                  @playlist
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
                
         | 
| 64 | 
            +
                def default_configuration
         | 
| 65 | 
            +
                  [ "feeds" => [] ]
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
                
         | 
| 68 | 
            +
                def configuration
         | 
| 69 | 
            +
                  return @configuration unless @configuration.nil?
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  Dir.mkdir(@configuration_dir) unless File.exists?(@configuration_dir)
         | 
| 72 | 
            +
                  if File.exists?(@configuration_file)
         | 
| 73 | 
            +
                    File.open(@configuration_file, 'r') do |f|
         | 
| 74 | 
            +
                      @configuration = YAML.load(f)
         | 
| 75 | 
            +
                    end
         | 
| 76 | 
            +
                  else
         | 
| 77 | 
            +
                    @configuration = default_configuration
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                  @configuration
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
                
         | 
| 82 | 
            +
                def feeds
         | 
| 83 | 
            +
                  @feeds ||= configuration["feeds"].keys.map do |k| 
         | 
| 84 | 
            +
                    options = configuration["feeds"][k]
         | 
| 85 | 
            +
                    options["directory"] = File.join(@configuration_dir, 'feeds', k) if options["directory"].nil?
         | 
| 86 | 
            +
                    Feed.new(k, options)
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
                
         | 
| 90 | 
            +
                def update
         | 
| 91 | 
            +
                  items = feeds.inject([]) { |ary, feed| ary + feed.items }
         | 
| 92 | 
            +
                  items.sort! { |a, b| a.pubdate <=> b.pubdate }
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  
         | 
| 95 | 
            +
                  playlist.each do |track|
         | 
| 96 | 
            +
                    in_rss_list = nil
         | 
| 97 | 
            +
                    items.each do |item|
         | 
| 98 | 
            +
                      if File.basename(item.enclosure.url) == track.filename
         | 
| 99 | 
            +
                        in_rss_list = item
         | 
| 100 | 
            +
                        break
         | 
| 101 | 
            +
                      end
         | 
| 102 | 
            +
                    end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                    # remove from device the tracks which aren't in rss or which are too old
         | 
| 105 | 
            +
                    if in_rss_list.nil? or in_rss_list.pubdate.to_time < in_rss_list.keep.days.ago
         | 
| 106 | 
            +
                      puts "removing #{track.filename}"
         | 
| 107 | 
            +
                      playlist.delete(track)
         | 
| 108 | 
            +
                      @device.delete(track)
         | 
| 109 | 
            +
                    else
         | 
| 110 | 
            +
                      puts "skipping #{track.filename}"
         | 
| 111 | 
            +
                      items.delete(in_rss_list)
         | 
| 112 | 
            +
                    end
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
                  
         | 
| 115 | 
            +
                  # download missing items
         | 
| 116 | 
            +
                  items.each do |item|
         | 
| 117 | 
            +
                    puts "downloading #{File.basename(item.enclosure.url)}"
         | 
| 118 | 
            +
                    uri = URI.parse(item.enclosure.url)
         | 
| 119 | 
            +
                    
         | 
| 120 | 
            +
                    # connect to server
         | 
| 121 | 
            +
                    Net::HTTP.start(uri.host, uri.port) do |http|
         | 
| 122 | 
            +
                      
         | 
| 123 | 
            +
                      # big hack to access socket io
         | 
| 124 | 
            +
                      netio = http.instance_variable_get(:@socket)
         | 
| 125 | 
            +
                      if netio.respond_to?(:io)
         | 
| 126 | 
            +
                        sock = netio.io # 1.9
         | 
| 127 | 
            +
                      else
         | 
| 128 | 
            +
                        sock = netio.instance_variable_get(:@socket) # 1.8
         | 
| 129 | 
            +
                      end
         | 
| 130 | 
            +
                      
         | 
| 131 | 
            +
                      # execute request
         | 
| 132 | 
            +
                      request = Net::HTTP::Get.new(uri.path)
         | 
| 133 | 
            +
                      request["host"] = (uri.port ? "#{uri.host}:#{uri.port}" : uri.host)
         | 
| 134 | 
            +
                      request.exec(sock, Net::HTTP::HTTPVersion, request.path)
         | 
| 135 | 
            +
                      
         | 
| 136 | 
            +
                      # fetch headers
         | 
| 137 | 
            +
                      while (str = sock.readline) != "\r\n"
         | 
| 138 | 
            +
                        if str.match(/^Content-Length:\s*(\d+)\s*$/)
         | 
| 139 | 
            +
                          length = $1.to_i
         | 
| 140 | 
            +
                        end
         | 
| 141 | 
            +
                      end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                      # store object
         | 
| 144 | 
            +
                      object = MP3Track.new
         | 
| 145 | 
            +
                      object.filename = File.basename(item.enclosure.url)
         | 
| 146 | 
            +
                      object.compressed_size = length
         | 
| 147 | 
            +
                      @device.send(object, sock) do |written, total|
         | 
| 148 | 
            +
                        printf("%02u%% %u/%u\r", written * 100/total, written, total)
         | 
| 149 | 
            +
                      end
         | 
| 150 | 
            +
                      printf("\n")
         | 
| 151 | 
            +
                      object.name = item.title
         | 
| 152 | 
            +
                      object.artist = item.itunes_author
         | 
| 153 | 
            +
                      object.duration = item.itunes_duration * 1000
         | 
| 154 | 
            +
                      playlist << object
         | 
| 155 | 
            +
                    end
         | 
| 156 | 
            +
                  end
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
              end
         | 
| 159 | 
            +
            end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
             | 
| 162 | 
            +
             | 
| @@ -0,0 +1,226 @@ | |
| 1 | 
            +
            module MTP
         | 
| 2 | 
            +
              class Properties
         | 
| 3 | 
            +
                def initialize(object)
         | 
| 4 | 
            +
                  @object = object
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
                
         | 
| 7 | 
            +
                def clear
         | 
| 8 | 
            +
                  @supported = nil
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
                
         | 
| 11 | 
            +
                def supported
         | 
| 12 | 
            +
                  return @supported unless @supported.nil?
         | 
| 13 | 
            +
                  
         | 
| 14 | 
            +
                  t = @object.ph.transaction.get_object_props_supported(@object.format)
         | 
| 15 | 
            +
                  t.expect("OK")
         | 
| 16 | 
            +
                  @supported = t.data.payload.unpack("K+").first
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def writable
         | 
| 20 | 
            +
                  supported.select { |p| Property.description(@object, p).writable? }
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
                
         | 
| 23 | 
            +
                def supports?(code)
         | 
| 24 | 
            +
                  supported.include?(code)
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
                
         | 
| 27 | 
            +
                def get_value(code)
         | 
| 28 | 
            +
                  code = Property.code(code)
         | 
| 29 | 
            +
                  raise UnsupportedProperty.new(@object, code)  unless supports?(code)
         | 
| 30 | 
            +
                  Property.get(@object, code)
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def set_value(code, value)
         | 
| 34 | 
            +
                  code = Property.code(code)
         | 
| 35 | 
            +
                  raise UnsupportedProperty.new(self, code) unless supports?(code)
         | 
| 36 | 
            +
                  Property.set(@object, code, value)
         | 
| 37 | 
            +
                end  
         | 
| 38 | 
            +
                
         | 
| 39 | 
            +
                def method_missing(method, *args)
         | 
| 40 | 
            +
                  method = method.to_s
         | 
| 41 | 
            +
                  if method[-1,1] == '='
         | 
| 42 | 
            +
                    method.slice!(-1,1)
         | 
| 43 | 
            +
                    set_value(method.camelize, *args).value
         | 
| 44 | 
            +
                  else
         | 
| 45 | 
            +
                    get_value(method.camelize, *args).value
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
                
         | 
| 49 | 
            +
                class Property
         | 
| 50 | 
            +
                  @@descriptions = {}
         | 
| 51 | 
            +
                  
         | 
| 52 | 
            +
                  attr_reader :description, :code, :value
         | 
| 53 | 
            +
                  
         | 
| 54 | 
            +
                  def self.description(object, code)
         | 
| 55 | 
            +
                    raise UnsupportedProperty.new(object, code) unless object.properties.supports?(code)
         | 
| 56 | 
            +
                    @@descriptions[code.to_i] ||= PropertyDescription.load(object.ph, code, object.format)
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                  
         | 
| 59 | 
            +
                  def self.code(code)
         | 
| 60 | 
            +
                    (code.is_a?(Datacode) ? code : Datacode.new(code, nil, MTP::Datacode::Masks::OBJECT_PROPERTY))
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
                  
         | 
| 63 | 
            +
                  def self.get(object, c)
         | 
| 64 | 
            +
                    raise MTPError.new("object is not stored on the device") if object.new_object?
         | 
| 65 | 
            +
                    prop = Property.new
         | 
| 66 | 
            +
                    prop.instance_eval do
         | 
| 67 | 
            +
                      @object = object
         | 
| 68 | 
            +
                      @code = Property.code(c)
         | 
| 69 | 
            +
                      @description = Property.description(object, @code)
         | 
| 70 | 
            +
                      t = object.ph.transaction.get_object_prop_value(object.id, @code)
         | 
| 71 | 
            +
                      t.expect("OK")
         | 
| 72 | 
            +
                      @value = @description.unpack(t.data.payload)
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
                    prop
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  def self.set(object, c, value)
         | 
| 78 | 
            +
                    raise MTPError.new("object is not stored on the device") if object.new_object?
         | 
| 79 | 
            +
                    prop = Property.new
         | 
| 80 | 
            +
                    prop.instance_eval do
         | 
| 81 | 
            +
                      @object = object
         | 
| 82 | 
            +
                      @code = Property.code(c)
         | 
| 83 | 
            +
                      @description = Property.description(object, @code)
         | 
| 84 | 
            +
                      t = object.ph.transaction
         | 
| 85 | 
            +
                      t.data = Data.new(@description.pack(value))
         | 
| 86 | 
            +
                      t.set_object_prop_value(object.id, @code)
         | 
| 87 | 
            +
                      t.expect("OK")
         | 
| 88 | 
            +
                      @value = value
         | 
| 89 | 
            +
                    end
         | 
| 90 | 
            +
                    prop
         | 
| 91 | 
            +
                  end
         | 
| 92 | 
            +
                  
         | 
| 93 | 
            +
                  def writable?
         | 
| 94 | 
            +
                    Property.description(@object, @code).writable?
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
                
         | 
| 98 | 
            +
                class PropertyDescription
         | 
| 99 | 
            +
                  attr_reader :datatype, :form
         | 
| 100 | 
            +
                  def self.load(ph, code, format)
         | 
| 101 | 
            +
                    prop_desc = PropertyDescription.new
         | 
| 102 | 
            +
                    t = ph.transaction.get_object_prop_desc(code, format)
         | 
| 103 | 
            +
                    t.expect("OK")
         | 
| 104 | 
            +
                    prop_desc.instance_eval do
         | 
| 105 | 
            +
                      @format = format
         | 
| 106 | 
            +
                      @code, @datatype, @access, str = t.data.payload.unpack("KSCa*")
         | 
| 107 | 
            +
                      @dts = Datacode.new(@datatype, MTP::DATA_TYPE_PACK)
         | 
| 108 | 
            +
                      @datatype = Datacode.new(@datatype, MTP::DATA_TYPES)
         | 
| 109 | 
            +
                      @default_value, group_code, form_flag, form =
         | 
| 110 | 
            +
                        str.unpack("#{@dts.name}VCa*")
         | 
| 111 | 
            +
                      @form = Forms.load(form_flag, form, @dts)
         | 
| 112 | 
            +
                    end
         | 
| 113 | 
            +
                    prop_desc
         | 
| 114 | 
            +
                  end
         | 
| 115 | 
            +
                  
         | 
| 116 | 
            +
                  def unpack(raw_value)
         | 
| 117 | 
            +
                    if raw_value.empty?
         | 
| 118 | 
            +
                      nil
         | 
| 119 | 
            +
                    else
         | 
| 120 | 
            +
                      @form.unpack(@datatype, @dts, raw_value)
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
                  end
         | 
| 123 | 
            +
                  
         | 
| 124 | 
            +
                  def pack(value)
         | 
| 125 | 
            +
                    @form.pack(@datatype, @dts, value)
         | 
| 126 | 
            +
                  end
         | 
| 127 | 
            +
                  
         | 
| 128 | 
            +
                  def writable?
         | 
| 129 | 
            +
                    @access == 1
         | 
| 130 | 
            +
                  end
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
                
         | 
| 133 | 
            +
                module Forms
         | 
| 134 | 
            +
                  def self.load(form_flag, form, dts)
         | 
| 135 | 
            +
                    FORM_FLAGS[form_flag].new(form, dts)
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
                  
         | 
| 138 | 
            +
                  class None
         | 
| 139 | 
            +
                    def initialize(form, dts)
         | 
| 140 | 
            +
                    end
         | 
| 141 | 
            +
                    
         | 
| 142 | 
            +
                    def unpack(datatype, dts, raw_value)
         | 
| 143 | 
            +
                      if datatype.name == "String"
         | 
| 144 | 
            +
                        raw_value.unpack("J").first
         | 
| 145 | 
            +
                      elsif (datatype & 0x4000) == 0x4000
         | 
| 146 | 
            +
                        raw_value.unpack("#{dts}+").first
         | 
| 147 | 
            +
                      else
         | 
| 148 | 
            +
                        raw_value.unpack("#{dts}").first
         | 
| 149 | 
            +
                      end
         | 
| 150 | 
            +
                    end
         | 
| 151 | 
            +
                    
         | 
| 152 | 
            +
                    def pack(datatype, dts, value)
         | 
| 153 | 
            +
                      if datatype.name == "String"
         | 
| 154 | 
            +
                        [ value ].pack("J")
         | 
| 155 | 
            +
                      elsif (datatype & 0x4000) == 0x4000
         | 
| 156 | 
            +
                        [ value ].pack("#{dts}+")
         | 
| 157 | 
            +
                      else
         | 
| 158 | 
            +
                        [ value ].pack("#{dts}")
         | 
| 159 | 
            +
                      end
         | 
| 160 | 
            +
                    end
         | 
| 161 | 
            +
                  end
         | 
| 162 | 
            +
                    
         | 
| 163 | 
            +
                  class Range < None
         | 
| 164 | 
            +
                    attr_reader :minimum_value, :maximum_value, :step_size
         | 
| 165 | 
            +
                    def initialize(form, dts)
         | 
| 166 | 
            +
                      @minimum_value, @maximum_value, @step_size = form.unpack(dts.name * 3)
         | 
| 167 | 
            +
                    end
         | 
| 168 | 
            +
                  end
         | 
| 169 | 
            +
                  
         | 
| 170 | 
            +
                  class Enumeration < None
         | 
| 171 | 
            +
                    attr_reader :values
         | 
| 172 | 
            +
                    def initialize(form, dts)
         | 
| 173 | 
            +
                      num, *@values = form.unpack("S#{dts.name}*")
         | 
| 174 | 
            +
                    end
         | 
| 175 | 
            +
                  end
         | 
| 176 | 
            +
                  
         | 
| 177 | 
            +
                  class DateTime < None
         | 
| 178 | 
            +
                    def initialize(form, dts)
         | 
| 179 | 
            +
                    end
         | 
| 180 | 
            +
                    
         | 
| 181 | 
            +
                    def unpack(datatype, dts, raw_value)
         | 
| 182 | 
            +
                      raw_value.unpack("D").first
         | 
| 183 | 
            +
                    end
         | 
| 184 | 
            +
                    
         | 
| 185 | 
            +
                    def pack(datatype, dts, value)
         | 
| 186 | 
            +
                      [ value ].pack("D")
         | 
| 187 | 
            +
                    end
         | 
| 188 | 
            +
                  end
         | 
| 189 | 
            +
                  
         | 
| 190 | 
            +
                  class FixedLengthArray < None
         | 
| 191 | 
            +
                    attr_reader :size
         | 
| 192 | 
            +
                    def initialize(form, dts)
         | 
| 193 | 
            +
                      @size = form.unpack("S")
         | 
| 194 | 
            +
                    end
         | 
| 195 | 
            +
                  end
         | 
| 196 | 
            +
                  
         | 
| 197 | 
            +
                  class RegularExpression < None
         | 
| 198 | 
            +
                    attr_reader :regexp
         | 
| 199 | 
            +
                    def initialize(form, dts)
         | 
| 200 | 
            +
                      @regexp = Regexp.new(form.unpack("J")) unless form.empty?
         | 
| 201 | 
            +
                    end
         | 
| 202 | 
            +
                  end
         | 
| 203 | 
            +
                  
         | 
| 204 | 
            +
                  class ByteArray < None
         | 
| 205 | 
            +
                    attr_reader :max_length
         | 
| 206 | 
            +
                    def initialize(form, dts)
         | 
| 207 | 
            +
                      @max_length = form.unpack("S")
         | 
| 208 | 
            +
                    end
         | 
| 209 | 
            +
                  end
         | 
| 210 | 
            +
                  
         | 
| 211 | 
            +
                  class LongString < None
         | 
| 212 | 
            +
                    attr_reader :max_length
         | 
| 213 | 
            +
                    def initialize(form, dts)
         | 
| 214 | 
            +
                      @max_length = form.unpack("S")
         | 
| 215 | 
            +
                    end
         | 
| 216 | 
            +
                  end
         | 
| 217 | 
            +
                  
         | 
| 218 | 
            +
                  FORM_FLAGS = { 
         | 
| 219 | 
            +
                    0x00 => None, 0x01 => Range, 0x02 => Enumeration, 0x03 => DateTime,
         | 
| 220 | 
            +
                    0x04 => FixedLengthArray, 0x05 => RegularExpression, 0x06 => ByteArray, 0xFF => String
         | 
| 221 | 
            +
                  }
         | 
| 222 | 
            +
                end
         | 
| 223 | 
            +
              end
         | 
| 224 | 
            +
              
         | 
| 225 | 
            +
             | 
| 226 | 
            +
            end
         |