fix: small issues

This commit is contained in:
omkar 2025-02-01 13:49:17 +05:30
parent 1910432364
commit 309be2be2c
5 changed files with 352 additions and 235 deletions

View file

@ -348,7 +348,20 @@ class Meta {
@JsonKey(name: "moviedb_id") @JsonKey(name: "moviedb_id")
final int? moviedbId; final int? moviedbId;
@JsonKey(name: "runtime") @JsonKey(name: "runtime")
final String? runtime; final String? runtime_;
String? get runtime {
try {
if (runtime_ == null) {
return runtime_;
}
return formatTimeFromMinutes(runtime_!);
} catch (e) {
return runtime_;
}
}
@JsonKey(name: "trailers") @JsonKey(name: "trailers")
final List<Trailer>? trailers; final List<Trailer>? trailers;
@JsonKey(name: "popularity") @JsonKey(name: "popularity")
@ -432,7 +445,7 @@ class Meta {
this.logo, this.logo,
this.awards, this.awards,
this.moviedbId, this.moviedbId,
this.runtime, this.runtime_,
this.trailers, this.trailers,
this.popularity, this.popularity,
required this.id, required this.id,
@ -520,7 +533,7 @@ class Meta {
logo: logo ?? this.logo, logo: logo ?? this.logo,
awards: awards ?? this.awards, awards: awards ?? this.awards,
moviedbId: moviedbId ?? this.moviedbId, moviedbId: moviedbId ?? this.moviedbId,
runtime: runtime ?? this.runtime, runtime_: runtime ?? this.runtime,
trailers: trailers ?? this.trailers, trailers: trailers ?? this.trailers,
popularity: popularity ?? this.popularity, popularity: popularity ?? this.popularity,
id: id ?? this.id, id: id ?? this.id,
@ -898,6 +911,14 @@ class VideoStream {
_$VideoStreamFromJson(json); _$VideoStreamFromJson(json);
Map<String, dynamic> toJson() => _$VideoStreamToJson(this); Map<String, dynamic> toJson() => _$VideoStreamToJson(this);
@override
int get hashCode => "$url$name$title$description".length;
@override
bool operator ==(Object other) {
return super.hashCode == other.hashCode;
}
} }
class StreamInfo { class StreamInfo {
@ -1002,8 +1023,10 @@ class StreamParser {
final unratedMatch = _unratedRegex.hasMatch(name); final unratedMatch = _unratedRegex.hasMatch(name);
final sizeMatch = _sizeRegex.firstMatch(name); final sizeMatch = _sizeRegex.firstMatch(name);
final res = resMatch?.group(1)?.toUpperCase();
return StreamInfo( return StreamInfo(
resolution: resMatch?.group(1)?.toUpperCase(), resolution: res == "2160P" ? "4K" : res,
quality: qualMatch?.group(1)?.toUpperCase(), quality: qualMatch?.group(1)?.toUpperCase(),
codec: codecMatch?.group(1)?.toUpperCase(), codec: codecMatch?.group(1)?.toUpperCase(),
audio: audioMatch?.group(1)?.toUpperCase(), audio: audioMatch?.group(1)?.toUpperCase(),
@ -1014,3 +1037,28 @@ class StreamParser {
); );
} }
} }
String formatTimeFromMinutes(String minutesInput) {
int? minutes = int.tryParse(minutesInput);
if (minutes == null) {
return minutesInput;
}
int hours = minutes ~/ 60;
int remainingMinutes = minutes % 60;
if (hours == 0) {
return '$remainingMinutes minutes';
}
if (remainingMinutes == 0) {
return '$hours hours';
}
return '$hours hours $remainingMinutes minutes';
// Format 2 (Alternative): Compact format
// return '${hours}h ${remainingMinutes}m';
// Format 3 (Alternative): Digital clock format
// return '${hours.toString().padLeft(2, '0')}:${remainingMinutes.toString().padLeft(2, '0')}';
}

View file

@ -6,6 +6,7 @@ import 'package:logging/logging.dart';
import 'package:madari_client/data/db.dart'; import 'package:madari_client/data/db.dart';
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart'; import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
import 'package:madari_client/utils/array-extension.dart'; import 'package:madari_client/utils/array-extension.dart';
import 'package:pocketbase/pocketbase.dart';
import '../../pocketbase/service/pocketbase.service.dart'; import '../../pocketbase/service/pocketbase.service.dart';
import '../../widgetter/plugins/stremio/models/cast_info.dart'; import '../../widgetter/plugins/stremio/models/cast_info.dart';
@ -208,8 +209,22 @@ class StremioAddonService {
await getInstalledAddons().refetch(); await getInstalledAddons().refetch();
} catch (e, stack) { } catch (e, stack) {
print(e); _logger.warning("Error to save addon", e, stack);
print(stack);
if (e is ClientException) {
try {
if (e.response.values.first["url"]["code"] ==
"validation_not_unique") {
throw Exception(
"Addon already installed make sure you don't have this in disabled addons.",
);
}
} catch (ex, stack) {
_logger.warning("Error find error", ex, stack);
throw Exception(e.response.values);
}
}
throw Exception('Failed to save addon: $e'); throw Exception('Failed to save addon: $e');
} }
} }

View file

@ -1,6 +1,7 @@
import 'package:cached_query_flutter/cached_query_flutter.dart'; import 'package:cached_query_flutter/cached_query_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:madari_client/features/streamio_addons/extension/query_extension.dart'; import 'package:madari_client/features/streamio_addons/extension/query_extension.dart';
import 'package:pocketbase/pocketbase.dart';
import '../models/stremio_base_types.dart'; import '../models/stremio_base_types.dart';
import '../service/stremio_addon_service.dart'; import '../service/stremio_addon_service.dart';
@ -65,7 +66,7 @@ class _AddAddonSheetState extends State<AddAddonSheet> {
final manifest = await query.queryFn(); final manifest = await query.queryFn();
if (!mounted) return; if (!mounted) return;
_installAddon(manifest); await _installAddon(manifest);
} }
Future<void> _installAddon(StremioManifest manifest) async { Future<void> _installAddon(StremioManifest manifest) async {
@ -79,9 +80,17 @@ class _AddAddonSheetState extends State<AddAddonSheet> {
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
} catch (e) { } catch (e) {
print(e);
if (mounted) { if (mounted) {
if (e is ClientException) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to install addon: ${e.response}')),
);
return;
}
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to install addon: $e')), SnackBar(content: Text('Failed to install addon: ${e}')),
); );
} }
} finally { } finally {
@ -144,7 +153,9 @@ class _AddAddonSheetState extends State<AddAddonSheet> {
FilledButton.icon( FilledButton.icon(
onPressed: () => _validateManifest(), onPressed: () => _validateManifest(),
icon: const Icon(Icons.check), icon: const Icon(Icons.check),
label: const Text('Validate'), label: _isInstalling
? const Text("Installing")
: const Text('Install'),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
const Text( const Text(

View file

@ -35,6 +35,7 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
Set<String> _selectedAudios = {}; Set<String> _selectedAudios = {};
Set<String> _selectedSizes = {}; Set<String> _selectedSizes = {};
final Set<String> _selectedAddons = {}; final Set<String> _selectedAddons = {};
final Map<String, List<StreamWithAddon>> streamsByAddon = {};
Set<String> _resolutions = {}; Set<String> _resolutions = {};
Set<String> _qualities = {}; Set<String> _qualities = {};
@ -53,7 +54,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
_logger.info('Loading streams for ${widget.meta.id}'); _logger.info('Loading streams for ${widget.meta.id}');
final addons = service.getInstalledAddons(); final addons = service.getInstalledAddons();
final result = await addons.queryFn(); final result = await addons.queryFn();
final count = result final count = result
@ -63,7 +63,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
return true; return true;
} }
} }
return false; return false;
}) })
.toList() .toList()
@ -86,7 +85,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
callback: (items, addonName, error) { callback: (items, addonName, error) {
setState(() { setState(() {
left -= 1; left -= 1;
if (left <= 0) { if (left <= 0) {
_isLoading = false; _isLoading = false;
} }
@ -98,7 +96,7 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
return; return;
} }
if (items != null) { if (items != null && addonName != null) {
final Set<String> resSet = {}; final Set<String> resSet = {};
final Set<String> qualSet = {}; final Set<String> qualSet = {};
final Set<String> codecSet = {}; final Set<String> codecSet = {};
@ -106,12 +104,10 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
final Set<String> sizeSet = {}; final Set<String> sizeSet = {};
final streamsWithAddon = items final streamsWithAddon = items
.map( .map((stream) => StreamWithAddon(
(stream) => StreamWithAddon( stream: stream,
stream: stream, addonName: addonName,
addonName: addonName, ))
),
)
.toList(); .toList();
for (var streamData in streamsWithAddon) { for (var streamData in streamsWithAddon) {
@ -135,14 +131,13 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
if (mounted) { if (mounted) {
setState(() { setState(() {
streams.addAll(streamsWithAddon); streamsByAddon[addonName] = streamsWithAddon;
if (addonName != null) _addons.add(addonName); _addons.add(addonName);
_resolutions = resSet; _resolutions.addAll(resSet);
_qualities = qualSet; _qualities.addAll(qualSet);
_codecs = codecSet; _codecs.addAll(codecSet);
_audios = audioSet; _audios.addAll(audioSet);
_sizes = sizeSet; _sizes.addAll(sizeSet);
_isLoading = false;
}); });
} }
} }
@ -150,26 +145,20 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
); );
} }
Color _getQualityColor(String resolution) {
final res = resolution.toUpperCase();
if (res.contains('2160P') || res.contains('4K') || res.contains('UHD')) {
return Colors.amberAccent;
} else if (res.contains('1080P')) {
return Colors.blue;
} else if (res.contains('720P')) {
return Colors.green;
}
return Colors.grey;
}
List<StreamWithAddon> _getFilteredStreams() { List<StreamWithAddon> _getFilteredStreams() {
return streams.where((streamData) { List<StreamWithAddon> allStreams = [];
if (streamData.stream.name == null) return false; if (_selectedAddons.isEmpty) {
streamsByAddon.values.forEach(allStreams.addAll);
if (_selectedAddons.isNotEmpty && } else {
!_selectedAddons.contains(streamData.addonName)) { for (var addon in _selectedAddons) {
return false; if (streamsByAddon.containsKey(addon)) {
allStreams.addAll(streamsByAddon[addon]!);
}
} }
}
return allStreams.where((streamData) {
if (streamData.stream.name == null) return false;
try { try {
final info = StreamParser.parseStreamName(streamData.stream.name!); final info = StreamParser.parseStreamName(streamData.stream.name!);
@ -195,7 +184,9 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
matchesSize; matchesSize;
} catch (e) { } catch (e) {
_logger.warning( _logger.warning(
'Error parsing stream info: ${streamData.stream.name}', e); 'Error parsing stream info: ${streamData.stream.name}',
e,
);
return false; return false;
} }
}).toList(); }).toList();
@ -263,154 +254,6 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
); );
} }
Widget _buildStreamCard(StreamWithAddon streamData, ThemeData theme) {
final stream = streamData.stream;
final info = StreamParser.parseStreamName(stream.name ?? '');
return InkWell(
onTap: stream.url != null
? () async {
if (stream.url != null) {
final settings =
await PlaybackSettingsService.instance.getSettings();
if (settings.externalPlayer) {
await ExternalPlayerService.openInExternalPlayer(
videoUrl: stream.url!,
playerPackage: settings.selectedExternalPlayer,
);
return;
}
String url =
'/player/${widget.meta.type}/${widget.meta.id}/${Uri.encodeQueryComponent(stream.url!)}?';
final List<String> query = [];
if (widget.meta.selectedVideoIndex != null) {
query.add("index=${widget.meta.selectedVideoIndex}");
}
if (stream.behaviorHints?["bingeGroup"] != null) {
query.add(
"binge-group=${Uri.encodeQueryComponent(stream.behaviorHints?["bingeGroup"])}",
);
}
if (mounted) {
context.push(
url + query.join("&"),
extra: {
"meta": widget.meta,
},
);
}
}
}
: null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (info.resolution != null)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: StreamTag(
text: info.resolution!,
color: _getQualityColor(info.resolution!),
outlined: true,
),
),
Text(
(stream.name ?? 'Unknown Title') +
(stream.url != null ? "" : " (Not supported)"),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
stream.title ?? 'Unknown Title',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
if (stream.description != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
stream.description!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
if (streamData.addonName != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'From: ${streamData.addonName}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
),
],
),
if (info.quality != null &&
info.codec != null &&
info.audio != null &&
info.size != null &&
info.unrated)
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (info.quality != null)
StreamTag(
text: info.quality!,
color: theme.colorScheme.secondary,
),
if (info.codec != null)
StreamTag(
text: info.codec!,
color: theme.colorScheme.tertiary,
),
if (info.audio != null)
StreamTag(
text: info.audio!,
color: theme.colorScheme.primary,
),
if (info.size != null)
StreamTag(
text: StreamParser.getSizeCategory(info.size),
color: theme.colorScheme.secondary,
),
if (info.unrated)
StreamTag(
text: 'UNRATED',
color: theme.colorScheme.error,
),
],
),
],
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading) { if (_isLoading) {
@ -490,18 +333,13 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
value: addon, value: addon,
child: Row( child: Row(
children: [ children: [
Checkbox( Icon(
value: _selectedAddons.contains(addon), _selectedAddons.contains(addon)
onChanged: (bool? value) { ? Icons.check_circle
setState(() { : Icons.circle_outlined,
if (value == true) { ),
_selectedAddons.add(addon); const SizedBox(
} else { width: 6,
_selectedAddons.remove(addon);
}
});
Navigator.pop(context);
},
), ),
Text(addon), Text(addon),
], ],
@ -538,11 +376,16 @@ class _StreamioStreamListState extends State<StreamioStreamList> {
) )
: ListView.separated( : ListView.separated(
itemCount: filteredStreams.length, itemCount: filteredStreams.length,
separatorBuilder: (context, index) => separatorBuilder: (context, index) => const Divider(
const Divider(height: 1), height: 1,
),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final streamData = filteredStreams[index]; final streamData = filteredStreams[index];
return _buildStreamCard(streamData, theme);
return StreamCard(
streamWithAddon: streamData,
meta: widget.meta,
);
}, },
), ),
), ),
@ -557,6 +400,13 @@ class StreamWithAddon {
final String? addonName; final String? addonName;
StreamWithAddon({required this.stream, this.addonName}); StreamWithAddon({required this.stream, this.addonName});
StreamWithAddon copy() {
return StreamWithAddon(
stream: stream.copyWith(),
addonName: addonName,
);
}
} }
class StreamTag extends StatelessWidget { class StreamTag extends StatelessWidget {
@ -597,3 +447,181 @@ class StreamTag extends StatelessWidget {
); );
} }
} }
class StreamCard extends StatelessWidget {
final StreamWithAddon streamWithAddon;
final Meta meta;
const StreamCard({
super.key,
required this.meta,
required this.streamWithAddon,
});
VideoStream get stream {
return streamWithAddon.stream.copyWith();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final info = StreamParser.parseStreamName(stream.name ?? '');
return InkWell(
onTap: stream.url != null
? () async {
if (stream.url != null) {
final settings =
await PlaybackSettingsService.instance.getSettings();
if (settings.externalPlayer) {
await ExternalPlayerService.openInExternalPlayer(
videoUrl: stream.url!,
playerPackage: settings.selectedExternalPlayer,
);
return;
}
String url =
'/player/${meta.type}/${meta.id}/${Uri.encodeQueryComponent(stream.url!)}?';
final List<String> query = [];
if (meta.selectedVideoIndex != null) {
query.add("index=${meta.selectedVideoIndex}");
}
if (stream.behaviorHints?["bingeGroup"] != null) {
query.add(
"binge-group=${Uri.encodeQueryComponent(stream.behaviorHints?["bingeGroup"])}",
);
}
if (context.mounted) {
context.push(
url + query.join("&"),
extra: {
"meta": meta,
},
);
}
}
}
: null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (info.resolution != null)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: StreamTag(
text: info.resolution!,
color: _getQualityColor(info.resolution!),
outlined: true,
),
),
Text(
(stream.name ?? 'Unknown Title') +
(stream.url != null ? "" : " (Not supported)"),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
if (stream.title != null)
Text(
stream.title!,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
if (stream.description != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
stream.description!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
if (streamWithAddon.addonName != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'From: ${streamWithAddon.addonName}',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
),
],
),
if (info.quality != null &&
info.codec != null &&
info.audio != null &&
info.size != null &&
info.unrated)
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
if (info.quality != null)
StreamTag(
text: info.quality!,
color: theme.colorScheme.secondary,
),
if (info.codec != null)
StreamTag(
text: info.codec!,
color: theme.colorScheme.tertiary,
),
if (info.audio != null)
StreamTag(
text: info.audio!,
color: theme.colorScheme.primary,
),
if (info.size != null)
StreamTag(
text: StreamParser.getSizeCategory(info.size),
color: theme.colorScheme.secondary,
),
if (info.unrated)
StreamTag(
text: 'UNRATED',
color: theme.colorScheme.error,
),
],
),
],
),
),
);
}
Color _getQualityColor(String resolution) {
final res = resolution.toUpperCase();
if (res.contains('2160P') || res.contains('4K') || res.contains('UHD')) {
return Colors.amberAccent;
} else if (res.contains('1080P')) {
return Colors.blue;
} else if (res.contains('720P')) {
return Colors.green;
}
return Colors.grey;
}
}

View file

@ -20,7 +20,11 @@ Future<void> openVideoStream(BuildContext context, Meta meta) async {
builder: (context) { builder: (context) {
return Scaffold( return Scaffold(
body: StreamioStreamList( body: StreamioStreamList(
meta: meta, meta: meta.type == "series"
? meta.copyWith(
selectedVideoIndex: meta.selectedVideoIndex ?? 0,
)
: meta,
), ),
); );
}, },
@ -115,19 +119,21 @@ class StreamioHeroSection extends StatelessWidget {
), ),
), ),
), ),
IconButton.filled( if (meta.type != "series")
onPressed: () { IconButton.filled(
_logger.info('Play button pressed for ${meta.name}'); onPressed: () {
_logger.info('Play button pressed for ${meta.name}');
openVideoStream(context, meta); openVideoStream(context, meta);
}, },
icon: const Icon(Icons.play_arrow, size: 32), icon: const Icon(Icons.play_arrow, size: 32),
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor:
foregroundColor: Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.onPrimary, foregroundColor:
Theme.of(context).colorScheme.onPrimary,
),
), ),
),
], ],
), ),
), ),
@ -149,11 +155,18 @@ class StreamioHeroSection extends StatelessWidget {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'${meta.year ?? ''}${meta.runtime ?? ''}${meta.genres?.join(', ') ?? ''}', '${meta.year ?? ''}${meta.genres?.join(', ') ?? ''}',
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.white.withValues(alpha: 0.7), color: Colors.white.withValues(alpha: 0.7),
), ),
), ),
if (meta.runtime != null)
Text(
meta.runtime!,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Colors.white.withValues(alpha: 0.7),
),
),
if (meta.imdbRating.isNotEmpty && if (meta.imdbRating.isNotEmpty &&
meta.imdbRating.toString() != "null") ...[ meta.imdbRating.toString() != "null") ...[
const SizedBox(height: 16), const SizedBox(height: 16),
@ -171,19 +184,21 @@ class StreamioHeroSection extends StatelessWidget {
], ],
), ),
], ],
const SizedBox( if (meta.type != "series") ...[
height: 12, const SizedBox(
), height: 12,
OutlinedButton.icon( ),
onPressed: () { OutlinedButton.icon(
openVideoStream( onPressed: () {
context, openVideoStream(
meta, context,
); meta,
}, );
icon: const Icon(Icons.play_arrow), },
label: const Text("Play"), icon: const Icon(Icons.play_arrow),
), label: const Text("Play"),
),
],
const SizedBox( const SizedBox(
height: 12, height: 12,
), ),