This commit is contained in:
NBA2K1 2025-07-25 22:18:44 +02:00
parent c74a4c428e
commit 4bad1a24e3

View file

@ -18,23 +18,23 @@ part 'myanimelist.g.dart';
@riverpod @riverpod
class MyAnimeList extends _$MyAnimeList { class MyAnimeList extends _$MyAnimeList {
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true}); final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
String baseOAuthUrl = 'https://myanimelist.net/v1/oauth2'; static const _baseOAuthUrl = 'https://myanimelist.net/v1/oauth2';
String baseApiUrl = 'https://api.myanimelist.net/v2'; static const _baseApiUrl = 'https://api.myanimelist.net/v2';
String codeVerifier = ""; String codeVerifier = "";
static final isDesktop = (Platform.isWindows || Platform.isLinux); static final _isDesktop = (Platform.isWindows || Platform.isLinux);
static const String desktopClientId = '39e9be346b4e7dbcc59a98357e2f8472'; static const _desktopClientId = '39e9be346b4e7dbcc59a98357e2f8472';
static const String mobileClientId = '0c9100ccd443ddb441a319a881180f7f'; static const _mobileClientId = '0c9100ccd443ddb441a319a881180f7f';
String clientId = isDesktop ? desktopClientId : mobileClientId; final _clientId = _isDesktop ? _desktopClientId : _mobileClientId;
String getFallbackClientId(String usedClientId) { String getFallbackClientId(String usedId) {
return usedClientId == desktopClientId ? mobileClientId : desktopClientId; return usedId == _desktopClientId ? _mobileClientId : _desktopClientId;
} }
@override @override
void build({required int syncId, required ItemType? itemType}) {} void build({required int syncId, required ItemType? itemType}) {}
Future<bool?> login() async { Future<bool?> login() async {
final callbackUrlScheme = isDesktop final callbackUrlScheme = _isDesktop
? 'http://localhost:43824' ? 'http://localhost:43824'
: 'mangayomi'; : 'mangayomi';
final loginUrl = _authUrl(); final loginUrl = _authUrl();
@ -44,25 +44,13 @@ class MyAnimeList extends _$MyAnimeList {
url: loginUrl, url: loginUrl,
callbackUrlScheme: callbackUrlScheme, callbackUrlScheme: callbackUrlScheme,
); );
final queryParams = Uri.parse(uri).queryParameters; final code = Uri.parse(uri).queryParameters['code'];
if (queryParams['code'] == null) return null; if (code == null) return null;
final oAuth = await _getOAuth(queryParams['code']!); final oAuthData = await _getOAuth(code);
final mALOAuth = OAuth.fromJson(oAuth as Map<String, dynamic>) final oAuth = _buildOAuth(oAuthData, _clientId);
..expiresIn = DateTime.now() final username = await _getUserName(oAuth.accessToken!);
.add(Duration(seconds: oAuth['expires_in'])) _saveOAuth(username, oAuth);
.millisecondsSinceEpoch
..clientId = clientId;
final username = await _getUserName(mALOAuth.accessToken!);
ref
.read(tracksProvider(syncId: syncId).notifier)
.login(
TrackPreference(
syncId: syncId,
username: username,
oAuth: jsonEncode(mALOAuth.toJson()),
),
);
return true; return true;
} catch (_) { } catch (_) {
@ -76,37 +64,48 @@ class MyAnimeList extends _$MyAnimeList {
jsonDecode(track!.oAuth!) as Map<String, dynamic>, jsonDecode(track!.oAuth!) as Map<String, dynamic>,
); );
final expiresIn = DateTime.fromMillisecondsSinceEpoch(mALOAuth.expiresIn!); final expiresIn = DateTime.fromMillisecondsSinceEpoch(mALOAuth.expiresIn!);
if (!DateTime.now().isAfter(expiresIn)) return mALOAuth.accessToken!; if (DateTime.now().isBefore(expiresIn)) return mALOAuth.accessToken!;
String primaryClientId = mALOAuth.clientId ?? clientId; final refreshed = await _tryRefreshToken(mALOAuth);
Map<String, String?> params = { if (refreshed == null) {
'client_id': primaryClientId,
'grant_type': 'refresh_token',
'refresh_token': mALOAuth.refreshToken,
};
Response response = await http.post(
Uri.parse('$baseOAuthUrl/token'),
body: params,
);
if (response.statusCode != 200) {
// Try the fallback client ID (desktop <-> mobile)
params['client_id'] = getFallbackClientId(primaryClientId);
response = await http.post(
Uri.parse('$baseOAuthUrl/token'),
body: params,
);
}
if (response.statusCode != 200) {
ref.read(tracksProvider(syncId: syncId).notifier).logout(); ref.read(tracksProvider(syncId: syncId).notifier).logout();
botToast("MyAnimeList Token expired"); botToast("MyAnimeList Token expired");
throw Exception("Token expired"); throw Exception("Token expired");
} }
final body = jsonDecode(response.body) as Map<String, dynamic>; final username = await _getUserName(refreshed.accessToken!);
final oAuth = OAuth.fromJson(body) _saveOAuth(username, refreshed);
return refreshed.accessToken!;
}
Future<OAuth?> _tryRefreshToken(OAuth oldOAuth) async {
String primaryClientId = oldOAuth.clientId ?? _clientId;
Future<OAuth?> tryRefresh(String cid) async {
final response = await http.post(
Uri.parse('$_baseOAuthUrl/token'),
body: {
'client_id': cid,
'grant_type': 'refresh_token',
'refresh_token': oldOAuth.refreshToken,
},
);
if (response.statusCode != 200) return null;
final body = jsonDecode(response.body) as Map<String, dynamic>;
return _buildOAuth(body, cid);
}
return await tryRefresh(primaryClientId) ??
await tryRefresh(getFallbackClientId(primaryClientId));
}
OAuth _buildOAuth(Map<String, dynamic> json, String clientId) {
return OAuth.fromJson(json)
..expiresIn = DateTime.now() ..expiresIn = DateTime.now()
.add(Duration(seconds: body['expires_in'])) .add(Duration(seconds: json['expires_in']))
.millisecondsSinceEpoch .millisecondsSinceEpoch
..clientId = params['client_id']; ..clientId = clientId;
final username = await _getUserName(oAuth.accessToken!); }
void _saveOAuth(String username, OAuth oAuth) {
ref ref
.read(tracksProvider(syncId: syncId).notifier) .read(tracksProvider(syncId: syncId).notifier)
.login( .login(
@ -117,13 +116,12 @@ class MyAnimeList extends _$MyAnimeList {
oAuth: jsonEncode(oAuth.toJson()), oAuth: jsonEncode(oAuth.toJson()),
), ),
); );
return oAuth.accessToken!;
} }
Future<List<TrackSearch>> search(String query, isManga) async { Future<List<TrackSearch>> search(String query, isManga) async {
final accessToken = await _getAccessToken(); final accessToken = await _getAccessToken();
final url = Uri.parse( final url = Uri.parse(
'$baseApiUrl/${isManga ? "manga" : "anime"}', '$_baseApiUrl/${isManga ? "manga" : "anime"}',
).replace(queryParameters: {'q': query.trim(), 'nsfw': 'true'}); ).replace(queryParameters: {'q': query.trim(), 'nsfw': 'true'});
final result = await _makeGetRequest(url, accessToken); final result = await _makeGetRequest(url, accessToken);
final res = jsonDecode(result.body) as Map<String, dynamic>; final res = jsonDecode(result.body) as Map<String, dynamic>;
@ -147,7 +145,7 @@ class MyAnimeList extends _$MyAnimeList {
) async { ) async {
final item = isManga ? "manga" : "anime"; final item = isManga ? "manga" : "anime";
final contentUnit = isManga ? "num_chapters" : "num_episodes"; final contentUnit = isManga ? "num_chapters" : "num_episodes";
final url = Uri.parse('$baseApiUrl/$item/$id').replace( final url = Uri.parse('$_baseApiUrl/$item/$id').replace(
queryParameters: { queryParameters: {
'fields': 'fields':
'id,title,synopsis,$contentUnit,main_picture,status,media_type,start_date,mean', 'id,title,synopsis,$contentUnit,main_picture,status,media_type,start_date,mean',
@ -179,7 +177,7 @@ class MyAnimeList extends _$MyAnimeList {
final accessToken = await _getAccessToken(); final accessToken = await _getAccessToken();
final item = isManga ? "manga" : "anime"; final item = isManga ? "manga" : "anime";
final contentUnit = isManga ? "num_chapters" : "num_episodes"; final contentUnit = isManga ? "num_chapters" : "num_episodes";
final url = Uri.parse('$baseApiUrl/$item/ranking').replace( final url = Uri.parse('$_baseApiUrl/$item/ranking').replace(
queryParameters: { queryParameters: {
'ranking_type': rankingType, 'ranking_type': rankingType,
'limit': '15', 'limit': '15',
@ -216,7 +214,7 @@ class MyAnimeList extends _$MyAnimeList {
final item = isManga ? "mangalist" : "animelist"; final item = isManga ? "mangalist" : "animelist";
final contentUnit = isManga ? "num_chapters" : "num_episodes"; final contentUnit = isManga ? "num_chapters" : "num_episodes";
final currentStatus = isManga ? "reading" : "watching"; final currentStatus = isManga ? "reading" : "watching";
final url = Uri.parse('$baseApiUrl/users/@me/$item').replace( final url = Uri.parse('$_baseApiUrl/users/@me/$item').replace(
queryParameters: { queryParameters: {
'status': currentStatus, 'status': currentStatus,
utf8.decode([110, 115, 102, 119]): 'true', utf8.decode([110, 115, 102, 119]): 'true',
@ -278,7 +276,7 @@ class MyAnimeList extends _$MyAnimeList {
String _authUrl() { String _authUrl() {
_codeVerifier(); _codeVerifier();
return '$baseOAuthUrl/authorize?client_id=$clientId&code_challenge=$codeVerifier&response_type=code'; return '$_baseOAuthUrl/authorize?client_id=$_clientId&code_challenge=$codeVerifier&response_type=code';
} }
TrackStatus _getMALTrackStatus(String status, bool isManga) { TrackStatus _getMALTrackStatus(String status, bool isManga) {
@ -331,13 +329,13 @@ class MyAnimeList extends _$MyAnimeList {
Future<dynamic> _getOAuth(String code) async { Future<dynamic> _getOAuth(String code) async {
final params = { final params = {
'client_id': clientId, 'client_id': _clientId,
'code': code, 'code': code,
'code_verifier': codeVerifier, 'code_verifier': codeVerifier,
'grant_type': 'authorization_code', 'grant_type': 'authorization_code',
}; };
final response = await http.post( final response = await http.post(
Uri.parse('$baseOAuthUrl/token'), Uri.parse('$_baseOAuthUrl/token'),
body: params, body: params,
); );
return jsonDecode(response.body); return jsonDecode(response.body);
@ -345,7 +343,7 @@ class MyAnimeList extends _$MyAnimeList {
Future<String> _getUserName(String accessToken) async { Future<String> _getUserName(String accessToken) async {
final response = await _makeGetRequest( final response = await _makeGetRequest(
Uri.parse('$baseApiUrl/users/@me'), Uri.parse('$_baseApiUrl/users/@me'),
accessToken, accessToken,
); );
return jsonDecode(response.body)['name']; return jsonDecode(response.body)['name'];
@ -355,7 +353,7 @@ class MyAnimeList extends _$MyAnimeList {
final type = isManga ? "manga" : "anime"; final type = isManga ? "manga" : "anime";
final contentUnit = isManga ? 'num_chapters' : 'num_episodes'; final contentUnit = isManga ? 'num_chapters' : 'num_episodes';
final accessToken = await _getAccessToken(); final accessToken = await _getAccessToken();
final uri = Uri.parse('$baseApiUrl/$type/${track.mediaId}').replace( final uri = Uri.parse('$_baseApiUrl/$type/${track.mediaId}').replace(
queryParameters: { queryParameters: {
'fields': '$contentUnit,my_list_status{start_date,finish_date}', 'fields': '$contentUnit,my_list_status{start_date,finish_date}',
}, },
@ -416,7 +414,7 @@ class MyAnimeList extends _$MyAnimeList {
final request = Request( final request = Request(
'PUT', 'PUT',
Uri.parse( Uri.parse(
'$baseApiUrl/${isManga ? "manga" : "anime"}' '$_baseApiUrl/${isManga ? "manga" : "anime"}'
'/${track.mediaId}/my_list_status', '/${track.mediaId}/my_list_status',
), ),
); );