From bbc7fa04f55de8c91f3c1cd40dcc58a5be728a50 Mon Sep 17 00:00:00 2001 From: Mehakdeep Singh <118588258+mrandhawa14@users.noreply.github.com> Date: Sat, 9 May 2026 19:07:45 -0700 Subject: [PATCH] fix(downloads): unstall anime downloads on sources that return URLs with query strings or HTTP 206 Two related bugs left anime downloads stuck at 0% with no error visible to the user. Manga downloads from clean HTTPS sources were unaffected. 1) lib/utils/extensions/string_extensions.dart isMediaVideo() did a plain endsWith on the full URL string, so URLs shaped like https://host/play/{id}/video.mp4?for={token} (used by AnimeGG and several other sources) failed the filter because of the trailing `?for=...` query string. With both m3u8Urls and nonM3u8Urls empty in downloadChapter, the surrounding Future.doWhile(() => isOk == true) poll never sets isOk and waits forever -- MDownloader is never constructed. Fix: match against Uri.tryParse(this)?.path instead of the full string, and use a leading "." in the suffix so e.g. "flashmp4" cannot accidentally match. 2) lib/services/download_manager/download_isolate_pool.dart Once the URL passes the filter, _downloadFile (anime branch) opens a streaming request. When the source extension sets Range: bytes=0-, the server correctly responds with HTTP 206 Partial Content. The previous "if (response.statusCode != 200)" check rejected that, retried 3x, and threw. The throw was masked by an outer catch(_) in downloadChapter, so the user only saw a forever-spinner. Fix: accept any 2xx (>= 200 && < 300). Same fix applied to _downloadSegment for HLS segment fetches. Repro - Source: AnimeGG (en) -- install via Mangayomi extensions - Pick any episode (tested with Toriko Ep 147, Gintama Ep 39, Grand Blue Ep 12) - Tap the download icon Before: an empty ".../AnimeGG (EN)//.mp4"-named folder is created, the download icon stays in the spinner state, no error toast. After: the .mp4 is written to disk at the size declared in Content-Length (65,026,283 bytes for Toriko Ep 147), plays in the system video player. Tested on macOS 26.3 / Apple Silicon with AnimeGG (multiple episodes, multiple series, including a 180 MB 720p) and a manga control (Asura Scans, 9 pages) on the same build to confirm no regression on the manga path. --- .../download_manager/download_isolate_pool.dart | 17 +++++++++++++---- lib/utils/extensions/string_extensions.dart | 8 ++++++-- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/services/download_manager/download_isolate_pool.dart b/lib/services/download_manager/download_isolate_pool.dart index 4c57da76..f06a9320 100644 --- a/lib/services/download_manager/download_isolate_pool.dart +++ b/lib/services/download_manager/download_isolate_pool.dart @@ -509,9 +509,14 @@ Future _downloadFile( var request = Request('GET', Uri.parse(pageUrl.url)); request.headers.addAll(pageUrl.headers ?? {}); StreamedResponse response = await client.send(request); - if (response.statusCode != 200) { + // Accept any 2xx — including 206 Partial Content, which the server + // returns when the source extension sends `Range: bytes=0-` on the + // streaming request (e.g. AnimeGG). Rejecting 206 here caused 3 + // retries → silent stall. + if (response.statusCode < 200 || response.statusCode >= 300) { throw DownloadPoolException( - 'Failed to download file: ${pageUrl.fileName!}', + 'Failed to download file: ${pageUrl.fileName!} ' + '(status ${response.statusCode})', ); } int total = response.contentLength ?? 0; @@ -617,8 +622,12 @@ Future _downloadSegment( } StreamedResponse response = await _withRetry(() => client.send(request), 3); - if (response.statusCode != 200) { - throw DownloadPoolException('Failed to download segment: ${ts.name}'); + // Accept any 2xx (including 206 Partial Content) — see comment in + // _downloadFile. + if (response.statusCode < 200 || response.statusCode >= 300) { + throw DownloadPoolException( + 'Failed to download segment: ${ts.name} (status ${response.statusCode})', + ); } final sink = file.openWrite(); diff --git a/lib/utils/extensions/string_extensions.dart b/lib/utils/extensions/string_extensions.dart index c3f4dd45..0b0e4aeb 100644 --- a/lib/utils/extensions/string_extensions.dart +++ b/lib/utils/extensions/string_extensions.dart @@ -59,7 +59,11 @@ extension StringExtensions on String { } bool isMediaVideo() { - return [ + // Match against the URL path only — query strings (e.g. AnimeGG's + // `?for=...`) and fragments must not defeat the suffix check. Use a + // leading `.` so e.g. `flashmp4` doesn't accidentally match. + final lower = (Uri.tryParse(this)?.path ?? this).toLowerCase(); + return const [ "3gp", "avi", "mpg", @@ -73,7 +77,7 @@ extension StringExtensions on String { "wmv", "mkv", "mov", - ].any((extension) => toLowerCase().endsWith(extension)); + ].any((extension) => lower.endsWith(".$extension")); } }