Skip to content

Commit

Permalink
scanners: ui: Show progress & Disallow multiple db sync jobs at once
Browse files Browse the repository at this point in the history
* db sync and YTM artist lookup shall not happen at once
* Scan jobs, such as scanLocal can still run concurrently (even though it doesn't really make
   sense at the moment
  • Loading branch information
mikooomich authored and reocat committed Jan 12, 2025
1 parent b4ebba9 commit 9e62a72
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 22 deletions.
5 changes: 3 additions & 2 deletions app/src/main/java/com/dd3boh/outertune/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ import com.dd3boh.outertune.utils.rememberPreference
import com.dd3boh.outertune.utils.reportException
import com.dd3boh.outertune.utils.scanners.LocalMediaScanner
import com.dd3boh.outertune.utils.scanners.LocalMediaScanner.Companion.destroyScanner
import com.dd3boh.outertune.utils.scanners.LocalMediaScanner.Companion.scannerActive
import com.dd3boh.outertune.utils.scanners.ScannerAbortException
import com.dd3boh.outertune.utils.urlEncode
import com.valentinilk.shimmer.LocalShimmerTheme
Expand Down Expand Up @@ -406,7 +407,7 @@ class MainActivity : ComponentActivity() {

CoroutineScope(Dispatchers.IO).launch {
// Check if the permissions for local media access
if (firstSetupPassed && localLibEnable && autoScan
if (!scannerActive.value && autoScan && firstSetupPassed && localLibEnable
&& checkSelfPermission(MEDIA_PERMISSION_LEVEL) == PackageManager.PERMISSION_GRANTED) {

// equivalent to (quick scan)
Expand All @@ -424,7 +425,7 @@ class MainActivity : ComponentActivity() {
)

// start artist linking job
if (lookupYtmArtists) {
if (lookupYtmArtists && !scannerActive.value) {
CoroutineScope(Dispatchers.IO).launch {
try {
scanner.localToRemoteArtist(database)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,12 @@ fun ExperimentalSettings(
title = { Text("DEBUG: Force local to remote artist migration NOW") },
icon = { Icon(Icons.Rounded.Backup, null) },
onClick = {
Toast.makeText(context, "Starting migration...", Toast.LENGTH_SHORT).show()
Toast.makeText(context, "Starting YouTube artist linking...", Toast.LENGTH_SHORT).show()
coroutineScope.launch(Dispatchers.IO) {
val scanner = LocalMediaScanner.getScanner(context, ScannerImpl.TAGLIB)
Timber.tag("Settings").d("Force Migrating local artists to YTM (MANUAL TRIGGERED)")
scanner.localToRemoteArtist(database)
Toast.makeText(context, "YouTube artist linking job complete", Toast.LENGTH_SHORT).show()
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
Expand Down Expand Up @@ -55,6 +57,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Expand Down Expand Up @@ -92,9 +95,13 @@ import com.dd3boh.outertune.utils.scanners.LocalMediaScanner.Companion.destroySc
import com.dd3boh.outertune.utils.scanners.LocalMediaScanner.Companion.getScanner
import com.dd3boh.outertune.utils.scanners.LocalMediaScanner.Companion.scannerActive
import com.dd3boh.outertune.utils.scanners.LocalMediaScanner.Companion.scannerFinished
import com.dd3boh.outertune.utils.scanners.LocalMediaScanner.Companion.scannerProgressCurrent
import com.dd3boh.outertune.utils.scanners.LocalMediaScanner.Companion.scannerProgressTotal
import com.dd3boh.outertune.utils.scanners.LocalMediaScanner.Companion.scannerRequestCancel
import com.dd3boh.outertune.utils.scanners.LocalMediaScanner.Companion.scannerShowLoading
import com.dd3boh.outertune.utils.scanners.ScannerAbortException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.ZoneOffset
Expand All @@ -116,7 +123,11 @@ fun LocalPlayerSettings(

// scanner vars
val isScannerActive by scannerActive.collectAsState()
val showLoading by scannerShowLoading.collectAsState()
val isScanFinished by scannerActive.collectAsState()
val scannerProgressTotal by scannerProgressTotal.collectAsState()
val scannerProgressCurrent by scannerProgressCurrent.collectAsState()

var scannerFailure = false
var mediaPermission by remember { mutableStateOf(true) }

Expand Down Expand Up @@ -336,7 +347,6 @@ fun LocalPlayerSettings(
}

scannerFinished.value = false
scannerActive.value = true
scannerFailure = false

coroutineScope.launch(Dispatchers.IO) {
Expand All @@ -357,10 +367,13 @@ fun LocalPlayerSettings(
)

// start artist linking job
if (lookupYtmArtists) {
if (lookupYtmArtists && !isScannerActive) {
coroutineScope.launch(Dispatchers.IO) {
Looper.prepare()
try {
Toast.makeText(context, "Starting YouTube artist linking...", Toast.LENGTH_SHORT).show()
scanner.localToRemoteArtist(database)
Toast.makeText(context, "YouTube artist linking job complete", Toast.LENGTH_SHORT).show()
} catch (e: ScannerAbortException) {
Looper.prepare()
Toast.makeText(
Expand Down Expand Up @@ -399,12 +412,14 @@ fun LocalPlayerSettings(
)

// start artist linking job
if (lookupYtmArtists) {
if (lookupYtmArtists && !isScannerActive) {
coroutineScope.launch(Dispatchers.IO) {
Looper.prepare()
try {
Toast.makeText(context, "Starting YouTube artist linking...", Toast.LENGTH_SHORT).show()
scanner.localToRemoteArtist(database)
Toast.makeText(context, "YouTube artist linking job complete", Toast.LENGTH_SHORT).show()
} catch (e: ScannerAbortException) {
Looper.prepare()
Toast.makeText(
context,
"Scanner (background task) failed: ${e.message}",
Expand All @@ -430,14 +445,13 @@ fun LocalPlayerSettings(
purgeCache()
cacheDirectoryTree(null)

scannerActive.value = false
onLastLocalScanChange(LocalDateTime.now().atOffset(ZoneOffset.UTC).toEpochSecond())
scannerFinished.value = true
}
}
) {
Text(
text = if (isScannerActive) {
text = if (isScannerActive || showLoading) {
"Cancel"
} else if (scannerFailure) {
"An Error Occurred"
Expand All @@ -453,21 +467,42 @@ fun LocalPlayerSettings(


// progress indicator
if (!isScannerActive) {
if (!showLoading) {
return@Row
}

// padding hax
VerticalDivider(
modifier = Modifier.padding(5.dp)
)
Spacer(Modifier.width(8.dp))

CircularProgressIndicator(
modifier = Modifier
.size(32.dp),
color = MaterialTheme.colorScheme.secondary,
trackColor = MaterialTheme.colorScheme.surfaceVariant,
)

Spacer(Modifier.width(8.dp))

if (scannerProgressTotal != -1) {
Column {
val isSyncing = scannerProgressCurrent > -1
Text(
text = if (isSyncing) "Syncing..." else "Scanning...",
color = MaterialTheme.colorScheme.secondary,
fontSize = 12.sp
)
Text(
text = "${if (isSyncing) scannerProgressCurrent else ""}/${
pluralStringResource(
R.plurals.n_song,
scannerProgressTotal,
scannerProgressTotal
)
} ${if (isSyncing) "processed" else "found"}",
color = MaterialTheme.colorScheme.secondary,
fontSize = 12.sp
)
}
}
}
// scanner checkboxes
Column(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ package com.dd3boh.outertune.utils.scanners
import android.content.Context
import android.media.MediaPlayer
import android.os.Environment
import androidx.datastore.dataStore
import androidx.datastore.preferences.core.edit
import com.dd3boh.outertune.MainActivity
import com.dd3boh.outertune.constants.AutomaticScannerKey
import com.dd3boh.outertune.constants.PlayerVolumeKey
import com.dd3boh.outertune.constants.ScannerImpl
import com.dd3boh.outertune.constants.ScannerImplKey
import com.dd3boh.outertune.constants.ScannerMatchCriteria
Expand Down Expand Up @@ -147,6 +144,7 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
): MutableStateFlow<DirectoryTree> {
val newDirectoryStructure = DirectoryTree(STORAGE_ROOT)
Timber.tag(TAG).d("------------ SCAN: Starting Full Scanner ------------")
scannerShowLoading.value = true

val scannerJobs = ArrayList<Deferred<SongTempData?>>()
runBlocking {
Expand Down Expand Up @@ -176,16 +174,19 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
// use async scanner
scannerJobs.add(
async(scannerSession) {
var ret: SongTempData?
if (scannerRequestCancel) {
if (SCANNER_DEBUG)
Timber.tag(TAG).d("WARNING: Canceling advanced scanner job.")
throw ScannerAbortException("")
}
try {
advancedScan(path)
ret = advancedScan(path)
scannerProgressTotal.value ++
} catch (e: InvalidAudioFileException) {
null
ret = null
}
ret
}
)
} else {
Expand All @@ -202,6 +203,7 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
newDirectoryStructure.insert(
s.substringAfter(STORAGE_ROOT), toInsert.song
)
scannerProgressTotal.value ++
}
}
}
Expand Down Expand Up @@ -230,6 +232,7 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
}
}

scannerShowLoading.value = false
Timber.tag(TAG).d("------------ SCAN: Finished Full Scanner ------------")
cacheDirectoryTree(newDirectoryStructure.androidStorageWorkaround().trimRoot())
return MutableStateFlow(newDirectoryStructure)
Expand Down Expand Up @@ -257,7 +260,13 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
refreshExisting: Boolean = false,
noDisable: Boolean = false
) {
if (scannerActive.value) {
Timber.tag(TAG).d("------------ SYNC: Scanner in use. Aborting Local Library Sync ------------")
return
}
Timber.tag(TAG).d("------------ SYNC: Starting Local Library Sync ------------")
scannerActive.value = true
scannerShowLoading.value = true
// deduplicate
val finalSongs = ArrayList<SongTempData>()
newSongs.forEach { song ->
Expand All @@ -266,14 +275,20 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
}
}
Timber.tag(TAG).d("Entries to process: ${newSongs.size}. After dedup: ${finalSongs.size}")
scannerProgressTotal.value = finalSongs.size
scannerProgressCurrent.value = 0

// sync
var runs = 0
finalSongs.forEach { song ->
runs ++
if (runs % 20 == 0) {
if (SCANNER_DEBUG && runs % 20 == 0) {
Timber.tag(TAG).d("------------ SYNC: Local Library Sync: $runs/${finalSongs.size} processed ------------")
}
if (runs % 5 == 0) {
scannerProgressCurrent.value += 5
}

if (scannerRequestCancel) {
if (SCANNER_DEBUG)
Timber.tag(TAG).d("WARNING: Requested to cancel Local Library Sync. Aborting.")
Expand Down Expand Up @@ -393,6 +408,8 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
if (!noDisable) {
finalize(finalSongs.map { it.song }, database)
}
scannerShowLoading.value = false
scannerActive.value = false
Timber.tag(TAG).d("------------ SYNC: Finished Local Library Sync ------------")
}

Expand All @@ -418,6 +435,7 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
) {
Timber.tag(TAG).d("------------ SYNC: Starting Quick (additive delta) Library Sync ------------")
Timber.tag(TAG).d("Entries to process: ${newSongs.size}")
scannerShowLoading.value = true

runBlocking(Dispatchers.IO) {
// get list of all songs in db, then get songs unknown to the database
Expand Down Expand Up @@ -496,6 +514,8 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
// we handle disabling songs here instead
finalize(newSongs, database)
}

scannerShowLoading.value = false
Timber.tag(TAG).d("------------ SYNC: Finished Quick (additive delta) Library Sync ------------")
}

Expand All @@ -521,6 +541,7 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
) {
Timber.tag(TAG).d("------------ SYNC: Starting FULL Library Sync ------------")
Timber.tag(TAG).d("Entries to process: ${newSongs.size}")
scannerShowLoading.value = true

runBlocking(Dispatchers.IO) {
val finalSongs = ArrayList<SongTempData>()
Expand Down Expand Up @@ -593,6 +614,8 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
Timber.tag(TAG).d("Not syncing, no valid songs found!")
}
}

scannerShowLoading.value = false
Timber.tag(TAG).d("------------ SYNC: Finished Quick (additive delta) Library Sync ------------")
}

Expand All @@ -601,8 +624,14 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
* Converts all local artists to remote artists if possible
*/
fun localToRemoteArtist(database: MusicDatabase) {
if (scannerActive.value) {
Timber.tag(TAG).d("------------ SYNC: Scanner in use. Aborting youtubeArtistLookup job ------------")
return
}
var runs = 0
Timber.tag(TAG).d("------------ SYNC: Starting youtubeArtistLookup job ------------")
scannerActive.value = true
scannerShowLoading.value = true
runBlocking(Dispatchers.IO) {
val allLocal = database.allLocalArtists().first()

Expand Down Expand Up @@ -666,6 +695,8 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
}
}

scannerShowLoading.value = false
scannerActive.value = false
Timber.tag(TAG).d("------------ SYNC: youtubeArtistLookup job ended------------")
}

Expand Down Expand Up @@ -744,10 +775,15 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm
/**
* TODO: Create a lock for background jobs like youtubeartists and etc
*/
var scannerActive = MutableStateFlow(false)
var scannerActive = MutableStateFlow(false) // TODO: make this an enum. scan -> sync -> ytmartist
var scannerShowLoading = MutableStateFlow(false)
var scannerFinished = MutableStateFlow(false)
var scannerRequestCancel = false

var scannerProgressTotal = MutableStateFlow(-1)
var scannerProgressCurrent = MutableStateFlow(-1)


/**
* ==========================
* Scanner management
Expand Down Expand Up @@ -776,13 +812,20 @@ class LocalMediaScanner(val context: Context, private val scannerImpl: ScannerIm

if (localScanner?.get() == null) {
localScanner = WeakReference(LocalMediaScanner(context, scannerImpl))
scannerProgressTotal.value = 0
}

return localScanner?.get() ?: throw IllegalStateException("Scanner is null")
}

fun destroyScanner() {
localScanner = null
scannerActive.value = false
scannerShowLoading.value = false
scannerFinished.value = false
scannerRequestCancel = false
scannerProgressTotal.value = -1
scannerProgressCurrent.value = -1
if (EXTRACTOR_DEBUG)
Timber.tag(EXTRACTOR_TAG).d("Scanner instance destroyed")
}
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
androidGradlePlugin = "8.9.0-alpha08"
androidGradlePlugin = "8.9.0-alpha09"
annotation = "1.9.1"
json = "20250107"
kotlin = "2.1.0"
Expand Down

0 comments on commit 9e62a72

Please sign in to comment.