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 +7 -0
- data/README.md +61 -0
- data/lib/atradio/native.rb +107 -0
- data/lib/atradio.rb +147 -0
- data/lib/manifest.json +12 -0
- metadata +83 -0
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: []
|