mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 21:35:32 +00:00
feat: can now download m3u8 videos
This commit is contained in:
parent
3bf1d08e0e
commit
8ed6b21125
14 changed files with 271 additions and 70 deletions
|
|
@ -41,11 +41,13 @@ class AnimePlayerView extends riv.ConsumerStatefulWidget {
|
||||||
|
|
||||||
class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
|
class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
|
||||||
String? _infoHash;
|
String? _infoHash;
|
||||||
|
HttpServer? _httpServer;
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (_infoHash != null) {
|
if (_infoHash != null) {
|
||||||
MTorrentServer().removeTorrent(_infoHash);
|
MTorrentServer().removeTorrent(_infoHash);
|
||||||
}
|
}
|
||||||
|
_httpServer?.close();
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||||
overlays: SystemUiOverlay.values);
|
overlays: SystemUiOverlay.values);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|
@ -58,8 +60,9 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
return serversData.when(
|
return serversData.when(
|
||||||
data: (data) {
|
data: (data) {
|
||||||
final (videos, isLocal, infoHash) = data;
|
final (videos, isLocal, infoHash, httpServer) = data;
|
||||||
_infoHash = infoHash;
|
_infoHash = infoHash;
|
||||||
|
_httpServer = httpServer;
|
||||||
if (videos.isEmpty &&
|
if (videos.isEmpty &&
|
||||||
!(widget.episode.manga.value!.isLocalArchive ?? false)) {
|
!(widget.episode.manga.value!.isLocalArchive ?? false)) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import 'package:mangayomi/providers/storage_provider.dart';
|
||||||
import 'package:mangayomi/services/get_video_list.dart';
|
import 'package:mangayomi/services/get_video_list.dart';
|
||||||
import 'package:mangayomi/services/get_chapter_pages.dart';
|
import 'package:mangayomi/services/get_chapter_pages.dart';
|
||||||
import 'package:mangayomi/services/http/m_client.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/extensions/string_extensions.dart';
|
||||||
import 'package:mangayomi/utils/headers.dart';
|
import 'package:mangayomi/utils/headers.dart';
|
||||||
import 'package:mangayomi/utils/reg_exp_matcher.dart';
|
import 'package:mangayomi/utils/reg_exp_matcher.dart';
|
||||||
|
|
@ -47,7 +48,9 @@ Future<List<PageUrl>> downloadChapter(
|
||||||
"downloads/${isManga ? "Manga" : "Anime"}/${manga.source} (${manga.lang!.toUpperCase()})/${manga.name!.replaceForbiddenCharacters('_')}${isManga ? "/$scanlator${chapter.name!.replaceForbiddenCharacters('_')}" : ""}";
|
"downloads/${isManga ? "Manga" : "Anime"}/${manga.source} (${manga.lang!.toUpperCase()})/${manga.name!.replaceForbiddenCharacters('_')}${isManga ? "/$scanlator${chapter.name!.replaceForbiddenCharacters('_')}" : ""}";
|
||||||
path = Directory("${path1!.path}$finalPath/");
|
path = Directory("${path1!.path}$finalPath/");
|
||||||
Map<String, String> videoHeader = {};
|
Map<String, String> videoHeader = {};
|
||||||
|
bool hasM3U8File = false;
|
||||||
|
bool nonM3U8File = false;
|
||||||
|
M3u8Downloader? m3u8Downloader;
|
||||||
void savePageUrls() {
|
void savePageUrls() {
|
||||||
final settings = isar.settings.getSync(227)!;
|
final settings = isar.settings.getSync(227)!;
|
||||||
List<ChapterPageurls>? chapterPageUrls = [];
|
List<ChapterPageurls>? chapterPageUrls = [];
|
||||||
|
|
@ -82,15 +85,36 @@ Future<List<PageUrl>> downloadChapter(
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
ref
|
ref
|
||||||
.read(getVideoListProvider(
|
.read(
|
||||||
episode: chapter,
|
getVideoListProvider(episode: chapter, ignoreM3u8File: true).future)
|
||||||
).future)
|
.then((value) async {
|
||||||
.then((value) {
|
final m3u8Urls = value.$1
|
||||||
final videosUrls = value.$1
|
.where((element) =>
|
||||||
|
element.originalUrl.endsWith(".m3u8") ||
|
||||||
|
element.originalUrl.endsWith(".m3u"))
|
||||||
|
.toList();
|
||||||
|
final nonM3u8Urls = value.$1
|
||||||
.where((element) => element.originalUrl.isMediaVideo())
|
.where((element) => element.originalUrl.isMediaVideo())
|
||||||
.toList();
|
.toList();
|
||||||
|
nonM3U8File = nonM3u8Urls.isNotEmpty && !Platform.isIOS;
|
||||||
|
hasM3U8File = nonM3U8File ? false : m3u8Urls.isNotEmpty;
|
||||||
|
final videosUrls = nonM3U8File
|
||||||
|
? nonM3u8Urls
|
||||||
|
: (hasM3U8File || Platform.isIOS)
|
||||||
|
? m3u8Urls
|
||||||
|
: nonM3u8Urls;
|
||||||
if (videosUrls.isNotEmpty) {
|
if (videosUrls.isNotEmpty) {
|
||||||
pageUrls = [PageUrl(videosUrls.first.url)];
|
List<TsInfo> 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 ?? {});
|
videoHeader.addAll(videosUrls.first.headers ?? {});
|
||||||
isOk = true;
|
isOk = true;
|
||||||
}
|
}
|
||||||
|
|
@ -202,6 +226,28 @@ Future<List<PageUrl>> downloadChapter(
|
||||||
if (file.existsSync()) {
|
if (file.existsSync()) {
|
||||||
await file.copy("${path.path}$chapterName.mp4");
|
await file.copy("${path.path}$chapterName.mp4");
|
||||||
await file.delete();
|
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 {
|
} else {
|
||||||
if ((await path.exists())) {
|
if ((await path.exists())) {
|
||||||
if (await File("${path.path}$chapterName.mp4").exists()) {
|
if (await File("${path.path}$chapterName.mp4").exists()) {
|
||||||
|
|
@ -259,13 +305,19 @@ Future<List<PageUrl>> downloadChapter(
|
||||||
await FileDownloader().downloadBatch(
|
await FileDownloader().downloadBatch(
|
||||||
tasks,
|
tasks,
|
||||||
batchProgressCallback: (succeeded, failed) async {
|
batchProgressCallback: (succeeded, failed) async {
|
||||||
if (isManga) {
|
if (isManga || hasM3U8File) {
|
||||||
if (succeeded == tasks.length) {
|
if (succeeded == tasks.length) {
|
||||||
savePageUrls();
|
if (hasM3U8File) {
|
||||||
if (ref.watch(saveAsCBZArchiveStateProvider)) {
|
} else {
|
||||||
await ref.watch(convertToCBZProvider(path!.path, mangaDir.path,
|
savePageUrls();
|
||||||
chapter.name!, pageUrls.map((e) => e.url).toList())
|
if (ref.watch(saveAsCBZArchiveStateProvider)) {
|
||||||
.future);
|
await ref.watch(convertToCBZProvider(
|
||||||
|
path!.path,
|
||||||
|
mangaDir.path,
|
||||||
|
chapter.name!,
|
||||||
|
pageUrls.map((e) => e.url).toList())
|
||||||
|
.future);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bool isEmpty = isar.downloads
|
bool isEmpty = isar.downloads
|
||||||
|
|
@ -301,7 +353,7 @@ Future<List<PageUrl>> downloadChapter(
|
||||||
},
|
},
|
||||||
taskProgressCallback: (taskProgress) async {
|
taskProgressCallback: (taskProgress) async {
|
||||||
final progress = taskProgress.progress;
|
final progress = taskProgress.progress;
|
||||||
if (!isManga) {
|
if (!isManga && !hasM3U8File) {
|
||||||
bool isEmpty = isar.downloads
|
bool isEmpty = isar.downloads
|
||||||
.filter()
|
.filter()
|
||||||
.chapterIdEqualTo(chapter.id!)
|
.chapterIdEqualTo(chapter.id!)
|
||||||
|
|
@ -335,7 +387,8 @@ Future<List<PageUrl>> downloadChapter(
|
||||||
if (progress == 1.0) {
|
if (progress == 1.0) {
|
||||||
final file = File(
|
final file = File(
|
||||||
"${tempDir.path}/${taskProgress.task.directory}/${taskProgress.task.filename}");
|
"${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();
|
await file.delete();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'download_provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$downloadChapterHash() => r'eca8ccbe5f93f07c3471af81355fe9b3a8ec11e8';
|
String _$downloadChapterHash() => r'ceb6f5d311f5da585b0272a0af598532ab511adc';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'aniskip.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$aniSkipHash() => r'04cf38b827f60d846b1d8fe87e994e9876d106ff';
|
String _$aniSkipHash() => r'2e5d19b025a2207ff64da7bf7908450ea9e5ff8c';
|
||||||
|
|
||||||
/// See also [AniSkip].
|
/// See also [AniSkip].
|
||||||
@ProviderFor(AniSkip)
|
@ProviderFor(AniSkip)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:mangayomi/models/chapter.dart';
|
||||||
import 'package:mangayomi/models/source.dart';
|
import 'package:mangayomi/models/source.dart';
|
||||||
import 'package:mangayomi/models/video.dart';
|
import 'package:mangayomi/models/video.dart';
|
||||||
import 'package:mangayomi/providers/storage_provider.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/services/torrent_server.dart';
|
||||||
import 'package:mangayomi/utils/utils.dart';
|
import 'package:mangayomi/utils/utils.dart';
|
||||||
import 'package:mangayomi/utils/extensions/string_extensions.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';
|
part 'get_video_list.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<(List<Video>, bool, String?)> getVideoList(
|
Future<(List<Video>, bool, String?, HttpServer?)> getVideoList(
|
||||||
GetVideoListRef ref, {
|
GetVideoListRef ref,
|
||||||
required Chapter episode,
|
{required Chapter episode,
|
||||||
}) async {
|
bool ignoreM3u8File = false}) async {
|
||||||
final storageProvider = StorageProvider();
|
final storageProvider = StorageProvider();
|
||||||
final mangaDirectory = await storageProvider.getMangaMainDirectory(episode);
|
final mangaDirectory = await storageProvider.getMangaMainDirectory(episode);
|
||||||
final isLocalArchive = episode.manga.value!.isLocalArchive! &&
|
final isLocalArchive = episode.manga.value!.isLocalArchive! &&
|
||||||
episode.manga.value!.source != "torrent";
|
episode.manga.value!.source != "torrent";
|
||||||
final mp4animePath =
|
final mp4animePath =
|
||||||
"${mangaDirectory!.path}${episode.name!.replaceForbiddenCharacters(' ')}.mp4";
|
"${mangaDirectory!.path}${episode.name!.replaceForbiddenCharacters(' ')}.mp4";
|
||||||
|
final episodeFolderPath =
|
||||||
|
"${mangaDirectory.path}${episode.name!.replaceForbiddenCharacters(' ')}";
|
||||||
|
|
||||||
if (await File(mp4animePath).exists() || isLocalArchive) {
|
if (await File(mp4animePath).exists() || isLocalArchive) {
|
||||||
final path = isLocalArchive ? episode.archivePath : mp4animePath;
|
final path = isLocalArchive ? episode.archivePath : mp4animePath;
|
||||||
return ([Video(path!, episode.name!, path, subtitles: [])], true, null);
|
return (
|
||||||
|
[Video(path!, episode.name!, path, subtitles: [])],
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
final source =
|
final source =
|
||||||
getSource(episode.manga.value!.lang!, episode.manga.value!.source!);
|
getSource(episode.manga.value!.lang!, episode.manga.value!.source!);
|
||||||
|
|
@ -34,7 +42,19 @@ Future<(List<Video>, bool, String?)> getVideoList(
|
||||||
if (source?.isTorrent ?? false || episode.manga.value!.source == "torrent") {
|
if (source?.isTorrent ?? false || episode.manga.value!.source == "torrent") {
|
||||||
final (videos, infohash) = await MTorrentServer()
|
final (videos, infohash) = await MTorrentServer()
|
||||||
.getTorrentPlaylist(episode.url, episode.archivePath);
|
.getTorrentPlaylist(episode.url, episode.archivePath);
|
||||||
return (videos, false, infohash);
|
return (videos, false, infohash, null);
|
||||||
|
}
|
||||||
|
if (File("$episodeFolderPath/index.m3u8").existsSync() && !ignoreM3u8File) {
|
||||||
|
const indexUrl = "http://localhost:3000/index.m3u8";
|
||||||
|
final httpServer = await m3u8Server(
|
||||||
|
m3u8Content: File("$episodeFolderPath/index.m3u8").readAsStringSync(),
|
||||||
|
episodeFolderPath: episodeFolderPath);
|
||||||
|
return (
|
||||||
|
[Video(indexUrl, episode.name!, indexUrl)],
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
httpServer
|
||||||
|
);
|
||||||
}
|
}
|
||||||
List<Video> list = [];
|
List<Video> list = [];
|
||||||
if (source?.sourceCodeLanguage == SourceCodeLanguage.dart) {
|
if (source?.sourceCodeLanguage == SourceCodeLanguage.dart) {
|
||||||
|
|
@ -48,5 +68,5 @@ Future<(List<Video>, bool, String?)> getVideoList(
|
||||||
videos.add(video);
|
videos.add(video);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (videos, false, null);
|
return (videos, false, null, null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'get_video_list.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$getVideoListHash() => r'46918505b5cf3401ea9f41a5c287f8746b93b1b8';
|
String _$getVideoListHash() => r'2002f381edbe8c3c5e8a00826b3d9aaf49410e57';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
@ -35,16 +35,18 @@ const getVideoListProvider = GetVideoListFamily();
|
||||||
|
|
||||||
/// See also [getVideoList].
|
/// See also [getVideoList].
|
||||||
class GetVideoListFamily
|
class GetVideoListFamily
|
||||||
extends Family<AsyncValue<(List<Video>, bool, String?)>> {
|
extends Family<AsyncValue<(List<Video>, bool, String?, HttpServer?)>> {
|
||||||
/// See also [getVideoList].
|
/// See also [getVideoList].
|
||||||
const GetVideoListFamily();
|
const GetVideoListFamily();
|
||||||
|
|
||||||
/// See also [getVideoList].
|
/// See also [getVideoList].
|
||||||
GetVideoListProvider call({
|
GetVideoListProvider call({
|
||||||
required Chapter episode,
|
required Chapter episode,
|
||||||
|
bool ignoreM3u8File = false,
|
||||||
}) {
|
}) {
|
||||||
return GetVideoListProvider(
|
return GetVideoListProvider(
|
||||||
episode: episode,
|
episode: episode,
|
||||||
|
ignoreM3u8File: ignoreM3u8File,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +56,7 @@ class GetVideoListFamily
|
||||||
) {
|
) {
|
||||||
return call(
|
return call(
|
||||||
episode: provider.episode,
|
episode: provider.episode,
|
||||||
|
ignoreM3u8File: provider.ignoreM3u8File,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,15 +76,17 @@ class GetVideoListFamily
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See also [getVideoList].
|
/// See also [getVideoList].
|
||||||
class GetVideoListProvider
|
class GetVideoListProvider extends AutoDisposeFutureProvider<
|
||||||
extends AutoDisposeFutureProvider<(List<Video>, bool, String?)> {
|
(List<Video>, bool, String?, HttpServer?)> {
|
||||||
/// See also [getVideoList].
|
/// See also [getVideoList].
|
||||||
GetVideoListProvider({
|
GetVideoListProvider({
|
||||||
required Chapter episode,
|
required Chapter episode,
|
||||||
|
bool ignoreM3u8File = false,
|
||||||
}) : this._internal(
|
}) : this._internal(
|
||||||
(ref) => getVideoList(
|
(ref) => getVideoList(
|
||||||
ref as GetVideoListRef,
|
ref as GetVideoListRef,
|
||||||
episode: episode,
|
episode: episode,
|
||||||
|
ignoreM3u8File: ignoreM3u8File,
|
||||||
),
|
),
|
||||||
from: getVideoListProvider,
|
from: getVideoListProvider,
|
||||||
name: r'getVideoListProvider',
|
name: r'getVideoListProvider',
|
||||||
|
|
@ -93,6 +98,7 @@ class GetVideoListProvider
|
||||||
allTransitiveDependencies:
|
allTransitiveDependencies:
|
||||||
GetVideoListFamily._allTransitiveDependencies,
|
GetVideoListFamily._allTransitiveDependencies,
|
||||||
episode: episode,
|
episode: episode,
|
||||||
|
ignoreM3u8File: ignoreM3u8File,
|
||||||
);
|
);
|
||||||
|
|
||||||
GetVideoListProvider._internal(
|
GetVideoListProvider._internal(
|
||||||
|
|
@ -103,13 +109,16 @@ class GetVideoListProvider
|
||||||
required super.debugGetCreateSourceHash,
|
required super.debugGetCreateSourceHash,
|
||||||
required super.from,
|
required super.from,
|
||||||
required this.episode,
|
required this.episode,
|
||||||
|
required this.ignoreM3u8File,
|
||||||
}) : super.internal();
|
}) : super.internal();
|
||||||
|
|
||||||
final Chapter episode;
|
final Chapter episode;
|
||||||
|
final bool ignoreM3u8File;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Override overrideWith(
|
Override overrideWith(
|
||||||
FutureOr<(List<Video>, bool, String?)> Function(GetVideoListRef provider)
|
FutureOr<(List<Video>, bool, String?, HttpServer?)> Function(
|
||||||
|
GetVideoListRef provider)
|
||||||
create,
|
create,
|
||||||
) {
|
) {
|
||||||
return ProviderOverride(
|
return ProviderOverride(
|
||||||
|
|
@ -122,43 +131,51 @@ class GetVideoListProvider
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
debugGetCreateSourceHash: null,
|
debugGetCreateSourceHash: null,
|
||||||
episode: episode,
|
episode: episode,
|
||||||
|
ignoreM3u8File: ignoreM3u8File,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AutoDisposeFutureProviderElement<(List<Video>, bool, String?)>
|
AutoDisposeFutureProviderElement<(List<Video>, bool, String?, HttpServer?)>
|
||||||
createElement() {
|
createElement() {
|
||||||
return _GetVideoListProviderElement(this);
|
return _GetVideoListProviderElement(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return other is GetVideoListProvider && other.episode == episode;
|
return other is GetVideoListProvider &&
|
||||||
|
other.episode == episode &&
|
||||||
|
other.ignoreM3u8File == ignoreM3u8File;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
hash = _SystemHash.combine(hash, episode.hashCode);
|
hash = _SystemHash.combine(hash, episode.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, ignoreM3u8File.hashCode);
|
||||||
|
|
||||||
return _SystemHash.finish(hash);
|
return _SystemHash.finish(hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin GetVideoListRef
|
mixin GetVideoListRef
|
||||||
on AutoDisposeFutureProviderRef<(List<Video>, bool, String?)> {
|
on AutoDisposeFutureProviderRef<(List<Video>, bool, String?, HttpServer?)> {
|
||||||
/// The parameter `episode` of this provider.
|
/// The parameter `episode` of this provider.
|
||||||
Chapter get episode;
|
Chapter get episode;
|
||||||
|
|
||||||
|
/// The parameter `ignoreM3u8File` of this provider.
|
||||||
|
bool get ignoreM3u8File;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GetVideoListProviderElement
|
class _GetVideoListProviderElement extends AutoDisposeFutureProviderElement<
|
||||||
extends AutoDisposeFutureProviderElement<(List<Video>, bool, String?)>
|
(List<Video>, bool, String?, HttpServer?)> with GetVideoListRef {
|
||||||
with GetVideoListRef {
|
|
||||||
_GetVideoListProviderElement(super.provider);
|
_GetVideoListProviderElement(super.provider);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Chapter get episode => (origin as GetVideoListProvider).episode;
|
Chapter get episode => (origin as GetVideoListProvider).episode;
|
||||||
|
@override
|
||||||
|
bool get ignoreM3u8File => (origin as GetVideoListProvider).ignoreM3u8File;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
|
|
|
||||||
64
lib/services/m3u8/m3u8_downloader.dart
Normal file
64
lib/services/m3u8/m3u8_downloader.dart
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:mangayomi/services/http/m_client.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
|
class TsInfo {
|
||||||
|
final String name;
|
||||||
|
final String url;
|
||||||
|
TsInfo(this.name, this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
class M3u8Downloader {
|
||||||
|
final String m3u8Url;
|
||||||
|
final String downloadDir;
|
||||||
|
final Map<String, String>? headers;
|
||||||
|
M3u8Downloader(
|
||||||
|
{required this.m3u8Url,
|
||||||
|
required this.downloadDir,
|
||||||
|
required this.headers});
|
||||||
|
|
||||||
|
Future<List<TsInfo>> getTsList() async {
|
||||||
|
final uri = Uri.parse(m3u8Url);
|
||||||
|
final m3u8Host = "${uri.scheme}://${uri.host}${path.dirname(uri.path)}";
|
||||||
|
final m3u8Body = await _getM3u8Body(m3u8Url, headers);
|
||||||
|
final tsList = _parseTsList(m3u8Host, m3u8Body);
|
||||||
|
if (kDebugMode) {
|
||||||
|
print("Total TS files to download: ${tsList.length}");
|
||||||
|
}
|
||||||
|
return tsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _getM3u8Body(String url, Map<String, String>? headers) async {
|
||||||
|
final response =
|
||||||
|
await MClient.httpClient().get(Uri.parse(url), headers: headers);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return response.body;
|
||||||
|
} else {
|
||||||
|
throw Exception("Failed to load m3u8 body");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TsInfo> _parseTsList(String host, String body) {
|
||||||
|
final lines = body.split("\n");
|
||||||
|
final tsList = <TsInfo>[];
|
||||||
|
int index = 0;
|
||||||
|
String allText = "";
|
||||||
|
for (final line in lines) {
|
||||||
|
if (!line.startsWith("#") && line.isNotEmpty) {
|
||||||
|
index++;
|
||||||
|
final tsUrl = line.startsWith("http")
|
||||||
|
? line
|
||||||
|
: "$host/${line.replaceFirst("/", "")}";
|
||||||
|
allText += "http://localhost:3000/TS_$index.ts\n";
|
||||||
|
tsList.add(TsInfo("TS_$index", tsUrl));
|
||||||
|
} else {
|
||||||
|
allText += "$line\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Directory(downloadDir).createSync(recursive: true);
|
||||||
|
File("$downloadDir/index.m3u8").writeAsStringSync(allText);
|
||||||
|
return tsList;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
lib/services/m3u8/m3u8_server.dart
Normal file
41
lib/services/m3u8/m3u8_server.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:shelf/shelf.dart';
|
||||||
|
import 'package:shelf/shelf_io.dart' as io;
|
||||||
|
|
||||||
|
Future<HttpServer> m3u8Server(
|
||||||
|
{required String m3u8Content, required String episodeFolderPath}) async {
|
||||||
|
final handler =
|
||||||
|
const Pipeline().addMiddleware(logRequests()).addHandler((request) {
|
||||||
|
return _handleRequest(request, m3u8Content, episodeFolderPath);
|
||||||
|
});
|
||||||
|
final server = await io.serve(handler, 'localhost', 3000);
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(
|
||||||
|
'[INFO-M3U8_SERVER] Listening on running on http://${server.address.host}:${server.port}');
|
||||||
|
}
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response _handleRequest(
|
||||||
|
Request request, String m3u8Content, String episodeFolderPath) {
|
||||||
|
if (request.url.path == 'index.m3u8') {
|
||||||
|
return Response.ok(m3u8Content,
|
||||||
|
headers: {'Content-Type': 'application/vnd.apple.mpegurl'});
|
||||||
|
}
|
||||||
|
if (request.url.path.endsWith('.ts')) {
|
||||||
|
final tsFilePath = '$episodeFolderPath/${request.url.pathSegments.last}';
|
||||||
|
final file = File(tsFilePath);
|
||||||
|
|
||||||
|
if (file.existsSync()) {
|
||||||
|
return Response.ok(file.openRead(), headers: {
|
||||||
|
'Content-Type': 'video/MP2T',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Response.notFound('File not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.notFound('Not exist');
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'anilist.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$anilistHash() => r'4d54bf86bb2f1133e77609c94b22f4ad17837f20';
|
String _$anilistHash() => r'd3a8852d689b13c3bde46ec05b464e7779149e58';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'kitsu.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$kitsuHash() => r'b9e7867b0c059c8983189d8b94bc6d6a1c1bd3c5';
|
String _$kitsuHash() => r'6953b7520cc144f42992bbecc0d5306841c2382f';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'myanimelist.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$myAnimeListHash() => r'90ac28c6fb5ea17c085e8ffa77eddbe48ea1948d';
|
String _$myAnimeListHash() => r'd69a03e6f385688047c13771528c086542e03218';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
|
||||||
26
pubspec.lock
26
pubspec.lock
|
|
@ -5,10 +5,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "72.0.0"
|
version: "73.0.0"
|
||||||
_macros:
|
_macros:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: dart
|
description: dart
|
||||||
|
|
@ -18,10 +18,10 @@ packages:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: analyzer
|
name: analyzer
|
||||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.7.0"
|
version: "6.8.0"
|
||||||
analyzer_plugin:
|
analyzer_plugin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -215,13 +215,13 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.10.0"
|
version: "4.10.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.19.0"
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -697,10 +697,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.1.0"
|
||||||
http_profile:
|
http_profile:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -1033,7 +1033,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.1"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||||
|
|
@ -1377,13 +1377,13 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
shelf:
|
shelf:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shelf
|
name: shelf
|
||||||
sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
|
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.2"
|
||||||
shelf_web_socket:
|
shelf_web_socket:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,8 @@ dependencies:
|
||||||
path: rhttp
|
path: rhttp
|
||||||
ref: main
|
ref: main
|
||||||
cupertino_http: ^1.5.1
|
cupertino_http: ^1.5.1
|
||||||
|
shelf: ^1.4.2
|
||||||
|
path: ^1.9.0
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
http: ^1.2.1
|
http: ^1.2.1
|
||||||
|
|
@ -104,6 +106,7 @@ dependency_overrides:
|
||||||
path: media_kit_video
|
path: media_kit_video
|
||||||
ref: 50c510d018cc5286eb6730f3ea165290f19dc5f6
|
ref: 50c510d018cc5286eb6730f3ea165290f19dc5f6
|
||||||
meta: ^1.15.0
|
meta: ^1.15.0
|
||||||
|
collection: ^1.19.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
36
rust/Cargo.lock
generated
36
rust/Cargo.lock
generated
|
|
@ -697,9 +697,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lebe"
|
name = "lebe"
|
||||||
|
|
@ -709,9 +709,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.150"
|
version = "0.2.158"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libfuzzer-sys"
|
name = "libfuzzer-sys"
|
||||||
|
|
@ -1096,9 +1096,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.2"
|
version = "1.10.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -1108,9 +1108,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.3"
|
version = "0.4.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
@ -1154,18 +1154,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.203"
|
version = "1.0.209"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
|
checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.203"
|
version = "1.0.209"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1222,9 +1222,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.66"
|
version = "2.0.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1252,18 +1252,18 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.61"
|
version = "1.0.63"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.61"
|
version = "1.0.63"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue