From 8ed6b21125894382a85f08815cd5f55c67a008ad Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:59:16 +0100 Subject: [PATCH] feat: can now download m3u8 videos --- lib/modules/anime/anime_player_view.dart | 5 +- .../download/providers/download_provider.dart | 83 +++++++++++++++---- .../providers/download_provider.g.dart | 2 +- lib/services/aniskip.g.dart | 2 +- lib/services/get_video_list.dart | 34 ++++++-- lib/services/get_video_list.g.dart | 39 ++++++--- lib/services/m3u8/m3u8_downloader.dart | 64 ++++++++++++++ lib/services/m3u8/m3u8_server.dart | 41 +++++++++ lib/services/trackers/anilist.g.dart | 2 +- lib/services/trackers/kitsu.g.dart | 2 +- lib/services/trackers/myanimelist.g.dart | 2 +- pubspec.lock | 26 +++--- pubspec.yaml | 3 + rust/Cargo.lock | 36 ++++---- 14 files changed, 271 insertions(+), 70 deletions(-) create mode 100644 lib/services/m3u8/m3u8_downloader.dart create mode 100644 lib/services/m3u8/m3u8_server.dart diff --git a/lib/modules/anime/anime_player_view.dart b/lib/modules/anime/anime_player_view.dart index 64870b60..4cf8a7e9 100644 --- a/lib/modules/anime/anime_player_view.dart +++ b/lib/modules/anime/anime_player_view.dart @@ -41,11 +41,13 @@ class AnimePlayerView extends riv.ConsumerStatefulWidget { class _AnimePlayerViewState extends riv.ConsumerState { String? _infoHash; + HttpServer? _httpServer; @override void dispose() { if (_infoHash != null) { MTorrentServer().removeTorrent(_infoHash); } + _httpServer?.close(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); super.dispose(); @@ -58,8 +60,9 @@ class _AnimePlayerViewState extends riv.ConsumerState { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); return serversData.when( data: (data) { - final (videos, isLocal, infoHash) = data; + final (videos, isLocal, infoHash, httpServer) = data; _infoHash = infoHash; + _httpServer = httpServer; if (videos.isEmpty && !(widget.episode.manga.value!.isLocalArchive ?? false)) { return Scaffold( diff --git a/lib/modules/manga/download/providers/download_provider.dart b/lib/modules/manga/download/providers/download_provider.dart index f3e218a0..b8f42e5e 100644 --- a/lib/modules/manga/download/providers/download_provider.dart +++ b/lib/modules/manga/download/providers/download_provider.dart @@ -13,6 +13,7 @@ import 'package:mangayomi/providers/storage_provider.dart'; import 'package:mangayomi/services/get_video_list.dart'; import 'package:mangayomi/services/get_chapter_pages.dart'; import 'package:mangayomi/services/http/m_client.dart'; +import 'package:mangayomi/services/m3u8/m3u8_downloader.dart'; import 'package:mangayomi/utils/extensions/string_extensions.dart'; import 'package:mangayomi/utils/headers.dart'; import 'package:mangayomi/utils/reg_exp_matcher.dart'; @@ -47,7 +48,9 @@ Future> downloadChapter( "downloads/${isManga ? "Manga" : "Anime"}/${manga.source} (${manga.lang!.toUpperCase()})/${manga.name!.replaceForbiddenCharacters('_')}${isManga ? "/$scanlator${chapter.name!.replaceForbiddenCharacters('_')}" : ""}"; path = Directory("${path1!.path}$finalPath/"); Map videoHeader = {}; - + bool hasM3U8File = false; + bool nonM3U8File = false; + M3u8Downloader? m3u8Downloader; void savePageUrls() { final settings = isar.settings.getSync(227)!; List? chapterPageUrls = []; @@ -82,15 +85,36 @@ Future> downloadChapter( }); } else { ref - .read(getVideoListProvider( - episode: chapter, - ).future) - .then((value) { - final videosUrls = value.$1 + .read( + getVideoListProvider(episode: chapter, ignoreM3u8File: true).future) + .then((value) async { + final m3u8Urls = value.$1 + .where((element) => + element.originalUrl.endsWith(".m3u8") || + element.originalUrl.endsWith(".m3u")) + .toList(); + final nonM3u8Urls = value.$1 .where((element) => element.originalUrl.isMediaVideo()) .toList(); + nonM3U8File = nonM3u8Urls.isNotEmpty && !Platform.isIOS; + hasM3U8File = nonM3U8File ? false : m3u8Urls.isNotEmpty; + final videosUrls = nonM3U8File + ? nonM3u8Urls + : (hasM3U8File || Platform.isIOS) + ? m3u8Urls + : nonM3u8Urls; if (videosUrls.isNotEmpty) { - pageUrls = [PageUrl(videosUrls.first.url)]; + List tsList = []; + if (hasM3U8File) { + m3u8Downloader = M3u8Downloader( + m3u8Url: videosUrls.first.url, + downloadDir: "${path!.path}$chapterName", + headers: videosUrls.first.headers ?? {}); + tsList = await m3u8Downloader!.getTsList(); + } + pageUrls = hasM3U8File + ? [...tsList.map((e) => PageUrl(e.url))] + : [PageUrl(videosUrls.first.url)]; videoHeader.addAll(videosUrls.first.headers ?? {}); isOk = true; } @@ -202,6 +226,28 @@ Future> downloadChapter( if (file.existsSync()) { await file.copy("${path.path}$chapterName.mp4"); await file.delete(); + } else if (hasM3U8File) { + final tempFile = File( + "${tempDir.path}/Mangayomi/$finalPath/$chapterName/TS_${index + 1}.ts"); + final file = File("${path.path}$chapterName/TS_${index + 1}.ts"); + if (tempFile.existsSync()) { + await tempFile + .copy("${path.path}$chapterName/TS_${index + 1}.ts"); + await tempFile.delete(); + } else if (file.existsSync()) { + } else { + tasks.add(DownloadTask( + taskId: page.url, + headers: pageHeaders, + url: page.url.trim().trimLeft().trimRight(), + filename: "TS_${index + 1}.ts", + baseDirectory: BaseDirectory.temporary, + directory: 'Mangayomi/$finalPath/$chapterName/', + updates: Updates.statusAndProgress, + allowPause: true, + retries: 3, + requiresWiFi: onlyOnWifi)); + } } else { if ((await path.exists())) { if (await File("${path.path}$chapterName.mp4").exists()) { @@ -259,13 +305,19 @@ Future> downloadChapter( await FileDownloader().downloadBatch( tasks, batchProgressCallback: (succeeded, failed) async { - if (isManga) { + if (isManga || hasM3U8File) { if (succeeded == tasks.length) { - savePageUrls(); - if (ref.watch(saveAsCBZArchiveStateProvider)) { - await ref.watch(convertToCBZProvider(path!.path, mangaDir.path, - chapter.name!, pageUrls.map((e) => e.url).toList()) - .future); + if (hasM3U8File) { + } else { + savePageUrls(); + if (ref.watch(saveAsCBZArchiveStateProvider)) { + await ref.watch(convertToCBZProvider( + path!.path, + mangaDir.path, + chapter.name!, + pageUrls.map((e) => e.url).toList()) + .future); + } } } bool isEmpty = isar.downloads @@ -301,7 +353,7 @@ Future> downloadChapter( }, taskProgressCallback: (taskProgress) async { final progress = taskProgress.progress; - if (!isManga) { + if (!isManga && !hasM3U8File) { bool isEmpty = isar.downloads .filter() .chapterIdEqualTo(chapter.id!) @@ -335,7 +387,8 @@ Future> downloadChapter( if (progress == 1.0) { final file = File( "${tempDir.path}/${taskProgress.task.directory}/${taskProgress.task.filename}"); - await file.copy("${path!.path}${taskProgress.task.filename}"); + await file.copy( + "${path!.path}${hasM3U8File ? "$chapterName/" : ""}${taskProgress.task.filename}"); await file.delete(); } }, diff --git a/lib/modules/manga/download/providers/download_provider.g.dart b/lib/modules/manga/download/providers/download_provider.g.dart index 8900d397..17848c5c 100644 --- a/lib/modules/manga/download/providers/download_provider.g.dart +++ b/lib/modules/manga/download/providers/download_provider.g.dart @@ -6,7 +6,7 @@ part of 'download_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$downloadChapterHash() => r'eca8ccbe5f93f07c3471af81355fe9b3a8ec11e8'; +String _$downloadChapterHash() => r'ceb6f5d311f5da585b0272a0af598532ab511adc'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/services/aniskip.g.dart b/lib/services/aniskip.g.dart index 518154bc..8671826a 100644 --- a/lib/services/aniskip.g.dart +++ b/lib/services/aniskip.g.dart @@ -6,7 +6,7 @@ part of 'aniskip.dart'; // RiverpodGenerator // ************************************************************************** -String _$aniSkipHash() => r'04cf38b827f60d846b1d8fe87e994e9876d106ff'; +String _$aniSkipHash() => r'2e5d19b025a2207ff64da7bf7908450ea9e5ff8c'; /// See also [AniSkip]. @ProviderFor(AniSkip) diff --git a/lib/services/get_video_list.dart b/lib/services/get_video_list.dart index eb191dc5..703dfe92 100644 --- a/lib/services/get_video_list.dart +++ b/lib/services/get_video_list.dart @@ -6,6 +6,7 @@ import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/models/video.dart'; import 'package:mangayomi/providers/storage_provider.dart'; +import 'package:mangayomi/services/m3u8/m3u8_server.dart'; import 'package:mangayomi/services/torrent_server.dart'; import 'package:mangayomi/utils/utils.dart'; import 'package:mangayomi/utils/extensions/string_extensions.dart'; @@ -13,20 +14,27 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'get_video_list.g.dart'; @riverpod -Future<(List