feat : Kitsu tracker

This commit is contained in:
kodjomoustapha 2023-11-11 14:15:19 +01:00
parent 7e534bd013
commit 48f8e319c8
16 changed files with 1002 additions and 71 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View file

@ -329,7 +329,8 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
false),
BridgeParameter(
'prefix',
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)),
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string),
nullable: true),
false),
]),
),
@ -721,6 +722,18 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
false),
]),
),
'base64': BridgeMethodDef(
BridgeFunctionDef(
returns: BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)),
params: [
BridgeParameter(
'string',
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)),
false),
BridgeParameter('type',
BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.int)), false),
]),
),
'regExp': BridgeMethodDef(
BridgeFunctionDef(
returns: BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)),
@ -774,6 +787,12 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
return $Future.wrap(MBridge.http(args[0]!.$reified, args[1]!.$reified)
.then((value) => $String(value)));
}),
'base64' => $Function((_, __, List<$Value?> args) {
final result = args[1]!.$reified == 0
? utf8.decode(base64Url.decode(args[0]!.$reified))
: base64Url.encode(utf8.encode(args[0]!.$reified));
return $String(result);
}),
"cryptoHandler" => $Function((_, __, List<$Value?> args) {
return $String(MBridge.cryptoHandler(args[0]!.$value, args[1]!.$value,
args[2]!.$value, args[3]!.$value));
@ -842,7 +861,7 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
$List.wrap(value.map((e) => _toMVideo(e)).toList())))),
"yourUploadExtractor" => $Function((_, __, List<$Value?> args) =>
$Future.wrap(MBridge.yourUploadExtractor(args[0]!.$value,
args[1]?.$value, args[2]?.$value, args[3]?.$value)
args[1]?.$value, args[2]?.$value, args[3]?.$value ?? "")
.then((value) =>
$List.wrap(value.map((e) => _toMVideo(e)).toList())))),
"gogoCdnExtractor" => $Function((_, __, List<$Value?> args) =>

View file

@ -500,6 +500,9 @@ class MBridge {
//Get body
final bodyMap = jsonDecode(datas)["body"] as Map?;
final useFormBuilder =
(jsonDecode(datas)["useFormBuilder"] as bool?) ?? false;
final url = jsonDecode(datas)["url"] as String;
//Convert body Map<dynamic,dynamic> to Map<String,String>
Map<String, dynamic> body = {};
@ -517,16 +520,28 @@ class MBridge {
//Get the serie source
final source = sourceId != null ? isar.sources.getSync(sourceId) : null;
var request = hp.Request(method, Uri.parse(url));
if (useFormBuilder) {
var request = hp.MultipartRequest(method, Uri.parse(url));
if (bodyMap != null) {
final fields = bodyMap
.map((key, value) => MapEntry(key.toString(), value.toString()));
request.fields.addAll(fields);
}
request.headers.addAll(headers);
if (bodyMap != null) {
request.body = json.encode(body);
res = await request.send();
} else {
var request = hp.Request(method, Uri.parse(url));
if (bodyMap != null) {
request.body = json.encode(body);
}
request.headers.addAll(headers);
res = await request.send();
}
request.headers.addAll(headers);
res = await request.send();
if (res.statusCode == 403 && (source?.hasCloudflare ?? false)) {
return await cloudflareBypass(
url: url, sourceId: source!.id.toString(), method: 0);

View file

@ -228,5 +228,11 @@
"automaic":"Automatic",
"preferred_domain":"Preferred Domain",
"load_more":"Load More",
"cancel_all_for_this_series":"Cancel all for this series"
"cancel_all_for_this_series":"Cancel all for this series",
"login":"Login",
"login_into":"Login into {tracker}",
"email_adress":"Email Address",
"password":"Password",
"log_out_from":"Log out from {tracker}?",
"log_out":"Log out"
}

View file

@ -228,5 +228,11 @@
"automaic":"Automatique",
"preferred_domain":"Domaine préféré",
"load_more":"Charger plus",
"cancel_all_for_this_series":"Tout annuler pour cette serie"
"cancel_all_for_this_series":"Tout annuler pour cette serie",
"login":"Connexion",
"login_into":"Connexion à {tracker}",
"email_adress":"Adresse couriel",
"password":"Mot de passe",
"log_out_from":"Se déconnecter de {tracker} ?",
"log_out":"Se déconnecter"
}

View file

@ -15,4 +15,8 @@ class Track {
String? label;
Track({this.file, this.label});
Track.fromJson(Map<String, dynamic> json) {
file = json['file'];
label = json['label'];
}
}

View file

@ -6,7 +6,7 @@ part of 'extension_preferences_providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$getMirrorPrefHash() => r'b56f6cf8dcb17279b2945c9233711182380dd0c5';
String _$getMirrorPrefHash() => r'9b39c2c0e06ce4efda23dfd078d4ff96c1441f53';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -2,6 +2,7 @@ import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/models/track_search.dart';
import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart';
import 'package:mangayomi/services/trackers/anilist.dart';
import 'package:mangayomi/services/trackers/kitsu.dart';
import 'package:mangayomi/services/trackers/myanimelist.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'track_state_providers.g.dart';
@ -37,6 +38,16 @@ class TrackState extends _$TrackState {
.read(anilistProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.updateLibAnime(track!);
} else if (track!.syncId == 3) {
updateTrack = isManga!
? await ref
.read(kitsuProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.updateLibManga(track!)
: await ref
.read(kitsuProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.updateLibAnime(track!);
}
ref
@ -46,7 +57,7 @@ class TrackState extends _$TrackState {
int getScoreMaxValue() {
int? maxValue;
if (track!.syncId == 1) {
if (track!.syncId == 1 || track!.syncId == 3) {
maxValue = 10;
} else if (track!.syncId == 2) {
maxValue = ref
@ -59,8 +70,8 @@ class TrackState extends _$TrackState {
}
String getTextMapper(String numberText) {
if (track!.syncId == 1) {
} else {
if (track!.syncId == 1 || track!.syncId == 3) {
} else if (track!.syncId == 2) {
numberText = ref
.read(anilistProvider(syncId: 2, isManga: isManga).notifier)
.displayScore(int.parse(numberText));
@ -70,7 +81,7 @@ class TrackState extends _$TrackState {
int getScoreStep() {
int? step;
if (track!.syncId == 1) {
if (track!.syncId == 1 || track!.syncId == 3) {
step = 1;
} else if (track!.syncId == 2) {
step = ref
@ -84,14 +95,14 @@ class TrackState extends _$TrackState {
String displayScore(int score) {
String? result;
if (track!.syncId == 1) {
if (track!.syncId == 1 || track!.syncId == 3) {
result = score.toString();
} else {
} else if (track!.syncId == 2) {
result = ref
.read(anilistProvider(syncId: 2, isManga: isManga).notifier)
.displayScore(score);
}
return result;
return result!;
}
Future setTrackSearch(
@ -105,7 +116,7 @@ class TrackState extends _$TrackState {
trackingUrl: trackSearch.trackingUrl,
title: trackSearch.title,
lastChapterRead: 0,
totalChapter: 0,
totalChapter: trackSearch.totalChapter,
status: TrackStatus.planToRead,
startedReadingDate: 0,
finishedReadingDate: 0);
@ -121,8 +132,7 @@ class TrackState extends _$TrackState {
: await ref
.read(anilistProvider(syncId: syncId, isManga: isManga).notifier)
.findLibAnime(track);
if (findManga == null) {
findManga = isManga!
findManga ??= isManga!
? await ref
.read(
anilistProvider(syncId: syncId, isManga: isManga).notifier)
@ -131,16 +141,14 @@ class TrackState extends _$TrackState {
.read(
anilistProvider(syncId: syncId, isManga: isManga).notifier)
.addLibAnime(track);
findManga = isManga!
? await ref
.read(
anilistProvider(syncId: syncId, isManga: isManga).notifier)
.findLibManga(track)
: await ref
.read(
anilistProvider(syncId: syncId, isManga: isManga).notifier)
.findLibAnime(track);
}
} else if (syncId == 3) {
findManga = isManga!
? await ref
.read(kitsuProvider(syncId: syncId, isManga: isManga).notifier)
.addLibManga(track)
: await ref
.read(kitsuProvider(syncId: syncId, isManga: isManga).notifier)
.addLibAnime(track);
}
ref
.read(tracksProvider(syncId: syncId).notifier)
@ -172,6 +180,16 @@ class TrackState extends _$TrackState {
.read(anilistProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.aniListStatusListAnime;
} else if (track!.syncId == 3) {
statusList = isManga!
? ref
.read(kitsuProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.kitsuStatusListManga
: ref
.read(kitsuProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.kitsuStatusListAnime;
}
for (var element in TrackStatus.values) {
if (statusList.contains(element)) {
@ -198,6 +216,16 @@ class TrackState extends _$TrackState {
.read(anilistProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.findLibAnime(track!);
} else if (track!.syncId == 3) {
findManga = isManga!
? await ref
.read(kitsuProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.findLibManga(track!)
: await ref
.read(kitsuProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.findLibAnime(track!);
}
return findManga;
}
@ -219,6 +247,16 @@ class TrackState extends _$TrackState {
.read(anilistProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.searchAnime(query);
} else if (track!.syncId == 3) {
tracks = isManga!
? await ref
.read(kitsuProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.search(query)
: await ref
.read(kitsuProvider(syncId: track!.syncId!, isManga: isManga)
.notifier)
.searchAnime(query);
}
return tracks;
}

View file

@ -6,7 +6,7 @@ part of 'update_manga_detail_providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$updateMangaDetailHash() => r'09b3b2a4e8fbfbcbbabf41ec5c098e2b837bb4e8';
String _$updateMangaDetailHash() => r'3fc355e233b300a45fe79e594be8ee9845fb2b72';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,7 @@ part of 'download_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$downloadChapterHash() => r'5d77345658ca2fbc718d74b9e7bb4c52c30e1eaa';
String _$downloadChapterHash() => r'58a9d03fdd3e4c413a33e6b0797ef7d34e21a86e';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -6,7 +6,9 @@ import 'package:mangayomi/models/track_preference.dart';
import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/services/trackers/anilist.dart';
import 'package:mangayomi/services/trackers/kitsu.dart';
import 'package:mangayomi/services/trackers/myanimelist.dart';
import 'package:mangayomi/utils/media_query.dart';
class TrackScreen extends ConsumerWidget {
const TrackScreen({super.key});
@ -28,6 +30,20 @@ class TrackScreen extends ConsumerWidget {
snapshot.hasData ? snapshot.data : [];
return Column(
children: [
TrackListile(
onTap: () async {
await ref
.read(anilistProvider(syncId: 2).notifier)
.login();
},
id: 2,
entries: entries!),
TrackListile(
onTap: () async {
_showDialogLogin(context, ref);
},
id: 3,
entries: entries),
TrackListile(
onTap: () async {
await ref
@ -36,14 +52,6 @@ class TrackScreen extends ConsumerWidget {
.login();
},
id: 1,
entries: entries!),
TrackListile(
onTap: () async {
await ref
.read(anilistProvider(syncId: 2).notifier)
.login();
},
id: 2,
entries: entries)
],
);
@ -52,3 +60,120 @@ class TrackScreen extends ConsumerWidget {
);
}
}
void _showDialogLogin(BuildContext context, WidgetRef ref) {
final passwordController = TextEditingController();
final emailController = TextEditingController();
String email = "";
String password = "";
String errorMessage = "";
bool isLoading = false;
bool obscureText = true;
final l10n = l10nLocalizations(context)!;
showDialog(
context: context,
builder: (context) => StatefulBuilder(builder: (context, setState) {
return AlertDialog(
title: Text(
l10n.login_into("Kitsu"),
style: const TextStyle(fontSize: 30),
),
content: SizedBox(
height: 300,
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: emailController,
autofocus: true,
onChanged: (value) => setState(() {
email = value;
}),
decoration: InputDecoration(
hintText: l10n.email_adress,
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5)),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide()))),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: passwordController,
obscureText: obscureText,
onChanged: (value) => setState(() {
password = value;
}),
decoration: InputDecoration(
hintText: l10n.password,
suffixIcon: IconButton(
onPressed: () => setState(() {
obscureText = !obscureText;
}),
icon: Icon(obscureText
? Icons.visibility_outlined
: Icons.visibility_off_outlined)),
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5)),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide()))),
),
const SizedBox(height: 10),
Text(errorMessage, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox(
width: mediaWidth(context, 1),
height: 50,
child: ElevatedButton(
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
final res = await ref
.read(kitsuProvider(syncId: 3).notifier)
.login(email, password);
if (!res.$1) {
setState(() {
isLoading = false;
errorMessage = res.$2;
});
} else {
if (context.mounted) {
Navigator.pop(context);
}
}
},
child: isLoading
? const CircularProgressIndicator()
: Text(l10n.login))),
)
],
),
),
);
}),
);
}

View file

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/colors.dart';
import 'package:mangayomi/utils/constant.dart';
class TrackListile extends ConsumerWidget {
@ -20,6 +22,7 @@ class TrackListile extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final bool isLogged =
entries.where((element) => element.syncId == id).isNotEmpty;
final l10n = l10nLocalizations(context)!;
return ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
leading: ClipRRect(
@ -46,29 +49,41 @@ class TrackListile extends ConsumerWidget {
context: context,
builder: (context) {
return AlertDialog(
title: Text(
"Log out from ${trackInfos(id).$2}",
),
title: Text(l10n.log_out_from(trackInfos(id).$2)),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
side: BorderSide(
color: secondaryColor(context)),
borderRadius:
BorderRadius.circular(20))),
onPressed: () {
Navigator.pop(context);
},
child: const Text("Cancel")),
const SizedBox(
width: 15,
),
TextButton(
child: Text(l10n.cancel)),
const SizedBox(width: 15),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Colors.red.withOpacity(0.7)),
onPressed: () {
ref
.read(tracksProvider(syncId: id).notifier)
.logout();
Navigator.pop(context);
},
child: const Text("Log out")),
child: Text(
l10n.log_out,
style:
TextStyle(color: secondaryColor(context)),
)),
],
)
],

View file

@ -96,24 +96,9 @@ class RapidCloudExtractor {
}
}
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;
List<Track>? tracks;
bool? encrypted;
Data({
@ -125,9 +110,9 @@ class Data {
Data.fromJson(Map<String, dynamic> json) {
sources = json['sources'];
if (json['tracks'] != null) {
tracks = <Tracks>[];
tracks = <Track>[];
json['tracks'].forEach((v) {
tracks!.add(Tracks.fromJson(v));
tracks!.add(Track.fromJson(v));
});
}
encrypted = json['encrypted'];

View file

@ -0,0 +1,525 @@
import 'dart:developer';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:mangayomi/eval/model/m_bridge.dart';
import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'dart:convert';
import 'package:mangayomi/models/track_search.dart';
import 'package:mangayomi/modules/more/settings/track/myanimelist/model.dart';
import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'kitsu.g.dart';
@riverpod
class Kitsu extends _$Kitsu {
final String _clientId =
'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd';
final String _clientSecret =
'54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151';
final String _baseUrl = 'https://kitsu.io/api/edge/';
final String _loginUrl = 'https://kitsu.io/api/oauth/token';
final String _algoliaKeyUrl = 'https://kitsu.io/api/edge/algolia-keys/media/';
final String _algoliaUrl =
'https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/query/';
final String _algoliaAppId = 'AWQO5J657S';
final String _algoliaFilter =
'&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D';
final String _algoliaFilterAnime =
'&facetFilters=%5B%22kind%3Aanime%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22episodeCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D';
String _mangaUrl(int id) {
return 'https://kitsu.io/manga/$id';
}
String _animeUrl(int id) {
return 'https://kitsu.io/anime/$id';
}
@override
build({required int syncId, bool? isManga}) {}
Future<(bool, String)> login(String username, String password) async {
try {
final request = http.MultipartRequest('POST', Uri.parse(_loginUrl));
request.fields.addAll({
'username': username,
'password': password,
'grant_type': 'password',
'client_id': _clientId,
'client_secret': _clientSecret,
});
final response = await request.send();
if (response.statusCode != 200) {
return (false, "${response.reasonPhrase!} ${response.statusCode}");
}
final res = jsonDecode(await response.stream.bytesToString())
as Map<String, dynamic>;
final aKOAuth = OAuth.fromJson(res);
final currenUser = await _getCurrentUser(aKOAuth.accessToken!);
ref.read(tracksProvider(syncId: syncId).notifier).login(TrackPreference(
username: currenUser.$1,
syncId: syncId,
prefs: jsonEncode({"ratingSystem": currenUser.$2}),
oAuth: jsonEncode(aKOAuth.toJson())));
return (true, "");
} catch (e) {
return (false, e.toString());
}
}
Future<Track?> addLibManga(Track track) async {
final userId = _getUserId();
final accessToken = _getAccesToken();
var data = jsonEncode({
'data': {
'type': 'libraryEntries',
'attributes': {
'status': toKitsuStatusManga(track.status),
'progress': track.lastChapterRead
},
'relationships': {
'user': {
'data': {'id': userId, 'type': 'users'},
},
'media': {
'data': {'id': track.mediaId, 'type': 'manga'},
},
},
},
});
var response = await http.post(
Uri.parse('${_baseUrl}library-entries'),
headers: {
'Content-Type': 'application/vnd.api+json',
'Authorization': 'Bearer $accessToken'
},
body: data,
);
if (response.statusCode != 200) {
return await findLibManga(track);
}
var jsonData = jsonDecode(response.body) as Map<String, dynamic>;
track.libraryId = int.parse(jsonData['data']['id']);
return track;
}
Future<Track?> addLibAnime(Track track) async {
final userId = _getUserId();
log(track.mediaId.toString());
final accessToken = _getAccesToken();
var data = jsonEncode({
'data': {
'type': 'libraryEntries',
'attributes': {
'status': toKitsuStatusManga(track.status),
'progress': track.lastChapterRead,
},
'relationships': {
'user': {
'data': {'id': userId, 'type': 'users'},
},
'media': {
'data': {'id': track.mediaId, 'type': 'anime'},
},
},
},
});
var response = await http.post(
Uri.parse('${_baseUrl}library-entries'),
headers: {
'Content-Type': 'application/vnd.api+json',
'Authorization': 'Bearer $accessToken'
},
body: data,
);
if (response.statusCode != 200) {
return await findLibAnime(track);
}
var jsonData = jsonDecode(response.body) as Map<String, dynamic>;
track.libraryId = int.parse(jsonData['data']['id']);
return track;
}
Future<Track> updateLibManga(Track track) async {
final accessToken = _getAccesToken();
final data = jsonEncode({
"data": {
"type": "libraryEntries",
"id": track.mediaId,
"attributes": {
"status": toKitsuStatusManga(track.status),
"progress": track.lastChapterRead,
"ratingTwenty": _toKitsuScore(track.score!),
"startedAt": _convertDate(track.startedReadingDate!),
"finishedAt": _convertDate(track.finishedReadingDate!),
}
}
});
await http.patch(
Uri.parse('$_baseUrl/library-entries/${track.mediaId}'),
headers: {
"Content-Type": "application/vnd.api+json",
'Authorization': 'Bearer $accessToken'
},
body: data,
);
return track;
}
Future<Track> updateLibAnime(Track track) async {
final accessToken = _getAccesToken();
final data = jsonEncode({
"data": {
"type": "libraryEntries",
"id": track.mediaId,
"attributes": {
"status": toKitsuStatusManga(track.status),
"progress": track.lastChapterRead,
"ratingTwenty": _toKitsuScore(track.score!),
"startedAt": _convertDate(track.startedReadingDate!),
"finishedAt": _convertDate(track.finishedReadingDate!),
}
}
});
await http.patch(
Uri.parse('$_baseUrl/library-entries/${track.mediaId}'),
headers: {
"Content-Type": "application/vnd.api+json",
'Authorization': 'Bearer $accessToken'
},
body: data,
);
return track;
}
Future<List<TrackSearch>> search(String search) async {
final accessToken = _getAccesToken();
final algoliaKeyResponse = await http.get(
Uri.parse(_algoliaKeyUrl),
headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer $accessToken'
},
);
final key = json.decode(algoliaKeyResponse.body)["media"]["key"];
final response = await http.post(
Uri.parse(_algoliaUrl),
headers: {
"Content-Type": "application/json",
"X-Algolia-Application-Id": _algoliaAppId,
"X-Algolia-API-Key": key,
},
body: json.encode(
{'params': 'query=${Uri.encodeComponent(search)}$_algoliaFilter'}),
);
final data = json.decode(response.body);
final entries = List<Map<String, dynamic>>.from(data['hits'])
.where((element) => element["subtype"] != "novel")
.toList();
return entries
.map((jsonRes) => TrackSearch(
libraryId: jsonRes['id'],
syncId: syncId,
trackingUrl: _mangaUrl(jsonRes['id']),
mediaId: jsonRes['id'],
summary: jsonRes['synopsis'] ?? "",
totalChapter: jsonRes['chapterCount'] ?? 0,
coverUrl: jsonRes['posterImage']['original'] ?? "",
title: jsonRes['canonicalTitle'],
startDate: "",
publishingType: jsonRes["subtype"] ?? "s",
publishingStatus:
jsonRes['endDate'] == null ? "Publishing" : "Finished"))
.toList();
}
Future<List<TrackSearch>> searchAnime(String search) async {
final accessToken = _getAccesToken();
final algoliaKeyResponse = await http.get(
Uri.parse(_algoliaKeyUrl),
headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer $accessToken'
},
);
final key = json.decode(algoliaKeyResponse.body)["media"]["key"];
final response = await http.post(
Uri.parse(_algoliaUrl),
headers: {
"Content-Type": "application/json",
"X-Algolia-Application-Id": _algoliaAppId,
"X-Algolia-API-Key": key,
},
body: json.encode({
'params': 'query=${Uri.encodeComponent(search)}$_algoliaFilterAnime'
}),
);
final data = json.decode(response.body);
final entries = List<Map<String, dynamic>>.from(data['hits'])
.where((element) => element["subtype"] != "novel")
.toList();
return entries
.map((jsonRes) => TrackSearch(
libraryId: jsonRes['id'],
syncId: syncId,
trackingUrl: _animeUrl(jsonRes['id']),
mediaId: jsonRes['id'],
summary: jsonRes['synopsis'] ?? "",
totalChapter: jsonRes['episodeCount'] ?? 0,
coverUrl: jsonRes['posterImage']['original'] ?? "",
title: jsonRes['canonicalTitle'],
startDate: "",
publishingType: jsonRes["subtype"] ?? "",
publishingStatus:
jsonRes['endDate'] == null ? "Publishing" : "Finished"))
.toList();
}
Future<Track?> getManga(Track track) async {
final accessToken = _getAccesToken();
final url = Uri.parse(
'${_baseUrl}library-entries?filter[id]=${track.mediaId}&include=manga');
final response = await http.get(url, headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer $accessToken'
});
if (response.statusCode == 200) {
final jsonResponse = jsonDecode(response.body);
final List<dynamic> data = jsonResponse['data'];
if (data.isNotEmpty) {
final obj = data[0];
track.mediaId = int.parse(obj["id"]);
track.libraryId = int.parse(obj["id"]);
track.syncId = syncId;
track.trackingUrl = _mangaUrl(int.parse(obj["id"]));
track.status = _getKitsuTrackStatus(obj["attributes"]["status"]);
track.title =
jsonResponse['included'][0]["attributes"]["canonicalTitle"];
track.totalChapter =
jsonResponse['included'][0]["attributes"]["chapterCount"] ?? 0;
track.score = ((obj["attributes"]["ratingTwenty"] ?? 0) / 2).toInt();
track.lastChapterRead = obj["attributes"]["progress"];
track.startedReadingDate = _parseDate(obj["attributes"]["startedAt"]);
track.finishedReadingDate = _parseDate(obj["attributes"]["finishedAt"]);
return track;
}
}
return null;
}
Future<Track?> findLibManga(Track track) async {
final userId = _getUserId();
final accessToken = _getAccesToken();
final url = Uri.parse(
'${_baseUrl}library-entries?filter[manga_id]=${track.mediaId}&filter[user_id]=$userId&include=manga');
final response = await http.get(url, headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer $accessToken'
});
if (response.statusCode == 200) {
final jsonResponse = jsonDecode(response.body);
final List<dynamic> data = jsonResponse['data'];
if (data.isNotEmpty) {
final obj = data[0];
track.mediaId = int.parse(obj["id"]);
track.libraryId = int.parse(obj["id"]);
track.syncId = syncId;
track.trackingUrl = _mangaUrl(int.parse(obj["id"]));
track.title =
jsonResponse['included'][0]["attributes"]["canonicalTitle"];
track.totalChapter =
jsonResponse['included'][0]["attributes"]["chapterCount"] ?? 0;
track.status = _getKitsuTrackStatus(obj["attributes"]["status"]);
track.score = ((obj["attributes"]["ratingTwenty"] ?? 0) / 2).toInt();
track.lastChapterRead = obj["attributes"]["progress"];
track.startedReadingDate = _parseDate(obj["attributes"]["startedAt"]);
track.finishedReadingDate = _parseDate(obj["attributes"]["finishedAt"]);
return track;
}
}
return await getManga(track);
}
Future<Track?> findLibAnime(Track track) async {
final userId = _getUserId();
final accessToken = _getAccesToken();
final url = Uri.parse(
'${_baseUrl}library-entries?filter[anime_id]=${track.mediaId}&filter[user_id]=$userId&include=anime');
final response = await http.get(url, headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer $accessToken'
});
if (response.statusCode == 200) {
final jsonResponse = jsonDecode(response.body);
final List<dynamic> data = jsonResponse['data'];
if (data.isNotEmpty) {
track.mediaId = int.parse(data[0]["id"]);
track.libraryId = int.parse(data[0]["id"]);
track.syncId = syncId;
track.trackingUrl = _mangaUrl(int.parse(data[0]["id"]));
track.status = _getKitsuTrackStatus(data[0]["attributes"]["status"]);
track.title =
jsonResponse['included'][0]["attributes"]["canonicalTitle"];
track.totalChapter =
jsonResponse['included'][0]["attributes"]["episodeCount"] ?? 0;
track.score =
((data[0]["attributes"]["ratingTwenty"] ?? 0) / 2).toInt();
track.lastChapterRead = data[0]["attributes"]["progress"];
track.startedReadingDate =
_parseDate(data[0]["attributes"]["startedAt"]);
track.finishedReadingDate =
_parseDate(data[0]["attributes"]["finishedAt"]);
return track;
}
}
return await getAnime(track);
}
Future<Track?> getAnime(Track track) async {
final accessToken = _getAccesToken();
final url = Uri.parse(
'${_baseUrl}library-entries?filter[id]=${track.mediaId}&include=anime');
final response = await http.get(url, headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer $accessToken'
});
if (response.statusCode == 200) {
final jsonResponse = jsonDecode(response.body);
final List<dynamic> data = jsonResponse['data'];
if (data.isNotEmpty) {
track.mediaId = int.parse(data[0]["id"]);
track.libraryId = int.parse(data[0]["id"]);
track.syncId = syncId;
track.trackingUrl = _mangaUrl(int.parse(data[0]["id"]));
track.status = _getKitsuTrackStatus(data[0]["attributes"]["status"]);
track.score =
((data[0]["attributes"]["ratingTwenty"] ?? 0) / 2).toInt();
track.title =
jsonResponse['included'][0]["attributes"]["canonicalTitle"];
track.totalChapter =
jsonResponse['included'][0]["attributes"]["episodeCount"] ?? 0;
track.lastChapterRead = data[0]["attributes"]["progress"];
track.startedReadingDate =
_parseDate(data[0]["attributes"]["startedAt"]);
track.finishedReadingDate =
_parseDate(data[0]["attributes"]["finishedAt"]);
return track;
}
}
return null;
}
Future<(String, String)> _getCurrentUser(String accessToken) async {
final response = await http.get(
Uri.parse("${_baseUrl}users?filter[self]=true"),
headers: {
"Content-Type": "application/json",
'Authorization': 'Bearer $accessToken'
},
);
final data = json.decode(response.body)['data'][0];
return (
data['id'].toString(),
data['attributes']['ratingSystem'].toString(),
);
}
String _getAccesToken() {
final track = ref.watch(tracksProvider(syncId: syncId));
final mAKOAuth =
OAuth.fromJson(jsonDecode(track!.oAuth!) as Map<String, dynamic>);
final expiresIn = DateTime.fromMillisecondsSinceEpoch(mAKOAuth.expiresIn!);
if (DateTime.now().isAfter(expiresIn)) {
ref.read(tracksProvider(syncId: syncId).notifier).logout();
botToast("Anilist Token expired");
throw Exception("Token expired");
}
return mAKOAuth.accessToken!;
}
String _getUserId() {
final track = ref.watch(tracksProvider(syncId: syncId));
return json.decode(track!.prefs!)["userId"];
}
TrackStatus _getKitsuTrackStatus(String status) {
return switch (status) {
"current" => TrackStatus.reading,
"completed" => TrackStatus.completed,
"on_hold" => TrackStatus.onHold,
_ => TrackStatus.planToRead,
};
}
List<TrackStatus> kitsuStatusListManga = [
TrackStatus.reading,
TrackStatus.completed,
TrackStatus.onHold,
TrackStatus.dropped,
TrackStatus.planToRead
];
List<TrackStatus> kitsuStatusListAnime = [
TrackStatus.watching,
TrackStatus.completed,
TrackStatus.onHold,
TrackStatus.dropped,
TrackStatus.planToWatch
];
String? toKitsuStatusManga(TrackStatus status) {
return switch (status) {
TrackStatus.reading => "current",
TrackStatus.completed => "completed",
TrackStatus.onHold => "on_hold",
TrackStatus.dropped => "dropped",
_ => "planned",
};
}
String? tokitsuStatusAnime(TrackStatus status) {
return switch (status) {
TrackStatus.watching => "current",
TrackStatus.completed => "completed",
TrackStatus.onHold => "on_hold",
TrackStatus.dropped => "dropped",
_ => "planned",
};
}
var formatter = DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "en");
String? _convertDate(int dateValue) {
if (dateValue == 0) return null;
return formatter.format(DateTime.fromMillisecondsSinceEpoch(dateValue));
}
int _parseDate(String? date) {
if (date == null) return 0;
var dateValue = formatter.parse(date);
return dateValue.millisecondsSinceEpoch;
}
String? _toKitsuScore(int score) {
return score > 0 ? (score / 5).toString() : null;
}
}

View file

@ -0,0 +1,192 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'kitsu.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$kitsuHash() => r'560c510503436a38bb1283c28978de2dcec7a367';
/// 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));
}
}
abstract class _$Kitsu extends BuildlessAutoDisposeNotifier<dynamic> {
late final int syncId;
late final bool? isManga;
dynamic build({
required int syncId,
bool? isManga,
});
}
/// See also [Kitsu].
@ProviderFor(Kitsu)
const kitsuProvider = KitsuFamily();
/// See also [Kitsu].
class KitsuFamily extends Family<dynamic> {
/// See also [Kitsu].
const KitsuFamily();
/// See also [Kitsu].
KitsuProvider call({
required int syncId,
bool? isManga,
}) {
return KitsuProvider(
syncId: syncId,
isManga: isManga,
);
}
@override
KitsuProvider getProviderOverride(
covariant KitsuProvider provider,
) {
return call(
syncId: provider.syncId,
isManga: provider.isManga,
);
}
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'kitsuProvider';
}
/// See also [Kitsu].
class KitsuProvider extends AutoDisposeNotifierProviderImpl<Kitsu, dynamic> {
/// See also [Kitsu].
KitsuProvider({
required int syncId,
bool? isManga,
}) : this._internal(
() => Kitsu()
..syncId = syncId
..isManga = isManga,
from: kitsuProvider,
name: r'kitsuProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$kitsuHash,
dependencies: KitsuFamily._dependencies,
allTransitiveDependencies: KitsuFamily._allTransitiveDependencies,
syncId: syncId,
isManga: isManga,
);
KitsuProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.syncId,
required this.isManga,
}) : super.internal();
final int syncId;
final bool? isManga;
@override
dynamic runNotifierBuild(
covariant Kitsu notifier,
) {
return notifier.build(
syncId: syncId,
isManga: isManga,
);
}
@override
Override overrideWith(Kitsu Function() create) {
return ProviderOverride(
origin: this,
override: KitsuProvider._internal(
() => create()
..syncId = syncId
..isManga = isManga,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
syncId: syncId,
isManga: isManga,
),
);
}
@override
AutoDisposeNotifierProviderElement<Kitsu, dynamic> createElement() {
return _KitsuProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is KitsuProvider &&
other.syncId == syncId &&
other.isManga == isManga;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, syncId.hashCode);
hash = _SystemHash.combine(hash, isManga.hashCode);
return _SystemHash.finish(hash);
}
}
mixin KitsuRef on AutoDisposeNotifierProviderRef<dynamic> {
/// The parameter `syncId` of this provider.
int get syncId;
/// The parameter `isManga` of this provider.
bool? get isManga;
}
class _KitsuProviderElement
extends AutoDisposeNotifierProviderElement<Kitsu, dynamic> with KitsuRef {
_KitsuProviderElement(super.provider);
@override
int get syncId => (origin as KitsuProvider).syncId;
@override
bool? get isManga => (origin as KitsuProvider).isManga;
}
// 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

@ -58,7 +58,8 @@ TrackStatus toTrackStatus(TrackStatus status, bool isManga, int syncId) {
(String, String) trackInfos(int id) {
return switch (id) {
1 => ("assets/trackers_icons/tracker_mal.webp", "MyAnimeList"),
_ => ("assets/trackers_icons/tracker_anilist.webp", "Anilist"),
2 => ("assets/trackers_icons/tracker_anilist.webp", "Anilist"),
_ => ("assets/trackers_icons/tracker_kitsu.webp", "Kitsu"),
};
}