mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-15 14:25:33 +02:00
fix(download): resolve missing album folders before publishing
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user