309 lines
12 KiB
Dart
309 lines
12 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
|
import 'package:http_interceptor/http_interceptor.dart';
|
|
import 'package:intl/intl.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/more/settings/track/myanimelist/model.dart';
|
|
import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart';
|
|
import 'package:mangayomi/services/http/m_client.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
part 'myanimelist.g.dart';
|
|
|
|
@riverpod
|
|
class MyAnimeList extends _$MyAnimeList {
|
|
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
|
|
String baseOAuthUrl = 'https://myanimelist.net/v1/oauth2';
|
|
String baseApiUrl = 'https://api.myanimelist.net/v2';
|
|
String codeVerifier = "";
|
|
String clientId = (Platform.isWindows || Platform.isLinux)
|
|
? '39e9be346b4e7dbcc59a98357e2f8472'
|
|
: '0c9100ccd443ddb441a319a881180f7f';
|
|
|
|
@override
|
|
void build({required int syncId, required bool? isManga}) {}
|
|
|
|
Future<bool?> login() async {
|
|
final callbackUrlScheme = (Platform.isWindows || Platform.isLinux) ? 'http://localhost:43824' : 'mangayomi';
|
|
final loginUrl = _authUrl();
|
|
|
|
try {
|
|
final uri = await FlutterWebAuth2.authenticate(url: loginUrl, callbackUrlScheme: callbackUrlScheme);
|
|
final queryParams = Uri.parse(uri).queryParameters;
|
|
if (queryParams['code'] == null) return null;
|
|
|
|
final oAuth = await _getOAuth(queryParams['code']!);
|
|
final mALOAuth = OAuth.fromJson(oAuth as Map<String, dynamic>);
|
|
final username = await _getUserName(mALOAuth.accessToken!);
|
|
ref
|
|
.read(tracksProvider(syncId: syncId).notifier)
|
|
.login(TrackPreference(syncId: syncId, username: username, oAuth: jsonEncode(mALOAuth.toJson())));
|
|
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<String> _getAccesToken() async {
|
|
final track = ref.watch(tracksProvider(syncId: syncId));
|
|
final mALOAuth = OAuth.fromJson(jsonDecode(track!.oAuth!) as Map<String, dynamic>);
|
|
final expiresIn = DateTime.fromMillisecondsSinceEpoch(mALOAuth.expiresIn!);
|
|
if (DateTime.now().isAfter(expiresIn)) {
|
|
final params = {
|
|
'client_id': clientId,
|
|
'grant_type': 'refresh_token',
|
|
'refresh_token': mALOAuth.refreshToken,
|
|
};
|
|
final response = await http.post(Uri.parse('$baseOAuthUrl/token'), body: params);
|
|
final oAuth = OAuth.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
|
|
final username = await _getUserName(oAuth.accessToken!);
|
|
ref
|
|
.read(tracksProvider(syncId: syncId).notifier)
|
|
.login(TrackPreference(syncId: syncId, username: username, prefs: "", oAuth: jsonEncode(oAuth.toJson())));
|
|
return oAuth.accessToken!;
|
|
}
|
|
return mALOAuth.accessToken!;
|
|
}
|
|
|
|
Future<List<TrackSearch>> search(String query) async {
|
|
final accessToken = await _getAccesToken();
|
|
final url = Uri.parse(isManga! ? '$baseApiUrl/manga' : '$baseApiUrl/anime').replace(queryParameters: {
|
|
'q': query.trim(),
|
|
'nsfw': 'true',
|
|
});
|
|
final result = await http.get(url, headers: {'Authorization': 'Bearer $accessToken'});
|
|
final res = jsonDecode(result.body) as Map<String, dynamic>;
|
|
|
|
List<int> mangaIds = res['data'] == null ? [] : (res['data'] as List).map((e) => e['node']["id"] as int).toList();
|
|
List<TrackSearch> trackSearchResult = [];
|
|
for (var mangaId in mangaIds) {
|
|
final trackSearch =
|
|
isManga! ? await getMangaDetails(mangaId, accessToken) : await getAnimeDetails(mangaId, accessToken);
|
|
trackSearchResult.add(trackSearch);
|
|
}
|
|
|
|
return trackSearchResult.where((element) => !element.publishingType!.contains("novel")).toList();
|
|
}
|
|
|
|
Future<TrackSearch> getMangaDetails(int id, String accessToken) async {
|
|
final url = Uri.parse('$baseApiUrl/manga/$id').replace(queryParameters: {
|
|
'fields': 'id,title,synopsis,num_chapters,main_picture,status,media_type,start_date',
|
|
});
|
|
|
|
final result = await http.get(url, headers: {'Authorization': 'Bearer $accessToken'});
|
|
final res = jsonDecode(result.body) as Map<String, dynamic>;
|
|
|
|
return TrackSearch(
|
|
mediaId: res["id"],
|
|
summary: res["synopsis"] ?? "",
|
|
totalChapter: res["num_chapters"],
|
|
coverUrl: res["main_picture"]["large"] ?? "",
|
|
title: res["title"],
|
|
startDate: res["start_date"] ?? "",
|
|
publishingType: res["media_type"].toString().replaceAll("_", " "),
|
|
publishingStatus: res["status"].toString().replaceAll("_", " "),
|
|
trackingUrl: "https://myanimelist.net/manga/${res["id"]}");
|
|
}
|
|
|
|
Future<TrackSearch> getAnimeDetails(int id, String accessToken) async {
|
|
final url = Uri.parse('$baseApiUrl/anime/$id').replace(queryParameters: {
|
|
'fields': 'id,title,synopsis,num_episodes,main_picture,status,media_type,start_date',
|
|
});
|
|
|
|
final result = await http.get(url, headers: {'Authorization': 'Bearer $accessToken'});
|
|
final res = jsonDecode(result.body) as Map<String, dynamic>;
|
|
|
|
return TrackSearch(
|
|
mediaId: res["id"],
|
|
summary: res["synopsis"] ?? "",
|
|
totalChapter: res["num_episodes"],
|
|
coverUrl: res["main_picture"]["large"] ?? "",
|
|
title: res["title"],
|
|
startDate: res["start_date"] ?? "",
|
|
publishingType: res["media_type"].toString().replaceAll("_", " "),
|
|
publishingStatus: res["status"].toString().replaceAll("_", " "),
|
|
trackingUrl: "https://myanimelist.net/anime/${res["id"]}");
|
|
}
|
|
|
|
String _convertToIsoDate(int? epochTime) {
|
|
String date = "";
|
|
try {
|
|
date = DateFormat("yyyy-MM-dd", "en_US").format(DateTime.fromMillisecondsSinceEpoch(epochTime!));
|
|
} catch (_) {}
|
|
return date;
|
|
}
|
|
|
|
String _codeVerifier() {
|
|
final random = Random.secure();
|
|
final values = List<int>.generate(200, (i) => random.nextInt(256));
|
|
codeVerifier = base64UrlEncode(values).substring(0, 128);
|
|
return codeVerifier;
|
|
}
|
|
|
|
String _authUrl() {
|
|
_codeVerifier();
|
|
return '$baseOAuthUrl/authorize?client_id=$clientId&code_challenge=$codeVerifier&response_type=code';
|
|
}
|
|
|
|
TrackStatus _getMALTrackStatusManga(String status) {
|
|
return switch (status) {
|
|
"reading" => TrackStatus.reading,
|
|
"completed" => TrackStatus.completed,
|
|
"on_hold" => TrackStatus.onHold,
|
|
"dropped" => TrackStatus.dropped,
|
|
"plan_to_read" => TrackStatus.planToRead,
|
|
_ => TrackStatus.rereading,
|
|
};
|
|
}
|
|
|
|
TrackStatus _getMALTrackStatusAnime(String status) {
|
|
return switch (status) {
|
|
"watching" => TrackStatus.watching,
|
|
"completed" => TrackStatus.completed,
|
|
"on_hold" => TrackStatus.onHold,
|
|
"dropped" => TrackStatus.dropped,
|
|
_ => TrackStatus.planToWatch,
|
|
};
|
|
}
|
|
|
|
List<TrackStatus> myAnimeListStatusListManga = [
|
|
TrackStatus.reading,
|
|
TrackStatus.completed,
|
|
TrackStatus.onHold,
|
|
TrackStatus.dropped,
|
|
TrackStatus.planToRead,
|
|
TrackStatus.rereading
|
|
];
|
|
List<TrackStatus> myAnimeListStatusListAnime = [
|
|
TrackStatus.watching,
|
|
TrackStatus.completed,
|
|
TrackStatus.onHold,
|
|
TrackStatus.dropped,
|
|
TrackStatus.planToWatch
|
|
];
|
|
|
|
String? toMyAnimeListStatusManga(TrackStatus status) {
|
|
return switch (status) {
|
|
TrackStatus.reading => "reading",
|
|
TrackStatus.completed => "completed",
|
|
TrackStatus.onHold => "on_hold",
|
|
TrackStatus.dropped => "dropped",
|
|
TrackStatus.planToRead => "plan_to_read",
|
|
_ => "reading",
|
|
};
|
|
}
|
|
|
|
String? toMyAnimeListStatusAnime(TrackStatus status) {
|
|
return switch (status) {
|
|
TrackStatus.watching => "watching",
|
|
TrackStatus.completed => "completed",
|
|
TrackStatus.onHold => "on_hold",
|
|
TrackStatus.dropped => "dropped",
|
|
_ => "plan_to_watch",
|
|
};
|
|
}
|
|
|
|
Future<dynamic> _getOAuth(String code) async {
|
|
final params = {
|
|
'client_id': clientId,
|
|
'code': code,
|
|
'code_verifier': codeVerifier,
|
|
'grant_type': 'authorization_code',
|
|
};
|
|
final response = await http.post(Uri.parse('$baseOAuthUrl/token'), body: params);
|
|
return jsonDecode(response.body);
|
|
}
|
|
|
|
Future<String> _getUserName(String accessToken) async {
|
|
final response =
|
|
await http.get(Uri.parse('$baseApiUrl/users/@me'), headers: {'Authorization': 'Bearer $accessToken'});
|
|
return jsonDecode(response.body)['name'];
|
|
}
|
|
|
|
Future<Track> findManga(Track track) async {
|
|
final accessToken = await _getAccesToken();
|
|
final uri = Uri.parse(isManga! ? '$baseApiUrl/manga/${track.mediaId}' : '$baseApiUrl/anime/${track.mediaId}')
|
|
.replace(queryParameters: {
|
|
'fields': isManga!
|
|
? 'num_chapters,my_list_status{start_date,finish_date}'
|
|
: 'num_episodes,my_list_status{start_date,finish_date}',
|
|
});
|
|
final response = await http.get(uri, headers: {'Authorization': 'Bearer $accessToken'});
|
|
final mJson = jsonDecode(response.body);
|
|
track.totalChapter = isManga! ? mJson['num_chapters'] ?? 0 : mJson['num_episodes'] ?? 0;
|
|
if (mJson['my_list_status'] != null) {
|
|
track =
|
|
isManga! ? _parseMangaItem(mJson["my_list_status"], track) : _parseAnimeItem(mJson["my_list_status"], track);
|
|
} else {
|
|
track = isManga! ? await updateManga(track) : await updateAnime(track);
|
|
}
|
|
return track;
|
|
}
|
|
|
|
Track _parseMangaItem(Map<String, dynamic> mJson, Track track) {
|
|
bool isRereading = mJson["is_rereading"] ?? false;
|
|
track.status = isRereading ? TrackStatus.rereading : _getMALTrackStatusManga(mJson["status"]);
|
|
track.lastChapterRead = int.parse(mJson["num_chapters_read"].toString());
|
|
track.score = int.parse(mJson["score"].toString());
|
|
track.startedReadingDate = _parseDate(mJson["start_date"]);
|
|
track.finishedReadingDate = _parseDate(mJson["finish_date"]);
|
|
return track;
|
|
}
|
|
|
|
Track _parseAnimeItem(Map<String, dynamic> mJson, Track track) {
|
|
bool isReWatching = mJson["is_rewatching"] ?? false;
|
|
track.status = isReWatching ? TrackStatus.reWatching : _getMALTrackStatusAnime(mJson["status"]);
|
|
track.lastChapterRead = int.parse(mJson["num_episodes_watched"].toString());
|
|
track.score = int.parse(mJson["score"].toString());
|
|
track.startedReadingDate = _parseDate(mJson["start_date"]);
|
|
track.finishedReadingDate = _parseDate(mJson["finish_date"]);
|
|
return track;
|
|
}
|
|
|
|
int? _parseDate(String? isoDate) {
|
|
if (isoDate == null) return null;
|
|
|
|
final date = DateFormat('yyyy-MM-dd', 'en_US').parse(isoDate);
|
|
return date.millisecondsSinceEpoch;
|
|
}
|
|
|
|
Future<Track> updateAnime(Track track) async {
|
|
final accessToken = await _getAccesToken();
|
|
final formBody = {
|
|
'status': (toMyAnimeListStatusAnime(track.status) ?? 'watching').toString(),
|
|
'is_rewatching': (track.status == TrackStatus.reWatching).toString(),
|
|
'score': track.score.toString(),
|
|
'num_watched_episodes': track.lastChapterRead.toString(),
|
|
if (track.startedReadingDate != null) 'start_date': _convertToIsoDate(track.startedReadingDate),
|
|
if (track.finishedReadingDate != null) 'finish_date': _convertToIsoDate(track.finishedReadingDate)
|
|
};
|
|
final request = Request('PUT', Uri.parse('$baseApiUrl/anime/${track.mediaId}/my_list_status'));
|
|
request.bodyFields = formBody;
|
|
request.headers.addAll({'Authorization': 'Bearer $accessToken'});
|
|
final response = await Client().send(request);
|
|
final mJson = jsonDecode(await response.stream.bytesToString());
|
|
return _parseAnimeItem(mJson, track);
|
|
}
|
|
|
|
Future<Track> updateManga(Track track) async {
|
|
final accessToken = await _getAccesToken();
|
|
final formBody = {
|
|
'status': (toMyAnimeListStatusManga(track.status) ?? 'reading').toString(),
|
|
'is_rereading': (track.status == TrackStatus.rereading).toString(),
|
|
'score': track.score.toString(),
|
|
'num_chapters_read': track.lastChapterRead.toString(),
|
|
if (track.startedReadingDate != null) 'start_date': _convertToIsoDate(track.startedReadingDate),
|
|
if (track.finishedReadingDate != null) 'finish_date': _convertToIsoDate(track.finishedReadingDate)
|
|
};
|
|
final request = Request('PUT', Uri.parse('$baseApiUrl/manga/${track.mediaId}/my_list_status'));
|
|
request.bodyFields = formBody;
|
|
request.headers.addAll({'Authorization': 'Bearer $accessToken'});
|
|
final response = await Client().send(request);
|
|
final mJson = jsonDecode(await response.stream.bytesToString());
|
|
return _parseMangaItem(mJson, track);
|
|
}
|
|
}
|