diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e08ae917..4792b87bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 4.0.1 + +#### What's Fixed +- Fixed music loading hanging on files without tags +- Fixed playlists being destroyed in poorly tagged libraries + ## 4.0.0 #### What's New diff --git a/README.md b/README.md index 94a1126cc..07052404a 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@

Auxio

A simple, rational music player for android.

- - Latest Version + + Latest Version Releases diff --git a/app/build.gradle b/app/build.gradle index a5c6bf437..fdc1e0eab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,8 +18,8 @@ android { defaultConfig { applicationId namespace - versionName "4.0.0" - versionCode 59 + versionName "4.0.1" + versionCode 60 minSdk min_sdk targetSdk target_sdk diff --git a/fastlane/metadata/android/en-US/changelogs/60.txt b/fastlane/metadata/android/en-US/changelogs/60.txt new file mode 100644 index 000000000..b24baaaae --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/60.txt @@ -0,0 +1,4 @@ +Auxio 4.0.0 completely overhauls the user experience, with a refreshed design based on the latest Material Design specs +and a brand new music loader with signifigant improvements to device and tag support. +This issue fixes critical issues with music loading. +For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v4.0.1. diff --git a/musikr/src/main/cpp/JInputStream.cpp b/musikr/src/main/cpp/JInputStream.cpp index 8c729a8db..b003b9eda 100644 --- a/musikr/src/main/cpp/JInputStream.cpp +++ b/musikr/src/main/cpp/JInputStream.cpp @@ -34,7 +34,7 @@ JInputStream::JInputStream(JNIEnv *env, jobject jInputStream) : env(env), jInput jmethodID jInputStreamNameMethod = jInputStreamClass.method("name", "()Ljava/lang/String;"); jInputStreamReadBlockMethod = jInputStreamClass.method("readBlock", - "(Ljava/nio/ByteBuffer;)Z"); + "(Ljava/nio/ByteBuffer;)I"); jInputStreamIsOpenMethod = jInputStreamClass.method("isOpen", "()Z"); jInputStreamSeekFromBeginningMethod = jInputStreamClass.method( "seekFromBeginning", "(J)Z"); @@ -58,22 +58,31 @@ TagLib::FileName /* const char * */JInputStream::name() const { return _name.toCString(true); } -TagLib::ByteVector JInputStream::readBlock(size_t length) { - // We have to invert the buffer allocation here siits not a perfect system (vykeen instead of korvax0 but i warped all over the hub and i dont think its possible to find a "perfect" purple system like you would withnce the JVM ByteBuffer allocation system - // uses a bugged caching mechanism that leaks memory if used in multithreaded contexts. - TagLib::ByteVector buf { static_cast(length), 0 }; +jint JInputStream::readBlockImpl(TagLib::ByteVector &buf) { jobject wrappedByteBuffer = env->NewDirectByteBuffer(buf.data(), buf.size()); if (wrappedByteBuffer == nullptr) { throw std::runtime_error("Failed to wrap ByteBuffer"); } - JObjectRef byteBuffer = { env, wrappedByteBuffer }; - jboolean result = env->CallBooleanMethod(jInputStream, - jInputStreamReadBlockMethod, *byteBuffer); - if (!result) { + JObjectRef byteBuffer { env, wrappedByteBuffer }; + jint read = env->CallIntMethod(jInputStream, jInputStreamReadBlockMethod, + *byteBuffer); + return read; +} + +TagLib::ByteVector JInputStream::readBlock(size_t length) { + // We have to invert the buffer allocation here + TagLib::ByteVector buf { static_cast(length), 0 }; + jint read = readBlockImpl(buf); + if (read >= 0) { + buf.resize(read); + return buf; + } else if (read == -1) { + buf.resize(0); + return buf; + } else { throw std::runtime_error("Failed to read block, see logs"); } - return buf; } void JInputStream::writeBlock(const TagLib::ByteVector &data) { diff --git a/musikr/src/main/cpp/JInputStream.h b/musikr/src/main/cpp/JInputStream.h index 026e6c3c3..5245dbe96 100644 --- a/musikr/src/main/cpp/JInputStream.h +++ b/musikr/src/main/cpp/JInputStream.h @@ -124,6 +124,7 @@ private: jmethodID jInputStreamSeekFromEndMethod; jmethodID jInputStreamTellMethod; jmethodID jInputStreamLengthMethod; + jint readBlockImpl(TagLib::ByteVector &buf); }; #endif //AUXIO_JINPUTSTREAM_H diff --git a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt index c4744c29e..d1700777c 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cache/CacheDatabase.kt @@ -41,7 +41,7 @@ import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.util.correctWhitespace import org.oxycblt.musikr.util.splitEscaped -@Database(entities = [CachedSong::class], version = 58, exportSchema = false) +@Database(entities = [CachedSong::class], version = 60, exportSchema = false) internal abstract class CacheDatabase : RoomDatabase() { abstract fun visibleDao(): VisibleCacheDao @@ -97,7 +97,7 @@ internal data class CachedSong( val bitrateHz: Int, val sampleRateHz: Int, val musicBrainzId: String?, - val name: String, + val name: String?, val sortName: String?, val track: Int?, val disc: Int?, diff --git a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt index 0d0e6104f..fc9420618 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/graph/MusicGraph.kt @@ -55,7 +55,7 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder { private val playlistVertices = mutableSetOf() override fun add(preSong: PreSong) { - val uid = preSong.uid + val uid = preSong.v363Uid if (songVertices.containsKey(uid)) { return } @@ -140,8 +140,10 @@ private class MusicGraphBuilderImpl : MusicGraph.Builder { vertex.genreVertices = vertex.genreVertices.distinct().toMutableList() playlistVertices.forEach { - val pointer = SongPointer.UID(entry.key) - it.pointerMap[pointer]?.forEach { index -> it.songVertices[index] = vertex } + val v363Pointer = SongPointer.UID(entry.key) + it.pointerMap[v363Pointer]?.forEach { index -> it.songVertices[index] = vertex } + val v400Pointer = SongPointer.UID(entry.value.preSong.v400Uid) + it.pointerMap[v400Pointer]?.forEach { index -> it.songVertices[index] = vertex } } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt index b8c22fb24..c7486e220 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/metadata/NativeInputStream.kt @@ -28,13 +28,12 @@ internal class NativeInputStream(private val deviceFile: DeviceFile, fis: FileIn fun name() = requireNotNull(deviceFile.path.name) - fun readBlock(buf: ByteBuffer): Boolean { + fun readBlock(buf: ByteBuffer): Int { try { - channel.read(buf) - return true + return channel.read(buf) } catch (e: Exception) { Log.d("NativeInputStream", "Error reading block", e) - return false + return -2 } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt index 99817b6eb..6031569b8 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/AlbumImpl.kt @@ -50,7 +50,7 @@ class AlbumImpl internal constructor(private val core: AlbumCore) : Album { // I don't know if there is any situation where an artist will have two albums with // the exact same name, but if there is, I would love to know. update(preAlbum.rawName) - update(preAlbum.preArtists.map { it.rawName }) + update(preAlbum.preArtists.mapNotNull { it.rawName }) } override val name = preAlbum.name override val releaseType = preAlbum.releaseType diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt index badcada45..e60d4f662 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/LibraryImpl.kt @@ -37,6 +37,7 @@ internal data class LibraryImpl( private val playlistInterpreter: PlaylistInterpreter ) : MutableLibrary { private val songUidMap = songs.associateBy { it.uid } + private val v400SongUidMap = songs.associateBy { it.v400Uid } private val albumUidMap = albums.associateBy { it.uid } private val artistUidMap = artists.associateBy { it.uid } private val genreUidMap = genres.associateBy { it.uid } @@ -44,7 +45,8 @@ internal data class LibraryImpl( override fun empty() = songs.isEmpty() - override fun findSong(uid: Music.UID) = songUidMap[uid] + // Compat hack. See TagInterpreter for why this needs to be done + override fun findSong(uid: Music.UID) = songUidMap[uid] ?: v400SongUidMap[uid] override fun findSongByPath(path: Path) = songs.find { it.path == path } diff --git a/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt b/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt index 6a34168c6..383abf11c 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/model/SongImpl.kt @@ -42,7 +42,10 @@ internal interface SongCore { internal class SongImpl(private val handle: SongCore) : Song { private val preSong = handle.preSong - override val uid = preSong.uid + override val uid = preSong.v363Uid + + val v400Uid = preSong.v400Uid + override val name = preSong.name override val track = preSong.track override val disc = preSong.disc diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt index 858421457..1d204fe5a 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExtractStep.kt @@ -131,7 +131,7 @@ private class ExtractStepImpl( metadata .map { fileWith -> if (fileWith.with != null) { - val tags = tagParser.parse(fileWith.file, fileWith.with) + val tags = tagParser.parse(fileWith.with) val cover = fileWith.with.cover?.let { storedCovers.write(it) } RawSong(fileWith.file, fileWith.with.properties, tags, cover, addingMs) } else { diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt index 292d8d221..1f6efc892 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/PipelineException.kt @@ -18,7 +18,6 @@ package org.oxycblt.musikr.pipeline -import android.util.Log import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.playlist.PlaylistFile import org.oxycblt.musikr.playlist.interpret.PrePlaylist @@ -55,45 +54,35 @@ sealed interface WhileProcessing { internal suspend fun wrap(file: DeviceFile, block: suspend (DeviceFile) -> R): R = try { - Log.d("wrap", "Processing DeviceFile ${file.path}") block(file) } catch (e: Exception) { - Log.e("wrap", "Error while processing DeviceFile ${file.path}", e) throw PipelineException(WhileProcessing.AFile(file), e) } internal suspend fun wrap(song: RawSong, block: suspend (RawSong) -> R): R = try { - Log.d("wrap", "Processing RawSong ${song.file.path}") block(song) } catch (e: Exception) { - Log.e("wrap", "Error while processing RawSong ${song.file.path}", e) throw PipelineException(WhileProcessing.ARawSong(song), e) } internal suspend fun wrap(file: PlaylistFile, block: suspend (PlaylistFile) -> R): R = try { - Log.d("wrap", "Processing PlaylistFile ${file.name}") block(file) } catch (e: Exception) { - Log.e("wrap", "Error while processing PlaylistFile ${file.name}", e) throw PipelineException(WhileProcessing.APlaylistFile(file), e) } internal suspend fun wrap(song: PreSong, block: suspend (PreSong) -> R): R = try { - Log.d("wrap", "Processing PreSong ${song.path}") block(song) } catch (e: Exception) { - Log.e("wrap", "Error while processing PreSong ${song.path}", e) throw PipelineException(WhileProcessing.APreSong(song), e) } internal suspend fun wrap(playlist: PrePlaylist, block: suspend (PrePlaylist) -> R): R = try { - Log.d("wrap", "Processing PrePlaylist ${playlist.name}") block(playlist) } catch (e: Exception) { - Log.e("wrap", "Error while processing PrePlaylist ${playlist.name}", e) throw PipelineException(WhileProcessing.APrePlaylist(playlist), e) } diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt index 4d8831acf..d5722b0a6 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt @@ -29,9 +29,10 @@ import org.oxycblt.musikr.tag.Disc import org.oxycblt.musikr.tag.Name import org.oxycblt.musikr.tag.ReleaseType import org.oxycblt.musikr.tag.ReplayGainAdjustment -import org.oxycblt.musikr.util.update internal data class PreSong( + val v363Uid: Music.UID, + val v400Uid: Music.UID, val musicBrainzId: UUID?, val name: Name.Known, val rawName: String, @@ -52,24 +53,7 @@ internal data class PreSong( val preAlbum: PreAlbum, val preArtists: List, val preGenres: List -) { - val uid = - musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) } - ?: Music.UID.auxio(Music.UID.Item.SONG) { - // Song UIDs are based on the raw data without parsing so that they remain - // consistent across music setting changes. Parents are not held up to the - // same standard since grouping is already inherently linked to settings. - update(rawName) - update(preAlbum.rawName) - update(date) - - update(track) - update(disc?.number) - - update(preArtists.map { artist -> artist.rawName }) - update(preAlbum.preArtists.map { artist -> artist.rawName }) - } -} +) {} internal data class PreAlbum( val musicBrainzId: UUID?, diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt index 1deb269aa..de6f07a49 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt @@ -19,6 +19,8 @@ package org.oxycblt.musikr.tag.interpret import org.oxycblt.musikr.Interpretation +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.fs.Format import org.oxycblt.musikr.pipeline.RawSong import org.oxycblt.musikr.tag.Disc @@ -29,6 +31,7 @@ import org.oxycblt.musikr.tag.ReplayGainAdjustment import org.oxycblt.musikr.tag.format.parseId3GenreNames import org.oxycblt.musikr.tag.parse.ParsedTags import org.oxycblt.musikr.util.toUuidOrNull +import org.oxycblt.musikr.util.update internal interface TagInterpreter { fun interpret(song: RawSong): PreSong @@ -53,22 +56,65 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T song.tags.albumArtistSortNames, interpretation) val preAlbum = - makePreAlbum(song.tags, individualPreArtists, albumPreArtists, interpretation) + makePreAlbum( + song.tags, song.file, individualPreArtists, albumPreArtists, interpretation) val rawArtists = individualPreArtists.ifEmpty { albumPreArtists }.ifEmpty { listOf(unknownPreArtist()) } val rawGenres = makePreGenres(song.tags, interpretation).ifEmpty { listOf(unknownPreGenre()) } val uri = song.file.uri + + val songNameOrFile = song.tags.name ?: requireNotNull(song.file.path.name) + val songNameOrFileWithoutExt = + song.tags.name ?: requireNotNull(song.file.path.name).split('.').first() + val albumNameOrDir = song.tags.albumName ?: song.file.path.directory.name + + val musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull() + val v363uid = + musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) } + ?: Music.UID.auxio(Music.UID.Item.SONG) { + update(songNameOrFileWithoutExt) + update(albumNameOrDir) + update(song.tags.date) + + update(song.tags.track) + update(song.tags.disc) + + update(song.tags.artistNames) + update(song.tags.albumArtistNames) + } + + // I was an idiot and accidentally changed the UID spec in v4.0.0, so we need to calculate + // the broken UID too and maintain compat for that version. + val v400uid = + musicBrainzId?.let { Music.UID.musicBrainz(Music.UID.Item.SONG, it) } + ?: Music.UID.auxio(Music.UID.Item.SONG) { + update(songNameOrFile) + update(song.tags.albumName) + update(song.tags.date) + + update(song.tags.track) + update(song.tags.disc) + + val artistNames = interpretation.separators.split(song.tags.artistNames) + update(artistNames.ifEmpty { listOf(null) }) + val albumArtistNames = + interpretation.separators.split(song.tags.albumArtistNames) + update(albumArtistNames.ifEmpty { artistNames }.ifEmpty { listOf(null) }) + } + return PreSong( + v363Uid = v363uid, + v400Uid = v400uid, uri = uri, path = song.file.path, size = song.file.size, format = Format.infer(song.file.mimeType, song.properties.mimeType), modifiedMs = song.file.modifiedMs, addedMs = song.addedMs, - musicBrainzId = song.tags.musicBrainzId?.toUuidOrNull(), - name = interpretation.naming.name(song.tags.name, song.tags.sortName), - rawName = song.tags.name, + musicBrainzId = musicBrainzId, + name = interpretation.naming.name(songNameOrFileWithoutExt, song.tags.sortName), + rawName = songNameOrFileWithoutExt, track = song.tags.track, disc = song.tags.disc?.let { Disc(it, song.tags.subtitle) }, date = song.tags.date, @@ -88,16 +134,16 @@ private class TagInterpreterImpl(private val interpretation: Interpretation) : T private fun makePreAlbum( parsedTags: ParsedTags, + deviceFile: DeviceFile, individualPreArtists: List, albumPreArtists: List, interpretation: Interpretation ): PreAlbum { + val name = parsedTags.albumName ?: deviceFile.path.directory.name return PreAlbum( musicBrainzId = parsedTags.albumMusicBrainzId?.toUuidOrNull(), - name = - interpretation.naming.name( - parsedTags.albumName, parsedTags.albumSortName, Placeholder.ALBUM), - rawName = parsedTags.albumName, + name = interpretation.naming.name(name, parsedTags.albumSortName, Placeholder.ALBUM), + rawName = name, releaseType = ReleaseType.parse(interpretation.separators.split(parsedTags.releaseTypes)) ?: ReleaseType.Album(null), diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt index a7dc4d3c5..1d7198a56 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/ParsedTags.kt @@ -25,7 +25,7 @@ internal data class ParsedTags( val replayGainTrackAdjustment: Float? = null, val replayGainAlbumAdjustment: Float? = null, val musicBrainzId: String? = null, - val name: String, + val name: String? = null, val sortName: String? = null, val track: Int? = null, val disc: Int? = null, diff --git a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt index 42d76af43..a4dbda470 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/tag/parse/TagParser.kt @@ -18,12 +18,10 @@ package org.oxycblt.musikr.tag.parse -import org.oxycblt.musikr.fs.DeviceFile import org.oxycblt.musikr.metadata.Metadata -import org.oxycblt.musikr.util.unlikelyToBeNull internal interface TagParser { - fun parse(file: DeviceFile, metadata: Metadata): ParsedTags + fun parse(metadata: Metadata): ParsedTags companion object { fun new(): TagParser = TagParserImpl @@ -31,14 +29,14 @@ internal interface TagParser { } private data object TagParserImpl : TagParser { - override fun parse(file: DeviceFile, metadata: Metadata): ParsedTags { + override fun parse(metadata: Metadata): ParsedTags { val compilation = metadata.isCompilation() return ParsedTags( durationMs = metadata.properties.durationMs, replayGainTrackAdjustment = metadata.replayGainTrackAdjustment(), replayGainAlbumAdjustment = metadata.replayGainAlbumAdjustment(), musicBrainzId = metadata.musicBrainzId(), - name = metadata.name() ?: unlikelyToBeNull(file.path.name), + name = metadata.name(), sortName = metadata.sortName(), track = metadata.track(), disc = metadata.disc(),