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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE +21 -0
- data/README.md +263 -0
- data/lib/pandoru/client.rb +298 -0
- data/lib/pandoru/client_builder.rb +526 -0
- data/lib/pandoru/errors.rb +147 -0
- data/lib/pandoru/models/_base.rb +363 -0
- data/lib/pandoru/models/bookmark.rb +81 -0
- data/lib/pandoru/models/playlist.rb +91 -0
- data/lib/pandoru/models/search.rb +75 -0
- data/lib/pandoru/models/station.rb +249 -0
- data/lib/pandoru/models/track_explanation.rb +41 -0
- data/lib/pandoru/models.rb +19 -0
- data/lib/pandoru/transport.rb +395 -0
- data/lib/pandoru/version.rb +3 -0
- data/lib/pandoru.rb +69 -0
- metadata +212 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2e2bd7b5d026ed3c16ee35173ca22f10839f4f486acd3259c8f5ed43c9b96053
|
|
4
|
+
data.tar.gz: 25ce0f4f8a3d6bcbf78dadf9a4eb7fa20ae3a0e77bb68e979ffd9a3acc1e2f91
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8561e8704472f4c3bbec08198a85d84bca87687fa43bad8a08239f49228e7890aaaff55b0d66bd591420b22639de25c40a1aa421124743452b1939601d900b09
|
|
7
|
+
data.tar.gz: cd0f0849b4a1128801be0debc5613933d4757dad32327cf5b914a093e8501e3be84596bb12a58b75db29dd4b970ae6d0a29ab730d4cf28d721522bf71188ddfd
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based
|
|
4
|
+
on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-05-25
|
|
8
|
+
|
|
9
|
+
Initial public release. A Ruby port of pydora (tracking upstream `pydora 2.3.1`)
|
|
10
|
+
targeting Pandora's partner/device JSON API (`tuner.pandora.com/services/json/`).
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Station` model now parses extended attributes: music seeds
|
|
14
|
+
(`seed_artists`, `seed_songs`, `seed_genres`) and feedback
|
|
15
|
+
(`thumbs_up`, `thumbs_down`) via new `StationSeed`/`StationSeeds`,
|
|
16
|
+
`SongFeedback`/`StationFeedback` sub-models.
|
|
17
|
+
- `TrackExplanation` model for `track.explainTrack`, exposing `focus_traits`
|
|
18
|
+
(the Music-Genome-derived trait tags) with the trailing filler entry
|
|
19
|
+
stripped. `APIClient#explain_track` now returns this model.
|
|
20
|
+
- `base64` declared as an explicit runtime dependency (removed from Ruby's
|
|
21
|
+
default gems in 3.4).
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- Partner authentication: the default partner **username** is now `android`
|
|
25
|
+
(the canonical partner) rather than `android-generic` (which is the
|
|
26
|
+
*device model*). The previous value caused `partnerLogin` to fail with
|
|
27
|
+
INVALID_PARTNER_LOGIN.
|
|
28
|
+
- Corrected the encryption/decryption key orientation in the bundled default
|
|
29
|
+
partner settings.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Dale Stevens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Pandoru
|
|
2
|
+
|
|
3
|
+
**Pandoru** is a Ruby port of the Python `pydora` library, providing a comprehensive client for the unofficial Pandora music streaming API. This gem allows you to interact with Pandora programmatically to manage stations, get playlists, search for music, and control playback.
|
|
4
|
+
|
|
5
|
+
> **Note**: This is an unofficial API client. Use at your own risk and respect Pandora's terms of service.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Station Management**: Get station lists, create/delete stations, rename stations
|
|
12
|
+
- **Playlist Access**: Retrieve playlists with track metadata and audio URLs
|
|
13
|
+
- **Music Search**: Search for songs, artists, and albums
|
|
14
|
+
- **User Interaction**: Thumbs up/down, bookmarks, sleep songs
|
|
15
|
+
- **Feedback Management**: Add/remove track feedback
|
|
16
|
+
- **Station Seeds & Genome Traits**: Inspect a station's seed artists/songs/genres and a track's Music-Genome focus traits (`explain_track`)
|
|
17
|
+
- **Genre Exploration**: Browse and create stations from genre seeds
|
|
18
|
+
- **Multiple Audio Qualities**: Support for low, medium, and high quality audio streams
|
|
19
|
+
- **Ruby Idioms**: Built with Ruby best practices and idiomatic patterns
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Add this line to your application's Gemfile:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
gem 'pandoru'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then execute:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle install
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or install it yourself as:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
gem install pandoru
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
### Configuration
|
|
48
|
+
|
|
49
|
+
Pandoru requires Pandora partner credentials to function. You can configure these in several ways:
|
|
50
|
+
|
|
51
|
+
#### 1. Using a Configuration Hash
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
require 'pandoru'
|
|
55
|
+
|
|
56
|
+
# Note: These are example credentials - you need real ones
|
|
57
|
+
settings = {
|
|
58
|
+
"PARTNER_USER" => "your-partner-user",
|
|
59
|
+
"PARTNER_PASSWORD" => "your-partner-password",
|
|
60
|
+
"DEVICE" => "your-device-type",
|
|
61
|
+
"DECRYPTION_KEY" => "your-decryption-key",
|
|
62
|
+
"ENCRYPTION_KEY" => "your-encryption-key"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
client = Pandoru::ClientBuilder.from_settings_hash(settings)
|
|
66
|
+
client.login("your_email@example.com", "your_password")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### 2. Using Configuration Files
|
|
70
|
+
|
|
71
|
+
Pandoru supports both pydora-style and pianobar-style configuration files:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# From a pydora config file
|
|
75
|
+
client = Pandoru::ClientBuilder.from_config_file("~/.pydora.cfg")
|
|
76
|
+
|
|
77
|
+
# From a pianobar config file
|
|
78
|
+
client = Pandoru::ClientBuilder.from_config_file("~/.config/pianobar/config")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Basic Operations
|
|
82
|
+
|
|
83
|
+
#### Managing Stations
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# Get all user stations
|
|
87
|
+
stations = client.get_station_list
|
|
88
|
+
|
|
89
|
+
# Create a new station from search results
|
|
90
|
+
search_results = client.search("Radiohead")
|
|
91
|
+
station = client.create_station(search_token: search_results.first.music_token)
|
|
92
|
+
|
|
93
|
+
# Rename a station
|
|
94
|
+
client.rename_station(station.token, "My Radiohead Station")
|
|
95
|
+
|
|
96
|
+
# Delete a station
|
|
97
|
+
client.delete_station(station.token)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### Working with Playlists
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# Get a playlist from a station
|
|
104
|
+
playlist = client.get_playlist(station.token)
|
|
105
|
+
|
|
106
|
+
playlist.each do |track|
|
|
107
|
+
puts "#{track.artist_name} - #{track.song_name}"
|
|
108
|
+
puts "Audio URL: #{track.audio_url}"
|
|
109
|
+
|
|
110
|
+
# Rate the track
|
|
111
|
+
track.thumbs_up if track.allow_feedback
|
|
112
|
+
|
|
113
|
+
# Bookmark the song or artist
|
|
114
|
+
track.bookmark_song
|
|
115
|
+
track.bookmark_artist
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
#### Searching for Music
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# Search for music
|
|
123
|
+
results = client.search("The Beatles", include_near_matches: true)
|
|
124
|
+
|
|
125
|
+
results.songs.each do |song|
|
|
126
|
+
puts "Song: #{song.song_name} by #{song.artist_name}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
results.artists.each do |artist|
|
|
130
|
+
puts "Artist: #{artist.artist_name}"
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### Managing Bookmarks
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
# Get user bookmarks
|
|
138
|
+
bookmarks = client.get_bookmarks
|
|
139
|
+
|
|
140
|
+
bookmarks.song_bookmarks.each do |bookmark|
|
|
141
|
+
puts "Bookmarked song: #{bookmark.song_name} by #{bookmark.artist_name}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
bookmarks.artist_bookmarks.each do |bookmark|
|
|
145
|
+
puts "Bookmarked artist: #{bookmark.artist_name}"
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Advanced Features
|
|
150
|
+
|
|
151
|
+
#### Genre Stations
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
# Browse genre stations
|
|
155
|
+
genre_stations = client.get_genre_stations
|
|
156
|
+
|
|
157
|
+
genre_stations.categories.each do |category|
|
|
158
|
+
puts "Category: #{category}"
|
|
159
|
+
genre_stations.stations_for_category(category).each do |station|
|
|
160
|
+
puts " Station: #{station.name}"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Audio Quality
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
# Set default audio quality when creating client
|
|
169
|
+
client = Pandoru::ClientBuilder.from_settings_hash(settings.merge(
|
|
170
|
+
"AUDIO_QUALITY" => "highQuality" # or "mediumQuality", "lowQuality"
|
|
171
|
+
))
|
|
172
|
+
|
|
173
|
+
# Get specific quality audio URL for a track
|
|
174
|
+
track = playlist.first
|
|
175
|
+
high_quality_url = track.audio_url("highQuality")
|
|
176
|
+
medium_quality_url = track.audio_url("mediumQuality")
|
|
177
|
+
low_quality_url = track.audio_url("lowQuality")
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### Error Handling
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
begin
|
|
184
|
+
client.login("user@example.com", "password")
|
|
185
|
+
rescue Pandoru::Errors::InvalidUserLogin
|
|
186
|
+
puts "Invalid username or password"
|
|
187
|
+
rescue Pandoru::Errors::PandoraException => e
|
|
188
|
+
puts "Pandora API error: #{e.message} (Code: #{e.code})"
|
|
189
|
+
end
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## API Reference
|
|
195
|
+
|
|
196
|
+
### Client Classes
|
|
197
|
+
|
|
198
|
+
- `Pandoru::Client::APIClient` - High-level API client with all Pandora operations
|
|
199
|
+
- `Pandoru::Client::BaseAPIClient` - Lower-level client for advanced usage
|
|
200
|
+
|
|
201
|
+
### Models
|
|
202
|
+
|
|
203
|
+
- `Pandoru::Models::Station` - Represents a Pandora station
|
|
204
|
+
- `Pandoru::Models::StationList` - Collection of user stations
|
|
205
|
+
- `Pandoru::Models::Playlist` - Collection of playlist items
|
|
206
|
+
- `Pandoru::Models::PlaylistItem` - Individual track in a playlist
|
|
207
|
+
- `Pandoru::Models::SearchResult` - Search results container
|
|
208
|
+
- `Pandoru::Models::BookmarkList` - User's bookmarked songs and artists
|
|
209
|
+
|
|
210
|
+
### Client Builders
|
|
211
|
+
|
|
212
|
+
- `Pandoru::ClientBuilder.from_settings_hash(hash)` - Create client from settings hash
|
|
213
|
+
- `Pandoru::ClientBuilder.from_config_file(path)` - Create client from config file
|
|
214
|
+
- `Pandoru::ClientBuilder.default_client()` - Create client with default settings
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Architecture
|
|
219
|
+
|
|
220
|
+
Pandoru is architected similarly to the original pydora library:
|
|
221
|
+
|
|
222
|
+
- **Transport Layer**: Handles HTTP communication and encryption/decryption
|
|
223
|
+
- **Client Layer**: Provides high-level API methods
|
|
224
|
+
- **Models Layer**: Represents Pandora data structures with Ruby idioms
|
|
225
|
+
- **Builders Layer**: Factory classes for creating configured clients
|
|
226
|
+
|
|
227
|
+
The Ruby port maintains compatibility with pydora's API while providing Ruby-style interfaces and error handling.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Development
|
|
232
|
+
|
|
233
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt.
|
|
234
|
+
|
|
235
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Contributing
|
|
240
|
+
|
|
241
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/TwilightCoders/pandoru.
|
|
242
|
+
|
|
243
|
+
1. Fork it
|
|
244
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
245
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
246
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
247
|
+
5. Create new Pull Request
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Acknowledgments
|
|
258
|
+
|
|
259
|
+
This Ruby gem is a port of the excellent [pydora](https://github.com/mcrute/pydora) Python library by Mike Crute and contributors. All credit for the original API reverse engineering and design goes to the pydora project.
|
|
260
|
+
|
|
261
|
+
## Disclaimer
|
|
262
|
+
|
|
263
|
+
This project is not affiliated with or endorsed by Pandora Media, Inc. Use of this library may violate Pandora's Terms of Service. Use at your own risk.
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Pandora API Client
|
|
2
|
+
#
|
|
3
|
+
# This module contains the top level API client that is responsible for calling
|
|
4
|
+
# the API and returning the results in model format. There is a base API client
|
|
5
|
+
# that is useful for lower level programming such as calling methods that aren't
|
|
6
|
+
# directly supported by the higher level API client.
|
|
7
|
+
#
|
|
8
|
+
# The high level API client is what most clients should use and provides API
|
|
9
|
+
# calls that map directly to the Pandora API and return model objects with
|
|
10
|
+
# mappings from the raw JSON structures to Ruby objects.
|
|
11
|
+
#
|
|
12
|
+
# For simplicity use a client builder from Pandoru::ClientBuilder to create an
|
|
13
|
+
# instance of a client.
|
|
14
|
+
|
|
15
|
+
module Pandoru
|
|
16
|
+
module Client
|
|
17
|
+
# Base Pandora API Client
|
|
18
|
+
# The base API client has lower level methods that are composed together to
|
|
19
|
+
# provide higher level functionality.
|
|
20
|
+
class BaseAPIClient
|
|
21
|
+
LOW_AUDIO_QUALITY = "lowQuality"
|
|
22
|
+
MED_AUDIO_QUALITY = "mediumQuality"
|
|
23
|
+
HIGH_AUDIO_QUALITY = "highQuality"
|
|
24
|
+
|
|
25
|
+
ALL_QUALITIES = [LOW_AUDIO_QUALITY, MED_AUDIO_QUALITY, HIGH_AUDIO_QUALITY].freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :transport, :partner_user, :partner_password, :device, :default_audio_quality
|
|
28
|
+
attr_accessor :username, :password
|
|
29
|
+
|
|
30
|
+
def initialize(transport, partner_user = nil, partner_password = nil, device = nil, default_audio_quality: MED_AUDIO_QUALITY)
|
|
31
|
+
@transport = transport
|
|
32
|
+
@partner_user = partner_user
|
|
33
|
+
@partner_password = partner_password
|
|
34
|
+
@device = device
|
|
35
|
+
@default_audio_quality = default_audio_quality
|
|
36
|
+
@username = nil
|
|
37
|
+
@password = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def login(username, password)
|
|
41
|
+
@username = username
|
|
42
|
+
@password = password
|
|
43
|
+
authenticate
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call(method, **kwargs)
|
|
47
|
+
begin
|
|
48
|
+
@transport.call(method, **kwargs)
|
|
49
|
+
rescue Errors::InvalidAuthToken
|
|
50
|
+
authenticate
|
|
51
|
+
@transport.call(method, **kwargs)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.get_qualities(start_at, return_all_if_invalid: true)
|
|
56
|
+
begin
|
|
57
|
+
idx = ALL_QUALITIES.index(start_at)
|
|
58
|
+
ALL_QUALITIES[0..idx]
|
|
59
|
+
rescue ArgumentError
|
|
60
|
+
return_all_if_invalid ? ALL_QUALITIES.dup : []
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def partner_login
|
|
67
|
+
partner = @transport.call(
|
|
68
|
+
"auth.partnerLogin",
|
|
69
|
+
username: @partner_user,
|
|
70
|
+
password: @partner_password,
|
|
71
|
+
deviceModel: @device,
|
|
72
|
+
version: (@transport.class.const_defined?(:API_VERSION) ? @transport.class::API_VERSION : "5")
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@transport.set_partner(partner)
|
|
76
|
+
partner
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def authenticate
|
|
80
|
+
partner_login
|
|
81
|
+
|
|
82
|
+
user = @transport.call(
|
|
83
|
+
"auth.userLogin",
|
|
84
|
+
loginType: "user",
|
|
85
|
+
username: @username,
|
|
86
|
+
password: @password,
|
|
87
|
+
includePandoraOneInfo: true,
|
|
88
|
+
includeSubscriptionExpiration: true,
|
|
89
|
+
returnCapped: true,
|
|
90
|
+
includeAdAttributes: true,
|
|
91
|
+
includeAdvertiserAttributes: true,
|
|
92
|
+
xplatformAdCapable: true
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@transport.set_user(user)
|
|
96
|
+
user
|
|
97
|
+
rescue Errors::InvalidPartnerLogin => e
|
|
98
|
+
raise Errors::InvalidUserLogin.new(e.message)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# High Level Pandora API Client
|
|
103
|
+
# The high level API client implements the entire functional API for Pandora.
|
|
104
|
+
# This is what clients should actually use.
|
|
105
|
+
class APIClient < BaseAPIClient
|
|
106
|
+
def get_station_list
|
|
107
|
+
data = call("user.getStationList", includeStationArtUrl: true)
|
|
108
|
+
Models::StationList.from_json(self, data)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def get_station_list_checksum
|
|
112
|
+
data = call("user.getStationListChecksum")
|
|
113
|
+
data["checksum"]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def get_playlist(station_token, additional_urls: nil)
|
|
117
|
+
params = {
|
|
118
|
+
stationToken: station_token,
|
|
119
|
+
includeTrackLength: true,
|
|
120
|
+
xplatformAdCapable: true,
|
|
121
|
+
audioAdPodCapable: true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if additional_urls
|
|
125
|
+
urls = additional_urls.map { |url| url.respond_to?(:value) ? url.value : url }
|
|
126
|
+
params[:additionalAudioUrl] = urls.join(",")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
data = call("station.getPlaylist", **params)
|
|
130
|
+
|
|
131
|
+
# Add additional URLs parameter to each item for ad processing
|
|
132
|
+
if additional_urls
|
|
133
|
+
data["items"]&.each { |item| item["_paramAdditionalUrls"] = additional_urls }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
playlist = Models::Playlist.from_json(self, data)
|
|
137
|
+
|
|
138
|
+
# Process ad items
|
|
139
|
+
playlist.each_with_index do |track, i|
|
|
140
|
+
if track.is_ad?
|
|
141
|
+
ad_track = get_ad_item(station_token, track.ad_token)
|
|
142
|
+
playlist[i] = ad_track
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
playlist
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def get_bookmarks
|
|
150
|
+
data = call("user.getBookmarks")
|
|
151
|
+
Models::BookmarkList.from_json(self, data)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def get_station(station_token)
|
|
155
|
+
data = call("station.getStation",
|
|
156
|
+
stationToken: station_token,
|
|
157
|
+
includeExtendedAttributes: true)
|
|
158
|
+
Models::Station.from_json(self, data)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def add_artist_bookmark(track_token)
|
|
162
|
+
call("bookmark.addArtistBookmark", trackToken: track_token)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def add_song_bookmark(track_token)
|
|
166
|
+
call("bookmark.addSongBookmark", trackToken: track_token)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def delete_song_bookmark(bookmark_token)
|
|
170
|
+
call("bookmark.deleteSongBookmark", bookmarkToken: bookmark_token)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def delete_artist_bookmark(bookmark_token)
|
|
174
|
+
call("bookmark.deleteArtistBookmark", bookmarkToken: bookmark_token)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def search(search_text, include_near_matches: false, include_genre_stations: false)
|
|
178
|
+
data = call("music.search",
|
|
179
|
+
searchText: search_text,
|
|
180
|
+
includeNearMatches: include_near_matches,
|
|
181
|
+
includeGenreStations: include_genre_stations)
|
|
182
|
+
Models::SearchResult.from_json(self, data)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def add_feedback(track_token, positive)
|
|
186
|
+
call("station.addFeedback",
|
|
187
|
+
trackToken: track_token,
|
|
188
|
+
isPositive: positive)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def add_music(music_token, station_token)
|
|
192
|
+
call("station.addMusic",
|
|
193
|
+
musicToken: music_token,
|
|
194
|
+
stationToken: station_token)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def create_station(search_token: nil, artist_token: nil, track_token: nil, song_token: nil)
|
|
198
|
+
params = {}
|
|
199
|
+
|
|
200
|
+
if search_token
|
|
201
|
+
params[:musicToken] = search_token
|
|
202
|
+
elsif artist_token
|
|
203
|
+
params[:musicToken] = artist_token
|
|
204
|
+
elsif track_token
|
|
205
|
+
params[:musicToken] = track_token
|
|
206
|
+
elsif song_token
|
|
207
|
+
params[:musicToken] = song_token
|
|
208
|
+
else
|
|
209
|
+
raise ArgumentError, "Must provide one of: search_token, artist_token, track_token, song_token"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
data = call("station.createStation", **params)
|
|
213
|
+
Models::Station.from_json(self, data)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def delete_feedback(feedback_id)
|
|
217
|
+
call("station.deleteFeedback", feedbackId: feedback_id)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def delete_music(seed_id)
|
|
221
|
+
call("station.deleteMusic", seedId: seed_id)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def delete_station(station_token)
|
|
225
|
+
call("station.deleteStation", stationToken: station_token)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def get_genre_stations
|
|
229
|
+
data = call("station.getGenreStations")
|
|
230
|
+
Models::GenreStationList.from_json(self, data)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def get_genre_stations_checksum
|
|
234
|
+
data = call("station.getGenreStationsChecksum")
|
|
235
|
+
data["checksum"]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def rename_station(station_token, name)
|
|
239
|
+
call("station.renameStation",
|
|
240
|
+
stationToken: station_token,
|
|
241
|
+
stationName: name)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def explain_track(track_token)
|
|
245
|
+
data = call("track.explainTrack", trackToken: track_token)
|
|
246
|
+
Models::TrackExplanation.from_json(self, data)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def set_quick_mix(*station_ids)
|
|
250
|
+
call("user.setQuickMix", quickMixStationIds: station_ids.flatten)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def sleep_song(track_token)
|
|
254
|
+
call("user.sleepSong", trackToken: track_token)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def share_station(station_id, station_token, *emails)
|
|
258
|
+
call("station.shareStation",
|
|
259
|
+
stationId: station_id,
|
|
260
|
+
stationToken: station_token,
|
|
261
|
+
emails: emails.flatten)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def transform_shared_station(station_token)
|
|
265
|
+
call("station.transformSharedStation", stationToken: station_token)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def share_music(music_token, *emails)
|
|
269
|
+
call("music.shareMusic",
|
|
270
|
+
musicToken: music_token,
|
|
271
|
+
emails: emails.flatten)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def get_ad_item(station_id, ad_token)
|
|
275
|
+
raise Errors::ParameterMissing, "station_id must be defined, got: '#{station_id}'" if station_id.nil? || station_id.empty?
|
|
276
|
+
|
|
277
|
+
ad_data = get_ad_metadata(ad_token)
|
|
278
|
+
ad_item = Models::AdItem.from_json(self, ad_data)
|
|
279
|
+
ad_item.station_id = station_id
|
|
280
|
+
ad_item.ad_token = ad_token
|
|
281
|
+
ad_item
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def get_ad_metadata(ad_token)
|
|
285
|
+
call("ad.getAdMetadata",
|
|
286
|
+
adToken: ad_token,
|
|
287
|
+
returnAdTrackingTokens: true,
|
|
288
|
+
supportAudioAds: true)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def register_ad(station_id, tokens)
|
|
292
|
+
call("ad.registerAd",
|
|
293
|
+
stationId: station_id,
|
|
294
|
+
adTrackingTokens: tokens)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|