Skip to content

Commit

Permalink
Combined Album stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
brahmkshatriya committed Feb 28, 2024
1 parent e6e3b60 commit d55642e
Show file tree
Hide file tree
Showing 28 changed files with 424 additions and 217 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import androidx.paging.PagingState
import dev.brahmkshatriya.echo.common.clients.AlbumClient
import dev.brahmkshatriya.echo.common.clients.ExtensionClient
import dev.brahmkshatriya.echo.common.clients.HomeFeedClient
import dev.brahmkshatriya.echo.common.clients.SearchClient
import dev.brahmkshatriya.echo.common.clients.TrackClient
import dev.brahmkshatriya.echo.data.offline.LocalAlbum
import dev.brahmkshatriya.echo.data.offline.LocalArtist
import dev.brahmkshatriya.echo.data.offline.LocalStream
import dev.brahmkshatriya.echo.data.offline.LocalTrack
import dev.brahmkshatriya.echo.common.models.Album
import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem
import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItemsContainer
import dev.brahmkshatriya.echo.common.models.ExtensionMetadata
Expand All @@ -23,13 +21,19 @@ import dev.brahmkshatriya.echo.common.models.QuickSearchItem
import dev.brahmkshatriya.echo.common.models.StreamableAudio
import dev.brahmkshatriya.echo.common.models.StreamableAudio.Companion.toAudio
import dev.brahmkshatriya.echo.common.models.Track
import dev.brahmkshatriya.echo.data.offline.LocalAlbum
import dev.brahmkshatriya.echo.data.offline.LocalArtist
import dev.brahmkshatriya.echo.data.offline.LocalStream
import dev.brahmkshatriya.echo.data.offline.LocalTrack
import dev.brahmkshatriya.echo.data.offline.sortedBy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.io.IOException

class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, TrackClient, HomeFeedClient {
class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, TrackClient,
HomeFeedClient, AlbumClient {

override fun getMetadata() = ExtensionMetadata(
override val metadata = ExtensionMetadata(
name = "Offline",
version = "1.0.0",
description = "Local media library",
Expand All @@ -41,18 +45,19 @@ class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, Tr

override suspend fun search(query: String): Flow<PagingData<MediaItemsContainer>> = flow {
val trimmed = query.trim()
val albums = LocalAlbum.search(context, trimmed, 1, 50)
.map { it.toMediaItem() }.ifEmpty { null }
val tracks = LocalTrack.search(context, trimmed, 1, 50)
.map { it.toMediaItem() }.ifEmpty { null }
val artists = LocalArtist.search(context, trimmed, 1, 50)
.map { it.toMediaItem() }.ifEmpty { null }

val result = listOfNotNull(
tracks?.toMediaItemsContainer("Tracks"),
albums?.toMediaItemsContainer("Albums"),
artists?.toMediaItemsContainer("Artists")
)
val albums =
LocalAlbum.search(context, trimmed, 1, 50).map { it.toMediaItem() }.ifEmpty { null }
val tracks =
LocalTrack.search(context, trimmed, 1, 50).map { it.toMediaItem() }.ifEmpty { null }
val artists =
LocalArtist.search(context, trimmed, 1, 50).map { it.toMediaItem() }.ifEmpty { null }

val result =
listOfNotNull(tracks?.let { it.first().track.title to it.toMediaItemsContainer("Tracks") },
albums?.let { it.first().album.title to it.toMediaItemsContainer("Albums") },
artists?.let { it.first().artist.name to it.toMediaItemsContainer("Artists") }).sortedBy(
query
) { it.first }.map { it.second }
emit(PagingData.from(result))
}

Expand All @@ -69,12 +74,14 @@ class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, Tr
val pageSize = params.loadSize
return try {
val items = if (page == 0) {
val albums = LocalAlbum.getAll(context, page, pageSize)
.map { it.toMediaItem() }.ifEmpty { null }
val tracks = LocalTrack.getShuffled(context, page, pageSize)
.map { it.toMediaItem() }.ifEmpty { null }
val artists = LocalArtist.getAll(context, page, pageSize)
.map { it.toMediaItem() }.ifEmpty { null }
val albums = LocalAlbum.getAll(context, page, pageSize).map { it.toMediaItem() }
.ifEmpty { null }
val tracks =
LocalTrack.getShuffled(context, page, pageSize).map { it.toMediaItem() }
.ifEmpty { null }
val artists =
LocalArtist.getAll(context, page, pageSize).map { it.toMediaItem() }
.ifEmpty { null }
val result = listOfNotNull(
tracks?.toMediaItemsContainer("Tracks"),
albums?.toMediaItemsContainer("Albums"),
Expand All @@ -85,15 +92,12 @@ class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, Tr
LocalTrack.getAll(context, page, pageSize)
.map { MediaItemsContainer.TrackItem(it) }
}
val nextKey =
if (items.isEmpty()) null
else if (page == 0) 1
else page + 1
val nextKey = if (items.isEmpty()) null
else if (page == 0) 1
else page + 1

LoadResult.Page(
data = items,
prevKey = if (page == 0) null else page - 1,
nextKey = nextKey
data = items, prevKey = if (page == 0) null else page - 1, nextKey = nextKey
)
} catch (exception: IOException) {
return LoadResult.Error(exception)
Expand All @@ -112,13 +116,29 @@ class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, Tr

override suspend fun getHomeGenres(): List<String> = listOf()

override suspend fun getHomeFeed(genre: String?) = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false
),
pagingSourceFactory = { OfflinePagingSource(context) }
).flow
override suspend fun getHomeFeed(genre: String?) = Pager(config = PagingConfig(
pageSize = 10, enablePlaceholders = false
), pagingSourceFactory = { OfflinePagingSource(context) }).flow

override suspend fun loadAlbum(small: Album.Small): Album.Full {
return LocalAlbum.get(context, small.uri)
}

override suspend fun getMediaItems(album: Album.Full): Flow<PagingData<MediaItemsContainer>> =
flow {
val artist = album.artists.first()
val tracks =
LocalTrack.search(context, artist.name, 1, 50).filter { it.album?.uri != album.uri }
.map { it.toMediaItem() }.ifEmpty { null }
val albums =
LocalAlbum.search(context, artist.name, 1, 50).filter { it.uri != album.uri }
.map { it.toMediaItem() }.ifEmpty { null }
val result = listOfNotNull(
tracks?.toMediaItemsContainer("More from ${artist.name}"),
albums?.toMediaItemsContainer("Albums")
)
emit(PagingData.from(result))
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ import dev.brahmkshatriya.echo.data.offline.LocalHelper.Companion.createCursor

interface LocalAlbum {

companion object{
companion object {

fun search(context: Context, query: String, page: Int, pageSize: Int): List<Album.WithCover> {
val whereCondition = "${MediaStore.Audio.Albums.ALBUM} LIKE ?"
val selectionArgs = arrayOf("%$query%")
fun search(
context: Context,
query: String,
page: Int,
pageSize: Int
): List<Album.WithCover> {
val whereCondition =
"${MediaStore.Audio.Media.ARTIST} LIKE ? OR ${MediaStore.Audio.Media.ALBUM} LIKE ?"
val selectionArgs = arrayOf("%$query%", "%$query%")
return context.queryAlbums(whereCondition, selectionArgs, page, pageSize)
.sortedBy(query) {
it.title
}
}

fun getAll(context: Context, page: Int, pageSize: Int): List<Album.WithCover> {
Expand All @@ -29,22 +38,33 @@ interface LocalAlbum {
return context.queryAlbums(whereCondition, selectionArgs, page, pageSize)
}

fun getByArtist(context: Context,artist: Artist.Small, page: Int, pageSize: Int): List<Album.WithCover> {
val whereCondition = "${MediaStore.Audio.Media.ARTIST} = ?"
val selectionArgs = arrayOf(artist.name)
fun getByArtist(
context: Context,
artist: Artist.Small,
page: Int,
pageSize: Int
): List<Album.WithCover> {
val whereCondition = "${MediaStore.Audio.Media.ARTIST} LIKE ?"
val selectionArgs = arrayOf("%${artist.name}%")
return context.queryAlbums(whereCondition, selectionArgs, page, pageSize)
}

private fun Context.queryAlbums(whereCondition: String, selectionArgs: Array<String>, page: Int, pageSize: Int): MutableList<Album.WithCover> {
val albums = mutableListOf<Album.WithCover>()
private fun Context.queryAlbums(
whereCondition: String,
selectionArgs: Array<String>,
page: Int,
pageSize: Int
): MutableList<Album.Full> {
val albums = mutableListOf<Album.Full>()
createCursor(
contentResolver = contentResolver,
collection = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
projection = arrayOf(
MediaStore.Audio.Albums._ID,
MediaStore.Audio.Albums.ALBUM,
MediaStore.Audio.Albums.ARTIST,
MediaStore.Audio.Albums.NUMBER_OF_SONGS
MediaStore.Audio.Albums.NUMBER_OF_SONGS,
MediaStore.Audio.Albums.FIRST_YEAR
),
whereCondition = whereCondition,
selectionArgs = selectionArgs,
Expand All @@ -58,6 +78,7 @@ interface LocalAlbum {
val albumColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM)
val artistColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST)
val tracksColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Albums.NUMBER_OF_SONGS)
val yearColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Albums.FIRST_YEAR)
while (it.moveToNext()) {
val uri = Uri.parse("$URI$ALBUM_AUTH${it.getLong(idColumn)}")
val coverUri = ContentUris.withAppendedId(
Expand All @@ -66,17 +87,35 @@ interface LocalAlbum {
)
val artistUri = Uri.parse("$URI$ARTIST_AUTH${it.getLong(idColumn)}")
albums.add(
Album.WithCover(
Album.Full(
uri = uri,
title = it.getString(albumColumn),
cover = coverUri.toImageHolder(),
artists = listOf(Artist.Small(artistUri, it.getString(artistColumn))),
numberOfTracks = it.getInt(tracksColumn)
numberOfTracks = it.getInt(tracksColumn),
releaseDate = it.getString(yearColumn),
tracks = emptyList(),
publisher = null,
duration = null,
description = null
)
)
}
}
return albums
}

fun get(context: Context, uri: Uri): Album.Full {
val id = uri.lastPathSegment!!.toLong()
val whereCondition = "${MediaStore.Audio.Albums._ID} = ?"
val selectionArgs = arrayOf(id.toString())
val album = context.queryAlbums(whereCondition, selectionArgs, 0, 1).first()
val tracks = LocalTrack.getByAlbum(context, album, 0, 50)
val duration = tracks.sumOf { it.duration ?: 0 }
return album.copy(
tracks = LocalTrack.getByAlbum(context, album, 0, 50),
duration = duration
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,28 @@ import dev.brahmkshatriya.echo.data.offline.LocalHelper.Companion.createCursor

interface LocalArtist {

companion object{
fun search(context: Context, query: String, page: Int, pageSize: Int): List<Artist.WithCover> {
val whereCondition = "${MediaStore.Audio.Media.ARTIST} LIKE ?"
val selectionArgs = arrayOf("%$query%")
companion object {
fun search(
context: Context, query: String, page: Int, pageSize: Int
): List<Artist.WithCover> {
val whereCondition =
"${MediaStore.Audio.Media.TITLE} LIKE ? OR ${MediaStore.Audio.Media.ARTIST} LIKE ? OR ${MediaStore.Audio.Media.ALBUM} LIKE ?"
val selectionArgs = arrayOf("%$query%", "%$query%", "%$query%")
return context.queryArtists(whereCondition, selectionArgs, page, pageSize)
.sortedBy(query) {
it.name
}
}

fun getAll(context: Context,page: Int, pageSize: Int): List<Artist.WithCover> {
fun getAll(context: Context, page: Int, pageSize: Int): List<Artist.WithCover> {
val whereCondition = ""
val selectionArgs = arrayOf<String>()
return context.queryArtists(whereCondition, selectionArgs, page, pageSize)
}

private fun Context.queryArtists(whereCondition: String, selectionArgs: Array<String>, page: Int, pageSize: Int): List<Artist.WithCover> {
private fun Context.queryArtists(
whereCondition: String, selectionArgs: Array<String>, page: Int, pageSize: Int
): List<Artist.WithCover> {
val artists = mutableListOf<Artist.WithCover>()
createCursor(
contentResolver = contentResolver,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ interface LocalTrack {
val selectionArgs = arrayOf("%$query%", "%$query%", "%$query%")

return context.queryTracks(whereCondition, selectionArgs, page, pageSize)
.sortedBy(query) {
it.title
}
}

fun getAll(context: Context, page: Int, pageSize: Int): List<Track> {
Expand Down
43 changes: 43 additions & 0 deletions app/src/main/java/dev/brahmkshatriya/echo/data/offline/SortedBy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.brahmkshatriya.echo.data.offline

fun <E> List<E>.sortedBy(query: String, block: (E) -> String): List<E> {
return sortedBy {
val distance = wagnerFischer(block(it), query)

val bonus = if (block(it).contains(query, true)) -20 else 0
distance + bonus
}
}

// taken from https://gist.github.com/jmarchesini/e330088e03daa394cf03ddedb8956fbe
fun wagnerFischer(s: String, t: String): Int {
val m = s.length
val n = t.length

if (s == t) return 0
if (s.isEmpty()) return n
if (t.isEmpty()) return m

val d = Array(m + 1) { IntArray(n + 1) { 0 } }

(1..m).forEach { i ->
d[i][0] = i
}

(1..n).forEach { j ->
d[0][j] = j
}

(1..n).forEach { j ->
(1..m).forEach { i ->
val cost = if (s[i - 1] == t[j - 1]) 0 else 1
val delCost = d[i - 1][j] + 1
val addCost = d[i][j - 1] + 1
val subCost = d[i - 1][j - 1] + cost

d[i][j] = minOf(delCost, addCost, subCost)
}
}

return d[m][n]
}
Loading

0 comments on commit d55642e

Please sign in to comment.