fix: trakt issues
Some checks are pending
Build and Deploy / build_windows (push) Waiting to run
Build and Deploy / build_android (push) Waiting to run
Build and Deploy / build_android_tv (push) Waiting to run
Build and Deploy / build_ipa (push) Waiting to run
Build and Deploy / build_linux (push) Waiting to run
Build and Deploy / build_macos (push) Waiting to run

This commit is contained in:
omkar 2025-01-14 12:28:17 +05:30
parent f9e4c0387a
commit f129eb7360
13 changed files with 709 additions and 456 deletions

View file

@ -105,8 +105,6 @@ abstract class BaseConnectionService {
Future<void> getStreams(
LibraryItem id, {
String? season,
String? episode,
OnStreamCallback? callback,
});

View file

@ -300,11 +300,7 @@ class StremioConnectionService extends BaseConnectionService {
return (item as Meta).copyWith(
progress: (res as Meta).progress,
nextSeason: res.nextSeason,
nextEpisode: res.nextEpisode,
nextEpisodeTitle: res.nextEpisodeTitle,
externalIds: res.externalIds,
episodeExternalIds: res.episodeExternalIds,
selectedVideoIndex: res.selectedVideoIndex,
);
}).catchError((err, stack) {
_logger.severe('Error fetching item: ${res.id}', err, stack);
@ -422,8 +418,6 @@ class StremioConnectionService extends BaseConnectionService {
@override
Future<void> getStreams(
LibraryItem id, {
String? season,
String? episode,
OnStreamCallback? callback,
}) async {
_logger.fine('Fetching streams for item: ${id.id}');
@ -491,8 +485,6 @@ class StremioConnectionService extends BaseConnectionService {
(item) => videoStreamToStreamList(
item,
meta,
season,
episode,
addonManifest,
),
)
@ -558,8 +550,6 @@ class StremioConnectionService extends BaseConnectionService {
StreamList? videoStreamToStreamList(
VideoStream item,
Meta meta,
String? season,
String? episode,
StremioManifest addonManifest,
) {
String streamTitle = (item.name != null
@ -597,8 +587,6 @@ class StremioConnectionService extends BaseConnectionService {
infoHash: item.infoHash!,
id: meta.id,
fileName: "$title.mp4",
season: season,
episode: episode,
);
}

View file

@ -1,4 +1,3 @@
import 'package:collection/collection.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:pocketbase/pocketbase.dart';
@ -279,7 +278,7 @@ class Meta extends LibraryItem {
@JsonKey(name: "id")
final String id;
@JsonKey(name: "videos")
final List<Video>? videos;
List<Video>? videos;
@JsonKey(name: "genres")
final List<String>? genres;
@JsonKey(name: "releaseInfo")
@ -299,25 +298,14 @@ class Meta extends LibraryItem {
@JsonKey(name: "dvdRelease")
final DateTime? dvdRelease;
final int? traktProgressId;
@JsonKey(includeFromJson: false, includeToJson: false)
final double? progress;
@JsonKey(includeFromJson: false, includeToJson: false)
final int? nextSeason;
final int? selectedVideoIndex;
@JsonKey(includeFromJson: false, includeToJson: false)
final int? nextEpisode;
@JsonKey(includeFromJson: false, includeToJson: false)
final String? nextEpisodeTitle;
@JsonKey(includeFromJson: false, includeToJson: false)
final int? traktId;
final dynamic externalIds;
final dynamic episodeExternalIds;
bool forceRegularMode;
bool? forceRegular = false;
String get imdbRating {
return (imdbRating_ ?? "").toString();
@ -332,15 +320,9 @@ class Meta extends LibraryItem {
return null;
}
return videos?.firstWhereOrNull(
(episode) {
if (episode.tvdbId != null && episodeExternalIds != null) {
return episode.tvdbId == episodeExternalIds['tvdb'];
}
if (selectedVideoIndex != null) return videos![selectedVideoIndex!];
return nextEpisode == episode.episode && nextSeason == episode.season;
},
);
return null;
}
Meta({
@ -349,22 +331,18 @@ class Meta extends LibraryItem {
this.popularities,
required this.type,
this.cast,
this.traktId,
this.forceRegular,
this.country,
this.forceRegularMode = false,
this.externalIds,
this.description,
this.selectedVideoIndex,
this.genre,
this.imdbRating_,
this.poster,
this.nextEpisode,
this.nextSeason,
this.released,
this.slug,
this.year,
this.status,
this.tvdbId,
this.nextEpisodeTitle,
this.director,
this.writer,
this.background,
@ -386,7 +364,7 @@ class Meta extends LibraryItem {
this.language,
this.dvdRelease,
this.progress,
this.episodeExternalIds,
this.traktProgressId,
}) : super(id: id);
Meta copyWith({
@ -407,6 +385,7 @@ class Meta extends LibraryItem {
dynamic tvdbId,
List<dynamic>? director,
List<String>? writer,
final dynamic traktInfo,
String? background,
String? logo,
dynamic externalIds,
@ -419,6 +398,7 @@ class Meta extends LibraryItem {
String? id,
List<Video>? videos,
List<String>? genres,
int? selectedVideoIndex,
String? releaseInfo,
List<TrailerStream>? trailerStreams,
List<Link>? links,
@ -427,10 +407,9 @@ class Meta extends LibraryItem {
List<CreditsCrew>? creditsCrew,
String? language,
DateTime? dvdRelease,
int? nextSeason,
int? nextEpisode,
String? nextEpisodeTitle,
double? progress,
bool? forceRegular,
int? traktProgressId,
}) =>
Meta(
imdbId: imdbId ?? this.imdbId,
@ -439,16 +418,18 @@ class Meta extends LibraryItem {
type: type ?? this.type,
cast: cast ?? this.cast,
country: country ?? this.country,
selectedVideoIndex: selectedVideoIndex ?? this.selectedVideoIndex,
description: description ?? this.description,
genre: genre ?? this.genre,
imdbRating_: imdbRating ?? imdbRating_.toString(),
poster: poster ?? this.poster,
released: released ?? this.released,
traktProgressId: traktProgressId ?? this.traktProgressId,
slug: slug ?? this.slug,
year: year ?? this.year,
forceRegular: forceRegular ?? this.forceRegular,
status: status ?? this.status,
tvdbId: tvdbId ?? this.tvdbId,
externalIds: externalIds ?? this.externalIds,
director: director ?? this.director,
writer: writer ?? this.writer,
background: background ?? this.background,
@ -469,11 +450,7 @@ class Meta extends LibraryItem {
creditsCrew: creditsCrew ?? this.creditsCrew,
language: language ?? this.language,
dvdRelease: dvdRelease ?? this.dvdRelease,
nextEpisode: nextEpisode ?? this.nextEpisode,
nextEpisodeTitle: nextEpisodeTitle ?? this.nextEpisodeTitle,
nextSeason: nextSeason ?? this.nextSeason,
progress: progress ?? this.progress,
episodeExternalIds: episodeExternalIds ?? this.episodeExternalIds,
);
factory Meta.fromJson(Map<String, dynamic> json) {
@ -679,7 +656,7 @@ class Trailer {
@JsonSerializable()
class Video {
@JsonKey(name: "name")
final String? name;
String? name;
@JsonKey(name: "season")
final int season;
@JsonKey(name: "number")
@ -687,7 +664,7 @@ class Video {
@JsonKey(name: "firstAired")
final DateTime? firstAired;
@JsonKey(name: "tvdb_id")
final int? tvdbId;
int? tvdbId;
@JsonKey(name: "overview")
final String? overview;
@JsonKey(name: "thumbnail")
@ -704,6 +681,8 @@ class Video {
final String? title;
@JsonKey(name: "moviedb_id")
final int? moviedbId;
double? progress;
dynamic ids;
Video({
this.name,
@ -712,13 +691,15 @@ class Video {
this.firstAired,
this.tvdbId,
this.overview,
required this.thumbnail,
this.thumbnail,
required this.id,
required this.released,
this.released,
this.episode,
this.description,
this.title,
this.moviedbId,
this.progress,
this.ids,
});
Video copyWith({

View file

@ -1,4 +1,5 @@
import 'package:cached_query_flutter/cached_query_flutter.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:madari_client/engine/engine.dart';
@ -310,7 +311,30 @@ class __RenderLibraryListState extends State<_RenderLibraryList> {
}
}
class RenderListItems extends StatelessWidget {
typedef OnContextTap = void Function(
String actionId,
LibraryItem item,
);
class ContextMenuItem {
final String id;
final String title;
final bool isDefaultAction;
final bool isDestructiveAction;
final IconData? icon;
final OnContextTap? onCallback;
ContextMenuItem({
required this.title,
this.isDefaultAction = false,
this.isDestructiveAction = false,
this.icon,
required this.id,
this.onCallback,
});
}
class RenderListItems extends StatefulWidget {
final ScrollController? controller;
final ScrollController? itemScrollController;
final bool isGrid;
@ -323,6 +347,8 @@ class RenderListItems extends StatelessWidget {
final bool isWide;
final bool isLoadingMore;
final VoidCallback? loadMore;
final List<ContextMenuItem> contextMenuItems;
final OnContextTap? onContextMenu;
const RenderListItems({
super.key,
@ -338,23 +364,30 @@ class RenderListItems extends StatelessWidget {
this.isWide = false,
this.isLoadingMore = false,
this.loadMore,
this.contextMenuItems = const [],
this.onContextMenu,
});
@override
State<RenderListItems> createState() => _RenderListItemsState();
}
class _RenderListItemsState extends State<RenderListItems> {
@override
Widget build(BuildContext context) {
final listHeight = getListHeight(context);
final itemWidth = getItemWidth(
context,
isWide: isWide,
isWide: widget.isWide,
);
return CustomScrollView(
controller: controller,
physics: isGrid
controller: widget.controller,
physics: widget.isGrid
? const AlwaysScrollableScrollPhysics()
: const NeverScrollableScrollPhysics(),
slivers: [
if (hasError)
if (widget.hasError)
SliverToBoxAdapter(
child: SizedBox(
height: listHeight,
@ -370,7 +403,7 @@ class RenderListItems extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Something went wrong while loading the library \n$error",
"Something went wrong while loading the library \n${widget.error}",
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
),
@ -379,7 +412,7 @@ class RenderListItems extends StatelessWidget {
),
TextButton.icon(
label: const Text("Retry"),
onPressed: onRefresh,
onPressed: widget.onRefresh,
icon: const Icon(
Icons.refresh,
),
@ -390,7 +423,7 @@ class RenderListItems extends StatelessWidget {
),
),
),
if (isGrid) ...[
if (widget.isGrid) ...[
SliverGrid.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: getGridResponsiveColumnCount(context),
@ -398,17 +431,17 @@ class RenderListItems extends StatelessWidget {
crossAxisSpacing: getGridResponsiveSpacing(context),
childAspectRatio: 2 / 3,
),
itemCount: items.length,
itemCount: widget.items.length,
itemBuilder: (ctx, index) {
final item = items[index];
final item = widget.items[index];
return service.renderCard(
return widget.service.renderCard(
item,
"${index}_$heroPrefix",
"${index}_${widget.heroPrefix}",
);
},
),
if (isLoadingMore)
if (widget.isLoadingMore)
SliverPadding(
padding: const EdgeInsets.only(
top: 8.0,
@ -439,35 +472,71 @@ class RenderListItems extends StatelessWidget {
),
),
] else ...[
if (!isLoadingMore)
if (!widget.isLoadingMore)
SliverToBoxAdapter(
child: SizedBox(
height: listHeight,
child: ListView.builder(
controller: itemScrollController,
itemBuilder: (ctx, index) {
final item = items[index];
child: CupertinoPageScaffold(
resizeToAvoidBottomInset: true,
child: SizedBox(
height: listHeight,
child: ListView.builder(
controller: widget.itemScrollController,
itemBuilder: (ctx, index) {
final item = widget.items[index];
return SizedBox(
width: itemWidth,
child: Container(
decoration: const BoxDecoration(),
child: service.renderCard(
item,
"${index}_${heroPrefix}",
if (widget.contextMenuItems.isEmpty) {
return SizedBox(
width: itemWidth,
child: Container(
child: widget.service.renderCard(
item,
"${index}_${widget.heroPrefix}",
),
),
);
}
return CupertinoContextMenu(
enableHapticFeedback: true,
actions: widget.contextMenuItems.map((menu) {
return CupertinoContextMenuAction(
isDefaultAction: menu.isDefaultAction,
isDestructiveAction: menu.isDestructiveAction,
trailingIcon: menu.icon,
onPressed: () {
if (widget.onContextMenu != null) {
widget.onContextMenu!(
menu.id,
item,
);
}
},
child: Text(menu.title),
);
}).toList(),
child: SizedBox(
width: itemWidth,
child: Container(
constraints: BoxConstraints(
maxHeight: listHeight,
),
child: widget.service.renderCard(
item,
"${index}_${widget.heroPrefix}",
),
),
),
),
);
},
scrollDirection: Axis.horizontal,
itemCount: items.length,
);
},
scrollDirection: Axis.horizontal,
itemCount: widget.items.length,
),
),
),
),
if (isLoadingMore)
if (widget.isLoadingMore)
SliverToBoxAdapter(
child: SpinnerCards(
isWide: isWide,
isWide: widget.isWide,
),
),
],

View file

@ -18,8 +18,6 @@ const kIsWeb = bool.fromEnvironment('dart.library.js_util');
class RenderStreamList extends StatefulWidget {
final BaseConnectionService service;
final LibraryItem id;
final String? episode;
final String? season;
final bool shouldPop;
final double? progress;
@ -27,8 +25,6 @@ class RenderStreamList extends StatefulWidget {
super.key,
required this.service,
required this.id,
this.season,
this.episode,
this.progress,
required this.shouldPop,
});
@ -173,8 +169,6 @@ class _RenderStreamListState extends State<RenderStreamList> {
await widget.service.getStreams(
widget.id,
episode: widget.episode,
season: widget.season,
callback: (items, error) {
if (mounted) {
setState(() {
@ -303,25 +297,7 @@ class _RenderStreamListState extends State<RenderStreamList> {
}
}
int? season;
int? episode;
if (widget.season != null) {
season = int.parse(widget.season!);
} else if ((widget.id as Meta).nextSeason != null) {
season = (widget.id as Meta).nextSeason!;
}
if (widget.episode != null) {
episode = int.parse(widget.episode!);
} else if ((widget.id as Meta).nextEpisode != null) {
episode = (widget.id as Meta).nextEpisode!;
}
final meta = (widget.id as Meta).copyWith(
nextSeason: season,
nextEpisode: episode,
);
final meta = (widget.id as Meta).copyWith();
Navigator.of(context).push(
MaterialPageRoute(
@ -329,7 +305,6 @@ class _RenderStreamListState extends State<RenderStreamList> {
source: item.source,
service: widget.service,
meta: meta,
season: meta.nextSeason?.toString(),
progress: widget.progress,
),
),

View file

@ -50,7 +50,8 @@ class _StremioCardState extends State<StremioCard> {
},
);
},
child: (meta.nextSeason == null || meta.progress != null)
child: ((meta.currentVideo == null || meta.progress != null) ||
(meta.forceRegular == true))
? _buildRegular(context, meta)
: _buildWideCard(context, meta),
),
@ -144,14 +145,15 @@ class _StremioCardState extends State<StremioCard> {
),
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
"S${meta.currentVideo?.season ?? meta.nextSeason} E${meta.currentVideo?.episode ?? meta.nextEpisode}",
"S${meta.currentVideo?.season} E${meta.currentVideo?.episode}",
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.black,
),
),
),
Text(
"${meta.nextEpisodeTitle}".trim(),
"${meta.currentVideo?.name ?? meta.currentVideo?.title}"
.trim(),
style: Theme.of(context).textTheme.headlineSmall,
),
],
@ -245,15 +247,8 @@ class _StremioCardState extends State<StremioCard> {
String? getBackgroundImage(Meta meta) {
String? backgroundImage;
if (meta.nextEpisode != null &&
meta.nextSeason != null &&
meta.videos != null) {
for (final video in meta.videos!) {
if (video.season == meta.nextSeason &&
video.episode == meta.nextEpisode) {
return video.thumbnail ?? meta.poster;
}
}
if (meta.currentVideo != null) {
return meta.currentVideo?.thumbnail ?? meta.poster;
}
if (meta.poster != null) {
@ -374,7 +369,7 @@ class _StremioCardState extends State<StremioCard> {
minHeight: 5,
),
),
if (meta.nextEpisode != null && meta.nextSeason != null)
if (meta.currentVideo != null)
Positioned(
bottom: 0,
left: 0,
@ -407,7 +402,7 @@ class _StremioCardState extends State<StremioCard> {
?.copyWith(fontWeight: FontWeight.w600),
),
Text(
"S${meta.currentVideo?.season ?? meta.nextSeason} E${meta.currentVideo?.episode ?? meta.nextEpisode}",
"S${meta.currentVideo?.season} E${meta.currentVideo?.episode}",
style: Theme.of(context)
.textTheme
.bodyMedium

View file

@ -279,7 +279,9 @@ class _StremioItemViewerState extends State<StremioItemViewer> {
widget.original?.type == "series" &&
widget.original?.videos?.isNotEmpty == true)
StremioItemSeasonSelector(
meta: (item as Meta),
meta: (item as Meta).copyWith(
selectedVideoIndex: widget.meta?.selectedVideoIndex,
),
service: widget.service,
),
SliverPadding(
@ -294,7 +296,6 @@ class _StremioItemViewerState extends State<StremioItemViewer> {
const SizedBox(
height: 12,
),
// Description
Text(
'Description',
style: Theme.of(context).textTheme.titleLarge,

View file

@ -6,6 +6,7 @@ import 'package:madari_client/features/connection/types/stremio.dart';
import 'package:madari_client/features/connections/service/base_connection_service.dart';
import 'package:madari_client/features/connections/widget/base/render_stream_list.dart';
import 'package:madari_client/features/trakt/service/trakt.service.dart';
import 'package:madari_client/utils/common.dart';
import '../../../doc_viewer/types/doc_source.dart';
import '../../../watch_history/service/base_watch_history.dart';
@ -37,15 +38,15 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
late final Map<int, List<Video>> seasonMap;
final zeeeWatchHistory = ZeeeWatchHistoryStatic.service;
late Meta meta = widget.meta;
final Map<String, double> _progress = {};
final Map<int, Map<int, double>> _traktProgress = {};
@override
void initState() {
super.initState();
seasonMap = _organizeEpisodes();
selectedSeason = widget.season;
if (seasonMap.keys.isEmpty) {
return;
@ -54,36 +55,42 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
_tabController = TabController(
length: seasonMap.keys.length,
vsync: this,
initialIndex: selectedSeason != null
? selectedSeason! - 1
: (seasonMap.keys.first == 0 ? 1 : 0),
initialIndex: getSelectedSeason(),
);
_tabController?.addListener(() {
// This is for rendering the component again for the selection of another tab
_tabController!.addListener(() {
setState(() {});
});
getWatchHistory();
}
int getSelectedSeason() {
return widget.meta.currentVideo?.season ??
widget.meta.videos?.lastWhereOrNull((item) {
return item.progress != null;
})?.season ??
widget.season ??
0;
}
getWatchHistory() async {
final traktService = TraktService.instance;
try {
if (TraktService.isEnabled()) {
final result = await traktService!.getProgress(widget.meta);
final result = await traktService!.getProgress(
widget.meta,
bypassCache: false,
);
for (final item in result) {
if (!_traktProgress.containsKey(item.season)) {
_traktProgress.addAll(<int, Map<int, double>>{
item.season!: {},
});
}
_traktProgress[item.season!] = _traktProgress[item.season] ?? {};
_traktProgress[item.season]![item.episode!] = item.progress;
}
setState(() {
meta = result;
});
setState(() {});
final index = getSelectedSeason();
_tabController?.animateTo(index);
return;
}
@ -107,7 +114,9 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
_progress[item.id] = item.progress.toDouble();
}
setState(() {});
final index = getSelectedSeason();
_tabController?.animateTo(index);
}
@override
@ -117,13 +126,12 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
}
Map<int, List<Video>> _organizeEpisodes() {
final episodes = widget.meta.videos ?? [];
final episodes = meta.videos ?? [];
return groupBy(episodes, (Video video) => video.season);
}
void openEpisode({
required int currentSeason,
required Video episode,
required int index,
}) async {
if (widget.service == null) {
return;
@ -131,26 +139,19 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
final onClose = showModalBottomSheet(
context: context,
builder: (context) {
final meta = widget.meta.copyWith(
nextSeason: currentSeason,
nextEpisode: episode.episode,
);
final meta = this.meta.copyWith(
selectedVideoIndex: index,
);
return Scaffold(
appBar: AppBar(
title: Text("Streams for S$currentSeason E${episode.episode}"),
title: Text(
"Streams for S${meta.currentVideo?.season} E${meta.currentVideo?.episode}",
),
),
body: RenderStreamList(
service: widget.service!,
id: episode.tvdbId != null
? meta.copyWith(
episodeExternalIds: {
"tvdb": episode.tvdbId,
},
)
: meta,
season: currentSeason.toString(),
episode: episode.number?.toString(),
id: meta,
shouldPop: widget.shouldPop,
),
);
@ -160,7 +161,7 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
if (widget.shouldPop) {
final val = await onClose;
if (val is MediaURLSource && context.mounted) {
if (val is MediaURLSource && context.mounted && mounted) {
Navigator.pop(
context,
val,
@ -225,11 +226,7 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
widget.meta.videos!.length,
);
openEpisode(
currentSeason:
widget.meta.videos![randomIndex].season,
episode: widget.meta.videos![randomIndex],
);
openEpisode(index: randomIndex);
},
),
),
@ -271,19 +268,24 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
final episodes = seasonMap[currentSeason]!;
final episode = episodes[index];
final progress = _traktProgress[episode.season]
?[episode.episode] ==
null
? (_progress[episode.id] ?? 0) / 100
: (_traktProgress[episode.season]![episode.episode]! / 100);
final videoIndex = meta.videos?.indexOf(episode);
final progress = ((!TraktService.isEnabled()
? (_progress[episode.id] ?? 0) / 100
: videoIndex != -1
? (meta.videos![videoIndex!].progress)
: 0.toDouble()) ??
0) /
100;
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () async {
openEpisode(
currentSeason: currentSeason,
episode: episode,
);
if (videoIndex != null) {
openEpisode(
index: videoIndex,
);
}
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,

View file

@ -14,7 +14,6 @@ import '../../../utils/load_language.dart';
import '../../connections/types/stremio/stremio_base.types.dart' as types;
import '../../connections/widget/stremio/stremio_season_selector.dart';
import '../../trakt/service/trakt.service.dart';
import '../../trakt/types/common.dart';
import '../../watch_history/service/zeee_watch_history.dart';
import '../types/doc_source.dart';
import 'video_viewer/video_viewer_ui.dart';
@ -55,7 +54,9 @@ class _VideoViewerState extends State<VideoViewer> {
return duration > 0 ? (position / duration * 100) : 0;
}
Future<List<TraktProgress>>? traktProgress;
bool timeLoaded = false;
Future<types.Meta>? traktProgress;
Future<void> saveWatchHistory() async {
final duration = player.state.duration.inSeconds;
@ -65,10 +66,17 @@ class _VideoViewerState extends State<VideoViewer> {
return;
}
final position = player.state.position.inSeconds;
final progress = duration > 0 ? (position / duration * 100).round() : 0;
if (gotFromTraktDuration == false) {
_logger.info(
"did not start the scrobbing because initially time is not retrieved from the api",
);
return;
}
if (progress == 0) {
final position = player.state.position.inSeconds;
final progress = duration > 0 ? (position / duration * 100) : 0;
if (progress < 0.01) {
_logger.info('No progress to save.');
return;
}
@ -99,7 +107,7 @@ class _VideoViewerState extends State<VideoViewer> {
await zeeeWatchHistory!.saveWatchHistory(
history: WatchHistory(
id: _source.id,
progress: progress,
progress: progress.round(),
duration: duration.toDouble(),
episode: _source.episode,
season: _source.season,
@ -116,44 +124,50 @@ class _VideoViewerState extends State<VideoViewer> {
late DocSource _source;
bool canCallOnce = false;
bool gotFromTraktDuration = false;
int? traktId;
Future<void> setDurationFromTrakt() async {
if (player.state.duration.inSeconds < 2) {
return;
try {
if (player.state.duration.inSeconds < 2) {
return;
}
if (gotFromTraktDuration) {
return;
}
gotFromTraktDuration = true;
if (!TraktService.isEnabled() || traktProgress == null) {
player.play();
return;
}
final progress = await traktProgress;
if (widget.meta is! types.Meta) {
return;
}
final meta = (progress ?? widget.meta) as types.Meta;
final duration = Duration(
seconds: calculateSecondsFromProgress(
player.state.duration.inSeconds.toDouble(),
meta.currentVideo?.progress ?? meta.progress ?? 0,
),
);
if (duration.inSeconds > 10) {
await player.seek(duration);
}
await player.play();
} catch (e) {
await player.play();
}
if (canCallOnce) {
return;
}
canCallOnce = true;
if (!TraktService.isEnabled() || traktProgress == null) {
player.play();
return;
}
final progress = await traktProgress;
if ((progress ?? []).isEmpty) {
player.play();
return;
}
traktId = progress!.first.traktId;
final duration = Duration(
seconds: calculateSecondsFromProgress(
player.state.duration.inSeconds.toDouble(),
progress.first.progress,
),
);
await player.seek(duration);
await player.play();
}
List<StreamSubscription> listener = [];
@ -190,9 +204,7 @@ class _VideoViewerState extends State<VideoViewer> {
});
_streamListen = player.stream.playing.listen((playing) {
if (playing) {
saveWatchHistory();
}
saveWatchHistory();
});
if (widget.meta is types.Meta && TraktService.isEnabled()) {
@ -250,7 +262,6 @@ class _VideoViewerState extends State<VideoViewer> {
}
}
late StreamSubscription<bool> _streamComplete;
late StreamSubscription<bool> _streamListen;
late StreamSubscription<dynamic> _duration;

View file

@ -1,10 +1,13 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:madari_client/features/connections/service/base_connection_service.dart';
import 'package:madari_client/features/trakt/service/trakt.service.dart';
import 'package:madari_client/utils/common.dart';
import '../../connections/types/stremio/stremio_base.types.dart';
import '../../connections/widget/base/render_library_list.dart';
import '../../settings/screen/trakt_integration_screen.dart';
@ -155,6 +158,49 @@ class TraktContainerState extends State<TraktContainer> {
}
}
late final Map<String, List<ContextMenuItem>> actions = {
"continue_watching": [
ContextMenuItem(
id: "remove",
icon: CupertinoIcons.clear,
title: 'Remove',
isDestructiveAction: true,
onCallback: (action, key) async {
if (key is! Meta) {
return;
}
if (key.traktProgressId == null) {
return;
}
await TraktService.instance!.removeFromContinueWatching(
key.traktProgressId!.toString(),
);
if (context.mounted && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Removed successfully"),
),
);
}
},
),
],
"watchlist": [
ContextMenuItem(
id: "remove",
icon: CupertinoIcons.clear,
title: 'Remove',
isDestructiveAction: true,
onCallback: (action, key) {
TraktService.instance!.removeFromWatchlist(key as Meta);
},
),
],
};
Future<void> refresh() async {
try {
_logger.info('Refreshing data for ${widget.loadId}');
@ -209,6 +255,16 @@ class TraktContainerState extends State<TraktContainer> {
},
items: _cachedItems ?? [],
error: _error,
contextMenuItems:
actions.containsKey(widget.loadId)
? actions[widget.loadId]!
: [],
onContextMenu: (action, items) {
actions[widget.loadId]!
.firstWhereOrNull((item) {
return item.id == action;
})?.onCallback!(action, items);
},
isLoadingMore: _isLoading,
hasError: _error != null,
heroPrefix: "trakt_up_next${widget.loadId}",
@ -276,9 +332,20 @@ class TraktContainerState extends State<TraktContainer> {
SizedBox(
height: getListHeight(context),
child: RenderListItems(
isWide: widget.loadId == "up_next_series",
isWide: widget.loadId == "up_next_series" ||
widget.loadId == "upcoming_schedule",
items: _cachedItems ?? [],
error: _error,
contextMenuItems: actions.containsKey(widget.loadId)
? actions[widget.loadId]!
: [],
onContextMenu: (action, items) async {
actions[widget.loadId]!.firstWhereOrNull((item) {
return item.id == action;
})?.onCallback!(action, items);
Navigator.of(context, rootNavigator: true).pop();
},
itemScrollController: _scrollController,
hasError: _error != null,
heroPrefix: "trakt_up_next${widget.loadId}",

View file

@ -6,6 +6,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:madari_client/utils/common.dart';
import 'package:pocketbase/pocketbase.dart';
import 'package:rxdart/rxdart.dart';
@ -14,7 +15,6 @@ 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');
@ -41,6 +41,92 @@ class TraktService {
);
}
Future<void> removeFromContinueWatching(String id) async {
if (!isEnabled()) {
_logger.info('Trakt integration is not enabled');
return;
}
try {
_logger.info(
'Removing item from history (continue watching): $id',
);
final response = await http.delete(
Uri.parse('$_baseUrl/sync/playback/$id'),
headers: headers,
);
if (response.statusCode != 204) {
_logger.severe(
'Failed to remove item from history: ${response.statusCode} $id',
);
throw Exception('Failed to remove item from history');
}
_cache.remove('$_baseUrl/sync/watched/shows');
_cache.remove('$_baseUrl/sync/playback');
refetchKey.add(["continue_watching", "up_next_series"]);
_logger.info(
'Successfully removed item from history (continue watching)',
);
} catch (e, stack) {
_logger.severe('Error removing item from history: $e', stack);
rethrow;
}
}
Future<void> removeFromWatchlist(Meta meta) async {
if (!isEnabled()) {
_logger.info('Trakt integration is not enabled');
return;
}
try {
_logger.info('Removing item from watchlist: ${meta.id}');
final response = await http.post(
Uri.parse('$_baseUrl/sync/watchlist/remove'),
headers: headers,
body: json.encode({
if (meta.type == "movie")
'movies': [
{
'ids': {
'imdb': meta.id,
},
},
],
if (meta.type == "shows")
'shows': [
{
'ids': {
'imdb': meta.id,
},
}
],
}),
);
if (response.statusCode != 200) {
_logger.severe(
'Failed to remove item from watchlist: ${response.statusCode}');
throw Exception('Failed to remove item from watchlist');
}
_cache.remove('$_baseUrl/sync/watchlist');
refetchKey.add(["watchlist"]);
_logger.info('Successfully removed item from watchlist');
} catch (e, stack) {
_logger.severe('Error removing item from watchlist: $e', stack);
rethrow;
}
}
clearCache() {
_logger.info('Clearing cache');
_cache.clear();
@ -165,41 +251,37 @@ class TraktService {
},
};
} else {
final Map<String, dynamic> episodeExternalIds =
meta.episodeExternalIds ?? {};
final isEmpty = episodeExternalIds.keys.isEmpty;
if (!isEmpty) {
return {
"episode": {
"ids": meta.episodeExternalIds,
},
};
}
if (meta.currentVideo?.id != null) {
if (meta.currentVideo?.tvdbId != null) {
return {
"episode": {
"ids": {
"imdb": meta.currentVideo?.id,
"tvdb": meta.currentVideo?.tvdbId!,
},
},
};
}
if (meta.currentVideo?.season != null &&
meta.currentVideo?.episode != null) {
return {
"episode": {
"season": meta.currentVideo!.season,
"episode": meta.currentVideo!.episode,
},
"show": {
"ids": {
"imdb": meta.imdbId ?? meta.id,
}
},
};
}
return {
"show": {
"title": meta.name,
"year": meta.year,
"ids": {
"imdb": meta.imdbId ?? meta.id,
}
},
"episode": {
"season": meta.currentVideo?.season ?? meta.nextSeason,
"number": meta.currentVideo?.number ?? meta.nextEpisode,
},
"ids": {
"imdb": meta.currentVideo?.id ?? meta.id,
}
}
};
}
}
@ -218,21 +300,14 @@ class TraktService {
try {
_logger.info('Fetching up next series');
final List<dynamic> watchedShows =
await _makeRequest('$_baseUrl/sync/watched/shows');
final List<dynamic> watchedShows = await _makeRequest(
'$_baseUrl/sync/watched/shows',
);
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
final items = watchedShows.where((show) {
try {
show['show']['ids']['trakt'];
show['show']['ids']['imdb'];
return true;
} catch (e) {
return false;
}
}).toList();
final items = watchedShows.toList();
if (startIndex >= items.length) {
yield [];
@ -260,23 +335,17 @@ class TraktService {
Meta(
type: "series",
id: imdb,
externalIds: show['show']['ids'],
episodeExternalIds: nextEpisode['ids'],
),
);
item as Meta;
return item.copyWith(
nextEpisode: nextEpisode['number'],
nextSeason: nextEpisode['season'],
nextEpisodeTitle: nextEpisode['title'],
externalIds: show['show']['ids'],
episodeExternalIds: nextEpisode['ids'],
);
return patchMetaObjectForShow(item as Meta, nextEpisode);
}
} catch (e) {
_logger.severe('Error fetching progress for show $showId: $e');
} catch (e, stack) {
_logger.severe(
'Error fetching progress for show $showId: $e',
e,
stack,
);
return null;
}
@ -295,8 +364,89 @@ class TraktService {
}
}
Future<List<LibraryItem>> getContinueWatching(
{int page = 1, int itemsPerPage = 5}) async {
Meta patchMetaObjectForShow(
Meta meta,
dynamic obj, {
double? progress,
}) {
if (meta.videos?.isEmpty == true) {
meta.videos = [];
meta.videos?.add(
Video(
season: obj['season'],
number: obj['number'],
thumbnail: meta.poster,
id: _traktIdsToMetaId(obj['ids']),
),
);
return meta;
}
final videoIndexByTvDB = meta.videos?.firstWhereOrNull((item) {
return item.tvdbId == obj['ids']['tvdb'] && item.tvdbId != null;
});
final videoBySeasonOrEpisode = meta.videos?.firstWhereOrNull((item) {
return item.season == obj['season'] && item.episode == obj['number'];
});
final video = videoIndexByTvDB ?? videoBySeasonOrEpisode;
if (video == null) {
final id = _traktIdsToMetaId(obj['ids']);
meta.videos = meta.videos ?? [];
meta.videos?.add(
Video(
name: obj['title'],
season: obj['season'],
number: obj['number'],
thumbnail: meta.poster,
id: id,
episode: obj['number'],
),
);
final videosIndex = meta.videos?.length ?? 1;
return meta.copyWith(
selectedVideoIndex: videosIndex - 1,
);
}
final index = meta.videos?.indexOf(video);
meta.videos![index!].name = obj['title'];
meta.videos![index].tvdbId =
meta.videos![index].tvdbId ?? obj['ids']['tvdb'];
meta.videos![index].ids = obj['ids'];
return meta.copyWith(
selectedVideoIndex: index,
);
}
String _traktIdsToMetaId(dynamic ids) {
String id;
if (ids['imdb'] != null) {
id = ids['imdb'];
} else if (ids['tmdb'] != null) {
id = "tmdb:${ids['tmdb']}";
} else if (ids['trakt']) {
id = "trakt:${ids['trakt']}";
} else {
id = "na";
}
return id;
}
Future<List<LibraryItem>> getContinueWatching({
int page = 1,
int itemsPerPage = 5,
}) async {
await initStremioService();
if (!isEnabled()) {
@ -306,61 +456,65 @@ class TraktService {
try {
_logger.info('Fetching continue watching');
final continueWatching = await _makeRequest('$_baseUrl/sync/playback');
final Map<String, double> progress = {};
final metaList = continueWatching
.map((movie) {
try {
if (movie['type'] == 'episode') {
progress[movie['show']['ids']['imdb']] = movie['progress'];
return Meta(
type: "series",
id: movie['show']['ids']['imdb'],
progress: movie['progress'],
nextSeason: movie['episode']['season'],
nextEpisode: movie['episode']['number'],
nextEpisodeTitle: movie['episode']['title'],
externalIds: movie['show']['ids'],
episodeExternalIds: movie['episode']['ids'],
);
}
final imdb = movie['movie']['ids']['imdb'];
progress[imdb] = movie['progress'];
return Meta(
type: "movie",
id: imdb,
progress: movie['progress'],
);
} catch (e) {
_logger.warning('Error mapping movie: $e');
return null;
}
})
.whereType<Meta>()
.toList();
final List<dynamic> continueWatching =
await _makeRequest('$_baseUrl/sync/playback');
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= metaList.length) {
if (startIndex >= continueWatching.length) {
return [];
}
final result = await stremioService!.getBulkItem(
metaList
.sublist(
startIndex,
endIndex > metaList.length ? metaList.length : endIndex,
)
.toList(),
);
final metaList = (await Future.wait(continueWatching
.sublist(
startIndex,
endIndex,
)
.map((movie) async {
try {
if (movie['type'] == 'episode') {
final meta = Meta(
type: "series",
id: _traktIdsToMetaId(
movie['show']['ids'],
),
);
return result;
return patchMetaObjectForShow(
(await stremioService!.getItemById(meta) as Meta),
movie['episode'],
).copyWith(
forceRegular: true,
progress: movie['progress'],
traktProgressId: movie['id'],
);
}
final movieId = _traktIdsToMetaId(movie['movie']['ids']);
final meta = Meta(
type: "movie",
id: movieId,
);
return ((await stremioService!.getItemById(meta)) as Meta).copyWith(
progress: movie['progress'],
traktProgressId: movie['id'],
);
} catch (e, stack) {
_logger.warning(
'Error mapping movie: $e',
e,
stack,
);
return null;
}
})))
.whereType<Meta>()
.toList();
return metaList;
} catch (e, stack) {
_logger.severe('Error fetching continue watching: $e', stack);
return [];
@ -384,36 +538,46 @@ class TraktService {
'$_baseUrl/calendars/my/shows/${DateFormat('yyyy-MM-dd').format(DateTime.now())}/7',
);
final result = await stremioService!.getBulkItem(
scheduleShows
.map((show) {
try {
final imdb = show['show']['ids']['imdb'];
return Meta(
type: "series",
id: imdb,
externalIds: show['show']['ids'],
);
} catch (e) {
_logger.warning('Error mapping show: $e');
return null;
}
})
.whereType<Meta>()
.toList(),
);
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= result.length) {
if (startIndex >= scheduleShows.length) {
return [];
}
return result.sublist(
final result = (await Future.wait(scheduleShows
.sublist(
startIndex,
endIndex > result.length ? result.length : endIndex,
);
endIndex > scheduleShows.length ? scheduleShows.length : endIndex,
)
.map((show) async {
try {
final imdb = _traktIdsToMetaId(
show['show']['ids'],
);
final result = Meta(
type: "series",
id: imdb,
);
final item = await stremioService!.getItemById(result);
return patchMetaObjectForShow(
(item ?? result) as Meta,
show['episode'],
).copyWith(
progress: null,
);
} catch (e) {
_logger.warning('Error mapping show: $e');
return null;
}
})))
.whereType<Meta>()
.toList();
return result;
} catch (e, stack) {
_logger.severe('Error fetching upcoming schedule: $e', stack);
return [];
@ -431,15 +595,29 @@ class TraktService {
try {
_logger.info('Fetching watchlist');
final watchlistItems = await _makeRequest('$_baseUrl/sync/watchlist');
final List<dynamic> watchlistItems =
await _makeRequest('$_baseUrl/sync/watchlist');
_logger.info('Got watchlist');
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= watchlistItems.length) {
return [];
}
final result = await stremioService!.getBulkItem(
watchlistItems
.sublist(
startIndex,
endIndex > watchlistItems.length
? watchlistItems.length
: endIndex,
)
.map((item) {
try {
final type = item['type'];
final imdb = item[type]['ids']['imdb'];
final imdb = _traktIdsToMetaId(item[type]['ids']);
if (type == "show") {
return Meta(
@ -452,8 +630,8 @@ class TraktService {
type: type,
id: imdb,
);
} catch (e) {
_logger.warning('Error mapping watchlist item: $e');
} catch (e, stack) {
_logger.warning('Error mapping watchlist item: $e', e, stack);
return null;
}
})
@ -461,25 +639,17 @@ class TraktService {
.toList(),
);
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= result.length) {
return [];
}
return result.sublist(
startIndex,
endIndex > result.length ? result.length : endIndex,
);
return result;
} catch (e, stack) {
_logger.severe('Error fetching watchlist: $e', stack);
return [];
}
}
Future<List<LibraryItem>> getShowRecommendations(
{int page = 1, int itemsPerPage = 5}) async {
Future<List<LibraryItem>> getShowRecommendations({
int page = 1,
int itemsPerPage = 5,
}) async {
await initStremioService();
if (!isEnabled()) {
@ -489,17 +659,27 @@ class TraktService {
try {
_logger.info('Fetching show recommendations');
final recommendedShows =
await _makeRequest('$_baseUrl/recommendations/shows');
final List<dynamic> recommendedShows = await _makeRequest(
'$_baseUrl/recommendations/shows',
);
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= recommendedShows.length) {
return [];
}
final result = (await stremioService!.getBulkItem(
recommendedShows
.sublist(
startIndex,
endIndex > recommendedShows.length
? recommendedShows.length
: endIndex,
)
.map((show) {
final imdb = show['ids']?['imdb'];
if (imdb == null) {
return null;
}
final imdb = _traktIdsToMetaId(show['ids']);
return Meta(
type: "series",
@ -510,18 +690,7 @@ class TraktService {
.toList(),
));
// Pagination logic
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= result.length) {
return [];
}
return result.sublist(
startIndex,
endIndex > result.length ? result.length : endIndex,
);
return result;
} catch (e, stack) {
_logger.severe('Error fetching show recommendations: $e', stack);
return [];
@ -541,15 +710,28 @@ class TraktService {
try {
_logger.info('Fetching movie recommendations');
final recommendedMovies = await _makeRequest(
final List<dynamic> recommendedMovies = await _makeRequest(
'$_baseUrl/recommendations/movies',
);
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= recommendedMovies.length) {
return [];
}
final result = await stremioService!.getBulkItem(
recommendedMovies
.sublist(
startIndex,
endIndex > recommendedMovies.length
? recommendedMovies.length
: endIndex,
)
.map((movie) {
try {
final imdb = movie['ids']['imdb'];
final imdb = _traktIdsToMetaId(movie['ids']);
return Meta(
type: "movie",
id: imdb,
@ -563,18 +745,7 @@ class TraktService {
.toList(),
);
// Pagination logic
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= result.length) {
return [];
}
return result.sublist(
startIndex,
endIndex > result.length ? result.length : endIndex,
);
return result;
} catch (e, stack) {
_logger.severe('Error fetching movie recommendations: $e', stack);
return [];
@ -794,58 +965,49 @@ class TraktService {
}
}
Future<List<TraktProgress>> getProgress(Meta meta) async {
Future<Meta> getProgress(
Meta meta, {
bool bypassCache = true,
}) async {
if (!isEnabled()) {
_logger.info('Trakt integration is not enabled');
return [];
return meta;
}
try {
if (meta.type == "series") {
final body = await _makeRequest(
final List<dynamic> body = await _makeRequest(
"$_baseUrl/sync/playback/episodes",
bypassCache: true,
bypassCache: bypassCache,
);
final List<TraktProgress> result = [];
for (final item in body) {
if (item["type"] != "episode") {
final isCurrentShow =
item["show"]?["ids"]?["imdb"] == (meta.imdbId ?? meta.id);
if (isCurrentShow == false) {
continue;
}
final isShow =
item["show"]?["ids"]?["imdb"] == (meta.imdbId ?? meta.id);
final currentEpisode = item["episode"]["number"];
final currentSeason = item["episode"]["season"];
if (isShow && meta.nextEpisode != null && meta.nextSeason != null) {
if (meta.nextSeason == currentSeason &&
meta.nextEpisode == currentEpisode) {
result.add(
TraktProgress(
id: meta.id,
progress: item["progress"]!,
episode: currentEpisode,
season: currentSeason,
traktId: item["show"]["ids"]["trakt"],
),
);
final result = meta.videos?.firstWhereOrNull((video) {
if (video.tvdbId != null &&
item['episode']['ids']['tvdb'] != null) {
return video.tvdbId == item['episode']['ids']['tvdb'];
}
} else if (isShow) {
result.add(
TraktProgress(
id: meta.id,
progress: item["progress"]!,
episode: currentEpisode,
season: currentSeason,
),
);
}
}
return result;
return video.season == item['season'] &&
video.number == item['number'];
});
if (result == null) {
continue;
}
final videoIndex = meta.videos!.indexOf(result);
meta.videos![videoIndex].progress = item['progress'];
}
return meta;
} else {
final body = await _makeRequest(
"$_baseUrl/sync/playback/movies",
@ -858,20 +1020,17 @@ class TraktService {
}
if (item["movie"]["ids"]["imdb"] == (meta.imdbId ?? meta.id)) {
return [
TraktProgress(
id: item["movie"]["ids"]["imdb"],
progress: item["progress"],
),
];
return meta.copyWith(
progress: item["progress"],
);
}
}
}
} catch (e) {
_logger.severe('Error fetching progress: $e');
return [];
return meta;
}
return [];
return meta;
}
}

View file

@ -66,7 +66,7 @@ class _StremioItemPageState extends State<StremioItemPage> {
void initState() {
super.initState();
if (widget.meta?.progress != null || widget.meta?.nextEpisode != null) {
if (widget.meta?.currentVideo != null) {
openVideo();
}
}
@ -78,11 +78,6 @@ class _StremioItemPageState extends State<StremioItemPage> {
);
if (mounted) {
final videoEpisode =
widget.meta?.currentVideo?.episode ?? widget.meta?.nextEpisode;
final videoSeason =
widget.meta?.currentVideo?.season ?? widget.meta?.nextSeason;
showModalBottomSheet(
context: context,
builder: (context) {
@ -95,7 +90,7 @@ class _StremioItemPageState extends State<StremioItemPage> {
icon: const Icon(Icons.close),
),
title: Text(
"Streams ${videoSeason != null ? "S$videoSeason" : ""} ${videoEpisode != null ? " E$videoEpisode" : ""}"
"Streams S${widget.meta?.currentVideo?.season ?? 0} E${widget.meta?.currentVideo?.episode ?? 0}"
.trim(),
),
),
@ -108,8 +103,6 @@ class _StremioItemPageState extends State<StremioItemPage> {
: null,
service: widget.service!,
id: widget.meta as LibraryItem,
season: videoSeason?.toString(),
episode: videoEpisode?.toString(),
shouldPop: false,
),
),
@ -161,7 +154,8 @@ class _StremioItemPageState extends State<StremioItemPage> {
return StremioItemViewer(
hero: widget.hero,
meta: meta ?? widget.meta,
meta: (meta ?? widget.meta)
?.copyWith(selectedVideoIndex: widget.meta?.selectedVideoIndex),
original: meta,
progress: widget.meta?.progress != null ? widget.meta!.progress : 0,
service: state.data == null

View file

@ -8,3 +8,16 @@ extension FirstWhereOrNullExtension<T> on Iterable<T> {
return null;
}
}
extension LastWhereOrNullExtension<T> on Iterable<T> {
T? lastWhereOrNull(bool Function(T) test) {
T? elementItem;
for (var element in this) {
if (test(element)) {
elementItem = element;
}
}
return elementItem;
}
}