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.
@@ -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
@@ -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
@@ -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