radio5 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 +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +256 -0
- data/lib/radio5/api.rb +66 -0
- data/lib/radio5/client/countries.rb +51 -0
- data/lib/radio5/client/islands.rb +51 -0
- data/lib/radio5/client/tracks.rb +116 -0
- data/lib/radio5/client/users.rb +11 -0
- data/lib/radio5/client.rb +34 -0
- data/lib/radio5/http.rb +123 -0
- data/lib/radio5/regexps.rb +38 -0
- data/lib/radio5/utils.rb +54 -0
- data/lib/radio5/validator.rb +83 -0
- data/lib/radio5/version.rb +5 -0
- data/lib/radio5.rb +25 -0
- metadata +62 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: edd4e5e61372538e86183d4e6171031695c51379482a619eb29c0b94378906e9
|
|
4
|
+
data.tar.gz: d25d7a72fdb56988bb4ae585a986671a395da01476fffa24f2ddb25d152fb85d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 94aeac4d8c3d39c751c22f5104c4f08fffa41c48f74db24f5ac984ee4f10d1f372f2bed4e361230a59f0791884adada50fa690bb8a42b523384a8ab6801f9622
|
|
7
|
+
data.tar.gz: 21475735cdf9bfc455c8a8841cdead2ab67b2e6e25503daf04c7fdd71eb32ce62f9cb8161a2c3a2f214a1365149efc4ea0711f58488fc8cdb64f74ebb52aeb3b
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Dmytro Horoshko
|
|
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,256 @@
|
|
|
1
|
+
# Radio5
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ocvit/radio5/actions)
|
|
4
|
+
[](https://coveralls.io/github/ocvit/radio5?branch=main)
|
|
5
|
+
|
|
6
|
+
Adapter for [Radiooooo](https://radiooooo.com/) private API.
|
|
7
|
+
|
|
8
|
+
For music exploration purposes only 🧐
|
|
9
|
+
|
|
10
|
+
## TL;DR
|
|
11
|
+
|
|
12
|
+
It turned out that 95% of all functionality doesn't even require an account (I'm not even talking about premium one), so here we go.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install the gem and add to Gemfile:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bundle add radio5
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or install it manually:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
gem install radio5
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Create a client:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
client = Radio5::Client.new
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
You can pass additional HTTP configration if needed:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
client = Radio5::Client.new(
|
|
40
|
+
open_timeout: 30, # default: 10
|
|
41
|
+
read_timeout: 30, # default: 10
|
|
42
|
+
write_timeout: 30, # default: 10
|
|
43
|
+
proxy_url: "http://user:pass@123.4.56.178:80", # default: nil
|
|
44
|
+
debug_output: $stdout # default: nil
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
To get random track:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
client.random_track
|
|
54
|
+
# => {
|
|
55
|
+
# id: "655f7bb24b0d722a021a2cf2",
|
|
56
|
+
# uuid: <uuid>,
|
|
57
|
+
# artist: "Kaye Ballard",
|
|
58
|
+
# title: "In Other Words (Fly Me to the Moon)",
|
|
59
|
+
# album: "In Other Words / Lazy Afternoon",
|
|
60
|
+
# year: "1954",
|
|
61
|
+
# label: "Decca",
|
|
62
|
+
# songwriter: "Bart Howard",
|
|
63
|
+
# length: 133,
|
|
64
|
+
# info: "It is the original recording of Fly me to the moon !",
|
|
65
|
+
# cover_url: "https://asset.radiooooo.com/cover/USA/1950/large/<uuid>_1.jpg",
|
|
66
|
+
# audio: {
|
|
67
|
+
# mpeg: {
|
|
68
|
+
# url: "https://radiooooo-track.b-cdn.net/USA/1950/<uuid>.mp3?token=<token>&expires=1704717060",
|
|
69
|
+
# expires_at: 2024-01-08 12:31:00 UTC
|
|
70
|
+
# },
|
|
71
|
+
# ogg: {
|
|
72
|
+
# url: "https://radiooooo-track.b-cdn.net/USA/1950/<uuid>.ogg?token=<token>&expires=1704717060",
|
|
73
|
+
# expires_at: 2024-01-08 12:31:00 UTC
|
|
74
|
+
# }
|
|
75
|
+
# },
|
|
76
|
+
# decade: 1950,
|
|
77
|
+
# mood: :slow,
|
|
78
|
+
# country: "USA",
|
|
79
|
+
# like_count: 3,
|
|
80
|
+
# created_at: nil,
|
|
81
|
+
# created_by: "655ec7fce03fdc024c70f698"
|
|
82
|
+
# }
|
|
83
|
+
#
|
|
84
|
+
# NOTES:
|
|
85
|
+
# - `created_at` - always nil here (API limitations), available via `#track` if needed
|
|
86
|
+
# - `created_by` - `id` of user who uploaded this track
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
To get random track using additional filters:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# with country
|
|
93
|
+
client.random_track(country: "FRA")
|
|
94
|
+
|
|
95
|
+
# with decade(s)
|
|
96
|
+
client.random_track(decades: [1960, 2000])
|
|
97
|
+
|
|
98
|
+
# with mood(s)
|
|
99
|
+
client.random_track(moods: [:slow, :weird])
|
|
100
|
+
|
|
101
|
+
# with everything together
|
|
102
|
+
client.random_track(country: "SWE", decades: [1940, 1980, 2010], moods: [:slow, :fast])
|
|
103
|
+
|
|
104
|
+
# in case no tracks match the filters
|
|
105
|
+
client.random_track(country: "KN1", decades: [1940], moods: [:weird])
|
|
106
|
+
# => nil
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
To get information about specific track using its `id`:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
client.track("655f7bb24b0d722a021a2cf2")
|
|
113
|
+
# => {
|
|
114
|
+
# ...
|
|
115
|
+
# created_at: 2023-11-23 16:20:02.283 UTC
|
|
116
|
+
# }
|
|
117
|
+
#
|
|
118
|
+
# output is exactly the same as from `#random_track`, but `created_at` is now filled
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
OK, what input parameters are available?
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# list of countries + additional info:
|
|
125
|
+
# - `exist` - "is it still around" flag
|
|
126
|
+
# - `rank` - subjective ranking provided by the website, only 10 countries have it
|
|
127
|
+
client.countries
|
|
128
|
+
# => {
|
|
129
|
+
# "AFG" => {name: "Afganistan", exist: true, rank: nil},
|
|
130
|
+
# "CBE" => {name: "Belgian Congo", exist: false, rank: nil},
|
|
131
|
+
# "FRA" => {name: "France", exist: true, rank: 2},
|
|
132
|
+
# ...
|
|
133
|
+
# }
|
|
134
|
+
|
|
135
|
+
# decades
|
|
136
|
+
client.decades
|
|
137
|
+
# => [1900, 1910, 1920, ..., 2010, 2020]
|
|
138
|
+
|
|
139
|
+
# moods
|
|
140
|
+
client.moods
|
|
141
|
+
# => [:fast, :slow, :weird]
|
|
142
|
+
#
|
|
143
|
+
# NOTE: by default all 3 moods are used in `#random_track` and `#island_track`
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It's also possible to get all valid `country`/`decade`/`moods` combinations in advance:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# grouped by country
|
|
150
|
+
client.countries_for_decade(1960)
|
|
151
|
+
# => {
|
|
152
|
+
# "THA" => [:fast, :slow, :weird],
|
|
153
|
+
# "TWN" => [:fast, :slow, :weird],
|
|
154
|
+
# "EST" => [:fast, :slow],
|
|
155
|
+
# "ALB" => [:fast, :slow],
|
|
156
|
+
# "NZL" => [:fast, :slow],
|
|
157
|
+
# ...
|
|
158
|
+
# }
|
|
159
|
+
|
|
160
|
+
# grouped by mood
|
|
161
|
+
client.countries_for_decade(1960, group_by: :mood)
|
|
162
|
+
# => {
|
|
163
|
+
# slow: ["FRA", "THA", "ALB", "GRC", "IRL", ...],
|
|
164
|
+
# fast: ["THA", "TWN", "EST", "ALB", "NZL", ...],
|
|
165
|
+
# weird: ["AZE", "POL", "IRN", "YUG", "DAH", ...]
|
|
166
|
+
# }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
How to work with the "islands" ("playlists" in the simple words):
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
# list all islands
|
|
173
|
+
client.islands
|
|
174
|
+
# => [{
|
|
175
|
+
# id: "5d330a3e06fb03d8872a3316",
|
|
176
|
+
# uuid: <uuid>,
|
|
177
|
+
# api_id: <api_id>,
|
|
178
|
+
# name: "Intimacy",
|
|
179
|
+
# info: "To get Laid. Slow to start. Fast to go further. Weird when nothing works.",
|
|
180
|
+
# category: "thematic",
|
|
181
|
+
# favourite_count: 1,
|
|
182
|
+
# play_count: 783339,
|
|
183
|
+
# rank: 17,
|
|
184
|
+
# icon_url: "https://asset.radiooooo.com/island/icon/<uuid>_2.svg",
|
|
185
|
+
# splash_url: "https://asset.radiooooo.com/island/splash/<uuid>_13.svg",
|
|
186
|
+
# marker_url: "https://asset.radiooooo.com/island/marker/<uuid>_9.svg",
|
|
187
|
+
# enabled: true,
|
|
188
|
+
# free: false,
|
|
189
|
+
# on_map: false,
|
|
190
|
+
# random: false,
|
|
191
|
+
# play_mode: "RANDOM",
|
|
192
|
+
# created_at: 2016-02-12 16:31:00 UTC,
|
|
193
|
+
# created_by: "5d3306de06fb03d8871fd119",
|
|
194
|
+
# updated_at: 2023-02-17 15:54:09.806 UTC,
|
|
195
|
+
# updated_by: "5d3306de06fb03d8871fd119"
|
|
196
|
+
# }, ...]
|
|
197
|
+
#
|
|
198
|
+
# NOTES:
|
|
199
|
+
# - `api_id` - have no idea where it is used
|
|
200
|
+
# - `rank` - 1..160, not unique, can be nil
|
|
201
|
+
# - `enabled` - is it searchable via web app, doesn't matter in our case ^^
|
|
202
|
+
# - `free` - do you need premium account to listen to it, doesn't matter ^^
|
|
203
|
+
# - `on_map` - is it displayed on a global map currently
|
|
204
|
+
# - `random` - there is only one playlist with `true` and it's called "Shuffle";
|
|
205
|
+
# basically the same as `#random_track` with no filters
|
|
206
|
+
# - `play_mode` - it is somehow used in a web app
|
|
207
|
+
# - `created_by` - `id` of user who created this island
|
|
208
|
+
# - `updated_by` - ...and who updated it last time
|
|
209
|
+
|
|
210
|
+
# to get random track from selected island
|
|
211
|
+
client.island_track(island_id: "5d330a3e06fb03d8872a3316")
|
|
212
|
+
|
|
213
|
+
# it's also possible to specify moods
|
|
214
|
+
client.island_track(island_id: "5d330a3e06fb03d8872a3316", moods: [:fast, :weird])
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
User endpoints - WIP.
|
|
218
|
+
|
|
219
|
+
## Auth?
|
|
220
|
+
|
|
221
|
+
There is just a couple of features that require login and/or premium account:
|
|
222
|
+
|
|
223
|
+
- history of "listened" tracks - track becomes "listened" when you got it via `#random_track` or `#island_track` (free)
|
|
224
|
+
- `followed` flag for `#user` - indicates whether or not you follow this user (free)
|
|
225
|
+
- `#user_liked_tracks` - list of tracks which user really rock'n'roll'ed to (free)
|
|
226
|
+
- ability to use multiple countries as a filter in `#random_track` (premium)
|
|
227
|
+
|
|
228
|
+
Currently auth is in a WIP state.
|
|
229
|
+
|
|
230
|
+
## TODO
|
|
231
|
+
|
|
232
|
+
- [x] HTTP client (no external deps, net/http hardcore only)
|
|
233
|
+
- [x] Basic API client (no auth)
|
|
234
|
+
- [x] Countries support
|
|
235
|
+
- [x] Islands support
|
|
236
|
+
- [x] Tracks support
|
|
237
|
+
- [ ] Users support
|
|
238
|
+
- [ ] Auth + auth'ed endpoints
|
|
239
|
+
|
|
240
|
+
## Development
|
|
241
|
+
|
|
242
|
+
```sh
|
|
243
|
+
bin/setup // install deps
|
|
244
|
+
bin/console // interactive prompt to play around
|
|
245
|
+
rake spec // test!
|
|
246
|
+
rake rubocop // lint!
|
|
247
|
+
sudo rm -rf / // relax, just kidding ^^
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Contributing
|
|
251
|
+
|
|
252
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ocvit/radio5.
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/radio5/api.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Radio5
|
|
4
|
+
class Api
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
class TrackNotFound < Error; end
|
|
7
|
+
class MatchingTrackNotFound < Error; end
|
|
8
|
+
class UnexpectedResponse < StandardError; end
|
|
9
|
+
|
|
10
|
+
HOST = "radiooooo.com"
|
|
11
|
+
PORT = 443
|
|
12
|
+
|
|
13
|
+
attr_reader :client
|
|
14
|
+
|
|
15
|
+
def initialize(client:)
|
|
16
|
+
@client = client
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get(path, query_params: {}, headers: {})
|
|
20
|
+
request(Net::HTTP::Get, path, query_params, nil, headers)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def post(path, query_params: {}, body: nil, headers: {})
|
|
24
|
+
request(Net::HTTP::Post, path, query_params, body, headers)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def request(http_method_class, path, query_params, body, headers)
|
|
30
|
+
http = create_http
|
|
31
|
+
response = http.request(http_method_class, path, query_params, body, headers)
|
|
32
|
+
|
|
33
|
+
case response.code
|
|
34
|
+
when "200", "400"
|
|
35
|
+
json = Utils.parse_json(response.body)
|
|
36
|
+
|
|
37
|
+
case json
|
|
38
|
+
in error: "No track with this id"
|
|
39
|
+
raise TrackNotFound
|
|
40
|
+
in error: "No track for this selection"
|
|
41
|
+
raise MatchingTrackNotFound
|
|
42
|
+
in error: other_error
|
|
43
|
+
raise Error, other_error
|
|
44
|
+
else
|
|
45
|
+
[response, json]
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
raise UnexpectedResponse, "code: #{response.code.inspect}, body: #{response.body.inspect}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# rubocop:disable Layout/HashAlignment
|
|
53
|
+
def create_http
|
|
54
|
+
Http.new(
|
|
55
|
+
host: HOST,
|
|
56
|
+
port: PORT,
|
|
57
|
+
open_timeout: client.open_timeout,
|
|
58
|
+
read_timeout: client.read_timeout,
|
|
59
|
+
write_timeout: client.write_timeout,
|
|
60
|
+
proxy_url: client.proxy_url,
|
|
61
|
+
debug_output: client.debug_output
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
# rubocop:enable Layout/HashAlignment
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Radio5
|
|
4
|
+
class Client
|
|
5
|
+
module Countries
|
|
6
|
+
def countries
|
|
7
|
+
_, json = api.get("/language/countries/en.json")
|
|
8
|
+
|
|
9
|
+
json.each_with_object({}) do |(iso_code, name, exist, rank), countries|
|
|
10
|
+
countries[iso_code] = {
|
|
11
|
+
name: name,
|
|
12
|
+
exist: exist,
|
|
13
|
+
rank: rank
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def countries_for_decade(decade, group_by: :country)
|
|
19
|
+
validate_decade!(decade)
|
|
20
|
+
|
|
21
|
+
# optimization to avoid doing this inside `case` to save HTTP request
|
|
22
|
+
unless [:mood, :country].include?(group_by)
|
|
23
|
+
raise ArgumentError, "invalid `group_by` value: #{group_by.inspect}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
_, json = api.get("/country/mood", query_params: {decade: decade})
|
|
27
|
+
|
|
28
|
+
grouped_by_mood = json.transform_keys do |mood_upcased|
|
|
29
|
+
mood = mood_upcased.downcase
|
|
30
|
+
|
|
31
|
+
validate_mood!(mood)
|
|
32
|
+
|
|
33
|
+
mood
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
case group_by
|
|
37
|
+
when :mood
|
|
38
|
+
grouped_by_mood
|
|
39
|
+
when :country
|
|
40
|
+
grouped_by_country = Hash.new { |hash, country| hash[country] = [] }
|
|
41
|
+
|
|
42
|
+
MOODS.each_with_object(grouped_by_country) do |mood, grouped_by_country|
|
|
43
|
+
grouped_by_mood[mood].each do |country|
|
|
44
|
+
grouped_by_country[country] << mood
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Radio5
|
|
4
|
+
class Client
|
|
5
|
+
module Islands
|
|
6
|
+
include Utils
|
|
7
|
+
|
|
8
|
+
# rubocop:disable Layout/HashAlignment
|
|
9
|
+
def islands
|
|
10
|
+
_, json = api.get("/island/all")
|
|
11
|
+
|
|
12
|
+
json.map do |island|
|
|
13
|
+
rank_value = island[:sort]
|
|
14
|
+
rank = rank_value if rank_value.is_a?(Integer)
|
|
15
|
+
|
|
16
|
+
created_at = parse_time_string(island.fetch(:created).fetch(:date))
|
|
17
|
+
created_by = island.fetch(:created).fetch(:user_id)
|
|
18
|
+
|
|
19
|
+
updated_node = island[:modified]
|
|
20
|
+
updated_at = updated_node && parse_time_string(updated_node.fetch(:date))
|
|
21
|
+
updated_by = updated_node&.fetch(:user_id)
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
id: island.fetch(:_id),
|
|
25
|
+
uuid: island.fetch(:uuid),
|
|
26
|
+
api_id: island[:apiid],
|
|
27
|
+
name: normalize_string(island.fetch(:name)),
|
|
28
|
+
info: normalize_string(island[:info]),
|
|
29
|
+
category: normalize_string(island[:category]),
|
|
30
|
+
favourite_count: island[:favorites],
|
|
31
|
+
play_count: island.fetch(:plays),
|
|
32
|
+
rank: rank,
|
|
33
|
+
icon_url: parse_asset_url(island, :icon),
|
|
34
|
+
splash_url: parse_asset_url(island, :splash),
|
|
35
|
+
marker_url: parse_asset_url(island, :marker),
|
|
36
|
+
enabled: island.fetch(:enabled),
|
|
37
|
+
free: island[:free],
|
|
38
|
+
on_map: island.fetch(:onmap),
|
|
39
|
+
random: island.fetch(:random),
|
|
40
|
+
play_mode: island[:play],
|
|
41
|
+
created_at: created_at,
|
|
42
|
+
created_by: created_by,
|
|
43
|
+
updated_at: updated_at,
|
|
44
|
+
updated_by: updated_by
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
# rubocop:enable Layout/HashAlignment
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Radio5
|
|
4
|
+
class Client
|
|
5
|
+
module Tracks
|
|
6
|
+
def track(track_id)
|
|
7
|
+
validate_track_id!(track_id)
|
|
8
|
+
|
|
9
|
+
_, json = api.get("/track/play/#{track_id}")
|
|
10
|
+
|
|
11
|
+
Parser.track_info(json)
|
|
12
|
+
rescue Api::TrackNotFound
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# TODO: technically, API accepts an array of countries, but without premium
|
|
17
|
+
# account only the first one is used during filtering.
|
|
18
|
+
# `country` should be used for now
|
|
19
|
+
# `countries` might be added in a future after implementation of auth
|
|
20
|
+
|
|
21
|
+
# rubocop:disable Layout/HashAlignment
|
|
22
|
+
def random_track(country: nil, decades: [], moods: MOODS)
|
|
23
|
+
iso_codes = country ? [country] : []
|
|
24
|
+
|
|
25
|
+
validate_country_iso_codes!(iso_codes)
|
|
26
|
+
validate_decades!(decades)
|
|
27
|
+
validate_moods!(moods)
|
|
28
|
+
|
|
29
|
+
body = {
|
|
30
|
+
mode: "explore",
|
|
31
|
+
isocodes: iso_codes,
|
|
32
|
+
decades: decades.uniq,
|
|
33
|
+
moods: stringify_moods(moods).uniq
|
|
34
|
+
}.to_json
|
|
35
|
+
|
|
36
|
+
_, json = api.post("/play", body: body)
|
|
37
|
+
|
|
38
|
+
Parser.track_info(json)
|
|
39
|
+
rescue Api::MatchingTrackNotFound
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
# rubocop:enable Layout/HashAlignment
|
|
43
|
+
|
|
44
|
+
# rubocop:disable Layout/HashAlignment
|
|
45
|
+
def island_track(island_id:, moods: MOODS)
|
|
46
|
+
validate_island_id!(island_id)
|
|
47
|
+
validate_moods!(moods)
|
|
48
|
+
|
|
49
|
+
body = {
|
|
50
|
+
mode: "islands",
|
|
51
|
+
island: island_id,
|
|
52
|
+
moods: stringify_moods(moods).uniq
|
|
53
|
+
}.to_json
|
|
54
|
+
|
|
55
|
+
_, json = api.post("/play", body: body)
|
|
56
|
+
|
|
57
|
+
Parser.track_info(json)
|
|
58
|
+
rescue Api::MatchingTrackNotFound
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
# rubocop:enable Layout/HashAlignment
|
|
62
|
+
|
|
63
|
+
module Parser
|
|
64
|
+
extend Utils
|
|
65
|
+
|
|
66
|
+
# rubocop:disable Layout/HashAlignment
|
|
67
|
+
def self.track_info(json)
|
|
68
|
+
created_node = json[:created]
|
|
69
|
+
created_at = created_node && parse_time_string(created_node.fetch(:date))
|
|
70
|
+
created_by = created_node ? created_node.fetch(:user_id) : json.fetch(:profile_id)
|
|
71
|
+
|
|
72
|
+
audio = {
|
|
73
|
+
mpeg: track_audio(json, :mpeg),
|
|
74
|
+
ogg: track_audio(json, :ogg)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
id: json.fetch(:_id),
|
|
79
|
+
uuid: json.fetch(:uuid),
|
|
80
|
+
artist: normalize_string(json.fetch(:artist)),
|
|
81
|
+
title: normalize_string(json.fetch(:title)),
|
|
82
|
+
album: normalize_string(json[:album]),
|
|
83
|
+
year: normalize_string(json.fetch(:year)),
|
|
84
|
+
label: normalize_string(json[:label]),
|
|
85
|
+
songwriter: normalize_string(json[:songwriter]),
|
|
86
|
+
length: json.fetch(:length),
|
|
87
|
+
info: normalize_string(json[:info]),
|
|
88
|
+
cover_url: parse_asset_url(json, :image, size: "large"),
|
|
89
|
+
audio: audio,
|
|
90
|
+
decade: json.fetch(:decade),
|
|
91
|
+
mood: symbolize_mood(json.fetch(:mood)),
|
|
92
|
+
country: json.fetch(:country),
|
|
93
|
+
like_count: json.fetch(:likes),
|
|
94
|
+
created_at: created_at,
|
|
95
|
+
created_by: created_by
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
# rubocop:enable Layout/HashAlignment
|
|
99
|
+
|
|
100
|
+
def self.track_audio(json, format)
|
|
101
|
+
url = json.fetch(:links).fetch(format)
|
|
102
|
+
url.gsub!(/#t=\d*,\d+/, "")
|
|
103
|
+
|
|
104
|
+
expires_at_unix = Integer(url[/(?<=expires=)\d+/])
|
|
105
|
+
expires_at = parse_unix_timestamp(expires_at_unix)
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
url: url,
|
|
109
|
+
expires_at: expires_at
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
private_constant :Parser
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Radio5
|
|
4
|
+
class Client
|
|
5
|
+
include Utils
|
|
6
|
+
include Validator
|
|
7
|
+
include Users
|
|
8
|
+
include Countries
|
|
9
|
+
include Islands
|
|
10
|
+
include Tracks
|
|
11
|
+
|
|
12
|
+
attr_accessor :open_timeout, :read_timeout, :write_timeout, :proxy_url, :debug_output
|
|
13
|
+
|
|
14
|
+
def initialize(open_timeout: nil, read_timeout: nil, write_timeout: nil, proxy_url: nil, debug_output: nil)
|
|
15
|
+
@open_timeout = open_timeout
|
|
16
|
+
@read_timeout = read_timeout
|
|
17
|
+
@write_timeout = write_timeout
|
|
18
|
+
@proxy_url = proxy_url
|
|
19
|
+
@debug_output = debug_output
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def api
|
|
23
|
+
@api ||= Api.new(client: self)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def decades
|
|
27
|
+
DECADES
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def moods
|
|
31
|
+
MOODS
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/radio5/http.rb
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Radio5
|
|
8
|
+
class Http
|
|
9
|
+
DEFAULT_OPEN_TIMEOUT = 10 # seconds
|
|
10
|
+
DEFAULT_READ_TIMEOUT = 10 # seconds
|
|
11
|
+
DEFAULT_WRITE_TIMEOUT = 10 # seconds
|
|
12
|
+
DEFAULT_DEBUG_OUTPUT = File.open(File::NULL, "w")
|
|
13
|
+
DEFAULT_MAX_RETRIES = 3
|
|
14
|
+
DEFAULT_HEADERS = {
|
|
15
|
+
"Content-Type" => "application/json; charset=utf-8",
|
|
16
|
+
"User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
17
|
+
}
|
|
18
|
+
RETRIABLE_ERRORS = [
|
|
19
|
+
Errno::ECONNREFUSED,
|
|
20
|
+
Errno::ECONNRESET,
|
|
21
|
+
Errno::ETIMEDOUT,
|
|
22
|
+
Net::OpenTimeout,
|
|
23
|
+
Net::ReadTimeout,
|
|
24
|
+
Net::WriteTimeout,
|
|
25
|
+
OpenSSL::SSL::SSLError
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
attr_reader :max_retries
|
|
29
|
+
|
|
30
|
+
# rubocop:disable Layout/ExtraSpacing
|
|
31
|
+
def initialize(
|
|
32
|
+
host:,
|
|
33
|
+
port:,
|
|
34
|
+
open_timeout: DEFAULT_OPEN_TIMEOUT,
|
|
35
|
+
read_timeout: DEFAULT_READ_TIMEOUT,
|
|
36
|
+
write_timeout: DEFAULT_WRITE_TIMEOUT,
|
|
37
|
+
proxy_url: nil,
|
|
38
|
+
max_retries: DEFAULT_MAX_RETRIES,
|
|
39
|
+
debug_output: DEFAULT_DEBUG_OUTPUT
|
|
40
|
+
)
|
|
41
|
+
# @host = host
|
|
42
|
+
# @port = port
|
|
43
|
+
# @open_timeout = open_timeout
|
|
44
|
+
# @read_timeout = read_timeout
|
|
45
|
+
# @write_timeout = write_timeout
|
|
46
|
+
# @proxy_url = proxy_url
|
|
47
|
+
# @debug_output = debug_output
|
|
48
|
+
|
|
49
|
+
proxy_uri = parse_proxy_uri(proxy_url)
|
|
50
|
+
@max_retries = max_retries
|
|
51
|
+
|
|
52
|
+
@http = Net::HTTP.new(host, port, proxy_uri&.host, proxy_uri&.port, proxy_uri&.user, proxy_uri&.pass)
|
|
53
|
+
|
|
54
|
+
@http.tap do |c|
|
|
55
|
+
c.use_ssl = port == 443
|
|
56
|
+
c.open_timeout = open_timeout
|
|
57
|
+
c.read_timeout = read_timeout
|
|
58
|
+
c.write_timeout = write_timeout
|
|
59
|
+
|
|
60
|
+
c.set_debug_output(debug_output)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
# rubocop:enable Layout/ExtraSpacing
|
|
64
|
+
|
|
65
|
+
def request(http_method_class, path, query_params, body, headers)
|
|
66
|
+
request = build_request(http_method_class, path, query_params, body, headers)
|
|
67
|
+
make_request(request)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def parse_proxy_uri(proxy_url)
|
|
73
|
+
return if proxy_url.nil?
|
|
74
|
+
|
|
75
|
+
proxy_uri = URI(proxy_url)
|
|
76
|
+
|
|
77
|
+
unless @proxy_uri.is_a?(URI::HTTP)
|
|
78
|
+
raise ArgumentError, "Invalid proxy URL: #{@proxy_uri}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
proxy_uri
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_request(http_method_class, path, query_params, body, headers)
|
|
85
|
+
path = add_query_params(path, query_params)
|
|
86
|
+
|
|
87
|
+
request = http_method_class.new(path)
|
|
88
|
+
add_body(request, body)
|
|
89
|
+
add_headers(request, headers)
|
|
90
|
+
|
|
91
|
+
request
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def add_query_params(path, query_params)
|
|
95
|
+
if query_params.empty?
|
|
96
|
+
path
|
|
97
|
+
else
|
|
98
|
+
"#{path}?#{URI.encode_www_form(query_params)}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def add_body(request, body)
|
|
103
|
+
request.body = body
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def add_headers(request, headers)
|
|
107
|
+
DEFAULT_HEADERS.merge(headers).each do |key, value|
|
|
108
|
+
request.delete(key)
|
|
109
|
+
request.add_field(key, value)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def make_request(request, retries: 0)
|
|
114
|
+
@http.request(request)
|
|
115
|
+
rescue *RETRIABLE_ERRORS => error
|
|
116
|
+
if retries < max_retries
|
|
117
|
+
make_request(request, retries: retries + 1)
|
|
118
|
+
else
|
|
119
|
+
raise error
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Radio5
|
|
4
|
+
module Regexps
|
|
5
|
+
# rubocop:disable Layout/ExtraSpacing
|
|
6
|
+
|
|
7
|
+
MONGO_ID = /^[a-f\d]{24}$/
|
|
8
|
+
UUID_GENERIC = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/
|
|
9
|
+
UUID = /^#{UUID_GENERIC}$/
|
|
10
|
+
|
|
11
|
+
COUNTRY_ISO_CODE_GENERIC = /([A-Z]{3}|KN1)/
|
|
12
|
+
COUNTRY_ISO_CODE = /^#{COUNTRY_ISO_CODE_GENERIC}$/
|
|
13
|
+
|
|
14
|
+
ASSET_URL = lambda do |sub_path, exts|
|
|
15
|
+
asset_host = Regexp.escape(Utils::ASSET_HOST)
|
|
16
|
+
sub_path = sub_path.is_a?(Regexp) ? sub_path : Regexp.escape(sub_path)
|
|
17
|
+
exts = /(#{exts.join("|")})/
|
|
18
|
+
|
|
19
|
+
/#{asset_host}#{sub_path}\/#{UUID_GENERIC}(_\d+)?\.#{exts}/
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ISLAND_ICON_URL = ASSET_URL.call("/island/icon", ["png", "svg"])
|
|
23
|
+
ISLAND_SPLASH_URL = ASSET_URL.call("/island/splash", ["png", "svg"])
|
|
24
|
+
ISLAND_MARKER_URL = ASSET_URL.call("/island/marker", ["png", "svg"])
|
|
25
|
+
TRACK_COVER_URL = ASSET_URL.call(/\/cover\/#{COUNTRY_ISO_CODE_GENERIC}\/\d{4}\/large/, ["jpg", "jpeg"])
|
|
26
|
+
|
|
27
|
+
AUDIO_URL = lambda do |exts|
|
|
28
|
+
exts = /(#{exts.join("|")})/
|
|
29
|
+
|
|
30
|
+
/.+\/#{UUID_GENERIC}\.#{exts}\?token=[^&]{22}&expires=\d{10}$/
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
MPEG_URL = AUDIO_URL.call(["mp3", "m4a"])
|
|
34
|
+
OGG_URL = AUDIO_URL.call(["ogg"])
|
|
35
|
+
|
|
36
|
+
# rubocop:enable Layout/ExtraSpacing
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/radio5/utils.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Radio5
|
|
7
|
+
module Utils
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
ASSET_HOST = "https://asset.radiooooo.com"
|
|
11
|
+
|
|
12
|
+
def parse_json(json_raw)
|
|
13
|
+
JSON.parse(json_raw, symbolize_names: true)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse_asset_url(hash, key, size: nil)
|
|
17
|
+
node = hash[key]
|
|
18
|
+
|
|
19
|
+
if node
|
|
20
|
+
path, filename = node.fetch_values(:path, :filename)
|
|
21
|
+
path << "#{size}/" if size
|
|
22
|
+
|
|
23
|
+
create_asset_url(path, filename)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_asset_url(path, filename)
|
|
28
|
+
URI.join(ASSET_HOST, path, filename).to_s
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse_time_string(time_string)
|
|
32
|
+
Time.parse(time_string).utc
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def parse_unix_timestamp(ts)
|
|
36
|
+
Time.at(ts).utc
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def normalize_string(string)
|
|
40
|
+
return if string.nil? || string.empty?
|
|
41
|
+
|
|
42
|
+
normalized = string.strip
|
|
43
|
+
normalized unless normalized.empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stringify_moods(moods)
|
|
47
|
+
moods.map { |mood| MOODS_MAPPING.fetch(mood) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def symbolize_mood(mood)
|
|
51
|
+
MOODS_MAPPING.key(mood)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Radio5
|
|
4
|
+
module Validator
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def mongo_id?(object)
|
|
8
|
+
object.is_a?(String) && object.match?(Regexps::MONGO_ID)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def country_iso_code?(object)
|
|
12
|
+
object.is_a?(String) && object.match?(Regexps::COUNTRY_ISO_CODE)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def decade?(object)
|
|
16
|
+
object.is_a?(Integer) && DECADES.include?(object)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def mood?(object)
|
|
20
|
+
object.is_a?(Symbol) && MOODS_MAPPING.key?(object)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_track_id!(object)
|
|
24
|
+
unless mongo_id?(object)
|
|
25
|
+
raise ArgumentError, "invalid track ID: #{object.inspect}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validate_island_id!(object)
|
|
30
|
+
unless mongo_id?(object)
|
|
31
|
+
raise ArgumentError, "invalid island ID: #{object.inspect}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validate_country_iso_codes!(iso_codes)
|
|
36
|
+
unless iso_codes.is_a?(Array)
|
|
37
|
+
raise ArgumentError, "country ISO codes should be an array"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
iso_codes.each do |iso_code|
|
|
41
|
+
validate_country_iso_code!(iso_code)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_country_iso_code!(object)
|
|
46
|
+
unless country_iso_code?(object)
|
|
47
|
+
raise ArgumentError, "invalid country ISO code: #{object.inspect}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate_decades!(decades)
|
|
52
|
+
unless decades.is_a?(Array)
|
|
53
|
+
raise ArgumentError, "decades should be an array"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
decades.each do |decade|
|
|
57
|
+
validate_decade!(decade)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_decade!(object)
|
|
62
|
+
unless decade?(object)
|
|
63
|
+
raise ArgumentError, "invalid decade: #{object.inspect}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def validate_moods!(moods)
|
|
68
|
+
unless moods.is_a?(Array)
|
|
69
|
+
raise ArgumentError, "moods should be an array"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
moods.each do |mood|
|
|
73
|
+
validate_mood!(mood)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def validate_mood!(object)
|
|
78
|
+
unless mood?(object)
|
|
79
|
+
raise ArgumentError, "invalid mood: #{object.inspect}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/radio5.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "radio5/version"
|
|
4
|
+
require_relative "radio5/utils"
|
|
5
|
+
require_relative "radio5/http"
|
|
6
|
+
require_relative "radio5/api"
|
|
7
|
+
require_relative "radio5/regexps"
|
|
8
|
+
require_relative "radio5/validator"
|
|
9
|
+
require_relative "radio5/client/users"
|
|
10
|
+
require_relative "radio5/client/countries"
|
|
11
|
+
require_relative "radio5/client/islands"
|
|
12
|
+
require_relative "radio5/client/tracks"
|
|
13
|
+
require_relative "radio5/client"
|
|
14
|
+
|
|
15
|
+
module Radio5
|
|
16
|
+
DECADES = (1900..2020).step(10).to_a.freeze
|
|
17
|
+
|
|
18
|
+
MOODS_MAPPING = {
|
|
19
|
+
fast: "FAST",
|
|
20
|
+
slow: "SLOW",
|
|
21
|
+
weird: "WEIRD"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
MOODS = MOODS_MAPPING.keys.freeze
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: radio5
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dmytro Horoshko
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2024-01-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Adapter for Radiooooo.com private API.
|
|
14
|
+
email:
|
|
15
|
+
- electric.molfar@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- CHANGELOG.md
|
|
21
|
+
- LICENSE.txt
|
|
22
|
+
- README.md
|
|
23
|
+
- lib/radio5.rb
|
|
24
|
+
- lib/radio5/api.rb
|
|
25
|
+
- lib/radio5/client.rb
|
|
26
|
+
- lib/radio5/client/countries.rb
|
|
27
|
+
- lib/radio5/client/islands.rb
|
|
28
|
+
- lib/radio5/client/tracks.rb
|
|
29
|
+
- lib/radio5/client/users.rb
|
|
30
|
+
- lib/radio5/http.rb
|
|
31
|
+
- lib/radio5/regexps.rb
|
|
32
|
+
- lib/radio5/utils.rb
|
|
33
|
+
- lib/radio5/validator.rb
|
|
34
|
+
- lib/radio5/version.rb
|
|
35
|
+
homepage: https://github.com/ocvit/radio5
|
|
36
|
+
licenses:
|
|
37
|
+
- MIT
|
|
38
|
+
metadata:
|
|
39
|
+
bug_tracker_uri: https://github.com/ocvit/radio5/issues
|
|
40
|
+
changelog_uri: https://github.com/ocvit/radio5/blob/main/CHANGELOG.md
|
|
41
|
+
homepage_uri: https://github.com/ocvit/radio5
|
|
42
|
+
source_code_uri: https://github.com/ocvit/radio5
|
|
43
|
+
post_install_message:
|
|
44
|
+
rdoc_options: []
|
|
45
|
+
require_paths:
|
|
46
|
+
- lib
|
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '2.7'
|
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
53
|
+
requirements:
|
|
54
|
+
- - ">="
|
|
55
|
+
- !ruby/object:Gem::Version
|
|
56
|
+
version: '0'
|
|
57
|
+
requirements: []
|
|
58
|
+
rubygems_version: 3.4.10
|
|
59
|
+
signing_key:
|
|
60
|
+
specification_version: 4
|
|
61
|
+
summary: Radiooooo.com unlocked!
|
|
62
|
+
test_files: []
|