Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -762,48 +762,56 @@ class DownloadCorePublicationInstrumentedTest {

@Test
fun safPreparedFinalizationRecoveryScansEachRootOnlyOnce() = runBlocking {
withStorage(true) {
val root = ManagedDownloadStorage.resolveRootBlocking(context)
val journalRoot = ManagedDownloadStorage.terminalTemporaryWriteCleanupJournalRoot(root)
val preferences = context.getSharedPreferences(
"terminal_temporary_write_cleanup_v1",
Context.MODE_PRIVATE
)
assertTrue(preferences.edit().clear().commit())
try {
repeat(64) { index ->
assertTrue(
PersistentTerminalTemporaryWriteCleanupJournal.prepareFinalizationTargets(
context = context,
root = journalRoot,
pendingAudioName = "pending-$index.mp3",
finalAudioName = "final-$index.mp3",
expectedOperationId = "operation-$index",
targets = listOf(
TerminalTemporaryWriteCleanupTarget("final-$index.mp3.npmeta.json")
GlobalDownloadManager.startupRecoveryMutex.withLock {
GlobalDownloadManager.pendingDownloadRecoverySlot.withLock {
awaitConcurrentCatalogReadersIdle()
withStorage(saf = true, startupRecoveryLockHeld = true) {
withTerminalTemporaryWriteCleanupPaused {
val root = ManagedDownloadStorage.resolveRootBlocking(context)
val journalRoot = ManagedDownloadStorage.terminalTemporaryWriteCleanupJournalRoot(root)
val preferences = context.getSharedPreferences(
"terminal_temporary_write_cleanup_v1",
Context.MODE_PRIVATE
)
assertTrue(preferences.edit().clear().commit())
try {
repeat(64) { index ->
assertTrue(
PersistentTerminalTemporaryWriteCleanupJournal.prepareFinalizationTargets(
context = context,
root = journalRoot,
pendingAudioName = "pending-$index.mp3",
finalAudioName = "final-$index.mp3",
expectedOperationId = "operation-$index",
targets = listOf(
TerminalTemporaryWriteCleanupTarget("final-$index.mp3.npmeta.json")
)
) != null
)
}
val provider = (root as ManagedDownloadRootHandle.TreeRoot).tree.uri
awaitConcurrentCatalogReadersIdle()
ManagedDownloadStorage.treeChildRegistry.clear()
fun queryCount() = requireNotNull(
context.contentResolver.call(
provider,
ManagedDownloadMigrationTestDocumentProvider.QUERY_COUNT,
null,
null
)
).getInt("count")
val before = queryCount()
ManagedDownloadStorage.recoverPreparedTerminalTemporaryWriteFinalizations(context)
assertEquals(
"recovery cost must depend on roots instead of preparation count",
1,
queryCount() - before
)
) != null
)
} finally {
assertTrue(preferences.edit().clear().commit())
}
}
}
val provider = (root as ManagedDownloadRootHandle.TreeRoot).tree.uri
ManagedDownloadStorage.treeChildRegistry.clear()
fun queryCount() = requireNotNull(
context.contentResolver.call(
provider,
ManagedDownloadMigrationTestDocumentProvider.QUERY_COUNT,
null,
null
)
).getInt("count")
val before = queryCount()
ManagedDownloadStorage.recoverPreparedTerminalTemporaryWriteFinalizations(context)
assertEquals(
"recovery cost must depend on roots instead of preparation count",
1,
queryCount() - before
)
} finally {
assertTrue(preferences.edit().clear().commit())
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.graphics.Color
import moe.ouom.neriplayer.core.player.download.AudioDownloadManager
import moe.ouom.neriplayer.ui.viewmodel.buildLocalOriginalSongInfo
import moe.ouom.neriplayer.ui.screen.nowplaying.edit.resolveEditSongLyricsForSave
import moe.ouom.neriplayer.data.local.audioimport.LocalAudioImportManager
import android.media.MediaExtractor
import android.media.MediaFormat
import android.os.ParcelFileDescriptor
Expand Down Expand Up @@ -124,6 +125,52 @@ class DownloadedM4aStagingRegressionTest {
assertEquals(payload, audioPayload(file))
}

@Test fun repeatedEditsAndRescanKeepOriginalLyricsWithoutEmptyCompanions() = fixture(false) { file ->
val context = InstrumentationRegistry.getInstrumentation().targetContext
val payload = audioPayload(file)
val original = SongItem(
id = 7L, name = "原始歌名", artist = "原始歌手", album = "本地专辑", albumId = 0L,
durationMs = 1000L, coverUrl = null, mediaUri = file.toURI().toString(),
localFilePath = file.absolutePath, localFileName = file.name, channelId = "local",
originalName = "原始歌名", originalArtist = "原始歌手",
originalLyric = "[00:00.00]原始歌词", originalTranslatedLyric = "",
originalRomanizedLyric = ""
)
repeat(2) { index ->
val edited = original.copy(
customName = "错误匹配$index", matchedLyric = "[00:00.00]错误歌词$index",
matchedTranslatedLyric = "", matchedRomanizedLyric = ""
)
assertEquals(LocalMediaMetadataWriteOutcome.SUCCESS, runBlocking {
LocalMediaSupport.writeEditableMetadata(context, edited, writeCover = false, writeLyrics = true)
})
}
val rescanned = LocalAudioImportManager.hydrateLocalSongTextMetadata(
context, original.copy(originalLyric = null, originalTranslatedLyric = null,
originalRomanizedLyric = null), resolveCoverFallback = false
)
assertEquals("[00:00.00]错误歌词1", rescanned.matchedLyric)
assertEquals("[00:00.00]原始歌词", rescanned.originalLyric)
assertEquals("", rescanned.originalTranslatedLyric)
val restored = rescanned.copy(
name = requireNotNull(rescanned.originalName), artist = requireNotNull(rescanned.originalArtist),
customName = null, customArtist = null, matchedLyric = rescanned.originalLyric,
matchedTranslatedLyric = rescanned.originalTranslatedLyric,
matchedRomanizedLyric = rescanned.originalRomanizedLyric
)
assertEquals(LocalMediaMetadataWriteOutcome.SUCCESS, runBlocking {
LocalMediaSupport.writeEditableMetadata(context, restored, writeCover = false, writeLyrics = true)
})
val inspected = requireNotNull(LocalMediaSupport.inspect(context, restored))
assertEquals("原始歌名", inspected.title)
assertEquals("原始歌手", inspected.artist)
assertEquals("[00:00.00]原始歌词", inspected.lyricContent)
assertFalse(File(file.parentFile, "${file.nameWithoutExtension}_trans.lrc").exists())
assertFalse(File(file.parentFile, "${file.nameWithoutExtension}_roma.lrc").exists())
assertEquals(1, file.parentFile!!.walkTopDown().count { it.extension == "lrc" })
assertEquals(payload, audioPayload(file))
}

private fun verifyDownload(
pending: Boolean,
pipe: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package moe.ouom.neriplayer.data.local.media

import android.content.Context
import android.graphics.Bitmap
import android.os.ParcelFileDescriptor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.kyant.taglib.TagLib
import java.io.File
import java.util.UUID
import kotlinx.coroutines.runBlocking
import moe.ouom.neriplayer.data.local.media.curation.LocalCoverBaselineInspector
import moe.ouom.neriplayer.data.model.SongItem
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class LocalCoverBaselineInspectorInstrumentedTest {
@Test
fun readablePrivateAudioWithoutPicturesConfirmsAnEmptyBaseline() = fixture { context, audio ->
val before = audio.readBytes()
assertTrue(LocalCoverBaselineInspector.hasConfirmedNoOriginalCover(context, song(audio)))
assertArrayEquals(before, audio.readBytes())
assertEquals(listOf(audio.name), requireNotNull(audio.parentFile?.list()).toList())
}

@Test
fun embeddedPicturesPreventAnEmptyBaselineWithoutAnAdjacentCover() = fixture { context, audio ->
val cover = File(audio.parentFile, "replacement.png")
val bitmap = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888)
try {
cover.outputStream().use { assertTrue(bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)) }
} finally {
bitmap.recycle()
}
assertEquals(LocalMediaMetadataWriteOutcome.SUCCESS, runBlocking {
LocalMediaSupport.writeEditableMetadata(context, song(audio), cover.toURI().toString(),
writeCover = true, writeLyrics = false)
})
requireNotNull(audio.parentFile?.listFiles()).filter { it != audio }.forEach {
assertTrue(it.deleteRecursively())
}
LocalMediaSupport.invalidateSongAssetCaches(song(audio))
ParcelFileDescriptor.open(audio, ParcelFileDescriptor.MODE_READ_ONLY).use {
assertTrue(TagLib.getPictures(it.dup().detachFd()).isNotEmpty())
}

assertFalse(LocalCoverBaselineInspector.hasConfirmedNoOriginalCover(context, song(audio)))
}

@Test
fun aBrokenCoverCandidateOrUnreadableContainerRemainsUnknown() = fixture { context, audio ->
val brokenCover = File(audio.parentFile, "probe.jpg").apply { writeText("invalid image") }
assertFalse(LocalCoverBaselineInspector.hasConfirmedNoOriginalCover(context, song(audio)))
assertTrue(brokenCover.delete())
audio.writeText("invalid audio")
LocalMediaSupport.invalidateSongAssetCaches(song(audio))

assertFalse(LocalCoverBaselineInspector.hasConfirmedNoOriginalCover(context, song(audio)))
}

private fun fixture(block: (Context, File) -> Unit) {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val context = instrumentation.targetContext
val directory = File(context.cacheDir, "cover-baseline-${UUID.randomUUID()}")
check(directory.mkdir())
val audio = File(directory, "probe.m4a")
try {
instrumentation.context.assets.open("metadata/synthetic-free-first-8.m4a").use { input ->
audio.outputStream().use(input::copyTo)
}
block(context, audio)
} finally {
LocalMediaSupport.invalidateSongAssetCaches(song(audio))
check(directory.deleteRecursively())
}
}

private fun song(audio: File) = SongItem(
id = 545L, name = "Original title", artist = "Original artist", album = "Album",
albumId = 0L, durationMs = 1_000L, coverUrl = null,
mediaUri = audio.toURI().toString(), localFilePath = audio.absolutePath,
localFileName = audio.name, channelId = "local"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ class LocalMediaSupportSafLyricsTest {
}

@Test
fun cachedManagedLyricsCreationDoesNotRescanAudioDirectory() = runBlocking {
fun staleManagedLyricsCreationRefreshesParentOnceAndReusesWarmCache() = runBlocking {
val previousDirectoryUri = ManagedDownloadStorage.configuredDirectoryUri()
val treeUri = DocumentsContract.buildTreeDocumentUri(
Issue339LyricsTestDocumentProvider.AUTHORITY,
Expand Down Expand Up @@ -631,7 +631,7 @@ class LocalMediaSupportSafLyricsTest {
)

assertEquals(LocalMediaMetadataWriteOutcome.SIDECAR_ONLY, outcome)
assertEquals(before, musicChildQueryCount())
assertEquals(before + 1, musicChildQueryCount())
assertEquals(
"[00:01.00]new romanized lyric",
LocalMediaSupport.inspect(targetContext, audioUri).romanizedLyricContent
Expand Down Expand Up @@ -672,11 +672,20 @@ class LocalMediaSupportSafLyricsTest {
writeLyrics = true
)
)
assertEquals(beforeIndexed, musicChildQueryCount())
assertEquals(beforeIndexed + 1, musicChildQueryCount())
assertEquals(
"[00:01.00]new translated lyric",
LocalMediaSupport.inspect(targetContext, audioUri).translatedLyricContent
)
val beforeWarmWrite = musicChildQueryCount()
assertEquals(
LocalMediaMetadataWriteOutcome.SIDECAR_ONLY,
LocalMediaSupport.writeEditableMetadata(
context = targetContext, song = translatedSong,
writeCover = false, writeLyrics = true
)
)
assertEquals(beforeWarmWrite, musicChildQueryCount())
} finally {
ManagedDownloadStorage.primeSettings(previousDirectoryUri, null)
}
Expand Down
Loading
Loading