mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-04-19 13:12:05 +00:00
Implementing trakt watched shows history
This commit is contained in:
parent
00d643a9fd
commit
cce3c2b8e4
3 changed files with 281 additions and 96 deletions
|
|
@ -34,13 +34,13 @@ class StremioItemSeasonSelector extends StatefulWidget {
|
|||
class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
||||
with SingleTickerProviderStateMixin {
|
||||
int? selectedSeason;
|
||||
late TabController? _tabController;
|
||||
late final Map<int, List<Video>> seasonMap;
|
||||
final zeeeWatchHistory = ZeeeWatchHistoryStatic.service;
|
||||
|
||||
late Meta meta = widget.meta;
|
||||
|
||||
final Map<String, double> _progress = {};
|
||||
Map<int, Set<int>> watchedEpisodesBySeason = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -51,26 +51,64 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
if (seasonMap.keys.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (seasonMap.isNotEmpty) {
|
||||
final seasons = seasonMap.keys.toList()..sort();
|
||||
int initialSeason = getSelectedSeason();
|
||||
|
||||
final index = getSelectedSeason();
|
||||
|
||||
_tabController = TabController(
|
||||
length: seasonMap.keys.length,
|
||||
vsync: this,
|
||||
initialIndex: index.clamp(
|
||||
0,
|
||||
seasonMap.keys.isNotEmpty ? seasonMap.keys.length - 1 : 0,
|
||||
),
|
||||
);
|
||||
|
||||
// This is for rendering the component again for the selection of another tab
|
||||
_tabController!.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
if (seasons.contains(initialSeason)) {
|
||||
// Check if initialSeason is in seasons
|
||||
selectedSeason = initialSeason;
|
||||
} else if (seasons.isNotEmpty) {
|
||||
selectedSeason = seasons.first; // Or any other default if not found
|
||||
}
|
||||
}
|
||||
|
||||
getWatchHistory();
|
||||
getWatchedHistory();
|
||||
}
|
||||
|
||||
getWatchedHistory() async {
|
||||
final traktService = TraktService.instance;
|
||||
try {
|
||||
final result =
|
||||
await traktService!.getWatchedShowsWithEpisodes(widget.meta);
|
||||
watchedEpisodesBySeason.clear();
|
||||
for (final show in result) {
|
||||
if (show.episodes != null) {
|
||||
for (final episode in show.episodes!) {
|
||||
if (!watchedEpisodesBySeason.containsKey(episode.season)) {
|
||||
watchedEpisodesBySeason[episode.season] = {};
|
||||
}
|
||||
watchedEpisodesBySeason[episode.season]!.add(episode.episode);
|
||||
}
|
||||
} else {
|
||||
//print("No episodes found for ${show.title}");
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {});
|
||||
return;
|
||||
} catch (e, stack) {
|
||||
print("Error fetching Trakt data: $e");
|
||||
print("Stack Trace: $stack");
|
||||
}
|
||||
}
|
||||
bool isEpisodeWatched(int season, int episode) {
|
||||
return watchedEpisodesBySeason.containsKey(season) &&
|
||||
watchedEpisodesBySeason[season]!.contains(episode);
|
||||
}
|
||||
|
||||
bool isSeasonWatched(int season) {
|
||||
if (!watchedEpisodesBySeason.containsKey(season)) {
|
||||
return false; // No episodes watched in this season
|
||||
}
|
||||
if (seasonMap.containsKey(season)) {
|
||||
return watchedEpisodesBySeason[season]!.length ==
|
||||
seasonMap[season]!.length;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int getSelectedSeason() {
|
||||
return widget.meta.currentVideo?.season ??
|
||||
widget.meta.videos?.lastWhereOrNull((item) {
|
||||
|
|
@ -94,8 +132,6 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
meta = result;
|
||||
});
|
||||
|
||||
final index = getSelectedSeason();
|
||||
_tabController?.animateTo(index);
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -119,14 +155,10 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
_progress[item.id] = item.progress.toDouble();
|
||||
}
|
||||
|
||||
final index = getSelectedSeason();
|
||||
|
||||
_tabController?.animateTo(index);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -190,22 +222,15 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
final isWideScreen = screenWidth > 900;
|
||||
final contentWidth = isWideScreen ? 900.0 : screenWidth;
|
||||
|
||||
if (_tabController == null) {
|
||||
if (seasonMap.keys.isEmpty) {
|
||||
return const SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 0,
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Center(child: Text("No seasons available")),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return SliverMainAxisGroup(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
|
|
@ -215,6 +240,7 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
sliver: SliverToBoxAdapter(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
|
|
@ -236,25 +262,47 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: TabBar(
|
||||
tabAlignment: TabAlignment.start,
|
||||
dividerColor: Colors.transparent,
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
splashBorderRadius: BorderRadius.circular(8),
|
||||
padding: const EdgeInsets.all(4),
|
||||
tabs: seasons.map((season) {
|
||||
return Tab(
|
||||
text: season == 0 ? "Specials" : 'Season $season',
|
||||
height: 40,
|
||||
);
|
||||
}).toList(),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 120),
|
||||
child: DropdownButtonFormField<int>(
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 2, vertical: 8),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surface.withOpacity(0.3),
|
||||
),
|
||||
|
||||
value: selectedSeason,
|
||||
onChanged: (newValue) {
|
||||
setState(() {
|
||||
selectedSeason = newValue!;
|
||||
});
|
||||
},
|
||||
items: seasons.map((season) {
|
||||
final isWatched = isSeasonWatched(season);
|
||||
return DropdownMenuItem<int>(
|
||||
value: season,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Text(season == 0 ? "Specials" : 'Season $season'),
|
||||
if (isWatched) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16,
|
||||
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -268,19 +316,23 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final currentSeason = seasons[_tabController!.index];
|
||||
(context, index) {
|
||||
final currentSeason = selectedSeason;
|
||||
if (currentSeason == null ||
|
||||
!seasonMap.containsKey(currentSeason)) {
|
||||
return const Center(child: Text("Select a season"));
|
||||
}
|
||||
final episodes = seasonMap[currentSeason]!;
|
||||
final episode = episodes[index];
|
||||
|
||||
final videoIndex = meta.videos?.indexOf(episode);
|
||||
|
||||
final progress = ((!TraktService.isEnabled()
|
||||
? (_progress[episode.id] ?? 0) / 100
|
||||
: videoIndex != -1
|
||||
? (meta.videos![videoIndex!].progress)
|
||||
: 0.toDouble()) ??
|
||||
0) /
|
||||
? (_progress[episode.id] ?? 0) / 100
|
||||
: videoIndex != -1
|
||||
? (meta.videos![videoIndex!].progress)
|
||||
: 0.toDouble()) ??
|
||||
0) /
|
||||
100;
|
||||
|
||||
return InkWell(
|
||||
|
|
@ -307,37 +359,37 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
children: [
|
||||
Container(
|
||||
child: episode.thumbnail != null &&
|
||||
episode.thumbnail!.isNotEmpty
|
||||
episode.thumbnail!.isNotEmpty
|
||||
? Image.network(
|
||||
episode.thumbnail!,
|
||||
width: 140,
|
||||
height: 90,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 140,
|
||||
height: 90,
|
||||
color: colorScheme
|
||||
.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.movie,
|
||||
color:
|
||||
colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
width: 140,
|
||||
height: 90,
|
||||
episode.thumbnail!,
|
||||
width: 140,
|
||||
height: 90,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 140,
|
||||
height: 90,
|
||||
color: colorScheme
|
||||
.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.movie,
|
||||
color:
|
||||
colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.movie,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
width: 140,
|
||||
height: 90,
|
||||
color:
|
||||
colorScheme.surfaceContainerHighest,
|
||||
child: Icon(
|
||||
Icons.movie,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
|
|
@ -359,7 +411,8 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
],
|
||||
),
|
||||
),
|
||||
if (progress > .9)
|
||||
if (isEpisodeWatched(
|
||||
currentSeason, episode.episode!))
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
|
|
@ -368,7 +421,7 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.teal,
|
||||
color: Colors.grey.shade900,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
|
|
@ -378,14 +431,15 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
top: 2.0,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"Watched",
|
||||
style: Theme.of(context)
|
||||
child: Icon(
|
||||
Icons.done_all,
|
||||
size: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall
|
||||
?.copyWith(
|
||||
color: Colors.black,
|
||||
),
|
||||
.bodyLarge!
|
||||
.fontSize,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary, // Use primary color from theme
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -448,8 +502,10 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
|
|||
),
|
||||
);
|
||||
},
|
||||
childCount:
|
||||
seasonMap[seasons[_tabController!.index]]?.length ?? 0,
|
||||
childCount: selectedSeason != null &&
|
||||
seasonMap.containsKey(selectedSeason!)
|
||||
? seasonMap[selectedSeason!]!.length
|
||||
: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import '../../../engine/engine.dart';
|
|||
import '../../connections/service/base_connection_service.dart';
|
||||
import '../../connections/types/stremio/stremio_base.types.dart';
|
||||
import '../../settings/types/connection.dart';
|
||||
import '../types/common.dart';
|
||||
|
||||
class TraktService {
|
||||
static final Logger _logger = Logger('TraktService');
|
||||
|
|
@ -1078,4 +1079,81 @@ class TraktService {
|
|||
|
||||
return meta;
|
||||
}
|
||||
Future<List<TraktShowWatched>> getWatchedShowsWithEpisodes(Meta meta) async {
|
||||
if (!isEnabled()) {
|
||||
_logger.info('Trakt integration is not enabled');
|
||||
return [];
|
||||
}
|
||||
if (meta.type == "series" ) {
|
||||
final watchedShows = await getWatchedShows();
|
||||
for (final show in watchedShows) {
|
||||
if(show.ids.imdb==meta.imdbId) {
|
||||
// await Future.delayed(const Duration(seconds: 5));
|
||||
show.episodes = await _getWatchedEpisodes(show.ids.trakt);
|
||||
}
|
||||
}
|
||||
return watchedShows;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
Future<List<TraktShowWatched>> getWatchedShows() async {
|
||||
if (!isEnabled()) {
|
||||
_logger.info('Trakt integration is not enabled');
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
final body = await _makeRequest(
|
||||
"$_baseUrl/sync/watched/shows/",
|
||||
bypassCache: true,
|
||||
);
|
||||
final List<TraktShowWatched> result = [];
|
||||
for (final item in body) {
|
||||
try {
|
||||
result.add(
|
||||
TraktShowWatched(
|
||||
title: item["show"]["title"],
|
||||
seasons: item["seasons"],
|
||||
ids: TraktIds.fromJson(item["show"]["ids"]),
|
||||
lastWatchedAt: item["last_watched_at"] != null ? DateTime.parse(item["last_watched_at"]) : null,
|
||||
plays: item["plays"],
|
||||
),
|
||||
);
|
||||
|
||||
} catch (e, stack) {
|
||||
_logger.warning('Error parsing watched show: $e\n$stack item: $item');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (e, stack) {
|
||||
_logger.severe('Error fetching watched shows: $e\n$stack');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Future<List<TraktEpisodeWatched>> _getWatchedEpisodes(int? traktId) async {
|
||||
if (traktId == null) return [];
|
||||
int page = 1;
|
||||
const int limit = 1000;
|
||||
try {
|
||||
final body = await _makeRequest(
|
||||
"$_baseUrl/sync/history/shows/$traktId?page=$page&limit=$limit",
|
||||
bypassCache: true,
|
||||
);
|
||||
final List<TraktEpisodeWatched> episodes = [];
|
||||
for (final item in body) {
|
||||
if (item['episode'] != null ) {
|
||||
episodes.add(
|
||||
TraktEpisodeWatched(
|
||||
season: item['episode']['season'],
|
||||
episode: item['episode']['number'],
|
||||
watchedAt: DateTime.parse(item['watched_at']),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return episodes;
|
||||
} catch (e, stack) {
|
||||
_logger.severe('Error fetching watched episodes: $e\n$stack');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,3 +13,54 @@ class TraktProgress {
|
|||
this.traktId,
|
||||
});
|
||||
}
|
||||
|
||||
class TraktShowWatched {
|
||||
final String title;
|
||||
final List<dynamic> seasons;
|
||||
final TraktIds ids;
|
||||
final DateTime? lastWatchedAt;
|
||||
final int plays;
|
||||
List<TraktEpisodeWatched>? episodes; // Add episodes list
|
||||
|
||||
TraktShowWatched({
|
||||
required this.title,
|
||||
required this.seasons,
|
||||
required this.ids,
|
||||
this.lastWatchedAt,
|
||||
required this.plays,
|
||||
this.episodes,
|
||||
});
|
||||
}
|
||||
|
||||
class TraktIds {
|
||||
final int? trakt;
|
||||
final String? slug;
|
||||
final String? imdb;
|
||||
final int? tmdb;
|
||||
|
||||
TraktIds({
|
||||
this.trakt,
|
||||
this.slug,
|
||||
this.imdb,
|
||||
this.tmdb,
|
||||
});
|
||||
|
||||
factory TraktIds.fromJson(Map<String, dynamic> json) => TraktIds(
|
||||
trakt: json['trakt'],
|
||||
slug: json['slug'],
|
||||
imdb: json['imdb'],
|
||||
tmdb: json['tmdb'],
|
||||
);
|
||||
}
|
||||
|
||||
class TraktEpisodeWatched {
|
||||
final int season;
|
||||
final int episode;
|
||||
final DateTime watchedAt;
|
||||
|
||||
TraktEpisodeWatched({
|
||||
required this.season,
|
||||
required this.episode,
|
||||
required this.watchedAt,
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue