Packages

Idiomatic Elixir SDK for Rockbox — pipe-friendly, builder-friendly, with real-time event subscriptions and a plugin system.

Current section

Files

Jump to
rockbox_ex lib rockbox library.ex
Raw

lib/rockbox/library.ex

defmodule Rockbox.Library do
@moduledoc """
Browse and manage the music library — albums, artists, tracks, likes,
search, and rescan.
"""
alias Rockbox.{Album, Artist, Client, SearchResults, Track, Transport, Util}
@track_fields ~S"""
fragment TrackFields on Track {
id title artist album genre disc trackString yearString
composer comment albumArtist grouping
discnum tracknum layer year bitrate frequency
filesize length elapsed path
albumId artistId genreId albumArt
}
"""
@album_fields ~S"""
fragment AlbumFields on Album {
id title artist year yearString albumArt md5 artistId copyrightMessage
}
"""
@artist_fields ~S"""
fragment ArtistFields on Artist {
id name bio image
}
"""
# ---------------------------------------------------------------------------
# Albums
# ---------------------------------------------------------------------------
@doc "List every album. Each album's `:tracks` are stub records (id/title/path/length/album_art)."
@spec albums(Client.t()) :: {:ok, [Album.t()]} | {:error, Exception.t()}
def albums(client) do
query =
@album_fields <>
"query Albums { albums { ...AlbumFields tracks { id title path length albumArt } } }"
with {:ok, %{"albums" => list}} <- Transport.execute(client, query) do
{:ok, Enum.map(list, &album_with_tracks/1)}
end
end
@spec albums!(Client.t()) :: [Album.t()]
def albums!(client), do: bang(albums(client))
@doc "Get a single album with full track info, or `{:ok, nil}`."
@spec album(Client.t(), String.t()) :: {:ok, Album.t() | nil} | {:error, Exception.t()}
def album(client, id) do
query =
@track_fields <>
@album_fields <>
"query Album($id: String!) { album(id: $id) { ...AlbumFields tracks { ...TrackFields } } }"
with {:ok, %{"album" => raw}} <- Transport.execute(client, query, %{id: id}) do
{:ok, album_with_tracks(raw)}
end
end
@spec album!(Client.t(), String.t()) :: Album.t() | nil
def album!(client, id), do: bang(album(client, id))
@doc "List albums the user has liked."
@spec liked_albums(Client.t()) :: {:ok, [Album.t()]} | {:error, Exception.t()}
def liked_albums(client) do
query = @album_fields <> "query LikedAlbums { likedAlbums { ...AlbumFields } }"
with {:ok, %{"likedAlbums" => list}} <- Transport.execute(client, query) do
{:ok, Util.to_struct_list(Album, list)}
end
end
@spec like_album(Client.t(), String.t()) :: :ok | {:error, Exception.t()}
def like_album(client, id),
do:
void(
Transport.execute(client, "mutation LikeAlbum($id: String!) { likeAlbum(id: $id) }", %{
id: id
})
)
@spec unlike_album(Client.t(), String.t()) :: :ok | {:error, Exception.t()}
def unlike_album(client, id),
do:
void(
Transport.execute(
client,
"mutation UnlikeAlbum($id: String!) { unlikeAlbum(id: $id) }",
%{
id: id
}
)
)
# ---------------------------------------------------------------------------
# Artists
# ---------------------------------------------------------------------------
@doc "List every artist with shallow album info."
@spec artists(Client.t()) :: {:ok, [Artist.t()]} | {:error, Exception.t()}
def artists(client) do
query =
@artist_fields <>
"query Artists { artists { ...ArtistFields albums { id title albumArt year } } }"
with {:ok, %{"artists" => list}} <- Transport.execute(client, query) do
{:ok, Enum.map(list, &artist_with_albums/1)}
end
end
@spec artists!(Client.t()) :: [Artist.t()]
def artists!(client), do: bang(artists(client))
@doc "Get a single artist with their albums and tracks."
@spec artist(Client.t(), String.t()) :: {:ok, Artist.t() | nil} | {:error, Exception.t()}
def artist(client, id) do
query =
@artist_fields <>
@track_fields <>
"""
query Artist($id: String!) {
artist(id: $id) {
...ArtistFields
albums { id title albumArt year yearString md5 artistId tracks { id title path length } }
tracks { ...TrackFields }
}
}
"""
with {:ok, %{"artist" => raw}} <- Transport.execute(client, query, %{id: id}) do
{:ok, artist_with_albums(raw)}
end
end
@spec artist!(Client.t(), String.t()) :: Artist.t() | nil
def artist!(client, id), do: bang(artist(client, id))
# ---------------------------------------------------------------------------
# Tracks
# ---------------------------------------------------------------------------
@doc "List every track."
@spec tracks(Client.t()) :: {:ok, [Track.t()]} | {:error, Exception.t()}
def tracks(client) do
query = @track_fields <> "query Tracks { tracks { ...TrackFields } }"
with {:ok, %{"tracks" => list}} <- Transport.execute(client, query) do
{:ok, Util.to_struct_list(Track, list)}
end
end
@spec tracks!(Client.t()) :: [Track.t()]
def tracks!(client), do: bang(tracks(client))
@doc "Get a single track by id, or `{:ok, nil}`."
@spec track(Client.t(), String.t()) :: {:ok, Track.t() | nil} | {:error, Exception.t()}
def track(client, id) do
query = @track_fields <> "query Track($id: String!) { track(id: $id) { ...TrackFields } }"
with {:ok, %{"track" => raw}} <- Transport.execute(client, query, %{id: id}) do
{:ok, Util.to_struct(Track, raw)}
end
end
@spec track!(Client.t(), String.t()) :: Track.t() | nil
def track!(client, id), do: bang(track(client, id))
@doc "List tracks the user has liked."
@spec liked_tracks(Client.t()) :: {:ok, [Track.t()]} | {:error, Exception.t()}
def liked_tracks(client) do
query = @track_fields <> "query LikedTracks { likedTracks { ...TrackFields } }"
with {:ok, %{"likedTracks" => list}} <- Transport.execute(client, query) do
{:ok, Util.to_struct_list(Track, list)}
end
end
@spec like_track(Client.t(), String.t()) :: :ok | {:error, Exception.t()}
def like_track(client, id),
do:
void(
Transport.execute(client, "mutation LikeTrack($id: String!) { likeTrack(id: $id) }", %{
id: id
})
)
@spec unlike_track(Client.t(), String.t()) :: :ok | {:error, Exception.t()}
def unlike_track(client, id),
do:
void(
Transport.execute(
client,
"mutation UnlikeTrack($id: String!) { unlikeTrack(id: $id) }",
%{
id: id
}
)
)
# ---------------------------------------------------------------------------
# Search
# ---------------------------------------------------------------------------
@doc "Full-text search across artists, albums and tracks."
@spec search(Client.t(), String.t()) :: {:ok, SearchResults.t()} | {:error, Exception.t()}
def search(client, term) do
query =
@track_fields <>
@album_fields <>
@artist_fields <>
"""
query Search($term: String!) {
search(term: $term) {
artists { ...ArtistFields }
albums { ...AlbumFields }
tracks { ...TrackFields }
likedTracks { ...TrackFields }
likedAlbums { ...AlbumFields }
}
}
"""
with {:ok, %{"search" => raw}} <- Transport.execute(client, query, %{term: term}) do
atomized = Util.atomize(raw)
{:ok,
%SearchResults{
artists: Util.to_struct_list(Artist, atomized.artists),
albums: Util.to_struct_list(Album, atomized.albums),
tracks: Util.to_struct_list(Track, atomized.tracks),
liked_tracks: Util.to_struct_list(Track, atomized.liked_tracks),
liked_albums: Util.to_struct_list(Album, atomized.liked_albums)
}}
end
end
@spec search!(Client.t(), String.t()) :: SearchResults.t()
def search!(client, term), do: bang(search(client, term))
# ---------------------------------------------------------------------------
# Library management
# ---------------------------------------------------------------------------
@doc "Trigger a full rescan of the configured `music_dir`."
@spec scan(Client.t()) :: :ok | {:error, Exception.t()}
def scan(client),
do: void(Transport.execute(client, "mutation ScanLibrary { scanLibrary }"))
# ---------------------------------------------------------------------------
# Internal
# ---------------------------------------------------------------------------
defp album_with_tracks(nil), do: nil
defp album_with_tracks(raw) do
atomized = Util.atomize(raw)
base = Util.to_struct(Album, raw)
%{base | tracks: Util.to_struct_list(Track, Map.get(atomized, :tracks, []))}
end
defp artist_with_albums(nil), do: nil
defp artist_with_albums(raw) do
atomized = Util.atomize(raw)
base = Util.to_struct(Artist, raw)
albums =
atomized
|> Map.get(:albums, [])
|> Enum.map(&album_with_tracks/1)
tracks = Util.to_struct_list(Track, Map.get(atomized, :tracks, []))
%{base | albums: albums, tracks: tracks}
end
defp void({:ok, _}), do: :ok
defp void(err), do: err
defp bang({:ok, value}), do: value
defp bang(:ok), do: :ok
defp bang({:error, exception}), do: raise(exception)
end