rocksky 0.10.0 → 0.11.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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +26 -1
- data/lib/rocksky/filter.rb +138 -0
- data/lib/rocksky/manifest.json +7 -7
- data/lib/rocksky/version.rb +1 -1
- data/lib/rocksky.rb +46 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 411359937d7cc586384c4b58c7e32f4824cc4cfb1ee0e3ec3f38c0071b0b3b30
|
|
4
|
+
data.tar.gz: 97525f3e9c77f00c136a9f349f18747965dba4aa02f4e55bb1fb6587d926fc6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49ce584a23a45ecab4725b82308b6968468e2e3104267d4e5ca6696241d149ce224872d8eb303b8dd7680c2e4bc97e738feac641760dfb05c7d5330ef527d5e8
|
|
7
|
+
data.tar.gz: 4cc152d4c19bfaa289a7422426abe6878d2fae69fda3f0063846b3ece0080c979f9d2b4077415f650bd34d4e7b466d13d3bac045c4e5c94a5e5495034253eee8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
- Added `Rocksky::Filter`, a fluent RSQL builder (`eq`, `ne`, `gt`, `ge`,
|
|
6
|
+
`lt`, `le`, `is_in`, `is_out`, `is_null`, `is_not_null`, combined with
|
|
7
|
+
`#and` / `#or`). Fields are Symbols (`:artist`, `:"track.artist"`); values
|
|
8
|
+
are quoted/escaped automatically, `*` wildcards stay bare.
|
|
9
|
+
- Added filterable reads: `Rocksky.catalog_songs`, `catalog_artists`,
|
|
10
|
+
`catalog_albums` (`limit:`, `offset:`, `genre:`, `filter:`) and
|
|
11
|
+
`Rocksky.scrobble_feed` (`did:`, `following:`, `limit:`, `offset:`,
|
|
12
|
+
`filter:`). `filter:` takes a `Rocksky::Filter` or a raw RSQL String.
|
|
13
|
+
|
|
3
14
|
## 0.10.0
|
|
4
15
|
|
|
5
16
|
- Remote-player protocol: now-playing records accept optional audio info —
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@ gem install rocksky
|
|
|
14
14
|
Or in a Gemfile:
|
|
15
15
|
|
|
16
16
|
```ruby
|
|
17
|
-
gem "rocksky", "~> 0.
|
|
17
|
+
gem "rocksky", "~> 0.11"
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
The gem is pure-Ruby; the native library is fetched from the GitHub release on
|
|
@@ -83,6 +83,31 @@ Rocksky.top_artists_interval(limit: 5, interval: :all)
|
|
|
83
83
|
**Match** — `Rocksky.match_song(title, artist, mb_id: nil, isrc: nil)` resolves a
|
|
84
84
|
bare title + artist into full canonical metadata.
|
|
85
85
|
|
|
86
|
+
### Filtering
|
|
87
|
+
|
|
88
|
+
`Rocksky::Filter` builds RSQL expressions for the `filter:` kwarg of
|
|
89
|
+
`catalog_songs`, `catalog_artists`, `catalog_albums` and `scrobble_feed`
|
|
90
|
+
(each also takes `limit:`, `offset:` and — catalogs only — `genre:`). Fields
|
|
91
|
+
are Symbols; dotted selectors on the scrobble feed reach the joined
|
|
92
|
+
track/user/artist (`:"track.artist"`, `:"user.handle"`, …).
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
filter = Rocksky::Filter.eq(:artist, "Daft Punk")
|
|
96
|
+
.and(Rocksky::Filter.gt(:duration, 200_000))
|
|
97
|
+
.or(Rocksky::Filter.is_in(:genre, %w[house electro]))
|
|
98
|
+
filter.to_s # => artist=="Daft Punk";duration=gt=200000,genre=in=(house,electro)
|
|
99
|
+
|
|
100
|
+
Rocksky.catalog_songs(limit: 20, filter: filter)
|
|
101
|
+
Rocksky.scrobble_feed(filter: Rocksky::Filter.eq(:"track.artist", "Daft Punk"))
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Constructors: `eq`, `ne`, `gt`, `ge`, `lt`, `le`, `is_in`, `is_out` (aliases
|
|
105
|
+
`in`/`out`), `is_null`, `is_not_null`. Combine with `#and` (`;`) and `#or`
|
|
106
|
+
(`,`); an OR operand inside an AND is parenthesized automatically. String
|
|
107
|
+
values are quoted/escaped when they contain reserved characters, and `*`
|
|
108
|
+
wildcards pass through unquoted (`Filter.eq(:artist, "Daft*")`). A raw RSQL
|
|
109
|
+
String is accepted anywhere a `Filter` is.
|
|
110
|
+
|
|
86
111
|
### Writes — `Rocksky::Agent`
|
|
87
112
|
|
|
88
113
|
`Agent.login(session_path, identifier, password, appview:, dedup_path:)` → an
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Standalone — no native core involved. Loads on its own via
|
|
4
|
+
# `require "rocksky/filter"`.
|
|
5
|
+
module Rocksky
|
|
6
|
+
# Fluent builder for RSQL filter expressions, accepted by the +filter:+
|
|
7
|
+
# parameter of the catalog and scrobble-feed queries
|
|
8
|
+
# (app.rocksky.song.getSongs, app.rocksky.artist.getArtists,
|
|
9
|
+
# app.rocksky.album.getAlbums, app.rocksky.scrobble.getScrobbles).
|
|
10
|
+
#
|
|
11
|
+
# require "rocksky"
|
|
12
|
+
#
|
|
13
|
+
# filter = Rocksky::Filter.eq(:artist, "Daft Punk")
|
|
14
|
+
# .and(Rocksky::Filter.gt(:duration, 200_000))
|
|
15
|
+
# .or(Rocksky::Filter.is_in(:genre, %w[house electro]))
|
|
16
|
+
#
|
|
17
|
+
# Rocksky.catalog_songs(filter: filter)
|
|
18
|
+
# # artist=="Daft Punk";duration=gt=200000,genre=in=(house,electro)
|
|
19
|
+
#
|
|
20
|
+
# Fields are Symbols (Strings work too); dotted selectors on the scrobble
|
|
21
|
+
# feed are written as +:"track.artist"+. String values are quoted and
|
|
22
|
+
# escaped automatically when they contain characters RSQL reserves; +*+
|
|
23
|
+
# wildcards pass through unquoted so <tt>Filter.eq(:artist, "Daft*")</tt>
|
|
24
|
+
# performs a case-insensitive match.
|
|
25
|
+
class Filter
|
|
26
|
+
# Characters that never need quoting in an RSQL value (+*+ kept bare so
|
|
27
|
+
# wildcards work).
|
|
28
|
+
SAFE_VALUE = /\A[A-Za-z0-9_.:@*+-]+\z/
|
|
29
|
+
private_constant :SAFE_VALUE
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# +field==value+ — equals; +*+ in string values is a wildcard.
|
|
33
|
+
def eq(field, value)
|
|
34
|
+
comparison(field, "==", value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# +field!=value+ — not equals.
|
|
38
|
+
def ne(field, value)
|
|
39
|
+
comparison(field, "!=", value)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# +field=gt=value+ — greater than.
|
|
43
|
+
def gt(field, value)
|
|
44
|
+
comparison(field, "=gt=", value)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# +field=ge=value+ — greater than or equal.
|
|
48
|
+
def ge(field, value)
|
|
49
|
+
comparison(field, "=ge=", value)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# +field=lt=value+ — less than.
|
|
53
|
+
def lt(field, value)
|
|
54
|
+
comparison(field, "=lt=", value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# +field=le=value+ — less than or equal.
|
|
58
|
+
def le(field, value)
|
|
59
|
+
comparison(field, "=le=", value)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# +field=in=(a,b)+ — matches any of the values.
|
|
63
|
+
def is_in(field, values)
|
|
64
|
+
list(field, "=in=", values)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# +field=out=(a,b)+ — matches none of the values.
|
|
68
|
+
def is_out(field, values)
|
|
69
|
+
list(field, "=out=", values)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
alias in is_in
|
|
73
|
+
alias out is_out
|
|
74
|
+
|
|
75
|
+
# +field==null+ — the field is NULL.
|
|
76
|
+
def is_null(field)
|
|
77
|
+
new("#{field}==null", :comparison)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# +field!=null+ — the field is not NULL.
|
|
81
|
+
def is_not_null(field)
|
|
82
|
+
new("#{field}!=null", :comparison)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def comparison(field, op, value)
|
|
88
|
+
new("#{field}#{op}#{render_value(value)}", :comparison)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def list(field, op, values)
|
|
92
|
+
name = op == "=in=" ? "is_in" : "is_out"
|
|
93
|
+
raise ArgumentError, "Filter.#{name}(#{field.inspect}, ...) needs at least one value" if values.empty?
|
|
94
|
+
|
|
95
|
+
new("#{field}#{op}(#{values.map { |v| render_value(v) }.join(",")})", :comparison)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def render_value(value)
|
|
99
|
+
return value.to_s if value.is_a?(Integer) || value.is_a?(Float) ||
|
|
100
|
+
value == true || value == false
|
|
101
|
+
|
|
102
|
+
str = value.to_s
|
|
103
|
+
return str if !str.empty? && SAFE_VALUE.match?(str)
|
|
104
|
+
|
|
105
|
+
%("#{str.gsub(/(["\\])/) { "\\#{Regexp.last_match(1)}" }}")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def initialize(expr, kind)
|
|
110
|
+
@expr = expr
|
|
111
|
+
@kind = kind
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Both sides must match (+;+). An +or+ operand is parenthesized to keep
|
|
115
|
+
# RSQL precedence.
|
|
116
|
+
def and(other)
|
|
117
|
+
Filter.new("#{operand_in_and};#{other.operand_in_and}", :and)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Either side may match (+,+). Never parenthesizes.
|
|
121
|
+
def or(other)
|
|
122
|
+
Filter.new("#{@expr},#{other.build}", :or)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The RSQL expression string to send as the +filter+ query param.
|
|
126
|
+
def build
|
|
127
|
+
@expr
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
alias to_s build
|
|
131
|
+
|
|
132
|
+
protected
|
|
133
|
+
|
|
134
|
+
def operand_in_and
|
|
135
|
+
@kind == :or ? "(#{@expr})" : @expr
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/rocksky/manifest.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repo": "tsirysndr/rocksky",
|
|
3
|
-
"tag": "bindings-v0.
|
|
3
|
+
"tag": "bindings-v0.8.0",
|
|
4
4
|
"checksums": {
|
|
5
|
-
"aarch64-apple-darwin": "
|
|
6
|
-
"aarch64-linux-gnu": "
|
|
7
|
-
"x86_64-apple-darwin": "
|
|
8
|
-
"x86_64-linux-gnu": "
|
|
9
|
-
"x86_64-unknown-freebsd": "
|
|
10
|
-
"x86_64-unknown-netbsd": "
|
|
5
|
+
"aarch64-apple-darwin": "31b58deb0f3d627d37b101f06b5b4a952401040f076b4cea7436aa338080a701",
|
|
6
|
+
"aarch64-linux-gnu": "dca60c186ee83401e9afcdf1e7b31c308594f6dcda4cab25acf77254739d2a49",
|
|
7
|
+
"x86_64-apple-darwin": "bdf2c98ddc866280712c928d78d4173c1760deac6d8c567417b90ea20fba1bb4",
|
|
8
|
+
"x86_64-linux-gnu": "74dede20bb5892eff8ad321721a3f4584227bf2c97d78c2febeec095d2df46b6",
|
|
9
|
+
"x86_64-unknown-freebsd": "e6af79359f73a9d42d6232eade70f5e3938e75b66bc240e1077d76334daa08fe",
|
|
10
|
+
"x86_64-unknown-netbsd": "d1765e3c66d99a1160ddb3361ff9cfcf9c1335a056e68d089ea1c97ef6ff3f16"
|
|
11
11
|
}
|
|
12
12
|
}
|
data/lib/rocksky/version.rb
CHANGED
data/lib/rocksky.rb
CHANGED
|
@@ -127,6 +127,51 @@ module Rocksky
|
|
|
127
127
|
unwrap(C.rocksky_global_stats(base.to_s))
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
+
# ---- catalog + feed reads with RSQL filtering ----
|
|
131
|
+
#
|
|
132
|
+
# +filter:+ accepts a Rocksky::Filter (see rocksky/filter.rb) or a raw RSQL
|
|
133
|
+
# String. Fields are Symbols, e.g. Rocksky::Filter.eq(:artist, "Daft Punk").
|
|
134
|
+
|
|
135
|
+
# The song catalog (app.rocksky.song.getSongs), optionally narrowed by
|
|
136
|
+
# +genre:+ and/or an RSQL +filter:+. Returns { "tracks" => [...] }.
|
|
137
|
+
def self.catalog_songs(limit: 50, offset: 0, genre: nil, filter: nil, base: nil)
|
|
138
|
+
get("app.rocksky.song.getSongs", catalog_params(limit, offset, genre, filter), base: base)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The artist catalog (app.rocksky.artist.getArtists). Returns
|
|
142
|
+
# { "artists" => [...] }.
|
|
143
|
+
def self.catalog_artists(limit: 50, offset: 0, genre: nil, filter: nil, base: nil)
|
|
144
|
+
get("app.rocksky.artist.getArtists", catalog_params(limit, offset, genre, filter), base: base)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The album catalog (app.rocksky.album.getAlbums). Returns
|
|
148
|
+
# { "albums" => [...] }.
|
|
149
|
+
def self.catalog_albums(limit: 50, offset: 0, genre: nil, filter: nil, base: nil)
|
|
150
|
+
get("app.rocksky.album.getAlbums", catalog_params(limit, offset, genre, filter), base: base)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# A social/global scrobbles feed (app.rocksky.scrobble.getScrobbles). Pass
|
|
154
|
+
# +did:+ to scope to an actor and +following: true+ for their follow graph.
|
|
155
|
+
# +filter:+ reaches the joined track/user/artist via dotted fields, e.g.
|
|
156
|
+
# Rocksky::Filter.eq(:"track.artist", "Daft Punk"). Returns
|
|
157
|
+
# { "scrobbles" => [...] }.
|
|
158
|
+
def self.scrobble_feed(did: nil, following: false, limit: 50, offset: 0, filter: nil, base: nil)
|
|
159
|
+
params = { limit: limit, offset: offset }
|
|
160
|
+
params[:did] = did unless did.nil?
|
|
161
|
+
params[:following] = following if following
|
|
162
|
+
params[:filter] = filter.to_s unless filter.nil?
|
|
163
|
+
get("app.rocksky.scrobble.getScrobbles", params, base: base)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Shared param assembly for the three catalog reads (omit-nil idiom).
|
|
167
|
+
def self.catalog_params(limit, offset, genre, filter)
|
|
168
|
+
params = { limit: limit, offset: offset }
|
|
169
|
+
params[:genre] = genre unless genre.nil?
|
|
170
|
+
params[:filter] = filter.to_s unless filter.nil?
|
|
171
|
+
params
|
|
172
|
+
end
|
|
173
|
+
private_class_method :catalog_params
|
|
174
|
+
|
|
130
175
|
# Universal read escape hatch — call any app.rocksky.* query by nsid. +params+
|
|
131
176
|
# is a hash of string params; the whole read-query catalog is reachable here.
|
|
132
177
|
#
|
|
@@ -301,5 +346,6 @@ module Rocksky
|
|
|
301
346
|
end
|
|
302
347
|
end
|
|
303
348
|
|
|
349
|
+
require_relative "rocksky/filter"
|
|
304
350
|
require_relative "rocksky/library"
|
|
305
351
|
require_relative "rocksky/remote"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rocksky
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rocksky
|
|
@@ -80,6 +80,7 @@ files:
|
|
|
80
80
|
- README.md
|
|
81
81
|
- exe/rocksky-console
|
|
82
82
|
- lib/rocksky.rb
|
|
83
|
+
- lib/rocksky/filter.rb
|
|
83
84
|
- lib/rocksky/library.rb
|
|
84
85
|
- lib/rocksky/manifest.json
|
|
85
86
|
- lib/rocksky/native.rb
|