fix(download): resolve missing album folders before publishing

This commit is contained in:
zarzet
2026-09-07 16:13:41 +07:00
parent fed88fc3a9
commit c48406f5d0
15 changed files with 383 additions and 13 deletions
@@ -186,6 +186,9 @@ object NativeDownloadFinalizer {
val itemObject = parseObject(itemJson)
val requestObject = parseObject(requestJson)
validateRequestContract(requestObject)
if (result.optBoolean("saf_deferred_publish", false) && result.has("saf_relative_dir")) {
requestObject.put("saf_relative_dir", result.getString("saf_relative_dir"))
}
val input = FinalizeInput(
itemId = itemId,
request = requestObject,
@@ -11,6 +11,24 @@ import kotlin.math.roundToInt
* finalizer's I/O-heavy orchestration.
*/
internal object NativeFinalizationPolicy {
fun resolvedAlbumRelativeDirectory(
relativeDirectory: String,
albumFolderTemplate: String,
resolvedAlbumFolder: String,
): String {
val albumFolder = resolvedAlbumFolder.trim()
if (!albumFolderTemplate.contains("{album}") || albumFolder.isEmpty()) {
return relativeDirectory
}
// The backend supplies one sanitized leaf, never a relative path.
if (albumFolder == "." || albumFolder == ".." ||
albumFolder.contains('/') || albumFolder.contains('\\')) {
return relativeDirectory
}
val parent = relativeDirectory.substringBeforeLast('/', "")
return if (parent.isEmpty()) albumFolder else "$parent/$albumFolder"
}
private val lyricsMetadataLinePattern = Regex(
"^\\[[a-z][a-z0-9_]*:.*]$",
RegexOption.IGNORE_CASE,
@@ -95,7 +95,7 @@ object SafDownloadHandler {
val stagedMimeType = if (useStagedOutput) STAGED_SAF_MIME_TYPE else mimeType
val existingDir = findDocumentDir(context, treeUri, relativeDir)
if (existingDir != null) {
if (existingDir != null && req.optString("album_folder_template", "").isBlank()) {
val existing = existingDir.findFile(fileName)
if (existing != null && existing.isFile && existing.length() > 0) {
deleteStaleStagedFiles(existingDir, fileName, outputExt)
@@ -109,11 +109,8 @@ object SafDownloadHandler {
}
}
val targetDir = ensureDocumentDir(context, treeUri, relativeDir)
?: return errorJson("Failed to access SAF directory")
if (deferSafPublish) {
deleteStaleStagedFiles(targetDir, fileName, outputExt)
existingDir?.let { deleteStaleStagedFiles(it, fileName, outputExt) }
val workingExt = outputExt.ifBlank { ".tmp" }
val workingFile = File.createTempFile("native_saf_work_", workingExt, context.cacheDir)
return try {
@@ -135,7 +132,12 @@ object SafDownloadHandler {
respObj.put("file_name", resolvedFileName)
respObj.put("saf_deferred_publish", true)
respObj.put("saf_final_file_name", resolvedFileName)
respObj.put("saf_relative_dir", relativeDir)
val resolvedDir = NativeFinalizationPolicy.resolvedAlbumRelativeDirectory(
relativeDirectory = relativeDir,
albumFolderTemplate = req.optString("album_folder_template", ""),
resolvedAlbumFolder = respObj.optString("resolved_album_folder", ""),
)
respObj.put("saf_relative_dir", resolvedDir)
respObj.put("saf_tree_uri", treeUriStr)
respObj.put("saf_output_ext", outputExt)
respObj.put("saf_final_mime_type", mimeType)
@@ -149,6 +151,9 @@ object SafDownloadHandler {
}
}
val targetDir = ensureDocumentDir(context, treeUri, relativeDir)
?: return errorJson("Failed to access SAF directory")
// Remove any stale partial from a previous killed attempt before
// creating the staged document: reusing it would let a shorter new
// write leave the old tail bytes in place (fd truncation is
@@ -7,6 +7,30 @@ import org.junit.Assert.assertTrue
import org.junit.Test
class NativeFinalizationPolicyTest {
@Test
fun lateAlbumMetadataResolvesOnlyThePendingFolderLeaf() {
assertEquals(
"Playlist/Artist/[2024] Album",
NativeFinalizationPolicy.resolvedAlbumRelativeDirectory(
"Playlist/Artist/[2024] Unknown", "[2024] {album}", "[2024] Album",
),
)
assertEquals(
"Album",
NativeFinalizationPolicy.resolvedAlbumRelativeDirectory("Unknown", "{album}", "Album"),
)
for (album in listOf("", "..", "../Album", "Album/Part", "Album\\Part")) {
assertEquals(
"Artist/Unknown",
NativeFinalizationPolicy.resolvedAlbumRelativeDirectory("Artist/Unknown", "{album}", album),
)
}
assertEquals(
"Unknown",
NativeFinalizationPolicy.resolvedAlbumRelativeDirectory("Unknown", "", "Album"),
)
}
@Test
fun matchesSharedLyricUsabilityCases() {
val stream = checkNotNull(