Add more features vor anime player

This commit is contained in:
kodjomoustapha 2023-08-28 19:37:20 +01:00
parent 44c5a54bbf
commit 6fbbbc5bc2
24 changed files with 565 additions and 72 deletions

View file

@ -74,6 +74,14 @@ class VideoModel {
String? quality;
String? originalUrl;
Map<String, String>? headers;
VideoModel({this.url, this.quality, this.originalUrl, this.headers});
List<TrackModel>? subtitles;
VideoModel(
{this.url, this.quality, this.originalUrl, this.headers, this.subtitles});
}
class TrackModel {
String? file;
String? label;
TrackModel({this.file, this.label});
}

View file

@ -0,0 +1,91 @@
import 'package:dart_eval/dart_eval.dart';
import 'package:dart_eval/dart_eval_bridge.dart';
import 'package:dart_eval/stdlib/core.dart';
import 'package:mangayomi/eval/bridge_class/model.dart';
class $TrackModel implements TrackModel, $Instance {
$TrackModel.wrap(this.$value) : _superclass = $Object($value);
static const $type = BridgeTypeRef(
BridgeTypeSpec('package:bridge_lib/bridge_lib.dart', 'TrackModel'));
static const $declaration = BridgeClassDef(BridgeClassType($type),
constructors: {
'': BridgeConstructorDef(BridgeFunctionDef(
returns: BridgeTypeAnnotation($type),
params: [],
namedParams: [
BridgeParameter(
'file',
BridgeTypeAnnotation(
BridgeTypeRef.type(RuntimeTypes.stringType)),
false),
BridgeParameter(
'label',
BridgeTypeAnnotation(
BridgeTypeRef.type(RuntimeTypes.stringType)),
false),
]))
},
// Specify class fields
fields: {
'file': BridgeFieldDef(
BridgeTypeAnnotation(BridgeTypeRef.type(RuntimeTypes.stringType))),
'label': BridgeFieldDef(
BridgeTypeAnnotation(BridgeTypeRef.type(RuntimeTypes.stringType))),
},
wrap: true);
static $Value? $new(Runtime runtime, $Value? target, List<$Value?> args) {
return $TrackModel.wrap(TrackModel());
}
@override
final TrackModel $value;
@override
TrackModel get $reified => $value;
final $Instance _superclass;
@override
$Value? $getProperty(Runtime runtime, String identifier) {
switch (identifier) {
case 'file':
return $String($value.file!);
case 'label':
return $String($value.label!);
default:
return _superclass.$getProperty(runtime, identifier);
}
}
@override
int $getRuntimeType(Runtime runtime) => runtime.lookupType($type.spec!);
@override
void $setProperty(Runtime runtime, String identifier, $Value value) {
switch (identifier) {
case 'file':
$value.file = value.$reified;
case 'label':
$value.label = value.$reified;
default:
_superclass.$setProperty(runtime, identifier, value);
}
}
@override
String? get file => $value.file;
@override
String? get label => $value.label;
@override
set file(String? file) {}
@override
set label(String? label) {}
}

View file

@ -2,6 +2,7 @@ import 'package:dart_eval/dart_eval.dart';
import 'package:dart_eval/dart_eval_bridge.dart';
import 'package:dart_eval/stdlib/core.dart';
import 'package:mangayomi/eval/bridge_class/model.dart';
import 'package:mangayomi/eval/bridge_class/track_model.dart';
class $VideoModel implements VideoModel, $Instance {
$VideoModel.wrap(this.$value) : _superclass = $Object($value);
@ -35,6 +36,11 @@ class $VideoModel implements VideoModel, $Instance {
BridgeTypeAnnotation(
BridgeTypeRef.type(RuntimeTypes.mapType)),
false),
BridgeParameter(
'substitles',
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.list,
[BridgeTypeRef.type(RuntimeTypes.dynamicType)])),
false),
]))
},
// Specify class fields
@ -47,6 +53,8 @@ class $VideoModel implements VideoModel, $Instance {
BridgeTypeAnnotation(BridgeTypeRef.type(RuntimeTypes.stringType))),
'headers': BridgeFieldDef(
BridgeTypeAnnotation(BridgeTypeRef.type(RuntimeTypes.mapType))),
'substitles': BridgeFieldDef(BridgeTypeAnnotation(BridgeTypeRef(
CoreTypes.list, [BridgeTypeRef.type(RuntimeTypes.dynamicType)]))),
},
wrap: true);
@ -75,6 +83,12 @@ class $VideoModel implements VideoModel, $Instance {
case 'headers':
return $Map.wrap($value.headers!);
case 'substitles':
return $List.wrap($value.subtitles!
.map((e) =>
$TrackModel.wrap(TrackModel(file: e.file, label: e.label)))
.toList());
default:
return _superclass.$getProperty(runtime, identifier);
}
@ -94,6 +108,8 @@ class $VideoModel implements VideoModel, $Instance {
$value.originalUrl = value.$reified;
case 'headers':
$value.headers = value.$reified as Map<String, String>;
case 'subtitles':
$value.subtitles = value.$reified as List<TrackModel>;
default:
_superclass.$setProperty(runtime, identifier, value);
@ -103,6 +119,9 @@ class $VideoModel implements VideoModel, $Instance {
@override
String? get url => $value.url;
@override
List<TrackModel>? get subtitles => $value.subtitles;
@override
String? get quality => $value.quality;
@ -123,4 +142,7 @@ class $VideoModel implements VideoModel, $Instance {
@override
set originalUrl(String? originalUrl) {}
@override
set subtitles(List? subtitles) {}
}

View file

@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:dart_eval/dart_eval.dart';
import 'package:mangayomi/eval/bridge_class/manga_model.dart';
import 'package:mangayomi/eval/bridge_class/track_model.dart';
import 'package:mangayomi/eval/bridge_class/video_model.dart';
import 'package:mangayomi/eval/m_bridge.dart';
@ -9,7 +10,8 @@ Uint8List compilerEval(String sourceCode) {
compiler.defineBridgeClasses([
$MBridge.$declaration,
$MangaModel.$declaration,
$VideoModel.$declaration
$VideoModel.$declaration,
$TrackModel.$declaration
]);
final program = compiler.compile({
'mangayomi': {'source_code.dart': sourceCode}

View file

@ -12,6 +12,7 @@ import 'package:intl/intl.dart';
import 'package:js_packer/js_packer.dart';
import 'package:json_path/json_path.dart';
import 'package:mangayomi/eval/bridge_class/model.dart';
import 'package:mangayomi/eval/bridge_class/track_model.dart';
import 'package:mangayomi/eval/bridge_class/video_model.dart';
import 'package:mangayomi/services/anime_extractors/dood_extractor.dart';
import 'package:mangayomi/services/anime_extractors/filemoon.dart';
@ -19,6 +20,7 @@ import 'package:mangayomi/services/anime_extractors/gogocdn_extractor.dart';
import 'package:mangayomi/services/anime_extractors/mp4upload_extractor.dart';
import 'package:mangayomi/services/anime_extractors/mytv_extractor.dart';
import 'package:mangayomi/services/anime_extractors/okru_extractor.dart';
import 'package:mangayomi/services/anime_extractors/rapidcloud_extractor.dart';
import 'package:mangayomi/services/anime_extractors/sendvid_extractor.dart';
import 'package:mangayomi/services/anime_extractors/sibnet_extractor.dart';
import 'package:mangayomi/services/anime_extractors/streamlare_extractor.dart';
@ -931,6 +933,11 @@ class MBridge {
.videosFromUrl(url, prefix: prefix, suffix: suffix);
}
static Future<List<Video>> rapidCloudExtractor(
String url, String prefix) async {
return await RapidCloudExtractor().videosFromUrl(url, prefix);
}
//Utility to use base64
static String bAse64(String text, int type) {
return utf8.decode(base64.decode(text));
@ -1138,6 +1145,24 @@ class $MBridge extends MBridge with $Bridge {
],
namedParams: []),
isStatic: true),
'rapidCloudExtractor': BridgeMethodDef(
BridgeFunctionDef(
returns: BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.future,
[BridgeTypeRef.type(RuntimeTypes.dynamicType)])),
params: [
BridgeParameter(
'url',
BridgeTypeAnnotation(
BridgeTypeRef.type(RuntimeTypes.stringType)),
false),
BridgeParameter(
'prefix',
BridgeTypeAnnotation(
BridgeTypeRef.type(RuntimeTypes.stringType)),
false),
],
namedParams: []),
isStatic: true),
'sendVidExtractor': BridgeMethodDef(
BridgeFunctionDef(
returns: BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.future,
@ -2044,6 +2069,11 @@ class $MBridge extends MBridge with $Bridge {
args[0]!.$value, args[1]!.$value, args[2]!.$value)
.then((value) =>
$List.wrap(value.map((e) => _toVideoModel(e)).toList())));
static $Future $rapidCloudExtractor(
Runtime runtime, $Value? target, List<$Value?> args) =>
$Future.wrap(MBridge.rapidCloudExtractor(args[0]!.$value, args[1]!.$value)
.then((value) =>
$List.wrap(value.map((e) => _toVideoModel(e)).toList())));
static $bool $isEmptyOrIsNotEmpty(
Runtime runtime, $Value? target, List<$Value?> args) {
@ -2075,4 +2105,11 @@ $VideoModel _toVideoModel(Video e) => $VideoModel.wrap(VideoModel()
..headers = e.headers
..originalUrl = e.originalUrl
..quality = e.quality
..url = e.url);
..url = e.url
..subtitles = $List.wrap(e.subtitles == null
? []
: e.subtitles!
.map((t) => $TrackModel.wrap(TrackModel()
..file = t.file
..label = t.label))
.toList()));

View file

@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:dart_eval/dart_eval.dart';
import 'package:mangayomi/eval/bridge_class/track_model.dart';
import 'package:mangayomi/eval/bridge_class/video_model.dart';
import 'package:mangayomi/eval/m_bridge.dart';
import 'package:mangayomi/eval/bridge_class/manga_model.dart';
@ -14,6 +15,8 @@ Runtime runtimeEval(Uint8List bytecode) {
'package:bridge_lib/bridge_lib.dart', 'MangaModel.', $MangaModel.$new);
runtime.registerBridgeFunc(
'package:bridge_lib/bridge_lib.dart', 'VideoModel.', $VideoModel.$new);
runtime.registerBridgeFunc(
'package:bridge_lib/bridge_lib.dart', 'TrackModel.', $TrackModel.$new);
runtime.registerBridgeFunc(
'package:bridge_lib/bridge_lib.dart', 'MBridge.http', $MBridge.$http);
runtime.registerBridgeFunc('package:bridge_lib/bridge_lib.dart',
@ -26,6 +29,8 @@ Runtime runtimeEval(Uint8List bytecode) {
'MBridge.gogoCdnExtractor', $MBridge.$gogoCdnExtractor);
runtime.registerBridgeFunc('package:bridge_lib/bridge_lib.dart',
'MBridge.doodExtractor', $MBridge.$doodExtractor);
runtime.registerBridgeFunc('package:bridge_lib/bridge_lib.dart',
'MBridge.rapidCloudExtractor', $MBridge.$rapidCloudExtractor);
runtime.registerBridgeFunc('package:bridge_lib/bridge_lib.dart',
'MBridge.streamTapeExtractor', $MBridge.$streamTapeExtractor);
runtime.registerBridgeFunc('package:bridge_lib/bridge_lib.dart',

View file

@ -3,6 +3,15 @@ class Video {
final String quality;
final String originalUrl;
final Map<String, String>? headers;
final List<Track>? subtitles;
Video(this.url, this.quality, this.originalUrl, {this.headers});
}
Video(this.url, this.quality, this.originalUrl,
{this.headers, this.subtitles});
}
class Track {
final String? file;
final String? label;
Track(this.file, this.label);
}

View file

@ -53,7 +53,7 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
));
return serversData.when(
data: (data) {
if (data.isEmpty &&
if (data.$1.isEmpty &&
(widget.episode.manga.value!.isLocalArchive ?? false) == false) {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
@ -80,12 +80,13 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
),
);
}
data.sort(
data.$1.sort(
(a, b) => a.quality.compareTo(b.quality),
);
return AnimeStreamPage(
episode: widget.episode,
videos: data,
videos: data.$1,
isLocal: data.$2,
);
},
error: (error, stackTrace) => Scaffold(
@ -146,7 +147,12 @@ class _AnimePlayerViewState extends riv.ConsumerState<AnimePlayerView> {
class AnimeStreamPage extends riv.ConsumerStatefulWidget {
final List<vid.Video> videos;
final Chapter episode;
const AnimeStreamPage({Key? key, required this.videos, required this.episode})
final bool isLocal;
const AnimeStreamPage(
{Key? key,
required this.isLocal,
required this.videos,
required this.episode})
: super(key: key);
@override
@ -160,7 +166,8 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
late final _streamController = AnimeStreamController(episode: widget.episode);
final ValueNotifier<vid.Video?> _video = ValueNotifier(null);
final ValueNotifier<VideoPrefs?> _video = ValueNotifier(null);
final ValueNotifier<VideoPrefs?> _subtitle = ValueNotifier(null);
final ValueNotifier<double> _playbackSpeed = ValueNotifier(1.0);
bool _seekToCurrentPosition = true;
late Duration _currentPosition = _streamController.geTCurrentPosition();
@ -184,14 +191,24 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
}
},
);
late final _firstVid = widget.videos.first;
@override
void initState() {
super.initState();
_currentPositionSub;
_video.value = widget.videos.first;
_player.open(
Media(_video.value!.originalUrl, httpHeaders: _video.value!.headers));
if (_firstVid.subtitles!.isNotEmpty) {
final firstSub = _firstVid.subtitles!.first;
_subtitle.value = VideoPrefs(
subtitle: SubtitleTrack.uri(firstSub.file!,
title: firstSub.label, language: firstSub.label));
}
_video.value = VideoPrefs(
videoTrack: VideoTrack(
_firstVid.originalUrl, _firstVid.quality, _firstVid.quality),
headers: _firstVid.headers);
_player.open(Media(_video.value!.videoTrack!.id,
httpHeaders: _video.value!.headers));
}
@override
@ -202,20 +219,44 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
}
void _onChangeVideoQuality() {
List<VideoPrefs> videoQuality = [];
List<VideoTrack> videoTracks = _player.state.tracks.video
.where((element) => element.w != null && element.h != null)
.toList();
for (var track in videoTracks) {
videoQuality.add(VideoPrefs(videoTrack: track, isLocal: true));
}
if (widget.videos.isNotEmpty && !widget.isLocal) {
for (var video in widget.videos) {
videoQuality.add(VideoPrefs(
videoTrack: VideoTrack(
video.url,
video.quality,
video.quality,
),
headers: video.headers,
isLocal: false));
}
}
final l10n = l10nLocalizations(context)!;
showCupertinoModalPopup(
context: context,
builder: (_) => CupertinoActionSheet(
title: Text(l10n.select_video_quality,
style: const TextStyle(fontSize: 30)),
actions: widget.videos
actions: videoQuality
.map((quality) => CupertinoActionSheetAction(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Text(
quality.quality,
quality.isLocal && !widget.isLocal
? "${_firstVid.quality} ${quality.videoTrack!.h}p"
: widget.isLocal
? _firstVid.quality
: quality.videoTrack!.title!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@ -225,7 +266,12 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
),
Icon(
Icons.check,
color: _video.value == quality
color: quality.isLocal &&
!widget.isLocal &&
"${quality.videoTrack!.title}${quality.videoTrack!.h}p" ==
"${_video.value!.videoTrack!.title}${_video.value!.videoTrack!.h}p" ||
"${_video.value!.videoTrack!.id}${_video.value!.videoTrack!.title}" ==
"${quality.videoTrack!.id}${quality.videoTrack!.title}"
? Theme.of(context).iconTheme.color
: Colors.transparent,
),
@ -233,8 +279,17 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
),
onPressed: () {
_video.value = quality; // change the video quality
_player.open(Media(quality.originalUrl,
httpHeaders: quality.headers));
if (quality.isLocal) {
if (widget.isLocal) {
_player.setVideoTrack(quality.videoTrack!);
} else {
_player.open(Media(quality.videoTrack!.id,
httpHeaders: quality.headers));
}
} else {
_player.open(Media(quality.videoTrack!.id,
httpHeaders: quality.headers));
}
_seekToCurrentPosition = true;
_currentPositionSub = _player.stream.position.listen(
(position) {
@ -264,21 +319,40 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
}
void _onChangeVideoSubtitle() {
List<VideoPrefs> videoSubtitle = [];
List<SubtitleTrack> videoSubs = _player.state.tracks.subtitle
.where((element) => element.title != null && element.language != null)
.toList();
for (var sub in videoSubs) {
videoSubtitle.add(VideoPrefs(isLocal: true, subtitle: sub));
}
if (widget.videos.isNotEmpty && !widget.isLocal) {
for (var video in widget.videos) {
for (var sub in video.subtitles!) {
videoSubtitle.add(VideoPrefs(
isLocal: false,
subtitle: SubtitleTrack.uri(sub.file!,
title: sub.label, language: sub.label)));
}
}
}
final l10n = l10nLocalizations(context)!;
showCupertinoModalPopup(
context: context,
builder: (_) => CupertinoActionSheet(
title: Text(l10n.select_video_subtitle,
style: const TextStyle(fontSize: 30)),
actions: _player.state.tracks.subtitle
actions: videoSubtitle
.map((subtitle) => CupertinoActionSheetAction(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
subtitle.title ??
subtitle.language ??
subtitle.channels ??
subtitle.subtitle!.title ??
subtitle.subtitle!.language ??
subtitle.subtitle!.channels ??
"N/A",
maxLines: 1,
overflow: TextOverflow.ellipsis,
@ -288,14 +362,17 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage> {
),
Icon(
Icons.check,
color: _player.state.track.subtitle == subtitle
color: _subtitle.value != null &&
"${_subtitle.value!.subtitle!.id}${_subtitle.value!.subtitle!.title}${_subtitle.value!.subtitle!.language}" ==
"${subtitle.subtitle!.id}${subtitle.subtitle!.title}${subtitle.subtitle!.language}"
? Theme.of(context).iconTheme.color
: Colors.transparent,
),
],
),
onPressed: () {
_player.setSubtitleTrack(subtitle);
_subtitle.value = subtitle;
_player.setSubtitleTrack(subtitle.subtitle!);
Navigator.maybePop(_);
},
))
@ -723,3 +800,12 @@ class MaterialPositionIndicatorState extends State<MaterialPositionIndicator> {
);
}
}
class VideoPrefs {
VideoTrack? videoTrack;
SubtitleTrack? subtitle;
bool isLocal;
final Map<String, String>? headers;
VideoPrefs(
{this.videoTrack, this.isLocal = true, this.headers, this.subtitle});
}

View file

@ -19,15 +19,17 @@ class ExtensionScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
if (isManga) {
ref.watch(fetchMangaSourcesListProvider(id: null));
ref.watch(fetchMangaSourcesListProvider(id: null, refresh: false));
} else {
ref.watch(fetchAnimeSourcesListProvider(id: null));
ref.watch(fetchAnimeSourcesListProvider(id: null, refresh: false));
}
return RefreshIndicator(
onRefresh: () => isManga
? ref.refresh(fetchMangaSourcesListProvider(id: null).future)
: ref.refresh(fetchAnimeSourcesListProvider(id: null).future),
? ref.refresh(
fetchMangaSourcesListProvider(id: null, refresh: true).future)
: ref.refresh(
fetchAnimeSourcesListProvider(id: null, refresh: true).future),
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: StreamBuilder(

View file

@ -9,8 +9,9 @@ import 'package:http/http.dart' as http;
part 'fetch_anime_sources.g.dart';
@riverpod
Future fetchAnimeSourcesList(FetchAnimeSourcesListRef ref, {int? id}) async {
if (ref.watch(checkForExtensionsUpdateStateProvider)) {
Future fetchAnimeSourcesList(FetchAnimeSourcesListRef ref,
{int? id, required bool refresh}) async {
if (ref.watch(checkForExtensionsUpdateStateProvider) || refresh) {
final info = await PackageInfo.fromPlatform();
final req = await http.get(Uri.parse(
"https://kodjodevf.github.io/mangayomi-extensions/anime_index.json"));
@ -64,7 +65,8 @@ Future fetchAnimeSourcesList(FetchAnimeSourcesListRef ref, {int? id}) async {
final headers = await getHeaders(req.body, source.baseUrl!);
isar.writeTxnSync(() {
isar.sources.putSync(sourc
..headers = headers
..headers = headers ?? ""
..isAdded = true
..sourceCode = req.body
..sourceCodeUrl = source.sourceCodeUrl
..id = source.id
@ -75,14 +77,13 @@ Future fetchAnimeSourcesList(FetchAnimeSourcesListRef ref, {int? id}) async {
..hasCloudflare = source.hasCloudflare
..iconUrl = source.iconUrl
..typeSource = source.typeSource
..isFullData = source.isFullData ?? false
..lang = source.lang
..isNsfw = source.isNsfw
..name = source.name
..version = source.version
..versionLast = source.version
..isManga = source.isManga
..isFullData = source.isFullData
..isFullData = source.isFullData ?? false
..appMinVerReq = source.appMinVerReq);
});
} else {

View file

@ -7,7 +7,7 @@ part of 'fetch_anime_sources.dart';
// **************************************************************************
String _$fetchAnimeSourcesListHash() =>
r'51594d405256b56228e36f285f095e24333ea273';
r'1808961fb57b3d53b12ec898ceb7ecb25886ea9e';
/// Copied from Dart SDK
class _SystemHash {
@ -44,9 +44,11 @@ class FetchAnimeSourcesListFamily extends Family<AsyncValue<dynamic>> {
/// See also [fetchAnimeSourcesList].
FetchAnimeSourcesListProvider call({
int? id,
required bool refresh,
}) {
return FetchAnimeSourcesListProvider(
id: id,
refresh: refresh,
);
}
@ -56,6 +58,7 @@ class FetchAnimeSourcesListFamily extends Family<AsyncValue<dynamic>> {
) {
return call(
id: provider.id,
refresh: provider.refresh,
);
}
@ -79,10 +82,12 @@ class FetchAnimeSourcesListProvider extends AutoDisposeFutureProvider<dynamic> {
/// See also [fetchAnimeSourcesList].
FetchAnimeSourcesListProvider({
this.id,
required this.refresh,
}) : super.internal(
(ref) => fetchAnimeSourcesList(
ref,
id: id,
refresh: refresh,
),
from: fetchAnimeSourcesListProvider,
name: r'fetchAnimeSourcesListProvider',
@ -96,16 +101,20 @@ class FetchAnimeSourcesListProvider extends AutoDisposeFutureProvider<dynamic> {
);
final int? id;
final bool refresh;
@override
bool operator ==(Object other) {
return other is FetchAnimeSourcesListProvider && other.id == id;
return other is FetchAnimeSourcesListProvider &&
other.id == id &&
other.refresh == refresh;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
hash = _SystemHash.combine(hash, refresh.hashCode);
return _SystemHash.finish(hash);
}

View file

@ -11,8 +11,9 @@ import 'package:http/http.dart' as http;
part 'fetch_manga_sources.g.dart';
@riverpod
Future fetchMangaSourcesList(FetchMangaSourcesListRef ref, {int? id}) async {
if (ref.watch(checkForExtensionsUpdateStateProvider)) {
Future fetchMangaSourcesList(FetchMangaSourcesListRef ref,
{int? id, required refresh}) async {
if (ref.watch(checkForExtensionsUpdateStateProvider) || refresh) {
final info = await PackageInfo.fromPlatform();
final req = await http.get(Uri.parse(
"https://kodjodevf.github.io/mangayomi-extensions/index.json"));
@ -66,7 +67,8 @@ Future fetchMangaSourcesList(FetchMangaSourcesListRef ref, {int? id}) async {
final headers = await getHeaders(req.body, source.baseUrl!);
isar.writeTxnSync(() {
isar.sources.putSync(sourc
..headers = headers
..headers = headers ?? ""
..isAdded = true
..sourceCode = req.body
..sourceCodeUrl = source.sourceCodeUrl
..id = source.id
@ -77,14 +79,13 @@ Future fetchMangaSourcesList(FetchMangaSourcesListRef ref, {int? id}) async {
..hasCloudflare = source.hasCloudflare
..iconUrl = source.iconUrl
..typeSource = source.typeSource
..isFullData = source.isFullData ?? false
..lang = source.lang
..isNsfw = source.isNsfw
..name = source.name
..version = source.version
..versionLast = source.version
..isManga = source.isManga
..isFullData = source.isFullData
..isFullData = source.isFullData ?? false
..appMinVerReq = source.appMinVerReq);
});
} else {

View file

@ -7,7 +7,7 @@ part of 'fetch_manga_sources.dart';
// **************************************************************************
String _$fetchMangaSourcesListHash() =>
r'719d582950ddb40cf7367ddca871ec46145ab0eb';
r'0cd22cf1bb061a13e86e34ec08061e1c52c2f7cf';
/// Copied from Dart SDK
class _SystemHash {
@ -44,9 +44,11 @@ class FetchMangaSourcesListFamily extends Family<AsyncValue<dynamic>> {
/// See also [fetchMangaSourcesList].
FetchMangaSourcesListProvider call({
int? id,
required dynamic refresh,
}) {
return FetchMangaSourcesListProvider(
id: id,
refresh: refresh,
);
}
@ -56,6 +58,7 @@ class FetchMangaSourcesListFamily extends Family<AsyncValue<dynamic>> {
) {
return call(
id: provider.id,
refresh: provider.refresh,
);
}
@ -79,10 +82,12 @@ class FetchMangaSourcesListProvider extends AutoDisposeFutureProvider<dynamic> {
/// See also [fetchMangaSourcesList].
FetchMangaSourcesListProvider({
this.id,
required this.refresh,
}) : super.internal(
(ref) => fetchMangaSourcesList(
ref,
id: id,
refresh: refresh,
),
from: fetchMangaSourcesListProvider,
name: r'fetchMangaSourcesListProvider',
@ -96,16 +101,20 @@ class FetchMangaSourcesListProvider extends AutoDisposeFutureProvider<dynamic> {
);
final int? id;
final dynamic refresh;
@override
bool operator ==(Object other) {
return other is FetchMangaSourcesListProvider && other.id == id;
return other is FetchMangaSourcesListProvider &&
other.id == id &&
other.refresh == refresh;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
hash = _SystemHash.combine(hash, refresh.hashCode);
return _SystemHash.finish(hash);
}

View file

@ -42,10 +42,12 @@ class _ExtensionListTileWidgetState
_isLoading = true;
});
widget.source.isManga!
? await ref.watch(
fetchMangaSourcesListProvider(id: widget.source.id).future)
: await ref.watch(
fetchAnimeSourcesListProvider(id: widget.source.id).future);
? await ref.watch(fetchMangaSourcesListProvider(
id: widget.source.id, refresh: true)
.future)
: await ref.watch(fetchAnimeSourcesListProvider(
id: widget.source.id, refresh: true)
.future);
if (mounted) {
setState(() {
_isLoading = false;
@ -133,12 +135,12 @@ class _ExtensionListTileWidgetState
_isLoading = true;
});
widget.source.isManga!
? await ref.watch(
fetchMangaSourcesListProvider(id: widget.source.id)
.future)
: await ref.watch(
fetchAnimeSourcesListProvider(id: widget.source.id)
.future);
? await ref.watch(fetchMangaSourcesListProvider(
id: widget.source.id, refresh: true)
.future)
: await ref.watch(fetchAnimeSourcesListProvider(
id: widget.source.id, refresh: true)
.future);
if (mounted) {
setState(() {
_isLoading = false;

View file

@ -59,7 +59,7 @@ Future<List<String>> downloadChapter(
).future)
.then((value) {
final videosUrls =
value.where((element) => !element.url.contains(".m3u8")).toList();
value.$1.where((element) => !element.url.contains(".mp4")).toList();
if (videosUrls.isNotEmpty) {
pageUrls = [videosUrls.first.url];
videoHeader.addAll(videosUrls.first.headers!);

View file

@ -40,7 +40,7 @@ final showNSFWStateProvider =
typedef _$ShowNSFWState = AutoDisposeNotifier<bool>;
String _$autoUpdateExtensionsStateHash() =>
r'dda58b6f78d486f82150e1edee4c1dadb9776df4';
r'30ce3c558504e005f9c85e2fc969ab7a581510cd';
/// See also [AutoUpdateExtensionsState].
@ProviderFor(AutoUpdateExtensionsState)

View file

@ -0,0 +1,107 @@
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
import 'package:mangayomi/models/video.dart';
import 'package:mangayomi/utils/cryptoaes/crypto_aes.dart';
class RapidCloudExtractor {
static const serverUrl = ['https://megacloud.tv', 'https://rapid-cloud.co'];
static const sourceUrl = [
'/embed-2/ajax/e-1/getSources?id=',
'/ajax/embed-6-v2/getSources?id='
];
static const sourceSpliter = ['/e-1/', '/embed-6-v2/'];
static const sourceKey = ['1', '6'];
final http.Client client = http.Client();
Future<(String, String)> cipherTextCleaner(String data, String type) async {
final response = await http.get(Uri.parse(
'https://raw.githubusercontent.com/Claudemirovsky/keys/e$type/key'));
final List indexPairs = json.decode(response.body);
String password = '';
String ciphertext = data;
int currentIndex = 0;
for (List item in indexPairs) {
final start = item.first + currentIndex;
final end = start + item.last;
final passSubstr = data.substring(start, end);
password += passSubstr;
ciphertext = ciphertext.replaceFirst(passSubstr, '');
currentIndex += item[1] as int;
}
return (ciphertext, password);
}
Future<String> decrypt(String ciphered, String type) async {
final result = await cipherTextCleaner(ciphered, type);
final res = CryptoAES.decryptAESCryptoJS(result.$1, result.$2);
return res;
}
Future<List<Video>> videosFromUrl(String url, String name) async {
try {
final type = url.startsWith('https://megacloud.tv') ? 0 : 1;
final keyType = sourceKey[type];
final id = url.split(sourceSpliter[type]).last.split('?').first;
final srcRes =
await http.get(Uri.parse('${serverUrl[type]}${sourceUrl[type]}$id'));
final data = Data.fromJson(json.decode(srcRes.body));
final decrypted = json.decode(await decrypt(data.sources!, keyType));
return [
Video(decrypted[0]["file"], name, decrypted[0]["file"],
subtitles:
data.tracks!.map((e) => Track(e.file!, e.label!)).toList())
];
} catch (_) {
return [];
}
}
}
class Tracks {
String? file;
String? label;
Tracks({
this.file,
this.label,
});
Tracks.fromJson(Map<String, dynamic> json) {
file = json['file'];
label = json['label'];
}
}
class Data {
String? sources;
List<Tracks>? tracks;
bool? encrypted;
Data({
this.sources,
this.tracks,
this.encrypted,
});
Data.fromJson(Map<String, dynamic> json) {
sources = json['sources'];
if (json['tracks'] != null) {
tracks = <Tracks>[];
json['tracks'].forEach((v) {
tracks!.add(Tracks.fromJson(v));
});
}
encrypted = json['encrypted'];
}
}

View file

@ -11,13 +11,16 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'get_anime_servers.g.dart';
@riverpod
Future<List<Video>> getAnimeServers(
Future<(List<Video>, bool)> getAnimeServers(
GetAnimeServersRef ref, {
required Chapter episode,
}) async {
List<Video> video = [];
if (episode.manga.value!.isLocalArchive!) {
return [Video(episode.archivePath!, episode.name!, episode.archivePath!)];
return (
[Video(episode.archivePath!, episode.name!, episode.archivePath!)],
true
);
}
final source =
getSource(episode.manga.value!.lang!, episode.manga.value!.source!);
@ -37,12 +40,19 @@ Future<List<Video>> getAnimeServers(
];
var res = await runtime.executeLib(
'package:mangayomi/source_code.dart', 'getVideoList');
// await Future.delayed(Duration(days: 1));
if (res is $List) {
video = res.$reified
.map(
(e) => Video(e.url, e.quality, e.originalUrl, headers: e.headers),
)
.toList();
video = res.$reified.map(
(e) {
List<Track>? subtitles = [];
var subs = e.subtitles;
if (subs is $List) {
subtitles = subs.map((e) => Track(e.file, e.label)).toList();
}
return Video(e.url, e.quality, e.originalUrl,
headers: e.headers, subtitles: subtitles);
},
).toList();
}
return video;
return (video, false);
}

View file

@ -6,7 +6,7 @@ part of 'get_anime_servers.dart';
// RiverpodGenerator
// **************************************************************************
String _$getAnimeServersHash() => r'26be3f4055c08947e6dca4d8e62abe07ddb98481';
String _$getAnimeServersHash() => r'a458dba029f241dd84237469ce6496526932ca32';
/// Copied from Dart SDK
class _SystemHash {
@ -29,14 +29,14 @@ class _SystemHash {
}
}
typedef GetAnimeServersRef = AutoDisposeFutureProviderRef<List<Video>>;
typedef GetAnimeServersRef = AutoDisposeFutureProviderRef<(List<Video>, bool)>;
/// See also [getAnimeServers].
@ProviderFor(getAnimeServers)
const getAnimeServersProvider = GetAnimeServersFamily();
/// See also [getAnimeServers].
class GetAnimeServersFamily extends Family<AsyncValue<List<Video>>> {
class GetAnimeServersFamily extends Family<AsyncValue<(List<Video>, bool)>> {
/// See also [getAnimeServers].
const GetAnimeServersFamily();
@ -74,7 +74,8 @@ class GetAnimeServersFamily extends Family<AsyncValue<List<Video>>> {
}
/// See also [getAnimeServers].
class GetAnimeServersProvider extends AutoDisposeFutureProvider<List<Video>> {
class GetAnimeServersProvider
extends AutoDisposeFutureProvider<(List<Video>, bool)> {
/// See also [getAnimeServers].
GetAnimeServersProvider({
required this.episode,

View file

@ -6,7 +6,7 @@ part of 'get_manga_detail.dart';
// RiverpodGenerator
// **************************************************************************
String _$getMangaDetailHash() => r'025ccc11f94f9b69bd85d86833bc261f66b74f7f';
String _$getMangaDetailHash() => r'97c04f6710cb2c5678c16f1c0e73c7bb73b77693';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,7 @@ part of 'get_popular_manga.dart';
// RiverpodGenerator
// **************************************************************************
String _$getPopularMangaHash() => r'f5a1f5a66bad652cb461ef0ed9d30f60ff7a000a';
String _$getPopularMangaHash() => r'7f785eaad6696c1181b1ddb3479c9987454397f4';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -0,0 +1,90 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
class CryptoAES {
static String encryptAESCryptoJS(String plainText, String passphrase) {
try {
final salt = genRandomWithNonZero(8);
var keyndIV = deriveKeyAndIV(passphrase, salt);
final key = encrypt.Key(keyndIV.$1);
final iv = encrypt.IV(keyndIV.$2);
final encrypter = encrypt.Encrypter(
encrypt.AES(key, mode: encrypt.AESMode.cbc, padding: "PKCS7"));
final encrypted = encrypter.encrypt(plainText, iv: iv);
Uint8List encryptedBytesWithSalt = Uint8List.fromList(
createUint8ListFromString("Salted__") + salt + encrypted.bytes);
return base64.encode(encryptedBytesWithSalt);
} catch (error) {
rethrow;
}
}
static String decryptAESCryptoJS(String encrypted, String passphrase) {
try {
Uint8List encryptedBytesWithSalt = base64.decode(encrypted);
Uint8List encryptedBytes =
encryptedBytesWithSalt.sublist(16, encryptedBytesWithSalt.length);
final salt = encryptedBytesWithSalt.sublist(8, 16);
var keyndIV = deriveKeyAndIV(passphrase, salt);
final key = encrypt.Key(keyndIV.$1);
final iv = encrypt.IV(keyndIV.$2);
final encrypter = encrypt.Encrypter(
encrypt.AES(key, mode: encrypt.AESMode.cbc, padding: "PKCS7"));
final decrypted =
encrypter.decrypt64(base64.encode(encryptedBytes), iv: iv);
return decrypted;
} catch (error) {
rethrow;
}
}
static (Uint8List, Uint8List) deriveKeyAndIV(
String passphrase, Uint8List salt) {
var password = createUint8ListFromString(passphrase);
Uint8List concatenatedHashes = Uint8List(0);
Uint8List currentHash = Uint8List(0);
bool enoughBytesForKey = false;
Uint8List preHash = Uint8List(0);
while (!enoughBytesForKey) {
// int preHashLength = currentHash.length + password.length + salt.length;
if (currentHash.isNotEmpty) {
preHash = Uint8List.fromList(currentHash + password + salt);
} else {
preHash = Uint8List.fromList(password + salt);
}
currentHash = md5.convert(preHash).bytes as Uint8List;
concatenatedHashes = Uint8List.fromList(concatenatedHashes + currentHash);
if (concatenatedHashes.length >= 48) enoughBytesForKey = true;
}
var keyBtyes = concatenatedHashes.sublist(0, 32);
var ivBtyes = concatenatedHashes.sublist(32, 48);
return (keyBtyes, ivBtyes);
}
static Uint8List createUint8ListFromString(String s) {
var ret = Uint8List(s.length);
for (var i = 0; i < s.length; i++) {
ret[i] = s.codeUnitAt(i);
}
return ret;
}
static Uint8List genRandomWithNonZero(int seedLength) {
final random = Random.secure();
const int randomMax = 245;
final Uint8List uint8list = Uint8List(seedLength);
for (int i = 0; i < seedLength; i++) {
uint8list[i] = random.nextInt(randomMax) + 1;
}
return uint8list;
}
}

View file

@ -242,7 +242,7 @@ packages:
source: hosted
version: "0.3.3+4"
crypto:
dependency: transitive
dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab

View file

@ -1,7 +1,7 @@
name: mangayomi
description: Free and open source manga reader multi plateform app inspired by Tachiyomi.
version: 0.0.35+13
version: 0.0.3+13
environment:
sdk: '>=3.1.0 <4.0.0'
@ -58,6 +58,7 @@ dependencies:
media_kit_libs_ios_video: ^1.1.3
media_kit_libs_macos_video: ^1.1.3
media_kit_libs_windows_video: ^1.0.7
crypto: ^3.0.3