{"id":"tonybolivar/lastfm-luau","name":"lastfm-luau","scope":"tonybolivar","platform":"roblox","description":"Last.fm API client for Luau (Roblox + Lune). Typed, async, with DataStore-backed session persistence.","version":"1.0.4","latest":"1.0.4","versions":["1.0.0","1.0.1","1.0.2","1.0.3","1.0.4"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"77884031f4824d5a3c09062c4cfb133a74b7e628e46aa38b3e237fdf264cf168","likes":0,"downloads":0,"install":"forest install tonybolivar/lastfm-luau","url":"https://forest.dev/p/roblox/tonybolivar/lastfm-luau","files":"https://api.forest.dev/ai/package/roblox/tonybolivar/lastfm-luau/files","readme":"# lastfm-luau\n\nAPI client for Luau. Roblox and Lune.\n\n## Install\n\n### Wally (Roblox)\n\n```toml\n[dependencies]\nLastFM = \"tonybolivar/lastfm-luau@1.0.4\"\n```\n\nThen `wally install`.\n\n### Lune\n\nClone and `require(\"./src\")`.\n\n## Quick start\n\n```lua\n--!strict\nlocal LastFM = require(ReplicatedStorage.Packages.LastFM)\n\nlocal client = LastFM.new({\n    ApiKey = \"...\",\n    Secret = \"...\",\n    UserAgent = \"MyGame/1.0\",\n})\n\nlocal info = client.Track:GetInfoAsync(\"Artist\", \"Track\"):Await():UnwrapOk()\nprint(info.Name, info.Artist.Name, info.PlayCount)\n\nlocal page = client.User:GetRecentTracksAsync(\"username\", { Limit = 50 }):Await():UnwrapOk()\nfor _, track in page.Items do\n    print(track.Date.Iso8601, track.Name, track.Artist.Name)\nend\nif page:HasMore() then\n    local next = page:NextAsync():Await():UnwrapOk()\nend\n```\n\n## Auth + DataStore persistence\n\n```lua\nlocal Players = game:GetService(\"Players\")\nlocal store = LastFM.SessionStore.RobloxDataStore.New({ Name = \"Sessions\", Scope = \"v1\" })\nlocal client = LastFM.new({ ApiKey = \"...\", Secret = \"...\", SessionStore = store })\n\nPlayers.PlayerAdded:Connect(function(player)\n    local loaded = client:LoadSessionAsync(player.UserId):Await()\n    if loaded:IsOk() and loaded:UnwrapOk() then\n        return\n    end\n\n    local token = client.Auth:GetTokenAsync():Await():UnwrapOk()\n    local approval = client.Auth:GetApprovalUrl(token)\n    LastFM.AuthGui.Show(player.PlayerGui, approval.Url)\n\n    local session = client.Auth:GetSessionAsync(token):Await():UnwrapOk()\n    client:SetSession(session)\n    client:SaveSessionAsync(player.UserId):Await():UnwrapOk()\nend)\n```\n\n## Writes\n\nRequire an active session:\n\n```lua\nclient.Track:LoveAsync(\"Artist\", \"Track\"):Await():UnwrapOk()\n\nclient.Track:ScrobbleAsync({\n    { Artist = \"A\", Track = \"T1\", Timestamp = os.time() - 240 },\n    { Artist = \"A\", Track = \"T2\", Timestamp = os.time() - 480 },\n}):Await():UnwrapOk()\n\nclient.Track:UpdateNowPlayingAsync(\"Artist\", \"Track\"):Await():UnwrapOk()\n```\n\n## Configuration\n\n| Field | Default | Notes |\n|---|---|---|\n| `ApiKey` | required | |\n| `Secret` | nil | Required for signed methods (auth, writes) |\n| `UserAgent` | `lastfm-luau/1.0` | Prepended to your value if provided |\n| `HttpClient` | auto-detect | Inject for tests or custom transport |\n| `Cache` | `InMemoryCache` | Implement the `Cache` interface to swap |\n| `SessionStore` | nil | First-class persistence, see auth section |\n| `RateLimit` | `{ RequestsPerSecond = 5, Burst = 5 }` | Token bucket, yields by default |\n| `Retry` | `{ Attempts = 3, BaseSeconds = 0.5, CapSeconds = 30 }` | Exponential backoff with full jitter |\n| `Logger` | noop | `(level, message, ctx) -> ()` |\n| `AutoInvalidateStoredSessions` | true | Drop session on API code 9 |\n\n## Security (Roblox)\n\n**Construct the client on the server only.** `Config.Secret` and session keys grant write access to user accounts and must never reach the client.\n\n- Put `LastFM.new(...)` in `ServerScriptService`, never `LocalScript`.\n- Don't return session keys through `RemoteFunction`.\n- For client-driven UIs, send the intent over `RemoteEvent` (the parameters) and execute the signed call from the server.\n- `Secret.New(...)` masks the value in `tostring` and blocks mutation, revealed only at signing time.\n\n## Lune usage\n\n```lua\nlocal LastFM = require(\"./src\")\nlocal client = LastFM.new({ ApiKey = \"...\" })\nlocal info = client.Track:GetInfoAsync(\"Artist\", \"Track\"):Await():UnwrapOk()\nprint(info.Name)\n```\n\n## Demo\n\n![DeathCow01 profile dump in Roblox Studio](images/demo.png)\n\n## License\n\n[MIT](LICENSE).\n","readmeTruncated":false}