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
@@ -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
|
data/lib/mtp/device.rb
ADDED
@@ -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
|
data/lib/mtp/fuse.rb
ADDED
@@ -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
|