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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 506387a27a8e92d1310a65f855110efff853b6079d9dbb65d6a3e1e56932cc18
4
- data.tar.gz: 9d190b167472ba41881d547b527e501e446a75d844cefd4ff5b3200cd461cc94
3
+ metadata.gz: 411359937d7cc586384c4b58c7e32f4824cc4cfb1ee0e3ec3f38c0071b0b3b30
4
+ data.tar.gz: 97525f3e9c77f00c136a9f349f18747965dba4aa02f4e55bb1fb6587d926fc6f
5
5
  SHA512:
6
- metadata.gz: 1a82ef8bb90f40689b29067ba2debfe55eb8e099ed0897c3a69b795a3f03e76c49b10ba42d2035377027ea6701b53ac7be2195b2aead08f0a609ebd4e3058e9c
7
- data.tar.gz: 78ca04c14127de3ae2b6993a942129ad1d9118679f1c8458807d315744e703932ae3c861fb32349fb615dff4d4086207bc38e19a2361d27a8fa047291576addf
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.7"
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
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "repo": "tsirysndr/rocksky",
3
- "tag": "bindings-v0.7.0",
3
+ "tag": "bindings-v0.8.0",
4
4
  "checksums": {
5
- "aarch64-apple-darwin": "e9a772586564269dccf8448d46162b85fe5290307b818b3a4fc26ff634c2ab57",
6
- "aarch64-linux-gnu": "fbfc5d12a8c532f437577f1c2e36c794760e78bb66749af33eef17d14a60cafc",
7
- "x86_64-apple-darwin": "81e5628ea2f269ac8efefce85fa5eae093b1d49848aba580da9f267a36f85115",
8
- "x86_64-linux-gnu": "3d3de1e3b691416676dd1c09879b14944a5c85d1d8911650113e6ee7783326e2",
9
- "x86_64-unknown-freebsd": "b0ad8071e2a6baf1096d4f82221c440e56bf9426e435244ade8687f08edabf69",
10
- "x86_64-unknown-netbsd": "08e9ff48d6273e63d6ebdc5eb3882777bc9bd227bc9126478a98c5041e2f1f05"
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
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rocksky
4
- VERSION = "0.10.0"
4
+ VERSION = "0.11.0"
5
5
  end
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.10.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