atradio 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2184b3dda3768d90ecd29545dd6caf206ab01350468e1d421df4c9058133f715
4
+ data.tar.gz: 425ce500b2eaddf704bd4c2c32866370ddf25ff5e49d35fab60e88d86a24dc17
5
+ SHA512:
6
+ metadata.gz: 103bd19a3eb1d18d944cf28950c01fd20fd43e813ade85dd8c08b9a58331ed0ef06ae626f46d216086ef0818209e7ac709ce1f361067b5382464df9d7cd7d143
7
+ data.tar.gz: 1cb1687bdfcc56dc20cd12ea31981460abb304fbd9ff8ed64f45e45e10fdb55a5f86c4f6973abd64ecc7c26b8c8e8cab059132529b3c21d0d7e152ab4695ed6b
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # atradio (Ruby SDK)
2
+
3
+ The official **Ruby SDK** for [atradio.fm](https://atradio.fm). It binds to the
4
+ shared Rust core (`atradio-sdk`) through the crate's plain **C ABI** using Ruby's
5
+ stdlib [`fiddle`](https://docs.ruby-lang.org/en/master/Fiddle.html) — no `ffi`
6
+ gem, no codegen. The auth / record / reconcile logic is identical to the Rust,
7
+ Go, TypeScript, and Python SDKs.
8
+
9
+ ## Setup
10
+
11
+ The native library is a build artifact. Build it once, then run:
12
+
13
+ ```bash
14
+ cd sdk/ruby
15
+ ./build.sh # cargo build + copy the native lib into lib/
16
+ ruby examples/smoke.rb
17
+ ```
18
+
19
+ ## Interactive console (IRB)
20
+
21
+ Play with the SDK in a REPL — the `Atradio` module and `Atradio::Agent` are
22
+ loaded and ready:
23
+
24
+ ```bash
25
+ bin/console # or: rake console
26
+ ```
27
+
28
+ ```ruby
29
+ Atradio.recent_stations(5)
30
+ Atradio.favorite_rkey("rb:...")
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```ruby
36
+ require "atradio"
37
+
38
+ # Reads — unauthenticated.
39
+ Atradio.recent_stations(10).each { |s| puts s.dig("station", "name") }
40
+ Atradio.popular_stations(10)
41
+ Atradio.favorites("alice.bsky.social")
42
+
43
+ # The favorite record key matches every other atradio SDK.
44
+ Atradio.favorite_rkey("rb:...") # 16-char hex
45
+
46
+ # Writes — app-password login (persists a session file). Stations are Hashes
47
+ # with camelCase keys (stationId, name, streamUrl, source).
48
+ agent = Atradio::Agent.login("session.json", "alice.bsky.social", "app-password")
49
+ agent.favorite("stationId" => "rb:...", "name" => "KEXP",
50
+ "streamUrl" => "https://…", "source" => "radio-browser")
51
+ agent.set_play_status(station)
52
+ agent.close
53
+ ```
54
+
55
+ ## How it works
56
+
57
+ - `crates/atradio-uniffi` exposes a plain C ABI (`capi.rs`): opaque agent handle,
58
+ JSON `{"ok"|"error"}` envelopes, `atradio_string_free`.
59
+ - `lib/atradio.rb` declares those functions with `fiddle` and marshals JSON —
60
+ the whole binding is one file, no gem dependency beyond stdlib `fiddle`.
61
+ - `build.sh` compiles the crate and drops the native library beside the module.
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Resolves the native atradio-uniffi library, downloading a prebuilt from the
4
+ # GitHub release on first use when it isn't already present locally.
5
+ #
6
+ # Order of preference:
7
+ # 1. libatradio_uniffi.<ext> next to this file — a local `./build.sh` dev build.
8
+ # 2. A checksum-verified copy in the user cache, downloaded on first load
9
+ # (the published-gem path: the ~11 MB lib is not bundled in the gem).
10
+ #
11
+ # The gem ships lib/manifest.json (repo, release tag, one sha256 per target
12
+ # triple) — filled from the release artifacts by sdk/scripts/gen-uniffi-manifest.sh.
13
+ require "json"
14
+ require "rbconfig"
15
+ require "digest"
16
+ require "fileutils"
17
+ require "net/http"
18
+ require "uri"
19
+
20
+ module Atradio
21
+ module Native
22
+ module_function
23
+
24
+ def ext
25
+ case RbConfig::CONFIG["host_os"]
26
+ when /darwin/ then "dylib"
27
+ when /mswin|mingw|cygwin/ then "dll"
28
+ else "so"
29
+ end
30
+ end
31
+
32
+ def arch
33
+ a = RbConfig::CONFIG["host_cpu"]
34
+ case a
35
+ when /x86_64|amd64/ then "x86_64"
36
+ when /arm64|aarch64/ then "aarch64"
37
+ else a
38
+ end
39
+ end
40
+
41
+ # Canonical target triple matching the release asset names.
42
+ def triple
43
+ case RbConfig::CONFIG["host_os"]
44
+ when /darwin/ then "#{arch}-apple-darwin"
45
+ when /linux/ then "#{arch}-linux-gnu"
46
+ when /freebsd/ then "#{arch}-unknown-freebsd"
47
+ when /netbsd/ then "#{arch}-unknown-netbsd"
48
+ when /openbsd/ then "#{arch}-unknown-openbsd"
49
+ else raise Error, "unsupported platform: #{RbConfig::CONFIG["host_os"]}"
50
+ end
51
+ end
52
+
53
+ def lib_dir
54
+ File.expand_path("..", __dir__) # sdk/ruby/lib
55
+ end
56
+
57
+ def cache_dir(tag)
58
+ base = ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache")
59
+ File.join(base, "atradio", tag)
60
+ end
61
+
62
+ def manifest
63
+ JSON.parse(File.read(File.join(lib_dir, "manifest.json")))
64
+ end
65
+
66
+ # Absolute path to a loadable native library, fetching it if necessary.
67
+ def resolve
68
+ local = File.join(lib_dir, "libatradio_uniffi.#{ext}")
69
+ return local if File.exist?(local)
70
+
71
+ m = manifest
72
+ t = triple
73
+ sha = m.dig("checksums", t) or
74
+ raise Error, "no prebuilt native lib for #{t} (manifest has no checksum)"
75
+ tag = m["tag"]
76
+ dest = File.join(cache_dir(tag), "libatradio_uniffi-#{t}.#{ext}")
77
+ return dest if File.exist?(dest) && Digest::SHA256.file(dest).hexdigest == sha
78
+
79
+ download_verify(m["repo"], tag, t, sha, dest)
80
+ dest
81
+ end
82
+
83
+ def download_verify(repo, tag, triple, sha, dest)
84
+ url = "https://github.com/#{repo}/releases/download/#{tag}/libatradio_uniffi-#{triple}.#{ext}"
85
+ body = fetch(url)
86
+ got = Digest::SHA256.hexdigest(body)
87
+ raise Error, "checksum mismatch for #{triple}: want #{sha}, got #{got}" unless got == sha
88
+
89
+ FileUtils.mkdir_p(File.dirname(dest))
90
+ tmp = "#{dest}.download"
91
+ File.binwrite(tmp, body)
92
+ File.chmod(0o755, tmp)
93
+ File.rename(tmp, dest)
94
+ end
95
+
96
+ def fetch(url, limit = 5)
97
+ raise Error, "too many redirects" if limit.zero?
98
+
99
+ res = Net::HTTP.get_response(URI(url))
100
+ case res
101
+ when Net::HTTPSuccess then res.body
102
+ when Net::HTTPRedirection then fetch(res["location"], limit - 1)
103
+ else raise Error, "download failed (#{res.code}) for #{url}"
104
+ end
105
+ end
106
+ end
107
+ end
data/lib/atradio.rb ADDED
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Official Ruby SDK for atradio.fm.
4
+ #
5
+ # A thin binding, via Ruby's stdlib `fiddle` (no `ffi` gem), to the plain C ABI
6
+ # of the shared Rust core (`atradio-uniffi` → `atradio-sdk`). The auth / record /
7
+ # reconcile logic is identical to the Rust, Go, TypeScript, and Python SDKs.
8
+ require "fiddle"
9
+ require "fiddle/import"
10
+ require "json"
11
+
12
+ module Atradio
13
+ VERSION = "0.1.0"
14
+
15
+ # Raised on native-lib resolution failure or an `{"error": …}` envelope.
16
+ class Error < StandardError; end
17
+ end
18
+
19
+ require_relative "atradio/native"
20
+
21
+ module Atradio
22
+ # Local dev build if present, else a checksum-verified download from the
23
+ # GitHub release (cached). See Atradio::Native.
24
+ LIB_PATH = Native.resolve
25
+
26
+ module C
27
+ extend Fiddle::Importer
28
+ dlload Atradio::LIB_PATH
29
+ [
30
+ "char* atradio_recent_stations(const char*, unsigned int)",
31
+ "char* atradio_popular_stations(const char*, unsigned int)",
32
+ "char* atradio_global_recently_played(const char*, unsigned int)",
33
+ "char* atradio_favorites(const char*, const char*, unsigned int)",
34
+ "char* atradio_favorite_rkey(const char*)",
35
+ "void atradio_string_free(void*)",
36
+ "void* atradio_agent_login(const char*, const char*, const char*, const char*)",
37
+ "char* atradio_last_error()",
38
+ "void atradio_agent_free(void*)",
39
+ "char* atradio_agent_favorite(void*, const char*)",
40
+ "char* atradio_agent_unfavorite(void*, const char*)",
41
+ "char* atradio_agent_comment(void*, const char*, const char*)",
42
+ "char* atradio_agent_set_play_status(void*, const char*)",
43
+ "char* atradio_agent_delete_play_status(void*)",
44
+ "char* atradio_agent_refresh_session(void*)"
45
+ ].each { |sig| extern sig }
46
+ end
47
+ private_constant :C
48
+
49
+ # C strings the core returns aren't length-tagged, so resolve their length
50
+ # with libc `strlen`.
51
+ STRLEN = Fiddle::Function.new(
52
+ Fiddle::Handle::DEFAULT["strlen"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_SIZE_T
53
+ )
54
+ private_constant :STRLEN
55
+
56
+ # Copy an owned C string into a Ruby string and free the original.
57
+ def self.take_string(ptr)
58
+ return nil if ptr.nil? || ptr.null?
59
+
60
+ len = STRLEN.call(ptr)
61
+ str = ptr[0, len].force_encoding("UTF-8")
62
+ C.atradio_string_free(ptr)
63
+ str
64
+ end
65
+
66
+ # Parse a `{"ok"|"error"}` envelope, raising Error on failure.
67
+ def self.unwrap(ptr)
68
+ parsed = JSON.parse(take_string(ptr))
69
+ raise Error, parsed["error"] if parsed.key?("error")
70
+
71
+ parsed["ok"]
72
+ end
73
+
74
+ # ---- reads (unauthenticated) ----
75
+
76
+ def self.recent_stations(limit = 50, base: nil)
77
+ unwrap(C.atradio_recent_stations(base.to_s, limit))
78
+ end
79
+
80
+ def self.popular_stations(limit = 50, base: nil)
81
+ unwrap(C.atradio_popular_stations(base.to_s, limit))
82
+ end
83
+
84
+ def self.global_recently_played(limit = 50, base: nil)
85
+ unwrap(C.atradio_global_recently_played(base.to_s, limit))
86
+ end
87
+
88
+ def self.favorites(actor, limit = 50, base: nil)
89
+ unwrap(C.atradio_favorites(base.to_s, actor, limit))
90
+ end
91
+
92
+ # The deterministic favorite record key — identical across every atradio SDK.
93
+ def self.favorite_rkey(station_id)
94
+ take_string(C.atradio_favorite_rkey(station_id))
95
+ end
96
+
97
+ # ---- authenticated agent ----
98
+ #
99
+ # Stations are passed as Hashes with camelCase keys (stationId, name,
100
+ # streamUrl, source, …), matching the wire record shape.
101
+ class Agent
102
+ def self.login(session_path, identifier, password, appview: nil)
103
+ ptr = C.atradio_agent_login(session_path, identifier, password, appview.to_s)
104
+ if ptr.null?
105
+ raise Error, (Atradio.take_string(C.atradio_last_error()) || "login failed")
106
+ end
107
+
108
+ new(ptr)
109
+ end
110
+
111
+ def initialize(ptr)
112
+ @ptr = ptr
113
+ end
114
+
115
+ def favorite(station)
116
+ Atradio.unwrap(C.atradio_agent_favorite(@ptr, JSON.generate(station)))
117
+ end
118
+
119
+ def unfavorite(station)
120
+ Atradio.unwrap(C.atradio_agent_unfavorite(@ptr, JSON.generate(station)))
121
+ end
122
+
123
+ def comment(station, text)
124
+ Atradio.unwrap(C.atradio_agent_comment(@ptr, JSON.generate(station), text))
125
+ end
126
+
127
+ def set_play_status(station)
128
+ Atradio.unwrap(C.atradio_agent_set_play_status(@ptr, JSON.generate(station)))
129
+ end
130
+
131
+ def delete_play_status
132
+ Atradio.unwrap(C.atradio_agent_delete_play_status(@ptr))
133
+ end
134
+
135
+ def refresh_session
136
+ Atradio.unwrap(C.atradio_agent_refresh_session(@ptr))
137
+ end
138
+
139
+ # Release the native handle. The agent is unusable afterwards.
140
+ def close
141
+ return if @ptr.null?
142
+
143
+ C.atradio_agent_free(@ptr)
144
+ @ptr = Fiddle::Pointer.new(0)
145
+ end
146
+ end
147
+ end
data/lib/manifest.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "repo": "tsirysndr/atradio.fm",
3
+ "tag": "bindings-v0.1.0",
4
+ "checksums": {
5
+ "aarch64-apple-darwin": "add588bdc7df1b3d44925df1d30b332921d091cdc2f7a21943c842c7193daca0",
6
+ "aarch64-linux-gnu": "0eebcd1592907b73caf96250a726591221f61d5669ba57ed87dfd796bf35f2cb",
7
+ "x86_64-apple-darwin": "bae2f0e29853309b5975c12e227b5cc27766263dac119fa54204d29a1a876ea6",
8
+ "x86_64-linux-gnu": "2db8025ea5f3dce745ebd6b6ecb64c90cfe8ceaa78d7f0b6afa0bfcc5da239b4",
9
+ "x86_64-unknown-freebsd": "f7d016cc10f5114404504e27b8286101381d51881bedd5b8457fe686c367bc63",
10
+ "x86_64-unknown-netbsd": "f22ff3e8972e3914b5a780fbcd8fcdad7a1a14f4ff1336b247882c45c8abe0bf"
11
+ }
12
+ }
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: atradio
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tsiry Sandratraina
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fiddle
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: irb
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ executables: []
55
+ extensions: []
56
+ extra_rdoc_files: []
57
+ files:
58
+ - README.md
59
+ - lib/atradio.rb
60
+ - lib/atradio/native.rb
61
+ - lib/manifest.json
62
+ homepage: https://github.com/tsirysndr/atradio.fm
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '3.0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 4.0.10
81
+ specification_version: 4
82
+ summary: Official Ruby SDK for atradio.fm (fiddle bindings to the shared Rust core)
83
+ test_files: []