Skip to content

Commit

Permalink
Merge pull request #115 from icefields/dev
Browse files Browse the repository at this point in the history
version 100-55
  • Loading branch information
icefields committed May 14, 2024
2 parents 850e2a4 + 87fb05f commit 47cc98f
Show file tree
Hide file tree
Showing 17 changed files with 205 additions and 195 deletions.
8 changes: 4 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ android {
applicationId = "luci.sixsixsix.powerampache2"
minSdk = 28
targetSdk = 34
versionCode = 54
versionName = "1.00-54"
val versionQuote = "This version is powered by the beginning of the music revolution (FDroid first release)"
versionCode = 55
versionName = "1.00-55"
val versionQuote = "This version is powered by the beginning of the music revolution (FDroid second release)"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

Expand Down Expand Up @@ -127,7 +127,7 @@ android {
resValue("string", "build_type", "Release")

isMinifyEnabled = false
vcsInfo.include = false
// vcsInfo.include = false

proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ object Constants {
const val USER_EMAIL_DEFAULT = "" //""[email protected]"
const val USER_FULL_NAME_PUBLIC_DEFAULT = 0
const val PLAYLIST_FETCH_LIMIT = 50
const val NETWORK_REQUEST_LIMIT_ARTISTS = 30
const val NETWORK_REQUEST_LIMIT_SONGS = 40
const val NETWORK_REQUEST_LIMIT_SONGS_SEARCH = 100
const val NETWORK_REQUEST_LIMIT_ALBUMS = 40

// TIMEOUTS (non-network)
const val LOCAL_SCROBBLE_TIMEOUT_MS = 20000L
Expand All @@ -66,7 +70,6 @@ object Constants {
const val ALWAYS_FETCH_ALL_PLAYLISTS = true

// DEBUG VALUES
const val NETWORK_REQUEST_LIMIT_DEBUG = 20
const val ERROR_TITLE = ERROR_STRING

// DONATION LINKS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import luci.sixsixsix.powerampache2.domain.AlbumsRepository
import luci.sixsixsix.powerampache2.domain.errors.ErrorHandler
import luci.sixsixsix.powerampache2.domain.errors.MusicException
import luci.sixsixsix.powerampache2.domain.models.Album
import luci.sixsixsix.powerampache2.domain.models.AmpacheModel
import luci.sixsixsix.powerampache2.domain.models.MusicAttribute
import luci.sixsixsix.powerampache2.domain.models.Song
import javax.inject.Inject
Expand All @@ -71,6 +72,7 @@ class AlbumsRepositoryImpl @Inject constructor(
): Flow<Resource<List<Album>>> = flow {
emit(Resource.Loading(true))
L("getAlbums - repo getSongs offset $offset")
val cred = getCurrentCredentials()

if (isOfflineModeEnabled()) {
// TRY using cached data instead of downloaded song info if available
Expand All @@ -81,26 +83,25 @@ class AlbumsRepositoryImpl @Inject constructor(
}
}

getUsername()?.let { username ->
dao.generateOfflineAlbums(username).forEach { dae ->
albumsList.add(
if (dbAlbumsHash.containsKey(dae.id)) {
(dbAlbumsHash[dae.id] ?: dae)
} else { dae }.toAlbum()
)
}
dao.generateOfflineAlbums(cred.username).forEach { dae ->
albumsList.add(
if (dbAlbumsHash.containsKey(dae.id)) {
(dbAlbumsHash[dae.id] ?: dae)
} else { dae }.toAlbum()
)
}

emit(Resource.Success(data = albumsList.toList()))
emit(Resource.Loading(false))
return@flow
}

val localAlbums = mutableListOf<Album>()
if (offset == 0) {
val localAlbums = dao.searchAlbum(query)
localAlbums.addAll(dao.searchAlbum(query).map { it.toAlbum() })
val isDbEmpty = localAlbums.isEmpty() && query.isEmpty()
if (!isDbEmpty) {
emit(Resource.Success(data = localAlbums.map { it.toAlbum() }))
emit(Resource.Success(data = localAlbums.toList()))
}
val shouldLoadCacheOnly = !isDbEmpty && !fetchRemote
if(shouldLoadCacheOnly) {
Expand All @@ -109,13 +110,15 @@ class AlbumsRepositoryImpl @Inject constructor(
}
}

val cred = getCurrentCredentials()
val response = api.getAlbums(authToken(), filter = query, offset = offset, limit = limit)
response.error?.let { throw(MusicException(it.toError())) }
val albums = response.albums!!.map { it.toAlbum() } // will throw exception if songs null
dao.insertAlbums(albums.map { it.toAlbumEntity(username = cred.username, serverUrl = cred.serverUrl) })
// stick to the single source of truth pattern despite performance deterioration
emit(Resource.Success(data = dao.searchAlbum(query).map { it.toAlbum() }, networkData = albums))
// append to the initial list to avoid ui flickering
val updatedDbAlbums = dao.searchAlbum(query).map { it.toAlbum() }.toMutableList()
AmpacheModel.appendToList(updatedDbAlbums, localAlbums)
emit(Resource.Success(data = localAlbums, networkData = albums))
emit(Resource.Loading(false))
}.catch { e -> errorHandler("getAlbums()", e, this) }

Expand Down Expand Up @@ -148,24 +151,28 @@ class AlbumsRepositoryImpl @Inject constructor(
val response = api.getAlbumsFromArtist(authToken(), artistId = artistId)
response.error?.let { throw(MusicException(it.toError())) }

// some albums come from web with no artists id, or with artist id zero, add the id manually
// so the database can find it (db is single source of truth)
val albums = response.albums!!.map { albumDto -> albumDto.toAlbum() } // will throw exception if songs null

L("albums from web ${albums.size}")
// some albums come from web with no artists id, add the id manually so the database can find it later
val albums = response.albums!!.map { albumDto -> albumDto.toAlbum() }.toMutableList() // will throw exception if albums null
dao.deleteAlbumsFromArtist(artistId)

albums.forEachIndexed { index, alb ->
try {
// check if the album contains the current artist Id. If not, add the artist to the list of featured artists
if (alb.artist.id != artistId && !alb.artists.map { art -> art.id }.contains(artistId)) {
val artName = dao.getArtist(artistId)?.name ?: ""
albums[index] = alb.copy(artists = alb.artists.toMutableList().apply {
add(MusicAttribute(id = artistId, name = artName))
})
}
} catch (e: Exception) {}

// REFRESH ALBUMS BY ARTIST, delete first then reinsert
albums.forEach { dao.deleteAlbumsFromArtist(it.artist.id) }
// delete the album, it will be reinserted right after
dao.deleteAlbum(alb.id)
}
dao.insertAlbums(albums.map { it.toAlbumEntity(username = cred.username, serverUrl = cred.serverUrl) })
// stick to the single source of truth pattern despite performance deterioration
val dbUpdatedAlbums = dao.getAlbumsFromArtist(artistId).map { it.toAlbum() }
// TODO: anti-pattern. Violating single source of data
// (inconsistencies between network and db responses)
// TODO: document, unit-test
emit(Resource.Success(
data = if (albums.size > dbUpdatedAlbums.size) albums else dbUpdatedAlbums,
networkData = albums
))
emit(Resource.Success(data = dbUpdatedAlbums, networkData = albums))
emit(Resource.Loading(false))
}.catch { e -> errorHandler("getAlbumsFromArtist()", e, this) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,60 +110,8 @@ class MusicRepositoryImpl @Inject constructor(
}
}
}

// --- TODO: REMOVE AFTER MIGRATION
GlobalScope.launch {
try { migrateDb() } catch (e: Exception) { }
}
}

// --- TODO: REMOVE AFTER MIGRATION
private suspend fun migrateDb() {
L("migrateDb start")
val credentials = dao.getCredentials()
if (credentials == null || credentials.serverUrl.isBlank() || credentials.username.isBlank()) return

val cred = getCurrentCredentials()

val multiuserId = multiuserDbKey(username = cred.username, serverUrl = cred.serverUrl)
val art = dao.getNotMigratedArtists().map { it.copy(multiUserId = multiuserId) }
val play = dao.getNotMigratedPlaylists().map { it.copy(multiUserId = multiuserId) }
val so = dao.getNotMigratedSongs().map { it.copy(multiUserId = multiuserId) }
val alb = dao.getNotMigratedAlbums().map { it.copy(multiUserId = multiuserId) }
val ps = dao.getNotMigratedPlaylistSong().map { it.copy(multiUserId = multiuserId) }
val off = dao.getNotMigratedOfflineSongs().map { it.copy(multiUserId = multiuserId) }

if(art.isNotEmpty())
dao.insertArtists(art)

if(play.isNotEmpty())
dao.insertPlaylists(play)

if(so.isNotEmpty())
dao.insertSongs(so)

if(alb.isNotEmpty())
dao.insertAlbums(alb)

if(ps.isNotEmpty())
dao.insertPlaylistSongs(ps)

if(off.isNotEmpty())
dao.addDownloadedSongs(off)

credentials?.copy(multiUserId = multiuserId)?.let {
dao.updateCredentials(it)
}

dao.getUser()?.copy(multiUserId = multiuserId)?.let {
dao.updateUser(it)
}
L("migrateDb end")
}

// --- TODO: END BLOCK TO REMOVE AFTER MIGRATION


private suspend fun setSession(se: Session) {
dao.updateSession(se.toSessionEntity())
val cred = getCurrentCredentials()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import kotlinx.coroutines.launch
import luci.sixsixsix.mrlog.L
import luci.sixsixsix.powerampache2.common.Constants
import luci.sixsixsix.powerampache2.common.Constants.ERROR_INT
import luci.sixsixsix.powerampache2.common.Constants.NETWORK_REQUEST_LIMIT_SONGS_SEARCH
import luci.sixsixsix.powerampache2.common.Resource
import luci.sixsixsix.powerampache2.common.WeakContext
import luci.sixsixsix.powerampache2.common.hasMore
Expand Down Expand Up @@ -135,9 +136,18 @@ class SongsRepositoryImpl @Inject constructor(
val minDbSongs = 200
// get songs from db, if the result is less than minDbSongs
// also get songs from network
val songsDb = if (query.isNullOrBlank()) dao.searchSong("") else listOf()
if (songsDb.size < minDbSongs) {
getSongsNetwork(fetchRemote = fetchRemote, query = query, offset = offset)
val isSearch = query.isNotBlank()
val songsDb = //if (query.isNullOrBlank())
dao.searchSong(query)
//else listOf()
// always check network in case of search, if online
if (isSearch || songsDb.size < minDbSongs) { // will always be less if it's a search
getSongsNetwork(
fetchRemote = fetchRemote,
query = query,
offset = offset,
initialList = songsDb.map { it.toSong() }.toMutableList()
)
} else {
flow {
emit(Resource.Success(data = songsDb.map { it.toSong()}))
Expand All @@ -150,34 +160,28 @@ class SongsRepositoryImpl @Inject constructor(
private suspend fun getSongsNetwork(
fetchRemote: Boolean,
query: String,
offset: Int
offset: Int,
initialList: MutableList<Song> = mutableListOf()
): Flow<Resource<List<Song>>> = flow {
emit(Resource.Loading(true))
emit(Resource.Success(data = initialList.toList()))

// network TODO WHAT IS THIS!!?? FIX !!!
val auth = getSession()!!
val auth = authToken()
val credentials = getCurrentCredentials()
val hashSet = LinkedHashSet<Song>()
val songs = if (query.isNullOrBlank()) {
// not a search
try {
api.getSongsStats(
filter = MainNetwork.StatFilter.random,
authKey = auth.auth,
authKey = auth,
username = getCredentials()?.username
).songs?.let { dto ->
val random = dto.map { it.toSong() }
hashSet.addAll(random)
if (random.isNotEmpty()) {
emit(Resource.Success(data = hashSet.toList()))
}
}
).songs!!.map { it.toSong() }
} catch (e: Exception) {
listOf()
}
hashSet.toList()
} else {
// if this is a search
val response = api.getSongs(auth.auth, filter = query, offset = offset)
val response = api.getSongs(auth, filter = query, offset = offset, limit = NETWORK_REQUEST_LIMIT_SONGS_SEARCH)
response.error?.let { throw(MusicException(it.toError())) }
response.songs!!.map { it.toSong() } // will throw exception if songs null
}
Expand All @@ -193,12 +197,10 @@ class SongsRepositoryImpl @Inject constructor(
val songsDb = dao.searchSong(query).map { it.toSong() }
L( "getSongs songs from db after web ${songsDb.size}")

val returnSongList = if (query.isNullOrBlank()) {
val returnSongList = if (query.isNullOrBlank()) {
// if not a search append the songsDb to the network result
ArrayList(songsDb).removeAll(songs.toSet())
ArrayList(songs).apply {
addAll(songsDb.shuffled())
}
AmpacheModel.appendToList(songsDb.toMutableList(), initialList)
initialList
} else {
// if it's a search return what the db found
songsDb
Expand Down Expand Up @@ -394,13 +396,26 @@ class SongsRepositoryImpl @Inject constructor(
emit(Resource.Loading(true))
val resultSet = HashSet<Song>()
// add downloaded songs
resultSet.addAll(dao.getOfflineSongs().map { it.toSong() })
try { resultSet.addAll(dao.getMostPlayedOfflineSongs().map { it.toSong() }) } catch (e: Exception) { }
try { resultSet.addAll(dao.getLikedOfflineSongs().map { it.toSong() }) } catch (e: Exception) { }
try { resultSet.addAll(dao.getHighestRatedOfflineSongs().map { it.toSong() }) } catch (e: Exception) { }
if (resultSet.size < Constants.QUICK_PLAY_MIN_SONGS) {
try { resultSet.addAll(dao.getOfflineSongs().map { it.toSong() }) } catch (e: Exception) { }
}
// add cached songs? Too many can cause a crash when saving state
// resultSet.addAll(dao.searchSong("").map { it.toSong() })
// if not big enough start fetching from web
if (!isOfflineModeEnabled()) {
// try add cached songs first
try { resultSet.addAll(dao.searchSong("").map { it.toSong() }) } catch (e: Exception) { }
if (resultSet.size < Constants.QUICK_PLAY_MIN_SONGS) {
try { resultSet.addAll(dao.getMostPlayedSongs().map { it.toSong() }) } catch (e: Exception) { }
}
if (resultSet.size < Constants.QUICK_PLAY_MIN_SONGS) {
try { resultSet.addAll(dao.getMostPlayedSongsLocal().map { it.toSong() }) } catch (e: Exception) { }
}
if (resultSet.size < Constants.QUICK_PLAY_MIN_SONGS) {
try { resultSet.addAll(dao.searchSong("").map { it.toSong() }) } catch (e: Exception) { }
}
try {
if (resultSet.size < Constants.QUICK_PLAY_MIN_SONGS) {
// if not enough downloaded songs fetch most played songs
Expand Down
Loading

0 comments on commit 47cc98f

Please sign in to comment.