@@ -504,6 +504,30 @@ extension DownloadDatabaseOperations on AppDatabase {
|
||||
return rows.map((row) => row.readTable(downloadQueue)).toList(growable: false);
|
||||
}
|
||||
|
||||
/// Fails every queued or active download after the target filesystem fills.
|
||||
///
|
||||
/// Clearing native task ids and queue rows prevents startup recovery from
|
||||
/// immediately re-enqueueing work after partial files have been discarded.
|
||||
Future<int> failActiveDownloadsForStorageFull(String errorMessage) {
|
||||
return transaction(() async {
|
||||
final active = await (select(
|
||||
downloadedMedia,
|
||||
)..where((row) => row.status.isIn([DownloadStatus.queued.index, DownloadStatus.downloading.index]))).get();
|
||||
if (active.isEmpty) return 0;
|
||||
|
||||
final globalKeys = active.map((item) => item.globalKey).toList(growable: false);
|
||||
await (update(downloadedMedia)..where((row) => row.globalKey.isIn(globalKeys))).write(
|
||||
DownloadedMediaCompanion(
|
||||
status: Value(DownloadStatus.failed.index),
|
||||
bgTaskId: const Value(null),
|
||||
errorMessage: Value(errorMessage),
|
||||
),
|
||||
);
|
||||
await (delete(downloadQueue)..where((row) => row.mediaGlobalKey.isIn(globalKeys))).go();
|
||||
return globalKeys.length;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateDownloadStatus(String globalKey, int status) async {
|
||||
await (update(
|
||||
downloadedMedia,
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Изтеглянето е добавено в опашката",
|
||||
"downloadResumed": "Изтеглянето е възобновено",
|
||||
"serverErrorBitrate": "Грешка на сървъра: файлът може да надвишава лимита за отдалечен битрейт",
|
||||
"storageFull": "Изтеглянията бяха спрени, защото паметта на устройството е пълна. Освободете място и опитайте отново.",
|
||||
"episodesQueued": "${count} епизода са добавени в опашката за изтегляне",
|
||||
"downloadDeleted": "Изтеглянето е изтрито",
|
||||
"deleteConfirm": "Да се изтрие ли \"${title}\" от това устройство?",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Download i kø",
|
||||
"downloadResumed": "Download genoptaget",
|
||||
"serverErrorBitrate": "Serverfejl: filen overskrider muligvis grænsen for ekstern bitrate",
|
||||
"storageFull": "Downloads blev stoppet, fordi enhedens lagerplads er fuld. Frigør plads, og prøv igen.",
|
||||
"episodesQueued": "${count} episoder i downloadkø",
|
||||
"downloadDeleted": "Download slettet",
|
||||
"deleteConfirm": "Slet \"${title}\" fra denne enhed?",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Download in Warteschlange",
|
||||
"downloadResumed": "Download fortgesetzt",
|
||||
"serverErrorBitrate": "Serverfehler: Datei überschreitet möglicherweise das Remote-Bitrate-Limit",
|
||||
"storageFull": "Die Downloads wurden angehalten, weil der Gerätespeicher voll ist. Gib Speicherplatz frei und versuche es erneut.",
|
||||
"episodesQueued": "${count} Episoden zum Download hinzugefügt",
|
||||
"downloadDeleted": "Download gelöscht",
|
||||
"deleteConfirm": "\"${title}\" von diesem Gerät löschen?",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Download queued",
|
||||
"downloadResumed": "Download resumed",
|
||||
"serverErrorBitrate": "Server error: file may exceed the remote bitrate limit",
|
||||
"storageFull": "Downloads stopped because device storage is full. Free some space, then retry.",
|
||||
"episodesQueued": "${count} episodes queued for download",
|
||||
"downloadDeleted": "Download deleted",
|
||||
"deleteConfirm": "Delete \"${title}\" from this device?",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Descarga en cola",
|
||||
"downloadResumed": "Descarga reanudada",
|
||||
"serverErrorBitrate": "Error del servidor: el archivo puede superar el límite remoto de tasa de bits",
|
||||
"storageFull": "Las descargas se detuvieron porque el almacenamiento del dispositivo está lleno. Libera espacio e inténtalo de nuevo.",
|
||||
"episodesQueued": "${count} episodios en cola para descargar",
|
||||
"downloadDeleted": "Descarga eliminada",
|
||||
"deleteConfirm": "¿Eliminar \"${title}\" de este dispositivo?",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Téléchargement en attente",
|
||||
"downloadResumed": "Téléchargement repris",
|
||||
"serverErrorBitrate": "Erreur du serveur : le fichier peut dépasser la limite de débit distant",
|
||||
"storageFull": "Les téléchargements ont été arrêtés car le stockage de l’appareil est plein. Libérez de l’espace, puis réessayez.",
|
||||
"episodesQueued": "${count} épisodes en attente de téléchargement",
|
||||
"downloadDeleted": "Téléchargement supprimé",
|
||||
"deleteConfirm": "Supprimer « ${title} » de cet appareil ?",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Letöltés sorba állítva",
|
||||
"downloadResumed": "Letöltés folytatva",
|
||||
"serverErrorBitrate": "Szerverhiba: a fájl meghaladhatja a távoli bitrátakorlátot",
|
||||
"storageFull": "A letöltések leálltak, mert az eszköz tárhelye megtelt. Szabadíts fel helyet, majd próbáld újra.",
|
||||
"episodesQueued": "${count} epizód letöltésre sorba állítva",
|
||||
"downloadDeleted": "Letöltés törölve",
|
||||
"deleteConfirm": "Törlöd a következőt: \"${title}\" erről az eszközről?",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Download in coda",
|
||||
"downloadResumed": "Download ripreso",
|
||||
"serverErrorBitrate": "Errore server: il file può superare il limite di bitrate remoto",
|
||||
"storageFull": "I download sono stati interrotti perché lo spazio di archiviazione del dispositivo è esaurito. Libera spazio e riprova.",
|
||||
"episodesQueued": "${count} episodi in coda per il download",
|
||||
"downloadDeleted": "Download eliminato",
|
||||
"deleteConfirm": "Eliminare \"${title}\" da questo dispositivo?",
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"downloadQueued": "ダウンロードをキューに追加しました",
|
||||
"downloadResumed": "ダウンロードを再開しました",
|
||||
"serverErrorBitrate": "サーバーエラー: ファイルがリモートビットレート制限を超えている可能性があります",
|
||||
"storageFull": "デバイスのストレージがいっぱいのため、ダウンロードを停止しました。空き容量を確保してから、もう一度お試しください。",
|
||||
"episodesQueued": "${count}エピソードをダウンロードキューに追加しました",
|
||||
"downloadDeleted": "ダウンロードを削除しました",
|
||||
"deleteConfirm": "このデバイスから「${title}」を削除しますか?",
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"downloadQueued": "다운로드 대기 중",
|
||||
"downloadResumed": "다운로드를 재개했습니다",
|
||||
"serverErrorBitrate": "서버 오류: 파일이 원격 비트레이트 제한을 초과할 수 있습니다",
|
||||
"storageFull": "기기 저장 공간이 가득 차서 다운로드를 중지했습니다. 공간을 확보한 후 다시 시도하세요.",
|
||||
"episodesQueued": "에피소드 ${count}개가 다운로드 대기열에 추가되었습니다",
|
||||
"downloadDeleted": "다운로드 삭제됨",
|
||||
"deleteConfirm": "이 기기에서 \"${title}\"을(를) 삭제할까요?",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Nedlasting i kø",
|
||||
"downloadResumed": "Nedlasting gjenopptatt",
|
||||
"serverErrorBitrate": "Serverfeil: filen kan overskride grensen for ekstern bitrate",
|
||||
"storageFull": "Nedlastingene ble stoppet fordi lagringsplassen på enheten er full. Frigjør plass, og prøv igjen.",
|
||||
"episodesQueued": "${count} episoder i nedlastingskø",
|
||||
"downloadDeleted": "Nedlasting slettet",
|
||||
"deleteConfirm": "Slette \"${title}\" fra denne enheten?",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Download in wachtrij",
|
||||
"downloadResumed": "Download hervat",
|
||||
"serverErrorBitrate": "Serverfout: bestand overschrijdt mogelijk de externe bitrate-limiet",
|
||||
"storageFull": "Downloads zijn gestopt omdat de opslag van het apparaat vol is. Maak ruimte vrij en probeer het opnieuw.",
|
||||
"episodesQueued": "${count} afleveringen in wachtrij voor download",
|
||||
"downloadDeleted": "Download verwijderd",
|
||||
"deleteConfirm": "\"${title}\" van dit apparaat verwijderen?",
|
||||
|
||||
@@ -1157,6 +1157,7 @@
|
||||
"downloadQueued": "Pobranie w kolejce",
|
||||
"downloadResumed": "Pobieranie wznowione",
|
||||
"serverErrorBitrate": "Błąd serwera: plik może przekraczać zdalny limit bitrate",
|
||||
"storageFull": "Pobieranie zostało zatrzymane, ponieważ pamięć urządzenia jest pełna. Zwolnij miejsce i spróbuj ponownie.",
|
||||
"episodesQueued": "${count} odcinków w kolejce pobierania",
|
||||
"downloadDeleted": "Pobranie usunięte",
|
||||
"deleteConfirm": "Usunąć \"${title}\" z tego urządzenia?",
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Download na fila",
|
||||
"downloadResumed": "Download retomado",
|
||||
"serverErrorBitrate": "Erro do servidor: o arquivo pode exceder o limite remoto de taxa de bits",
|
||||
"storageFull": "Os downloads foram interrompidos porque o armazenamento do dispositivo está cheio. Libere espaço e tente novamente.",
|
||||
"episodesQueued": "${count} episódios na fila de download",
|
||||
"downloadDeleted": "Download excluído",
|
||||
"deleteConfirm": "Excluir \"${title}\" deste dispositivo?",
|
||||
|
||||
@@ -1157,6 +1157,7 @@
|
||||
"downloadQueued": "Загрузка поставлена в очередь",
|
||||
"downloadResumed": "Загрузка возобновлена",
|
||||
"serverErrorBitrate": "Ошибка сервера: файл может превышать установленное на сервере ограничение битрейта",
|
||||
"storageFull": "Загрузки остановлены, так как хранилище устройства заполнено. Освободите место и повторите попытку.",
|
||||
"episodesQueued": "${count} эпизодов поставлено в очередь загрузки",
|
||||
"downloadDeleted": "Загрузка удалена",
|
||||
"deleteConfirm": "Удалить \"${title}\" с этого устройства?",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 18
|
||||
/// Strings: 26028 (1446 per locale)
|
||||
/// Strings: 26046 (1447 per locale)
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$bg extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Изтеглянето е добавено в опашката';
|
||||
@override String get downloadResumed => 'Изтеглянето е възобновено';
|
||||
@override String get serverErrorBitrate => 'Грешка на сървъра: файлът може да надвишава лимита за отдалечен битрейт';
|
||||
@override String get storageFull => 'Изтеглянията бяха спрени, защото паметта на устройството е пълна. Освободете място и опитайте отново.';
|
||||
@override String episodesQueued({required Object count}) => '${count} епизода са добавени в опашката за изтегляне';
|
||||
@override String get downloadDeleted => 'Изтеглянето е изтрито';
|
||||
@override String deleteConfirm({required Object title}) => 'Да се изтрие ли "${title}" от това устройство?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsBg {
|
||||
'downloads.downloadQueued' => 'Изтеглянето е добавено в опашката',
|
||||
'downloads.downloadResumed' => 'Изтеглянето е възобновено',
|
||||
'downloads.serverErrorBitrate' => 'Грешка на сървъра: файлът може да надвишава лимита за отдалечен битрейт',
|
||||
'downloads.storageFull' => 'Изтеглянията бяха спрени, защото паметта на устройството е пълна. Освободете място и опитайте отново.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} епизода са добавени в опашката за изтегляне',
|
||||
'downloads.downloadDeleted' => 'Изтеглянето е изтрито',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Да се изтрие ли "${title}" от това устройство?',
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$da extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Download i kø';
|
||||
@override String get downloadResumed => 'Download genoptaget';
|
||||
@override String get serverErrorBitrate => 'Serverfejl: filen overskrider muligvis grænsen for ekstern bitrate';
|
||||
@override String get storageFull => 'Downloads blev stoppet, fordi enhedens lagerplads er fuld. Frigør plads, og prøv igen.';
|
||||
@override String episodesQueued({required Object count}) => '${count} episoder i downloadkø';
|
||||
@override String get downloadDeleted => 'Download slettet';
|
||||
@override String deleteConfirm({required Object title}) => 'Slet "${title}" fra denne enhed?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsDa {
|
||||
'downloads.downloadQueued' => 'Download i kø',
|
||||
'downloads.downloadResumed' => 'Download genoptaget',
|
||||
'downloads.serverErrorBitrate' => 'Serverfejl: filen overskrider muligvis grænsen for ekstern bitrate',
|
||||
'downloads.storageFull' => 'Downloads blev stoppet, fordi enhedens lagerplads er fuld. Frigør plads, og prøv igen.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} episoder i downloadkø',
|
||||
'downloads.downloadDeleted' => 'Download slettet',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Slet "${title}" fra denne enhed?',
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$de extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Download in Warteschlange';
|
||||
@override String get downloadResumed => 'Download fortgesetzt';
|
||||
@override String get serverErrorBitrate => 'Serverfehler: Datei überschreitet möglicherweise das Remote-Bitrate-Limit';
|
||||
@override String get storageFull => 'Die Downloads wurden angehalten, weil der Gerätespeicher voll ist. Gib Speicherplatz frei und versuche es erneut.';
|
||||
@override String episodesQueued({required Object count}) => '${count} Episoden zum Download hinzugefügt';
|
||||
@override String get downloadDeleted => 'Download gelöscht';
|
||||
@override String deleteConfirm({required Object title}) => '"${title}" von diesem Gerät löschen?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsDe {
|
||||
'downloads.downloadQueued' => 'Download in Warteschlange',
|
||||
'downloads.downloadResumed' => 'Download fortgesetzt',
|
||||
'downloads.serverErrorBitrate' => 'Serverfehler: Datei überschreitet möglicherweise das Remote-Bitrate-Limit',
|
||||
'downloads.storageFull' => 'Die Downloads wurden angehalten, weil der Gerätespeicher voll ist. Gib Speicherplatz frei und versuche es erneut.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} Episoden zum Download hinzugefügt',
|
||||
'downloads.downloadDeleted' => 'Download gelöscht',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => '"${title}" von diesem Gerät löschen?',
|
||||
|
||||
@@ -3330,6 +3330,9 @@ class Translations$downloads$en {
|
||||
/// en: 'Server error: file may exceed the remote bitrate limit'
|
||||
String get serverErrorBitrate => 'Server error: file may exceed the remote bitrate limit';
|
||||
|
||||
/// en: 'Downloads stopped because device storage is full. Free some space, then retry.'
|
||||
String get storageFull => 'Downloads stopped because device storage is full. Free some space, then retry.';
|
||||
|
||||
/// en: '${count} episodes queued for download'
|
||||
String episodesQueued({required Object count}) => '${count} episodes queued for download';
|
||||
|
||||
@@ -6101,6 +6104,7 @@ extension on Translations {
|
||||
'downloads.downloadQueued' => 'Download queued',
|
||||
'downloads.downloadResumed' => 'Download resumed',
|
||||
'downloads.serverErrorBitrate' => 'Server error: file may exceed the remote bitrate limit',
|
||||
'downloads.storageFull' => 'Downloads stopped because device storage is full. Free some space, then retry.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} episodes queued for download',
|
||||
'downloads.downloadDeleted' => 'Download deleted',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Delete "${title}" from this device?',
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$es extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Descarga en cola';
|
||||
@override String get downloadResumed => 'Descarga reanudada';
|
||||
@override String get serverErrorBitrate => 'Error del servidor: el archivo puede superar el límite remoto de tasa de bits';
|
||||
@override String get storageFull => 'Las descargas se detuvieron porque el almacenamiento del dispositivo está lleno. Libera espacio e inténtalo de nuevo.';
|
||||
@override String episodesQueued({required Object count}) => '${count} episodios en cola para descargar';
|
||||
@override String get downloadDeleted => 'Descarga eliminada';
|
||||
@override String deleteConfirm({required Object title}) => '¿Eliminar "${title}" de este dispositivo?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsEs {
|
||||
'downloads.downloadQueued' => 'Descarga en cola',
|
||||
'downloads.downloadResumed' => 'Descarga reanudada',
|
||||
'downloads.serverErrorBitrate' => 'Error del servidor: el archivo puede superar el límite remoto de tasa de bits',
|
||||
'downloads.storageFull' => 'Las descargas se detuvieron porque el almacenamiento del dispositivo está lleno. Libera espacio e inténtalo de nuevo.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} episodios en cola para descargar',
|
||||
'downloads.downloadDeleted' => 'Descarga eliminada',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => '¿Eliminar "${title}" de este dispositivo?',
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$fr extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Téléchargement en attente';
|
||||
@override String get downloadResumed => 'Téléchargement repris';
|
||||
@override String get serverErrorBitrate => 'Erreur du serveur : le fichier peut dépasser la limite de débit distant';
|
||||
@override String get storageFull => 'Les téléchargements ont été arrêtés car le stockage de l’appareil est plein. Libérez de l’espace, puis réessayez.';
|
||||
@override String episodesQueued({required Object count}) => '${count} épisodes en attente de téléchargement';
|
||||
@override String get downloadDeleted => 'Téléchargement supprimé';
|
||||
@override String deleteConfirm({required Object title}) => 'Supprimer « ${title} » de cet appareil ?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsFr {
|
||||
'downloads.downloadQueued' => 'Téléchargement en attente',
|
||||
'downloads.downloadResumed' => 'Téléchargement repris',
|
||||
'downloads.serverErrorBitrate' => 'Erreur du serveur : le fichier peut dépasser la limite de débit distant',
|
||||
'downloads.storageFull' => 'Les téléchargements ont été arrêtés car le stockage de l’appareil est plein. Libérez de l’espace, puis réessayez.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} épisodes en attente de téléchargement',
|
||||
'downloads.downloadDeleted' => 'Téléchargement supprimé',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Supprimer « ${title} » de cet appareil ?',
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$hu extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Letöltés sorba állítva';
|
||||
@override String get downloadResumed => 'Letöltés folytatva';
|
||||
@override String get serverErrorBitrate => 'Szerverhiba: a fájl meghaladhatja a távoli bitrátakorlátot';
|
||||
@override String get storageFull => 'A letöltések leálltak, mert az eszköz tárhelye megtelt. Szabadíts fel helyet, majd próbáld újra.';
|
||||
@override String episodesQueued({required Object count}) => '${count} epizód letöltésre sorba állítva';
|
||||
@override String get downloadDeleted => 'Letöltés törölve';
|
||||
@override String deleteConfirm({required Object title}) => 'Törlöd a következőt: "${title}" erről az eszközről?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsHu {
|
||||
'downloads.downloadQueued' => 'Letöltés sorba állítva',
|
||||
'downloads.downloadResumed' => 'Letöltés folytatva',
|
||||
'downloads.serverErrorBitrate' => 'Szerverhiba: a fájl meghaladhatja a távoli bitrátakorlátot',
|
||||
'downloads.storageFull' => 'A letöltések leálltak, mert az eszköz tárhelye megtelt. Szabadíts fel helyet, majd próbáld újra.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} epizód letöltésre sorba állítva',
|
||||
'downloads.downloadDeleted' => 'Letöltés törölve',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Törlöd a következőt: "${title}" erről az eszközről?',
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$it extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Download in coda';
|
||||
@override String get downloadResumed => 'Download ripreso';
|
||||
@override String get serverErrorBitrate => 'Errore server: il file può superare il limite di bitrate remoto';
|
||||
@override String get storageFull => 'I download sono stati interrotti perché lo spazio di archiviazione del dispositivo è esaurito. Libera spazio e riprova.';
|
||||
@override String episodesQueued({required Object count}) => '${count} episodi in coda per il download';
|
||||
@override String get downloadDeleted => 'Download eliminato';
|
||||
@override String deleteConfirm({required Object title}) => 'Eliminare "${title}" da questo dispositivo?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsIt {
|
||||
'downloads.downloadQueued' => 'Download in coda',
|
||||
'downloads.downloadResumed' => 'Download ripreso',
|
||||
'downloads.serverErrorBitrate' => 'Errore server: il file può superare il limite di bitrate remoto',
|
||||
'downloads.storageFull' => 'I download sono stati interrotti perché lo spazio di archiviazione del dispositivo è esaurito. Libera spazio e riprova.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} episodi in coda per il download',
|
||||
'downloads.downloadDeleted' => 'Download eliminato',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Eliminare "${title}" da questo dispositivo?',
|
||||
|
||||
@@ -1397,6 +1397,7 @@ class _Translations$downloads$ja extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'ダウンロードをキューに追加しました';
|
||||
@override String get downloadResumed => 'ダウンロードを再開しました';
|
||||
@override String get serverErrorBitrate => 'サーバーエラー: ファイルがリモートビットレート制限を超えている可能性があります';
|
||||
@override String get storageFull => 'デバイスのストレージがいっぱいのため、ダウンロードを停止しました。空き容量を確保してから、もう一度お試しください。';
|
||||
@override String episodesQueued({required Object count}) => '${count}エピソードをダウンロードキューに追加しました';
|
||||
@override String get downloadDeleted => 'ダウンロードを削除しました';
|
||||
@override String deleteConfirm({required Object title}) => 'このデバイスから「${title}」を削除しますか?';
|
||||
@@ -3205,6 +3206,7 @@ extension on TranslationsJa {
|
||||
'downloads.downloadQueued' => 'ダウンロードをキューに追加しました',
|
||||
'downloads.downloadResumed' => 'ダウンロードを再開しました',
|
||||
'downloads.serverErrorBitrate' => 'サーバーエラー: ファイルがリモートビットレート制限を超えている可能性があります',
|
||||
'downloads.storageFull' => 'デバイスのストレージがいっぱいのため、ダウンロードを停止しました。空き容量を確保してから、もう一度お試しください。',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count}エピソードをダウンロードキューに追加しました',
|
||||
'downloads.downloadDeleted' => 'ダウンロードを削除しました',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'このデバイスから「${title}」を削除しますか?',
|
||||
|
||||
@@ -1397,6 +1397,7 @@ class _Translations$downloads$ko extends Translations$downloads$en {
|
||||
@override String get downloadQueued => '다운로드 대기 중';
|
||||
@override String get downloadResumed => '다운로드를 재개했습니다';
|
||||
@override String get serverErrorBitrate => '서버 오류: 파일이 원격 비트레이트 제한을 초과할 수 있습니다';
|
||||
@override String get storageFull => '기기 저장 공간이 가득 차서 다운로드를 중지했습니다. 공간을 확보한 후 다시 시도하세요.';
|
||||
@override String episodesQueued({required Object count}) => '에피소드 ${count}개가 다운로드 대기열에 추가되었습니다';
|
||||
@override String get downloadDeleted => '다운로드 삭제됨';
|
||||
@override String deleteConfirm({required Object title}) => '이 기기에서 "${title}"을(를) 삭제할까요?';
|
||||
@@ -3205,6 +3206,7 @@ extension on TranslationsKo {
|
||||
'downloads.downloadQueued' => '다운로드 대기 중',
|
||||
'downloads.downloadResumed' => '다운로드를 재개했습니다',
|
||||
'downloads.serverErrorBitrate' => '서버 오류: 파일이 원격 비트레이트 제한을 초과할 수 있습니다',
|
||||
'downloads.storageFull' => '기기 저장 공간이 가득 차서 다운로드를 중지했습니다. 공간을 확보한 후 다시 시도하세요.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '에피소드 ${count}개가 다운로드 대기열에 추가되었습니다',
|
||||
'downloads.downloadDeleted' => '다운로드 삭제됨',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => '이 기기에서 "${title}"을(를) 삭제할까요?',
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$nb extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Nedlasting i kø';
|
||||
@override String get downloadResumed => 'Nedlasting gjenopptatt';
|
||||
@override String get serverErrorBitrate => 'Serverfeil: filen kan overskride grensen for ekstern bitrate';
|
||||
@override String get storageFull => 'Nedlastingene ble stoppet fordi lagringsplassen på enheten er full. Frigjør plass, og prøv igjen.';
|
||||
@override String episodesQueued({required Object count}) => '${count} episoder i nedlastingskø';
|
||||
@override String get downloadDeleted => 'Nedlasting slettet';
|
||||
@override String deleteConfirm({required Object title}) => 'Slette "${title}" fra denne enheten?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsNb {
|
||||
'downloads.downloadQueued' => 'Nedlasting i kø',
|
||||
'downloads.downloadResumed' => 'Nedlasting gjenopptatt',
|
||||
'downloads.serverErrorBitrate' => 'Serverfeil: filen kan overskride grensen for ekstern bitrate',
|
||||
'downloads.storageFull' => 'Nedlastingene ble stoppet fordi lagringsplassen på enheten er full. Frigjør plass, og prøv igjen.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} episoder i nedlastingskø',
|
||||
'downloads.downloadDeleted' => 'Nedlasting slettet',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Slette "${title}" fra denne enheten?',
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$nl extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Download in wachtrij';
|
||||
@override String get downloadResumed => 'Download hervat';
|
||||
@override String get serverErrorBitrate => 'Serverfout: bestand overschrijdt mogelijk de externe bitrate-limiet';
|
||||
@override String get storageFull => 'Downloads zijn gestopt omdat de opslag van het apparaat vol is. Maak ruimte vrij en probeer het opnieuw.';
|
||||
@override String episodesQueued({required Object count}) => '${count} afleveringen in wachtrij voor download';
|
||||
@override String get downloadDeleted => 'Download verwijderd';
|
||||
@override String deleteConfirm({required Object title}) => '"${title}" van dit apparaat verwijderen?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsNl {
|
||||
'downloads.downloadQueued' => 'Download in wachtrij',
|
||||
'downloads.downloadResumed' => 'Download hervat',
|
||||
'downloads.serverErrorBitrate' => 'Serverfout: bestand overschrijdt mogelijk de externe bitrate-limiet',
|
||||
'downloads.storageFull' => 'Downloads zijn gestopt omdat de opslag van het apparaat vol is. Maak ruimte vrij en probeer het opnieuw.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} afleveringen in wachtrij voor download',
|
||||
'downloads.downloadDeleted' => 'Download verwijderd',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => '"${title}" van dit apparaat verwijderen?',
|
||||
|
||||
@@ -1406,6 +1406,7 @@ class _Translations$downloads$pl extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Pobranie w kolejce';
|
||||
@override String get downloadResumed => 'Pobieranie wznowione';
|
||||
@override String get serverErrorBitrate => 'Błąd serwera: plik może przekraczać zdalny limit bitrate';
|
||||
@override String get storageFull => 'Pobieranie zostało zatrzymane, ponieważ pamięć urządzenia jest pełna. Zwolnij miejsce i spróbuj ponownie.';
|
||||
@override String episodesQueued({required Object count}) => '${count} odcinków w kolejce pobierania';
|
||||
@override String get downloadDeleted => 'Pobranie usunięte';
|
||||
@override String deleteConfirm({required Object title}) => 'Usunąć "${title}" z tego urządzenia?';
|
||||
@@ -3214,6 +3215,7 @@ extension on TranslationsPl {
|
||||
'downloads.downloadQueued' => 'Pobranie w kolejce',
|
||||
'downloads.downloadResumed' => 'Pobieranie wznowione',
|
||||
'downloads.serverErrorBitrate' => 'Błąd serwera: plik może przekraczać zdalny limit bitrate',
|
||||
'downloads.storageFull' => 'Pobieranie zostało zatrzymane, ponieważ pamięć urządzenia jest pełna. Zwolnij miejsce i spróbuj ponownie.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} odcinków w kolejce pobierania',
|
||||
'downloads.downloadDeleted' => 'Pobranie usunięte',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Usunąć "${title}" z tego urządzenia?',
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$pt extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Download na fila';
|
||||
@override String get downloadResumed => 'Download retomado';
|
||||
@override String get serverErrorBitrate => 'Erro do servidor: o arquivo pode exceder o limite remoto de taxa de bits';
|
||||
@override String get storageFull => 'Os downloads foram interrompidos porque o armazenamento do dispositivo está cheio. Libere espaço e tente novamente.';
|
||||
@override String episodesQueued({required Object count}) => '${count} episódios na fila de download';
|
||||
@override String get downloadDeleted => 'Download excluído';
|
||||
@override String deleteConfirm({required Object title}) => 'Excluir "${title}" deste dispositivo?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsPt {
|
||||
'downloads.downloadQueued' => 'Download na fila',
|
||||
'downloads.downloadResumed' => 'Download retomado',
|
||||
'downloads.serverErrorBitrate' => 'Erro do servidor: o arquivo pode exceder o limite remoto de taxa de bits',
|
||||
'downloads.storageFull' => 'Os downloads foram interrompidos porque o armazenamento do dispositivo está cheio. Libere espaço e tente novamente.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} episódios na fila de download',
|
||||
'downloads.downloadDeleted' => 'Download excluído',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Excluir "${title}" deste dispositivo?',
|
||||
|
||||
@@ -1406,6 +1406,7 @@ class _Translations$downloads$ru extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Загрузка поставлена в очередь';
|
||||
@override String get downloadResumed => 'Загрузка возобновлена';
|
||||
@override String get serverErrorBitrate => 'Ошибка сервера: файл может превышать установленное на сервере ограничение битрейта';
|
||||
@override String get storageFull => 'Загрузки остановлены, так как хранилище устройства заполнено. Освободите место и повторите попытку.';
|
||||
@override String episodesQueued({required Object count}) => '${count} эпизодов поставлено в очередь загрузки';
|
||||
@override String get downloadDeleted => 'Загрузка удалена';
|
||||
@override String deleteConfirm({required Object title}) => 'Удалить "${title}" с этого устройства?';
|
||||
@@ -3214,6 +3215,7 @@ extension on TranslationsRu {
|
||||
'downloads.downloadQueued' => 'Загрузка поставлена в очередь',
|
||||
'downloads.downloadResumed' => 'Загрузка возобновлена',
|
||||
'downloads.serverErrorBitrate' => 'Ошибка сервера: файл может превышать установленное на сервере ограничение битрейта',
|
||||
'downloads.storageFull' => 'Загрузки остановлены, так как хранилище устройства заполнено. Освободите место и повторите попытку.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} эпизодов поставлено в очередь загрузки',
|
||||
'downloads.downloadDeleted' => 'Загрузка удалена',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Удалить "${title}" с этого устройства?',
|
||||
|
||||
@@ -1400,6 +1400,7 @@ class _Translations$downloads$sv extends Translations$downloads$en {
|
||||
@override String get downloadQueued => 'Nedladdning köad';
|
||||
@override String get downloadResumed => 'Nedladdning återupptagen';
|
||||
@override String get serverErrorBitrate => 'Serverfel: filen kan överskrida serverns bithastighetsgräns';
|
||||
@override String get storageFull => 'Nedladdningarna stoppades eftersom enhetens lagringsutrymme är fullt. Frigör utrymme och försök igen.';
|
||||
@override String episodesQueued({required Object count}) => '${count} avsnitt köade för nedladdning';
|
||||
@override String get downloadDeleted => 'Nedladdning borttagen';
|
||||
@override String deleteConfirm({required Object title}) => 'Ta bort "${title}" från den här enheten?';
|
||||
@@ -3208,6 +3209,7 @@ extension on TranslationsSv {
|
||||
'downloads.downloadQueued' => 'Nedladdning köad',
|
||||
'downloads.downloadResumed' => 'Nedladdning återupptagen',
|
||||
'downloads.serverErrorBitrate' => 'Serverfel: filen kan överskrida serverns bithastighetsgräns',
|
||||
'downloads.storageFull' => 'Nedladdningarna stoppades eftersom enhetens lagringsutrymme är fullt. Frigör utrymme och försök igen.',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} avsnitt köade för nedladdning',
|
||||
'downloads.downloadDeleted' => 'Nedladdning borttagen',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => 'Ta bort "${title}" från den här enheten?',
|
||||
|
||||
@@ -1397,6 +1397,7 @@ class Translations$downloads$zh extends Translations$downloads$en {
|
||||
@override String get downloadQueued => '下载已排队';
|
||||
@override String get downloadResumed => '下载已继续';
|
||||
@override String get serverErrorBitrate => '服务器错误:文件可能超过远程比特率限制';
|
||||
@override String get storageFull => '设备存储空间已满,因此下载已停止。请释放空间后重试。';
|
||||
@override String episodesQueued({required Object count}) => '${count} 集已加入下载队列';
|
||||
@override String get downloadDeleted => '下载已删除';
|
||||
@override String deleteConfirm({required Object title}) => '要从此设备删除“${title}”吗?';
|
||||
@@ -3205,6 +3206,7 @@ extension on TranslationsZh {
|
||||
'downloads.downloadQueued' => '下载已排队',
|
||||
'downloads.downloadResumed' => '下载已继续',
|
||||
'downloads.serverErrorBitrate' => '服务器错误:文件可能超过远程比特率限制',
|
||||
'downloads.storageFull' => '设备存储空间已满,因此下载已停止。请释放空间后重试。',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '${count} 集已加入下载队列',
|
||||
'downloads.downloadDeleted' => '下载已删除',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => '要从此设备删除“${title}”吗?',
|
||||
|
||||
@@ -1398,6 +1398,7 @@ class _Translations$downloads$zh_Hant extends Translations$downloads$zh {
|
||||
@override String get downloadQueued => '下載已排隊';
|
||||
@override String get downloadResumed => '下載已繼續';
|
||||
@override String get serverErrorBitrate => '伺服器錯誤:檔案位元率可能超過遠端位元率限制';
|
||||
@override String get storageFull => '裝置儲存空間已滿,因此下載已停止。請釋出空間後再試一次。';
|
||||
@override String episodesQueued({required Object count}) => '已將 ${count} 集影片加入下載佇列';
|
||||
@override String get downloadDeleted => '下載內容已刪除';
|
||||
@override String deleteConfirm({required Object title}) => '確定要從此裝置刪除「${title}」嗎?';
|
||||
@@ -3206,6 +3207,7 @@ extension on TranslationsZhHant {
|
||||
'downloads.downloadQueued' => '下載已排隊',
|
||||
'downloads.downloadResumed' => '下載已繼續',
|
||||
'downloads.serverErrorBitrate' => '伺服器錯誤:檔案位元率可能超過遠端位元率限制',
|
||||
'downloads.storageFull' => '裝置儲存空間已滿,因此下載已停止。請釋出空間後再試一次。',
|
||||
'downloads.episodesQueued' => ({required Object count}) => '已將 ${count} 集影片加入下載佇列',
|
||||
'downloads.downloadDeleted' => '下載內容已刪除',
|
||||
'downloads.deleteConfirm' => ({required Object title}) => '確定要從此裝置刪除「${title}」嗎?',
|
||||
|
||||
@@ -1151,6 +1151,7 @@
|
||||
"downloadQueued": "Nedladdning köad",
|
||||
"downloadResumed": "Nedladdning återupptagen",
|
||||
"serverErrorBitrate": "Serverfel: filen kan överskrida serverns bithastighetsgräns",
|
||||
"storageFull": "Nedladdningarna stoppades eftersom enhetens lagringsutrymme är fullt. Frigör utrymme och försök igen.",
|
||||
"episodesQueued": "${count} avsnitt köade för nedladdning",
|
||||
"downloadDeleted": "Nedladdning borttagen",
|
||||
"deleteConfirm": "Ta bort \"${title}\" från den här enheten?",
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"downloadQueued": "下載已排隊",
|
||||
"downloadResumed": "下載已繼續",
|
||||
"serverErrorBitrate": "伺服器錯誤:檔案位元率可能超過遠端位元率限制",
|
||||
"storageFull": "裝置儲存空間已滿,因此下載已停止。請釋出空間後再試一次。",
|
||||
"episodesQueued": "已將 ${count} 集影片加入下載佇列",
|
||||
"downloadDeleted": "下載內容已刪除",
|
||||
"deleteConfirm": "確定要從此裝置刪除「${title}」嗎?",
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
"downloadQueued": "下载已排队",
|
||||
"downloadResumed": "下载已继续",
|
||||
"serverErrorBitrate": "服务器错误:文件可能超过远程比特率限制",
|
||||
"storageFull": "设备存储空间已满,因此下载已停止。请释放空间后重试。",
|
||||
"episodesQueued": "${count} 集已加入下载队列",
|
||||
"downloadDeleted": "下载已删除",
|
||||
"deleteConfirm": "要从此设备删除“${title}”吗?",
|
||||
|
||||
+32
-8
@@ -64,6 +64,7 @@ import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'services/jellyfin_api_cache.dart';
|
||||
import 'services/plex_api_cache.dart';
|
||||
import 'database/app_database.dart';
|
||||
import 'database/download_operations.dart';
|
||||
import 'database/tvos_database_recovery_store.dart';
|
||||
import 'screens/video_player_screen.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
@@ -82,6 +83,7 @@ import 'utils/navigation_transitions.dart';
|
||||
import 'utils/log_redaction_manager.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'utils/android_exit_diagnostics.dart';
|
||||
import 'utils/storage_failure.dart';
|
||||
|
||||
const bool _enableSentry = bool.fromEnvironment('ENABLE_SENTRY', defaultValue: false);
|
||||
const String _sentryDsn = 'https://6a1a6ef8c72140099b2798973c1bfb2f@bugs.plezy.app/1';
|
||||
@@ -349,6 +351,30 @@ class _StartupDependencies {
|
||||
final TvosDatabaseRecoveryOutcome databaseRecoveryOutcome;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<AppDatabaseBootstrap> openAppDatabaseWithDownloadRecovery({
|
||||
required Future<AppDatabaseBootstrap> Function() openDatabase,
|
||||
required Future<void> Function() recoverNativeDownloads,
|
||||
required String storageFullMessage,
|
||||
}) async {
|
||||
try {
|
||||
return await openDatabase();
|
||||
} catch (error, stackTrace) {
|
||||
if (!isStorageFullError(error)) rethrow;
|
||||
appLogger.w(
|
||||
'Application database could not open because storage is full; discarding interrupted downloads',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
await recoverNativeDownloads();
|
||||
final bootstrap = await openDatabase();
|
||||
final failedCount = await bootstrap.database.failActiveDownloadsForStorageFull(storageFullMessage);
|
||||
appLogger.w('Recovered startup after storage exhaustion; stopped $failedCount active download(s)');
|
||||
return bootstrap;
|
||||
}
|
||||
|
||||
Future<_StartupDependencies> _initializeStartup(SettingsService settings) async {
|
||||
final startupWatch = Stopwatch()..start();
|
||||
var lastStartupMarkMs = 0;
|
||||
@@ -386,7 +412,11 @@ Future<_StartupDependencies> _initializeStartup(SettingsService settings) async
|
||||
markStartupPhase('platform-services');
|
||||
|
||||
AndroidExitDiagnostics.markStartupPhase(AndroidStartupPhase.databaseOpenStarted);
|
||||
final databaseBootstrap = await AppDatabase.open(isTvos: PlatformDetector.isAppleTV());
|
||||
final databaseBootstrap = await openAppDatabaseWithDownloadRecovery(
|
||||
openDatabase: () => AppDatabase.open(isTvos: PlatformDetector.isAppleTV()),
|
||||
recoverNativeDownloads: DownloadManagerService.discardInterruptedNativeDownloadsAfterStorageFailure,
|
||||
storageFullMessage: t.downloads.storageFull,
|
||||
);
|
||||
openedDatabase = databaseBootstrap.database;
|
||||
AndroidExitDiagnostics.markStartupPhase(AndroidStartupPhase.databaseReady);
|
||||
markStartupPhase('database-recovery');
|
||||
@@ -516,13 +546,7 @@ FutureOr<SentryEvent?> _beforeSend(SentryEvent event, Hint _) {
|
||||
return true;
|
||||
}
|
||||
// Device out of disk space
|
||||
if (v != null &&
|
||||
(v.contains('SQLITE_FULL') ||
|
||||
v.contains('No space left on device') ||
|
||||
v.contains('errno = 112') ||
|
||||
v.contains('database or disk is full'))) {
|
||||
return true;
|
||||
}
|
||||
if (isStorageFullMessage(v)) return true;
|
||||
// Native HTTP errors from CFNetwork (server errors, not actionable)
|
||||
if (e.type == 'HTTPClientError') return true;
|
||||
// Benign EventChannel teardown race: the engine replies this when a
|
||||
|
||||
@@ -35,6 +35,7 @@ import '../utils/serial_future_queue.dart';
|
||||
import '../utils/active_client_scope.dart';
|
||||
import '../utils/codec_utils.dart';
|
||||
import '../utils/global_key_utils.dart';
|
||||
import '../utils/storage_failure.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
typedef MediaClientResolver = MediaServerClient? Function(ServerId serverId, {String? clientScopeId});
|
||||
@@ -142,6 +143,7 @@ class DownloadManagerService {
|
||||
bool _isProcessingQueue = false;
|
||||
bool _isRepairingArtwork = false;
|
||||
bool _disposed = false;
|
||||
bool _queueBlockedByStorageFailure = false;
|
||||
bool _loggedDownloadsUnsupported = false;
|
||||
|
||||
// Debounce timers for DB progress writes (keyed by globalKey).
|
||||
@@ -163,6 +165,22 @@ class DownloadManagerService {
|
||||
@visibleForTesting
|
||||
static bool downloadsSupportedFor({required bool tvosBuild}) => !tvosBuild;
|
||||
|
||||
/// Cancels native work and discards resumable partial files so startup can
|
||||
/// recover enough space to reopen the application database.
|
||||
static Future<void> discardInterruptedNativeDownloadsAfterStorageFailure() async {
|
||||
final downloader = FileDownloader();
|
||||
try {
|
||||
await downloader.reset(group: _downloadGroup);
|
||||
} catch (error, stackTrace) {
|
||||
appLogger.w('Failed to cancel native downloads during storage recovery', error: error, stackTrace: stackTrace);
|
||||
}
|
||||
try {
|
||||
await downloader.database.deleteAllRecords(group: _downloadGroup);
|
||||
} catch (error, stackTrace) {
|
||||
appLogger.w('Failed to discard partial downloads during storage recovery', error: error, stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<bool> shouldBlockDownloadOnCellular() async {
|
||||
final List<ConnectivityResult> connectivity;
|
||||
try {
|
||||
@@ -1465,6 +1483,7 @@ class DownloadManagerService {
|
||||
int mediaIndex = 0,
|
||||
}) async {
|
||||
if (_skipDownloadsUnsupported('queue download')) return;
|
||||
_resumeQueueAfterStorageFailure('new download');
|
||||
|
||||
final globalKey = metadata.globalKey;
|
||||
|
||||
@@ -1511,7 +1530,7 @@ class DownloadManagerService {
|
||||
/// Process the download queue — prepares and enqueues items with background_downloader.
|
||||
/// Non-blocking: returns after all queued items are enqueued (downloads run natively).
|
||||
Future<void> _processQueue(MediaServerClient client) async {
|
||||
if (_skipDownloadsUnsupported('download queue processing')) return;
|
||||
if (_skipDownloadsUnsupported('download queue processing') || _queueBlockedByStorageFailure) return;
|
||||
final queueProcessorOverride = _queueProcessorOverride;
|
||||
if (queueProcessorOverride != null) {
|
||||
_fallbackClient = client;
|
||||
@@ -1525,7 +1544,7 @@ class DownloadManagerService {
|
||||
try {
|
||||
await (_fileDownloaderInitializerOverride?.call() ?? _initializeFileDownloader());
|
||||
|
||||
while (true) {
|
||||
while (!_queueBlockedByStorageFailure) {
|
||||
if (_consecutiveQueueFailures >= _maxConsecutiveFailures) {
|
||||
appLogger.w('Circuit breaker: $_consecutiveQueueFailures consecutive failures, pausing queue');
|
||||
break;
|
||||
@@ -1677,6 +1696,7 @@ class DownloadManagerService {
|
||||
) async {
|
||||
if (_skipDownloadsUnsupported('download enqueue')) return false;
|
||||
if (_cancellingKeys.contains(globalKey)) return true;
|
||||
if (_queueBlockedByStorageFailure) return true;
|
||||
|
||||
try {
|
||||
// Guard: don't re-enqueue an item that's already completed or was deleted
|
||||
@@ -1697,6 +1717,7 @@ class DownloadManagerService {
|
||||
await _database.removeFromQueue(globalKey);
|
||||
return true;
|
||||
}
|
||||
if (_queueBlockedByStorageFailure) return true;
|
||||
await _transitionStatus(globalKey, DownloadStatus.downloading);
|
||||
|
||||
final parsed = parseGlobalKey(globalKey);
|
||||
@@ -1770,6 +1791,7 @@ class DownloadManagerService {
|
||||
final MediaItem resolvedMetadata = metadata;
|
||||
|
||||
final becameInactive = await _serializeSafOwnership(() async {
|
||||
if (_queueBlockedByStorageFailure) return true;
|
||||
final metadata = resolvedMetadata;
|
||||
final safBaseUri = _storageService.safBaseUri;
|
||||
if (_storageService.isUsingSaf && safBaseUri != null) {
|
||||
@@ -2026,7 +2048,7 @@ class DownloadManagerService {
|
||||
case TaskStatus.complete:
|
||||
await _onDownloadComplete(globalKey, update.task);
|
||||
case TaskStatus.failed:
|
||||
await _onDownloadFailed(globalKey, update.task.taskId, update.exception?.description ?? 'Download failed');
|
||||
await _onDownloadFailed(globalKey, update.task.taskId, update.exception);
|
||||
case TaskStatus.notFound:
|
||||
await _onDownloadPermanentlyFailed(globalKey, update.task.taskId, 'File not found (404)');
|
||||
case TaskStatus.canceled:
|
||||
@@ -2072,6 +2094,43 @@ class DownloadManagerService {
|
||||
await _requeueDownload(globalKey);
|
||||
}
|
||||
|
||||
void _resumeQueueAfterStorageFailure(String operation) {
|
||||
if (!_queueBlockedByStorageFailure) return;
|
||||
appLogger.i('Resuming download queue after storage failure: $operation');
|
||||
_queueBlockedByStorageFailure = false;
|
||||
_consecutiveQueueFailures = 0;
|
||||
}
|
||||
|
||||
Future<void> _handleStorageFullFailure(String globalKey, String taskId) async {
|
||||
_queueBlockedByStorageFailure = true;
|
||||
for (final timer in _autoRetryTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_autoRetryTimers.clear();
|
||||
|
||||
if (downloadsSupported) {
|
||||
try {
|
||||
await FileDownloader().database.deleteRecordWithId(taskId);
|
||||
} catch (error, stackTrace) {
|
||||
appLogger.w('Failed to discard partial download for $globalKey', error: error, stackTrace: stackTrace);
|
||||
}
|
||||
try {
|
||||
await FileDownloader().reset(group: _downloadGroup);
|
||||
} catch (error, stackTrace) {
|
||||
appLogger.w(
|
||||
'Failed to stop native download queue after storage exhaustion',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final errorMessage = t.downloads.storageFull;
|
||||
final failedCount = await _database.failActiveDownloadsForStorageFull(errorMessage);
|
||||
_emitProgress(globalKey, DownloadStatus.failed, 0, errorMessage: errorMessage);
|
||||
appLogger.e('Device storage exhausted; stopped $failedCount active download(s)');
|
||||
}
|
||||
|
||||
bool _isRetryablePrepareFailure(Object error) {
|
||||
if (error is! MediaServerHttpException || error.isCancellation) return false;
|
||||
final status = error.statusCode;
|
||||
@@ -2101,9 +2160,13 @@ class DownloadManagerService {
|
||||
if (processQueueAfterProgress) unawaited(_processQueue(client));
|
||||
}
|
||||
|
||||
/// Handle a failed download — auto-retry if retries remain, otherwise permanently fail.
|
||||
/// Native retries (Range-based resume) are already exhausted at this point.
|
||||
Future<void> _onDownloadFailed(String globalKey, String taskId, String errorMessage) async {
|
||||
bool _isStorageFullDownloadFailure(TaskException? exception) {
|
||||
return exception != null && isStorageFullMessage(exception.description);
|
||||
}
|
||||
|
||||
/// Handle a failed download — stop the queue on storage exhaustion,
|
||||
/// otherwise auto-retry if retries remain.
|
||||
Future<void> _onDownloadFailed(String globalKey, String taskId, TaskException? exception) async {
|
||||
if (_cancellingKeys.contains(globalKey)) {
|
||||
appLogger.d('Ignoring failure for $globalKey: cancellation in progress');
|
||||
return;
|
||||
@@ -2122,6 +2185,11 @@ class DownloadManagerService {
|
||||
if (existing == null) return;
|
||||
_cancelDownloadTimers(globalKey);
|
||||
_pendingDownloadContext.remove(globalKey);
|
||||
if (_isStorageFullDownloadFailure(exception)) {
|
||||
await _handleStorageFullFailure(globalKey, taskId);
|
||||
return;
|
||||
}
|
||||
final errorMessage = exception?.description ?? 'Download failed';
|
||||
final retryCount = existing.retryCount;
|
||||
|
||||
// DNS/connection errors fail instantly and exhaust native retries in milliseconds,
|
||||
@@ -2663,6 +2731,7 @@ class DownloadManagerService {
|
||||
/// Resume a paused download
|
||||
Future<void> resumeDownload(String globalKey, MediaServerClient client) async {
|
||||
if (_skipDownloadsUnsupported('download resume')) return;
|
||||
_resumeQueueAfterStorageFailure('manual resume');
|
||||
|
||||
final bgTaskId = await _database.getBgTaskId(globalKey);
|
||||
|
||||
@@ -2714,6 +2783,7 @@ class DownloadManagerService {
|
||||
/// Retry a failed download
|
||||
Future<void> retryDownload(String globalKey, MediaServerClient client) async {
|
||||
if (_skipDownloadsUnsupported('download retry')) return;
|
||||
_resumeQueueAfterStorageFailure('manual retry');
|
||||
|
||||
_autoRetryTimers.remove(globalKey)?.cancel();
|
||||
await _cleanupStaleDownload(globalKey);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
bool isStorageFullError(Object? error) => isStorageFullMessage(error?.toString());
|
||||
|
||||
bool isStorageFullMessage(String? message) {
|
||||
if (message == null || message.isEmpty) return false;
|
||||
final value = message.toLowerCase();
|
||||
return value.contains('sqlite_full') ||
|
||||
value.contains('database or disk is full') ||
|
||||
value.contains('no space left on device') ||
|
||||
value.contains('enospc') ||
|
||||
value.contains('errno = 112') ||
|
||||
value.contains('not enough space on the disk') ||
|
||||
value.contains('insufficient space to store') ||
|
||||
value.contains('volume is out of space') ||
|
||||
value.contains('nsfilewriteoutofspaceerror');
|
||||
}
|
||||
Reference in New Issue
Block a user