mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-03-11 17:15:39 +00:00
Project import generated by Copybara.
GitOrigin-RevId: ac3722bf830b8d24ba6123ca5646b837de017eb7
This commit is contained in:
parent
43e84608a0
commit
2473e0f513
7 changed files with 376 additions and 254 deletions
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:pocketbase/pocketbase.dart';
|
||||
|
||||
|
|
@ -324,9 +325,15 @@ class Meta extends LibraryItem {
|
|||
}
|
||||
|
||||
Video? get currentVideo {
|
||||
return videos?.firstWhere((episode) {
|
||||
return nextEpisode == episode.episode && nextSeason == episode.season;
|
||||
});
|
||||
if (type == "movie") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return videos?.firstWhereOrNull(
|
||||
(episode) {
|
||||
return nextEpisode == episode.episode && nextSeason == episode.season;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Meta({
|
||||
|
|
|
|||
|
|
@ -255,11 +255,31 @@ class StremioCard extends StatelessWidget {
|
|||
child: (backgroundImage == null)
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(meta.name ?? "No name"),
|
||||
if (meta.description != null) Text(meta.description!),
|
||||
const Expanded(
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
meta.name ?? "No name",
|
||||
style:
|
||||
Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Colors.black54,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
|
@ -331,7 +351,8 @@ class StremioCard extends StatelessWidget {
|
|||
left: 0,
|
||||
right: 0,
|
||||
child: LinearProgressIndicator(
|
||||
value: meta.progress,
|
||||
value: meta.progress! / 100,
|
||||
minHeight: 5,
|
||||
),
|
||||
),
|
||||
if (meta.nextEpisode != null && meta.nextSeason != null)
|
||||
|
|
|
|||
|
|
@ -200,10 +200,12 @@ class _VideoViewerState extends State<VideoViewer> {
|
|||
});
|
||||
|
||||
final oneMore = player.stream.completed.listen((item) {
|
||||
TraktService.instance!.stopScrobbling(
|
||||
meta: widget.meta as types.Meta,
|
||||
progress: currentProgressInPercentage,
|
||||
);
|
||||
if (item && player.state.duration.inSeconds > 10) {
|
||||
TraktService.instance!.stopScrobbling(
|
||||
meta: widget.meta as types.Meta,
|
||||
progress: currentProgressInPercentage,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
listener.add(streams);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:cached_query_flutter/cached_query_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:madari_client/features/connections/service/base_connection_service.dart';
|
||||
import 'package:madari_client/features/trakt/service/trakt.service.dart';
|
||||
|
||||
import '../../connections/widget/base/render_library_list.dart';
|
||||
import '../../settings/screen/trakt_integration_screen.dart';
|
||||
import '../service/trakt_cache.service.dart';
|
||||
|
||||
class TraktContainer extends StatefulWidget {
|
||||
final String loadId;
|
||||
|
|
@ -14,43 +14,45 @@ class TraktContainer extends StatefulWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
State<TraktContainer> createState() => _TraktContainerState();
|
||||
State<TraktContainer> createState() => TraktContainerState();
|
||||
}
|
||||
|
||||
class _TraktContainerState extends State<TraktContainer> {
|
||||
late Query<List<LibraryItem>> _query;
|
||||
class TraktContainerState extends State<TraktContainer> {
|
||||
late final TraktCacheService _cacheService;
|
||||
List<LibraryItem>? _cachedItems;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cacheService = TraktCacheService();
|
||||
_loadData();
|
||||
}
|
||||
|
||||
_query = Query(
|
||||
key: widget.loadId,
|
||||
config: QueryConfig(
|
||||
cacheDuration: const Duration(days: 30),
|
||||
refetchDuration: const Duration(minutes: 10),
|
||||
storageDuration: const Duration(days: 30),
|
||||
),
|
||||
queryFn: () {
|
||||
switch (widget.loadId) {
|
||||
case "up_next":
|
||||
case "up_next_series":
|
||||
return TraktService.instance!.getUpNextSeries();
|
||||
case "continue_watching":
|
||||
return TraktService.instance!.getContinueWatching();
|
||||
case "upcoming_schedule":
|
||||
return TraktService.instance!.getUpcomingSchedule();
|
||||
case "watchlist":
|
||||
return TraktService.instance!.getWatchlist();
|
||||
case "show_recommendations":
|
||||
return TraktService.instance!.getShowRecommendations();
|
||||
case "movie_recommendations":
|
||||
return TraktService.instance!.getMovieRecommendations();
|
||||
default:
|
||||
throw Exception("Invalid loadId: ${widget.loadId}");
|
||||
}
|
||||
},
|
||||
);
|
||||
Future<void> _loadData() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final items = await _cacheService.fetchData(widget.loadId);
|
||||
setState(() {
|
||||
_cachedItems = items;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
await _cacheService.refresh(widget.loadId);
|
||||
await _loadData();
|
||||
}
|
||||
|
||||
String get title {
|
||||
|
|
@ -61,97 +63,89 @@ class _TraktContainerState extends State<TraktContainer> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return QueryBuilder(
|
||||
query: _query,
|
||||
builder: (context, snapshot) {
|
||||
final theme = Theme.of(context);
|
||||
final item = snapshot.data;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Trakt - $title"),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RenderListItems(
|
||||
items: item ?? [],
|
||||
error: snapshot.error,
|
||||
hasError:
|
||||
snapshot.status == QueryStatus.error,
|
||||
heroPrefix: "trakt_up_next${widget.loadId}",
|
||||
service: TraktService.stremioService!,
|
||||
isGrid: true,
|
||||
isWide: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Show more",
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: Colors.white70,
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 30,
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Trakt - $title"),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: RenderListItems(
|
||||
items: _cachedItems ?? [],
|
||||
error: _error,
|
||||
hasError: _error != null,
|
||||
heroPrefix: "trakt_up_next${widget.loadId}",
|
||||
service: TraktService.stremioService!,
|
||||
isGrid: true,
|
||||
isWide: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
"Show more",
|
||||
style: theme.textTheme.labelMedium?.copyWith(
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
if ((item ?? []).isEmpty &&
|
||||
snapshot.status != QueryStatus.loading)
|
||||
const Positioned.fill(
|
||||
child: Center(
|
||||
child: Text("Nothing to see here"),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: getListHeight(context),
|
||||
child: snapshot.status == QueryStatus.loading
|
||||
? SpinnerCards(
|
||||
isWide: widget.loadId == "up_next_series",
|
||||
)
|
||||
: RenderListItems(
|
||||
isWide: widget.loadId == "up_next_series",
|
||||
items: item ?? [],
|
||||
error: snapshot.error,
|
||||
hasError: snapshot.status == QueryStatus.error,
|
||||
heroPrefix: "trakt_up_next${widget.loadId}",
|
||||
service: TraktService.stremioService!,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
const SizedBox(
|
||||
height: 8,
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
if ((_cachedItems ?? []).isEmpty && !_isLoading)
|
||||
const Positioned.fill(
|
||||
child: Center(
|
||||
child: Text("Nothing to see here"),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: getListHeight(context),
|
||||
child: _isLoading
|
||||
? SpinnerCards(
|
||||
isWide: widget.loadId == "up_next_series",
|
||||
)
|
||||
: RenderListItems(
|
||||
isWide: widget.loadId == "up_next_series",
|
||||
items: _cachedItems ?? [],
|
||||
error: _error,
|
||||
hasError: _error != null,
|
||||
heroPrefix: "trakt_up_next${widget.loadId}",
|
||||
service: TraktService.stremioService!,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:cached_storage/cached_storage.dart';
|
||||
|
|
@ -16,18 +17,40 @@ class TraktService {
|
|||
static const String _baseUrl = 'https://api.trakt.tv';
|
||||
static const String _apiVersion = '2';
|
||||
|
||||
static const int _authedPostLimit = 100;
|
||||
static const int _authedGetLimit = 1000;
|
||||
static const Duration _rateLimitWindow = Duration(minutes: 5);
|
||||
|
||||
static const Duration _cacheRevalidationInterval = Duration(minutes: 5);
|
||||
|
||||
static TraktService? _instance;
|
||||
static TraktService? get instance => _instance;
|
||||
static BaseConnectionService? stremioService;
|
||||
|
||||
int _postRequestCount = 0;
|
||||
int _getRequestCount = 0;
|
||||
DateTime _lastRateLimitReset = DateTime.now();
|
||||
|
||||
final Map<String, dynamic> _cache = {};
|
||||
Timer? _cacheRevalidationTimer;
|
||||
|
||||
static ensureInitialized() async {
|
||||
if (_instance != null) {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
AppEngine.engine.pb.authStore.onChange.listen((item) {
|
||||
if (!AppEngine.engine.pb.authStore.isValid) {
|
||||
_instance?._cache.clear();
|
||||
}
|
||||
});
|
||||
|
||||
final traktService = TraktService();
|
||||
await traktService.initStremioService();
|
||||
_instance = traktService;
|
||||
|
||||
// Start cache revalidation timer
|
||||
_instance!._startCacheRevalidation();
|
||||
}
|
||||
|
||||
Future<BaseConnectionService> initStremioService() async {
|
||||
|
|
@ -75,6 +98,93 @@ class TraktService {
|
|||
'Authorization': 'Bearer $_token',
|
||||
};
|
||||
|
||||
Future<void> _checkRateLimit(String method) async {
|
||||
final now = DateTime.now();
|
||||
if (now.difference(_lastRateLimitReset) > _rateLimitWindow) {
|
||||
_postRequestCount = 0;
|
||||
_getRequestCount = 0;
|
||||
_lastRateLimitReset = now;
|
||||
}
|
||||
|
||||
if (method == 'GET') {
|
||||
if (_getRequestCount >= _authedGetLimit) {
|
||||
throw Exception('GET rate limit exceeded');
|
||||
}
|
||||
_getRequestCount++;
|
||||
} else if (method == 'POST' || method == 'PUT' || method == 'DELETE') {
|
||||
if (_postRequestCount >= _authedPostLimit) {
|
||||
throw Exception('POST/PUT/DELETE rate limit exceeded');
|
||||
}
|
||||
_postRequestCount++;
|
||||
}
|
||||
}
|
||||
|
||||
void _startCacheRevalidation() {
|
||||
_cacheRevalidationTimer = Timer.periodic(
|
||||
_cacheRevalidationInterval,
|
||||
(_) async {
|
||||
await _revalidateCache();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _revalidateCache() async {
|
||||
for (final key in _cache.keys) {
|
||||
final cachedData = _cache[key];
|
||||
if (cachedData != null) {
|
||||
final updatedData = await _makeRequest(key, bypassCache: true);
|
||||
_cache[key] = updatedData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> _makeRequest(String url, {bool bypassCache = false}) async {
|
||||
if (!bypassCache && _cache.containsKey(url)) {
|
||||
return _cache[url];
|
||||
}
|
||||
|
||||
await _checkRateLimit('GET');
|
||||
|
||||
final response = await http.get(Uri.parse(url), headers: headers);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to fetch data from $url');
|
||||
}
|
||||
|
||||
final data = json.decode(response.body);
|
||||
_cache[url] = data;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildObjectForMeta(Meta meta) {
|
||||
if (meta.type == "movie") {
|
||||
return {
|
||||
'movie': {
|
||||
'title': meta.name,
|
||||
'year': meta.year,
|
||||
'ids': {
|
||||
'imdb': meta.imdbId ?? meta.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
"show": {
|
||||
"title": meta.name,
|
||||
"year": meta.year,
|
||||
"ids": {
|
||||
"imdb": meta.imdbId ?? meta.id,
|
||||
}
|
||||
},
|
||||
"episode": {
|
||||
"season": meta.nextSeason,
|
||||
"number": meta.nextEpisode,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<LibraryItem>> getUpNextSeries() async {
|
||||
await initStremioService();
|
||||
|
||||
|
|
@ -83,33 +193,19 @@ class TraktService {
|
|||
}
|
||||
|
||||
try {
|
||||
final watchedResponse = await http.get(
|
||||
Uri.parse('$_baseUrl/sync/watched/shows'),
|
||||
headers: headers,
|
||||
final List<dynamic> watchedShows = await _makeRequest(
|
||||
'$_baseUrl/sync/watched/shows',
|
||||
);
|
||||
|
||||
if (watchedResponse.statusCode != 200) {
|
||||
throw ArgumentError('Failed to fetch watched shows');
|
||||
}
|
||||
|
||||
final watchedShows = json.decode(watchedResponse.body) as List;
|
||||
|
||||
final progressFutures = watchedShows.map((show) async {
|
||||
final showId = show['show']['ids']['trakt'];
|
||||
final imdb = show['show']['ids']['imdb'];
|
||||
|
||||
try {
|
||||
final progressResponse = await http.get(
|
||||
Uri.parse('$_baseUrl/shows/$showId/progress/watched'),
|
||||
headers: headers,
|
||||
final progress = await _makeRequest(
|
||||
'$_baseUrl/shows/$showId/progress/watched',
|
||||
);
|
||||
|
||||
if (progressResponse.statusCode != 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final progress = json.decode(progressResponse.body);
|
||||
|
||||
final nextEpisode = progress['next_episode'];
|
||||
|
||||
if (nextEpisode != null && imdb != null) {
|
||||
|
|
@ -155,16 +251,7 @@ class TraktService {
|
|||
}
|
||||
|
||||
try {
|
||||
final watchedResponse = await http.get(
|
||||
Uri.parse('$_baseUrl/sync/playback'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (watchedResponse.statusCode != 200) {
|
||||
throw Exception('Failed to fetch watched movies');
|
||||
}
|
||||
|
||||
final continueWatching = json.decode(watchedResponse.body) as List;
|
||||
final continueWatching = await _makeRequest('$_baseUrl/sync/playback');
|
||||
|
||||
final Map<String, double> progress = {};
|
||||
|
||||
|
|
@ -232,20 +319,10 @@ class TraktService {
|
|||
}
|
||||
|
||||
try {
|
||||
final scheduleResponse = await http.get(
|
||||
Uri.parse(
|
||||
'$_baseUrl/calendars/my/shows/${DateFormat('yyyy-MM-dd').format(DateTime.now())}/7',
|
||||
),
|
||||
headers: headers,
|
||||
final List<dynamic> scheduleShows = await _makeRequest(
|
||||
'$_baseUrl/calendars/my/shows/${DateFormat('yyyy-MM-dd').format(DateTime.now())}/7',
|
||||
);
|
||||
|
||||
if (scheduleResponse.statusCode != 200) {
|
||||
print('Failed to fetch upcoming schedule');
|
||||
throw Error();
|
||||
}
|
||||
|
||||
final scheduleShows = json.decode(scheduleResponse.body) as List;
|
||||
|
||||
final result = await stremioService!.getBulkItem(
|
||||
scheduleShows.map((show) {
|
||||
final imdb = show['show']['ids']['imdb'];
|
||||
|
|
@ -272,16 +349,7 @@ class TraktService {
|
|||
}
|
||||
|
||||
try {
|
||||
final watchlistResponse = await http.get(
|
||||
Uri.parse('$_baseUrl/sync/watchlist'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (watchlistResponse.statusCode != 200) {
|
||||
throw Exception('Failed to fetch watchlist');
|
||||
}
|
||||
|
||||
final watchlistItems = json.decode(watchlistResponse.body) as List;
|
||||
final watchlistItems = await _makeRequest('$_baseUrl/sync/watchlist');
|
||||
|
||||
final result = await stremioService!.getBulkItem(
|
||||
watchlistItems
|
||||
|
|
@ -317,17 +385,8 @@ class TraktService {
|
|||
}
|
||||
|
||||
try {
|
||||
final recommendationsResponse = await http.get(
|
||||
Uri.parse('$_baseUrl/recommendations/shows'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (recommendationsResponse.statusCode != 200) {
|
||||
throw Exception('Failed to fetch show recommendations');
|
||||
}
|
||||
|
||||
final recommendedShows =
|
||||
json.decode(recommendationsResponse.body) as List;
|
||||
await _makeRequest('$_baseUrl/recommendations/shows');
|
||||
|
||||
final result = (await stremioService!.getBulkItem(
|
||||
recommendedShows
|
||||
|
|
@ -363,17 +422,8 @@ class TraktService {
|
|||
}
|
||||
|
||||
try {
|
||||
final recommendationsResponse = await http.get(
|
||||
Uri.parse('$_baseUrl/recommendations/movies'),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (recommendationsResponse.statusCode != 200) {
|
||||
throw Exception('Failed to fetch movie recommendations');
|
||||
}
|
||||
|
||||
final recommendedMovies =
|
||||
json.decode(recommendationsResponse.body) as List;
|
||||
await _makeRequest('$_baseUrl/recommendations/movies');
|
||||
|
||||
final result = await stremioService!.getBulkItem(
|
||||
recommendedMovies
|
||||
|
|
@ -421,16 +471,7 @@ class TraktService {
|
|||
}
|
||||
|
||||
Future<int?> getTraktIdForMovie(String imdb) async {
|
||||
final id = await http.get(
|
||||
Uri.parse("$_baseUrl/search/imdb/$imdb"),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (id.statusCode != 200) {
|
||||
throw ArgumentError("failed to get trakt id");
|
||||
}
|
||||
|
||||
final body = jsonDecode(id.body) as List<dynamic>;
|
||||
final body = await _makeRequest("$_baseUrl/search/imdb/$imdb");
|
||||
|
||||
if (body.isEmpty) {
|
||||
return null;
|
||||
|
|
@ -457,6 +498,8 @@ class TraktService {
|
|||
return;
|
||||
}
|
||||
|
||||
await _checkRateLimit('POST');
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/scrobble/start'),
|
||||
|
|
@ -472,6 +515,9 @@ class TraktService {
|
|||
print(response.body);
|
||||
throw Exception('Failed to start scrobbling');
|
||||
}
|
||||
|
||||
_cache.remove('$_baseUrl/sync/watched/shows');
|
||||
_cache.remove('$_baseUrl/sync/playback');
|
||||
} catch (e, stack) {
|
||||
print('Error starting scrobbling: $e');
|
||||
print(stack);
|
||||
|
|
@ -487,6 +533,8 @@ class TraktService {
|
|||
return;
|
||||
}
|
||||
|
||||
await _checkRateLimit('POST');
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/scrobble/pause'),
|
||||
|
|
@ -507,34 +555,6 @@ class TraktService {
|
|||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> _buildObjectForMeta(Meta meta) {
|
||||
if (meta.type == "movie") {
|
||||
return {
|
||||
'movie': {
|
||||
'title': meta.name,
|
||||
'year': meta.year,
|
||||
'ids': {
|
||||
'imdb': meta.imdbId ?? meta.id,
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
"show": {
|
||||
"title": meta.name,
|
||||
"year": meta.year,
|
||||
"ids": {
|
||||
"imdb": meta.imdbId ?? meta.id,
|
||||
}
|
||||
},
|
||||
"episode": {
|
||||
"season": meta.nextSeason,
|
||||
"number": meta.nextEpisode,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stopScrobbling({
|
||||
required Meta meta,
|
||||
required double progress,
|
||||
|
|
@ -543,6 +563,8 @@ class TraktService {
|
|||
return;
|
||||
}
|
||||
|
||||
await _checkRateLimit('POST');
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$_baseUrl/scrobble/stop'),
|
||||
|
|
@ -557,6 +579,9 @@ class TraktService {
|
|||
print(response.statusCode);
|
||||
throw Exception('Failed to stop scrobbling');
|
||||
}
|
||||
|
||||
_cache.remove('$_baseUrl/sync/watched/shows');
|
||||
_cache.remove('$_baseUrl/sync/playback');
|
||||
} catch (e, stack) {
|
||||
print('Error stopping scrobbling: $e');
|
||||
print(stack);
|
||||
|
|
@ -571,16 +596,7 @@ class TraktService {
|
|||
|
||||
try {
|
||||
if (meta.type == "series") {
|
||||
final response = await http.get(
|
||||
Uri.parse("$_baseUrl/sync/playback/episodes"),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final body = jsonDecode(response.body) as List;
|
||||
final body = await _makeRequest("$_baseUrl/sync/playback/episodes");
|
||||
|
||||
final List<TraktProgress> result = [];
|
||||
|
||||
|
|
@ -621,16 +637,7 @@ class TraktService {
|
|||
|
||||
return result;
|
||||
} else {
|
||||
final response = await http.get(
|
||||
Uri.parse("$_baseUrl/sync/playback/movies"),
|
||||
headers: headers,
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final body = jsonDecode(response.body) as List;
|
||||
final body = await _makeRequest("$_baseUrl/sync/playback/movies");
|
||||
|
||||
for (final item in body) {
|
||||
if (item["type"] != "movie") {
|
||||
|
|
|
|||
65
lib/features/trakt/service/trakt_cache.service.dart
Normal file
65
lib/features/trakt/service/trakt_cache.service.dart
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import 'package:madari_client/features/trakt/service/trakt.service.dart';
|
||||
|
||||
import '../../connections/service/base_connection_service.dart';
|
||||
|
||||
class TraktCacheService {
|
||||
static final TraktCacheService _instance = TraktCacheService._internal();
|
||||
factory TraktCacheService() => _instance;
|
||||
TraktCacheService._internal();
|
||||
|
||||
final Map<String, List<LibraryItem>> _cache = {};
|
||||
final Map<String, bool> _isLoading = {};
|
||||
final Map<String, String?> _errors = {};
|
||||
|
||||
Future<List<LibraryItem>> fetchData(String loadId) async {
|
||||
if (_cache.containsKey(loadId)) {
|
||||
return _cache[loadId]!;
|
||||
}
|
||||
|
||||
_isLoading[loadId] = true;
|
||||
_errors[loadId] = null;
|
||||
|
||||
try {
|
||||
final data = await _fetchFromTrakt(loadId);
|
||||
_cache[loadId] = data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
_errors[loadId] = e.toString();
|
||||
rethrow;
|
||||
} finally {
|
||||
_isLoading[loadId] = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refresh(String loadId) async {
|
||||
_cache.remove(loadId);
|
||||
_errors.remove(loadId);
|
||||
await fetchData(loadId);
|
||||
}
|
||||
|
||||
List<LibraryItem>? getCachedData(String loadId) => _cache[loadId];
|
||||
|
||||
bool isLoading(String loadId) => _isLoading[loadId] ?? false;
|
||||
|
||||
String? getError(String loadId) => _errors[loadId];
|
||||
|
||||
Future<List<LibraryItem>> _fetchFromTrakt(String loadId) async {
|
||||
switch (loadId) {
|
||||
case "up_next":
|
||||
case "up_next_series":
|
||||
return TraktService.instance!.getUpNextSeries();
|
||||
case "continue_watching":
|
||||
return TraktService.instance!.getContinueWatching();
|
||||
case "upcoming_schedule":
|
||||
return TraktService.instance!.getUpcomingSchedule();
|
||||
case "watchlist":
|
||||
return TraktService.instance!.getWatchlist();
|
||||
case "show_recommendations":
|
||||
return TraktService.instance!.getShowRecommendations();
|
||||
case "movie_recommendations":
|
||||
return TraktService.instance!.getMovieRecommendations();
|
||||
default:
|
||||
throw Exception("Invalid loadId: $loadId");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,28 @@ class _HomeTabPageState extends State<HomeTabPage> {
|
|||
],
|
||||
);
|
||||
|
||||
final Map<int, GlobalKey<TraktContainerState>> _keyMap = {};
|
||||
|
||||
GlobalKey<TraktContainerState> _getKey(int id) {
|
||||
return _keyMap.putIfAbsent(
|
||||
id,
|
||||
() => GlobalKey<TraktContainerState>(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onRefresh() async {
|
||||
final List<Future> promises = [];
|
||||
for (final item in traktLibraries) {
|
||||
final state = _getKey(traktLibraries.indexOf(item)).currentState;
|
||||
|
||||
if (state == null) continue;
|
||||
|
||||
promises.add(state.refresh());
|
||||
}
|
||||
|
||||
await Future.wait(promises);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
|
|
@ -91,6 +113,7 @@ class _HomeTabPageState extends State<HomeTabPage> {
|
|||
setState(() {
|
||||
traktLibraries = getTraktLibraries();
|
||||
});
|
||||
await _onRefresh();
|
||||
return;
|
||||
},
|
||||
child: QueryBuilder(
|
||||
|
|
@ -123,6 +146,8 @@ class _HomeTabPageState extends State<HomeTabPage> {
|
|||
setState(() {
|
||||
traktLibraries = getTraktLibraries();
|
||||
});
|
||||
|
||||
await _onRefresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
@ -140,6 +165,7 @@ class _HomeTabPageState extends State<HomeTabPage> {
|
|||
final category = traktLibraries[index];
|
||||
|
||||
return TraktContainer(
|
||||
key: _getKey(index),
|
||||
loadId: category,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue