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
1 change: 1 addition & 0 deletions BookPlayer/Base.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
"gesture_swipe_vertically_title" = "Swipe vertically to create bookmark";
"details_title" = "Details";
"download_title" = "Download";
"download_incomplete_error" = "The download was incomplete. Please try again.";
"cancel_download_title" = "Cancel download";
"remove_downloaded_file_title" = "Remove from device";
"download_from_url_title" = "Download from URL";
Expand Down
12 changes: 12 additions & 0 deletions BookPlayer/Library/ItemList/Views/ItemArtworkView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ struct ItemArtworkView: View {
) { _ in
downloadState = .notDownloaded
}
/// A download/verification failure (e.g. a discarded truncated file) must reset
/// the cell — otherwise, now that completion is gated on verification, it would
/// stay stuck on the progress spinner until the view recomputes from disk. The
/// error payload only carries `relativePath`, so this matches the item directly
/// (a bound-book child error doesn't reset the parent cell — same limitation as
/// the Watch).
.onReceive(
syncService.downloadErrorPublisher
.filter { $0.0 == item.relativePath }
) { _ in
downloadState = .notDownloaded
}
}

@ViewBuilder
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "اسحب عموديًا لإنشاء إشارة مرجعية";
"details_title" = "تفاصيل";
"download_title" = "تحميل";
"download_incomplete_error" = "لم يكتمل التنزيل. يُرجى المحاولة مرة أخرى.";
"cancel_download_title" = "إلغاء التنزيل";
"remove_downloaded_file_title" = "إزالة من الجهاز";
"download_from_url_title" = "تنزيل من URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/ca.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Llisca verticalment per crear un marcador";
"details_title" = "Detalls";
"download_title" = "Descarrega";
"download_incomplete_error" = "La baixada no s'ha completat. Torneu-ho a provar.";
"cancel_download_title" = "Cancel·la la descàrrega";
"remove_downloaded_file_title" = "Suprimeix del dispositiu";
"download_from_url_title" = "Descarrega des de l'URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/cs.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Záložku vytvoříte přejetím prstem svisle";
"details_title" = "Podrobnosti";
"download_title" = "Stažení";
"download_incomplete_error" = "Stahování nebylo dokončeno. Zkuste to prosím znovu.";
"cancel_download_title" = "Zrušit stahování";
"remove_downloaded_file_title" = "Odebrat ze zařízení";
"download_from_url_title" = "Stáhnout z URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/da.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Stryg lodret for at oprette et bogmærke";
"details_title" = "detaljer";
"download_title" = "Hent";
"download_incomplete_error" = "Overførslen blev ikke fuldført. Prøv igen.";
"cancel_download_title" = "Annuller download";
"remove_downloaded_file_title" = "Fjern fra enheden";
"download_from_url_title" = "Download fra URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Streiche vertikal, um ein Lesezeichen zu erstellen";
"details_title" = "Einzelheiten";
"download_title" = "Herunterladen";
"download_incomplete_error" = "Der Download wurde nicht vollständig abgeschlossen. Bitte versuche es erneut.";
"cancel_download_title" = "Download abbrechen";
"remove_downloaded_file_title" = "Vom Gerät entfernen";
"download_from_url_title" = "Von URL herunterladen";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/el.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Σύρετε κάθετα για να δημιουργήσετε σελιδοδείκτη";
"details_title" = "Λεπτομέριες";
"download_title" = "Κατεβάστε";
"download_incomplete_error" = "Η λήψη δεν ολοκληρώθηκε. Δοκιμάστε ξανά.";
"cancel_download_title" = "Ακύρωση λήψης";
"remove_downloaded_file_title" = "Αφαιρέστε από τη συσκευή";
"download_from_url_title" = "Λήψη από τη διεύθυνση URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Swipe vertically to create bookmark";
"details_title" = "Details";
"download_title" = "Download";
"download_incomplete_error" = "The download was incomplete. Please try again.";
"cancel_download_title" = "Cancel download";
"remove_downloaded_file_title" = "Remove from device";
"download_from_url_title" = "Download from URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/es.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Desliza verticalmente para crear un marcador";
"details_title" = "Detalles";
"download_title" = "Descargar";
"download_incomplete_error" = "La descarga no se completó. Inténtalo de nuevo.";
"cancel_download_title" = "Cancelar descarga";
"remove_downloaded_file_title" = "Eliminar del dispositivo";
"download_from_url_title" = "Descargar desde URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/fi.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Luo kirjanmerkki pyyhkäisemällä pystysuunnassa";
"details_title" = "Yksityiskohdat";
"download_title" = "ladata";
"download_incomplete_error" = "Lataus jäi kesken. Yritä uudelleen.";
"cancel_download_title" = "Peruuta lataus";
"remove_downloaded_file_title" = "Poista laitteesta";
"download_from_url_title" = "Lataa osoitteesta URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/fr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Faites glisser verticalement pour créer un signet";
"details_title" = "Détails";
"download_title" = "Télécharger";
"download_incomplete_error" = "Le téléchargement n'a pas abouti. Veuillez réessayer.";
"cancel_download_title" = "Annuler le téléchargement";
"remove_downloaded_file_title" = "Supprimer de l’appareil";
"download_from_url_title" = "Télécharger à partir de l'URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/hu.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Függőleges csúsztatással hozzon létre könyvjelzőt";
"details_title" = "Részletek";
"download_title" = "Letöltés";
"download_incomplete_error" = "A letöltés nem fejeződött be. Próbáld újra.";
"cancel_download_title" = "Letöltés megszakítása";
"remove_downloaded_file_title" = "Távolítsa el az eszközről";
"download_from_url_title" = "Letöltés URL-ről";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/it.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Scorri verticalmente per creare un segnalibro";
"details_title" = "Dettagli";
"download_title" = "Scaricamento";
"download_incomplete_error" = "Il download non è stato completato. Riprova.";
"cancel_download_title" = "Annulla il download";
"remove_downloaded_file_title" = "Rimuovi dal dispositivo";
"download_from_url_title" = "Scarica dall'URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "縦にスワイプしてブックマークを作成";
"details_title" = "詳細";
"download_title" = "ダウンロード";
"download_incomplete_error" = "ダウンロードが完了しませんでした。もう一度お試しください。";
"cancel_download_title" = "ダウンロードをキャンセル";
"remove_downloaded_file_title" = "デバイスから削除";
"download_from_url_title" = "URLを指定してダウンロード";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/nb.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Sveip vertikalt for å opprette bokmerke";
"details_title" = "Detaljer";
"download_title" = "Last ned";
"download_incomplete_error" = "Nedlastingen ble ikke fullført. Prøv igjen.";
"cancel_download_title" = "Avbryt nedlasting";
"remove_downloaded_file_title" = "Fjern fra enhet";
"download_from_url_title" = "Last ned fra URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/nl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Veeg verticaal om een bladwijzer te maken";
"details_title" = "Details";
"download_title" = "Downloaden";
"download_incomplete_error" = "De download is niet voltooid. Probeer het opnieuw.";
"cancel_download_title" = "Download annuleren";
"remove_downloaded_file_title" = "Verwijderen van apparaat";
"download_from_url_title" = "Downloaden van URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/pl.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Przeciągnij w pionie, aby utworzyć zakładkę";
"details_title" = "Szczegóły";
"download_title" = "Pobierz";
"download_incomplete_error" = "Pobieranie nie zostało ukończone. Spróbuj ponownie.";
"cancel_download_title" = "Anuluj pobieranie";
"remove_downloaded_file_title" = "Usuń z urządzenia";
"download_from_url_title" = "Pobierz z adresu URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/pt-BR.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Deslize verticalmente para criar um marcador";
"details_title" = "Detalhes";
"download_title" = "Download";
"download_incomplete_error" = "O download não foi concluído. Tente novamente.";
"cancel_download_title" = "Cancelar download";
"remove_downloaded_file_title" = "Remover do dispositivo";
"download_from_url_title" = "Baixar do URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/pt-PT.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Deslize na vertical para criar um marcador";
"details_title" = "Detalhes";
"download_title" = "Download";
"download_incomplete_error" = "O download não foi concluído. Tente novamente.";
"cancel_download_title" = "Cancelar o download";
"remove_downloaded_file_title" = "Apagar do dispositivo";
"download_from_url_title" = "Descarregar do URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/ro.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Glisați vertical pentru a crea un marcaj";
"details_title" = "Detalii";
"download_title" = "Descarca";
"download_incomplete_error" = "Descărcarea nu s-a finalizat. Încercați din nou.";
"cancel_download_title" = "Anulați descărcarea";
"remove_downloaded_file_title" = "Scoateți de pe dispozitiv";
"download_from_url_title" = "Descărcați de la URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/ru.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Проведите по вертикали, чтобы создать закладку";
"details_title" = "Подробности";
"download_title" = "Скачать";
"download_incomplete_error" = "Загрузка не завершена. Повторите попытку.";
"cancel_download_title" = "Отменить загрузку";
"remove_downloaded_file_title" = "Удалить с устройства";
"download_from_url_title" = "Скачать с URL-адреса";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/sk-SK.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Potiahnutím zvisle vytvoríte záložku";
"details_title" = "Podrobnosti";
"download_title" = "Stiahnuť";
"download_incomplete_error" = "Sťahovanie sa nedokončilo. Skúste to znova.";
"cancel_download_title" = "Zrušiť sťahovanie";
"remove_downloaded_file_title" = "Odstrániť zo zariadenia";
"download_from_url_title" = "Sťahovanie z adresy URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/sv.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Svep vertikalt för att skapa ett bokmärke";
"details_title" = "Detaljer";
"download_title" = "Ladda ner";
"download_incomplete_error" = "Nedladdningen slutfördes inte. Försök igen.";
"cancel_download_title" = "Avbryt nedladdning";
"remove_downloaded_file_title" = "Ta bort från enheten";
"download_from_url_title" = "Ladda ner från URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/tr.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Yer imi oluşturmak için dikey olarak kaydırın";
"details_title" = "Detaylar";
"download_title" = "İndirmek";
"download_incomplete_error" = "İndirme tamamlanmadı. Lütfen tekrar deneyin.";
"cancel_download_title" = "İndirmeyi iptal et";
"remove_downloaded_file_title" = "Cihazdan kaldır";
"download_from_url_title" = "URL'den indir";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/uk.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "Проведіть вертикально, щоб створити закладку";
"details_title" = "Подробиці";
"download_title" = "Завантажити";
"download_incomplete_error" = "Завантаження не завершено. Спробуйте ще раз.";
"cancel_download_title" = "Скасувати завантаження";
"remove_downloaded_file_title" = "Видалити з пристрою";
"download_from_url_title" = "Завантажити з URL";
Expand Down
1 change: 1 addition & 0 deletions BookPlayer/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
"gesture_swipe_vertically_title" = "垂直滑动以创建书签";
"details_title" = "细节";
"download_title" = "下载";
"download_incomplete_error" = "下载未完成,请重试。";
"cancel_download_title" = "取消下载";
"remove_downloaded_file_title" = "从设备中删除";
"download_from_url_title" = "从网址下载";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,20 @@ class RemoteItemCellViewModel: ObservableObject {

self?.downloadState = .downloading(progress: progress)
}.store(in: &disposeBag)

/// A download/verification failure (e.g. a truncated file that was discarded)
/// must reset the cell — otherwise it would stay stuck showing the in-progress
/// or downloaded state for a file that no longer exists on disk. The error
/// publisher only carries the failing `relativePath`, so this matches single
/// items directly (bound-book chapter errors are surfaced but don't reset the
/// parent cell — handled when the error payload carries the initiating path).
coreServices.syncService.downloadErrorPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] (relativePath, _) in
guard relativePath == self?.item.relativePath else { return }

self?.downloadState = .notDownloaded
}.store(in: &disposeBag)
}

func startDownload() async throws {
Expand Down
39 changes: 38 additions & 1 deletion Shared/Network/BPTaskDownloadDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,25 @@

import Foundation

/// Errors raised when a download finishes but the resulting file can't be trusted.
/// The associated values carry diagnostic detail (logged at `.warning`); the
/// user-facing `errorDescription` is a single localized, non-technical message.
public enum DownloadError: LocalizedError {
/// The transfer finished without a network error, but fewer bytes were written
/// than the server's `Content-Length`, i.e. a truncated/botched file.
case incompleteDownload(received: Int64, expected: Int64)
/// The downloaded file's real audio duration is meaningfully shorter than the
/// duration we expected from the sync metadata.
case durationMismatch(expected: Double, actual: Double)
/// The duration couldn't be read from the downloaded file at all, despite a
/// known-good synced duration — a strong truncation/corruption signal.
case durationUnreadable

public var errorDescription: String? {
return "download_incomplete_error".localized
}
}

public class BPTaskDownloadDelegate: NSObject, URLSessionDownloadDelegate {
/// Callback triggered when the download task is finished
public var didFinishDownloadingTask: ((URLSessionTask, URL?, Error?) -> Void)?
Expand All @@ -25,7 +44,7 @@ public class BPTaskDownloadDelegate: NSObject, URLSessionDownloadDelegate {
didFinishDownloadingTask?(
downloadTask,
location,
parseErrorFromTask(downloadTask)
parseErrorFromTask(downloadTask) ?? verifyDownloadIsComplete(downloadTask)
)
}

Expand Down Expand Up @@ -56,6 +75,24 @@ public class BPTaskDownloadDelegate: NSObject, URLSessionDownloadDelegate {
}
}

/// Verifies the bytes written to disk match the `Content-Length` the server
/// reported. Background downloads can finish "successfully" with a truncated
/// body (interrupted transfer, early connection close) and `URLSession` won't
/// flag it, so we treat a short file as an error instead of accepting it.
private func verifyDownloadIsComplete(_ task: URLSessionTask) -> Error? {
let expectedBytes = task.countOfBytesExpectedToReceive
/// `NSURLSessionTransferSizeUnknown` (-1) means the server didn't send a
/// Content-Length; we can't validate the size, so don't block the download.
guard expectedBytes != NSURLSessionTransferSizeUnknown else { return nil }

let receivedBytes = task.countOfBytesReceived
guard receivedBytes >= expectedBytes else {
return DownloadError.incompleteDownload(received: receivedBytes, expected: expectedBytes)
}

return nil
}

private func parseErrorFromTask(_ task: URLSessionTask) -> Error? {
guard
let response = task.response as? HTTPURLResponse,
Expand Down
18 changes: 18 additions & 0 deletions Shared/Services/LibraryService+Sync.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public protocol LibrarySyncProtocol {
func updateLibraryLastBook(with relativePath: String?) async
/// Returns boolean determining if the item exists for the relativePath
func itemExists(for relativePath: String) async -> Bool
/// Fetch the stored (synced) duration for the item, read on a background context
/// so it's safe to call off the main thread (e.g. from a download delegate queue).
func getItemDuration(at relativePath: String) async -> Double?
/// Load encoded chapters from file into DB
func loadChaptersIfNeeded(relativePath: String) async

Expand Down Expand Up @@ -85,6 +88,21 @@ extension LibraryService: LibrarySyncProtocol {
}
}

public func getItemDuration(at relativePath: String) async -> Double? {
return await withCheckedContinuation { continuation in
let context = dataManager.getBackgroundContext()
context.perform { [unowned self, context] in
let duration = self.getItemProperty(
#keyPath(LibraryItem.duration),
relativePath: relativePath,
context: context
) as? Double

continuation.resume(returning: duration)
}
}
}

public func updateInfo(for itemsDict: [String: SyncableItem], parentFolder: String?) async {
return await withCheckedContinuation { continuation in
let context = dataManager.getBackgroundContext()
Expand Down
Loading
Loading