Skip to content

Commit

Permalink
Merge branch 'dev' into release/0.7.x
Browse files Browse the repository at this point in the history
  • Loading branch information
mikooomich committed Jan 21, 2025
2 parents 27fec3b + e00a083 commit 32b403c
Show file tree
Hide file tree
Showing 24 changed files with 464 additions and 201 deletions.
27 changes: 18 additions & 9 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@file:Suppress("UnstableApiUsage")

val isFullBuild: Boolean by rootProject.extra

plugins {
id("com.android.application")
Expand All @@ -19,8 +18,8 @@ android {
applicationId = "com.dd3boh.outertune"
minSdk = 24
targetSdk = 35
versionCode = 34
versionName = "0.7.0"
versionCode = 35
versionName = "0.7.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
Expand All @@ -33,6 +32,13 @@ android {
debug {
applicationIdSuffix = ".debug"
}

// userdebug is release builds without minify
create("userdebug") {
initWith(getByName("release"))
isMinifyEnabled = false
isShrinkResources = false
}
}

buildFeatures {
Expand Down Expand Up @@ -77,13 +83,16 @@ android {
abiFilters.add("x86_64")
}
}
// for uncommon, but non-obscure architectures
create("uncommon_abi") {
dimension = "abi"
ndk {
abiFilters.addAll(listOf("x86", "x86_64", "armeabi-v7a"))
}

applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName = "OuterTune-${variant.versionName}-${variant.baseName}.apk"
output.outputFileName = outputFileName
}
}
}

compileOptions {
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/dd3boh/outertune/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ class MainActivity : ComponentActivity() {
lateinit var syncUtils: SyncUtils

lateinit var activityLauncher: ActivityLauncherHelper
lateinit var connectivityObserver: NetworkConnectivityObserver

private var playerConnection by mutableStateOf<PlayerConnection?>(null)
private val serviceConnection = object : ServiceConnection {
Expand Down Expand Up @@ -314,11 +315,11 @@ class MainActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)

activityLauncher = ActivityLauncherHelper(this)
connectivityObserver = NetworkConnectivityObserver(this)

setContent {
val haptic = LocalHapticFeedback.current

val connectivityObserver = NetworkConnectivityObserver(this)
val isNetworkConnected by connectivityObserver.networkStatus.collectAsState(false)

val enableDynamicTheme by rememberPreference(DynamicThemeKey, defaultValue = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ val ShowLyricsKey = booleanPreferencesKey("showLyrics")
val LyricsTextPositionKey = stringPreferencesKey("lyricsTextPosition")
val MultilineLrcKey = booleanPreferencesKey("multilineLrc")
val LyricTrimKey = booleanPreferencesKey("lyricTrim")
val LyricSourcePrefKey = booleanPreferencesKey("preferLocalLyrics")
val LyricFontSizeKey = intPreferencesKey("lyricFontSize")


/**
Expand All @@ -77,6 +79,7 @@ val MaxImageCacheSizeKey = intPreferencesKey("maxImageCacheSize")
* Privacy
*/
val PauseListenHistoryKey = booleanPreferencesKey("pauseListenHistory")
val PauseRemoteListenHistoryKey = booleanPreferencesKey("pauseRemoteListenHistory")
val PauseSearchHistoryKey = booleanPreferencesKey("pauseSearchHistory")
val EnableKugouKey = booleanPreferencesKey("enableKugou")
val EnableLrcLibKey = booleanPreferencesKey("enableLrcLib")
Expand Down
45 changes: 41 additions & 4 deletions app/src/main/java/com/dd3boh/outertune/lyrics/LyricsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import android.content.Context
import android.os.Build
import android.util.LruCache
import androidx.annotation.RequiresApi
import com.dd3boh.outertune.constants.LyricSourcePrefKey
import com.dd3boh.outertune.db.MusicDatabase
import com.dd3boh.outertune.db.entities.LyricsEntity
import com.dd3boh.outertune.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND
import com.dd3boh.outertune.models.MediaMetadata
import com.dd3boh.outertune.utils.dataStore
import com.dd3boh.outertune.utils.get
import com.dd3boh.outertune.utils.reportException
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.first
import javax.inject.Inject

// true will prioritize local lyric files over all cloud providers, true is vice versa
private const val PREFER_LOCAL_LYRIC = true
class LyricsHelper @Inject constructor(
@ApplicationContext private val context: Context,
) {
Expand All @@ -23,17 +25,25 @@ class LyricsHelper @Inject constructor(
/**
* Retrieve lyrics from all sources
*
* How lyrics are resolved are determined by PreferLocalLyrics settings key. If this is true, prioritize local lyric
* files over all cloud providers, true is vice versa.
*
* Lyrics stored in the database are fetched first. If this is not available, it is resolved by other means.
* If local lyrics are preferred, lyrics from the lrc file is fetched, and then resolve by other means.
*
* @param mediaMetadata Song to fetch lyrics for
* @param database MusicDatabase connection. Database lyrics are prioritized over all sources.
* If no database is provided, the database source is disabled
*/
suspend fun getLyrics(mediaMetadata: MediaMetadata, database: MusicDatabase? = null): String {
val prefLocal = context.dataStore.get(LyricSourcePrefKey, true)

val cached = cache.get(mediaMetadata.id)?.firstOrNull()
if (cached != null) {
return cached.lyrics
}
val dbLyrics = database?.lyrics(mediaMetadata.id)?.let { it.first()?.lyrics }
if (dbLyrics != null) {
if (dbLyrics != null && !prefLocal) {
return dbLyrics
}

Expand All @@ -46,26 +56,53 @@ class LyricsHelper @Inject constructor(
val remoteLyrics: String?

// fallback to secondary provider when primary is unavailable
if (PREFER_LOCAL_LYRIC) {
if (prefLocal) {
if (localLyrics != null) {
return localLyrics
}
if (dbLyrics != null) {
return dbLyrics
}

// "lazy eval" the remote lyrics cuz it is laughably slow
remoteLyrics = getRemoteLyrics(mediaMetadata)
if (remoteLyrics != null) {
database?.query {
upsert(
LyricsEntity(
id = mediaMetadata.id,
lyrics = remoteLyrics
)
)
}
return remoteLyrics
}
} else {
remoteLyrics = getRemoteLyrics(mediaMetadata)
if (remoteLyrics != null) {
database?.query {
upsert(
LyricsEntity(
id = mediaMetadata.id,
lyrics = remoteLyrics
)
)
}
return remoteLyrics
} else if (localLyrics != null) {
return localLyrics
}

}

database?.query {
upsert(
LyricsEntity(
id = mediaMetadata.id,
lyrics = LYRICS_NOT_FOUND
)
)
}
return LYRICS_NOT_FOUND
}

Expand Down
22 changes: 0 additions & 22 deletions app/src/main/java/com/dd3boh/outertune/lyrics/LyricsUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,28 +81,6 @@ object LyricsUtils {
return emptyList<LyricsEntry>() + parseLyrics(lyrics, parserOptions).convertForLegacy()
}


// (OuterTune) Support compressed lyrics. We don't support word for word lyrics (Extended LRC) anyways
fun uncompressLyrics(lyrics: String): String {
var result = ""

lyrics.lines().forEach { it ->
val regex = Regex("""\[\d+:\d+\.\d+\]""")
val matches = regex.findAll(it)

if (matches.count() < 1) {
result += "$it\n"
} else {
val c = it.split("\\[(\\d+):(\\d{2})([.:]\\d+)?]".toRegex())
val content = c[c.size - 1]
for (string in matches) {
result += "${string.value}${content}\n"
}
}
}
return result
}

@OptIn(UnstableApi::class)
fun loadAndParseLyricsFile(musicFile: File?, parserOptions: LrcParserOptions): SemanticLyrics? {
val lrcFile = musicFile?.let { File(it.parentFile, it.nameWithoutExtension + ".lrc") }
Expand Down
18 changes: 12 additions & 6 deletions app/src/main/java/com/dd3boh/outertune/lyrics/SemanticLyrics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ private sealed class SyntacticLrc {
companion object {
// also eats space if present
val timeMarksRegex = "\\[(\\d{2}):(\\d{2})([.:]\\d+)?]".toRegex()
val timeMarksAfterWsRegex = "([ \t]+)\\[(\\d{2}):(\\d{2})([.:]\\d+)?]".toRegex()
val timeWordMarksRegex = "<(\\d{2}):(\\d{2})([.:]\\d+)?>".toRegex()
val metadataRegex = "\\[([a-zA-Z#]+):([^]]*)]".toRegex()

Expand Down Expand Up @@ -133,16 +134,21 @@ private sealed class SyntacticLrc {
// but hey, we tried. Can't do much about it.
// If you want to write something that looks like a timestamp into your lyrics,
// you'll probably have to delete the following three lines.
if (!(out.isNotEmpty() && out.last() is NewLine
|| out.isNotEmpty() && out.last() is SyncPoint)
)
if (!(out.lastOrNull() is NewLine || out.lastOrNull() is SyncPoint))
out.add(NewLine.SyntheticNewLine())
out.add(SyncPoint(parseTime(tmMatch)))
pos += tmMatch.value.length
continue
}
// Skip spaces in between of compressed lyric sync points. They really are
// completely useless information we can and should discard.
val tmwMatch = timeMarksAfterWsRegex.matchAt(text, pos)
if (out.lastOrNull() is SyncPoint && pos + 7 < text.length && tmwMatch != null) {
pos += tmwMatch.groupValues[1].length
continue
}
// Speaker points can only appear directly after a sync point
if (out.isNotEmpty() && out.last() is SyncPoint) {
if (out.lastOrNull() is SyncPoint) {
if (pos + 2 < text.length && text.regionMatches(pos, "v1:", 0, 3)) {
out.add(SpeakerTag(SpeakerEntity.Voice1))
pos += 3
Expand Down Expand Up @@ -240,7 +246,7 @@ private sealed class SyntacticLrc {
.let { if (it == pos - 1) text.length else it }
.let { if (it == pos) it + 1 else it }
val subText = text.substring(pos, firstUnsafeCharPos)
val last = if (out.isNotEmpty()) out.last() else null
val last = out.lastOrNull()
// Only count lyric text as lyric text if there is at least one kind of timestamp
// associated.
if (out.indexOfLast { it is NewLine } <
Expand All @@ -259,7 +265,7 @@ private sealed class SyntacticLrc {
}
pos = firstUnsafeCharPos
}
if (out.isNotEmpty() && out.last() is SyncPoint)
if (out.lastOrNull() is SyncPoint)
out.add(InvalidText(""))
if (out.isNotEmpty() && out.last() !is NewLine)
out.add(NewLine.SyntheticNewLine())
Expand Down
16 changes: 2 additions & 14 deletions app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleLike
import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleRepeatMode
import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleShuffle
import com.dd3boh.outertune.constants.PauseListenHistoryKey
import com.dd3boh.outertune.constants.PauseRemoteListenHistoryKey
import com.dd3boh.outertune.constants.PersistentQueueKey
import com.dd3boh.outertune.constants.PlayerVolumeKey
import com.dd3boh.outertune.constants.RepeatModeKey
Expand All @@ -73,7 +74,6 @@ import com.dd3boh.outertune.constants.minPlaybackDurKey
import com.dd3boh.outertune.db.MusicDatabase
import com.dd3boh.outertune.db.entities.Event
import com.dd3boh.outertune.db.entities.FormatEntity
import com.dd3boh.outertune.db.entities.LyricsEntity
import com.dd3boh.outertune.db.entities.RelatedSongMap
import com.dd3boh.outertune.di.DownloadCache
import com.dd3boh.outertune.extensions.SilentHandler
Expand Down Expand Up @@ -351,18 +351,6 @@ class MusicService : MediaLibraryService(),
dataStore.data.map { it[ShowLyricsKey] ?: false }.distinctUntilChanged()
) { mediaMetadata, showLyrics ->
mediaMetadata to showLyrics
}.collectLatest(scope) { (mediaMetadata, showLyrics) ->
if (showLyrics && mediaMetadata != null && database.lyrics(mediaMetadata.id).first() == null) {
val lyrics = lyricsHelper.getLyrics(mediaMetadata)
database.query {
upsert(
LyricsEntity(
id = mediaMetadata.id,
lyrics = lyrics
)
)
}
}
}

dataStore.data
Expand Down Expand Up @@ -832,7 +820,7 @@ class MusicService : MediaLibraryService(),
}

// TODO: support playlist id
if (mediaItem.metadata?.isLocal != true) {
if (mediaItem.metadata?.isLocal != true && dataStore.get(PauseRemoteListenHistoryKey, true)) {
CoroutineScope(Dispatchers.IO).launch {
val playbackUrl = database.format(mediaItem.mediaId).first()?.playbackUrl
?: YTPlayerUtils.playerResponseForMetadata(mediaItem.mediaId, null).getOrNull()?.playbackTracking?.videostatsPlaybackUrl?.baseUrl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ fun CreatePlaylistDialog(
) {
Column() {
Text(
text = "Sync Playlist",
text = stringResource(R.string.create_sync_playlist),
style = MaterialTheme.typography.titleLarge,
)

Text(
text = "Note: This allows for syncing with YouTube Music. This is NOT changeable later. You cannot add local songs to synced playlists.",
text = stringResource(R.string.create_sync_playlist_description),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.fillMaxWidth(0.7f)
)
Expand Down
Loading

0 comments on commit 32b403c

Please sign in to comment.