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,51 @@
1
+ module MTP
2
+ DATA_TYPES = {
3
+ 0x0000 => "Undefined",
4
+ 0x0001 => "INT8",
5
+ 0x0002 => "UINT8",
6
+ 0x0003 => "INT16",
7
+ 0x0004 => "UINT16",
8
+ 0x0005 => "INT32",
9
+ 0x0006 => "UINT32",
10
+ 0x0007 => "INT64",
11
+ 0x0008 => "UINT64",
12
+ 0x0009 => "INT128",
13
+ 0x000A => "UINT128",
14
+ 0x4001 => "AINT8",
15
+ 0x4002 => "AUINT8",
16
+ 0x4003 => "AINT16",
17
+ 0x4004 => "AUINT16",
18
+ 0x4005 => "AINT32",
19
+ 0x4006 => "AUINT32",
20
+ 0x4007 => "AINT64",
21
+ 0x4008 => "AUINT64",
22
+ 0x4009 => "AINT128",
23
+ 0x400A => "AUINT128",
24
+ 0xFFFF => "String"
25
+ }
26
+
27
+ DATA_TYPE_PACK = {
28
+ 0x0000 => "",
29
+ 0x0001 => "c",
30
+ 0x0002 => "C",
31
+ 0x0003 => "s",
32
+ 0x0004 => "S",
33
+ 0x0005 => "v",
34
+ 0x0006 => "V",
35
+ 0x0007 => "q",
36
+ 0x0008 => "Q",
37
+ 0x0009 => "y",
38
+ 0x000A => "Y",
39
+ 0x4001 => "c+",
40
+ 0x4002 => "C+",
41
+ 0x4003 => "s+",
42
+ 0x4004 => "S+",
43
+ 0x4005 => "v+",
44
+ 0x4006 => "V+",
45
+ 0x4007 => "q+",
46
+ 0x4008 => "Q+",
47
+ 0x4009 => "y+",
48
+ 0x400A => "Y+",
49
+ 0xFFFF => "J"
50
+ }
51
+ end
@@ -0,0 +1,270 @@
1
+ module MTP
2
+ class Device
3
+ class << self
4
+
5
+ # discover connected usb devices
6
+ def find
7
+ ary = Array.new
8
+ USB.devices.find_all do |usbdev|
9
+ ary << Device.new(usbdev) if Protocol.mtp?(usbdev)
10
+ end
11
+ ary
12
+ end
13
+ end
14
+
15
+ FUNCTIONAL_MODES = { 0x0000 => "Undefined", 0x0001 => "Sleep state",
16
+ 0xC001 => "Non-responsive playback", 0xC002 => "Responsive playback" }
17
+
18
+ attr_reader :manufacturer, :model
19
+
20
+ private
21
+ # create a new MTP device from the corresponding USB device
22
+ def initialize(usb_device)
23
+ raise WrongDeviceType.new(self) unless Protocol.mtp?(usb_device)
24
+ @ph = Protocol.new(self, usb_device)
25
+ info
26
+ end
27
+
28
+ # parse the device info data set
29
+ def info
30
+ begin
31
+ @ph.open
32
+ @ph.info do |data|
33
+ @standard_version, @mtp_vendor_extension_id, @mtp_version, @mtp_extensions,
34
+ @functional_mode, @operations_supported, @events_supported, @device_properties_supported,
35
+ @capture_formats, @playback_formats, @manufacturer, @model, @device_version, @serial_number =
36
+ data.payload.unpack("SVSJSK+S+S+K+K+JJJJ")
37
+ end
38
+ ensure
39
+ @ph.close
40
+ end
41
+ #dump
42
+ self
43
+ end
44
+
45
+ # dump device info
46
+ def dump
47
+ puts "Device info:"
48
+ puts " Manufacturer: #{@manufacturer}"
49
+ puts " Model: #{@model}"
50
+ puts " Device version: #{@device_version}"
51
+ puts " Serial number: #{@serial_number}"
52
+ puts " Standard version: #{@standard_version}"
53
+ puts " Vendor extension description: #{@mtp_extensions}"
54
+ puts sprintf(" Vendor extension ID: 0x%08x", @mtp_vendor_extension_id)
55
+ puts " Functional mode: " + FUNCTIONAL_MODES[@functional_mode]
56
+ puts "Supported operations"
57
+ @operations_supported.each { |o| puts sprintf(" 0x%04x: %s", o, o.name) }
58
+ puts "Supported capture formats"
59
+ @capture_formats.each { |o| puts sprintf(" 0x%04x: %s", o, o.name) }
60
+ puts "Supported playback formats"
61
+ @playback_formats.each { |o| puts sprintf(" 0x%04x: %s", o, o.name) }
62
+ end
63
+
64
+ public
65
+ def inspect #:nodoc:
66
+ "#<MTP::Device manufacturer=%s model=%s>" % [ @manufacturer.inspect, @model.inspect ]
67
+ end
68
+
69
+ # check if the device support a particular request
70
+ def support?(request)
71
+ code = Datacode.new(request)
72
+ return @operations_supported.include?(code)
73
+ end
74
+
75
+ # open the device
76
+ #
77
+ # If a block is given, the library will ensure that the device is
78
+ # correctly closed outside of the block, even if an exception is
79
+ # raised in it.
80
+ def open
81
+ MTP.logger.info("opening session")
82
+ @ph.open
83
+ begin
84
+ t = @ph.transaction.open_session(1)
85
+ unless t.response.one_of?("OK", "SessionAlreadyOpened")
86
+ reset
87
+ t = @ph.transaction.open_session(1)
88
+ end
89
+ rescue Exception => e
90
+ @ph.close
91
+ raise e
92
+ end
93
+
94
+ if block_given?
95
+ begin
96
+ yield self
97
+ ensure
98
+ begin
99
+ close
100
+ rescue Exception => e
101
+ MTP.logger.error(e)
102
+ end
103
+ end
104
+ else
105
+ self
106
+ end
107
+ end
108
+
109
+ # close the device
110
+ def close
111
+ MTP.logger.info("closing session")
112
+ begin
113
+ @ph.transaction.close_session.expect("OK")
114
+ ensure
115
+ @ph.close
116
+ end
117
+ end
118
+
119
+ # reset the device (this will close all sessions
120
+ def reset
121
+ MTP.logger.info("reseting device")
122
+ @ph.transaction.reset_device.expect("OK", "SessionNotOpen")
123
+ end
124
+
125
+ # clear cached data
126
+ def clear
127
+ @storages = @objects = @objects_by_filename = @tracks = @playlists = nil
128
+ end
129
+
130
+ # return the available storages on the device
131
+ def storages
132
+ return @storages unless @storages.nil?
133
+
134
+ MTP.logger.info("fetching storages")
135
+ t = @ph.transaction.get_storage_ids
136
+ t.expect("OK")
137
+ num, *ids = t.data.payload.unpack("I*")
138
+ @storages = ids.map { |id| Storage.load(@ph, id) }
139
+ end
140
+
141
+ # return the MTP objects stored on the device
142
+ def objects
143
+ return @objects.values unless @objects.nil?
144
+
145
+ MTP.logger.info("fetching objects")
146
+ @objects = {}
147
+ @objects_by_filename = {}
148
+
149
+ # fetch on each storage
150
+ storages.each do |storage|
151
+ t = @ph.transaction.get_object_handles(storage.id, 0, 0)
152
+ t.expect("OK")
153
+
154
+ num, *objects = t.data.payload.unpack("I*")
155
+
156
+ MTP.logger.info("retrieving #{num} objects")
157
+ objects.each do |id|
158
+ o = @objects[id] = Object.load(@ph, id, storage)
159
+ @objects_by_filename[o.filename] = o
160
+ end
161
+ end
162
+ @objects.values
163
+ end
164
+
165
+ # call-seq:
166
+ # device[id] => object
167
+ # device[filename] => object
168
+ #
169
+ # retrieve an object from the device by its id or filename.
170
+ def [](id)
171
+ objects if @objects.nil?
172
+ if id.is_a?(String)
173
+ @objects_by_filename[id]
174
+ else
175
+ @objects[id]
176
+ end
177
+ end
178
+
179
+ # call-seq:
180
+ # device.send(object) => object
181
+ # device.send(object, io) => object
182
+ # device.send(object, io) { |written, total| ... } => object
183
+ #
184
+ # send object to the device
185
+ #
186
+ # If the io argument is given, the binary data to be send to the
187
+ # device will be read from it. If a block is given, each time a
188
+ # packet is send, it will be called with the number of bytes
189
+ # written and the total number of bytes to write.
190
+ def send(object, io = nil, &block)
191
+ raise InvalidObject.new(object) unless object.valid?
192
+
193
+ MTP.logger.info("sending object #{object}")
194
+ t = @ph.transaction
195
+ t.data = Data.new(object.pack)
196
+ t.send_object_info(0, 0)
197
+ t.expect("OK")
198
+ storage_id, parent_id, object_id = *t.response.parameters
199
+
200
+ storage = storages.detect { |s| s.id == storage_id }
201
+ object.instance_eval do
202
+ @storage, @parent_id, @id = storage, parent_id, object_id
203
+ end
204
+
205
+ object.before_sending(@ph)
206
+
207
+ t = @ph.transaction
208
+ t.data = object.data
209
+ if io.nil?
210
+ t.send_object(&block)
211
+ else
212
+ t.send_object_with_io(io, &block)
213
+ end
214
+ t.expect("OK")
215
+ object.after_sending
216
+ @objects_by_filename[object.filename] = @objects[object.id] = object
217
+ if object.is_a?(Track)
218
+ tracks << object
219
+ elsif object.is_a?(Playlist)
220
+ playlists << object
221
+ end
222
+ end
223
+
224
+ # call-seq:
225
+ # device.get(object) => object
226
+ # device.get(object, io) => object
227
+ # device.get(object, io) { |read, total| ... } => object
228
+ #
229
+ # retrieve object to the device
230
+ #
231
+ # If the io argument is given, the binary data to be read from the
232
+ # device will be written to it. If a block is given, each time a
233
+ # packet is received, it will be called with the number of bytes
234
+ # read and the total number of bytes to read.
235
+ def get(object, io = nil, &block)
236
+ MTP.logger.info("receiving object #{object}")
237
+ t = @ph.transaction
238
+ if io.nil?
239
+ t.get_object(object.id, &block)
240
+ else
241
+ t.get_object_with_io(io, object.id, &block)
242
+ end
243
+ t.expect("OK")
244
+ return t.data.payload
245
+ end
246
+
247
+ # the list of tracks on the device
248
+ def tracks
249
+ @tracks ||= objects.select { |o| o.is_a?(Track) }
250
+ end
251
+
252
+ # the list of playlists on the device
253
+ def playlists
254
+ @playlists ||= objects.select { |o| o.is_a?(Playlist) }
255
+ end
256
+
257
+ # delete object from the device
258
+ def delete(object)
259
+ MTP.logger.info("deleting object #{object.id}")
260
+ @ph.transaction.delete_object(object.id, 0).expect("OK")
261
+ @objects_by_filename[object.filename] = @objects[object.id] = nil
262
+ if object.is_a?(Track)
263
+ tracks.delete(object)
264
+ elsif object.is_a?(Playlist)
265
+ playlists.delete(object)
266
+ end
267
+ object
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,368 @@
1
+ require 'logger'
2
+ require 'fusefs'
3
+ require 'mtp'
4
+ require 'stringio'
5
+ $DEBUG=1
6
+
7
+ module MTP
8
+ class FileSystem < FuseFS::FuseDir
9
+ @@logger = Logger.new(STDERR)
10
+ @@logger.level = Logger::INFO
11
+
12
+ def self.logger
13
+ @@logger
14
+ end
15
+
16
+ def logger
17
+ @@logger
18
+ end
19
+
20
+ @@roots = {}
21
+ def self.register_root(name, klass)
22
+ @@roots[name] = klass
23
+ end
24
+
25
+ def initialize(device)
26
+ @device = device
27
+ @device.objects
28
+ end
29
+
30
+ def dispatch(method, *args, &block)
31
+ ps = scan_path(args.shift)
32
+
33
+ if ps.empty?
34
+ yield ps, *args
35
+ else
36
+ type = ps.shift
37
+ klass = @@roots[type]
38
+ handler = (klass.nil? ? nil : klass.new(@device))
39
+ if !handler.nil? and handler.respond_to?(method)
40
+ handler.send(method, ps, *args)
41
+ else
42
+ yield ps.unshift(type), *args
43
+ end
44
+ end
45
+ end
46
+
47
+ def directory?(path)
48
+ logger.debug("#{path}: directory?")
49
+ dispatch(:directory?, path) do |path|
50
+ @@roots.keys.include?(path)
51
+ end
52
+ end
53
+
54
+ def file?(path)
55
+ logger.debug("#{path}: file?")
56
+ dispatch(:file?, path) do |path|
57
+ false
58
+ end
59
+ end
60
+
61
+ def executable?(path)
62
+ logger.debug("#{path}: executable?")
63
+ false
64
+ end
65
+
66
+ def size(path)
67
+ logger.debug("#{path}: size")
68
+ dispatch(:size, path) do |path|
69
+ 0
70
+ end
71
+ end
72
+
73
+ def contents(path)
74
+ logger.debug("#{path}: contents")
75
+ dispatch(:contents, path) do |path|
76
+ @@roots.keys
77
+ end
78
+ end
79
+
80
+ def read_file(path)
81
+ dispatch(:read_file, path) do |path|
82
+ track = @device[path.last]
83
+ @device.get(track)
84
+ end
85
+ end
86
+
87
+ def can_write?(path)
88
+ dispatch(:can_write?, path) do |path|
89
+ @device[path.last].nil?
90
+ end
91
+ end
92
+
93
+ def write_to(path, str)
94
+ dispatch(:write_to, path, str) do |path, str|
95
+ track = MTP::MP3Track.new
96
+ track.filename = path.last
97
+ track.compressed_size = str.length
98
+ track.data = MTP::Data.new(str)
99
+ @device.send(track)
100
+ StringIO.open(str, 'r') do |io|
101
+ info = Mp3Info.new(io)
102
+ track.set_properties_from_mp3info(info)
103
+ end
104
+ end
105
+ end
106
+
107
+ def can_delete?(path)
108
+ dispatch(:can_delete?, path) do |path|
109
+ true
110
+ end
111
+ end
112
+
113
+ def delete(path)
114
+ dispatch(:delete, path) do |path|
115
+ @device.delete(@device[path.last])
116
+ end
117
+ end
118
+
119
+ def can_mkdir?(path)
120
+ dispatch(:can_mkdir?, path) do |path|
121
+ false
122
+ end
123
+ end
124
+
125
+ def mkdir(path)
126
+ dispatch(:mkdir, path) do |path|
127
+ false
128
+ end
129
+ end
130
+
131
+ def can_rmdir?(path)
132
+ dispatch(:can_rmdir?, path) do |path|
133
+ false
134
+ end
135
+ end
136
+
137
+ def rmdir(path)
138
+ dispatch(:rmdir, path) do |path|
139
+ end
140
+ end
141
+
142
+ class Albums
143
+ FileSystem.register_root("albums", self)
144
+
145
+ def initialize(device, artist = nil)
146
+ @device = device
147
+ @artist = artist
148
+ end
149
+
150
+ def directory?(path)
151
+ path.size <= 1
152
+ end
153
+
154
+ def file?(path)
155
+ path.size == 2
156
+ end
157
+
158
+ def size(path)
159
+ @device[path.last].compressed_size
160
+ end
161
+
162
+ def can_rmdir?(path)
163
+ !path.empty?
164
+ end
165
+
166
+ def rmdir(path)
167
+ @device.objects.each do |object|
168
+ @device.delete(object) if object.is_a?(Track) and (@artist.nil? or @artist == object.artist) and object.album_name == path.first
169
+ end
170
+ end
171
+
172
+ def contents(path)
173
+ if path.empty?
174
+ if @artist.nil?
175
+ @device.tracks.map { |t| t.album_name }.uniq
176
+ else
177
+ albums = []
178
+ @device.objects.each do |o|
179
+ if o.is_a?(Track) and o.artist == @artist and !albums.include?(o.album_name)
180
+ albums << o.album_name
181
+ end
182
+ end
183
+ albums
184
+ end
185
+ elsif path.size == 1
186
+ @device.objects.select { |o| o.is_a?(Track) and o.album_name == path[0] }.map { |t| t.filename }
187
+ else
188
+ []
189
+ end
190
+ end
191
+ end
192
+
193
+ class Artists
194
+ FileSystem.register_root("artists", self)
195
+
196
+ def initialize(device, genre = nil)
197
+ @device = device
198
+ @genre = genre
199
+ end
200
+
201
+ def dispatch(method, path, &block)
202
+ if path.size > 1 or (path.size == 1 and method == :contents)
203
+ Albums.new(@device, path.shift).send(method, path)
204
+ else
205
+ yield path
206
+ end
207
+ end
208
+
209
+ def directory?(path)
210
+ path.size <= 2
211
+ end
212
+
213
+ def file?(path)
214
+ path.size == 3
215
+ end
216
+
217
+ def size(path)
218
+ @device[path.last].compressed_size
219
+ end
220
+
221
+ def can_rmdir?(path)
222
+ dispatch(:can_rmdir?, path) do |path|
223
+ !path.empty?
224
+ end
225
+ end
226
+
227
+ def rmdir(path)
228
+ dispatch(:rmdir, path) do |path|
229
+ @device.objects.each do |object|
230
+ @device.delete(object) if object.is_a?(Track) and path.first == object.artist
231
+ end
232
+ end
233
+ end
234
+
235
+ def contents(path)
236
+ dispatch(:contents, path) do |path|
237
+ if @genre.nil?
238
+ @device.tracks.map { |t| t.artist }.uniq
239
+ else
240
+ artists = []
241
+ @device.objects.each do |o|
242
+ if o.is_a?(Track) and o.genre == @genre and !artists.include?(o.artist)
243
+ artists << o.artist
244
+ end
245
+ end
246
+ artists
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ class Genres
253
+ FileSystem.register_root("genres", self)
254
+
255
+ def initialize(device)
256
+ @device = device
257
+ end
258
+
259
+ def dispatch(method, path, &block)
260
+ if path.size > 1 or (path.size == 1 and method == :contents)
261
+ Artists.new(@device, path.shift).send(method, path)
262
+ else
263
+ yield path
264
+ end
265
+ end
266
+
267
+ def directory?(path)
268
+ path.size <= 3
269
+ end
270
+
271
+ def file?(path)
272
+ path.size == 4
273
+ end
274
+
275
+ def size(path)
276
+ @device[path.last].compressed_size
277
+ end
278
+
279
+ def can_rmdir?(path)
280
+ dispatch(:can_rmdir?, path) do |path|
281
+ !path.empty?
282
+ end
283
+ end
284
+
285
+ def rmdir(path)
286
+ dispatch(:rmdir, path) do |path|
287
+ @device.objects.each do |object|
288
+ @device.delete(object) if object.is_a?(Track) and path.first == object.genre
289
+ end
290
+ end
291
+ end
292
+
293
+ def contents(path)
294
+ dispatch(:contents, path) do |path|
295
+ @device.tracks.map { |t| t.genre }.uniq
296
+ end
297
+ end
298
+ end
299
+
300
+ class Playlists
301
+ FileSystem.register_root("playlists", Playlists)
302
+
303
+ def initialize(device)
304
+ @device = device
305
+ end
306
+
307
+ def playlist(name)
308
+ @device.playlists.detect { |p| p.name == name }
309
+ end
310
+
311
+ def directory?(path)
312
+ path.size.zero? or (path.size == 1 and !playlist(path.first).nil?)
313
+ end
314
+
315
+ def file?(path)
316
+ path.size == 2
317
+ end
318
+
319
+ def size(path)
320
+ @device[path.last].compressed_size
321
+ end
322
+
323
+ def can_write?(path)
324
+ path.size == 2
325
+ end
326
+
327
+ def write_to(path, str)
328
+ playlist(path.first) << @device[path.last] unless @device[path.last].nil? or playlist.include?(@device[path.last])
329
+ end
330
+
331
+ def can_delete?(path)
332
+ !path.empty?
333
+ end
334
+
335
+ def delete(path)
336
+ playlist(path.first).delete(@device[path.last])
337
+ end
338
+
339
+ def can_mkdir?(path)
340
+ path.size == 1 and playlist(path.first).nil?
341
+ end
342
+
343
+ def mkdir(path)
344
+ playlist = MTP::Playlist.new
345
+ @device.send(playlist)
346
+ playlist.name = path.first
347
+ puts "haha #{playlist.name}"
348
+ end
349
+
350
+ def can_rmdir?(path)
351
+ !path.empty?
352
+ end
353
+
354
+ def rmdir(path)
355
+ @device.delete(playlist(path.first))
356
+ end
357
+
358
+ def contents(path)
359
+ if path.empty?
360
+ @device.playlists.map { |p| p.name }
361
+ else
362
+ playlist(path.first).to_a.map { |o| o.filename }
363
+ end
364
+ end
365
+ end
366
+
367
+ end
368
+ end