{"id":37028858,"url":"https://github.com/ealva-com/ealvabrainz","last_synced_at":"2026-01-14T03:27:24.284Z","repository":{"id":57718955,"uuid":"239522242","full_name":"ealva-com/eAlvaBrainz","owner":"ealva-com","description":"MusicBrainz Kotlin Retrofit libraries for Android","archived":false,"fork":false,"pushed_at":"2023-06-06T21:18:40.000Z","size":2345,"stargazers_count":3,"open_issues_count":3,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2023-06-30T05:33:58.878Z","etag":null,"topics":["android","coroutines","coverartarchive","flow","kotlin","kotlin-coroutines","kotlin-dsl","kotlin-test","moshi","musicbrainz","okhttp","retrofit"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ealva-com.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-02-10T13:46:59.000Z","updated_at":"2022-04-08T22:15:27.000Z","dependencies_parsed_at":"2022-08-27T18:30:14.029Z","dependency_job_id":null,"html_url":"https://github.com/ealva-com/eAlvaBrainz","commit_stats":null,"previous_names":[],"tags_count":0,"template":null,"template_full_name":null,"purl":"pkg:github/ealva-com/eAlvaBrainz","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ealva-com%2FeAlvaBrainz","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ealva-com%2FeAlvaBrainz/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ealva-com%2FeAlvaBrainz/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ealva-com%2FeAlvaBrainz/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ealva-com","download_url":"https://codeload.github.com/ealva-com/eAlvaBrainz/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ealva-com%2FeAlvaBrainz/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408843,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","coroutines","coverartarchive","flow","kotlin","kotlin-coroutines","kotlin-dsl","kotlin-test","moshi","musicbrainz","okhttp","retrofit"],"created_at":"2026-01-14T03:27:23.611Z","updated_at":"2026-01-14T03:27:24.270Z","avatar_url":"https://github.com/ealva-com.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"eAlvaBrainz\n===========\nKotlin [MusicBrainz][brainz]/[CoverArtArchive][coverart] [Retrofit][retrofit] libraries for Android\n\n**Currently in an beta state, mostly stable API**.\n\nA few small examples to start:\n\n```kotlin\n// Get the artist represented by the ArtistMbid and include all Misc info\nbrainzSvc.lookupArtist(mbid) { include(*Artist.Include.values()) }\n  .onSuccess { artist -\u003e handleArtist(artist, mbid) }\n  .onFailure { brainzMsg -\u003e displayError(brainzMsg.toString()) }\n\n// Get the artist Nirvana's info and include aliases\nval nirvana = ArtistMbid(\"5b11f4ce-a62d-471e-81fc-a69a8278c7da\") // maybe obtained via find\nlookupArtist(nirvana) { misc(Artist.Misc.Aliases) }\n  .onSuccess {}\n  .onFailure {}\n\n// Find releases for the artist name and release title\nval jethroTull = ArtistName(\"Jethro Tull\")\nval aqualung = AlbumTitle(\"Aqualung\")\nfindRelease(Limit(4)) { artist(jethroTull) and release(aqualung) }\n\n// Browse events for the given artist and limit the results to 15\nval metallicaMbid = ArtistMbid(\"65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab\") // maybe obtained via find\nval limit = Limit(15)\nbrowseEvents(EventBrowse.BrowseOn.Artist(metallicaMbid), limit)\n\n// Browse Releases by an artist and limit the results to official, album releases (no bootlegs or\n// promos and no singles, compilations, etc)\nval theBeatles = ArtistMbid(\"b10bbbfc-cf9e-42e0-be17-e2c3e1d2600d\") // maybe obtained via find\nbrowseReleases(ReleaseBrowse.BrowseOn.Artist(theBeatles)) {\n  types(ReleaseGroup.Type.Album)\n  status(Release.Status.Official)\n}.onSuccess { browseReleaseList -\u003e\n  // handle browse\n}.onFailure {\n  // handle error path\n}\n\n// Find all ReleaseGroups by The Beatles whose first release date was between 1967 and 1969\n// inclusively, where the release was an album, but not a compilation or interview, and was an\n// official release (not bootleg or promotion)\nfindReleaseGroup {\n  artist(ArtistName(\"The Beatles\")) and\n    firstReleaseDate { Year(\"1967\") inclusive Year(\"1969\") } and\n    primaryType(ReleaseGroup.Type.Album) and\n    !secondaryType { ReleaseGroup.Type.Compilation or ReleaseGroup.Type.Interview } and\n    status(Release.Status.Official)\n}.onSuccess { releaseGroupList -\u003e\n  // handle group list\n}.onFailure {\n  // handle error path\n}\n```\n\n# Design philosophy\nThe design philosophy is to provide a type safe interface to the MusicBrainz server, dispatching\nwork on a background thread using coroutine dispatchers, providing many of the requirements for\nwell-behaved clients (rate limiting, user agent, etc), and converting responses to easily handled\nresults. Given the complexity of a typical call path regarding the litany of possible errors with\ncalling remote servers, parsing Json, etc., special care is given to returned values. A Result\nmonad style was chosen to make sunny day and error path code straightforward and to avoid throwing\nexceptions across coroutine boundaries. This is not a functional library, but the style of\n[Railway Oriented Programming][railway] fits very nicely with handling the various result\npossibilities.\n\nFor lookup and browse functions, a call specific lambda receiver is provided to guide the client\nwith regard to what options are available without needing to know the underlying details. Find\nfunctions provide call specific lambda receivers which are the base of a relatively simple, but\nextensive, DSL for building a lucene query. This style provides type safety and attempts to\nconstrain choices to a valid set of options. Using Android Studio's basic or smart completion inside\none of the lambda receivers will display the possibilities for building the lookup, browse, or find\nin scope. Using the bare MusicBrainz retrofit client would require extensive string building which\ncan be error-prone and lacks type support. There are quite a few value (inline) classes to provide\ntype support without generating extra garbage.\n\nThe current version covers the majority of the MusicBrainz API. There is an escape hatch of\nsorts in that the client can indirectly call the Retrofit interfaces via the MusicBrainzService\ninterface and have correct dispatching and error handling behavior. This is the same method used\ninternally. There are currently no write capabilities (can't set ratings or create collections),\n\nThis is a Kotlin library and not much thought was given to possible Java clients. Input and pull\nrequests are welcome.\n\nThis repository consists of 3 parts:\n  * **ealvabrainz** - A library which consists of 2 Retrofit interfaces, MusicBrainz and CoverArt,\n    and supporting data classes to generate a MusicBrainz REST client.\n  * **ealvabrainz-service** - Higher-level abstractions that wrap the Retrofit clients with a\n    richer interface, configures necessary Retrofit/OkHttp clients, provides support for cache\n    control/throttling/user agent/authentication/etc, and dispatches calls on background threads \n    using main-safe suspend functions.\n  * **app** - Demonstrates search and lookup\n  \nCheck [here][maven-ealvabrainz] and [here][maven-ealvabrainz-service] for the latest published\nreleases. **Pull requests welcome.** \n\nFor the latest SNAPSHOT check [here][ealvabrainz-snapshot] and [here][ealvabrainz-service-snapshot]\n  \n# Libraries\n## ealvabrainz\nProvides MusicBrainz and CoverArt interfaces which Retrofit.Builder can use to generate a REST \nclient for the MusicBrainz and CoverArtArchive servers. This module contains the bulk of the code\nto build the requests and decode the responses into objects. \n\nThe data classes created as response to MusicBrainz requests, plus added annotations/JsonAdapters, \nare provided to support the Null Object Pattern. Null is avoided almost entirely (one specific case \nremains). Null Strings become empty Strings, null Lists become empty lists, and a null reference \nis replaced by a specific instance of the class - known as a Null Object. \n\nMissing objects default to the their Null Object counterparts. Checking for null is not required, \nbut it is possible to check for the Null Object via instance comparison. The Area class provides a \nshort example:\n```kotlin\n@JsonClass(generateAdapter = true)\ndata class Area(\n  var id: String = \"\",\n  var name: String = \"\",\n  @field:Json(name = \"sort-name\") var sortName: String = \"\",\n  var disambiguation: String = \"\",\n  @field:Json(name = \"iso-3166-1-codes\") var iso31661Codes: List\u003cString\u003e = emptyList()\n) {\n  companion object {\n    val NullArea = Area()\n    val fallbackMapping: Pair\u003cString, Any\u003e = Area::class.java.name to NullArea\n  }\n}\n\ninline val Area.isNullObject\n  get() = this === NullArea\n\n@JvmInline\nvalue class AreaMbid(override val value: String) : Mbid\n\ninline val Area.mbid\n  get() = AreaMbid(id)\n```\nA companion object is defined which contains the Null Object and a mapping between the class name \nand the fallback NullArea object. An extension function defines a Boolean isNullObject val. Also \nnote the AreaMbid value class. Since a MusicBrainz identifier (MBID) is just a string, these inline \nclasses are meant to differentiate types of MBID to facilitate compile time type checking.\n\nThe MusicBrainz and CoverArt interfaces are defined with suspend functions, so are only callable\nfrom a coroutine. It is expected that the service module will be used to handle constructing and\ncalling the generated Retrofit classes.\n```kotlin\ninterface CoverArt {\n  /**\n   * An example for looking up release artwork by mbid would be:\n   * https://coverartarchive.org/release/91975b77-c9f2-46d1-a03b-f1fffbda1d1c\n   *\n   * @param entity either \"release\" or \"release-group\"\n   * @param mbid the release or release-group mbid. In the example this would be:\n   * 91975b77-c9f2-46d1-a03b-f1fffbda1d1c\n   *\n   * @return the CoverArtRelease associated with the mbid, wrapped in a Response\n   */\n  @GET(\"{entity}/{mbid}\")\n  suspend fun getArtwork(\n    @Path(\"entity\") entity: String,\n    @Path(\"mbid\") mbid: String\n  ): Response\u003cCoverArtRelease\u003e\n}\n```\nNote that ```getArtwork()``` is suspending and may only be called from a coroutine. The higher level \nabstractions in ealvabrainz-service also define suspend functions along with providing [flows][flow] \nof images.    \n## ealvabrainz-service\nProvides CoverArtService and MusicBrainzService, which wrap the CoverArt and MusicBrainz Retrofit \nclients providing a higher-level function. \n#### CoverArtService    \nThe CoverArtService provides functions to retrieve artwork based on an MusicBrainz ID (MBID). It \nalso has extension functions to convert flows of MBIDs to cover art images. The CoverArtService \nimplementation builds and contains the necessary OkHttp client and Retrofit implementation of the \nCoverArt class.\n```kotlin\ninterface CoverArtService {\n\n  suspend fun getReleaseArt(mbid: ReleaseMbid): CoverArtResult\n\n  suspend fun getReleaseGroupArt(mbid: ReleaseGroupMbid): CoverArtResult\n\n  companion object {\n    /**\n     * Instantiate a CoverArtService implementation which handles MusicBrainz server requirements\n     * such as a required User-Agent format, throttling requests, and factories/adapters to support\n     * converting Json to objects.\n     */\n    operator fun invoke(\n      ctx: Context,\n      appName: String,\n      appVersion: String,\n      contactEmail: String,\n      dispatcher: CoroutineDispatcher = Dispatchers.IO\n    ): CoverArtService\n  }\n}\n\nfun Flow\u003cReleaseMbid\u003e.transform(service: CoverArtService): Flow\u003cCoverArtImageInfo\u003e\n\nfun Flow\u003cReleaseGropMbid\u003e.transform(service: CoverArtService): Flow\u003cCoverArtImageInfo\u003e\n```\n#### MusicBrainzService\nThis service is similar to CoverArtService in that it provides a higher-level abstraction and builds\nthe appropriate underlying Retrofit/OkHttp classes. MusicBrainzService has functions that take type \nspecific parameters and format these into parameters for the underlying calls to the MusicBrainz\nRetrofit client. There is also a generic ```brainz()``` function accepting a lambda which allows\ndirect calls to the MusicBrainz Retrofit client while providing correct coroutine dispatch and\nsimplifying error handling.\n\nThe MusicBrainz server API is extensively supported. Below are 3 examples of a lookup, a browse,\na find (query), and brainz function which underlies all calls to the server.\n```kotlin\ntypealias BrainzCall\u003cT\u003e = suspend MusicBrainz.() -\u003e Response\u003cT\u003e\ntypealias BrainzResult\u003cT\u003e = Result\u003cT, BrainzMessage\u003e\n\ninterface MusicBrainzService {\n  /**\n   * Find the Artist with the mbid ID. Provide an optional lambda with an ArtistLookup\n   * receiver to specify if any other information should be included.\n   */\n  suspend fun lookupArtist(\n    mbid: ArtistMbid,\n    lookup: ArtistLookup.() -\u003e Unit = {}\n  ): BrainzResult\u003cArtist\u003e\n\n  /**\n   * Browse the recordings of the entity specified by [browseOn] (eg. Artist, Collection, Release,\n   * or Work). Use [limit] and [offset] to page through the results. Provide an optional lambda with\n   * a RecordingBrowse receiver to specify if other information should be included, such as\n   * Artist Credits or some other relationships. BrowseRecordingList contains the total\n   * number of Recordings, the offset returned, and a list of Recording objects.\n   */\n  suspend fun browseRecordings(\n    browseOn: RecordingBrowse.BrowseOn,\n    limit: Limit? = null,\n    offset: Offset? = null,\n    browse: RecordingBrowse.() -\u003e Unit = {}\n  ): BrainzResult\u003cBrowseRecordingList\u003e\n\n\n  suspend fun findRelease(\n    limit: Limit? = null,\n    offset: Offset? = null,\n    search: ReleaseSearch.() -\u003e Unit\n  ): BrainzResult\u003cReleaseList\u003e\n\n  // Calls the [block]\n  suspend fun \u003cT : Any\u003e brainz(\n    block: suspend MusicBrainz.() -\u003e Response\u003cT\u003e\n  ): Result\u003cT, BrainzMessage\u003e\n}\n```\nThe MusicBrainzService is constructed with a CoverArtService instance. This allows \nMusicBrainzService to provide functionality such as:\n``` kotlin\nsuspend fun getReleaseGroupArtwork(mbid: ReleaseGroupMbid): Uri\n```\n\nA small find release group example showing the query DSL:\n```kotlin\nval result = findReleaseGroup {\n  artist(LED_ZEPPELIN) and releaseGroup(HOUSES_OF_THE_HOLY)\n}\n```\nThe ReleaseGroupSearch supports all 17 possible query fields and the term DSL support: required,\nprohibited, regular expressions, ranges, fuzzy search, proximity, and boosting. See the MusicBrainz\ndocs for details.\n```kotlin\nval revolver = Field(\"album\", Term(\"Revolver\"))\nval rubberSoul = Field(\"album\", Term(\"Rubber Soul\"))\nval beatles = Field(\"artist\", +Term(\"The Beatles\"))  // + operator indicated required term\nval exp = beatles and (revolver or rubberSoul)\nval exp2 = beatles and revolver or rubberSoul\n```\n\nMost MusicBrainzService functions return a Result\u003cT, BrainzMessage\u003e. Result\u003cV, E\u003e is a monad for \nmodelling success (Ok) or failure (Err) operations. When Result is of type Ok, the \nvalue of type T is the result of the call to MusicBrainz. If an Err is returned, the error is a\nsubtype of BrainzMessage which indicates the type of error. Result is from the \n[kotlin-result] library and provides a nice implementation for\n[Railway Oriented Programming][railway].\n\n### Building and Executing Integration Tests and App\nSeveral items must be defined in the local.properties file in the root folder of this project to\nsuccessfully build and run the app and integration tests. If the file doesn't exist, create it and\nadd the following (substituting your valid information):\n```text\nBRAINZ_APP_NAME=\"YourAppName\"\nBRAINZ_APP_VERSION=\"0.0.1\"\nBRAINZ_CONTACT_EMAIL=\"your@email.com\"\n```\nThe fields will be combined into a user agent passed to the servers.\n\nOne or more integration tests require authentication with the MusicBrainz server. The required\nusername and password must be defined in the local.properties file in the root folder of this\nproject. This file is not committed to version control as it's contents are private (obviously).\nIt would typically look something like:\n```text\nBRAINZ_USERNAME=\"my_username\"\nBRAINZ_PASSWORD=\"my_password\"\n```\nwhere BRAINZ_USERNAME and BRAINZ_PASSWORD are set to your MusicBrainz.org credentials. If you don't\nhave an account, go to https://musicbrainz.org/register and create one. Not mandatory, however not \nsetting these values correctly will result in some tests failing due to authentication errors.\n\nApplications which call functions requiring authentication must implement the CredentialsProvider\ninterface and use it when constructing a MusicBrainzService implementation.\n\n## app\nThe application demonstrates searching, browsing, and display of various MusicBrainz entities. Only\na few APIs are called - look at the integration tests for more examples.\n\nGiven Kotlin coroutines, flows, and lifecycle scope, it is easy to define flows that properly\nset and remove listeners based on component lifecycle, to conflate events, and to possibly emit\nricher objects than provided by underlying Views. Consumers only need to collect from a flow and \nthe underlying listener is properly registered/unregistered based on lifecycle.\n\nThe app uses no layout XML and instead uses a Kotlin DSL from the [Splitties][splitties] library.\nThe UI is defined in a way so as to segregate UI functionality and, as a result, classes such as\nActivities, Fragments, ViewHolders, etc. are very small. Using this DSL keeps the UI definition\nand implementation together in a single file/class, is inherently type safe/null safe, eliminates \nthe need for findViewById or view binding, eliminates reflection used during inflation, and greatly \nreduces development friction (1 language/1 class vs 2 languages/multiple files). \n\nIt's expected the app will be ported to Compose some time in the future.\n  \nOf Note\n=======\nThis library contains classes others may find useful in a different context. While not necessarily\ncanonical, these may be used as examples or a starting point:\n* Moshi annotated data classes for codegen and json adapter generation\n* Moshi combination of data class style, annotations, and adapters to support the Null Object\n  Pattern \n* Moshi annotation and adapter to support a fallback strategy for items missing from json\n  (part of Null Object pattern)\n* Moshi adapter that peeks names to determine which subtype to instantiate in relationships\n* Retrofit interfaces defined with suspend\n* Retrofit, OkHttp, and Moshi builders to fully support the Rest client\n* Sealed class Result monad return from service calls avoiding exceptions across coroutine\n  boundaries and providing transform/mapping/chaining style of result handling.\n* Coroutine test strategy with a JUnit rule and a test dispatcher (test concurrent code)\n* App uses Kotlin Views DSL for UI ([Splitties][splitties]) - no XML layout files \n* App defines some event callback flows to automate listener register/unregister based on lifecycle\n  resulting in less client boilerplate\n\nRelated\n=======\n* [MusicBrainz][brainz]\n* [CoverArtArchive][coverart]\n* [Retrofit][retrofit]\n* [Moshi][moshi]\n* [OkHttp][okhttp]\n* [Kotlin coroutines][coroutines] and [flows][flow]\n* [Splitties][splitties]\n  \n[brainz]: https://musicbrainz.org/\n[coverart]: https://musicbrainz.org/doc/Cover_Art_Archive\n[retrofit]: https://github.com/square/retrofit\n[moshi]: https://github.com/square/moshi\n[okhttp]: https://github.com/square/okhttp/\n[coroutines]: https://kotlinlang.org/docs/reference/coroutines-overview.html\n[flow]: https://kotlinlang.org/docs/reference/coroutines/flow.html  \n[splitties]: https://github.com/LouisCAD/Splitties\n[kotlin-result]: https://github.com/michaelbull/kotlin-result\n[railway]: https://fsharpforfunandprofit.com/rop/\n[maven-ealvabrainz]: https://search.maven.org/search?q=g:com.ealva%20AND%20a:ealvabrainz\n[maven-ealvabrainz-service]: https://search.maven.org/search?q=g:com.ealva%20AND%20a:ealvabrainz-service\n[ealvabrainz-snapshot]: https://oss.sonatype.org/content/repositories/snapshots/com/ealva/ealvabrainz-service/\n[ealvabrainz-service-snapshot]: https://oss.sonatype.org/content/repositories/snapshots/com/ealva/ealvabrainz-service/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fealva-com%2Fealvabrainz","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fealva-com%2Fealvabrainz","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fealva-com%2Fealvabrainz/lists"}