pandoru 0.1.0

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,363 @@
1
+ require 'time'
2
+
3
+ module Pandoru
4
+ module Models
5
+ # Simple base model for Pandora API objects
6
+ class Base
7
+ attr_reader :data
8
+
9
+ def self.fields
10
+ @fields ||= {}
11
+ end
12
+
13
+ def self.field(name, json_key = nil, type: nil, &block)
14
+ json_key ||= name.to_s
15
+ fields[name] = { json_key: json_key, type: type, formatter: block }
16
+ attr_accessor name
17
+ end
18
+
19
+ def self.date_field(name, json_key = nil)
20
+ field(name, json_key, type: :date)
21
+ end
22
+
23
+ def self.from_json(api_client, data)
24
+ return nil unless data
25
+ instance = new(data, api_client)
26
+ instance.populate_from_json(data)
27
+ instance
28
+ end
29
+
30
+ def self.from_json_list(api_client, data_list)
31
+ return [] unless data_list
32
+ data_list.map { |data| from_json(api_client, data) }
33
+ end
34
+
35
+ def initialize(data = {}, api_client = nil)
36
+ @data = data
37
+ @api_client = api_client
38
+ end
39
+
40
+ def populate_from_json(data)
41
+ self.class.fields.each do |name, config|
42
+ json_key = config[:json_key]
43
+ type = config[:type]
44
+ formatter = config[:formatter]
45
+
46
+ value = data[json_key]
47
+
48
+ # Apply type conversion
49
+ value = case type
50
+ when :date
51
+ parse_date(value)
52
+ when :boolean
53
+ parse_boolean(value)
54
+ else
55
+ value
56
+ end
57
+
58
+ # Apply custom formatter if provided
59
+ value = formatter.call(value) if formatter && value
60
+
61
+ instance_variable_set("@#{name}", value)
62
+ end
63
+ end
64
+
65
+ def to_h
66
+ hash = {}
67
+ self.class.fields.each do |name, _|
68
+ hash[name] = instance_variable_get("@#{name}")
69
+ end
70
+ hash
71
+ end
72
+
73
+ def inspect
74
+ attrs = self.class.fields.keys.map do |name|
75
+ value = instance_variable_get("@#{name}")
76
+ "#{name}=#{value.inspect}"
77
+ end.join(' ')
78
+ "#<#{self.class.name} #{attrs}>"
79
+ end
80
+
81
+ private
82
+
83
+ def parse_date(value)
84
+ return nil unless value
85
+ if value.is_a?(Hash) && value['time']
86
+ Time.at(value['time'] / 1000.0).utc
87
+ elsif value.is_a?(Numeric)
88
+ Time.at(value / 1000.0).utc
89
+ else
90
+ value
91
+ end
92
+ end
93
+
94
+ def parse_boolean(value)
95
+ case value
96
+ when true, false
97
+ value
98
+ when 'true', '1', 1
99
+ true
100
+ when 'false', '0', 0
101
+ false
102
+ else
103
+ value
104
+ end
105
+ end
106
+ end
107
+
108
+ # Base class for collections of models
109
+ class Collection < Base
110
+ include Enumerable
111
+
112
+ def initialize(data = {}, api_client = nil)
113
+ super
114
+ @items = []
115
+ end
116
+
117
+ def <<(item)
118
+ @items << item
119
+ end
120
+
121
+ def [](index)
122
+ @items[index]
123
+ end
124
+
125
+ def each(&block)
126
+ @items.each(&block)
127
+ end
128
+
129
+ def length
130
+ @items.length
131
+ end
132
+ alias_method :size, :length
133
+ alias_method :count, :length
134
+
135
+ def empty?
136
+ @items.empty?
137
+ end
138
+
139
+ def first
140
+ @items.first
141
+ end
142
+
143
+ def last
144
+ @items.last
145
+ end
146
+
147
+ def to_a
148
+ @items
149
+ end
150
+ end
151
+
152
+ # PandoraModel base class similar to Python implementation
153
+ class PandoraModel
154
+ def self.fields
155
+ @fields ||= {}
156
+ end
157
+
158
+ def self.field(name, json_key = nil, type: nil, &block)
159
+ json_key ||= name.to_s
160
+ fields[name] = { json_key: json_key, type: type, formatter: block }
161
+ attr_accessor name
162
+ end
163
+
164
+ def self.date_field(name, json_key = nil)
165
+ field(name, json_key, type: :date)
166
+ end
167
+
168
+ def self.from_json(api_client, data)
169
+ return nil unless data
170
+ instance = new(api_client)
171
+ instance.populate_from_json(data)
172
+ instance
173
+ end
174
+
175
+ def self.from_json_list(api_client, data_list)
176
+ return [] unless data_list
177
+ data_list.map { |data| from_json(api_client, data) }
178
+ end
179
+
180
+ def initialize(api_client)
181
+ @api_client = api_client
182
+ end
183
+
184
+ def populate_from_json(data)
185
+ self.class.fields.each do |name, config|
186
+ json_key = config[:json_key]
187
+ type = config[:type]
188
+ formatter = config[:formatter]
189
+
190
+ value = data[json_key]
191
+
192
+ # Apply type conversion
193
+ value = case type
194
+ when :date
195
+ parse_date(value)
196
+ when :boolean
197
+ parse_boolean(value)
198
+ else
199
+ value
200
+ end
201
+
202
+ # Apply custom formatter if provided
203
+ value = formatter.call(value) if formatter && value
204
+
205
+ instance_variable_set("@#{name}", value)
206
+ end
207
+ end
208
+
209
+ def to_h
210
+ hash = {}
211
+ self.class.fields.each do |name, _|
212
+ hash[name] = instance_variable_get("@#{name}")
213
+ end
214
+ hash
215
+ end
216
+
217
+ def inspect
218
+ attrs = self.class.fields.keys.map do |name|
219
+ value = instance_variable_get("@#{name}")
220
+ "#{name}=#{value.inspect}"
221
+ end.join(' ')
222
+ "#<#{self.class.name} #{attrs}>"
223
+ end
224
+
225
+ private
226
+
227
+ def parse_date(value)
228
+ return nil unless value
229
+ if value.is_a?(Hash) && value['time']
230
+ Time.at(value['time'] / 1000.0).utc
231
+ elsif value.is_a?(Numeric)
232
+ Time.at(value / 1000.0).utc
233
+ else
234
+ value
235
+ end
236
+ end
237
+
238
+ def parse_boolean(value)
239
+ case value
240
+ when true, false
241
+ value
242
+ when 'true', '1', 1
243
+ true
244
+ when 'false', '0', 0
245
+ false
246
+ else
247
+ value
248
+ end
249
+ end
250
+ end
251
+
252
+ # List Model for indexed collections
253
+ class PandoraListModel < PandoraModel
254
+ include Enumerable
255
+
256
+ def self.from_json(api_client, data)
257
+ instance = new(api_client)
258
+ instance.populate_from_json(data)
259
+
260
+ # Extract the list items
261
+ list_key = instance.class.list_key
262
+ list_model = instance.class.list_model
263
+
264
+ if list_key && list_model && data[list_key]
265
+ items = list_model.from_json_list(api_client, data[list_key])
266
+ instance.instance_variable_set(:@items, items)
267
+
268
+ # Create index if specified
269
+ if instance.class.index_key
270
+ index = {}
271
+ items.each { |item| index[item.send(instance.class.index_key)] = item }
272
+ instance.instance_variable_set(:@index, index)
273
+ end
274
+ else
275
+ instance.instance_variable_set(:@items, [])
276
+ instance.instance_variable_set(:@index, {})
277
+ end
278
+
279
+ instance
280
+ end
281
+
282
+ def self.list_key
283
+ @list_key
284
+ end
285
+
286
+ def self.list_model
287
+ @list_model
288
+ end
289
+
290
+ def self.index_key
291
+ @index_key
292
+ end
293
+
294
+ def self.set_list_config(list_key:, list_model:, index_key: nil)
295
+ @list_key = list_key
296
+ @list_model = list_model
297
+ @index_key = index_key
298
+ end
299
+
300
+ def initialize(api_client)
301
+ super(api_client)
302
+ @items = []
303
+ @index = {}
304
+ end
305
+
306
+ def each(&block)
307
+ @items.each(&block)
308
+ end
309
+
310
+ def [](key)
311
+ if key.is_a?(Integer)
312
+ @items[key]
313
+ else
314
+ @index[key] if @index
315
+ end
316
+ end
317
+
318
+ def []=(index, value)
319
+ @items[index] = value if index.is_a?(Integer)
320
+ end
321
+
322
+ def size
323
+ @items.size
324
+ end
325
+
326
+ def length
327
+ @items.length
328
+ end
329
+
330
+ def empty?
331
+ @items.empty?
332
+ end
333
+ end
334
+
335
+ # Dictionary List Model
336
+ # For models that have both array and hash-like access patterns
337
+ class PandoraDictListModel < PandoraListModel
338
+ # Additional dictionary-like methods can be added here
339
+ end
340
+
341
+ # Date Field similar to Python implementation
342
+ class DateField
343
+ attr_reader :field
344
+
345
+ def initialize(field)
346
+ @field = field
347
+ end
348
+
349
+ def formatter(api_client, data, value)
350
+ return nil unless value
351
+ return nil if value.is_a?(Hash) && value.empty?
352
+
353
+ if value.is_a?(Hash) && value["time"]
354
+ Time.at(value["time"] / 1000.0).utc
355
+ elsif value.is_a?(Numeric)
356
+ Time.at(value / 1000.0).utc
357
+ else
358
+ value
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end
@@ -0,0 +1,81 @@
1
+ require_relative '_base'
2
+
3
+ module Pandoru
4
+ module Models
5
+ class Bookmark < Base
6
+ field :bookmark_token, 'bookmarkToken'
7
+ field :music_token, 'musicToken'
8
+ field :artist_name, 'artistName'
9
+ field :song_name, 'songName'
10
+ field :art_url, 'artUrl'
11
+ field :sample_url, 'sampleUrl'
12
+ field :sample_gain, 'sampleGain'
13
+ field :album_name, 'albumName'
14
+ date_field :date_created, 'dateCreated'
15
+
16
+ def song_bookmark?
17
+ !song_name.nil?
18
+ end
19
+
20
+ def artist_bookmark?
21
+ song_name.nil?
22
+ end
23
+
24
+ def delete
25
+ return false unless @api_client
26
+ if song_bookmark?
27
+ @api_client.delete_song_bookmark(bookmark_token)
28
+ else
29
+ @api_client.delete_artist_bookmark(bookmark_token)
30
+ end
31
+ true
32
+ end
33
+ end
34
+
35
+ class BookmarkList < Collection
36
+ def self.from_json(api_client, data)
37
+ instance = new(data, api_client)
38
+
39
+ # Add song bookmarks
40
+ if data['songs']
41
+ Bookmark.from_json_list(api_client, data['songs']).each do |bookmark|
42
+ instance << bookmark
43
+ end
44
+ end
45
+
46
+ # Add artist bookmarks (ensure song_name is nil for artists)
47
+ if data['artists']
48
+ data['artists'].each do |artist_data|
49
+ artist_data['songName'] = nil unless artist_data.key?('songName')
50
+ bookmark = Bookmark.from_json(api_client, artist_data)
51
+ instance << bookmark
52
+ end
53
+ end
54
+
55
+ instance
56
+ end
57
+
58
+ def song_bookmarks
59
+ select(&:song_bookmark?)
60
+ end
61
+
62
+ def artist_bookmarks
63
+ select(&:artist_bookmark?)
64
+ end
65
+
66
+ def find_song_bookmark(song_name, artist_name = nil)
67
+ song_bookmarks.find do |bookmark|
68
+ matches_song = bookmark.song_name&.casecmp(song_name) == 0
69
+ matches_artist = artist_name.nil? || bookmark.artist_name&.casecmp(artist_name) == 0
70
+ matches_song && matches_artist
71
+ end
72
+ end
73
+
74
+ def find_artist_bookmark(artist_name)
75
+ artist_bookmarks.find do |bookmark|
76
+ bookmark.artist_name&.casecmp(artist_name) == 0
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,91 @@
1
+ require_relative '_base'
2
+
3
+ module Pandoru
4
+ module Models
5
+ class PlaylistItem < Base
6
+ field :track_token, "trackToken"
7
+ field :artist_name, "artistName"
8
+ field :album_name, "albumName"
9
+ field :song_name, "songName"
10
+ field :song_rating, "songRating"
11
+ field :track_length, "trackLength"
12
+ field :allow_feedback, "allowFeedback", type: :boolean
13
+ field :ad_token, "adToken"
14
+ field :audio_url_map, "audioUrlMap"
15
+ field :is_ad, "isAd", type: :boolean
16
+
17
+ def initialize(data = {}, api_client = nil)
18
+ super(data, api_client)
19
+ end
20
+
21
+ def is_ad?
22
+ @is_ad || false
23
+ end
24
+
25
+ def audio_url(quality = nil)
26
+ return nil unless @audio_url_map
27
+
28
+ quality ||= @api_client&.default_audio_quality || "mediumQuality"
29
+ @audio_url_map[quality]
30
+ end
31
+
32
+ def thumbs_up
33
+ return unless @api_client && @allow_feedback
34
+ @api_client.add_feedback(@track_token, true)
35
+ end
36
+
37
+ def thumbs_down
38
+ return unless @api_client && @allow_feedback
39
+ @api_client.add_feedback(@track_token, false)
40
+ end
41
+
42
+ def bookmark_song
43
+ return unless @api_client
44
+ @api_client.add_song_bookmark(@track_token)
45
+ end
46
+
47
+ def bookmark_artist
48
+ return unless @api_client
49
+ @api_client.add_artist_bookmark(@track_token)
50
+ end
51
+
52
+ def sleep
53
+ return unless @api_client
54
+ @api_client.sleep_song(@track_token)
55
+ end
56
+ end
57
+
58
+ class Playlist < Collection
59
+ def self.from_json(api_client, data)
60
+ playlist = new({}, api_client)
61
+ playlist.populate_from_json(data)
62
+
63
+ # Add playlist items
64
+ if data["items"]
65
+ data["items"].each do |item_data|
66
+ item = PlaylistItem.from_json(api_client, item_data)
67
+ playlist << item
68
+ end
69
+ end
70
+
71
+ playlist
72
+ end
73
+ end
74
+
75
+ class AdItem < PlaylistItem
76
+ field :ad_token, "adToken"
77
+ field :company_name, "companyName"
78
+ field :title, "title"
79
+ field :click_through_url, "clickThroughUrl"
80
+ field :image_url, "imageUrl"
81
+ field :tracking_tokens, "trackingTokens"
82
+
83
+ attr_accessor :station_id
84
+
85
+ def initialize(data = {}, api_client = nil)
86
+ super(data, api_client)
87
+ @is_ad = true
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,75 @@
1
+ require_relative '_base'
2
+
3
+ module Pandoru
4
+ module Models
5
+ class SearchResultItem < Base
6
+ field :artist_name, 'artistName'
7
+ field :song_name, 'songName'
8
+ field :music_token, 'musicToken'
9
+ field :score, 'score'
10
+ field :likely_match, 'likelyMatch', type: :boolean
11
+
12
+ def song?
13
+ !song_name.nil?
14
+ end
15
+
16
+ def artist?
17
+ song_name.nil?
18
+ end
19
+
20
+ def create_station
21
+ return nil unless @api_client
22
+ if song?
23
+ @api_client.create_station(song_token: music_token)
24
+ else
25
+ @api_client.create_station(artist_token: music_token)
26
+ end
27
+ end
28
+ end
29
+
30
+ class SearchResult < Collection
31
+ field :near_matches_available, 'nearMatchesAvailable', type: :boolean
32
+ field :explanation, 'explanation'
33
+
34
+ def self.from_json(api_client, data)
35
+ instance = new(data, api_client)
36
+ instance.populate_from_json(data)
37
+
38
+ # Add songs
39
+ if data['songs']
40
+ SearchResultItem.from_json_list(api_client, data['songs']).each do |item|
41
+ instance << item
42
+ end
43
+ end
44
+
45
+ # Add artists (convert to SearchResultItem format)
46
+ if data['artists']
47
+ data['artists'].each do |artist_data|
48
+ item_data = {
49
+ 'artistName' => artist_data['artistName'],
50
+ 'musicToken' => artist_data['musicToken'],
51
+ 'score' => artist_data['score'],
52
+ 'likelyMatch' => artist_data['likelyMatch']
53
+ }
54
+ item = SearchResultItem.from_json(api_client, item_data)
55
+ instance << item
56
+ end
57
+ end
58
+
59
+ instance
60
+ end
61
+
62
+ def songs
63
+ select(&:song?)
64
+ end
65
+
66
+ def artists
67
+ select(&:artist?)
68
+ end
69
+
70
+ def best_match
71
+ max_by(&:score)
72
+ end
73
+ end
74
+ end
75
+ end