feat: add custom streaming torrent

This commit is contained in:
kodjomoustapha 2024-07-16 17:45:36 +01:00
parent 98612f3511
commit 7741beb8ac
10 changed files with 536 additions and 89 deletions

View file

@ -326,5 +326,10 @@
"edit_code": "Edit code",
"use_libass": "Enable libass",
"use_libass_info": "Use libass based subtitle rendering for native backend.",
"libass_not_disable_message": "Disable `use libass` in player settings to be able to customize the subtitles."
"libass_not_disable_message": "Disable `use libass` in player settings to be able to customize the subtitles.",
"torrent_stream": "Torrent Stream",
"add_torrent": "Add torrent",
"enter_torrent_hint_text": "Enter magnet or torrent file url",
"torrent_url": "Torrent url",
"or": "OR"
}

View file

@ -16,6 +16,7 @@ import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/library/providers/add_torrent.dart';
import 'package:mangayomi/modules/library/providers/local_archive.dart';
import 'package:mangayomi/modules/manga/detail/providers/update_manga_detail_providers.dart';
import 'package:mangayomi/modules/more/categories/providers/isar_providers.dart';
@ -1767,6 +1768,9 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
value: 1, child: Text(l10n.open_random_entry)),
PopupMenuItem<int>(
value: 2, child: Text(l10n.import)),
if (!widget.isManga)
PopupMenuItem<int>(
value: 3, child: Text(l10n.torrent_stream)),
];
},
onSelected: (value) {
@ -1786,8 +1790,13 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
mangaM: randomManga,
source: randomManga.source!);
});
} else {
}
if (value == 2) {
_importLocal(context, widget.isManga);
} else {
if (!widget.isManga) {
addTorrent(context);
}
}
}),
],
@ -1795,7 +1804,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
}
}
_importLocal(BuildContext context, bool isManga) {
void _importLocal(BuildContext context, bool isManga) {
final l10n = l10nLocalizations(context)!;
bool isLoading = false;
showDialog(
@ -1899,3 +1908,173 @@ _importLocal(BuildContext context, bool isManga) {
);
});
}
void addTorrent(BuildContext context, {Manga? manga}) {
final l10n = l10nLocalizations(context)!;
String torrentUrl = "";
bool isLoading = false;
showDialog(
context: context,
barrierDismissible: !isLoading,
builder: (context) {
return AlertDialog(
title: Text(
l10n.add_torrent,
),
content: StatefulBuilder(
builder: (context, setState) {
return Consumer(builder: (context, ref, _) {
return SizedBox(
height: 150,
child: Column(
children: [
Row(
children: [
Expanded(
child: TextFormField(
onChanged: (value) {
setState(() {
torrentUrl = value;
});
},
decoration: InputDecoration(
hintText: l10n.enter_torrent_hint_text,
labelText: l10n.torrent_url,
isDense: true,
filled: true,
fillColor: Colors.transparent,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: context.secondaryColor)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: context.secondaryColor)),
border: OutlineInputBorder(
borderSide: BorderSide(
color: context.secondaryColor))),
),
),
TextButton(
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
try {
await ref.watch(
addTorrentFromUrlOrFromFileProvider(
manga,
init: true,
url: torrentUrl)
.future);
} catch (_) {}
setState(() {
isLoading = false;
});
Navigator.pop(context);
},
child: Text(l10n.add))
],
),
Padding(
padding: const EdgeInsets.all(20),
child: Text(l10n.or),
),
Stack(
children: [
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(3),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10))),
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
try {
await ref.watch(
addTorrentFromUrlOrFromFileProvider(
manga,
init: true)
.future);
} catch (_) {}
setState(() {
isLoading = false;
});
Navigator.pop(context);
},
child: Column(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly,
children: [
const Icon(Icons.archive_outlined),
Text("import .torrent file",
style: TextStyle(
color: Theme.of(context)
.textTheme
.bodySmall!
.color,
fontSize: 10))
],
),
),
),
),
],
),
if (isLoading)
Positioned.fill(
child: Container(
width: 300,
height: 150,
color: Colors.transparent,
child: UnconstrainedBox(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Theme.of(context)
.scaffoldBackgroundColor,
),
height: 50,
width: 50,
child: const Center(
child: ProgressCenter())),
),
),
)
],
),
],
),
);
});
},
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel)),
const SizedBox(
width: 15,
),
],
)
],
);
});
}

View file

@ -0,0 +1,82 @@
import 'package:file_picker/file_picker.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/services/torrent_server.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'add_torrent.g.dart';
@riverpod
Future addTorrentFromUrlOrFromFile(
AddTorrentFromUrlOrFromFileRef ref, Manga? mManga,
{required bool init, String? url}) async {
FilePickerResult? result;
if (url == null) {
result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: ['torrent']);
}
if (result != null || url != null) {
String torrentName = "";
if (url != null) {
torrentName = (await MTorrentServer().getTorrentPlaylist(url, null))
.$1
.first
.quality;
}
final dateNow = DateTime.now().millisecondsSinceEpoch;
final manga = mManga ??
Manga(
favorite: true,
source: 'torrent',
author: '',
isManga: false,
genre: [],
imageUrl: '',
lang: '',
link: '',
name: url != null ? torrentName : _getName(result!.files.first.path!),
dateAdded: dateNow,
lastUpdate: dateNow,
status: Status.unknown,
description: '',
isLocalArchive: true,
artist: '',
);
if (url != null) {
manga.customCoverImage = null;
isar.writeTxnSync(() {
isar.mangas.putSync(manga);
final chapters = Chapter(name: torrentName, url: url, mangaId: manga.id)
..manga.value = manga;
isar.chapters.putSync(chapters);
chapters.manga.saveSync();
});
} else {
for (var file in result!.files.reversed.toList()) {
String name = _getName(file.path!);
if (init) {
manga.customCoverImage = null;
}
isar.writeTxnSync(() {
isar.mangas.putSync(manga);
final chapters =
Chapter(name: name, archivePath: file.path, mangaId: manga.id)
..manga.value = manga;
isar.chapters.putSync(chapters);
chapters.manga.saveSync();
});
}
}
}
return "";
}
String _getName(String path) {
return path.split('/').last.split("\\").last.replaceAll(
RegExp(r'\.(mp4|mov|avi|flv|wmv|mpeg|mkv|cbz|zip|cbt|tar|torrent)'), '');
}

View file

@ -0,0 +1,194 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'add_torrent.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$addTorrentFromUrlOrFromFileHash() =>
r'473a3494fd8c5089afdd460637f37faf2a498400';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [addTorrentFromUrlOrFromFile].
@ProviderFor(addTorrentFromUrlOrFromFile)
const addTorrentFromUrlOrFromFileProvider = AddTorrentFromUrlOrFromFileFamily();
/// See also [addTorrentFromUrlOrFromFile].
class AddTorrentFromUrlOrFromFileFamily extends Family<AsyncValue> {
/// See also [addTorrentFromUrlOrFromFile].
const AddTorrentFromUrlOrFromFileFamily();
/// See also [addTorrentFromUrlOrFromFile].
AddTorrentFromUrlOrFromFileProvider call(
Manga? mManga, {
required bool init,
String? url,
}) {
return AddTorrentFromUrlOrFromFileProvider(
mManga,
init: init,
url: url,
);
}
@override
AddTorrentFromUrlOrFromFileProvider getProviderOverride(
covariant AddTorrentFromUrlOrFromFileProvider provider,
) {
return call(
provider.mManga,
init: provider.init,
url: provider.url,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'addTorrentFromUrlOrFromFileProvider';
}
/// See also [addTorrentFromUrlOrFromFile].
class AddTorrentFromUrlOrFromFileProvider
extends AutoDisposeFutureProvider<Object?> {
/// See also [addTorrentFromUrlOrFromFile].
AddTorrentFromUrlOrFromFileProvider(
Manga? mManga, {
required bool init,
String? url,
}) : this._internal(
(ref) => addTorrentFromUrlOrFromFile(
ref as AddTorrentFromUrlOrFromFileRef,
mManga,
init: init,
url: url,
),
from: addTorrentFromUrlOrFromFileProvider,
name: r'addTorrentFromUrlOrFromFileProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$addTorrentFromUrlOrFromFileHash,
dependencies: AddTorrentFromUrlOrFromFileFamily._dependencies,
allTransitiveDependencies:
AddTorrentFromUrlOrFromFileFamily._allTransitiveDependencies,
mManga: mManga,
init: init,
url: url,
);
AddTorrentFromUrlOrFromFileProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.mManga,
required this.init,
required this.url,
}) : super.internal();
final Manga? mManga;
final bool init;
final String? url;
@override
Override overrideWith(
FutureOr<Object?> Function(AddTorrentFromUrlOrFromFileRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AddTorrentFromUrlOrFromFileProvider._internal(
(ref) => create(ref as AddTorrentFromUrlOrFromFileRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
mManga: mManga,
init: init,
url: url,
),
);
}
@override
AutoDisposeFutureProviderElement<Object?> createElement() {
return _AddTorrentFromUrlOrFromFileProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AddTorrentFromUrlOrFromFileProvider &&
other.mManga == mManga &&
other.init == init &&
other.url == url;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, mManga.hashCode);
hash = _SystemHash.combine(hash, init.hashCode);
hash = _SystemHash.combine(hash, url.hashCode);
return _SystemHash.finish(hash);
}
}
mixin AddTorrentFromUrlOrFromFileRef on AutoDisposeFutureProviderRef<Object?> {
/// The parameter `mManga` of this provider.
Manga? get mManga;
/// The parameter `init` of this provider.
bool get init;
/// The parameter `url` of this provider.
String? get url;
}
class _AddTorrentFromUrlOrFromFileProviderElement
extends AutoDisposeFutureProviderElement<Object?>
with AddTorrentFromUrlOrFromFileRef {
_AddTorrentFromUrlOrFromFileProviderElement(super.provider);
@override
Manga? get mManga => (origin as AddTorrentFromUrlOrFromFileProvider).mManga;
@override
bool get init => (origin as AddTorrentFromUrlOrFromFileProvider).init;
@override
String? get url => (origin as AddTorrentFromUrlOrFromFileProvider).url;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -15,6 +15,7 @@ import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'package:mangayomi/models/track_search.dart';
import 'package:mangayomi/modules/library/library_screen.dart';
import 'package:mangayomi/modules/library/providers/local_archive.dart';
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
import 'package:mangayomi/modules/manga/detail/widgets/tracker_search_widget.dart';
@ -624,14 +625,24 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
.secondaryColor),
),
onPressed: () async {
await ref.watch(importArchivesFromFileProvider(
isManga: widget
.manga!
.isManga!,
widget
.manga,
init: false)
.future);
final manga =
widget.manga;
if ((manga!.isLocalArchive ??
false) &&
manga.source ==
"torrent") {
addTorrent(
context,
manga: manga);
} else {
await ref.watch(importArchivesFromFileProvider(
isManga: manga
.isManga!,
manga,
init:
false)
.future);
}
},
)
],

View file

@ -125,7 +125,7 @@ final aniSkipTimeoutLengthStateProvider =
);
typedef _$AniSkipTimeoutLengthState = AutoDisposeNotifier<int>;
String _$useLibassStateHash() => r'09c661f72c8777f360f48f2203d767b9caf6e4e7';
String _$useLibassStateHash() => r'91e5bbde72651f57f8775bf0fec14145ea42ced6';
/// See also [UseLibassState].
@ProviderFor(UseLibassState)

View file

@ -18,24 +18,24 @@ Future<(List<Video>, bool, String?)> getVideoList(
}) async {
final storageProvider = StorageProvider();
final mangaDirectory = await storageProvider.getMangaMainDirectory(episode);
final isLocalArchive = episode.manga.value!.isLocalArchive!;
final isLocalArchive = episode.manga.value!.isLocalArchive! &&
episode.manga.value!.source != "torrent";
final mp4animePath = "${mangaDirectory!.path}${episode.name}.mp4";
if (await File(mp4animePath).exists() || isLocalArchive) {
final path = isLocalArchive ? episode.archivePath : mp4animePath;
return ([Video(path!, episode.name!, path, subtitles: [])], true, null);
}
final source =
getSource(episode.manga.value!.lang!, episode.manga.value!.source!)!;
getSource(episode.manga.value!.lang!, episode.manga.value!.source!);
if (source.isTorrent) {
final (videos, infohash) =
await MTorrentServer().getTorrentPlaylist(episode.url!);
if (source?.isTorrent ?? false || episode.manga.value!.source == "torrent") {
final (videos, infohash) = await MTorrentServer()
.getTorrentPlaylist(episode.url, episode.archivePath);
return (videos, false, infohash);
}
List<Video> list = [];
if (source.sourceCodeLanguage == SourceCodeLanguage.dart) {
if (source?.sourceCodeLanguage == SourceCodeLanguage.dart) {
list = await DartExtensionService(source).getVideoList(episode.url!);
} else {
list = await JsExtensionService(source).getVideoList(episode.url!);

View file

@ -6,7 +6,7 @@ part of 'get_video_list.dart';
// RiverpodGenerator
// **************************************************************************
String _$getVideoListHash() => r'c95e1f62e30989547ee977fdd9faad84f49673fd';
String _$getVideoListHash() => r'46918505b5cf3401ea9f41a5c287f8746b93b1b8';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -10,9 +10,6 @@ import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
import 'package:mangayomi/ffi/torrent_server_ffi.dart' as libmtorrentserver_ffi;
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'torrent_server.g.dart';
class MTorrentServer {
final http = MClient.init();
Future<bool> removeTorrent(String? inforHash) async {
@ -42,9 +39,11 @@ class MTorrentServer {
}
}
Future<String> getInfohash(String url) async {
Future<String> getInfohash(String url, bool isFilePath) async {
try {
final torrentByte = (await http.get(Uri.parse(url))).bodyBytes;
final torrentByte = isFilePath
? File(url).readAsBytesSync()
: (await http.get(Uri.parse(url))).bodyBytes;
var request =
MultipartRequest('POST', Uri.parse('$_baseUrl/torrent/add'));
@ -57,45 +56,52 @@ class MTorrentServer {
}
}
Future<(List<Video>, String?)> getTorrentPlaylist(String url) async {
final isRunning = await check();
if (!isRunning) {
final path = (await StorageProvider().getBtDirectory())!.path;
final config = jsonEncode({"path": path, "address": "127.0.0.1:0"});
int port = 0;
if (Platform.isAndroid || Platform.isIOS) {
const channel =
MethodChannel('com.kodjodevf.mangayomi.libmtorrentserver');
port = await channel.invokeMethod('start', {"config": config});
Future<(List<Video>, String?)> getTorrentPlaylist(
String? url, String? archivePath) async {
try {
final isFilePath = archivePath?.isNotEmpty ?? false;
final isRunning = await check();
if (!isRunning) {
final path = (await StorageProvider().getBtDirectory())!.path;
final config = jsonEncode({"path": path, "address": "127.0.0.1:0"});
int port = 0;
if (Platform.isAndroid || Platform.isIOS) {
const channel =
MethodChannel('com.kodjodevf.mangayomi.libmtorrentserver');
port = await channel.invokeMethod('start', {"config": config});
} else {
port = await Isolate.run(() async {
return libmtorrentserver_ffi.start(config);
});
}
_setBtServerPort(port);
}
url = isFilePath ? archivePath! : url!;
bool isMagnet = url.startsWith("magnet:?");
String finalUrl = "";
String? infohash;
if (!isMagnet) {
infohash = await getInfohash(url, isFilePath);
finalUrl = "$_baseUrl/torrent/play?infohash=$infohash";
} else {
port = await Isolate.run(() async {
return libmtorrentserver_ffi.start(config);
});
finalUrl = "$_baseUrl/torrent/play?magnet=$url";
}
_setBtServerPort(port);
}
bool isMagnet = !url.startsWith("http");
String finalUrl = "";
String? infohash;
if (!isMagnet) {
infohash = await getInfohash(url);
finalUrl = "$_baseUrl/torrent/play?infohash=$infohash";
} else {
finalUrl = "$_baseUrl/torrent/play?magnet=$url";
}
final masterPlaylist = (await http.get(Uri.parse(finalUrl))).body;
final videoList = <Video>[];
const separator = "#EXTINF:";
for (var e in masterPlaylist.substringAfter(separator).split(separator)) {
final fileName = e.substringAfter("-1,").substringBefore("\n");
if (fileName.isMediaVideo()) {
var videoUrl = e.substringAfter("\n").substringBefore("\n");
videoList.add(Video(videoUrl, fileName, videoUrl));
final masterPlaylist = (await http.get(Uri.parse(finalUrl))).body;
final videoList = <Video>[];
const separator = "#EXTINF:";
for (var e in masterPlaylist.substringAfter(separator).split(separator)) {
final fileName = e.substringAfter("-1,").substringBefore("\n");
if (fileName.isMediaVideo()) {
var videoUrl = e.substringAfter("\n").substringBefore("\n");
videoList.add(Video(videoUrl, fileName, videoUrl));
}
}
}
return (videoList, infohash);
return (videoList, infohash);
} catch (e) {
rethrow;
}
}
}
@ -110,8 +116,3 @@ void _setBtServerPort(int newPort) {
isar.writeTxnSync(() => isar.settings
.putSync(isar.settings.getSync(227)!..btServerPort = newPort));
}
@riverpod
Future<bool> mTorrentIsRunning(MTorrentIsRunningRef ref) async {
return await MTorrentServer().check();
}

View file

@ -1,25 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'torrent_server.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$mTorrentIsRunningHash() => r'04f7fd62d1b64299c040e24721a430a25d2d6b73';
/// See also [mTorrentIsRunning].
@ProviderFor(mTorrentIsRunning)
final mTorrentIsRunningProvider = AutoDisposeFutureProvider<bool>.internal(
mTorrentIsRunning,
name: r'mTorrentIsRunningProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$mTorrentIsRunningHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef MTorrentIsRunningRef = AutoDisposeFutureProviderRef<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member