Skip to content

Commit

Permalink
Merge pull request #197 from 'reocat/pr' int dev
Browse files Browse the repository at this point in the history
Handle when FFmpeg plugin is not installed
  • Loading branch information
mikooomich authored Jan 8, 2025
2 parents 78e2878 + 54c0380 commit c0fef9a
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 25 deletions.
2 changes: 1 addition & 1 deletion app/src/main/java/com/dd3boh/outertune/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -387,10 +387,10 @@ class MainActivity : ComponentActivity() {
// Check if the permissions for local media access
if (firstSetupPassed && localLibEnable && autoScan
&& checkSelfPermission(MEDIA_PERMISSION_LEVEL) == PackageManager.PERMISSION_GRANTED) {
val scanner = LocalMediaScanner.getScanner(this@MainActivity, scannerImpl)

// equivalent to (quick scan)
try {
val scanner = LocalMediaScanner.getScanner(this@MainActivity, scannerImpl)
val directoryStructure = scanner.scanLocal(
database,
scanPaths.split('\n'),
Expand Down
17 changes: 12 additions & 5 deletions app/src/main/java/com/dd3boh/outertune/ui/component/Preference.kt
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ fun <T> ListPreference(
valueText: @Composable (T) -> String,
onValueSelected: (T) -> Unit,
isEnabled: Boolean = true,
disabled: (T) -> Boolean = { false }
) {
var showDialog by remember {
mutableStateOf(false)
Expand All @@ -105,25 +106,29 @@ fun <T> ListPreference(
onDismiss = { showDialog = false }
) {
items(values) { value ->
val isDisabled = disabled(value)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
.clickable(enabled = !isDisabled) {
showDialog = false
onValueSelected(value)
}
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
RadioButton(
selected = value == selectedValue,
onClick = null
onClick = null,
enabled = !isDisabled
)

Text(
text = valueText(value),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
modifier = Modifier
.padding(start = 16.dp)
.alpha(if (isDisabled) 0.5f else 1f)
)
}
}
Expand All @@ -149,7 +154,8 @@ inline fun <reified T : Enum<T>> EnumListPreference(
noinline valueText: @Composable (T) -> String,
noinline onValueSelected: (T) -> Unit,
isEnabled: Boolean = true,
values: List<T> = enumValues<T>().toList()
values: List<T> = enumValues<T>().toList(),
noinline disabled: (T) -> Boolean = { false }
) {
ListPreference(
modifier = modifier,
Expand All @@ -159,7 +165,8 @@ inline fun <reified T : Enum<T>> EnumListPreference(
values = values,
valueText = valueText,
onValueSelected = onValueSelected,
isEnabled = isEnabled
isEnabled = isEnabled,
disabled = disabled
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ fun ExperimentalSettings(
onClick = {
Toast.makeText(context, "Starting migration...", Toast.LENGTH_SHORT).show()
coroutineScope.launch(Dispatchers.IO) {
val scanner = LocalMediaScanner.getScanner(context, scannerImpl)
val scanner = LocalMediaScanner.getScanner(context, ScannerImpl.TAGLIB)
Timber.tag("Settings").d("Force Migrating local artists to YTM (MANUAL TRIGGERED)")
scanner.localToRemoteArtist(database)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ package com.dd3boh.outertune.ui.screens.settings

import android.Manifest
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Looper
Expand Down Expand Up @@ -40,6 +44,8 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -78,6 +84,7 @@ import com.dd3boh.outertune.ui.component.SwitchPreference
import com.dd3boh.outertune.ui.utils.DEFAULT_SCAN_PATH
import com.dd3boh.outertune.ui.utils.backToMain
import com.dd3boh.outertune.ui.utils.cacheDirectoryTree
import com.dd3boh.outertune.utils.isPackageInstalled
import com.dd3boh.outertune.utils.purgeCache
import com.dd3boh.outertune.utils.rememberEnumPreference
import com.dd3boh.outertune.utils.rememberPreference
Expand All @@ -92,6 +99,7 @@ import kotlinx.coroutines.launch
import java.time.LocalDateTime
import java.time.ZoneOffset


val MEDIA_PERMISSION_LEVEL =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) Manifest.permission.READ_MEDIA_AUDIO
else Manifest.permission.READ_EXTERNAL_STORAGE
Expand Down Expand Up @@ -332,10 +340,10 @@ fun LocalPlayerSettings(
scannerFailure = false

coroutineScope.launch(Dispatchers.IO) {
val scanner = getScanner(context, scannerImpl)
// full rescan
if (fullRescan) {
try {
val scanner = getScanner(context, scannerImpl)
val directoryStructure =
scanner.scanLocal(
database,
Expand Down Expand Up @@ -378,6 +386,7 @@ fun LocalPlayerSettings(
} else {
// quick scan
try {
val scanner = getScanner(context, scannerImpl)
val directoryStructure = scanner.scanLocal(
database,
scanPaths.split('\n'),
Expand Down Expand Up @@ -536,24 +545,38 @@ fun LocalPlayerSettings(
onCheckedChange = onStrictExtensionsChange
)
// scanner type
if (true) { // todo: detect if ext library is installed
EnumListPreference(
title = { Text(stringResource(R.string.scanner_type_title)) },
icon = { Icon(Icons.Rounded.Speed, null) },
selectedValue = scannerImpl,
onValueSelected = onScannerImplChange,
valueText = {
when (it) {
ScannerImpl.TAGLIB -> stringResource(R.string.scanner_type_taglib)
ScannerImpl.FFMPEG_EXT -> stringResource(R.string.scanner_type_ffmpeg_ext)
}
}
)
}
}

val isFFmpegInstalled = rememberFFmpegAvailability()

// if plugin is not found, although we reset if a scan is run, ensure the user is made aware if in settings page
LaunchedEffect(isFFmpegInstalled) {
if (scannerImpl == ScannerImpl.FFMPEG_EXT && !isFFmpegInstalled) {
onScannerImplChange(ScannerImpl.TAGLIB)
}
}

EnumListPreference(
title = { Text(stringResource(R.string.scanner_type_title)) },
icon = { Icon(Icons.Rounded.Speed, null) },
selectedValue = scannerImpl,
onValueSelected = {
if (it == ScannerImpl.FFMPEG_EXT && isFFmpegInstalled) {
onScannerImplChange(it)
} else {
Toast.makeText(context, "FFmpeg extractor not detected.", Toast.LENGTH_LONG).show()
// Explicitly revert to TagLib if FFmpeg is not available
onScannerImplChange(ScannerImpl.TAGLIB)
}
},
valueText = {
when (it) {
ScannerImpl.TAGLIB -> stringResource(R.string.scanner_type_taglib)
ScannerImpl.FFMPEG_EXT -> stringResource(R.string.scanner_type_ffmpeg_ext)
}
},
values = ScannerImpl.entries,
disabled = { it == ScannerImpl.FFMPEG_EXT && !isFFmpegInstalled }
)
}

TopAppBar(
title = { Text(stringResource(R.string.local_player_settings_title)) },
Expand All @@ -570,4 +593,44 @@ fun LocalPlayerSettings(
},
scrollBehavior = scrollBehavior
)
}

@Composable
fun rememberFFmpegAvailability(): Boolean {
val context = LocalContext.current
var isFFmpegInstalled by remember {
mutableStateOf(isPackageInstalled("wah.mikooomich.ffMetadataEx", context.packageManager))
}

DisposableEffect(context) {
val packageReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_PACKAGE_REMOVED,
Intent.ACTION_PACKAGE_ADDED -> {
isFFmpegInstalled = context?.packageManager?.let {
isPackageInstalled(
"wah.mikooomich.ffMetadataEx",
it
)
} == true
}
}
}
}

val filter = IntentFilter().apply {
addAction(Intent.ACTION_PACKAGE_REMOVED)
addAction(Intent.ACTION_PACKAGE_ADDED)
addDataScheme("package")
}

context.registerReceiver(packageReceiver, filter)

onDispose {
context.unregisterReceiver(packageReceiver)
}
}

return isFFmpegInstalled
}
13 changes: 13 additions & 0 deletions app/src/main/java/com/dd3boh/outertune/utils/Utils.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.dd3boh.outertune.utils

import android.content.pm.PackageManager
import com.dd3boh.outertune.db.entities.Artist
import com.dd3boh.outertune.ui.screens.settings.NavigationTab

Expand Down Expand Up @@ -102,4 +103,16 @@ fun numberToAlpha(l: Long): String {
alphabetMap[it.digitToInt()]
}
}.joinToString("")
}

/**
* Check if a package with the specified package name is installed
*/
fun isPackageInstalled(packageName: String, packageManager: PackageManager): Boolean {
return try {
packageManager.getPackageInfo(packageName, 0)
true
} catch (e: PackageManager.NameNotFoundException) {
false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ 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
import com.dd3boh.outertune.db.MusicDatabase
import com.dd3boh.outertune.db.entities.ArtistEntity
Expand All @@ -22,6 +28,8 @@ import com.dd3boh.outertune.ui.utils.SYNC_SCANNER
import com.dd3boh.outertune.ui.utils.cacheDirectoryTree
import com.dd3boh.outertune.ui.utils.scannerSession
import com.dd3boh.outertune.utils.closestMatch
import com.dd3boh.outertune.utils.dataStore
import com.dd3boh.outertune.utils.isPackageInstalled
import com.dd3boh.outertune.utils.reportException
import com.zionhuang.innertube.YouTube
import kotlinx.coroutines.Deferred
Expand Down Expand Up @@ -744,8 +752,24 @@ class LocalMediaScanner(val context: Context, val scannerImpl: ScannerImpl) {
* Trust me bro, it should never be null
*/
fun getScanner(context: Context, scannerImpl: ScannerImpl): LocalMediaScanner {
/*
if the FFmpeg extractor is suddenly removed and a scan is ran, reset to taglib, disable auto scanner.
we don't want to run the taglib scanner fallback if the user explicitly selected FFmpeg as differences
can muck with the song detection. Throw the error to the ui where it can be handled there
*/
val isFFmpegInstalled = isPackageInstalled("wah.mikooomich.ffMetadataEx", context.packageManager)
if (scannerImpl == ScannerImpl.FFMPEG_EXT && !isFFmpegInstalled) {
runBlocking {
context.dataStore.edit { settings ->
settings[ScannerImplKey] = ScannerImpl.TAGLIB.toString()
settings[AutomaticScannerKey] = false
}
}
throw ScannerAbortException("FFmpeg extractor was selected, but the package is no longer available. Reset to taglib scanner and disabled automatic scanning")
}

if (localScanner == null) {
localScanner = LocalMediaScanner(context, scannerImpl)
localScanner = LocalMediaScanner(context, if (isFFmpegInstalled) scannerImpl else ScannerImpl.TAGLIB)
}

return localScanner!!
Expand Down

0 comments on commit c0fef9a

Please sign in to comment.