mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-06 20:08:48 +02:00
Comment cleanup
This commit is contained in:
@@ -98,7 +98,6 @@ class LocalLibraryItem(
|
||||
return true
|
||||
}
|
||||
|
||||
/** App-private files have paths; SAF files must be validated through their persisted URI. */
|
||||
@JsonIgnore
|
||||
private fun trackExists(ctx: Context, contentUrl: String?, path: String): Boolean {
|
||||
return LocalFile("", null, contentUrl ?: "", "", path, "", null, 0).exists(ctx)
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.audiobookshelf.app.models.DownloadItem
|
||||
import com.audiobookshelf.app.models.DownloadItemPart
|
||||
import java.io.File
|
||||
|
||||
/** Creates local-library records from the completed download manifest, not a recursive rescan. */
|
||||
/** Creates local-library records from a completed download manifest. */
|
||||
class FolderScanner(private val ctx: Context) {
|
||||
private val tag = "FolderScanner"
|
||||
|
||||
@@ -19,9 +19,13 @@ class FolderScanner(private val ctx: Context) {
|
||||
var localMediaProgress: LocalMediaProgress?
|
||||
)
|
||||
|
||||
private fun localLibraryItemId(mediaItemId: String) = "local_${DeviceManager.getBase64Id(mediaItemId)}"
|
||||
private fun localLibraryItemId(mediaItemId: String) =
|
||||
"local_${DeviceManager.getBase64Id(mediaItemId)}"
|
||||
|
||||
private fun createLocalFile(part: DownloadItemPart, externalFile: DocumentFile? = null): LocalFile? {
|
||||
private fun createLocalFile(
|
||||
part: DownloadItemPart,
|
||||
externalFile: DocumentFile? = null
|
||||
): LocalFile? {
|
||||
if (part.isInternalStorage) {
|
||||
val file = File(part.finalDestinationPath)
|
||||
if (!file.exists()) return null
|
||||
@@ -43,7 +47,8 @@ class FolderScanner(private val ctx: Context) {
|
||||
try {
|
||||
ctx.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor ->
|
||||
descriptor.statSize.coerceAtLeast(0L)
|
||||
} ?: 0L
|
||||
}
|
||||
?: 0L
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Could not open completed SAF file: $contentUrl", e)
|
||||
return null
|
||||
@@ -123,8 +128,7 @@ class FolderScanner(private val ctx: Context) {
|
||||
if (part.isInternalStorage) {
|
||||
null
|
||||
} else {
|
||||
part.completedDestinationUri
|
||||
?.let { DocumentFileCompat.fromUri(ctx, Uri.parse(it)) }
|
||||
part.completedDestinationUri?.let { DocumentFileCompat.fromUri(ctx, Uri.parse(it)) }
|
||||
?: resolveExternalFile(externalFolder, part)
|
||||
}
|
||||
Log.d(tag, "Resolve part ${part.filename}: externalFile=${externalFile?.uri}")
|
||||
@@ -203,7 +207,9 @@ class FolderScanner(private val ctx: Context) {
|
||||
|
||||
val result = DownloadItemScanResult(localItem, null)
|
||||
item.userMediaProgress?.let { progress ->
|
||||
val progressId = if (item.episodeId.isNullOrEmpty()) localItem.id else "${localItem.id}-$localEpisodeId"
|
||||
val progressId =
|
||||
if (item.episodeId.isNullOrEmpty()) localItem.id
|
||||
else "${localItem.id}-$localEpisodeId"
|
||||
result.localMediaProgress =
|
||||
LocalMediaProgress(
|
||||
progressId,
|
||||
@@ -247,14 +253,18 @@ class FolderScanner(private val ctx: Context) {
|
||||
*/
|
||||
private fun resolveExternalFile(folder: DocumentFile?, part: DownloadItemPart): DocumentFile? {
|
||||
if (folder == null) return null
|
||||
folder.findFile(part.filename)?.let { return it }
|
||||
folder.findFile(part.filename)?.let {
|
||||
return it
|
||||
}
|
||||
val expectedBaseName = part.filename.substringBeforeLast('.')
|
||||
return folder.listFiles().firstOrNull { document ->
|
||||
document.name == part.filename ||
|
||||
document.fullName == part.filename ||
|
||||
(part.audioTrack != null && document.isFile &&
|
||||
(part.audioTrack != null &&
|
||||
document.isFile &&
|
||||
(document.name ?: "").substringBeforeLast('.') == expectedBaseName) ||
|
||||
(part.audioTrack != null && document.isFile &&
|
||||
(part.audioTrack != null &&
|
||||
document.isFile &&
|
||||
document.fullName.substringBeforeLast('.') == expectedBaseName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +123,9 @@ class DbManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* The downloader now persists app-owned staging paths instead of DownloadManager state. Old
|
||||
* queue entries cannot be resumed safely, but completed local media lives in other books and is
|
||||
* deliberately left alone.
|
||||
* Removes DownloadManager queue entries that cannot be resumed by the app-managed downloader.
|
||||
*
|
||||
* This migration preserves completed local media, which is stored in separate books.
|
||||
*/
|
||||
fun clearLegacyDownloadQueueOnce() {
|
||||
val metadata = Paper.book("downloadQueueMetadata")
|
||||
|
||||
@@ -24,10 +24,7 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Call
|
||||
|
||||
/**
|
||||
* Process-owned Android download queue. Every network write goes through app-owned staging and is
|
||||
* admitted only after reserving enough space for the complete operation.
|
||||
*/
|
||||
/** Manages the process-owned queue for app-managed downloads. */
|
||||
class DownloadItemManager(
|
||||
private val folderScanner: FolderScanner,
|
||||
private val context: Context,
|
||||
@@ -36,7 +33,6 @@ class DownloadItemManager(
|
||||
private val tag = "DownloadItemManager"
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val activeCalls = ConcurrentHashMap<String, Call>()
|
||||
/** DocumentsProvider does not make concurrent createDirectory/findFile calls atomic. */
|
||||
private val safFolderLocks = ConcurrentHashMap<String, Any>()
|
||||
private val reservations = mutableMapOf<String, Long>()
|
||||
private val lastPersistTime = mutableMapOf<String, Long>()
|
||||
@@ -366,8 +362,6 @@ class DownloadItemManager(
|
||||
}
|
||||
|
||||
private fun tryReserve(part: DownloadItemPart): Boolean {
|
||||
// Covers from older servers often omit a size. Keep unknown-length work serial and use the
|
||||
// runtime low-space guard rather than leaving those queue items permanently deferred.
|
||||
if (part.fileSize <= 0L && currentDownloadItemParts.any { it.fileSize <= 0L }) return false
|
||||
val staging = File(part.destinationPath)
|
||||
staging.parentFile?.mkdirs()
|
||||
|
||||
+2
-4
@@ -14,7 +14,7 @@ import com.audiobookshelf.app.models.DownloadItem
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/** Removes only terminally failed download items after their retention window has elapsed. */
|
||||
/** Removes terminally failed downloads after their retention window elapses. */
|
||||
object IncompleteDownloadCleanup {
|
||||
private const val tag = "IncompleteDownloadCleanup"
|
||||
private const val RETENTION_MS = 24L * 60L * 60L * 1000L
|
||||
@@ -34,7 +34,7 @@ object IncompleteDownloadCleanup {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(WORK_PREFIX + itemId)
|
||||
}
|
||||
|
||||
/** Called on app/service startup as a catch-up for work delayed by Android or force-stop. */
|
||||
/** Removes failures retained longer than 24 hours when scheduled work did not run. */
|
||||
fun cleanupExpired(context: Context): Set<String> {
|
||||
val now = System.currentTimeMillis()
|
||||
return DeviceManager.dbManager.getDownloadItems()
|
||||
@@ -49,7 +49,6 @@ object IncompleteDownloadCleanup {
|
||||
private fun isEligible(item: DownloadItem, now: Long): Boolean {
|
||||
val failedAt = item.terminalFailureAt ?: return false
|
||||
if (now - failedAt < RETENTION_MS) return false
|
||||
// Do not remove an item while another part is still downloading, waiting, or finalizing.
|
||||
return item.downloadItemParts.all { part ->
|
||||
part.moved || (part.failed && !part.isMoving)
|
||||
}
|
||||
@@ -61,7 +60,6 @@ object IncompleteDownloadCleanup {
|
||||
if (part.isInternalStorage && part.moved) {
|
||||
deleteAppOwnedFile(context, File(part.finalDestinationPath))
|
||||
} else if (!part.isInternalStorage && part.moved) {
|
||||
// Never infer an arbitrary SAF path during cleanup. The stored document URI is authoritative.
|
||||
part.completedDestinationUri?.let { uriString ->
|
||||
try {
|
||||
DocumentFile.fromSingleUri(context, Uri.parse(uriString))?.delete()
|
||||
|
||||
+4
-2
@@ -20,8 +20,10 @@ class InternalDownloadManager(
|
||||
) {
|
||||
private val tag = "InternalDownloadManager"
|
||||
/**
|
||||
* Returns the active call so the queue can cancel a stalled transfer. A partial staging file is
|
||||
* retained only when the server proves that it honoured a subsequent range request.
|
||||
* Starts or resumes a download.
|
||||
*
|
||||
* @param url authenticated download URL
|
||||
* @return active call, used to cancel a stalled transfer
|
||||
*/
|
||||
fun download(url: String): Call {
|
||||
destinationFile.parentFile?.mkdirs()
|
||||
|
||||
@@ -15,7 +15,6 @@ data class DownloadItemPart(
|
||||
val downloadItemId: String,
|
||||
val filename: String,
|
||||
val fileSize: Long,
|
||||
/** App-owned staging location. This is intentionally a String so it survives process storage. */
|
||||
@JsonIgnore val destinationPath: String,
|
||||
val finalDestinationPath:String,
|
||||
val serverPath: String,
|
||||
@@ -32,15 +31,12 @@ data class DownloadItemPart(
|
||||
@JsonIgnore val uri: Uri,
|
||||
@JsonIgnore val destinationUri: Uri,
|
||||
@JsonIgnore val finalDestinationUri: Uri,
|
||||
/** Final SAF document returned by the provider after a successful move. */
|
||||
/** Persisted Android-only SAF URI used to reopen a completed document after process recovery. */
|
||||
@JsonIgnore var completedDestinationUri: String?,
|
||||
val finalDestinationSubfolder: String,
|
||||
var downloadId: Long?,
|
||||
@JsonIgnore var lastUpdateTime: Long?,
|
||||
var progress: Long,
|
||||
var bytesDownloaded: Long,
|
||||
/** Android queue state; hidden from the shared Capacitor download-part payload. */
|
||||
@JsonIgnore var retryCount: Int = 0,
|
||||
@JsonIgnore var waitingForSpace: Boolean = false
|
||||
) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.util.Log
|
||||
import com.audiobookshelf.app.MainActivity
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.device.FolderScanner
|
||||
import com.audiobookshelf.app.models.DownloadItem
|
||||
import com.audiobookshelf.app.models.DownloadItemPart
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
@@ -27,7 +26,6 @@ class AbsDownloader : Plugin() {
|
||||
|
||||
lateinit var mainActivity: MainActivity
|
||||
lateinit var apiHandler: ApiHandler
|
||||
lateinit var folderScanner: FolderScanner
|
||||
lateinit var downloadItemManager: DownloadItemManager
|
||||
|
||||
private val clientEventEmitter = (object : DownloadItemManager.DownloadEventEmitter {
|
||||
@@ -45,7 +43,6 @@ class AbsDownloader : Plugin() {
|
||||
|
||||
override fun load() {
|
||||
mainActivity = (activity as MainActivity)
|
||||
folderScanner = FolderScanner(mainActivity)
|
||||
apiHandler = ApiHandler(mainActivity)
|
||||
downloadItemManager = DownloadServiceHost.ensure(mainActivity)
|
||||
DownloadServiceHost.attachBridge(mainActivity, clientEventEmitter)
|
||||
@@ -56,10 +53,7 @@ class AbsDownloader : Plugin() {
|
||||
super.handleOnDestroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue restoration happens before the WebView mounts. Replay its parent items when Vue registers
|
||||
* the listener so subsequent part updates always have a matching store entry.
|
||||
*/
|
||||
/** Replays restored queue items when the frontend subscribes to download events. */
|
||||
@PluginMethod(returnType = PluginMethod.RETURN_NONE)
|
||||
override fun addListener(call: PluginCall) {
|
||||
super.addListener(call)
|
||||
@@ -151,8 +145,6 @@ class AbsDownloader : Plugin() {
|
||||
val isInternal = localFolder.id.startsWith("internal-")
|
||||
|
||||
val finalInternalFolderPath = "${mainActivity.filesDir}/downloads/${libraryItem.id}"
|
||||
// Keep internal staging on the same filesystem as its final file so finalization is a rename.
|
||||
// External-folder downloads can use app external storage and are moved through SAF afterwards.
|
||||
val tempFolderPath =
|
||||
if (isInternal) {
|
||||
"${mainActivity.filesDir}/download-staging/${libraryItem.id}"
|
||||
|
||||
@@ -14,8 +14,6 @@ import com.audiobookshelf.app.models.DownloadItemPart
|
||||
|
||||
/** Android-owned foreground lifecycle for transfers that must outlive the WebView and Activity. */
|
||||
class DownloadService : Service() {
|
||||
private var lastPart: DownloadItemPart? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createChannel()
|
||||
@@ -40,7 +38,6 @@ class DownloadService : Service() {
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
fun onPartUpdate(part: DownloadItemPart) {
|
||||
lastPart = part
|
||||
val text = if (part.waitingForSpace) "Waiting for available storage" else "Downloading ${part.filename}"
|
||||
val progress = part.progress.coerceIn(0L, 100L).toInt()
|
||||
val notification = notification(text, progress, part.fileSize > 0L)
|
||||
|
||||
@@ -28,10 +28,9 @@ object DownloadServiceHost {
|
||||
return manager!!
|
||||
}
|
||||
|
||||
/** Attaches the frontend after restored queue items have been emitted. */
|
||||
@Synchronized
|
||||
fun attachBridge(context: Context, emitter: DownloadItemManager.DownloadEventEmitter) {
|
||||
// Rehydrate the frontend's parent items before allowing part-progress events through.
|
||||
// Otherwise a running restored queue can emit a part before Vue knows its DownloadItem.
|
||||
bridgeReady = false
|
||||
bridgeEmitter = emitter
|
||||
val queue = ensure(context)
|
||||
|
||||
Reference in New Issue
Block a user