Metadata-Version: 2.4
Name: rocksky
Version: 0.9.0
Summary: Python SDK for Rocksky — native bindings to the shared Rust core
Project-URL: Homepage, https://rocksky.app
Project-URL: Documentation, https://docs.rocksky.app
Project-URL: Repository, https://github.com/tsirysndr/rocksky
Project-URL: Issues, https://github.com/tsirysndr/rocksky/issues
Author: Rocksky
License: MIT
License-File: LICENSE
Keywords: atproto,bluesky,lastfm,music,rocksky,scrobble
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# rocksky — Python SDK

Python bindings to the shared Rocksky Rust core (`rocksky-sdk`) via
[UniFFI](https://mozilla.github.io/uniffi-rs/): AppView reads, AT Protocol PDS
writes (scrobble fan-out, like, follow, shout, now-playing), a local
duplicate-prevention index, and the identity hashes — the same engine behind
every Rocksky SDK (Ruby, Kotlin, Clojure, Erlang/Elixir/Gleam).

## Install

```sh
pip install rocksky==0.7.0        # or: uv add rocksky==0.7.0
```

The wheel is pure-Python; the ~14 MB native library is fetched from the GitHub
release on first import and cached (checksum-verified). For a local checkout,
build it once:

```sh
./build-core.sh
```

## Quickstart

```python
from rocksky import AppView, Agent, ScrobbleInput, song_hash

# Reads — unauthenticated. Pass a base URL to override https://api.rocksky.app.
av = AppView()  # defaults to https://api.rocksky.app; AppView(base, token) for auth-gated reads
print(av.global_stats().scrobbles)
for t in av.top_tracks(10, 0):
    print(t.artist, "—", t.title)

# Typed date-window charts, and a bare title+artist resolved to canonical metadata.
from rocksky import DateInterval
for t in av.top_tracks_interval(10, 0, DateInterval.LAST_DAYS(days=7)):
    print(t.artist, "—", t.title)
meta = av.match_song("Chaser", "Calibro 35", None, None)  # JSON string

# Writes — log in once (session persisted at the given path).
agent = Agent.login_password("session.json", "alice.bsky.social", "app-password", None, None)
out = agent.scrobble(ScrobbleInput(
    title="Chaser", artist="Calibro 35",
    album="Jazzploitation", album_artist="Calibro 35", duration_ms=182320,
))
print(out.scrobble_uri)   # also artist_uri / album_uri / song_uri
```

## API

### Reads — `AppView(base=None, token=None)`

Pass a base URL to target a custom AppView; pass `token` to send
`Authorization: Bearer <token>` on every read (for auth-gated queries).

**Typed views** (return dataclasses): `profile(actor)`,
`scrobbles(actor, limit, offset)`, `songs(actor, limit, offset)`,
`albums(actor, limit, offset)`, `artists(actor, limit, offset)`,
`loved_songs(actor, limit, offset)`, `catalog_albums`, `catalog_artists`,
`catalog_songs`, `album_tracks(uri)`, `artist_albums(uri)`, `artist_tracks`,
`scrobble_feed`, `scrobble(uri)` (single), `follows`, `followers`,
`known_followers`, `top_tracks(limit, offset)`, `top_artists(limit, offset)`,
`global_stats()`.

**Typed date-window charts**: `top_tracks_interval(limit, offset, interval)` and
`top_artists_interval(...)`, where `interval` is a `DateInterval`:
`DateInterval.ALL_TIME`, `DateInterval.LAST_DAYS(days=7)`,
`DateInterval.LAST_WEEKS(weeks=n)`, `DateInterval.LAST_MONTHS(months=n)`,
`DateInterval.LAST_YEARS(years=n)`, `DateInterval.RANGE(start=..., end=...)`
(RFC-3339 bounds). `top_tracks` / `top_artists` are the all-time shorthands.

**Match**: `match_song(title, artist, mb_id, isrc)` resolves a bare title+artist
into full canonical metadata (returns a JSON string).

**Raw-JSON long tail** (each returns a JSON string): `album`, `artist`, `song`,
`playlists`, `playlist`, `stats`, `wrapped`, `scrobbles_chart`,
`recommendations`, `neighbours`, shouts, and more.

**Universal escape hatch**: `av.get(nsid, params)` calls *any* read query by
NSID — `params` is a `dict[str, str]`, and it returns a JSON string.

### Writes — `Agent`

`Agent.login_password(session_path, identifier, password, appview=None, dedup_path=None)`
returns an agent. Then:

- `scrobble(ScrobbleInput)` → `ScrobbleResult` — from full metadata, writes
  **artist + album + song + scrobble**, skipping any that already exist (with a
  dedup store).
- `scrobble_match(ScrobbleMatchInput)` → `ScrobbleResult` — resolves the track
  via `match_song` first, then fans out the same way. `ScrobbleMatchInput(title,
  artist, album=None, mb_id=None, isrc=None, timestamp=None)` — `title`/`artist`
  required; `album` overrides the resolved album, `mb_id`/`isrc` are match
  anchors, `timestamp` is the scrobbled-at Unix seconds
  (e.g. `agent.scrobble_match(ScrobbleMatchInput(title="Chaser", artist="Calibro 35"))`).
- `create_song(SongInput)`, `create_album(AlbumInput)`, `create_artist(ArtistInput)`
- `like(uri, cid)`, `unlike(uri)`, `follow(did)`, `unfollow(did)`
- `shout(subject_uri, subject_cid, message)`, `reply_shout(...)`
- `set_now_playing(NowPlayingInput)`, `clear_now_playing()`
- `refresh_session()`, `did()`, `profile()`
- `sync_repo()` — mirror the repo into the local dedup index, returning per-collection
  counts as JSON (needs `dedup_path`)
- `hydrate_from_jetstream()` — keep the dedup index live off the Jetstream firehose
  (needs `dedup_path`)

### Identity hashes

`song_hash(title, artist, album)`, `album_hash(album, album_artist)`,
`artist_hash(album_artist)` — lowercase-hex SHA-256, identical to the server and
every other Rocksky SDK.

## Example

```sh
python examples/native_core.py
```

## License

MIT.
