mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-21 07:41:58 +00:00
added Trakt.tv
This commit is contained in:
parent
61575f4795
commit
f6a9c41c1d
10 changed files with 703 additions and 13 deletions
BIN
assets/trackers_icons/tracker_trakt.webp
Normal file
BIN
assets/trackers_icons/tracker_trakt.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3 KiB |
|
|
@ -10,6 +10,7 @@ import 'package:mangayomi/services/trackers/base_tracker.dart';
|
|||
import 'package:mangayomi/services/trackers/kitsu.dart';
|
||||
import 'package:mangayomi/services/trackers/myanimelist.dart';
|
||||
import 'package:mangayomi/services/trackers/simkl.dart';
|
||||
import 'package:mangayomi/services/trackers/trakt_tv.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'track_state_providers.g.dart';
|
||||
|
||||
|
|
@ -30,6 +31,9 @@ class TrackState extends _$TrackState {
|
|||
),
|
||||
3 => ref.read(kitsuProvider(syncId: syncId, itemType: itemType).notifier),
|
||||
4 => ref.read(simklProvider(syncId: syncId, itemType: itemType).notifier),
|
||||
5 => ref.read(
|
||||
traktTvProvider(syncId: syncId, itemType: itemType).notifier,
|
||||
),
|
||||
_ => throw Exception('Unsupported syncId: $syncId'),
|
||||
};
|
||||
}
|
||||
|
|
@ -118,6 +122,8 @@ class TrackState extends _$TrackState {
|
|||
findManga = await tracker.update(newTrack, _isManga);
|
||||
} else if (syncId == TrackerProviders.simkl.syncId) {
|
||||
findManga = await tracker.findLibItem(newTrack, _isManga);
|
||||
} else if (syncId == TrackerProviders.trakt.syncId) {
|
||||
findManga = await tracker.findLibItem(newTrack, _isManga);
|
||||
}
|
||||
writeBack(findManga!);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'track_state_providers.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$trackStateHash() => r'a96b4e702c16304cb16604d6b2d7dca5d65ca8b0';
|
||||
String _$trackStateHash() => r'0b2fa471cb843d5880f921f1c721a05b54bc1515';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:mangayomi/models/manga.dart';
|
|||
import 'package:mangayomi/models/track.dart';
|
||||
import 'package:mangayomi/models/track_preference.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/widgets/tracker_widget.dart';
|
||||
import 'package:mangayomi/modules/tracker_library/tracker_library_screen.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
|
|
@ -20,10 +21,14 @@ class TrackingDetail extends StatefulWidget {
|
|||
class _TrackingDetailState extends State<TrackingDetail>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabBarController;
|
||||
bool get isMovies =>
|
||||
widget.trackerPref.syncId == TrackerProviders.simkl.syncId ||
|
||||
widget.trackerPref.syncId == TrackerProviders.trakt.syncId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabBarController = TabController(length: 2, vsync: this);
|
||||
_tabBarController = TabController(length: isMovies ? 1 : 2, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -35,9 +40,10 @@ class _TrackingDetailState extends State<TrackingDetail>
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = l10nLocalizations(context)!;
|
||||
|
||||
return DefaultTabController(
|
||||
animationDuration: Duration.zero,
|
||||
length: 2,
|
||||
length: isMovies ? 1 : 2,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
|
|
@ -51,7 +57,7 @@ class _TrackingDetailState extends State<TrackingDetail>
|
|||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
controller: _tabBarController,
|
||||
tabs: [
|
||||
Tab(text: l10n.manga),
|
||||
if (!isMovies) Tab(text: l10n.manga),
|
||||
Tab(text: l10n.anime),
|
||||
],
|
||||
),
|
||||
|
|
@ -59,10 +65,11 @@ class _TrackingDetailState extends State<TrackingDetail>
|
|||
body: TabBarView(
|
||||
controller: _tabBarController,
|
||||
children: [
|
||||
TrackingTab(
|
||||
itemType: ItemType.manga,
|
||||
syncId: widget.trackerPref.syncId!,
|
||||
),
|
||||
if (!isMovies)
|
||||
TrackingTab(
|
||||
itemType: ItemType.manga,
|
||||
syncId: widget.trackerPref.syncId!,
|
||||
),
|
||||
TrackingTab(
|
||||
itemType: ItemType.anime,
|
||||
syncId: widget.trackerPref.syncId!,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'package:mangayomi/services/trackers/anilist.dart';
|
|||
import 'package:mangayomi/services/trackers/kitsu.dart';
|
||||
import 'package:mangayomi/services/trackers/myanimelist.dart';
|
||||
import 'package:mangayomi/services/trackers/simkl.dart';
|
||||
import 'package:mangayomi/services/trackers/trakt_tv.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
|
||||
class TrackScreen extends ConsumerWidget {
|
||||
|
|
@ -113,6 +114,20 @@ class TrackScreen extends ConsumerWidget {
|
|||
id: TrackerProviders.simkl.syncId,
|
||||
entries: entries,
|
||||
),
|
||||
TrackListile(
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(
|
||||
traktTvProvider(
|
||||
syncId: TrackerProviders.trakt.syncId,
|
||||
itemType: null,
|
||||
).notifier,
|
||||
)
|
||||
.login();
|
||||
},
|
||||
id: TrackerProviders.trakt.syncId,
|
||||
entries: entries,
|
||||
),
|
||||
ListTile(
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ enum TrackerProviders {
|
|||
myAnimeList(syncId: 1, name: "MAL"),
|
||||
anilist(syncId: 2, name: "AL"),
|
||||
kitsu(syncId: 3, name: "Kitsu"),
|
||||
simkl(syncId: 4, name: "Simkl");
|
||||
simkl(syncId: 4, name: "Simkl"),
|
||||
trakt(syncId: 5, name: "Trakt");
|
||||
|
||||
const TrackerProviders({required this.syncId, required this.name});
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
|
|||
2 => _sectionsAL(trackerProvider.syncId, itemType),
|
||||
3 => _sectionsKitsu(trackerProvider.syncId, itemType),
|
||||
4 => _sectionsSimkl(trackerProvider.syncId, itemType),
|
||||
5 => _sectionsTrakt(trackerProvider.syncId, itemType),
|
||||
_ => [],
|
||||
};
|
||||
if (_isSearch && _query.isNotEmpty) {
|
||||
|
|
@ -87,7 +89,10 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
|
|||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
"${trackerProvider.name} | ${itemType == ItemType.anime ? l10n.anime : l10n.manga}",
|
||||
(trackerProvider.syncId == TrackerProviders.simkl.syncId ||
|
||||
trackerProvider.syncId == TrackerProviders.trakt.syncId)
|
||||
? trackerProvider.name
|
||||
: "${trackerProvider.name} | ${itemType == ItemType.anime ? l10n.anime : l10n.manga}",
|
||||
),
|
||||
leading: !_isSearch ? null : Container(),
|
||||
actions: [
|
||||
|
|
@ -212,6 +217,67 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
List<TrackLibrarySection> _sectionsTrakt(int syncId, ItemType itemType) {
|
||||
return [
|
||||
TrackLibrarySection(
|
||||
name: "Continue watching movies",
|
||||
syncId: syncId,
|
||||
func: _fetchUserData(syncId, ItemType.manga),
|
||||
itemType: ItemType.anime,
|
||||
),
|
||||
TrackLibrarySection(
|
||||
name: "Continue watching series",
|
||||
syncId: syncId,
|
||||
func: _fetchUserData(syncId, ItemType.anime),
|
||||
itemType: ItemType.anime,
|
||||
),
|
||||
TrackLibrarySection(
|
||||
name: "Trending Movies",
|
||||
syncId: syncId,
|
||||
func: _fetchGeneralData(syncId, ItemType.manga),
|
||||
itemType: ItemType.anime,
|
||||
),
|
||||
TrackLibrarySection(
|
||||
name: "Trending Series",
|
||||
syncId: syncId,
|
||||
func: _fetchGeneralData(syncId, ItemType.anime),
|
||||
itemType: ItemType.anime,
|
||||
),
|
||||
TrackLibrarySection(
|
||||
name: "Popular Movies",
|
||||
syncId: syncId,
|
||||
func: _fetchGeneralData(syncId, ItemType.manga, rankingType: "popular"),
|
||||
itemType: ItemType.anime,
|
||||
),
|
||||
TrackLibrarySection(
|
||||
name: "Popular Series",
|
||||
syncId: syncId,
|
||||
func: _fetchGeneralData(syncId, ItemType.anime, rankingType: "popular"),
|
||||
itemType: ItemType.anime,
|
||||
),
|
||||
TrackLibrarySection(
|
||||
name: "Top Movies (All Time)",
|
||||
syncId: syncId,
|
||||
func: _fetchGeneralData(
|
||||
syncId,
|
||||
ItemType.manga,
|
||||
rankingType: "favorited/all",
|
||||
),
|
||||
itemType: ItemType.anime,
|
||||
),
|
||||
TrackLibrarySection(
|
||||
name: "Top Series (All Time)",
|
||||
syncId: syncId,
|
||||
func: _fetchGeneralData(
|
||||
syncId,
|
||||
ItemType.anime,
|
||||
rankingType: "favorited/all",
|
||||
),
|
||||
itemType: ItemType.anime,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<TrackLibrarySection> _sectionsSimkl(int syncId, ItemType itemType) {
|
||||
return [
|
||||
TrackLibrarySection(
|
||||
|
|
@ -548,6 +614,7 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
|
|||
_getListile(l10n, TrackerProviders.anilist.syncId),
|
||||
_getListile(l10n, TrackerProviders.kitsu.syncId),
|
||||
_getListile(l10n, TrackerProviders.simkl.syncId),
|
||||
_getListile(l10n, TrackerProviders.trakt.syncId),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
@ -604,7 +671,9 @@ class _TrackerLibraryScreenState extends ConsumerState<TrackerLibraryScreen> {
|
|||
),
|
||||
enabled: isLoggedIn,
|
||||
onTap: () {
|
||||
if (isManga == null && syncId != TrackerProviders.simkl.syncId) {
|
||||
if (isManga == null &&
|
||||
syncId != TrackerProviders.simkl.syncId &&
|
||||
syncId != TrackerProviders.trakt.syncId) {
|
||||
context.pop();
|
||||
_openSwitchTypeDialog(l10n, syncId);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'simkl.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$simklHash() => r'7385cd7925dff352509f1b51d4ab2eb6af733b15';
|
||||
String _$simklHash() => r'3b8ff48675ba743d39aef595dc6cd70f4bd404cf';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
|
|
|||
394
lib/services/trackers/trakt_tv.dart
Normal file
394
lib/services/trackers/trakt_tv.dart
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
|
||||
import 'package:http_interceptor/http_interceptor.dart';
|
||||
import 'package:mangayomi/eval/model/m_bridge.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/track.dart';
|
||||
import 'package:mangayomi/models/track_preference.dart';
|
||||
import 'package:mangayomi/models/track_search.dart';
|
||||
import 'package:mangayomi/modules/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 'base_tracker.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'trakt_tv.g.dart';
|
||||
|
||||
@riverpod
|
||||
class TraktTv extends _$TraktTv implements BaseTracker {
|
||||
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
|
||||
static const _baseOAuthUrl = 'https://api.trakt.tv/oauth';
|
||||
static const _baseApiUrl = 'https://api.trakt.tv';
|
||||
static final _isDesktop = (Platform.isWindows || Platform.isLinux);
|
||||
static const _redirectUri = 'http://localhost:43824';
|
||||
static const _clientId =
|
||||
'5520c7e24da0d8d73ec80315b61b9849483583b013cb7f296c6db723eb9886a1';
|
||||
static const _clientSecret =
|
||||
'b512565ad92d4179290de257b6e435d03ee47b2e4371b3bd918081beb6121734';
|
||||
|
||||
String getFallbackClientId(String usedId) {
|
||||
return _clientId;
|
||||
}
|
||||
|
||||
@override
|
||||
void build({required int syncId, required ItemType? itemType}) {}
|
||||
|
||||
Future<bool?> login() async {
|
||||
final callbackUrlScheme = _isDesktop
|
||||
? 'http://localhost:43824'
|
||||
: 'mangayomi';
|
||||
final loginUrl = _authUrl();
|
||||
|
||||
try {
|
||||
final uri = await FlutterWebAuth2.authenticate(
|
||||
url: "$loginUrl&redirect_uri=$callbackUrlScheme",
|
||||
callbackUrlScheme: callbackUrlScheme,
|
||||
);
|
||||
final code = Uri.parse(uri).queryParameters['code'];
|
||||
if (code == null) return null;
|
||||
|
||||
final oAuthData = await _getOAuth(code);
|
||||
final oAuth = _buildOAuth(oAuthData, _clientId);
|
||||
final username = await _getUserName(oAuth.accessToken!);
|
||||
_saveOAuth(username, oAuth);
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TrackSearch>> fetchGeneralData({
|
||||
bool isManga = true,
|
||||
String rankingType = "trending",
|
||||
}) async {
|
||||
/// isManga <> isMovie
|
||||
final type = isManga ? "movies" : "shows";
|
||||
final accessToken = await _getAccessToken();
|
||||
final url = Uri.parse('$_baseApiUrl/$type/$rankingType').replace(
|
||||
queryParameters: {
|
||||
'extended': 'full',
|
||||
'clientId': _clientId,
|
||||
'limit': '15',
|
||||
},
|
||||
);
|
||||
final result = await _makeGetRequest(url, accessToken);
|
||||
final data = jsonDecode(result.body) as List?;
|
||||
return data?.map((e) {
|
||||
final type = isManga ? "movie" : "show";
|
||||
final typeName = type == 'movie' ? 'movies' : 'shows';
|
||||
return TrackSearch(
|
||||
mediaId: e['ids']?['trakt'] ?? e[type]?['ids']?['trakt'],
|
||||
summary:
|
||||
e['overview'] ??
|
||||
e[type]?['overview'] ??
|
||||
'No summary available.',
|
||||
totalChapter:
|
||||
e['aired_episodes'] ?? e[type]?['aired_episodes'] ?? 1,
|
||||
coverUrl: 'https://wsrv.nl/?url=wsrv.nl/lichtenstein.jpg',
|
||||
title: e['title'] ?? e[type]?['title'] ?? 'Unknown Title',
|
||||
score: double.tryParse(
|
||||
(e["rating"] ?? e[type]?["rating"] as num?)
|
||||
?.toDouble()
|
||||
.toStringAsFixed(2) ??
|
||||
"",
|
||||
),
|
||||
startDate: e["first_aired"] ?? e[type]?["first_aired"] ?? "",
|
||||
publishingType: type,
|
||||
publishingStatus: e["status"] ?? e[type]["status"],
|
||||
trackingUrl:
|
||||
"https://trakt.tv/$typeName/${e['ids']?['slug'] ?? e[type]?['ids']?['slug']}",
|
||||
syncId: syncId,
|
||||
);
|
||||
}).toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TrackSearch>> fetchUserData({bool isManga = true}) async {
|
||||
final type = isManga ? "movies" : "shows";
|
||||
final accessToken = await _getAccessToken();
|
||||
final url = Uri.parse(
|
||||
'$_baseApiUrl/sync/watched/$type',
|
||||
).replace(queryParameters: {"extended": "full"});
|
||||
final result = await _makeGetRequest(url, accessToken);
|
||||
final data = jsonDecode(result.body) as List?;
|
||||
return data?.map((e) {
|
||||
final type = e['movie'] != null ? "movie" : "show";
|
||||
final typeName = type == 'movie' ? 'movies' : 'shows';
|
||||
return TrackSearch(
|
||||
mediaId: e[type]?['ids']?['trakt'],
|
||||
summary: e[type]?['overview'] ?? 'No summary available.',
|
||||
totalChapter: e[type]?['aired_episodes'] ?? 1,
|
||||
coverUrl: 'https://wsrv.nl/?url=wsrv.nl/lichtenstein.jpg',
|
||||
title: e[type]['title'] ?? 'Unknown Title',
|
||||
score: double.tryParse(
|
||||
(e[type]?["rating"] as num?)?.toDouble().toStringAsFixed(2) ?? "",
|
||||
),
|
||||
startDate: e[type]?["first_aired"] ?? "",
|
||||
publishingType: type,
|
||||
publishingStatus: e[type]["status"],
|
||||
trackingUrl:
|
||||
"https://trakt.tv/$typeName/${e[type]?['ids']?['slug']}",
|
||||
syncId: syncId,
|
||||
);
|
||||
}).toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Track?> findLibItem(Track track, bool isManga) async {
|
||||
final accessToken = await _getAccessToken();
|
||||
final isMovie =
|
||||
track.trackingUrl?.replaceAll("https://trakt.tv/", "").split("/")[0] ==
|
||||
"movies";
|
||||
final url = Uri.parse(
|
||||
'$_baseApiUrl/sync/history/${isMovie ? "movies" : "shows"}/${track.mediaId}',
|
||||
).replace(queryParameters: {"extended": "full"});
|
||||
final result = await _makeGetRequest(url, accessToken);
|
||||
final data = jsonDecode(result.body) as List?;
|
||||
if (data?.isNotEmpty ?? false) {
|
||||
if (!isMovie) {
|
||||
track.lastChapterRead = data!
|
||||
.where((e) => e["type"] == "episode")
|
||||
.length;
|
||||
}
|
||||
if ((track.lastChapterRead ?? 0) >= (track.totalChapter ?? 0)) {
|
||||
track.finishedReadingDate = DateTime.tryParse(
|
||||
data!.firstOrNull?["watched_at"],
|
||||
)?.millisecondsSinceEpoch;
|
||||
}
|
||||
return track;
|
||||
}
|
||||
return await update(track, isManga);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TrackSearch>> search(String query, bool isManga) async {
|
||||
final accessToken = await _getAccessToken();
|
||||
final url = Uri.parse('$_baseApiUrl/search/movie,show').replace(
|
||||
queryParameters: {
|
||||
'query': query,
|
||||
'extended': 'full',
|
||||
'clientId': _clientId,
|
||||
'limit': '15',
|
||||
},
|
||||
);
|
||||
final result = await _makeGetRequest(url, accessToken);
|
||||
final data = jsonDecode(result.body) as List?;
|
||||
return data?.map((e) {
|
||||
final type = e['type'];
|
||||
final typeName = e['type'] == 'movie' ? 'movies' : 'shows';
|
||||
return TrackSearch(
|
||||
mediaId: e[type]?['ids']?['trakt'],
|
||||
summary: e[type]?['overview'] ?? 'No summary available.',
|
||||
totalChapter: e[type]?['aired_episodes'] ?? 1,
|
||||
coverUrl: 'https://wsrv.nl/?url=wsrv.nl/lichtenstein.jpg',
|
||||
title: e[type]['title'] ?? 'Unknown Title',
|
||||
score: (e[type]?["rating"] as num?)?.toDouble(),
|
||||
startDate: e[type]?["first_aired"] ?? "",
|
||||
publishingType: type,
|
||||
publishingStatus: e[type]["status"],
|
||||
trackingUrl:
|
||||
"https://trakt.tv/$typeName/${e[type]?['ids']?['slug']}",
|
||||
syncId: syncId,
|
||||
);
|
||||
}).toList() ??
|
||||
[];
|
||||
}
|
||||
|
||||
@override
|
||||
List<TrackStatus> statusList(bool isManga) => [];
|
||||
|
||||
@override
|
||||
Future<Track> update(Track track, bool isManga) async {
|
||||
final accessToken = await _getAccessToken();
|
||||
final isMovie =
|
||||
track.trackingUrl?.replaceAll("https://trakt.tv/", "").split("/")[0] ==
|
||||
"movies";
|
||||
final urlRemove = Uri.parse(
|
||||
"$_baseApiUrl/sync/history/remove",
|
||||
).replace(queryParameters: {'clientId': _clientId});
|
||||
final bodyRemove = isMovie
|
||||
? {
|
||||
'movies': [
|
||||
{
|
||||
'ids': {'trakt': track.mediaId},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
'shows': [
|
||||
{
|
||||
'ids': {'trakt': track.mediaId},
|
||||
},
|
||||
],
|
||||
};
|
||||
await _makePostRequest(urlRemove, accessToken, bodyRemove);
|
||||
final url = Uri.parse(
|
||||
"$_baseApiUrl/sync/history",
|
||||
).replace(queryParameters: {'extended': 'full', 'clientId': _clientId});
|
||||
final body = isMovie
|
||||
? {
|
||||
'movies': [
|
||||
{
|
||||
'watched_at': DateTime.now().toIso8601String(),
|
||||
'ids': {'trakt': track.mediaId},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
'shows': [
|
||||
{
|
||||
'watched_at': DateTime.now().toIso8601String(),
|
||||
'ids': {'trakt': track.mediaId},
|
||||
'seasons': [
|
||||
{
|
||||
'number': 1,
|
||||
'episodes': [
|
||||
for (int i = 1; i <= (track.lastChapterRead ?? 1); i++)
|
||||
{
|
||||
'watched_at': DateTime.now().toIso8601String(),
|
||||
'number': i,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
await _makePostRequest(url, accessToken, body);
|
||||
return track;
|
||||
}
|
||||
|
||||
Future<String> _getAccessToken() async {
|
||||
final track = ref.read(tracksProvider(syncId: syncId));
|
||||
final mALOAuth = OAuth.fromJson(
|
||||
jsonDecode(track!.oAuth!) as Map<String, dynamic>,
|
||||
);
|
||||
final expiresIn = DateTime.fromMillisecondsSinceEpoch(mALOAuth.expiresIn!);
|
||||
if (DateTime.now().isBefore(expiresIn)) return mALOAuth.accessToken!;
|
||||
final refreshed = await _tryRefreshToken(mALOAuth);
|
||||
if (refreshed == null) {
|
||||
ref.read(tracksProvider(syncId: syncId).notifier).logout();
|
||||
botToast("Trakt.tv Token expired");
|
||||
throw Exception("Token expired");
|
||||
}
|
||||
final username = await _getUserName(refreshed.accessToken!);
|
||||
_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: {
|
||||
'refresh_token': oldOAuth.refreshToken,
|
||||
'client_id': cid,
|
||||
'client_secret': _clientSecret,
|
||||
'redirect_uri': _redirectUri,
|
||||
'grant_type': 'refresh_token',
|
||||
},
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
);
|
||||
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()
|
||||
.add(Duration(seconds: json['expires_in']))
|
||||
.millisecondsSinceEpoch
|
||||
..clientId = clientId;
|
||||
}
|
||||
|
||||
void _saveOAuth(String username, OAuth oAuth) {
|
||||
ref
|
||||
.read(tracksProvider(syncId: syncId).notifier)
|
||||
.login(
|
||||
TrackPreference(
|
||||
syncId: syncId,
|
||||
username: username,
|
||||
prefs: "",
|
||||
oAuth: jsonEncode(oAuth.toJson()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> _getUserName(String accessToken) async {
|
||||
final response = await _makeGetRequest(
|
||||
Uri.parse('$_baseApiUrl/users/settings'),
|
||||
accessToken,
|
||||
);
|
||||
return "${jsonDecode(response.body)['user']['username']}";
|
||||
}
|
||||
|
||||
String _authUrl() {
|
||||
return '$_baseOAuthUrl/authorize?client_id=$_clientId&response_type=code';
|
||||
}
|
||||
|
||||
Future<dynamic> _getOAuth(String code) async {
|
||||
final params = {
|
||||
'code': code,
|
||||
'client_id': _clientId,
|
||||
'client_secret': _clientSecret,
|
||||
'redirect_uri': _redirectUri,
|
||||
'grant_type': 'authorization_code',
|
||||
};
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseApiUrl/oauth/token'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(params),
|
||||
);
|
||||
return jsonDecode(response.body);
|
||||
}
|
||||
|
||||
Future<Response> _makeGetRequest(Uri url, String accessToken) async {
|
||||
return await http.get(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $accessToken',
|
||||
'trakt-api-key': _clientId,
|
||||
'trakt-api-version': "2",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<Response> _makePostRequest(
|
||||
Uri url,
|
||||
String accessToken,
|
||||
Object? body,
|
||||
) async {
|
||||
return await http.post(
|
||||
url,
|
||||
headers: {
|
||||
'Authorization': 'Bearer $accessToken',
|
||||
'trakt-api-key': _clientId,
|
||||
'trakt-api-version': "2",
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String displayScore(int score) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
(int, int) getScoreValue() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
194
lib/services/trackers/trakt_tv.g.dart
Normal file
194
lib/services/trackers/trakt_tv.g.dart
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'trakt_tv.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$traktTvHash() => r'de97ae0edbc905d07af2ce8758441fba6cdd7be2';
|
||||
|
||||
/// 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 _$TraktTv extends BuildlessAutoDisposeNotifier<void> {
|
||||
late final int syncId;
|
||||
late final ItemType? itemType;
|
||||
|
||||
void build({
|
||||
required int syncId,
|
||||
required ItemType? itemType,
|
||||
});
|
||||
}
|
||||
|
||||
/// See also [TraktTv].
|
||||
@ProviderFor(TraktTv)
|
||||
const traktTvProvider = TraktTvFamily();
|
||||
|
||||
/// See also [TraktTv].
|
||||
class TraktTvFamily extends Family<void> {
|
||||
/// See also [TraktTv].
|
||||
const TraktTvFamily();
|
||||
|
||||
/// See also [TraktTv].
|
||||
TraktTvProvider call({
|
||||
required int syncId,
|
||||
required ItemType? itemType,
|
||||
}) {
|
||||
return TraktTvProvider(
|
||||
syncId: syncId,
|
||||
itemType: itemType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TraktTvProvider getProviderOverride(
|
||||
covariant TraktTvProvider provider,
|
||||
) {
|
||||
return call(
|
||||
syncId: provider.syncId,
|
||||
itemType: provider.itemType,
|
||||
);
|
||||
}
|
||||
|
||||
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'traktTvProvider';
|
||||
}
|
||||
|
||||
/// See also [TraktTv].
|
||||
class TraktTvProvider extends AutoDisposeNotifierProviderImpl<TraktTv, void> {
|
||||
/// See also [TraktTv].
|
||||
TraktTvProvider({
|
||||
required int syncId,
|
||||
required ItemType? itemType,
|
||||
}) : this._internal(
|
||||
() => TraktTv()
|
||||
..syncId = syncId
|
||||
..itemType = itemType,
|
||||
from: traktTvProvider,
|
||||
name: r'traktTvProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$traktTvHash,
|
||||
dependencies: TraktTvFamily._dependencies,
|
||||
allTransitiveDependencies: TraktTvFamily._allTransitiveDependencies,
|
||||
syncId: syncId,
|
||||
itemType: itemType,
|
||||
);
|
||||
|
||||
TraktTvProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.syncId,
|
||||
required this.itemType,
|
||||
}) : super.internal();
|
||||
|
||||
final int syncId;
|
||||
final ItemType? itemType;
|
||||
|
||||
@override
|
||||
void runNotifierBuild(
|
||||
covariant TraktTv notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
syncId: syncId,
|
||||
itemType: itemType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(TraktTv Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: TraktTvProvider._internal(
|
||||
() => create()
|
||||
..syncId = syncId
|
||||
..itemType = itemType,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
syncId: syncId,
|
||||
itemType: itemType,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<TraktTv, void> createElement() {
|
||||
return _TraktTvProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is TraktTvProvider &&
|
||||
other.syncId == syncId &&
|
||||
other.itemType == itemType;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, syncId.hashCode);
|
||||
hash = _SystemHash.combine(hash, itemType.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin TraktTvRef on AutoDisposeNotifierProviderRef<void> {
|
||||
/// The parameter `syncId` of this provider.
|
||||
int get syncId;
|
||||
|
||||
/// The parameter `itemType` of this provider.
|
||||
ItemType? get itemType;
|
||||
}
|
||||
|
||||
class _TraktTvProviderElement
|
||||
extends AutoDisposeNotifierProviderElement<TraktTv, void> with TraktTvRef {
|
||||
_TraktTvProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
int get syncId => (origin as TraktTvProvider).syncId;
|
||||
@override
|
||||
ItemType? get itemType => (origin as TraktTvProvider).itemType;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
|
|
@ -72,11 +72,16 @@ TrackStatus toTrackStatus(TrackStatus status, ItemType itemType, int syncId) {
|
|||
"Kitsu",
|
||||
const Color.fromRGBO(18, 25, 35, 1),
|
||||
),
|
||||
_ => (
|
||||
4 => (
|
||||
"assets/trackers_icons/tracker_simkl.png",
|
||||
"Simkl",
|
||||
const Color.fromARGB(255, 35, 15, 90),
|
||||
),
|
||||
_ => (
|
||||
"assets/trackers_icons/tracker_trakt.webp",
|
||||
"Trakt",
|
||||
const Color.fromARGB(255, 90, 51, 81),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue