rocksky 0.9.1 → 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: 84425f844d397e7187168e44115168f310f5a48269da6b1f3efc424cbf9991aa
4
- data.tar.gz: 89e6d0d11fd1f88ec4e80498251f1d1d24cc4e77587860b7c96b0508306a3817
3
+ metadata.gz: 411359937d7cc586384c4b58c7e32f4824cc4cfb1ee0e3ec3f38c0071b0b3b30
4
+ data.tar.gz: 97525f3e9c77f00c136a9f349f18747965dba4aa02f4e55bb1fb6587d926fc6f
5
5
  SHA512:
6
- metadata.gz: b48c2675d3ecedc52f6b2c7b70a728d927dd69a2564bfe16bc8b55101dfbf866e44b67957b1ad866c57819b0b66360aa637745a73c87238b0c769aa5b5b5e49a
7
- data.tar.gz: bbc58f41ea90eda2c8878cde5e6c182a364a5d8de5f4056ab5e6baad5178c67756cf2f02da5343a2b1a679d83839c02b0c1787de64ec6119a42c979abc9a9e2a
6
+ metadata.gz: 49ce584a23a45ecab4725b82308b6968468e2e3104267d4e5ca6696241d149ce224872d8eb303b8dd7680c2e4bc97e738feac641760dfb05c7d5330ef527d5e8
7
+ data.tar.gz: 4cc152d4c19bfaa289a7422426abe6878d2fae69fda3f0063846b3ece0080c979f9d2b4077415f650bd34d4e7b466d13d3bac045c4e5c94a5e5495034253eee8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
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
+
14
+ ## 0.10.0
15
+
16
+ - Remote-player protocol: now-playing records accept optional audio info —
17
+ `codec` (audio codec/container, e.g. "mp3", "flac") and `sampleRate` (sample
18
+ rate in Hz, e.g. 44100). A player includes them in the `track` payload when
19
+ set (`codec` / `sample_rate` on the wire, omitted otherwise); a controller's
20
+ `:now_playing` event exposes them on the `:track` hash.
21
+
3
22
  ## 0.9.1
4
23
 
5
24
  - Fix a segfault when tearing down a `RemotePlayer` / `RemoteController`.
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.6.0",
3
+ "tag": "bindings-v0.8.0",
4
4
  "checksums": {
5
- "aarch64-apple-darwin": "e2de89af96e1330427898524254ddb57728453947b607c4dd29fb8ed7c513b27",
6
- "aarch64-linux-gnu": "37b8f5ec3812cb15f7eeb75beb44ffe857cca95c11064d1e781eaff6edf7c14d",
7
- "x86_64-apple-darwin": "7a550b51fc75e1e4d7c5393423f2920dda980ea8f026c9bde8edd926d18d3795",
8
- "x86_64-linux-gnu": "683e9c14579041c77fa69b53f73b43a44432719c608d7af74f950c9263c5c53b",
9
- "x86_64-unknown-freebsd": "4ecf5a61000b46111aef2c66e241c97e712c8ef6af386a01afddfe2c05136893",
10
- "x86_64-unknown-netbsd": "0c619e60f6cf95c42bbe61adb26be8550024c8cbea08be6de9fb3127db627900"
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
  }
@@ -27,7 +27,8 @@ module Rocksky
27
27
  #
28
28
  # Records (now-playing / queue items) are passed as Hashes with camelCase keys
29
29
  # (title, artist, album, albumArtist, albumArt, durationMs, elapsedMs,
30
- # isPlaying / songUri, albumUri, trackNumber), matching the wire record shape.
30
+ # isPlaying, codec, sampleRate / songUri, albumUri, trackNumber), matching the
31
+ # wire record shape.
31
32
  class RemotePlayer
32
33
  # Connect and register a controllable player in the background. +token+ is a
33
34
  # Rocksky access token (JWT); +name+ is the device-picker label; +url+
@@ -79,7 +80,10 @@ module Rocksky
79
80
 
80
81
  # Advertise the currently-playing track. Call whenever it changes, and
81
82
  # periodically (~every 1–4s) with a fresh +elapsedMs+ so controllers show
82
- # smooth progress. +track+ is a camelCase Hash.
83
+ # smooth progress. +track+ is a camelCase Hash. Optional audio info —
84
+ # +codec+ (audio codec/container, e.g. "mp3", "flac") and +sampleRate+
85
+ # (sample rate in Hz, e.g. 44100) — is included in the track payload when
86
+ # set, and omitted otherwise.
83
87
  def set_now_playing(track)
84
88
  Rocksky.unwrap(C.rocksky_remote_player_set_now_playing(@ptr, JSON.generate(track)))
85
89
  end
@@ -164,7 +168,9 @@ module Rocksky
164
168
  # Register a handler for a server event type, one of +:devices+,
165
169
  # +:device_registered+, +:device_unregistered+, +:primary_changed+,
166
170
  # +:now_playing+, +:status+, +:queue+. The block receives the full
167
- # symbol-keyed event Hash. Returns +self+ for chaining.
171
+ # symbol-keyed event Hash. A +:now_playing+ event carries the advertised
172
+ # track under +:track+, including optional +:codec+ / +:sampleRate+ audio
173
+ # info when the player supplied it. Returns +self+ for chaining.
168
174
  def on(event_type, &block)
169
175
  @handlers[event_type.to_sym] = block
170
176
  self
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rocksky
4
- VERSION = "0.9.1"
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.9.1
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