From b736d68c817d1ed5caee623ed26a7fd77429a2fb Mon Sep 17 00:00:00 2001 From: mikooomich Date: Tue, 3 Sep 2024 13:19:37 -0400 Subject: [PATCH 01/10] app: Fix some crashes on older Android versions * Crash below SDK 30 (R) when accessing database, fixes #31 * Crash when accessing lyrics on SDK 24 (N) --- .../com/dd3boh/outertune/db/DatabaseDao.kt | 8 ++--- .../dd3boh/outertune/lyrics/LyricsHelper.kt | 29 ++++++++++++++----- .../outertune/playback/PlayerConnection.kt | 20 ++++++------- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/dd3boh/outertune/db/DatabaseDao.kt b/app/src/main/java/com/dd3boh/outertune/db/DatabaseDao.kt index e6c77c839..0c252cc05 100644 --- a/app/src/main/java/com/dd3boh/outertune/db/DatabaseDao.kt +++ b/app/src/main/java/com/dd3boh/outertune/db/DatabaseDao.kt @@ -544,15 +544,15 @@ interface DatabaseDao { fun albumWithSongs(albumId: String): Flow @Transaction - @Query("SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE bookmarkedAt IS NOT NULL OR isLocal = true ORDER BY rowId") + @Query("SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE bookmarkedAt IS NOT NULL OR isLocal = 1 ORDER BY rowId") fun playlistsByCreateDateAsc(): Flow> @Transaction - @Query("SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE bookmarkedAt IS NOT NULL OR isLocal = true ORDER BY name COLLATE NOCASE ASC") + @Query("SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE bookmarkedAt IS NOT NULL OR isLocal = 1 ORDER BY name COLLATE NOCASE ASC") fun playlistsByNameAsc(): Flow> @Transaction - @Query("SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE bookmarkedAt IS NOT NULL OR isLocal = true ORDER BY songCount") + @Query("SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE bookmarkedAt IS NOT NULL OR isLocal = 1 ORDER BY songCount") fun playlistsBySongCountAsc(): Flow> @Transaction @@ -595,7 +595,7 @@ interface DatabaseDao { fun playlistByBrowseId(browseId: String): Flow @Transaction - @Query("UPDATE playlist SET isLocal = true WHERE id = :playlistId") + @Query("UPDATE playlist SET isLocal = 1 WHERE id = :playlistId") fun playlistDesync(playlistId: String): Unit @Transaction diff --git a/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsHelper.kt b/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsHelper.kt index 1b191746d..1c970ddd8 100644 --- a/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsHelper.kt +++ b/app/src/main/java/com/dd3boh/outertune/lyrics/LyricsHelper.kt @@ -3,10 +3,13 @@ package com.dd3boh.outertune.lyrics import android.content.Context import android.os.Build import android.util.LruCache +import androidx.annotation.RequiresApi +import com.dd3boh.outertune.db.MusicDatabase import com.dd3boh.outertune.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND import com.dd3boh.outertune.models.MediaMetadata 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 @@ -17,14 +20,30 @@ class LyricsHelper @Inject constructor( private val lyricsProviders = listOf(YouTubeSubtitleLyricsProvider, LrcLibLyricsProvider, KuGouLyricsProvider, YouTubeLyricsProvider) private val cache = LruCache>(MAX_CACHE_SIZE) - suspend fun getLyrics(mediaMetadata: MediaMetadata): String { + /** + * Retrieve lyrics from all sources + * + * @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 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) { + return dbLyrics + } + + // Nougat support is likely going to be dropped soon + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return getRemoteLyrics(mediaMetadata) ?: LYRICS_NOT_FOUND + } val localLyrics = getLocalLyrics(mediaMetadata) - var remoteLyrics: String? + val remoteLyrics: String? // fallback to secondary provider when primary is unavailable if (PREFER_LOCAL_LYRIC) { @@ -74,10 +93,8 @@ class LyricsHelper @Inject constructor( /** * Lookup lyrics from local disk (.lrc) file */ + @RequiresApi(Build.VERSION_CODES.O) suspend fun getLocalLyrics(mediaMetadata: MediaMetadata): String? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - throw Exception("Local lyrics are not supported below SDK 26 (Oreo)") - } if (LocalLyricsProvider.isEnabled(context)) { LocalLyricsProvider.getLyrics( mediaMetadata.id, @@ -86,8 +103,6 @@ class LyricsHelper @Inject constructor( mediaMetadata.duration ).onSuccess { lyrics -> return lyrics - }.onFailure { - reportException(it) } } diff --git a/app/src/main/java/com/dd3boh/outertune/playback/PlayerConnection.kt b/app/src/main/java/com/dd3boh/outertune/playback/PlayerConnection.kt index 47d8f50ca..928857db1 100644 --- a/app/src/main/java/com/dd3boh/outertune/playback/PlayerConnection.kt +++ b/app/src/main/java/com/dd3boh/outertune/playback/PlayerConnection.kt @@ -47,19 +47,17 @@ class PlayerConnection( database.song(it?.id) } val currentLyrics = mediaMetadata.flatMapLatest { mediaMetadata -> - // local songs will always look at lrc files first - if (mediaMetadata?.isLocal == true) { - val lyrics = service.lyricsHelper.getLocalLyrics(mediaMetadata) - if (lyrics != null) { - return@flatMapLatest flowOf( - LyricsEntity( - id = mediaMetadata.id, - lyrics = lyrics - ) + if (mediaMetadata != null) { + val lyrics = service.lyricsHelper.getLyrics(mediaMetadata, database) + return@flatMapLatest flowOf( + LyricsEntity( + id = mediaMetadata.id, + lyrics = lyrics ) - } + ) + } else { + return@flatMapLatest flowOf() } - database.lyrics(mediaMetadata?.id) } val currentFormat = mediaMetadata.flatMapLatest { mediaMetadata -> database.format(mediaMetadata?.id) From 8bc443ca1ea996783bd8ca60e76df78b2e204a62 Mon Sep 17 00:00:00 2001 From: mikooomich Date: Tue, 3 Sep 2024 14:51:19 -0400 Subject: [PATCH 02/10] gradle: IzzyOnDroid * test --- app/build.gradle.kts | 8 ++++++++ app/prebuilt/ffMetadataEx-release.aar | Bin 5893873 -> 5893690 bytes .../{FFProbeScanner.kt => FFMpegScanner.kt} | 16 ++++++++-------- .../utils/scanners/LocalMediaScanner.kt | 2 +- ffMetadataEx/build.gradle.kts | 6 +++--- ffMetadataEx/src/main/cpp/CMakeLists.txt | 6 +++--- .../main/cpp/{ffprobejni.c => ffMetaExJni.c} | 4 ++-- .../dd3boh/ffMetadataEx/FFMpegWrapper.kt} | 4 ++-- 8 files changed, 27 insertions(+), 19 deletions(-) rename app/src/main/java/com/dd3boh/outertune/utils/scanners/{FFProbeScanner.kt => FFMpegScanner.kt} (95%) rename ffMetadataEx/src/main/cpp/{ffprobejni.c => ffMetaExJni.c} (96%) rename ffMetadataEx/src/main/java/{wah/mikooomich/ffMetadataEx/FFprobeWrapper.kt => com/dd3boh/ffMetadataEx/FFMpegWrapper.kt} (72%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8e16aeb3c..66acc2c30 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -99,6 +99,14 @@ android { jvmTarget = "17" } + // for IzzyOnDroid + dependenciesInfo { + // Disables dependency metadata when building APKs. + includeInApk = false + // Disables dependency metadata when building Android App Bundles. + includeInBundle = false + } + testOptions { unitTests.isIncludeAndroidResources = true unitTests.isReturnDefaultValues = true diff --git a/app/prebuilt/ffMetadataEx-release.aar b/app/prebuilt/ffMetadataEx-release.aar index af618f9a99e3a222e5da93810909523f60b27966..7d229167b2f208c679d36df306b529721a37f431 100644 GIT binary patch delta 14508 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 If?!AbKU)ffB>(^b diff --git a/app/src/main/java/com/dd3boh/outertune/utils/scanners/FFProbeScanner.kt b/app/src/main/java/com/dd3boh/outertune/utils/scanners/FFMpegScanner.kt similarity index 95% rename from app/src/main/java/com/dd3boh/outertune/utils/scanners/FFProbeScanner.kt rename to app/src/main/java/com/dd3boh/outertune/utils/scanners/FFMpegScanner.kt index bfb1f9a53..d77815129 100644 --- a/app/src/main/java/com/dd3boh/outertune/utils/scanners/FFProbeScanner.kt +++ b/app/src/main/java/com/dd3boh/outertune/utils/scanners/FFMpegScanner.kt @@ -1,5 +1,6 @@ package com.dd3boh.outertune.utils.scanners +import com.dd3boh.ffMetadataEx.FFMpegWrapper import com.dd3boh.outertune.db.entities.AlbumEntity import com.dd3boh.outertune.db.entities.ArtistEntity import com.dd3boh.outertune.db.entities.FormatEntity @@ -9,7 +10,6 @@ import com.dd3boh.outertune.db.entities.SongEntity import com.dd3boh.outertune.models.SongTempData import com.dd3boh.outertune.ui.utils.ARTIST_SEPARATORS import timber.log.Timber -import wah.mikooomich.ffMetadataEx.FFprobeWrapper import java.io.File import java.lang.Integer.parseInt import java.lang.Long.parseLong @@ -21,15 +21,15 @@ import kotlin.math.roundToLong const val EXTRACTOR_DEBUG = false const val DEBUG_SAVE_OUTPUT = false // ignored (will be false) when EXTRACTOR_DEBUG IS false -const val EXTRACTOR_TAG = "FFProbeExtractor" +const val EXTRACTOR_TAG = "FFMpegExtractor" const val toSeconds = 1000 * 60 * 16.7 // convert FFmpeg duration to seconds -class FFProbeScanner : MetadataScanner { +class FFMpegScanner : MetadataScanner { // load advanced scanner libs init { System.loadLibrary("avcodec") System.loadLibrary("avdevice") - System.loadLibrary("ffprobejni") + System.loadLibrary("ffmetaexjni") System.loadLibrary("avfilter") System.loadLibrary("avformat") System.loadLibrary("avutil") @@ -45,8 +45,8 @@ class FFProbeScanner : MetadataScanner { override fun getMediaStoreSupplement(path: String): ExtraMetadataWrapper { if (EXTRACTOR_DEBUG) Timber.tag(EXTRACTOR_TAG).d("Starting MediaStoreSupplement session on: $path") - val ffprobe = FFprobeWrapper() - val data = ffprobe.getAudioMetadata(path) + val ffmpeg = FFMpegWrapper() + val data = ffmpeg.getAudioMetadata(path) if (EXTRACTOR_DEBUG && DEBUG_SAVE_OUTPUT) { Timber.tag(EXTRACTOR_TAG).d("Full output for: $path \n $data") @@ -77,8 +77,8 @@ class FFProbeScanner : MetadataScanner { override fun getAllMetadata(path: String): SongTempData { if (EXTRACTOR_DEBUG) Timber.tag(EXTRACTOR_TAG).d("Starting Full Extractor session on: $path") - val ffprobe = FFprobeWrapper() - val data = ffprobe.getFullAudioMetadata(path) + val ffmpeg = FFMpegWrapper() + val data = ffmpeg.getFullAudioMetadata(path) if (EXTRACTOR_DEBUG && DEBUG_SAVE_OUTPUT) { Timber.tag(EXTRACTOR_TAG).d("Full output for: $path \n $data") diff --git a/app/src/main/java/com/dd3boh/outertune/utils/scanners/LocalMediaScanner.kt b/app/src/main/java/com/dd3boh/outertune/utils/scanners/LocalMediaScanner.kt index feb48aa52..a4ff76c9d 100644 --- a/app/src/main/java/com/dd3boh/outertune/utils/scanners/LocalMediaScanner.kt +++ b/app/src/main/java/com/dd3boh/outertune/utils/scanners/LocalMediaScanner.kt @@ -613,7 +613,7 @@ class LocalMediaScanner { */ fun getAdvancedScanner(): MetadataScanner? { // kotlin won't let me return MetadataScanner even if it cant possibly be null broooo - return if (advancedScannerImpl is FFProbeScanner) advancedScannerImpl else FFProbeScanner() + return if (advancedScannerImpl is FFMpegScanner) advancedScannerImpl else FFMpegScanner() } fun unloadAdvancedScanner() { diff --git a/ffMetadataEx/build.gradle.kts b/ffMetadataEx/build.gradle.kts index f46c1967a..5c717e66e 100644 --- a/ffMetadataEx/build.gradle.kts +++ b/ffMetadataEx/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } android { - namespace = "com.dd3boh.ffmpegex" + namespace = "com.dd3boh.ffMetadataEx" compileSdk = 35 defaultConfig { @@ -12,14 +12,14 @@ android { externalNativeBuild { cmake { - cppFlags("") + arguments += listOf("-DCMAKE_SHARED_LINKER_FLAGS=-Wl,--build-id=none") } } } buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = false // proguard or whatever isn't set up } } diff --git a/ffMetadataEx/src/main/cpp/CMakeLists.txt b/ffMetadataEx/src/main/cpp/CMakeLists.txt index ab614bf1d..2f13a56d1 100644 --- a/ffMetadataEx/src/main/cpp/CMakeLists.txt +++ b/ffMetadataEx/src/main/cpp/CMakeLists.txt @@ -38,14 +38,14 @@ set_target_properties( # Specifies the target library. # Include FFmpeg headers include_directories(${CMAKE_SOURCE_DIR}/ffmpeg-android-maker/output/include/${ANDROID_ABI}) -add_library(ffprobejni SHARED ffprobejni.c) +add_library(ffmetaexjni SHARED ffMetaExJni.c) # Link FFmpeg libraries -target_link_libraries(ffprobejni +target_link_libraries(ffmetaexjni avformat avutil avcodec ) # Set the output directory for the .so file -set_target_properties(ffprobejni PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}) \ No newline at end of file +set_target_properties(ffmetaexjni PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}) \ No newline at end of file diff --git a/ffMetadataEx/src/main/cpp/ffprobejni.c b/ffMetadataEx/src/main/cpp/ffMetaExJni.c similarity index 96% rename from ffMetadataEx/src/main/cpp/ffprobejni.c rename to ffMetadataEx/src/main/cpp/ffMetaExJni.c index bb6a2a376..48c7cc378 100644 --- a/ffMetadataEx/src/main/cpp/ffprobejni.c +++ b/ffMetadataEx/src/main/cpp/ffMetaExJni.c @@ -4,7 +4,7 @@ #include JNIEXPORT jstring JNICALL -Java_wah_mikooomich_ffMetadataEx_FFprobeWrapper_getAudioMetadata(JNIEnv* env, jobject obj, jstring filePath) { +Java_com_dd3boh_ffMetadataEx_FFMpegWrapper_getAudioMetadata(JNIEnv* env, jobject obj, jstring filePath) { const char* file_path = (*env)->GetStringUTFChars(env, filePath, NULL); if (!file_path) { return (*env)->NewStringUTF(env, "Error getting file path"); @@ -63,7 +63,7 @@ Java_wah_mikooomich_ffMetadataEx_FFprobeWrapper_getAudioMetadata(JNIEnv* env, jo JNIEXPORT jstring JNICALL -Java_wah_mikooomich_ffMetadataEx_FFprobeWrapper_getFullAudioMetadata(JNIEnv* env, jobject obj, jstring filePath) { +Java_com_dd3boh_ffMetadataEx_FFMpegWrapper_getFullAudioMetadata(JNIEnv* env, jobject obj, jstring filePath) { const char* file_path = (*env)->GetStringUTFChars(env, filePath, NULL); if (!file_path) { return (*env)->NewStringUTF(env, "Error getting file path"); diff --git a/ffMetadataEx/src/main/java/wah/mikooomich/ffMetadataEx/FFprobeWrapper.kt b/ffMetadataEx/src/main/java/com/dd3boh/ffMetadataEx/FFMpegWrapper.kt similarity index 72% rename from ffMetadataEx/src/main/java/wah/mikooomich/ffMetadataEx/FFprobeWrapper.kt rename to ffMetadataEx/src/main/java/com/dd3boh/ffMetadataEx/FFMpegWrapper.kt index 069c841d8..06cc3a456 100644 --- a/ffMetadataEx/src/main/java/wah/mikooomich/ffMetadataEx/FFprobeWrapper.kt +++ b/ffMetadataEx/src/main/java/com/dd3boh/ffMetadataEx/FFMpegWrapper.kt @@ -1,9 +1,9 @@ -package wah.mikooomich.ffMetadataEx +package com.dd3boh.ffMetadataEx /** * Pain and suffering. */ -class FFprobeWrapper { +class FFMpegWrapper { external fun getAudioMetadata(filePath: String): String external fun getFullAudioMetadata(filePath: String): String From 304ec7cf036fbb231b022e4f5596539c9f44389e Mon Sep 17 00:00:00 2001 From: mikooomich Date: Tue, 3 Sep 2024 23:04:55 -0400 Subject: [PATCH 03/10] Revert "app: MusicService: Pause before re-shuffling" This reverts commit c154b207b206009e65d84df364e5af5e6d1d8705. * Does not work anymore (if it did at all). Causes issues with app closing itself --- app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt b/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt index 52fba41f6..f84556be4 100644 --- a/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt +++ b/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt @@ -278,10 +278,8 @@ class MusicService : MediaLibraryService(), if (player.currentMediaItemIndex == 0 && lastMediaItemIndex == player.mediaItemCount - 1 && (reason == MEDIA_ITEM_TRANSITION_REASON_AUTO || reason == MEDIA_ITEM_TRANSITION_REASON_SEEK) && isShuffleEnabled.value && player.repeatMode == REPEAT_MODE_ALL) { - player.pause() queueBoard.shuffleCurrent(this@MusicService, false) // reshuffle queue queueBoard.setCurrQueue(this@MusicService) - player.play() } lastMediaItemIndex = player.currentMediaItemIndex From c62f3b2e41142868f1dd0091495b5d383f547051 Mon Sep 17 00:00:00 2001 From: mikooomich Date: Thu, 5 Sep 2024 20:12:52 -0400 Subject: [PATCH 04/10] Revert "app: Use a FG notification to keep app alive" This reverts commit a04bc667484511ffef6bcdf6d2152688ecba8306. * MusicService will still die when it's not primary player notification * In favour of resumption --- app/src/main/AndroidManifest.xml | 7 -- .../outertune/constants/PreferenceKeys.kt | 1 - .../dd3boh/outertune/playback/KeepAlive.kt | 77 ------------------- .../dd3boh/outertune/playback/MusicService.kt | 17 ---- .../ui/screens/settings/PlayerSettings.kt | 68 ---------------- 5 files changed, 170 deletions(-) delete mode 100644 app/src/main/java/com/dd3boh/outertune/playback/KeepAlive.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4b85e956b..ff64d9feb 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -140,13 +140,6 @@ - - - = Build.VERSION_CODES.O) { - val serviceChannel = NotificationChannel( - KEEP_ALIVE_CHANNEL_ID, - "ot_keep_alive", - NotificationManager.IMPORTANCE_MIN - ) - val manager = getSystemService(NotificationManager::class.java) - manager.createNotificationChannel(serviceChannel) - } - } - - private fun getNotification(): Notification { - val notificationIntent = Intent(this, MainActivity::class.java) - val pendingIntent = PendingIntent.getActivity( - this, 0, notificationIntent, - PendingIntent.FLAG_IMMUTABLE - ) - val builder = NotificationCompat.Builder(this, KEEP_ALIVE_CHANNEL_ID) - .setContentTitle("Keep alive") - .setSmallIcon(R.drawable.small_icon) - .setContentIntent(pendingIntent) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setVisibility(NotificationCompat.VISIBILITY_SECRET) - .setAutoCancel(false) - .setShowWhen(false) - .setLocalOnly(true) - .setOngoing(true) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - builder.setForegroundServiceBehavior(Notification.FOREGROUND_SERVICE_IMMEDIATE) - } - return builder.build() - } - - companion object { - const val KEEP_ALIVE_CHANNEL_ID = "outertune_keep_alive" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt b/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt index f84556be4..4771c450a 100644 --- a/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt +++ b/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt @@ -55,7 +55,6 @@ import com.dd3boh.outertune.R import com.dd3boh.outertune.constants.AudioNormalizationKey import com.dd3boh.outertune.constants.AudioQuality import com.dd3boh.outertune.constants.AudioQualityKey -import com.dd3boh.outertune.constants.KeepAliveKey import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleLike import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleRepeatMode import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleShuffle @@ -189,22 +188,6 @@ class MusicService : MediaLibraryService(), setSmallIcon(R.drawable.small_icon) } ) - - // FG notification - if (dataStore.get(KeepAliveKey, false)) { - try { - startService(Intent(this, KeepAlive::class.java)) - } catch (e: Exception) { - reportException(e) - } - } else { - try { - stopService(Intent(this, KeepAlive::class.java)) - } catch (e: Exception) { - reportException(e) - } - } - player = ExoPlayer.Builder(this) .setMediaSourceFactory(DefaultMediaSourceFactory(createDataSourceFactory())) .setRenderersFactory(createRenderersFactory()) diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PlayerSettings.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PlayerSettings.kt index 97dbf6733..204284784 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PlayerSettings.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/settings/PlayerSettings.kt @@ -1,11 +1,5 @@ package com.dd3boh.outertune.ui.screens.settings -import android.Manifest -import android.app.Activity -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Build -import android.widget.Toast import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -21,7 +15,6 @@ import androidx.compose.material.icons.rounded.ClearAll import androidx.compose.material.icons.rounded.FastForward import androidx.compose.material.icons.rounded.GraphicEq import androidx.compose.material.icons.rounded.Lyrics -import androidx.compose.material.icons.rounded.NoCell import androidx.compose.material.icons.rounded.Sync import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -31,7 +24,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -39,24 +31,20 @@ import androidx.compose.runtime.remember 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.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.core.app.ActivityCompat import androidx.navigation.NavController import com.dd3boh.outertune.LocalPlayerAwareWindowInsets import com.dd3boh.outertune.R import com.dd3boh.outertune.constants.AudioNormalizationKey import com.dd3boh.outertune.constants.AudioQuality import com.dd3boh.outertune.constants.AudioQualityKey -import com.dd3boh.outertune.constants.KeepAliveKey import com.dd3boh.outertune.constants.PersistentQueueKey import com.dd3boh.outertune.constants.SkipOnErrorKey import com.dd3boh.outertune.constants.SkipSilenceKey import com.dd3boh.outertune.constants.StopMusicOnTaskClearKey import com.dd3boh.outertune.constants.minPlaybackDurKey -import com.dd3boh.outertune.playback.KeepAlive import com.dd3boh.outertune.ui.component.ActionPromptDialog import com.dd3boh.outertune.ui.component.EnumListPreference import com.dd3boh.outertune.ui.component.IconButton @@ -66,7 +54,6 @@ import com.dd3boh.outertune.ui.component.SwitchPreference import com.dd3boh.outertune.ui.utils.backToMain import com.dd3boh.outertune.utils.rememberEnumPreference import com.dd3boh.outertune.utils.rememberPreference -import com.dd3boh.outertune.utils.reportException @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -74,8 +61,6 @@ fun PlayerSettings( navController: NavController, scrollBehavior: TopAppBarScrollBehavior, ) { - val context = LocalContext.current - val (audioQuality, onAudioQualityChange) = rememberEnumPreference(key = AudioQualityKey, defaultValue = AudioQuality.AUTO) val (persistentQueue, onPersistentQueueChange) = rememberPreference(key = PersistentQueueKey, defaultValue = true) val (skipSilence, onSkipSilenceChange) = rememberPreference(key = SkipSilenceKey, defaultValue = false) @@ -83,7 +68,6 @@ fun PlayerSettings( val (audioNormalization, onAudioNormalizationChange) = rememberPreference(key = AudioNormalizationKey, defaultValue = true) val (stopMusicOnTaskClear, onStopMusicOnTaskClearChange) = rememberPreference(key = StopMusicOnTaskClearKey, defaultValue = false) val (minPlaybackDur, onMinPlaybackDurChange) = rememberPreference(minPlaybackDurKey, defaultValue = 30) - val (keepAlive, onKeepAliveChange) = rememberPreference(key = KeepAliveKey, defaultValue = false) var showMinPlaybackDur by remember { mutableStateOf(false) @@ -92,51 +76,6 @@ fun PlayerSettings( mutableIntStateOf(minPlaybackDur) } - fun toggleKeepAlive(newValue: Boolean) { - // disable and request if disabled - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - && context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - onKeepAliveChange(false) - Toast.makeText( - context, - "Notification permission is required", - Toast.LENGTH_SHORT - ).show() - - ActivityCompat.requestPermissions( - context as Activity, - arrayOf( Manifest.permission.POST_NOTIFICATIONS), PackageManager.PERMISSION_GRANTED - ) - return - } - - if (keepAlive != newValue) { - onKeepAliveChange(newValue) - // start/stop service accordingly - if (newValue) { - try { - context.startService(Intent(context, KeepAlive::class.java)) - } catch (e: Exception) { - reportException(e) - } - } else { - try { - context.stopService(Intent(context, KeepAlive::class.java)) - } catch (e: Exception) { - reportException(e) - } - } - } - } - - // reset if no permission - LaunchedEffect(keepAlive) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - && context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - onKeepAliveChange(false) - } - } - if (showMinPlaybackDur) { ActionPromptDialog( @@ -246,13 +185,6 @@ fun PlayerSettings( checked = stopMusicOnTaskClear, onCheckedChange = onStopMusicOnTaskClearChange ) - SwitchPreference( - title = { Text(stringResource(R.string.keep_alive_title)) }, - description = stringResource(R.string.keep_alive_description), - icon = { Icon(Icons.Rounded.NoCell, null) }, - checked = keepAlive, - onCheckedChange = { toggleKeepAlive(it) } - ) } TopAppBar( From 3f891dac54b2a7f1c531f6479618e27f4064d598 Mon Sep 17 00:00:00 2001 From: mikooomich Date: Thu, 5 Sep 2024 23:31:42 -0400 Subject: [PATCH 05/10] player: Playback resumption feature --- app/src/main/AndroidManifest.xml | 7 +++++++ .../main/java/com/dd3boh/outertune/MainActivity.kt | 10 ++++++++++ .../com/dd3boh/outertune/constants/PreferenceKeys.kt | 2 ++ .../com/dd3boh/outertune/playback/MusicService.kt | 11 ++++++++++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ff64d9feb..891c6d6d6 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -140,6 +140,13 @@ + + + + + + + settings[LastPosKey] = playerConnection!!.player.currentPosition + } + } + if (dataStore.get(StopMusicOnTaskClearKey, false) && playerConnection?.isPlaying?.value == true && isFinishing) { stopService(Intent(this, MusicService::class.java)) diff --git a/app/src/main/java/com/dd3boh/outertune/constants/PreferenceKeys.kt b/app/src/main/java/com/dd3boh/outertune/constants/PreferenceKeys.kt index 76c2adee9..2c723061d 100644 --- a/app/src/main/java/com/dd3boh/outertune/constants/PreferenceKeys.kt +++ b/app/src/main/java/com/dd3boh/outertune/constants/PreferenceKeys.kt @@ -3,6 +3,7 @@ package com.dd3boh.outertune.constants import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.floatPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey val DynamicThemeKey = booleanPreferencesKey("dynamicTheme") @@ -125,6 +126,7 @@ val LyricsTextPositionKey = stringPreferencesKey("lyricsTextPosition") val PlayerVolumeKey = floatPreferencesKey("playerVolume") val RepeatModeKey = intPreferencesKey("repeatMode") +val LastPosKey = longPreferencesKey("lastPosKey") val LockQueueKey = booleanPreferencesKey("lockQueue") val SearchSourceKey = stringPreferencesKey("searchSource") diff --git a/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt b/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt index 4771c450a..aed70b6dc 100644 --- a/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt +++ b/app/src/main/java/com/dd3boh/outertune/playback/MusicService.kt @@ -55,6 +55,7 @@ import com.dd3boh.outertune.R import com.dd3boh.outertune.constants.AudioNormalizationKey import com.dd3boh.outertune.constants.AudioQuality import com.dd3boh.outertune.constants.AudioQualityKey +import com.dd3boh.outertune.constants.LastPosKey import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleLike import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleRepeatMode import com.dd3boh.outertune.constants.MediaSessionConstants.CommandToggleShuffle @@ -368,7 +369,10 @@ class MusicService : MediaLibraryService(), if (queue != null) { isShuffleEnabled.value = queue.shuffled CoroutineScope(Dispatchers.Main).launch { - queueBoard.setCurrQueue(this@MusicService) + val queuePos = queueBoard.setCurrQueue(this@MusicService, false) + if (queuePos != null) { + player.seekTo(queuePos, dataStore.get(LastPosKey, C.TIME_UNSET)) + } } } } @@ -809,6 +813,11 @@ class MusicService : MediaLibraryService(), override fun onDestroy() { if (dataStore.get(PersistentQueueKey, true)) { saveQueueToDisk() + scope.launch { + dataStore.edit { settings -> + settings[LastPosKey] = player.currentPosition + } + } } mediaSession.release() player.removeListener(this) From 82fa9d654db0ea9c3b2f927e144be9a589afd4fb Mon Sep 17 00:00:00 2001 From: mikooomich Date: Fri, 6 Sep 2024 00:20:01 -0400 Subject: [PATCH 06/10] readme: Add download button graphics --- README.md | 4 +- assets/IzzyOnDroidButtonGreyBorder.svg | 76 +++++++++++++++++++++++++ assets/badge_github.png | Bin 0 -> 13094 bytes 3 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 assets/IzzyOnDroidButtonGreyBorder.svg create mode 100644 assets/badge_github.png diff --git a/README.md b/README.md index 503f6adb7..75669baef 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ A Material 3 YouTube Music client & local music player for Android [![License](https://img.shields.io/github/license/DD3Boh/OuterTune)](https://www.gnu.org/licenses/gpl-3.0) [![Downloads](https://img.shields.io/github/downloads/DD3Boh/OuterTune/total)](https://github.com/DD3Boh/OuterTune/releases) -[Get it on GitHub](https://github.com/DD3Boh/OuterTune/releases/latest) - +[Get it on GitHub](https://github.com/DD3Boh/OuterTune/releases/latest) +[Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.dd3boh.outertune) ## Features OuterTune is a supercharged fork of [InnerTune](https://github.com/z-huang/InnerTune), with advanced account synchronization, local media playback, multiple queues, and a new take on UI design. diff --git a/assets/IzzyOnDroidButtonGreyBorder.svg b/assets/IzzyOnDroidButtonGreyBorder.svg new file mode 100644 index 000000000..184cfd7bb --- /dev/null +++ b/assets/IzzyOnDroidButtonGreyBorder.svg @@ -0,0 +1,76 @@ + +IzzyOnDroid Button + + + + + +GET IT ONIzzyOnDroidimage/svg+xmlIzzyOnDroid ButtonIzzy, Wolfshappen diff --git a/assets/badge_github.png b/assets/badge_github.png new file mode 100644 index 0000000000000000000000000000000000000000..4440f3767e3bb3efefbbe5ffbb67cc64c15670d8 GIT binary patch literal 13094 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 literal 0 HcmV?d00001 From 3952080e4bb74facaf392dd0e720e0003e0aa989 Mon Sep 17 00:00:00 2001 From: mikooomich Date: Fri, 6 Sep 2024 14:01:28 -0400 Subject: [PATCH 07/10] ui: Reland explicit inLibrary toggle * fixes #33 --- .../com/dd3boh/outertune/ui/menu/AlbumMenu.kt | 33 ++++++++++++++++ .../dd3boh/outertune/ui/menu/PlayerMenu.kt | 24 ++++++++++++ .../outertune/ui/menu/SelectionSongsMenu.kt | 38 +++++++++++++++++++ .../com/dd3boh/outertune/ui/menu/SongMenu.kt | 23 +++++++++++ app/src/main/res/values/strings.xml | 4 ++ 5 files changed, 122 insertions(+) diff --git a/app/src/main/java/com/dd3boh/outertune/ui/menu/AlbumMenu.kt b/app/src/main/java/com/dd3boh/outertune/ui/menu/AlbumMenu.kt index ddbc38a3c..921f6899f 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/menu/AlbumMenu.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/menu/AlbumMenu.kt @@ -19,6 +19,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.PlaylistAdd import androidx.compose.material.icons.automirrored.rounded.PlaylistPlay import androidx.compose.material.icons.automirrored.rounded.QueueMusic +import androidx.compose.material.icons.rounded.LibraryAdd +import androidx.compose.material.icons.rounded.LibraryAddCheck import androidx.compose.material.icons.rounded.Share import androidx.compose.material.icons.rounded.Sync import androidx.compose.material3.HorizontalDivider @@ -77,6 +79,7 @@ import com.dd3boh.outertune.ui.component.ListDialog import com.zionhuang.innertube.YouTube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.time.LocalDateTime @Composable fun AlbumMenu( @@ -94,6 +97,13 @@ fun AlbumMenu( var songs by remember { mutableStateOf(emptyList()) } + val allInLibrary = remember(songs) { + songs.all { it.song.inLibrary != null } + } +// for when local albums are a thing +// val allLocal by remember(songs) { // if only local songs in this selection +// mutableStateOf(songs.isNotEmpty() && songs.all { it.song.isLocal }) +// } val coroutineScope = rememberCoroutineScope() @@ -269,6 +279,29 @@ fun AlbumMenu( ) { showChoosePlaylistDialog = true } + if (allInLibrary) { + GridMenuItem( + icon = Icons.Rounded.LibraryAddCheck, + title = R.string.remove_all_from_library + ) { + database.transaction { + songs.forEach { + inLibrary(it.id, null) + } + } + } + } else { + GridMenuItem( + icon = Icons.Rounded.LibraryAdd, + title = R.string.add_all_to_library + ) { + database.transaction { + songs.forEach { + inLibrary(it.id, LocalDateTime.now()) + } + } + } + } DownloadGridMenu( state = downloadState, onDownload = { diff --git a/app/src/main/java/com/dd3boh/outertune/ui/menu/PlayerMenu.kt b/app/src/main/java/com/dd3boh/outertune/ui/menu/PlayerMenu.kt index 111f28a46..8fbbbc46f 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/menu/PlayerMenu.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/menu/PlayerMenu.kt @@ -23,6 +23,8 @@ import androidx.compose.material.icons.automirrored.rounded.PlaylistAdd import androidx.compose.material.icons.automirrored.rounded.VolumeUp import androidx.compose.material.icons.rounded.AddCircleOutline import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.LibraryAdd +import androidx.compose.material.icons.rounded.LibraryAddCheck import androidx.compose.material.icons.rounded.Radio import androidx.compose.material.icons.rounded.RemoveCircleOutline import androidx.compose.material.icons.rounded.Share @@ -83,6 +85,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import java.time.LocalDateTime import kotlin.math.log2 import kotlin.math.pow import kotlin.math.round @@ -102,6 +105,7 @@ fun PlayerMenu( val playerConnection = LocalPlayerConnection.current ?: return val playerVolume = playerConnection.service.playerVolume.collectAsState() val activityResultLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { } + val librarySong by database.song(mediaMetadata.id).collectAsState(initial = null) val coroutineScope = rememberCoroutineScope() val download by LocalDownloadUtil.current.getDownload(mediaMetadata.id).collectAsState(initial = null) @@ -325,6 +329,26 @@ fun PlayerMenu( ) } ) + if (librarySong?.song?.inLibrary != null) { + GridMenuItem( + icon = Icons.Rounded.LibraryAddCheck, + title = R.string.remove_from_library, + ) { + database.query { + inLibrary(mediaMetadata.id, null) + } + } + } else { + GridMenuItem( + icon = Icons.Rounded.LibraryAdd, + title = R.string.add_to_library, + ) { + database.transaction { + insert(mediaMetadata) + inLibrary(mediaMetadata.id, LocalDateTime.now()) + } + } + } GridMenuItem( icon = R.drawable.artist, title = R.string.view_artist diff --git a/app/src/main/java/com/dd3boh/outertune/ui/menu/SelectionSongsMenu.kt b/app/src/main/java/com/dd3boh/outertune/ui/menu/SelectionSongsMenu.kt index 946b2a0a6..ad1b47fec 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/menu/SelectionSongsMenu.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/menu/SelectionSongsMenu.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.PlaylistAdd +import androidx.compose.material.icons.rounded.LibraryAdd +import androidx.compose.material.icons.rounded.LibraryAddCheck import androidx.compose.material.icons.rounded.PlaylistRemove import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -43,6 +45,7 @@ import com.dd3boh.outertune.ui.component.DefaultDialog import com.dd3boh.outertune.ui.component.DownloadGridMenu import com.dd3boh.outertune.ui.component.GridMenu import com.dd3boh.outertune.ui.component.GridMenuItem +import java.time.LocalDateTime @Composable @@ -57,6 +60,13 @@ fun SelectionSongMenu( val downloadUtil = LocalDownloadUtil.current val playerConnection = LocalPlayerConnection.current ?: return + val allInLibrary by remember(songSelection) { // exclude local songs + mutableStateOf(songSelection.isNotEmpty() && songSelection.all { !it.song.isLocal && it.song.inLibrary != null }) + } + val allLocal by remember(songSelection) { // if only local songs in this selection + mutableStateOf(songSelection.isNotEmpty() && songSelection.all { it.song.isLocal }) + } + var downloadState by remember { mutableIntStateOf(Download.STATE_STOPPED) } @@ -219,6 +229,34 @@ fun SelectionSongMenu( } ) + if (!allLocal) { + if (allInLibrary) { + GridMenuItem( + icon = Icons.Rounded.LibraryAddCheck, + title = R.string.remove_all_from_library + ) { + database.transaction { + songSelection.forEach { song -> + inLibrary(song.id, null) + } + } + } + } else { + GridMenuItem( + icon = Icons.Rounded.LibraryAdd, + title = R.string.add_all_to_library + ) { + database.transaction { + songSelection.forEach { song -> + if (!song.song.isLocal) { + inLibrary(song.id, LocalDateTime.now()) + } + } + } + } + } + } + GridMenuItem( icon = if (songSelection.all{it.song.liked}) R.drawable.favorite else R.drawable.favorite_border, title = R.string.like_all diff --git a/app/src/main/java/com/dd3boh/outertune/ui/menu/SongMenu.kt b/app/src/main/java/com/dd3boh/outertune/ui/menu/SongMenu.kt index 98d4a4683..61122c4e5 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/menu/SongMenu.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/menu/SongMenu.kt @@ -20,6 +20,8 @@ import androidx.compose.material.icons.automirrored.rounded.QueueMusic import androidx.compose.material.icons.rounded.Album import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.LibraryAdd +import androidx.compose.material.icons.rounded.LibraryAddCheck import androidx.compose.material.icons.rounded.PlaylistRemove import androidx.compose.material.icons.rounded.Radio import androidx.compose.material.icons.rounded.Share @@ -343,6 +345,27 @@ fun SongMenu( } context.startActivity(Intent.createChooser(intent, null)) } + if (!song.song.isLocal) { + if (song.song.inLibrary == null) { + GridMenuItem( + icon = Icons.Rounded.LibraryAdd, + title = R.string.add_to_library + ) { + database.query { + update(song.song.toggleLibrary()) + } + } + } else { + GridMenuItem( + icon = Icons.Rounded.LibraryAddCheck, + title = R.string.remove_from_library + ) { + database.query { + update(song.song.toggleLibrary()) + } + } + } + } if (event != null) { GridMenuItem( icon = Icons.Rounded.Delete, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 209ffb764..ba3142888 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,6 +87,10 @@ Play Play next Add to queue + Add to library + Add all to library + Remove from library + Remove all from library Download Downloading Remove download From c15129358635bf2ec04c72125a11ee1de34da621 Mon Sep 17 00:00:00 2001 From: mikooomich Date: Sat, 7 Sep 2024 22:48:52 -0400 Subject: [PATCH 08/10] multiqueue: Play playlist content instead of endpoint * Before, clicking on a playlist song starts a radio-like queue, instead of loading the playlist content. Correct this design flaw. * fixes #37 --- .../outertune/ui/menu/YouTubePlaylistMenu.kt | 18 ++++++++-- .../ui/screens/artist/ArtistScreen.kt | 21 +++++------ .../screens/playlist/OnlinePlaylistScreen.kt | 35 +++++++++++++------ .../ui/screens/search/OnlineSearchResult.kt | 10 ------ 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/dd3boh/outertune/ui/menu/YouTubePlaylistMenu.kt b/app/src/main/java/com/dd3boh/outertune/ui/menu/YouTubePlaylistMenu.kt index 49e15fcda..e027eda1d 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/menu/YouTubePlaylistMenu.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/menu/YouTubePlaylistMenu.kt @@ -49,6 +49,7 @@ import com.dd3boh.outertune.extensions.toMediaItem import com.dd3boh.outertune.models.toMediaMetadata import com.dd3boh.outertune.playback.ExoDownloadService import com.dd3boh.outertune.playback.PlayerConnection.Companion.queueBoard +import com.dd3boh.outertune.playback.queues.ListQueue import com.dd3boh.outertune.playback.queues.YouTubeQueue import com.dd3boh.outertune.ui.component.DefaultDialog import com.dd3boh.outertune.ui.component.DownloadGridMenu @@ -261,7 +262,14 @@ fun YouTubePlaylistMenu( title = R.string.play ) { println("Play: ${it.playlistId}, ${it.params}") - playerConnection.playQueue(YouTubeQueue(it)) + playerConnection.playQueue( + ListQueue( + playlistId = playlist.playEndpoint!!.playlistId, + title = playlist.title, + items = songs.map { it.toMediaMetadata() }, + ) + ) + onDismiss() } } @@ -272,7 +280,13 @@ fun YouTubePlaylistMenu( title = R.string.shuffle ) { println("Shuffle: id: ${shuffleEndpoint.playlistId}, params: ${shuffleEndpoint.params}") - playerConnection.playQueue(YouTubeQueue(shuffleEndpoint)) + playerConnection.playQueue( + ListQueue( + playlistId = playlist.playEndpoint!!.playlistId, + title = playlist.title, + items = songs.map { it.toMediaMetadata() }.shuffled(), + ) + ) onDismiss() } } diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/artist/ArtistScreen.kt index 7c9e84495..ac5205332 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/artist/ArtistScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack @@ -236,10 +237,10 @@ fun ArtistScreen( ) } - items( + itemsIndexed( items = librarySongs, - key = { "local_${it.id}" } - ) { song -> + key = { _, item -> item.hashCode() } + ) { index, song -> SwipeToQueueBox( item = song.toMediaItem(), content = { @@ -270,19 +271,13 @@ fun ArtistScreen( .combinedClickable { if (song.id == mediaMetadata?.id) { playerConnection.player.togglePlayPause() - } else if (song.song.isLocal) { + } else { playerConnection.playQueue( ListQueue( title = "Library: ${artistPage.artist.title}", - items = librarySongs.filter { it.song.isLocal } .toList().shuffled().map { it.toMediaMetadata() } - ) - ) - } else { - playerConnection.playQueue( - YouTubeQueue( - WatchEndpoint( - videoId = song.id - ), song.toMediaMetadata() + items = librarySongs.filter { it.song.isLocal }.toList() + .shuffled().map { it.toMediaMetadata() }, + startIndex = index ) ) } diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/playlist/OnlinePlaylistScreen.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/playlist/OnlinePlaylistScreen.kt index d4371f5c3..4d604a9c4 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/playlist/OnlinePlaylistScreen.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/playlist/OnlinePlaylistScreen.kt @@ -16,7 +16,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText @@ -87,7 +87,7 @@ import com.dd3boh.outertune.extensions.toMediaItem import com.dd3boh.outertune.extensions.togglePlayPause import com.dd3boh.outertune.models.toMediaMetadata import com.dd3boh.outertune.playback.ExoDownloadService -import com.dd3boh.outertune.playback.queues.YouTubeQueue +import com.dd3boh.outertune.playback.queues.ListQueue import com.dd3boh.outertune.ui.component.AutoResizeText import com.dd3boh.outertune.ui.component.DefaultDialog import com.dd3boh.outertune.ui.component.FontSizeRange @@ -106,7 +106,6 @@ import com.dd3boh.outertune.ui.utils.ItemWrapper import com.dd3boh.outertune.ui.utils.backToMain import com.dd3boh.outertune.viewmodels.OnlinePlaylistViewModel import com.zionhuang.innertube.models.SongItem -import com.zionhuang.innertube.models.WatchEndpoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -442,7 +441,13 @@ fun OnlinePlaylistScreen( playlist.playEndpoint?.let { playEndpoint -> Button( onClick = { - playerConnection.playQueue(YouTubeQueue(playEndpoint)) + playerConnection.playQueue( + ListQueue( + playlistId = playlist.playEndpoint!!.playlistId, + title = playlist.title, + items = songs.map { it.toMediaMetadata() }, + ) + ) }, contentPadding = ButtonDefaults.ButtonWithIconContentPadding, modifier = Modifier.weight(1f) @@ -457,10 +462,16 @@ fun OnlinePlaylistScreen( } } - playlist.shuffleEndpoint?.let { shuffleEndpoint -> + playlist.shuffleEndpoint?.let { OutlinedButton( onClick = { - playerConnection.playQueue(YouTubeQueue(shuffleEndpoint)) + playerConnection.playQueue( + ListQueue( + playlistId = playlist.playEndpoint!!.playlistId, + title = playlist.title, + items = songs.map { it.toMediaMetadata() }.shuffled(), + ) + ) }, contentPadding = ButtonDefaults.ButtonWithIconContentPadding, modifier = Modifier.weight(1f) @@ -492,9 +503,9 @@ fun OnlinePlaylistScreen( } } - items( + itemsIndexed( items = wrappedSongs - ) { song -> + ) { index, song -> SwipeToQueueBox( item = song.item.toMediaItem(), content = { @@ -528,9 +539,11 @@ fun OnlinePlaylistScreen( playerConnection.player.togglePlayPause() } else { playerConnection.playQueue( - YouTubeQueue( - song.item.endpoint ?: WatchEndpoint(videoId = song.item.id), - song.item.toMediaMetadata() + ListQueue( + playlistId = playlist.id, + title = playlist.title, + items = songs.map { it.toMediaMetadata() }, + startIndex = index ) ) } diff --git a/app/src/main/java/com/dd3boh/outertune/ui/screens/search/OnlineSearchResult.kt b/app/src/main/java/com/dd3boh/outertune/ui/screens/search/OnlineSearchResult.kt index 79405a25f..0f5529145 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/screens/search/OnlineSearchResult.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/screens/search/OnlineSearchResult.kt @@ -44,7 +44,6 @@ import com.dd3boh.outertune.constants.SearchFilterHeight import com.dd3boh.outertune.extensions.toMediaItem import com.dd3boh.outertune.extensions.togglePlayPause import com.dd3boh.outertune.models.toMediaMetadata -import com.dd3boh.outertune.playback.queues.ListQueue import com.dd3boh.outertune.playback.queues.YouTubeQueue import com.dd3boh.outertune.ui.component.ChipsRow import com.dd3boh.outertune.ui.component.EmptyPlaceholder @@ -161,15 +160,6 @@ fun OnlineSearchResult( is SongItem -> { if (item.id == mediaMetadata?.id) { playerConnection.player.togglePlayPause() - } else if (item.id.startsWith("LA")) { - playerConnection.playQueue( - ListQueue( - title = "Search", - items = listOf(item.toMediaMetadata()) - ), - replace = true, - title = "Search", - ) } else { playerConnection.playQueue( YouTubeQueue( From 8e4382cb03337a0bdd0913773052093c30b1e96e Mon Sep 17 00:00:00 2001 From: mikooomich Date: Fri, 6 Sep 2024 00:20:31 -0400 Subject: [PATCH 09/10] gradle: Version bump to 0.6.1 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 66acc2c30..927349826 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,8 +19,8 @@ android { applicationId = "com.dd3boh.outertune" minSdk = 24 targetSdk = 34 - versionCode = 21 - versionName = "0.6.0" + versionCode = 22 + versionName = "0.6.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { From f5f09963ff88af41c1f25d8766730cf10014b697 Mon Sep 17 00:00:00 2001 From: mikooomich Date: Sat, 7 Sep 2024 23:19:35 -0400 Subject: [PATCH 10/10] ui: Remove like when removing from library * fixes #33 --- .../main/java/com/dd3boh/outertune/db/DatabaseDao.kt | 11 +++++++++++ .../com/dd3boh/outertune/db/entities/SongEntity.kt | 6 +++++- .../java/com/dd3boh/outertune/ui/menu/AlbumMenu.kt | 4 ++-- .../java/com/dd3boh/outertune/ui/menu/PlayerMenu.kt | 4 ++-- .../dd3boh/outertune/ui/menu/SelectionSongsMenu.kt | 4 ++-- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/dd3boh/outertune/db/DatabaseDao.kt b/app/src/main/java/com/dd3boh/outertune/db/DatabaseDao.kt index 0c252cc05..f2fc6960e 100644 --- a/app/src/main/java/com/dd3boh/outertune/db/DatabaseDao.kt +++ b/app/src/main/java/com/dd3boh/outertune/db/DatabaseDao.kt @@ -662,9 +662,20 @@ interface DatabaseDao { incrementPlayCount(songId, time.year, time.monthValue) } + @Transaction + fun toggleInLibrary(songId: String, inLibrary: LocalDateTime?) { + inLibrary(songId, inLibrary) + if (inLibrary == null) { + removeLike(songId) + } + } + @Query("UPDATE song SET inLibrary = :inLibrary WHERE id = :songId") fun inLibrary(songId: String, inLibrary: LocalDateTime?) + @Query("UPDATE song SET liked = 0, likedDate = null WHERE id = :songId") + fun removeLike(songId: String) + @Query("UPDATE song SET inLibrary = null WHERE localPath = null") fun disableInvalidLocalSongs() @Query("UPDATE song SET inLibrary = null, localPath = null WHERE id = :songId") diff --git a/app/src/main/java/com/dd3boh/outertune/db/entities/SongEntity.kt b/app/src/main/java/com/dd3boh/outertune/db/entities/SongEntity.kt index 244b9e778..27d9369b4 100644 --- a/app/src/main/java/com/dd3boh/outertune/db/entities/SongEntity.kt +++ b/app/src/main/java/com/dd3boh/outertune/db/entities/SongEntity.kt @@ -61,7 +61,11 @@ data class SongEntity( } } - fun toggleLibrary() = copy(inLibrary = if (inLibrary == null) LocalDateTime.now() else null) + fun toggleLibrary() = copy( + inLibrary = if (inLibrary == null) LocalDateTime.now() else null, + liked = if (inLibrary == null) liked else false, + likedDate = if (inLibrary == null) likedDate else null + ) /** * Returns a full date string. If no full date is present, returns the year. diff --git a/app/src/main/java/com/dd3boh/outertune/ui/menu/AlbumMenu.kt b/app/src/main/java/com/dd3boh/outertune/ui/menu/AlbumMenu.kt index 921f6899f..26b74b90e 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/menu/AlbumMenu.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/menu/AlbumMenu.kt @@ -286,7 +286,7 @@ fun AlbumMenu( ) { database.transaction { songs.forEach { - inLibrary(it.id, null) + toggleInLibrary(it.id, null) } } } @@ -297,7 +297,7 @@ fun AlbumMenu( ) { database.transaction { songs.forEach { - inLibrary(it.id, LocalDateTime.now()) + toggleInLibrary(it.id, LocalDateTime.now()) } } } diff --git a/app/src/main/java/com/dd3boh/outertune/ui/menu/PlayerMenu.kt b/app/src/main/java/com/dd3boh/outertune/ui/menu/PlayerMenu.kt index 8fbbbc46f..6e76f679f 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/menu/PlayerMenu.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/menu/PlayerMenu.kt @@ -335,7 +335,7 @@ fun PlayerMenu( title = R.string.remove_from_library, ) { database.query { - inLibrary(mediaMetadata.id, null) + toggleInLibrary(mediaMetadata.id, null) } } } else { @@ -345,7 +345,7 @@ fun PlayerMenu( ) { database.transaction { insert(mediaMetadata) - inLibrary(mediaMetadata.id, LocalDateTime.now()) + toggleInLibrary(mediaMetadata.id, LocalDateTime.now()) } } } diff --git a/app/src/main/java/com/dd3boh/outertune/ui/menu/SelectionSongsMenu.kt b/app/src/main/java/com/dd3boh/outertune/ui/menu/SelectionSongsMenu.kt index ad1b47fec..04a4ae26a 100644 --- a/app/src/main/java/com/dd3boh/outertune/ui/menu/SelectionSongsMenu.kt +++ b/app/src/main/java/com/dd3boh/outertune/ui/menu/SelectionSongsMenu.kt @@ -237,7 +237,7 @@ fun SelectionSongMenu( ) { database.transaction { songSelection.forEach { song -> - inLibrary(song.id, null) + toggleInLibrary(song.id, null) } } } @@ -249,7 +249,7 @@ fun SelectionSongMenu( database.transaction { songSelection.forEach { song -> if (!song.song.isLocal) { - inLibrary(song.id, LocalDateTime.now()) + toggleInLibrary(song.id, LocalDateTime.now()) } } }