PlayerAndroid/app/src/main/java/code/name/monkey/retromusic/util/MusicUtil.kt

538 lines
19 KiB
Kotlin
Raw Normal View History

package code.name.monkey.retromusic.util
2021-07-25 05:10:13 +00:00
import android.app.Activity
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.Intent
2020-09-09 12:37:25 +00:00
import android.database.Cursor
import android.net.Uri
2021-07-25 05:10:13 +00:00
import android.os.Build
import android.os.Environment
import android.provider.BaseColumns
import android.provider.MediaStore
import android.text.TextUtils
2020-09-09 12:37:25 +00:00
import android.util.Log
import android.widget.Toast
2021-07-25 05:10:13 +00:00
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import androidx.fragment.app.FragmentActivity
import code.name.monkey.appthemehelper.util.VersionUtils
import code.name.monkey.retromusic.R
import code.name.monkey.retromusic.db.PlaylistEntity
import code.name.monkey.retromusic.db.SongEntity
import code.name.monkey.retromusic.db.toSongEntity
import code.name.monkey.retromusic.extensions.getLong
import code.name.monkey.retromusic.helper.MusicPlayerRemote.removeFromQueue
import code.name.monkey.retromusic.model.Artist
import code.name.monkey.retromusic.model.Playlist
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.model.lyrics.AbsSynchronizedLyrics
import code.name.monkey.retromusic.repository.RealPlaylistRepository
import code.name.monkey.retromusic.repository.Repository
import code.name.monkey.retromusic.repository.SongRepository
import code.name.monkey.retromusic.service.MusicService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jaudiotagger.audio.AudioFileIO
import org.jaudiotagger.tag.FieldKey
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import java.io.File
import java.io.IOException
import java.util.*
import java.util.regex.Pattern
object MusicUtil : KoinComponent {
fun createShareSongFileIntent(song: Song, context: Context): Intent? {
return try {
Intent().setAction(Intent.ACTION_SEND).putExtra(
Intent.EXTRA_STREAM,
FileProvider.getUriForFile(
context,
context.applicationContext.packageName,
File(song.data)
)
).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION).setType("audio/*")
} catch (e: IllegalArgumentException) {
// TODO the path is most likely not like /storage/emulated/0/... but something like /storage/28C7-75B0/...
e.printStackTrace()
Toast.makeText(
context,
"Could not share this file, I'm aware of the issue.",
Toast.LENGTH_SHORT
).show()
Intent()
}
}
fun buildInfoString(string1: String?, string2: String?): String {
if (string1.isNullOrEmpty()) {
return if (string2.isNullOrEmpty()) "" else string2
}
return if (string2.isNullOrEmpty()) if (string1.isNullOrEmpty()) "" else string1 else "$string1$string2"
}
fun createAlbumArtFile(context: Context): File {
return File(
createAlbumArtDir(context),
System.currentTimeMillis().toString()
)
}
private fun createAlbumArtDir(context: Context): File {
val albumArtDir = File(
if (VersionUtils.hasR()) context.cacheDir else Environment.getExternalStorageDirectory(),
"/albumthumbs/"
)
if (!albumArtDir.exists()) {
albumArtDir.mkdirs()
try {
File(albumArtDir, ".nomedia").createNewFile()
} catch (e: IOException) {
e.printStackTrace()
}
}
return albumArtDir
}
fun deleteAlbumArt(context: Context, albumId: Long) {
val contentResolver = context.contentResolver
val localUri = Uri.parse("content://media/external/audio/albumart")
contentResolver.delete(ContentUris.withAppendedId(localUri, albumId), null, null)
contentResolver.notifyChange(localUri, null)
}
fun getArtistInfoString(
context: Context,
artist: Artist
): String {
val albumCount = artist.albumCount
val songCount = artist.songCount
val albumString =
if (albumCount == 1) context.resources.getString(R.string.album)
else context.resources.getString(R.string.albums)
val songString =
if (songCount == 1) context.resources.getString(R.string.song)
else context.resources.getString(R.string.songs)
return "$albumCount $albumString$songCount $songString"
}
//iTunes uses for example 1002 for track 2 CD1 or 3011 for track 11 CD3.
//this method converts those values to normal tracknumbers
fun getFixedTrackNumber(trackNumberToFix: Int): Int {
return trackNumberToFix % 1000
}
fun getLyrics(song: Song): String? {
2020-09-06 17:56:39 +00:00
var lyrics: String? = "No lyrics found"
val file = File(song.data)
try {
lyrics = AudioFileIO.read(file).tagOrCreateDefault.getFirst(FieldKey.LYRICS)
} catch (e: Exception) {
e.printStackTrace()
}
if (lyrics == null || lyrics.trim { it <= ' ' }.isEmpty() || AbsSynchronizedLyrics
.isSynchronized(lyrics)
) {
val dir = file.absoluteFile.parentFile
if (dir != null && dir.exists() && dir.isDirectory) {
val format = ".*%s.*\\.(lrc|txt)"
val filename = Pattern.quote(
FileUtil.stripExtension(file.name)
)
val songtitle = Pattern.quote(song.title)
val patterns =
ArrayList<Pattern>()
patterns.add(
Pattern.compile(
String.format(format, filename),
Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE
)
)
patterns.add(
Pattern.compile(
String.format(format, songtitle),
Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE
)
)
val files =
dir.listFiles { f: File ->
for (pattern in patterns) {
if (pattern.matcher(f.name).matches()) {
return@listFiles true
}
}
false
}
2020-09-06 17:56:39 +00:00
if (files != null && files.isNotEmpty()) {
for (f in files) {
try {
val newLyrics =
FileUtil.read(f)
if (newLyrics != null && newLyrics.trim { it <= ' ' }.isNotEmpty()) {
if (AbsSynchronizedLyrics.isSynchronized(newLyrics)) {
return newLyrics
}
lyrics = newLyrics
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
return lyrics
}
2021-09-24 10:32:35 +00:00
@JvmStatic
fun getMediaStoreAlbumCoverUri(albumId: Long): Uri {
val sArtworkUri = Uri.parse("content://media/external/audio/albumart")
return ContentUris.withAppendedId(sArtworkUri, albumId)
}
fun getPlaylistInfoString(
context: Context,
songs: List<Song>
): String {
val duration = getTotalDuration(songs)
return buildInfoString(
getSongCountString(context, songs.size),
getReadableDurationString(duration)
)
}
fun playlistInfoString(
context: Context,
songs: List<SongEntity>
): String {
return getSongCountString(context, songs.size)
}
fun getReadableDurationString(songDurationMillis: Long): String {
var minutes = songDurationMillis / 1000 / 60
val seconds = songDurationMillis / 1000 % 60
return if (minutes < 60) {
String.format(
Locale.getDefault(),
"%02d:%02d",
minutes,
seconds
)
} else {
val hours = minutes / 60
minutes %= 60
String.format(
Locale.getDefault(),
"%02d:%02d:%02d",
hours,
minutes,
seconds
)
}
}
fun getSectionName(mediaTitle: String?): String {
var musicMediaTitle = mediaTitle
return try {
if (TextUtils.isEmpty(musicMediaTitle)) {
return ""
}
musicMediaTitle = musicMediaTitle!!.trim { it <= ' ' }.lowercase()
if (musicMediaTitle.startsWith("the ")) {
musicMediaTitle = musicMediaTitle.substring(4)
} else if (musicMediaTitle.startsWith("a ")) {
musicMediaTitle = musicMediaTitle.substring(2)
}
if (musicMediaTitle.isEmpty()) {
""
} else musicMediaTitle.substring(0, 1).uppercase()
} catch (e: Exception) {
""
}
}
fun getSongCountString(context: Context, songCount: Int): String {
val songString = if (songCount == 1) context.resources
.getString(R.string.song) else context.resources.getString(R.string.songs)
return "$songCount $songString"
}
fun getSongFileUri(songId: Long): Uri {
return ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
songId
)
}
fun getSongFilePath(context: Context, uri: Uri): String? {
val projection = arrayOf(MediaStore.MediaColumns.DATA)
return context.contentResolver.query(uri, projection, null, null, null)?.use {
if (it.moveToFirst()) {
it.getString(0)
} else {
""
}
}
}
fun getTotalDuration(songs: List<Song>): Long {
var duration: Long = 0
for (i in songs.indices) {
duration += songs[i].duration
}
return duration
}
fun getYearString(year: Int): String {
return if (year > 0) year.toString() else "-"
}
fun indexOfSongInList(songs: List<Song>, songId: Long): Int {
return songs.indexOfFirst { it.id == songId }
}
fun insertAlbumArt(
context: Context,
albumId: Long,
path: String?
) {
val contentResolver = context.contentResolver
val artworkUri = Uri.parse("content://media/external/audio/albumart")
contentResolver.delete(ContentUris.withAppendedId(artworkUri, albumId), null, null)
val values = ContentValues()
values.put("album_id", albumId)
values.put("_data", path)
contentResolver.insert(artworkUri, values)
contentResolver.notifyChange(artworkUri, null)
}
fun isArtistNameUnknown(artistName: String?): Boolean {
if (TextUtils.isEmpty(artistName)) {
return false
}
if (artistName == Artist.UNKNOWN_ARTIST_DISPLAY_NAME) {
return true
}
val tempName = artistName!!.trim { it <= ' ' }.lowercase()
return tempName == "unknown" || tempName == "<unknown>"
}
2020-09-21 04:31:53 +00:00
fun isVariousArtists(artistName: String?): Boolean {
if (TextUtils.isEmpty(artistName)) {
return false
}
2020-09-21 04:17:53 +00:00
if (artistName == Artist.VARIOUS_ARTISTS_DISPLAY_NAME) {
return true
}
return false
}
fun isFavorite(context: Context, song: Song): Boolean {
return PlaylistsUtil
.doPlaylistContains(context, getFavoritesPlaylist(context).id, song.id)
}
fun isFavoritePlaylist(
context: Context,
playlist: Playlist
): Boolean {
2020-08-21 18:01:52 +00:00
return playlist.name == context.getString(R.string.favorites)
}
val repository = get<Repository>()
fun toggleFavorite(context: Context, song: Song) {
GlobalScope.launch {
2021-11-27 08:36:49 +00:00
val playlist: PlaylistEntity = repository.favoritePlaylist()
if (playlist != null) {
val songEntity = song.toSongEntity(playlist.playListId)
val isFavorite = repository.isFavoriteSong(songEntity).isNotEmpty()
if (isFavorite) {
repository.removeSongFromPlaylist(songEntity)
} else {
repository.insertSongs(listOf(song.toSongEntity(playlist.playListId)))
}
}
context.sendBroadcast(Intent(MusicService.FAVORITE_STATE_CHANGED))
}
}
private fun getFavoritesPlaylist(context: Context): Playlist {
return RealPlaylistRepository(context.contentResolver).playlist(context.getString(R.string.favorites))
}
private fun getOrCreateFavoritesPlaylist(context: Context): Playlist {
return RealPlaylistRepository(context.contentResolver).playlist(
PlaylistsUtil.createPlaylist(
context,
context.getString(R.string.favorites)
)
)
}
fun deleteTracks(
activity: FragmentActivity,
songs: List<Song>,
safUris: List<Uri>?,
callback: Runnable?
) {
val songRepository: SongRepository = get()
val projection = arrayOf(
BaseColumns._ID, MediaStore.MediaColumns.DATA
)
// Split the query into multiple batches, and merge the resulting cursors
2020-08-21 18:01:52 +00:00
var batchStart: Int
var batchEnd = 0
val batchSize =
1000000 / 10 // 10^6 being the SQLite limite on the query lenth in bytes, 10 being the max number of digits in an int, used to store the track ID
val songCount = songs.size
while (batchEnd < songCount) {
batchStart = batchEnd
val selection = StringBuilder()
selection.append(BaseColumns._ID + " IN (")
var i = 0
while (i < batchSize - 1 && batchEnd < songCount - 1) {
selection.append(songs[batchEnd].id)
selection.append(",")
i++
batchEnd++
}
// The last element of a batch
// The last element of a batch
selection.append(songs[batchEnd].id)
batchEnd++
selection.append(")")
try {
val cursor = activity.contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
null, null
)
if (cursor != null) {
// Step 1: Remove selected tracks from the current playlist, as well
// as from the album art cache
cursor.moveToFirst()
while (!cursor.isAfterLast) {
val id = cursor.getLong(BaseColumns._ID)
val song: Song = songRepository.song(id)
removeFromQueue(song)
cursor.moveToNext()
}
// Step 2: Remove selected tracks from the database
activity.contentResolver.delete(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
selection.toString(), null
)
// Step 3: Remove files from card
cursor.moveToFirst()
var index = batchStart
while (!cursor.isAfterLast) {
val name = cursor.getString(1)
val safUri =
if (safUris == null || safUris.size <= index) null else safUris[index]
SAFUtil.delete(activity, name, safUri)
index++
cursor.moveToNext()
}
cursor.close()
}
} catch (ignored: SecurityException) {
}
activity.contentResolver.notifyChange(Uri.parse("content://media"), null)
activity.runOnUiThread {
Toast.makeText(
activity,
activity.getString(R.string.deleted_x_songs, songCount),
Toast.LENGTH_SHORT
)
.show()
callback?.run()
}
2020-09-09 12:37:25 +00:00
}
}
2020-09-09 12:37:25 +00:00
suspend fun deleteTracks(context: Context, songs: List<Song>) {
val projection = arrayOf(BaseColumns._ID, MediaStore.MediaColumns.DATA)
2020-09-09 12:37:25 +00:00
val selection = StringBuilder()
selection.append(BaseColumns._ID + " IN (")
for (i in songs.indices) {
selection.append(songs[i].id)
if (i < songs.size - 1) {
selection.append(",")
}
}
selection.append(")")
var deletedCount = 0
try {
val cursor: Cursor? = context.contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection.toString(),
null, null
)
if (cursor != null) {
removeFromQueue(songs)
2020-09-09 12:37:25 +00:00
// Step 2: Remove files from card
cursor.moveToFirst()
while (!cursor.isAfterLast) {
val id: Int = cursor.getInt(0)
val name: String = cursor.getString(1)
try { // File.delete can throw a security exception
val f = File(name)
if (f.delete()) {
// Step 3: Remove selected track from the database
context.contentResolver.delete(
ContentUris.withAppendedId(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
id.toLong()
), null, null
)
deletedCount++
} else {
// I'm not sure if we'd ever get here (deletion would
// have to fail, but no exception thrown)
Log.e("MusicUtils", "Failed to delete file $name")
}
cursor.moveToNext()
} catch (ex: SecurityException) {
cursor.moveToNext()
} catch (e: NullPointerException) {
Log.e("MusicUtils", "Failed to find file $name")
}
}
cursor.close()
}
withContext(Dispatchers.Main) {
Toast.makeText(
context,
context.getString(R.string.deleted_x_songs, deletedCount),
Toast.LENGTH_SHORT
).show()
}
2020-09-09 12:37:25 +00:00
} catch (ignored: SecurityException) {
}
}
@RequiresApi(Build.VERSION_CODES.R)
2021-11-23 05:47:26 +00:00
fun deleteTracksR(activity: Activity, songs: List<Song>) {
2021-07-25 05:10:13 +00:00
val pendingIntent = MediaStore.createDeleteRequest(activity.contentResolver, songs.map {
getSongFileUri(it.id)
})
2021-11-27 08:36:49 +00:00
activity.startIntentSenderForResult(pendingIntent.intentSender, 45, null, 0, 0, 0, null)
2021-07-25 05:10:13 +00:00
}
fun songByGenre(genreId: Long): Song {
return repository.getSongByGenre(genreId)
}
}