mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-03-11 17:15:39 +00:00
fix: fixed trakt integration
This commit is contained in:
parent
356f197a4c
commit
0e97a8ae61
17 changed files with 913 additions and 479 deletions
1
lib/data/global_logs.dart
Normal file
1
lib/data/global_logs.dart
Normal file
|
|
@ -0,0 +1 @@
|
|||
final List<String> globalLogs = [];
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
194
lib/features/settings/screen/logs_screen.dart
Normal file
194
lib/features/settings/screen/logs_screen.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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!,
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
13
lib/features/trakt/types/common.dart
Normal file
13
lib/features/trakt/types/common.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue