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)
-[](https://github.com/DD3Boh/OuterTune/releases/latest)
-
+[](https://github.com/DD3Boh/OuterTune/releases/latest)
+[](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/app/build.gradle.kts b/app/build.gradle.kts
index 8e16aeb3c..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 {
@@ -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 af618f9a9..7d229167b 100644
Binary files a/app/prebuilt/ffMetadataEx-release.aar and b/app/prebuilt/ffMetadataEx-release.aar differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4b85e956b..891c6d6d6 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -140,12 +140,12 @@
-
-
+
+
+
+
+
+ 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 23576e56b..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")
@@ -32,7 +33,6 @@ val SkipSilenceKey = booleanPreferencesKey("skipSilence")
val SkipOnErrorKey = booleanPreferencesKey("skipOnError")
val AudioNormalizationKey = booleanPreferencesKey("audioNormalization")
val YtmSyncKey = booleanPreferencesKey("ytmSync")
-val KeepAliveKey = booleanPreferencesKey("keepAlive")
val StopMusicOnTaskClearKey = booleanPreferencesKey("stopMusicOnTaskClear")
val MaxImageCacheSizeKey = intPreferencesKey("maxImageCacheSize")
@@ -126,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/db/DatabaseDao.kt b/app/src/main/java/com/dd3boh/outertune/db/DatabaseDao.kt
index e6c77c839..f2fc6960e 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
@@ -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/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/KeepAlive.kt b/app/src/main/java/com/dd3boh/outertune/playback/KeepAlive.kt
deleted file mode 100644
index c2cd6842c..000000000
--- a/app/src/main/java/com/dd3boh/outertune/playback/KeepAlive.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.dd3boh.outertune.playback
-
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.app.Service
-import android.content.Intent
-import android.os.Binder
-import android.os.Build
-import androidx.core.app.NotificationCompat
-import com.dd3boh.outertune.MainActivity
-import com.dd3boh.outertune.R
-
-/**
- * Dummy service that does nothing but post a notification
- */
-class KeepAlive : Service() {
- private lateinit var notif: Notification
- private val binder = DummyBinder()
-
- inner class DummyBinder : Binder() {
- val service: KeepAlive
- get() = this@KeepAlive
- }
-
- override fun onCreate() {
- super.onCreate()
- notif = getNotification()
- createNotificationChannel()
- }
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- startForeground(1, notif)
- return START_STICKY
- }
-
- override fun onBind(intent: Intent?) = binder
-
- private fun createNotificationChannel() {
- if (Build.VERSION.SDK_INT >= 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 52fba41f6..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,7 +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.KeepAliveKey
+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
@@ -189,22 +189,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())
@@ -278,10 +262,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
@@ -387,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))
+ }
}
}
}
@@ -828,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)
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)
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..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
@@ -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 {
+ toggleInLibrary(it.id, null)
+ }
+ }
+ }
+ } else {
+ GridMenuItem(
+ icon = Icons.Rounded.LibraryAdd,
+ title = R.string.add_all_to_library
+ ) {
+ database.transaction {
+ songs.forEach {
+ toggleInLibrary(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..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
@@ -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 {
+ toggleInLibrary(mediaMetadata.id, null)
+ }
+ }
+ } else {
+ GridMenuItem(
+ icon = Icons.Rounded.LibraryAdd,
+ title = R.string.add_to_library,
+ ) {
+ database.transaction {
+ insert(mediaMetadata)
+ toggleInLibrary(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..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
@@ -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 ->
+ toggleInLibrary(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) {
+ toggleInLibrary(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/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(
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(
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/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
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 @@
+
+
diff --git a/assets/badge_github.png b/assets/badge_github.png
new file mode 100644
index 000000000..4440f3767
Binary files /dev/null and b/assets/badge_github.png differ
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