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/protocol.rb
ADDED
@@ -0,0 +1,298 @@
|
|
1
|
+
require 'facet/string/camelize'
|
2
|
+
|
3
|
+
module MTP
|
4
|
+
class Protocol
|
5
|
+
|
6
|
+
@@logger = Logger.new(STDERR)
|
7
|
+
@@logger.level = Logger::INFO
|
8
|
+
|
9
|
+
def self.logger
|
10
|
+
@@logger
|
11
|
+
end
|
12
|
+
|
13
|
+
class EndPoints
|
14
|
+
attr_reader :in, :out, :interrupt
|
15
|
+
def initialize(protocol)
|
16
|
+
protocol.interface.endpoints.each do |ep|
|
17
|
+
if (ep.bEndpointAddress & 0b10000000).zero?
|
18
|
+
@out = ep
|
19
|
+
else
|
20
|
+
if ep.bmAttributes == 0x02
|
21
|
+
@in = ep
|
22
|
+
elsif ep.bmAttributes == 0x03
|
23
|
+
@interrupt = ep
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Transaction
|
31
|
+
attr_accessor :request, :data, :io
|
32
|
+
attr_reader :response
|
33
|
+
def initialize(ph)
|
34
|
+
@ph = ph
|
35
|
+
@io = io
|
36
|
+
end
|
37
|
+
|
38
|
+
def io?
|
39
|
+
!@io.nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
def send_data(&block)
|
43
|
+
@data.transaction_id = @request.transaction_id
|
44
|
+
@data.code = @request.code
|
45
|
+
|
46
|
+
if @ph.split_data_packets?
|
47
|
+
Protocol.logger.debug(sprintf("=> %-8s: 0x%08x, 0x%04x - %s", "DATA", @data.transaction_id, @data.code, @data.code.name))
|
48
|
+
@ph.raw_send(@data.pack_header)
|
49
|
+
if io?
|
50
|
+
@ph.raw_send_io(@io, @data.size, &block)
|
51
|
+
else
|
52
|
+
@ph.raw_send(@data.pack_payload, &block)
|
53
|
+
end
|
54
|
+
else
|
55
|
+
@ph.send(@data)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def read_data(&block)
|
60
|
+
@data = @response
|
61
|
+
if @ph.split_data_packets?
|
62
|
+
if io?
|
63
|
+
@data.payload = @ph.raw_read_io(@io, @data.length - 12, &block)
|
64
|
+
else
|
65
|
+
@data.payload = @ph.raw_read
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def execute(&block)
|
71
|
+
@ph.send(@request)
|
72
|
+
send_data(&block) unless @data.nil?
|
73
|
+
@response = @ph.receive
|
74
|
+
if @response.is_a?(Data)
|
75
|
+
read_data(&block)
|
76
|
+
@response = @ph.receive
|
77
|
+
end
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def expect(*expects)
|
82
|
+
unless expects.empty? or @response.one_of?(*expects)
|
83
|
+
raise CommandError.new(@ph, @request, @response)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def method_missing(method, *args, &block)
|
88
|
+
method = method.to_s
|
89
|
+
if method.match(/^(.*)_with_io$/)
|
90
|
+
method = $1
|
91
|
+
@io = args.shift
|
92
|
+
end
|
93
|
+
@request = Request.for(method.to_s.camelize, *args)
|
94
|
+
execute(&block)
|
95
|
+
self
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.mtp?(usb_device, timeout = 1000)
|
100
|
+
ret = true
|
101
|
+
usb_device.open do |usb|
|
102
|
+
# check for MS OS descriptors
|
103
|
+
mod = usb.get_string_simple(238)
|
104
|
+
|
105
|
+
if mod.nil? or mod[0,4] != "MSFT"
|
106
|
+
logger.debug("Microsoft OS Descriptor invalid")
|
107
|
+
ret = false
|
108
|
+
break
|
109
|
+
end
|
110
|
+
bVendorCode = mod[7]
|
111
|
+
result = "\0" * 0x20
|
112
|
+
expd = usb.usb_control_msg(USB::USB_ENDPOINT_IN|USB::USB_RECIP_DEVICE|USB::USB_TYPE_VENDOR, bVendorCode, 0, 4, result, timeout)
|
113
|
+
unless expd > 0x15 and result[0x12,3] == "MTP"
|
114
|
+
logger.debug("Microsoft Extended Descriptor invalid")
|
115
|
+
ret = false
|
116
|
+
break
|
117
|
+
end
|
118
|
+
|
119
|
+
result = "\0" * 0x20
|
120
|
+
expd = usb.usb_control_msg(USB::USB_ENDPOINT_IN|USB::USB_RECIP_DEVICE|USB::USB_TYPE_VENDOR, bVendorCode, 0, 5, result, timeout)
|
121
|
+
unless expd > 0x15 and result[0x12,3] == "MTP"
|
122
|
+
logger.debug("Microsoft Extended Descriptor invalid")
|
123
|
+
ret = false
|
124
|
+
break
|
125
|
+
end
|
126
|
+
end
|
127
|
+
ret
|
128
|
+
end
|
129
|
+
|
130
|
+
def logger
|
131
|
+
Protocol.logger
|
132
|
+
end
|
133
|
+
|
134
|
+
attr_reader :device
|
135
|
+
def initialize(device, usb_device)
|
136
|
+
@device = device
|
137
|
+
@transaction_id = 0
|
138
|
+
@split_data_packets = false
|
139
|
+
@usb_handle = nil
|
140
|
+
@usb_device = usb_device
|
141
|
+
@timeout = 10000
|
142
|
+
logger.info("new device #{@usb_device.product}")
|
143
|
+
end
|
144
|
+
|
145
|
+
def split_data_packets?
|
146
|
+
@split_data_packets
|
147
|
+
end
|
148
|
+
|
149
|
+
def configuration
|
150
|
+
@usb_device.configurations.first
|
151
|
+
end
|
152
|
+
|
153
|
+
def interface
|
154
|
+
@usb_device.interfaces.first
|
155
|
+
end
|
156
|
+
|
157
|
+
def transaction_id
|
158
|
+
tid = @transaction_id
|
159
|
+
@transaction_id = (tid % 0xffffffff) + 1
|
160
|
+
tid
|
161
|
+
end
|
162
|
+
|
163
|
+
def raw_send(raw_packet, &block)
|
164
|
+
raw_packet = raw_packet.clone
|
165
|
+
if (raw_packet.length % @end_points.out.wMaxPacketSize).zero?
|
166
|
+
send_empty_packet = true
|
167
|
+
logger.warn("packet match max packet size, need to send NULL packet")
|
168
|
+
end
|
169
|
+
written, total = 0, raw_packet.length
|
170
|
+
until raw_packet.empty?
|
171
|
+
if (wrt = @usb_handle.usb_bulk_write(@end_points.out.bEndpointAddress,
|
172
|
+
raw_packet.slice!(0, @end_points.out.wMaxPacketSize), @timeout)) < 0
|
173
|
+
raise WriteError.new(self)
|
174
|
+
end
|
175
|
+
written += wrt
|
176
|
+
yield written, total if block_given?
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def raw_send_io(io, size, &block)
|
181
|
+
packet_size = @end_points.out.wMaxPacketSize
|
182
|
+
written = 0
|
183
|
+
until (str = io.read(packet_size)).nil?
|
184
|
+
if (wrt = @usb_handle.usb_bulk_write(@end_points.out.bEndpointAddress, str, @timeout)) < 0
|
185
|
+
raise WriteError.new(self)
|
186
|
+
end
|
187
|
+
written += wrt
|
188
|
+
yield written, size if block_given?
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def send(request)
|
193
|
+
unless request.is_a?(Data) or request.code.name == "GetDeviceInfo"
|
194
|
+
raise UnsupportedRequest.new(self, request) unless @device.support?(request.code)
|
195
|
+
request.transaction_id = transaction_id
|
196
|
+
end
|
197
|
+
|
198
|
+
raw_send(request.pack)
|
199
|
+
|
200
|
+
logger.debug(sprintf("=> %-8s: 0x%08x, 0x%04x - %s", (request.is_a?(Data) ? "DATA" : "REQUEST"), request.transaction_id, request.code, request.code.name))
|
201
|
+
request
|
202
|
+
end
|
203
|
+
|
204
|
+
def raw_read
|
205
|
+
read = packet_size = @end_points.in.wMaxPacketSize
|
206
|
+
raw_packet = ""
|
207
|
+
|
208
|
+
# packet is received when we receive an emtpy packet
|
209
|
+
# or a packet whose size is smaller than the packet size
|
210
|
+
while read == packet_size
|
211
|
+
buffer = "\0" * packet_size
|
212
|
+
read = @usb_handle.usb_bulk_read(@end_points.in.bEndpointAddress, buffer, @timeout)
|
213
|
+
if read.zero?
|
214
|
+
logger.warn("packet match max packet size, need to send NULL packet")
|
215
|
+
end
|
216
|
+
raw_packet << buffer[0, read]
|
217
|
+
end
|
218
|
+
raw_packet
|
219
|
+
end
|
220
|
+
|
221
|
+
def raw_read_io(io, size, &block)
|
222
|
+
read = packet_size = @end_points.in.wMaxPacketSize
|
223
|
+
|
224
|
+
total = 0
|
225
|
+
# packet is received when we receive an emtpy packet
|
226
|
+
# or a packet whose size is smaller than the packet size
|
227
|
+
while read == packet_size
|
228
|
+
buffer = "\0" * packet_size
|
229
|
+
read = @usb_handle.usb_bulk_read(@end_points.in.bEndpointAddress, buffer, @timeout)
|
230
|
+
if read.zero?
|
231
|
+
logger.warn("packet match max packet size, need to send NULL packet")
|
232
|
+
end
|
233
|
+
io.write(buffer[0, read])
|
234
|
+
total += read
|
235
|
+
yield total, size if block_given?
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def receive
|
240
|
+
raw_packet = raw_read
|
241
|
+
response = Container.parse(raw_packet)
|
242
|
+
logger.debug(sprintf("<= %-8s: 0x%08x, 0x%04x - %s", (response.is_a?(Data) ? "DATA" : "RESPONSE"),
|
243
|
+
response.transaction_id, response.code, response.code.name))
|
244
|
+
response
|
245
|
+
end
|
246
|
+
|
247
|
+
# do not use normal procedure as we need to determine if we use
|
248
|
+
# splitted header / data
|
249
|
+
def info
|
250
|
+
request = Request.for("GetDeviceInfo")
|
251
|
+
request.transaction_id = 0
|
252
|
+
send(request)
|
253
|
+
data = receive
|
254
|
+
|
255
|
+
raise CommandError.new(self, request, data, "expected a data packet") unless data.is_a?(Data)
|
256
|
+
|
257
|
+
if data.payload.empty?
|
258
|
+
logger.debug("device split packet headers from payload")
|
259
|
+
@split_data_packets = true
|
260
|
+
data.payload = raw_read
|
261
|
+
end
|
262
|
+
response = receive
|
263
|
+
yield data if block_given?
|
264
|
+
end
|
265
|
+
|
266
|
+
def open
|
267
|
+
logger.debug("low level open")
|
268
|
+
@usb_handle = @usb_device.usb_open
|
269
|
+
begin
|
270
|
+
@usb_handle.set_configuration(configuration)
|
271
|
+
@usb_handle.claim_interface(interface.settings.first)
|
272
|
+
@end_points = EndPoints.new(self)
|
273
|
+
rescue Exception => e
|
274
|
+
close
|
275
|
+
end
|
276
|
+
self
|
277
|
+
end
|
278
|
+
|
279
|
+
def reset_end_points
|
280
|
+
@usb_handle.usb_clear_halt(@end_points.out.bEndpointAddress)
|
281
|
+
@usb_handle.usb_clear_halt(@end_points.in.bEndpointAddress)
|
282
|
+
@usb_handle.usb_clear_halt(@end_points.interrupt.bEndpointAddress)
|
283
|
+
end
|
284
|
+
|
285
|
+
def close
|
286
|
+
logger.debug("low level close")
|
287
|
+
reset_end_points
|
288
|
+
@usb_handle.release_interface(interface.settings.first)
|
289
|
+
@usb_handle.usb_reset
|
290
|
+
@usb_handle.usb_close
|
291
|
+
end
|
292
|
+
|
293
|
+
def transaction
|
294
|
+
Transaction.new(self)
|
295
|
+
end
|
296
|
+
|
297
|
+
end
|
298
|
+
end
|
data/lib/mtp/storage.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module MTP
|
2
|
+
class Storage
|
3
|
+
attr_reader(:id, :type, :file_system, :access_capability, :max_capability, :free_space_in_bytes, :free_space_in_objects,
|
4
|
+
:description, :volume_label)
|
5
|
+
|
6
|
+
TYPES = {
|
7
|
+
0x0000 => "Undefined", 0x0001 => "Fixed ROM", 0x0002 => "Removable ROM",
|
8
|
+
0x0003 => "Fixed RAM", 0x0004 => "Removable RAM"
|
9
|
+
}
|
10
|
+
|
11
|
+
FILE_SYSTEMS = {
|
12
|
+
0x0000 => "Undefined", 0x0001 => "Generic Flat", 0x0002 => "Generic Hierarchical", 0x0003 => "DCF"
|
13
|
+
}
|
14
|
+
|
15
|
+
|
16
|
+
def self.load(ph, id)
|
17
|
+
storage = Storage.new
|
18
|
+
t = ph.transaction.get_storage_info(id)
|
19
|
+
t.expect("OK")
|
20
|
+
storage.instance_eval do
|
21
|
+
@id = id
|
22
|
+
@type, @file_system, @access_capability, @max_capability, @free_space_in_bytes,
|
23
|
+
@free_space_in_objects, @description, @volume_label =
|
24
|
+
t.data.payload.unpack("SSSQQIJJ")
|
25
|
+
@type = Datacode.new(@type, TYPES)
|
26
|
+
@file_system = Datacode.new(@file_system, FILE_SYSTEMS)
|
27
|
+
end
|
28
|
+
storage
|
29
|
+
end
|
30
|
+
|
31
|
+
def type_description
|
32
|
+
TYPES[@type]
|
33
|
+
end
|
34
|
+
|
35
|
+
def file_system_description
|
36
|
+
FILE_SYSTEMS[@file_system]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/mtp/track.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'mp3info'
|
2
|
+
|
3
|
+
module MTP
|
4
|
+
class Track < Object
|
5
|
+
end
|
6
|
+
|
7
|
+
class MP3Track < Track
|
8
|
+
register_format self, Datacode.new("MP3")
|
9
|
+
property_writer :artist, :track, :genre, :original_release_date, :name, :album_name
|
10
|
+
property_accessor :duration
|
11
|
+
|
12
|
+
def initialize(localname = nil)
|
13
|
+
super()
|
14
|
+
self.format = "MP3"
|
15
|
+
self.localname = localname unless localname.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def load(ph, id, payload)
|
19
|
+
super(ph, id, payload)
|
20
|
+
end
|
21
|
+
|
22
|
+
# read track data and info from a local file
|
23
|
+
def localname=(localname)
|
24
|
+
@localname = localname
|
25
|
+
@info = Mp3Info.new(localname)
|
26
|
+
@info.close
|
27
|
+
self.filename = File.basename(@localname)
|
28
|
+
self.compressed_size = File.size(@localname)
|
29
|
+
end
|
30
|
+
|
31
|
+
def artist
|
32
|
+
new_object? ? @info.tag.artist : properties.artist
|
33
|
+
end
|
34
|
+
|
35
|
+
def name
|
36
|
+
new_object? ? @info.tag.title : properties.name
|
37
|
+
end
|
38
|
+
|
39
|
+
def track
|
40
|
+
new_object? ? @info.tag.tracknum : properties.track
|
41
|
+
end
|
42
|
+
|
43
|
+
def genre
|
44
|
+
new_object? ? @info.tag.genre_s : properties.genre
|
45
|
+
end
|
46
|
+
|
47
|
+
def original_release_date
|
48
|
+
new_object? ? Time.local(@info.tag.year) : properties.original_release_date
|
49
|
+
end
|
50
|
+
|
51
|
+
def album_name
|
52
|
+
new_object? ? @info.tag.album : properties.album_name
|
53
|
+
end
|
54
|
+
|
55
|
+
def inspect
|
56
|
+
"#<MTP::MP3Track:0x%08x @parent_id=0x%08x @artist=%s @track=%u @name=%s @genre=%s @album_name%s>" %
|
57
|
+
[ @id, @parent_id, artist.inspect, track, name, genre.inspect, album_name.inspect]
|
58
|
+
end
|
59
|
+
|
60
|
+
def set_properties_from_mp3info(info)
|
61
|
+
self.artist = info.tag.artist
|
62
|
+
self.name = info.tag.title
|
63
|
+
self.track = info.tag.tracknum
|
64
|
+
self.genre = info.tag.genre_s
|
65
|
+
self.original_release_date = Time.local(info.tag.year) unless info.tag.year.nil?
|
66
|
+
self.album_name = info.tag.album
|
67
|
+
end
|
68
|
+
|
69
|
+
def after_sending
|
70
|
+
super
|
71
|
+
set_properties_from_mp3info(@info) unless @info.nil?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
diff -ur ruby-mp3info-0.5/lib/mp3info.rb /var/lib/gems/1.8/gems/ruby-mp3info-0.5/lib/mp3info.rb
|
2
|
+
--- ruby-mp3info-0.5/lib/mp3info.rb 2005-12-05 23:30:22.000000000 +0100
|
3
|
+
+++ /var/lib/gems/1.8/gems/ruby-mp3info-0.5/lib/mp3info.rb 2007-02-14 13:48:48.000000000 +0100
|
4
|
+
@@ -155,8 +155,6 @@
|
5
|
+
# Instantiate a new Mp3Info object with name +filename+
|
6
|
+
def initialize(filename)
|
7
|
+
$stderr.puts("#{self.class}::new() does not take block; use #{self.class}::open() instead") if block_given?
|
8
|
+
- raise(Mp3InfoError, "empty file") unless File.stat(filename).size? #FIXME
|
9
|
+
- @filename = filename
|
10
|
+
@hastag1 = false
|
11
|
+
|
12
|
+
@tag1 = {}
|
13
|
+
@@ -164,7 +162,15 @@
|
14
|
+
|
15
|
+
@tag2 = ID3v2.new
|
16
|
+
|
17
|
+
- @file = File.new(filename, "rb")
|
18
|
+
+ if filename.is_a?(String)
|
19
|
+
+ raise(Mp3InfoError, "empty file") unless File.stat(filename).size? #FIXME
|
20
|
+
+ @filename = filename
|
21
|
+
+ @file = File.new(filename, "rb")
|
22
|
+
+ @is_io = false
|
23
|
+
+ else
|
24
|
+
+ @is_io = true
|
25
|
+
+ @file = filename
|
26
|
+
+ end
|
27
|
+
@file.extend(Mp3FileMethods)
|
28
|
+
|
29
|
+
begin
|
30
|
+
@@ -245,7 +251,8 @@
|
31
|
+
@vbr = true
|
32
|
+
else
|
33
|
+
# for cbr, calculate duration with the given bitrate
|
34
|
+
- @streamsize = @file.stat.size - (@hastag1 ? TAGSIZE : 0) - (@tag2.valid? ? @tag2.io_position : 0)
|
35
|
+
+ size = (@file.respond_to?(:stat) ? @file.stat.size : @file.size)
|
36
|
+
+ @streamsize = size - (@hastag1 ? TAGSIZE : 0) - (@tag2.valid? ? @tag2.io_position : 0)
|
37
|
+
@length = ((@streamsize << 3)/1000.0)/@bitrate
|
38
|
+
if @tag2["TLEN"]
|
39
|
+
# but if another duration is given and it isn't close (within 5%)
|
40
|
+
@@ -313,7 +320,7 @@
|
41
|
+
|
42
|
+
# write to another filename at close()
|
43
|
+
def rename(new_filename)
|
44
|
+
- @filename = new_filename
|
45
|
+
+ @filename = new_filename unless @is_io
|
46
|
+
end
|
47
|
+
|
48
|
+
# Flush pending modifications to tags and close the file
|
49
|
+
@@ -332,7 +339,7 @@
|
50
|
+
|
51
|
+
if @tag1 != @tag1_orig
|
52
|
+
puts "@tag1 has changed" if $DEBUG
|
53
|
+
- raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename)
|
54
|
+
+ raise(Mp3InfoError, "file is not writable") if @is_io or !File.writable?(@filename)
|
55
|
+
@tag1_orig.update(@tag1)
|
56
|
+
#puts "@tag1_orig: #{@tag1_orig.inspect}"
|
57
|
+
File.open(@filename, 'rb+') do |file|
|
58
|
+
@@ -405,7 +412,7 @@
|
59
|
+
|
60
|
+
### parses the id3 tags of the currently open @file
|
61
|
+
def parse_tags
|
62
|
+
- return if @file.stat.size < TAGSIZE # file is too small
|
63
|
+
+ #return if @file.stat.size < TAGSIZE # file is too small
|
64
|
+
@file.seek(0)
|
65
|
+
f3 = @file.read(3)
|
66
|
+
gettag1 if f3 == "TAG" # v1 tag at beginning
|
67
|
+
@@ -467,7 +474,12 @@
|
68
|
+
# is a id3v2 tag.
|
69
|
+
|
70
|
+
#dummyproof = @file.stat.size - @file.pos => WAS TOO MUCH
|
71
|
+
- dummyproof = [ @file.stat.size - @file.pos, 2000000 ].min
|
72
|
+
+ if @file.respond_to?(:stat)
|
73
|
+
+ size = @file.stat.size
|
74
|
+
+ else
|
75
|
+
+ size = @file.size
|
76
|
+
+ end
|
77
|
+
+ dummyproof = [ size - @file.pos, 2000000 ].min
|
78
|
+
dummyproof.times do |i|
|
79
|
+
if @file.getc == 0xff
|
80
|
+
data = @file.read(3)
|