mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 17:25:32 +00:00
add support to convert downloaded m3u8 files to mp4
This commit is contained in:
parent
e180919551
commit
6e1f6c1375
11 changed files with 186 additions and 162 deletions
|
|
@ -28,7 +28,6 @@ import 'package:media_kit_video/media_kit_video.dart';
|
|||
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class AnimePlayerView extends riv.ConsumerStatefulWidget {
|
||||
|
|
@ -41,13 +40,11 @@ class AnimePlayerView extends riv.ConsumerStatefulWidget {
|
|||
|
||||
class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
|
||||
String? _infoHash;
|
||||
HttpServer? _httpServer;
|
||||
@override
|
||||
void dispose() {
|
||||
if (_infoHash != null) {
|
||||
MTorrentServer().removeTorrent(_infoHash);
|
||||
}
|
||||
_httpServer?.close();
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
overlays: SystemUiOverlay.values);
|
||||
super.dispose();
|
||||
|
|
@ -60,9 +57,8 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
|
|||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
return serversData.when(
|
||||
data: (data) {
|
||||
final (videos, isLocal, infoHash, httpServer) = data;
|
||||
final (videos, isLocal, infoHash) = data;
|
||||
_infoHash = infoHash;
|
||||
_httpServer = httpServer;
|
||||
if (videos.isEmpty &&
|
||||
!(widget.episode.manga.value!.isLocalArchive ?? false)) {
|
||||
return Scaffold(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:mangayomi/models/page.dart';
|
||||
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
|
@ -51,7 +52,23 @@ Future<List<PageUrl>> downloadChapter(
|
|||
bool hasM3U8File = false;
|
||||
bool nonM3U8File = false;
|
||||
M3u8Downloader? m3u8Downloader;
|
||||
String? tsKey;
|
||||
Uint8List? tsKey;
|
||||
Uint8List? tsIv;
|
||||
int? m3u8MediaSequence;
|
||||
|
||||
Future<void> processConvert() async {
|
||||
if (hasM3U8File) {
|
||||
await m3u8Downloader?.mergeTsToMp4(
|
||||
"${path!.path}$chapterName.mp4", "${path.path}$chapterName");
|
||||
} else {
|
||||
if (ref.watch(saveAsCBZArchiveStateProvider)) {
|
||||
await ref.watch(convertToCBZProvider(path!.path, mangaDir!.path,
|
||||
chapter.name!, pageUrls.map((e) => e.url).toList())
|
||||
.future);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void savePageUrls() {
|
||||
final settings = isar.settings.getSync(227)!;
|
||||
List<ChapterPageurls>? chapterPageUrls = [];
|
||||
|
|
@ -85,10 +102,7 @@ Future<List<PageUrl>> downloadChapter(
|
|||
}
|
||||
});
|
||||
} else {
|
||||
ref
|
||||
.read(
|
||||
getVideoListProvider(episode: chapter, ignoreM3u8File: true).future)
|
||||
.then((value) async {
|
||||
ref.read(getVideoListProvider(episode: chapter).future).then((value) async {
|
||||
final m3u8Urls = value.$1
|
||||
.where((element) =>
|
||||
element.originalUrl.endsWith(".m3u8") ||
|
||||
|
|
@ -107,7 +121,8 @@ Future<List<PageUrl>> downloadChapter(
|
|||
m3u8Url: videosUrls.first.url,
|
||||
downloadDir: "${path!.path}$chapterName",
|
||||
headers: videosUrls.first.headers ?? {});
|
||||
(tsList, tsKey) = await m3u8Downloader!.getTsList();
|
||||
(tsList, tsKey, tsIv, m3u8MediaSequence) =
|
||||
await m3u8Downloader!.getTsList();
|
||||
}
|
||||
pageUrls = hasM3U8File
|
||||
? [...tsList.map((e) => PageUrl(e.url))]
|
||||
|
|
@ -179,6 +194,7 @@ Future<List<PageUrl>> downloadChapter(
|
|||
final file = File(
|
||||
"${tempDir.path}/Mangayomi/$finalPath/${padIndex(index + 1)}.jpg");
|
||||
if (file.existsSync()) {
|
||||
Directory(path.path).createSync(recursive: true);
|
||||
await file.copy("${path.path}${padIndex(index + 1)}.jpg");
|
||||
await file.delete();
|
||||
} else {
|
||||
|
|
@ -228,6 +244,7 @@ Future<List<PageUrl>> downloadChapter(
|
|||
"${tempDir.path}/Mangayomi/$finalPath/$chapterName/TS_${index + 1}.ts");
|
||||
final file = File("${path.path}$chapterName/TS_${index + 1}.ts");
|
||||
if (tempFile.existsSync()) {
|
||||
Directory("${path.path}$chapterName").createSync(recursive: true);
|
||||
await tempFile
|
||||
.copy("${path.path}$chapterName/TS_${index + 1}.ts");
|
||||
await tempFile.delete();
|
||||
|
|
@ -284,6 +301,7 @@ Future<List<PageUrl>> downloadChapter(
|
|||
}
|
||||
|
||||
if (tasks.isEmpty && pageUrls.isNotEmpty) {
|
||||
await processConvert();
|
||||
savePageUrls();
|
||||
final download = Download(
|
||||
succeeded: 0,
|
||||
|
|
@ -299,23 +317,16 @@ Future<List<PageUrl>> downloadChapter(
|
|||
isar.downloads.putSync(download..chapter.value = chapter);
|
||||
});
|
||||
} else {
|
||||
if (hasM3U8File) {
|
||||
await Directory("${path.path}$chapterName").create(recursive: true);
|
||||
}
|
||||
savePageUrls();
|
||||
await FileDownloader().downloadBatch(
|
||||
tasks,
|
||||
batchProgressCallback: (succeeded, failed) async {
|
||||
if (isManga || hasM3U8File) {
|
||||
if (succeeded == tasks.length) {
|
||||
if (hasM3U8File) {
|
||||
} else {
|
||||
if (ref.watch(saveAsCBZArchiveStateProvider)) {
|
||||
await ref.watch(convertToCBZProvider(
|
||||
path!.path,
|
||||
mangaDir.path,
|
||||
chapter.name!,
|
||||
pageUrls.map((e) => e.url).toList())
|
||||
.future);
|
||||
}
|
||||
}
|
||||
await processConvert();
|
||||
}
|
||||
bool isEmpty = isar.downloads
|
||||
.filter()
|
||||
|
|
@ -384,9 +395,13 @@ Future<List<PageUrl>> downloadChapter(
|
|||
if (progress == 1.0) {
|
||||
final file = File(
|
||||
"${tempDir.path}/${taskProgress.task.directory}/${taskProgress.task.filename}");
|
||||
await file.copy(
|
||||
final newFile = await file.copy(
|
||||
"${path!.path}${hasM3U8File ? "$chapterName/" : ""}${taskProgress.task.filename}");
|
||||
await file.delete();
|
||||
if (hasM3U8File) {
|
||||
await m3u8Downloader?.processBytes(
|
||||
newFile, tsKey, tsIv, m3u8MediaSequence);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'download_provider.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$downloadChapterHash() => r'c5e5cf07f28a558d6dc827a1e08933125ef52261';
|
||||
String _$downloadChapterHash() => r'3209fc6c18197b4b2337d6fa9859474ec144694b';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class DownloadQueueScreen extends ConsumerWidget {
|
|||
groupSeparatorBuilder: (String groupByValue) {
|
||||
final sourceQueueLength = entries
|
||||
.where((element) =>
|
||||
element.chapter.value!.manga.value!.source! ==
|
||||
(element.chapter.value?.manga.value?.source ?? "") ==
|
||||
groupByValue)
|
||||
.toList()
|
||||
.length;
|
||||
|
|
@ -87,7 +87,8 @@ class DownloadQueueScreen extends ConsumerWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
element.chapter.value!.manga.value!.name!,
|
||||
element.chapter.value?.manga.value?.name ??
|
||||
"",
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
|
|
@ -97,7 +98,7 @@ class DownloadQueueScreen extends ConsumerWidget {
|
|||
],
|
||||
),
|
||||
Text(
|
||||
element.chapter.value!.name!,
|
||||
element.chapter.value?.name ?? "",
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
const SizedBox(
|
||||
|
|
@ -151,12 +152,12 @@ class DownloadQueueScreen extends ConsumerWidget {
|
|||
} else if (value.toString() == 'CancelAll') {
|
||||
final chapterIds = entries
|
||||
.where((e) =>
|
||||
e.chapter.value!.manga.value!.name ==
|
||||
element.chapter.value!.manga.value!
|
||||
.name &&
|
||||
e.chapter.value!.manga.value!.source ==
|
||||
element.chapter.value!.manga.value!
|
||||
.source)
|
||||
e.chapter.value?.manga.value?.name ==
|
||||
element.chapter.value?.manga.value
|
||||
?.name &&
|
||||
e.chapter.value?.manga.value?.source ==
|
||||
element.chapter.value?.manga.value
|
||||
?.source)
|
||||
.map((e) => e.chapterId)
|
||||
.toList();
|
||||
for (var chapterId in chapterIds) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ 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';
|
||||
|
|
@ -14,27 +13,18 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||
part 'get_video_list.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<(List<Video>, bool, String?, HttpServer?)> getVideoList(
|
||||
GetVideoListRef ref,
|
||||
{required Chapter episode,
|
||||
bool ignoreM3u8File = false}) async {
|
||||
Future<(List<Video>, bool, String?)> getVideoList(GetVideoListRef ref,
|
||||
{required Chapter episode}) async {
|
||||
final storageProvider = StorageProvider();
|
||||
final mangaDirectory = await storageProvider.getMangaMainDirectory(episode);
|
||||
final isLocalArchive = episode.manga.value!.isLocalArchive! &&
|
||||
episode.manga.value!.source != "torrent";
|
||||
final mp4animePath =
|
||||
"${mangaDirectory!.path}${episode.name!.replaceForbiddenCharacters(' ')}.mp4";
|
||||
final episodeFolderPath =
|
||||
"${mangaDirectory.path}${episode.name!.replaceForbiddenCharacters(' ')}";
|
||||
|
||||
if (await File(mp4animePath).exists() || isLocalArchive) {
|
||||
final path = isLocalArchive ? episode.archivePath : mp4animePath;
|
||||
return (
|
||||
[Video(path!, episode.name!, path, subtitles: [])],
|
||||
true,
|
||||
null,
|
||||
null
|
||||
);
|
||||
return ([Video(path!, episode.name!, path, subtitles: [])], true, null);
|
||||
}
|
||||
final source =
|
||||
getSource(episode.manga.value!.lang!, episode.manga.value!.source!);
|
||||
|
|
@ -42,20 +32,9 @@ Future<(List<Video>, bool, String?, HttpServer?)> getVideoList(
|
|||
if (source?.isTorrent ?? false || episode.manga.value!.source == "torrent") {
|
||||
final (videos, infohash) = await MTorrentServer()
|
||||
.getTorrentPlaylist(episode.url, episode.archivePath);
|
||||
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
|
||||
);
|
||||
return (videos, false, infohash);
|
||||
}
|
||||
|
||||
List<Video> list = [];
|
||||
if (source?.sourceCodeLanguage == SourceCodeLanguage.dart) {
|
||||
list = await DartExtensionService(source).getVideoList(episode.url!);
|
||||
|
|
@ -68,5 +47,5 @@ Future<(List<Video>, bool, String?, HttpServer?)> getVideoList(
|
|||
videos.add(video);
|
||||
}
|
||||
}
|
||||
return (videos, false, null, null);
|
||||
return (videos, false, null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'get_video_list.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getVideoListHash() => r'2002f381edbe8c3c5e8a00826b3d9aaf49410e57';
|
||||
String _$getVideoListHash() => r'e5cc579c492bdf4cd226b93c42766599cece4cd6';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
@ -35,18 +35,16 @@ const getVideoListProvider = GetVideoListFamily();
|
|||
|
||||
/// See also [getVideoList].
|
||||
class GetVideoListFamily
|
||||
extends Family<AsyncValue<(List<Video>, bool, String?, HttpServer?)>> {
|
||||
extends Family<AsyncValue<(List<Video>, bool, String?)>> {
|
||||
/// See also [getVideoList].
|
||||
const GetVideoListFamily();
|
||||
|
||||
/// See also [getVideoList].
|
||||
GetVideoListProvider call({
|
||||
required Chapter episode,
|
||||
bool ignoreM3u8File = false,
|
||||
}) {
|
||||
return GetVideoListProvider(
|
||||
episode: episode,
|
||||
ignoreM3u8File: ignoreM3u8File,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +54,6 @@ class GetVideoListFamily
|
|||
) {
|
||||
return call(
|
||||
episode: provider.episode,
|
||||
ignoreM3u8File: provider.ignoreM3u8File,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -76,17 +73,15 @@ class GetVideoListFamily
|
|||
}
|
||||
|
||||
/// See also [getVideoList].
|
||||
class GetVideoListProvider extends AutoDisposeFutureProvider<
|
||||
(List<Video>, bool, String?, HttpServer?)> {
|
||||
class GetVideoListProvider
|
||||
extends AutoDisposeFutureProvider<(List<Video>, bool, String?)> {
|
||||
/// See also [getVideoList].
|
||||
GetVideoListProvider({
|
||||
required Chapter episode,
|
||||
bool ignoreM3u8File = false,
|
||||
}) : this._internal(
|
||||
(ref) => getVideoList(
|
||||
ref as GetVideoListRef,
|
||||
episode: episode,
|
||||
ignoreM3u8File: ignoreM3u8File,
|
||||
),
|
||||
from: getVideoListProvider,
|
||||
name: r'getVideoListProvider',
|
||||
|
|
@ -98,7 +93,6 @@ class GetVideoListProvider extends AutoDisposeFutureProvider<
|
|||
allTransitiveDependencies:
|
||||
GetVideoListFamily._allTransitiveDependencies,
|
||||
episode: episode,
|
||||
ignoreM3u8File: ignoreM3u8File,
|
||||
);
|
||||
|
||||
GetVideoListProvider._internal(
|
||||
|
|
@ -109,16 +103,13 @@ class GetVideoListProvider extends AutoDisposeFutureProvider<
|
|||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.episode,
|
||||
required this.ignoreM3u8File,
|
||||
}) : super.internal();
|
||||
|
||||
final Chapter episode;
|
||||
final bool ignoreM3u8File;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<(List<Video>, bool, String?, HttpServer?)> Function(
|
||||
GetVideoListRef provider)
|
||||
FutureOr<(List<Video>, bool, String?)> Function(GetVideoListRef provider)
|
||||
create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
|
|
@ -131,51 +122,43 @@ class GetVideoListProvider extends AutoDisposeFutureProvider<
|
|||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
episode: episode,
|
||||
ignoreM3u8File: ignoreM3u8File,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<(List<Video>, bool, String?, HttpServer?)>
|
||||
AutoDisposeFutureProviderElement<(List<Video>, bool, String?)>
|
||||
createElement() {
|
||||
return _GetVideoListProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is GetVideoListProvider &&
|
||||
other.episode == episode &&
|
||||
other.ignoreM3u8File == ignoreM3u8File;
|
||||
return other is GetVideoListProvider && other.episode == episode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, episode.hashCode);
|
||||
hash = _SystemHash.combine(hash, ignoreM3u8File.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin GetVideoListRef
|
||||
on AutoDisposeFutureProviderRef<(List<Video>, bool, String?, HttpServer?)> {
|
||||
on AutoDisposeFutureProviderRef<(List<Video>, bool, String?)> {
|
||||
/// The parameter `episode` of this provider.
|
||||
Chapter get episode;
|
||||
|
||||
/// The parameter `ignoreM3u8File` of this provider.
|
||||
bool get ignoreM3u8File;
|
||||
}
|
||||
|
||||
class _GetVideoListProviderElement extends AutoDisposeFutureProviderElement<
|
||||
(List<Video>, bool, String?, HttpServer?)> with GetVideoListRef {
|
||||
class _GetVideoListProviderElement
|
||||
extends AutoDisposeFutureProviderElement<(List<Video>, bool, String?)>
|
||||
with GetVideoListRef {
|
||||
_GetVideoListProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
Chapter get episode => (origin as GetVideoListProvider).episode;
|
||||
@override
|
||||
bool get ignoreM3u8File => (origin as GetVideoListProvider).ignoreM3u8File;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ class LoggerInterceptor extends InterceptorContract {
|
|||
required BaseRequest request,
|
||||
}) async {
|
||||
Logger.add(LoggerLevel.info,
|
||||
'----- Request -----\n${request.toString()}\nheader: ${request.headers.toString()}');
|
||||
'----- Request -----\n${request.toString()}\nheaders: ${request.headers.toString()}');
|
||||
return request;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
// ignore_for_file: depend_on_referenced_packages
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mangayomi/services/http/m_client.dart';
|
||||
import 'package:mangayomi/utils/extensions/string_extensions.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||
import 'package:convert/convert.dart';
|
||||
|
||||
class TsInfo {
|
||||
final String name;
|
||||
|
|
@ -19,23 +25,37 @@ class M3u8Downloader {
|
|||
required this.downloadDir,
|
||||
required this.headers});
|
||||
|
||||
Future<(List<TsInfo>, String?)> getTsList() async {
|
||||
String? key;
|
||||
Future<(List<TsInfo>, Uint8List?, Uint8List?, int?)> getTsList() async {
|
||||
Uint8List? key;
|
||||
Uint8List? iv;
|
||||
int? mediaSequence;
|
||||
final uri = Uri.parse(m3u8Url);
|
||||
final m3u8Host = "${uri.scheme}://${uri.host}${path.dirname(uri.path)}";
|
||||
final m3u8Body = await _getM3u8Body(m3u8Url);
|
||||
final tsList = _parseTsList(m3u8Host, m3u8Body);
|
||||
mediaSequence = _extractMediaSequence(m3u8Body);
|
||||
if (kDebugMode) {
|
||||
print("Total TS files to download: ${tsList.length}");
|
||||
}
|
||||
String? tsKey = await getM3u8Key(m3u8Body);
|
||||
final (tsKey, tsIv) = await _getM3u8KeyAndIv(m3u8Body);
|
||||
if (tsKey?.isNotEmpty ?? false) {
|
||||
if (kDebugMode) {
|
||||
print("TS Key: $tsKey");
|
||||
}
|
||||
key = tsKey;
|
||||
}
|
||||
return (tsList, key);
|
||||
if (tsIv != null) {
|
||||
if (kDebugMode) {
|
||||
print("TS Iv: $tsIv");
|
||||
}
|
||||
iv = Uint8List.fromList(hex.decode(tsIv.replaceFirst("0x", "")));
|
||||
}
|
||||
if (mediaSequence != null) {
|
||||
if (kDebugMode) {
|
||||
print("Media sequence: $mediaSequence");
|
||||
}
|
||||
}
|
||||
return (tsList, key, iv, mediaSequence);
|
||||
}
|
||||
|
||||
Future<String> _getM3u8Body(
|
||||
|
|
@ -52,50 +72,122 @@ class M3u8Downloader {
|
|||
|
||||
List<TsInfo> _parseTsList(String host, String body) {
|
||||
final lines = body.split("\n");
|
||||
final tsList = <TsInfo>[];
|
||||
List<TsInfo> tsList = [];
|
||||
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;
|
||||
}
|
||||
|
||||
Future<String?> getM3u8Key(String m3u8Body) async {
|
||||
Future<(Uint8List?, String?)> _getM3u8KeyAndIv(String m3u8Body) async {
|
||||
final uri = Uri.parse(m3u8Url);
|
||||
final m3u8Host = "${uri.scheme}://${uri.host}${path.dirname(uri.path)}";
|
||||
final lines = m3u8Body.split("\n");
|
||||
for (final line in lines) {
|
||||
if (line.contains("#EXT-X-KEY")) {
|
||||
final keyUrl = _extractKeyUrl(m3u8Host, line);
|
||||
final response =
|
||||
await MClient.httpClient().get(Uri.parse(keyUrl), headers: headers);
|
||||
if (response.statusCode == 200) {
|
||||
return response.body;
|
||||
final (keyUrl, iv) = _extractKeyAttributes(line, m3u8Host);
|
||||
if (keyUrl != null) {
|
||||
final response = await MClient.httpClient()
|
||||
.get(Uri.parse(keyUrl), headers: headers);
|
||||
if (response.statusCode == 200) {
|
||||
return (response.bodyBytes, iv);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
(String?, String?) _extractKeyAttributes(String content, String host) {
|
||||
final keyPattern = RegExp(
|
||||
r'#EXT-X-KEY:METHOD=AES-128(?:,URI="([^"]+)")?(?:,IV=0x([A-F0-9]+))?',
|
||||
caseSensitive: false);
|
||||
final match = keyPattern.firstMatch(content);
|
||||
|
||||
String? uri = match?.group(1);
|
||||
if (uri != null) {
|
||||
if (!uri.contains("http")) {
|
||||
uri = "$host/$uri";
|
||||
}
|
||||
}
|
||||
final iv = match?.group(2);
|
||||
|
||||
return (uri, iv);
|
||||
}
|
||||
|
||||
Uint8List _aesDecrypt(int sequence, Uint8List encrypted, Uint8List key,
|
||||
{Uint8List? iv}) {
|
||||
if (iv == null) {
|
||||
iv = Uint8List(16);
|
||||
ByteData.view(iv.buffer).setUint64(8, sequence);
|
||||
}
|
||||
|
||||
final encrypter = encrypt.Encrypter(
|
||||
encrypt.AES(encrypt.Key(key), mode: encrypt.AESMode.cbc));
|
||||
|
||||
try {
|
||||
final decrypted = encrypter.decryptBytes(encrypt.Encrypted(encrypted),
|
||||
iv: encrypt.IV(iv));
|
||||
|
||||
return Uint8List.fromList(decrypted);
|
||||
} catch (e) {
|
||||
throw ArgumentError('Decryption failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
int? _extractMediaSequence(String content) {
|
||||
final lines = content.split('\n');
|
||||
for (var line in lines) {
|
||||
if (line.startsWith('#EXT-X-MEDIA-SEQUENCE')) {
|
||||
final sequenceStr = line.substringAfter(':');
|
||||
return int.tryParse(sequenceStr.trim());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _extractKeyUrl(String host, String line) {
|
||||
final uriPos = line.indexOf("URI");
|
||||
final quotationMarkPos = line.lastIndexOf("\"");
|
||||
var keyUrl = line.substring(uriPos, quotationMarkPos).split("\"")[1];
|
||||
if (!line.contains("http")) {
|
||||
keyUrl = "$host/$keyUrl";
|
||||
Future<void> mergeTsToMp4(String fileName, String directory) async {
|
||||
await Isolate.run(() async {
|
||||
List<String> tsPathList = [];
|
||||
final outFile = File(fileName).openWrite();
|
||||
final dir = Directory(directory);
|
||||
await for (var entity in dir.list()) {
|
||||
if (entity is File && entity.path.endsWith('.ts')) {
|
||||
tsPathList.add(entity.path);
|
||||
}
|
||||
}
|
||||
tsPathList.sort((a, b) =>
|
||||
int.parse(a.substringAfter("TS_").substringBefore(".")).compareTo(
|
||||
int.parse(b.substringAfter("TS_").substringBefore("."))));
|
||||
for (var path in tsPathList) {
|
||||
final bytes = await File(path).readAsBytes();
|
||||
outFile.add(bytes);
|
||||
}
|
||||
await outFile.flush();
|
||||
await outFile.close();
|
||||
await dir.delete(recursive: true);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> processBytes(File newFile, Uint8List? tsKey, Uint8List? tsIv,
|
||||
int? m3u8Sequence) async {
|
||||
Uint8List bytes = await newFile.readAsBytes();
|
||||
if (tsKey != null) {
|
||||
final index =
|
||||
int.parse(newFile.path.substringAfter("TS_").substringBefore("."));
|
||||
bytes = _aesDecrypt((m3u8Sequence ?? 1) + (index - 1), bytes, tsKey,
|
||||
iv: tsIv);
|
||||
}
|
||||
return keyUrl;
|
||||
|
||||
await newFile.writeAsBytes(bytes);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
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 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');
|
||||
}
|
||||
14
pubspec.lock
14
pubspec.lock
|
|
@ -58,10 +58,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: asn1lib
|
||||
sha256: "2ca377ad4d677bbadca278e0ba4da4e057b80a10b927bfc8f7d8bda8fe2ceb75"
|
||||
sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.4"
|
||||
version: "1.5.5"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -371,10 +371,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: extended_image
|
||||
sha256: c7b628bdbeb398bdd824cfc9d521f18e04b63ad2af811d182e885d3ae4ef45de
|
||||
sha256: "75235b2cf90de7663640c2da43b0549fc1f373340b7ee925696e92e8ec55c4db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.3"
|
||||
version: "8.2.4"
|
||||
extended_image_library:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -972,10 +972,10 @@ packages:
|
|||
dependency: "direct overridden"
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1360,7 +1360,7 @@ packages:
|
|||
source: hosted
|
||||
version: "4.0.0"
|
||||
shelf:
|
||||
dependency: "direct main"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ dependencies:
|
|||
rust_lib_mangayomi:
|
||||
path: rust_builder
|
||||
pseudom: ^1.0.1
|
||||
shelf: ^1.4.2
|
||||
path: ^1.9.0
|
||||
freezed_annotation: ^2.0.0
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue