Skip to content

Instantly share code, notes, and snippets.

@kgilmer
Created August 22, 2018 00:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kgilmer/b643d9d82206db034778b651b63d1411 to your computer and use it in GitHub Desktop.
Save kgilmer/b643d9d82206db034778b651b63d1411 to your computer and use it in GitHub Desktop.
class JsonSource(context: Context, source: Uri) : AbstractMusicSource() {
private var catalog: List<MediaMetadataCompat> = emptyList()
init {
state = STATE_INITIALIZING
UpdateCatalogTask(Glide.with(context)) { mediaItems ->
catalog = mediaItems
state = STATE_INITIALIZED
}.execute(source)
}
override fun iterator(): Iterator<MediaMetadataCompat> = catalog.iterator()
}
/**
* Task to connect to remote URIs and download/process JSON files that correspond to
* [MediaMetadataCompat] objects.
*/
private class UpdateCatalogTask(val glide: RequestManager,
val listener: (List<MediaMetadataCompat>) -> Unit) :
AsyncTask<Uri, Void, List<MediaMetadataCompat>>() {
override fun doInBackground(vararg params: Uri): List<MediaMetadataCompat> {
val gson = Gson()
val mediaItems = ArrayList<MediaMetadataCompat>()
params.forEach { catalogUri ->
val catalogConn = URL(catalogUri.toString())
val reader = BufferedReader(InputStreamReader(catalogConn.openStream()))
val musicCat = gson.fromJson<JsonCatalog>(reader, JsonCatalog::class.java)
// Get the base URI to fix up relative references later.
val baseUri = catalogUri.toString().removeSuffix(catalogUri.lastPathSegment)
mediaItems += musicCat.music.map { song ->
// The JSON may have paths that are relative to the source of the JSON
// itself. We need to fix them up here to turn them into absolute paths.
if (!song.source.startsWith(catalogUri.scheme)) {
song.source = baseUri + song.source
}
if (!song.image.startsWith(catalogUri.scheme)) {
song.image = baseUri + song.image
}
// Block on downloading artwork.
val art = glide.applyDefaultRequestOptions(glideOptions)
.asBitmap()
.load(song.image)
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.get()
MediaMetadataCompat.Builder()
.from(song)
.apply {
albumArt = art
}
.build()
}.toList()
}
return mediaItems
}
override fun onPostExecute(mediaItems: List<MediaMetadataCompat>) {
super.onPostExecute(mediaItems)
listener(mediaItems)
}
}
/**
* Extension method for [MediaMetadataCompat.Builder] to set the fields from
* our JSON constructed object (to make the code a bit easier to see).
*/
fun MediaMetadataCompat.Builder.from(jsonMusic: JsonMusic): MediaMetadataCompat.Builder {
// The duration from the JSON is given in seconds, but the rest of the code works in
// milliseconds. Here's where we convert to the proper units.
val durationMs = TimeUnit.SECONDS.toMillis(jsonMusic.duration)
id = jsonMusic.id
title = jsonMusic.title
artist = jsonMusic.artist
album = jsonMusic.album
duration = durationMs
genre = jsonMusic.genre
mediaUri = jsonMusic.source
albumArtUri = jsonMusic.image
trackNumber = jsonMusic.trackNumber
trackCount = jsonMusic.totalTrackCount
flag = MediaItem.FLAG_PLAYABLE
// To make things easier for *displaying* these, set the display properties as well.
displayTitle = jsonMusic.title
displaySubtitle = jsonMusic.artist
displayDescription = jsonMusic.album
displayIconUri = jsonMusic.image
// Add downloadStatus to force the creation of an "extras" bundle in the resulting
// MediaMetadataCompat object. This is needed to send accurate metadata to the
// media session during updates.
downloadStatus = STATUS_NOT_DOWNLOADED
// Allow it to be used in the typical builder style.
return this
}
/**
* Wrapper object for our JSON in order to be processed easily by GSON.
*/
class JsonCatalog {
var music: List<JsonMusic> = ArrayList()
}
/**
* An individual piece of music included in our JSON catalog.
* The format from the server is as specified:
* ```
* { "music" : [
* { "title" : // Title of the piece of music
* "album" : // Album title of the piece of music
* "artist" : // Artist of the piece of music
* "genre" : // Primary genre of the music
* "source" : // Path to the music, which may be relative
* "image" : // Path to the art for the music, which may be relative
* "trackNumber" : // Track number
* "totalTrackCount" : // Track count
* "duration" : // Duration of the music in seconds
* "site" : // Source of the music, if applicable
* }
* ]}
* ```
*
* `source` and `image` can be provided in either relative or
* absolute paths. For example:
* ``
* "source" : "https://www.example.com/music/ode_to_joy.mp3",
* "image" : "ode_to_joy.jpg"
* ``
*
* The `source` specifies the full URI to download the piece of music from, but
* `image` will be fetched relative to the path of the JSON file itself. This means
* that if the JSON was at "https://www.example.com/json/music.json" then the image would be found
* at "https://www.example.com/json/ode_to_joy.jpg".
*/
class JsonMusic {
var id: String = ""
var title: String = ""
var album: String = ""
var artist: String = ""
var genre: String = ""
var source: String = ""
var image: String = ""
var trackNumber: Long = 0
var totalTrackCount: Long = 0
var duration: Long = -1
var site: String = ""
}
private const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
private val glideOptions = RequestOptions()
.fallback(R.drawable.default_art)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment