mangayomi/lib/services/m3u8/m3u8_downloader.dart
2024-12-14 12:38:56 +02:00

180 lines
5.5 KiB
Dart

// 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;
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>, 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}");
}
final (tsKey, tsIv) = await _getM3u8KeyAndIv(m3u8Body);
if (tsKey?.isNotEmpty ?? false) {
if (kDebugMode) {
print("TS Key: $tsKey");
}
key = tsKey;
}
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(
String url,
) 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");
List<TsInfo> tsList = [];
int index = 0;
for (final line in lines) {
if (!line.startsWith("#") && line.isNotEmpty) {
index++;
final tsUrl = line.startsWith("http") ? line : "$host/${line.replaceFirst("/", "")}";
tsList.add(TsInfo("TS_$index", tsUrl));
}
}
return tsList;
}
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, 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;
}
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 {
await Isolate.run(() 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);
}
await newFile.writeAsBytes(bytes);
});
}
}