fix: fixed trakt integration

This commit is contained in:
omkar 2025-01-11 09:16:15 +05:30
parent 356f197a4c
commit 0e97a8ae61
17 changed files with 913 additions and 479 deletions

View file

@ -0,0 +1 @@
final List<String> globalLogs = [];

View file

@ -24,13 +24,13 @@ class _AutoImportState extends State<AutoImport> {
late StremioService _stremio;
final List<FolderItem> _selected = [];
bool _isLoading = false;
bool _selectAll = false;
Future<List<FolderItem>>? _folders;
@override
void initState() {
super.initState();
initialValueImport();
}
@ -89,6 +89,21 @@ class _AutoImportState extends State<AutoImport> {
}
}
void toggleSelectAll() async {
final folders = await _folders;
if (folders == null) return;
setState(() {
_selectAll = !_selectAll;
if (_selectAll) {
_selected.clear();
_selected.addAll(folders);
} else {
_selected.clear();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -97,6 +112,17 @@ class _AutoImportState extends State<AutoImport> {
title: const Text("Import Libraries"),
backgroundColor: Colors.transparent,
actions: [
IconButton(
icon: Icon(_selectAll
? Icons.check_box_outlined
: Icons.check_box_outline_blank),
onPressed: () {
toggleSelectAll();
},
),
const SizedBox(
width: 12,
),
ElevatedButton.icon(
onPressed: _selected.isNotEmpty
? () {
@ -130,25 +156,28 @@ class _AutoImportState extends State<AutoImport> {
);
}
return ListView.builder(
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (item, index) {
final item = snapshot.data![index];
final folders = snapshot.data!;
final selected =
_selected.where((selected) => selected.id == item.id);
return ListView.builder(
itemCount: folders.length,
itemBuilder: (context, index) {
final item = folders[index];
final isSelected =
_selected.any((selected) => selected.id == item.id);
return ListTile(
onTap: () {
setState(() {
if (selected.isEmpty) {
_selected.add(item);
if (isSelected) {
_selected
.removeWhere((selected) => selected.id == item.id);
} else {
_selected.remove(item);
_selected.add(item);
}
});
},
leading: selected.isNotEmpty
leading: isSelected
? const Icon(Icons.check)
: const Icon(Icons.check_box_outline_blank),
title: Text(item.title),

View file

@ -5,6 +5,7 @@ import 'package:cached_query/cached_query.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:json_annotation/json_annotation.dart';
import 'package:logging/logging.dart';
import 'package:madari_client/features/connections/types/base/base.dart';
import 'package:madari_client/features/connections/widget/stremio/stremio_card.dart';
import 'package:madari_client/features/connections/widget/stremio/stremio_list_item.dart';
@ -23,87 +24,96 @@ typedef OnStreamCallback = void Function(List<StreamList>? items, Error?);
class StremioConnectionService extends BaseConnectionService {
final StremioConfig config;
final Logger _logger = Logger('StremioConnectionService');
StremioConnectionService({
required super.connectionId,
required this.config,
});
}) {
_logger.info('StremioConnectionService initialized with config: $config');
}
@override
Future<LibraryItem?> getItemById(LibraryItem id) async {
_logger.fine('Fetching item by ID: ${id.id}');
return Query<LibraryItem?>(
key: "meta_${id.id}",
config: QueryConfig(
cacheDuration: const Duration(days: 30),
refetchDuration: (id as Meta).type == "movie"
? const Duration(days: 30)
: const Duration(
days: 1,
),
: const Duration(days: 1),
),
queryFn: () async {
for (final addon in config.addons) {
_logger.finer('Checking addon: $addon');
final manifest = await _getManifest(addon);
if (manifest.resources == null) {
_logger.finer('No resources found in manifest for addon: $addon');
continue;
}
List<String> idPrefixes = [];
bool isMeta = false;
for (final item in manifest.resources!) {
if (item.name == "meta") {
idPrefixes.addAll(
(item.idPrefix ?? []) + (item.idPrefixes ?? []));
idPrefixes
.addAll((item.idPrefix ?? []) + (item.idPrefixes ?? []));
isMeta = true;
break;
}
}
if (isMeta == false) {
if (!isMeta) {
_logger
.finer('No meta resource found in manifest for addon: $addon');
continue;
}
final ids = ((manifest.idPrefixes ?? []) + idPrefixes)
.firstWhere((item) => id.id.startsWith(item),
orElse: () => "");
.firstWhere((item) => id.id.startsWith(item), orElse: () => "");
if (ids.isEmpty) {
_logger.finer('No matching ID prefix found for addon: $addon');
continue;
}
final result = await http.get(
Uri.parse(
"${_getAddonBaseURL(addon)}/meta/${id.type}/${id.id}.json",
),
"${_getAddonBaseURL(addon)}/meta/${id.type}/${id.id}.json"),
);
final item = jsonDecode(result.body);
if (item['meta'] == null) {
_logger.finer(
'No meta data found for item: ${id.id} in addon: $addon');
return null;
}
return StreamMetaResponse.fromJson(item).meta;
}
_logger.warning('No meta data found for item: ${id.id} in any addon');
return null;
})
},
)
.stream
.where((item) {
return item.status != QueryStatus.loading;
})
.where((item) => item.status != QueryStatus.loading)
.first
.then((docs) {
if (docs.error != null) {
throw docs.error;
_logger.severe('Error fetching item by ID: ${docs.error}');
throw docs.error!;
}
return docs.data;
});
}
List<InternalManifestItemConfig> getConfig(dynamic configOutput) {
_logger.fine('Parsing config output');
final List<InternalManifestItemConfig> configItems = [];
for (final item in configOutput) {
@ -113,6 +123,7 @@ class StremioConnectionService extends BaseConnectionService {
configItems.add(itemToPush);
}
_logger.finer('Config parsed successfully: $configItems');
return configItems;
}
@ -124,6 +135,7 @@ class StremioConnectionService extends BaseConnectionService {
int? perPage,
String? cursor,
}) async {
_logger.fine('Fetching items for library: ${library.id}');
final List<Meta> returnValue = [];
final configItems = getConfig(library.config);
@ -160,27 +172,22 @@ class StremioConnectionService extends BaseConnectionService {
final result = await Query(
config: QueryConfig(
cacheDuration: const Duration(
hours: 8,
),
cacheDuration: const Duration(hours: 8),
),
queryFn: () async {
final httpBody = await http.get(
Uri.parse(url),
);
_logger.finer('Fetching catalog from URL: $url');
final httpBody = await http.get(Uri.parse(url));
return StrmioMeta.fromJson(jsonDecode(httpBody.body));
},
key: url,
)
.stream
.where((item) {
return item.status != QueryStatus.loading;
})
.where((item) => item.status != QueryStatus.loading)
.first
.then((docs) {
if (docs.error != null) {
throw docs.error;
_logger.severe('Error fetching catalog: ${docs.error}');
throw docs.error!;
}
return docs.data!;
});
@ -189,6 +196,7 @@ class StremioConnectionService extends BaseConnectionService {
returnValue.addAll(result.metas ?? []);
}
_logger.finer('Items fetched successfully: ${returnValue.length} items');
return PagePaginatedResult(
items: returnValue.toList(),
currentPage: page ?? 1,
@ -199,6 +207,7 @@ class StremioConnectionService extends BaseConnectionService {
@override
Widget renderCard(LibraryItem item, String heroPrefix) {
_logger.fine('Rendering card for item: ${item.id}');
return StremioCard(
item: item,
prefix: heroPrefix,
@ -209,7 +218,9 @@ class StremioConnectionService extends BaseConnectionService {
@override
Future<List<LibraryItem>> getBulkItem(List<LibraryItem> ids) async {
_logger.fine('Fetching bulk items: ${ids.length} items');
if (ids.isEmpty) {
_logger.finer('No items to fetch');
return [];
}
@ -218,6 +229,7 @@ class StremioConnectionService extends BaseConnectionService {
(res) async {
return getItemById(res).then((item) {
if (item == null) {
_logger.finer('Item not found: ${res.id}');
return null;
}
@ -228,9 +240,8 @@ class StremioConnectionService extends BaseConnectionService {
nextEpisodeTitle: res.nextEpisodeTitle,
);
}).catchError((err, stack) {
print(err);
print(stack);
return null;
_logger.severe('Error fetching item: ${res.id}', err, stack);
return (res as Meta);
});
},
),
@ -241,10 +252,12 @@ class StremioConnectionService extends BaseConnectionService {
@override
Widget renderList(LibraryItem item, String heroPrefix) {
_logger.fine('Rendering list item: ${item.id}');
return StremioListItem(item: item);
}
Future<StremioManifest> _getManifest(String url) async {
_logger.fine('Fetching manifest from URL: $url');
return Query(
key: url,
config: QueryConfig(
@ -254,31 +267,33 @@ class StremioConnectionService extends BaseConnectionService {
queryFn: () async {
final String result;
if (manifestCache.containsKey(url)) {
_logger.finer('Manifest found in cache for URL: $url');
result = manifestCache[url]!;
} else {
_logger.finer('Fetching manifest from network for URL: $url');
result = (await http.get(Uri.parse(url))).body;
manifestCache[url] = result;
}
final body = jsonDecode(result);
final resultFinal = StremioManifest.fromJson(body);
_logger.finer('Manifest successfully parsed for URL: $url');
return resultFinal;
})
},
)
.stream
.where((item) {
return item.status != QueryStatus.loading;
})
.where((item) => item.status != QueryStatus.loading)
.first
.then((docs) {
if (docs.error != null) {
throw docs.error;
_logger.severe('Error fetching manifest: ${docs.error}');
throw docs.error!;
}
return docs.data!;
});
;
}
_getAddonBaseURL(String input) {
String _getAddonBaseURL(String input) {
return input.endsWith("/manifest.json")
? input.replaceAll("/manifest.json", "")
: input;
@ -286,6 +301,7 @@ class StremioConnectionService extends BaseConnectionService {
@override
Future<List<ConnectionFilter<T>>> getFilters<T>(LibraryRecord library) async {
_logger.fine('Fetching filters for library: ${library.id}');
final configItems = getConfig(library.config);
List<ConnectionFilter<T>> filters = [];
@ -294,6 +310,7 @@ class StremioConnectionService extends BaseConnectionService {
final addonManifest = await _getManifest(addon.addon);
if ((addonManifest.catalogs?.isEmpty ?? true) == true) {
_logger.finer('No catalogs found for addon: ${addon.addon}');
continue;
}
@ -303,6 +320,7 @@ class StremioConnectionService extends BaseConnectionService {
for (final catalog in catalogs) {
if (catalog.extra == null) {
_logger.finer('No extra filters found for catalog: ${catalog.id}');
continue;
}
for (final extraItem in catalog.extra!) {
@ -326,8 +344,11 @@ class StremioConnectionService extends BaseConnectionService {
}
}
}
} catch (e) {}
} catch (e) {
_logger.severe('Error fetching filters', e);
}
_logger.finer('Filters fetched successfully: $filters');
return filters;
}
@ -338,6 +359,7 @@ class StremioConnectionService extends BaseConnectionService {
String? episode,
OnStreamCallback? callback,
}) async {
_logger.fine('Fetching streams for item: ${id.id}');
final List<StreamList> streams = [];
final meta = id as Meta;
@ -351,6 +373,9 @@ class StremioConnectionService extends BaseConnectionService {
final resource = resource_ as ResourceObject;
if (!doesAddonSupportStream(resource, addonManifest, meta)) {
_logger.finer(
'Addon does not support stream: ${addonManifest.name}',
);
continue;
}
@ -363,6 +388,9 @@ class StremioConnectionService extends BaseConnectionService {
final result = await http.get(Uri.parse(url), headers: {});
if (result.statusCode == 404) {
_logger.warning(
'Invalid status code for addon: ${addonManifest.name}',
);
if (callback != null) {
callback(
null,
@ -377,15 +405,14 @@ class StremioConnectionService extends BaseConnectionService {
},
)
.stream
.where((item) {
return item.status != QueryStatus.loading;
})
.where((item) => item.status != QueryStatus.loading)
.first
.then((docs) {
return docs.data;
});
if (result == null) {
_logger.finer('No stream data found for URL: $url');
continue;
}
@ -411,7 +438,7 @@ class StremioConnectionService extends BaseConnectionService {
}
}
}).catchError((error, stacktrace) {
print(stacktrace);
_logger.severe('Error fetching streams', error, stacktrace);
if (callback != null) callback(null, error);
});
@ -419,7 +446,7 @@ class StremioConnectionService extends BaseConnectionService {
}
await Future.wait(promises);
_logger.finer('Streams fetched successfully: ${streams.length} streams');
return;
}
@ -429,6 +456,7 @@ class StremioConnectionService extends BaseConnectionService {
Meta meta,
) {
if (resource.name != "stream") {
_logger.finer('Resource is not a stream: ${resource.name}');
return false;
}
@ -438,10 +466,12 @@ class StremioConnectionService extends BaseConnectionService {
final types = resource.types ?? addonManifest.types;
if (types == null || !types.contains(meta.type)) {
_logger.finer('Addon does not support type: ${meta.type}');
return false;
}
if ((idPrefixes ?? []).isEmpty == true) {
_logger.finer('No ID prefixes found, assuming support');
return true;
}
@ -450,9 +480,11 @@ class StremioConnectionService extends BaseConnectionService {
);
if (hasIdPrefix.isEmpty) {
_logger.finer('No matching ID prefix found');
return false;
}
_logger.finer('Addon supports stream');
return true;
}
@ -470,17 +502,19 @@ class StremioConnectionService extends BaseConnectionService {
try {
streamTitle = utf8.decode(streamTitle.runes.toList());
} catch (e) {}
} catch (e) {
_logger.warning('Failed to decode stream title', e);
}
String? streamDescription = item.description;
try {
streamDescription = item.description != null
? utf8.decode(
(item.description!).runes.toList(),
)
? utf8.decode((item.description!).runes.toList())
: null;
} catch (e) {}
} catch (e) {
_logger.warning('Failed to decode stream description', e);
}
String title = meta.name ?? item.title ?? "No title";
@ -506,17 +540,19 @@ class StremioConnectionService extends BaseConnectionService {
}
if (source == null) {
_logger.finer('No valid source found for stream');
return null;
}
String addonName = addonManifest.name;
try {
addonName = utf8.decode(
(addonName).runes.toList(),
);
} catch (e) {}
addonName = utf8.decode((addonName).runes.toList());
} catch (e) {
_logger.warning('Failed to decode addon name', e);
}
_logger.finer('Stream list created successfully');
return StreamList(
title: streamTitle,
description: streamDescription,

View file

@ -315,6 +315,7 @@ class Meta extends LibraryItem {
final int? traktId;
final dynamic externalIds;
final dynamic episodeExternalIds;
bool forceRegularMode;
@ -333,6 +334,13 @@ class Meta extends LibraryItem {
return videos?.firstWhereOrNull(
(episode) {
if (episode.tvdbId != null && episodeExternalIds != null) {
return episode.tvdbId == episodeExternalIds['tvdb'];
}
print(episode.tvdbId);
print(episodeExternalIds?['tvdb']);
return nextEpisode == episode.episode && nextSeason == episode.season;
},
);
@ -381,6 +389,7 @@ class Meta extends LibraryItem {
this.language,
this.dvdRelease,
this.progress,
this.episodeExternalIds,
}) : super(id: id);
Meta copyWith({
@ -403,6 +412,8 @@ class Meta extends LibraryItem {
List<String>? writer,
String? background,
String? logo,
dynamic externalIds,
dynamic episodeExternalIds,
String? awards,
int? moviedbId,
String? runtime,
@ -440,6 +451,7 @@ class Meta extends LibraryItem {
year: year ?? this.year,
status: status ?? this.status,
tvdbId: tvdbId ?? this.tvdbId,
externalIds: externalIds ?? this.externalIds,
director: director ?? this.director,
writer: writer ?? this.writer,
background: background ?? this.background,
@ -464,6 +476,7 @@ class Meta extends LibraryItem {
nextEpisodeTitle: nextEpisodeTitle ?? this.nextEpisodeTitle,
nextSeason: nextSeason ?? this.nextSeason,
progress: progress ?? this.progress,
episodeExternalIds: episodeExternalIds ?? this.episodeExternalIds,
);
factory Meta.fromJson(Map<String, dynamic> json) {

View file

@ -289,21 +289,17 @@ class __RenderLibraryListState extends State<_RenderLibraryList> {
.whereType<LibraryItem>()
.toList();
if (data.status == QueryStatus.loading && items.isEmpty) {
return const CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SpinnerCards(),
)
],
);
}
return RenderListItems(
hasError: data.status == QueryStatus.error,
onRefresh: () {
query.refetch();
},
loadMore: () {
query.getNextPage();
},
itemScrollController: _scrollController,
isLoadingMore: data.status == QueryStatus.loading ||
data.status == QueryStatus.loading && items.isEmpty,
isGrid: widget.isGrid,
items: items,
heroPrefix: widget.item.id,
@ -326,6 +322,8 @@ class RenderListItems extends StatelessWidget {
final String heroPrefix;
final dynamic error;
final bool isWide;
final bool isLoadingMore;
final VoidCallback? loadMore;
const RenderListItems({
super.key,
@ -339,6 +337,8 @@ class RenderListItems extends StatelessWidget {
this.itemScrollController,
this.error,
this.isWide = false,
this.isLoadingMore = false,
this.loadMore,
});
@override
@ -351,7 +351,9 @@ class RenderListItems extends StatelessWidget {
return CustomScrollView(
controller: controller,
physics: isGrid ? null : const NeverScrollableScrollPhysics(),
physics: isGrid
? const AlwaysScrollableScrollPhysics()
: const NeverScrollableScrollPhysics(),
slivers: [
if (hasError)
SliverToBoxAdapter(
@ -389,7 +391,7 @@ class RenderListItems extends StatelessWidget {
),
),
),
if (isGrid)
if (isGrid) ...[
SliverGrid.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: getGridResponsiveColumnCount(context),
@ -407,11 +409,47 @@ class RenderListItems extends StatelessWidget {
);
},
),
if (!isGrid)
if (isLoadingMore)
SliverPadding(
padding: const EdgeInsets.only(
top: 8.0,
right: 8.0,
),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: getGridResponsiveColumnCount(context),
mainAxisSpacing: getGridResponsiveSpacing(context),
crossAxisSpacing: getGridResponsiveSpacing(context),
childAspectRatio: 2 / 3,
),
delegate: SliverChildBuilderDelegate(
(ctx, index) {
return ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Shimmer.fromColors(
baseColor: Colors.grey[800]!,
highlightColor: Colors.grey[700]!,
child: Container(
color: Colors.grey[800],
),
),
);
},
childCount: 4, // Fixed number of loading items
),
),
),
] else ...[
if (isLoadingMore)
const SliverToBoxAdapter(
child: SpinnerCards(),
),
if (!isLoadingMore)
SliverToBoxAdapter(
child: SizedBox(
height: listHeight,
child: ListView.builder(
controller: itemScrollController,
itemBuilder: (ctx, index) {
final item = items[index];
@ -431,6 +469,7 @@ class RenderListItems extends StatelessWidget {
),
),
),
],
SliverPadding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom,

View file

@ -6,7 +6,7 @@ import 'package:intl/intl.dart';
import 'package:madari_client/features/connection/types/stremio.dart';
import 'package:madari_client/features/connections/service/base_connection_service.dart';
class StremioCard extends StatelessWidget {
class StremioCard extends StatefulWidget {
final LibraryItem item;
final String prefix;
final String connectionId;
@ -20,9 +20,16 @@ class StremioCard extends StatelessWidget {
required this.service,
});
@override
State<StremioCard> createState() => _StremioCardState();
}
class _StremioCardState extends State<StremioCard> {
bool hasErrorWhileLoading = false;
@override
Widget build(BuildContext context) {
final meta = item as Meta;
final meta = widget.item as Meta;
return Card(
margin: const EdgeInsets.only(right: 8),
@ -36,10 +43,10 @@ class StremioCard extends StatelessWidget {
borderRadius: BorderRadius.circular(12),
onTap: () {
context.push(
"/info/stremio/$connectionId/${meta.type}/${meta.id}?hero=$prefix${meta.type}${item.id}",
"/info/stremio/${widget.connectionId}/${meta.type}/${meta.id}?hero=${widget.prefix}${meta.type}${widget.item.id}",
extra: {
'meta': meta,
'service': service,
'service': widget.service,
},
);
},
@ -52,7 +59,7 @@ class StremioCard extends StatelessWidget {
}
bool get isInFuture {
final video = (item as Meta).currentVideo;
final video = (widget.item as Meta).currentVideo;
return video != null &&
video.firstAired != null &&
video.firstAired!.isAfter(DateTime.now());
@ -70,8 +77,15 @@ class StremioCard extends StatelessWidget {
image: DecorationImage(
image: CachedNetworkImageProvider(
"https://proxy-image.syncws.com/insecure/plain/${Uri.encodeQueryComponent(
meta.currentVideo?.thumbnail ?? meta.background!,
hasErrorWhileLoading
? meta.background!
: (meta.currentVideo?.thumbnail ?? meta.background!),
)}@webp",
errorListener: (error) {
setState(() {
hasErrorWhileLoading = true;
});
},
imageRenderMethodForWeb: ImageRenderMethodForWeb.HttpGet,
),
fit: BoxFit.cover,
@ -130,7 +144,7 @@ class StremioCard extends StatelessWidget {
),
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
"S${meta.nextSeason} E${meta.nextEpisode}",
"S${meta.currentVideo?.season ?? meta.nextSeason} E${meta.currentVideo?.episode ?? meta.nextEpisode}",
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.black,
),
@ -254,7 +268,7 @@ class StremioCard extends StatelessWidget {
meta.poster ?? meta.logo ?? getBackgroundImage(meta);
return Hero(
tag: "$prefix${meta.type}${item.id}",
tag: "${widget.prefix}${meta.type}${widget.item.id}",
child: (backgroundImage == null)
? Center(
child: Column(

View file

@ -128,7 +128,8 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
context: context,
builder: (context) {
final meta = widget.meta.copyWith(
id: episode.id,
nextSeason: currentSeason,
nextEpisode: episode.episode,
);
return Scaffold(
@ -139,6 +140,7 @@ class _StremioItemSeasonSelectorState extends State<StremioItemSeasonSelector>
service: widget.service!,
id: meta,
season: currentSeason.toString(),
episode: episode.number?.toString(),
shouldPop: widget.shouldPop,
),
);

View file

@ -16,6 +16,7 @@ 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/desktop_video_player.dart';
@ -425,11 +426,17 @@ class _VideoViewerState extends State<VideoViewer> {
);
String subtitleStyleName = config.subtitleStyle ?? 'Normal';
String subtitleStyleColor = config.subtitleColor ?? 'white';
double subtitleSize = config.subtitleSize ;
double subtitleSize = config.subtitleSize;
Color hexToColor(String hexColor) {
final hexCode = hexColor.replaceAll('#', '');
try {
return Color(int.parse('0x$hexCode'));
} catch (e) {
return Colors.white;
}
}
FontStyle getFontStyleFromString(String styleName) {
switch (styleName.toLowerCase()) {
case 'italic':
@ -439,13 +446,15 @@ class _VideoViewerState extends State<VideoViewer> {
return FontStyle.normal;
}
}
FontStyle currentFontStyle = getFontStyleFromString(subtitleStyleName);
return MaterialVideoControlsTheme(
fullscreen: mobile,
normal: mobile,
child: Video(
subtitleViewConfiguration: SubtitleViewConfiguration(
style: TextStyle(color: hexToColor(subtitleStyleColor),
style: TextStyle(
color: hexToColor(subtitleStyleColor),
fontSize: subtitleSize,
fontStyle: currentFontStyle,
fontWeight: FontWeight.bold),

View file

@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../data/global_logs.dart';
class LogsPage extends StatefulWidget {
const LogsPage({super.key});
@override
State<LogsPage> createState() => _LogsPageState();
}
class _LogsPageState extends State<LogsPage> {
List<LogEntry> parsedLogs = [];
@override
void initState() {
super.initState();
_parseLogs();
}
void _parseLogs() {
parsedLogs = globalLogs.reversed.map((log) => LogEntry.parse(log)).toList();
}
void _copyToClipboard() {
Clipboard.setData(ClipboardData(text: globalLogs.join('\n')));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Logs copied to clipboard'),
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 2),
),
);
}
Color _getLevelColor(String level) {
switch (level.toUpperCase()) {
case 'ERROR':
return Colors.red;
case 'WARN':
case 'WARNING':
return Colors.orange;
case 'INFO':
return Colors.blue;
case 'DEBUG':
return Colors.grey;
default:
return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
"Application Logs",
style: TextStyle(fontWeight: FontWeight.bold),
),
actions: [
IconButton(
onPressed: _copyToClipboard,
icon: const Icon(Icons.copy),
tooltip: 'Copy all logs',
),
IconButton(
onPressed: () {
setState(() {
_parseLogs();
});
},
icon: const Icon(Icons.refresh),
tooltip: 'Refresh logs',
),
],
),
body: parsedLogs.isEmpty
? const Center(
child: Text(
'No logs available',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
)
: ListView.builder(
itemCount: parsedLogs.length,
itemBuilder: (context, index) {
final log = parsedLogs[index];
return Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Colors.grey.withOpacity(0.2),
width: 1,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _getLevelColor(log.level).withOpacity(0.1),
borderRadius: BorderRadius.circular(3),
border: Border.all(
color:
_getLevelColor(log.level).withOpacity(0.3),
),
),
child: Text(
log.level,
style: TextStyle(
fontSize: 11,
color: _getLevelColor(log.level),
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
Text(
'${log.service}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
),
),
Text(
log.timestamp,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 4),
SelectableText(
log.message,
style: const TextStyle(
fontSize: 13,
fontFamily: 'monospace',
),
),
],
),
);
},
),
);
}
}
class LogEntry {
final String level;
final String service;
final String timestamp;
final String message;
LogEntry({
required this.level,
required this.service,
required this.timestamp,
required this.message,
});
factory LogEntry.parse(String logLine) {
final parts = logLine.split(RegExp(r'\s+'));
if (parts.length >= 3) {
final level = parts[0];
final service = parts[1];
final timestamp = parts[2];
final message = parts.skip(3).join(' ');
return LogEntry(
level: level,
service: service,
timestamp: timestamp,
message: message,
);
}
return LogEntry(
level: 'UNKNOWN',
service: 'Unknown',
timestamp: '',
message: logLine,
);
}
}

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:madari_client/engine/engine.dart';
import 'package:madari_client/features/trakt/service/trakt.service.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../utils/auth_refresh.dart';
@ -190,30 +189,6 @@ class _TraktIntegrationState extends State<TraktIntegration> {
),
centerTitle: true,
elevation: 0,
actions: [
TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
return Scaffold(
appBar: AppBar(
title: const Text("Logs"),
),
body: ListView.builder(
itemCount: TraktService.instance!.debugLogs.length,
itemBuilder: (context, item) {
return Text(
TraktService.instance!.debugLogs[item],
);
},
),
);
}),
);
},
child: Text("Debug logs"),
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),

View file

@ -1,12 +1,12 @@
import 'dart:async';
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 '../../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;
@ -20,48 +20,121 @@ class TraktContainer extends StatefulWidget {
}
class TraktContainerState extends State<TraktContainer> {
late final TraktCacheService _cacheService;
final Logger _logger = Logger('TraktContainerState');
List<LibraryItem>? _cachedItems;
bool _isLoading = false;
String? _error;
late Timer _timer;
int _currentPage = 1;
static const _itemsPerPage = 5;
final _scrollController = ScrollController();
bool get _isBottom {
if (!_scrollController.hasClients) return false;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.offset;
return currentScroll >= (maxScroll * 0.9);
}
@override
void initState() {
super.initState();
_cacheService = TraktCacheService();
_logger.info('Initializing TraktContainerState');
_loadData();
_timer = Timer.periodic(
const Duration(seconds: 30),
(timer) {
_loadData();
},
);
_scrollController.addListener(() {
if (_isBottom) {
_loadData(isLoadMore: true);
}
});
}
@override
void dispose() {
_logger.info('Disposing TraktContainerState');
_scrollController.dispose();
super.dispose();
_timer.cancel();
}
Future<void> _loadData() async {
setState(() {
Future<void> _loadData({
bool isLoadMore = false,
}) async {
_logger.info('Started loading data for the _loadData');
if (_isLoading) {
_logger.warning('Load data called while already loading');
return;
}
_isLoading = true;
setState(() {
_error = null;
});
try {
final items = await _cacheService.fetchData(widget.loadId);
final page = isLoadMore ? _currentPage + 1 : _currentPage;
List<LibraryItem>? newItems;
_logger.info('Loading data for loadId: ${widget.loadId}, page: $page');
switch (widget.loadId) {
case "up_next_series":
newItems = await TraktService.instance!
.getUpNextSeries(
page: page,
itemsPerPage: _itemsPerPage,
)
.first;
break;
case "continue_watching":
newItems = await TraktService.instance!.getContinueWatching(
page: page,
itemsPerPage: _itemsPerPage,
);
break;
case "upcoming_schedule":
newItems = await TraktService.instance!.getUpcomingSchedule(
page: page,
itemsPerPage: _itemsPerPage,
);
break;
case "watchlist":
newItems = await TraktService.instance!.getWatchlist(
page: page,
itemsPerPage: _itemsPerPage,
);
break;
case "show_recommendations":
newItems = await TraktService.instance!.getShowRecommendations(
page: page,
itemsPerPage: _itemsPerPage,
);
break;
case "movie_recommendations":
newItems = await TraktService.instance!.getMovieRecommendations(
page: page,
itemsPerPage: _itemsPerPage,
);
break;
default:
_logger.severe('Invalid loadId: ${widget.loadId}');
throw Exception("Invalid loadId: ${widget.loadId}");
}
if (mounted) {
setState(() {
_cachedItems = items;
_currentPage = page;
_cachedItems = [...?_cachedItems, ...?newItems];
_isLoading = false;
});
_logger.info('Data loaded successfully for loadId: ${widget.loadId}');
}
} catch (e) {
_logger.severe('Error loading data: $e');
if (mounted) {
setState(() {
_error = e.toString();
@ -72,7 +145,7 @@ class TraktContainerState extends State<TraktContainer> {
}
Future<void> refresh() async {
await _cacheService.refresh(widget.loadId);
_logger.info('Refreshing data');
await _loadData();
}
@ -103,6 +176,7 @@ class TraktContainerState extends State<TraktContainer> {
height: 30,
child: TextButton(
onPressed: () {
_logger.info('Navigating to Trakt details page');
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
@ -113,8 +187,14 @@ class TraktContainerState extends State<TraktContainer> {
body: Padding(
padding: const EdgeInsets.all(8.0),
child: RenderListItems(
loadMore: () {
_loadData(
isLoadMore: true,
);
},
items: _cachedItems ?? [],
error: _error,
isLoadingMore: _isLoading,
hasError: _error != null,
heroPrefix: "trakt_up_next${widget.loadId}",
service: TraktService.stremioService!,
@ -148,16 +228,15 @@ class TraktContainerState extends State<TraktContainer> {
child: Text("Nothing to see here"),
),
),
if (_isLoading && (_cachedItems ?? []).isEmpty)
const SpinnerCards(),
SizedBox(
height: getListHeight(context),
child: _isLoading
? SpinnerCards(
isWide: widget.loadId == "up_next_series",
)
: RenderListItems(
child: RenderListItems(
isWide: widget.loadId == "up_next_series",
items: _cachedItems ?? [],
error: _error,
itemScrollController: _scrollController,
hasError: _error != null,
heroPrefix: "trakt_up_next${widget.loadId}",
service: TraktService.stremioService!,

View file

@ -2,18 +2,19 @@ import 'dart:async';
import 'dart:convert';
import 'package:cached_query/cached_query.dart';
import 'package:cached_storage/cached_storage.dart';
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:pocketbase/pocketbase.dart';
import 'package:rxdart/rxdart.dart';
import '../../../engine/connection_type.dart';
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');
@ -25,9 +26,9 @@ class TraktService {
static const int _authedGetLimit = 1000;
static const Duration _rateLimitWindow = Duration(minutes: 5);
static const Duration _cacheRevalidationInterval = Duration(
hours: 1,
);
final refetchKey = BehaviorSubject<List<String>>();
static const Duration _cacheRevalidationInterval = Duration(hours: 1);
static TraktService? _instance;
static TraktService? get instance => _instance;
@ -65,9 +66,8 @@ class TraktService {
_logger.info('Initializing TraktService');
final result = await CachedQuery.instance.storage?.get(
"trakt_integration_cache",
);
final result =
await CachedQuery.instance.storage?.get("trakt_integration_cache");
AppEngine.engine.pb.authStore.onChange.listen((item) {
if (!AppEngine.engine.pb.authStore.isValid) {
@ -103,8 +103,7 @@ class TraktService {
final connection = ConnectionResponse(
connection: Connection.fromRecord(model_),
connectionTypeRecord: ConnectionTypeRecord.fromRecord(
model_.get<RecordModel>("expand.type"),
),
model_.get<RecordModel>("expand.type")),
);
stremioService = BaseConnectionService.connectionById(connection);
@ -139,29 +138,6 @@ 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) {
_logger.severe('GET rate limit exceeded');
throw Exception('GET rate limit exceeded');
}
_getRequestCount++;
} else if (method == 'POST' || method == 'PUT' || method == 'DELETE') {
if (_postRequestCount >= _authedPostLimit) {
_logger.severe('POST/PUT/DELETE rate limit exceeded');
throw Exception('POST/PUT/DELETE rate limit exceeded');
}
_postRequestCount++;
}
}
void _startCacheRevalidation() {
_logger.info('Starting cache revalidation timer');
_cacheRevalidationTimer = Timer.periodic(
@ -191,8 +167,6 @@ class TraktService {
return _cache[url];
}
await _checkRateLimit('GET');
_logger.info('Making GET request to $url');
final response = await http.get(Uri.parse(url), headers: headers);
@ -218,7 +192,7 @@ class TraktService {
'year': meta.year,
'ids': {
'imdb': meta.imdbId ?? meta.id,
...(meta.externalIds ?? {}),
...(meta.episodeExternalIds ?? {}),
},
},
};
@ -229,30 +203,31 @@ class TraktService {
"year": meta.year,
"ids": {
"imdb": meta.imdbId ?? meta.id,
...(meta.externalIds ?? {}),
...(meta.episodeExternalIds ?? {}),
}
},
"episode": {
"season": meta.nextSeason,
"number": meta.nextEpisode,
"season": meta.currentVideo?.season ?? meta.nextSeason,
"number": meta.currentVideo?.number ?? meta.nextEpisode,
},
};
}
}
Future<List<LibraryItem>> getUpNextSeries() async {
Stream<List<LibraryItem>> getUpNextSeries(
{int page = 1, int itemsPerPage = 5}) async* {
await initStremioService();
if (!isEnabled()) {
_logger.info('Trakt integration is not enabled');
return [];
yield [];
return;
}
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 progressFutures = watchedShows.map((show) async {
final showId = show['show']['ids']['trakt'];
@ -271,6 +246,7 @@ class TraktService {
type: "series",
id: imdb,
externalIds: show['show']['ids'],
episodeExternalIds: nextEpisode['ids'],
),
);
@ -280,6 +256,8 @@ class TraktService {
nextEpisode: nextEpisode['number'],
nextSeason: nextEpisode['season'],
nextEpisodeTitle: nextEpisode['title'],
externalIds: show['show']['ids'],
episodeExternalIds: nextEpisode['ids'],
);
}
} catch (e) {
@ -291,15 +269,31 @@ class TraktService {
}).toList();
final results = await Future.wait(progressFutures);
final validResults = results.whereType<Meta>().toList();
return results.whereType<Meta>().toList();
// Pagination logic
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= validResults.length) {
yield [];
return;
}
final paginatedResults = validResults.sublist(
startIndex,
endIndex > validResults.length ? validResults.length : endIndex,
);
yield paginatedResults;
} catch (e, stack) {
_logger.severe('Error fetching up next episodes: $e', stack);
return [];
yield [];
}
}
Future<List<LibraryItem>> getContinueWatching() async {
Future<List<LibraryItem>> getContinueWatching(
{int page = 1, int itemsPerPage = 5}) async {
await initStremioService();
if (!isEnabled()) {
@ -327,6 +321,8 @@ class TraktService {
nextSeason: movie['episode']['season'],
nextEpisode: movie['episode']['number'],
nextEpisodeTitle: movie['episode']['title'],
externalIds: movie['show']['ids'],
episodeExternalIds: movie['episode']['ids'],
);
}
@ -347,71 +343,28 @@ class TraktService {
.toList(),
);
return result.map((res) {
Meta returnValue = res as Meta;
// Pagination logic
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (progress.containsKey(res.id)) {
returnValue = res.copyWith(
progress: progress[res.id],
if (startIndex >= result.length) {
return [];
}
return result.sublist(
startIndex,
endIndex > result.length ? result.length : endIndex,
);
}
if (res.type == "series") {
return returnValue.copyWith();
}
return returnValue;
}).toList();
} catch (e, stack) {
_logger.severe('Error fetching continue watching: $e', stack);
return [];
}
}
Future<void> _retryPostRequest(
String cacheKey,
String url,
Map<String, dynamic> body, {
int retryCount = 2,
Future<List<LibraryItem>> getUpcomingSchedule({
int page = 1,
int itemsPerPage = 5,
}) async {
for (int i = 0; i < retryCount; i++) {
try {
await _checkRateLimit('POST');
_logger.info('Making POST request to $url');
final response = await http.post(
Uri.parse(url),
headers: headers,
body: json.encode(body),
);
if (response.statusCode == 201) {
_logger.info('POST request successful');
return;
} else if (response.statusCode == 429) {
_logger.warning('Rate limit hit, retrying...');
await Future.delayed(Duration(seconds: 10));
continue;
} else {
_logger.severe('Failed to make POST request: ${response.statusCode}');
throw Exception(
'Failed to make POST request: ${response.statusCode}');
}
} catch (e) {
if (i == retryCount - 1) {
_logger
.severe('Failed to make POST request after $retryCount attempts');
if (_cache.containsKey(cacheKey)) {
_logger.info('Returning cached data');
return _cache[cacheKey];
}
rethrow;
}
}
}
}
Future<List<LibraryItem>> getUpcomingSchedule() async {
await initStremioService();
if (!isEnabled()) {
@ -443,14 +396,25 @@ class TraktService {
.toList(),
);
return result;
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= result.length) {
return [];
}
return result.sublist(
startIndex,
endIndex > result.length ? result.length : endIndex,
);
} catch (e, stack) {
_logger.severe('Error fetching upcoming schedule: $e', stack);
return [];
}
}
Future<List<LibraryItem>> getWatchlist() async {
Future<List<LibraryItem>> getWatchlist(
{int page = 1, int itemsPerPage = 5}) async {
await initStremioService();
if (!isEnabled()) {
@ -461,6 +425,7 @@ class TraktService {
try {
_logger.info('Fetching watchlist');
final watchlistItems = await _makeRequest('$_baseUrl/sync/watchlist');
_logger.info('Got watchlist');
final result = await stremioService!.getBulkItem(
watchlistItems
@ -489,14 +454,25 @@ class TraktService {
.toList(),
);
return result;
final startIndex = (page - 1) * itemsPerPage;
final endIndex = startIndex + itemsPerPage;
if (startIndex >= result.length) {
return [];
}
return result.sublist(
startIndex,
endIndex > result.length ? result.length : endIndex,
);
} catch (e, stack) {
_logger.severe('Error fetching watchlist: $e', stack);
return [];
}
}
Future<List<LibraryItem>> getShowRecommendations() async {
Future<List<LibraryItem>> getShowRecommendations(
{int page = 1, int itemsPerPage = 5}) async {
await initStremioService();
if (!isEnabled()) {
@ -527,14 +503,28 @@ class TraktService {
.toList(),
));
return result;
// 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,
);
} catch (e, stack) {
_logger.severe('Error fetching show recommendations: $e', stack);
return [];
}
}
Future<List<LibraryItem>> getMovieRecommendations() async {
Future<List<LibraryItem>> getMovieRecommendations({
int page = 1,
int itemsPerPage = 5,
}) async {
await initStremioService();
if (!isEnabled()) {
@ -544,8 +534,9 @@ class TraktService {
try {
_logger.info('Fetching movie recommendations');
final recommendedMovies =
await _makeRequest('$_baseUrl/recommendations/movies');
final recommendedMovies = await _makeRequest(
'$_baseUrl/recommendations/movies',
);
final result = await stremioService!.getBulkItem(
recommendedMovies
@ -565,7 +556,18 @@ class TraktService {
.toList(),
);
return result;
// 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,
);
} catch (e, stack) {
_logger.severe('Error fetching movie recommendations: $e', stack);
return [];
@ -624,10 +626,11 @@ class TraktService {
return;
}
await _checkRateLimit('POST');
try {
_logger.info('Starting scrobbling for ${meta.type} with ID: ${meta.id}');
print(_buildObjectForMeta(meta));
final response = await http.post(
Uri.parse('$_baseUrl/scrobble/start'),
headers: headers,
@ -660,6 +663,8 @@ class TraktService {
return;
}
print(_buildObjectForMeta(meta));
final cacheKey = '${meta.id}_pauseScrobbling';
_activeScrobbleRequests[cacheKey]?.completeError('Cancelled');
@ -683,6 +688,50 @@ class TraktService {
}
}
Future<void> _retryPostRequest(
String cacheKey,
String url,
Map<String, dynamic> body, {
int retryCount = 2,
}) async {
for (int i = 0; i < retryCount; i++) {
try {
_logger.info('Making POST request to $url');
final response = await http.post(
Uri.parse(url),
headers: headers,
body: json.encode(body),
);
if (response.statusCode == 201) {
_logger.info('POST request successful');
return;
} else if (response.statusCode == 429) {
_logger.warning('Rate limit hit, retrying...');
await Future.delayed(
const Duration(seconds: 10),
);
continue;
} else {
_logger.severe('Failed to make POST request: ${response.statusCode}');
throw Exception(
'Failed to make POST request: ${response.statusCode}',
);
}
} catch (e) {
if (i == retryCount - 1) {
_logger
.severe('Failed to make POST request after $retryCount attempts');
if (_cache.containsKey(cacheKey)) {
_logger.info('Returning cached data');
return _cache[cacheKey];
}
rethrow;
}
}
}
}
Future<void> stopScrobbling({
required Meta meta,
required double progress,
@ -699,6 +748,7 @@ class TraktService {
try {
_logger.info('Stopping scrobbling for ${meta.type} with ID: ${meta.id}');
_logger.info(_buildObjectForMeta(meta));
await _retryPostRequest(
cacheKey,
'$_baseUrl/scrobble/stop',
@ -710,6 +760,11 @@ class TraktService {
_cache.remove('$_baseUrl/sync/watched/shows');
_cache.remove('$_baseUrl/sync/playback');
refetchKey.add([
"continue_watching",
if (meta.type == "series") "up_next_series",
]);
} catch (e, stack) {
_logger.severe('Error stopping scrobbling: $e', stack);
rethrow;
@ -792,19 +847,3 @@ class TraktService {
return [];
}
}
class TraktProgress {
final String id;
final int? episode;
final int? season;
final double progress;
TraktProgress({
required this.id,
this.episode,
this.season,
required this.progress,
});
}
extension StaticInstance on CachedStorage {}

View file

@ -1,65 +0,0 @@
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");
}
}
}

View file

@ -0,0 +1,13 @@
class TraktProgress {
final String id;
final int? episode;
final int? season;
final double progress;
TraktProgress({
required this.id,
this.episode,
this.season,
required this.progress,
});
}

View file

@ -17,6 +17,7 @@ import 'package:media_kit/media_kit.dart';
import 'package:path/path.dart' as path;
import 'package:window_manager/window_manager.dart';
import 'data/global_logs.dart';
import 'features/doc_viewer/container/iframe.dart';
import 'features/downloads/service/service.dart';
import 'features/watch_history/service/zeee_watch_history.dart';
@ -29,15 +30,34 @@ void main() async {
print("Unable");
}
Logger.root.level = Level.ALL;
Logger.root.level = Level.INFO;
Logger.root.onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}');
final logs =
'${record.level.name.padRight(10)}${record.loggerName.padRight(30)}${record.time.hour}:${record.time.minute}:${record.time.second}:${record.time.millisecond}: ${record.message}';
print(logs);
globalLogs.add(logs);
if (globalLogs.length > 1000) {
globalLogs.removeAt(0);
}
if (record.error != null) {
print('Error: ${record.error}');
final error = 'Error: ${record.time} ${record.error}';
print(error);
globalLogs.add(error);
if (globalLogs.length > 1000) {
globalLogs.removeAt(0);
}
}
if (record.stackTrace != null) {
print('StackTrace: ${record.stackTrace}');
final error = 'StackTrace: ${record.stackTrace}';
print(error);
globalLogs.add(error);
if (globalLogs.length > 1000) {
globalLogs.removeAt(0);
}
}
});
@ -113,7 +133,6 @@ class _MadariAppState extends State<MadariApp> {
void _initializeFileHandling() {
platform.setMethodCallHandler((call) async {
if (call.method == "openFile") {
// Handle the new file data structure
_openedFileData = call.arguments as Map<String, dynamic>?;
if (_openedFileData != null) {

View file

@ -9,6 +9,7 @@ import 'package:madari_client/pages/sign_in.page.dart';
import '../features/settings/screen/account_screen.dart';
import '../features/settings/screen/connection_screen.dart';
import '../features/settings/screen/logs_screen.dart';
import '../features/settings/screen/playback_settings_screen.dart';
import '../features/settings/screen/profile_button.dart';
@ -76,6 +77,35 @@ class MoreContainer extends StatelessWidget {
),
),
),
_buildListHeader('Debug'),
ListTile(
leading: const Icon(Icons.text_snippet),
title: const Text("Show logs"),
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return const LogsPage();
},
),
);
},
),
ListTile(
leading: const Icon(Icons.clear),
title: const Text("Clear Local Watch History"),
onTap: () async {
await ZeeeWatchHistoryStatic.service?.clear();
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Removed local watch history"),
),
);
}
},
),
ListTile(
leading: const Icon(Icons.clear_all),
title: const Text("Clear Cache"),

View file

@ -67,10 +67,16 @@ class _StremioItemPageState extends State<StremioItemPage> {
super.initState();
if (widget.meta?.progress != null || widget.meta?.nextEpisode != null) {
Future.delayed(
const Duration(milliseconds: 500),
() {
openVideo();
}
}
openVideo() async {
if (widget.meta != null && widget.service != null) {
await Future.delayed(
const Duration(milliseconds: 500),
);
if (mounted) {
final season = widget.meta?.nextSeason == null
? ""
@ -104,8 +110,12 @@ class _StremioItemPageState extends State<StremioItemPage> {
: null,
service: widget.service!,
id: widget.meta as LibraryItem,
season: widget.meta?.nextSeason?.toString(),
episode: widget.meta?.nextEpisode?.toString(),
season: (widget.meta?.currentVideo?.season ??
widget.meta?.nextSeason)
?.toString(),
episode: (widget.meta?.currentVideo?.episode ??
widget.meta?.nextEpisode)
?.toString(),
shouldPop: false,
),
),
@ -115,9 +125,6 @@ class _StremioItemPageState extends State<StremioItemPage> {
);
}
}
},
);
}
}
@override