rubymtp 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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