# Lastex

A [Last.fm API](https://www.last.fm/api) client for Elixir.

## Installation

Add `lastex` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:lastex, "~> 0.1.0"}
  ]
end
```

## Configuration

Get an API key and secret by [creating a Last.fm API account](https://www.last.fm/api/account/create),
then configure them:

```elixir
config :lastex,
  api_key: System.get_env("LASTFM_API_KEY"),
  api_secret: System.get_env("LASTFM_API_SECRET")
```

The secret is only needed for authenticated calls.

## Usage

Every function returns `{:ok, result}` or `{:error, %Lastex.Error{}}`. Read
functions take options as a keyword list, which is passed through to the Last.fm
query parameters — so any option the API documents (`autocorrect`, `lang`,
`from`, `to`, …) works even when it isn't named below.

Names are always given artist-first: `Lastex.Track.info("Opeth", "Blackwater Park")`.

### User

```elixir
Lastex.User.info("rj")
Lastex.User.friends("rj", limit: 50)
Lastex.User.recent_tracks("rj", limit: 200)
Lastex.User.loved_tracks("rj", limit: 50)
Lastex.User.top_artists("rj", period: "overall", limit: 10)
Lastex.User.top_albums("rj", period: "6month", limit: 10)
Lastex.User.top_tracks("rj", period: "1month", limit: 10)
Lastex.User.top_tags("rj", limit: 10)
Lastex.User.personal_tags("rj", "progressive metal", "artist")

# Weekly charts
Lastex.User.weekly_chart_list("rj")
Lastex.User.weekly_artist_chart("rj", from: 1_108_296_000, to: 1_108_900_800)
Lastex.User.weekly_album_chart("rj")
Lastex.User.weekly_track_chart("rj")
```

`period` is one of `"overall"`, `"7day"`, `"1month"`, `"3month"`, `"6month"`,
or `"12month"`.

### Artist

```elixir
Lastex.Artist.info("Opeth")
Lastex.Artist.correction("opeth")
Lastex.Artist.similar("Opeth", limit: 10)
Lastex.Artist.tags("Opeth", user: "rj")
Lastex.Artist.top_tags("Opeth")
Lastex.Artist.top_albums("Opeth", limit: 10)
Lastex.Artist.top_tracks("Opeth", limit: 10)
Lastex.Artist.search("Opeth", limit: 5)
```

### Album

```elixir
Lastex.Album.info("Opeth", "Blackwater Park")
Lastex.Album.tags("Opeth", "Blackwater Park", user: "rj")
Lastex.Album.top_tags("Opeth", "Blackwater Park")
Lastex.Album.search("Blackwater Park", limit: 5)
```

### Track

```elixir
Lastex.Track.info("Opeth", "Blackwater Park")
Lastex.Track.correction("opeth", "blackwater park")
Lastex.Track.similar("Opeth", "Blackwater Park", limit: 10)
Lastex.Track.tags("Opeth", "Blackwater Park", user: "rj")
Lastex.Track.top_tags("Opeth", "Blackwater Park")
Lastex.Track.search("Blackwater Park", limit: 5)
```

### Tag

```elixir
Lastex.Tag.info("progressive metal")
Lastex.Tag.similar("progressive metal")
Lastex.Tag.top_artists("progressive metal", limit: 10)
Lastex.Tag.top_albums("progressive metal", limit: 10)
Lastex.Tag.top_tracks("progressive metal", limit: 10)
Lastex.Tag.top_tags()
Lastex.Tag.weekly_chart_list("progressive metal")
```

### Chart, Geo, and Library

```elixir
Lastex.Chart.top_artists(limit: 10)
Lastex.Chart.top_tracks(limit: 10)
Lastex.Chart.top_tags(limit: 10)

Lastex.Geo.top_artists("Sweden", limit: 10)
Lastex.Geo.top_tracks("Sweden", limit: 10)

Lastex.Library.artists("rj", limit: 50)
```

## Authentication

Write operations need a session key, obtained by having the user authorize your
API key. Session keys do not expire, so store them alongside the user.

### Desktop flow

```elixir
# 1. Fetch an unauthorized request token
{:ok, %{"token" => token}} = Lastex.Auth.token()

# 2. Send the user to authorize it in their browser
Lastex.Auth.url(token)
#=> "https://www.last.fm/api/auth/?api_key=YOUR_KEY&token=TOKEN"

# 3. Once they've authorized, exchange the token for a session key
{:ok, %{"session" => %{"key" => session_key, "name" => username}}} =
  Lastex.Auth.session(token)
```

### Web flow

Send the user to `https://www.last.fm/api/auth/?api_key=YOUR_KEY`. Last.fm
redirects them to the callback URL registered on your API account with a `token`
query parameter, which you exchange via `Lastex.Auth.session/1` as above. Tokens
are single-use and valid for 60 minutes.

### Mobile flow

For applications that collect credentials in their own UI. Requires the mobile
authentication permission on your API account.

```elixir
{:ok, %{"session" => %{"key" => session_key}}} =
  Lastex.Auth.mobile_session("username", "password")
```

### Authenticated calls

Pass the session key as the last argument:

```elixir
Lastex.Track.love("Opeth", "Blackwater Park", session_key)
Lastex.Track.unlove("Opeth", "Blackwater Park", session_key)

Lastex.Artist.add_tags("Opeth", "progressive metal,swedish", session_key)
Lastex.Artist.remove_tag("Opeth", "swedish", session_key)

Lastex.Album.add_tags("Opeth", "Blackwater Park", "progressive metal", session_key)
Lastex.Album.remove_tag("Opeth", "Blackwater Park", "progressive metal", session_key)

Lastex.Track.add_tags("Opeth", "Blackwater Park", "progressive metal", session_key)
Lastex.Track.remove_tag("Opeth", "Blackwater Park", "progressive metal", session_key)
```

Signing is automatic. Lastex builds the `api_sig` by sorting all parameters
alphabetically (excluding `format` and `callback`), concatenating them as
`<name><value>`, appending your API secret, and taking the MD5 hash.

## Pagination

List endpoints return a `Lastex.Page` struct and accept `page` and `limit`:

```elixir
{:ok, page} = Lastex.User.recent_tracks("rj", page: 1, limit: 200)

page.data         # the list of results
page.page         # 1
page.per_page     # 200
page.total        # 85_000
page.total_pages  # 425
```

Endpoints returning a single entity — `Lastex.User.info/1`, `Lastex.Artist.info/2`,
`Lastex.Album.info/3`, `Lastex.Track.info/3`, `Lastex.Tag.info/2`, the
`weekly_chart_list` functions, and the correction functions — return the decoded
response body directly.

## Error handling

```elixir
case Lastex.User.info("nonexistent") do
  {:ok, user} -> user
  {:error, %Lastex.Error{code: 6}} -> :not_found
  {:error, reason} -> {:error, reason}
end
```

`Lastex.Error` is an exception, so it also works with `raise`. Common codes:

| Code | Meaning |
|---|---|
| 6 | Invalid parameters / resource not found |
| 8 | Operation failed — try again |
| 9 | Invalid session key |
| 10 | Invalid API key |
| 11 | Service offline |
| 13 | Invalid method signature |
| 26 | API key suspended |
| 29 | Rate limit exceeded |

Transport failures pass the underlying `Req`/`Finch` reason through unchanged.

## HTTP client

Lastex uses [Req](https://hexdocs.pm/req). Options are merged into every
request, which is how the test suite stubs the API:

```elixir
config :lastex, req_options: [plug: {Req.Test, Lastex.Client}]
```

Last.fm asks for no more than 5 requests per second per key. Lastex does not
throttle for you — rate limiting is on the roadmap.

## Not yet supported

- Scrobbling (`track.scrobble`, `track.updateNowPlaying`)
- Client-side rate limiting

See the [roadmap](https://gitlab.com/tristanperalta/lastex/-/blob/main/ROADMAP.md).

## License

MIT — see [LICENSE](LICENSE).
