mirror of
https://github.com/madari-media/madari-oss.git
synced 2026-04-25 06:33:08 +00:00
Some checks failed
Build and Deploy / build_windows (push) Has been cancelled
Build and Deploy / build_android (push) Has been cancelled
Build and Deploy / build_android_tv (push) Has been cancelled
Build and Deploy / build_ipa (push) Has been cancelled
Build and Deploy / build_linux (push) Has been cancelled
Build and Deploy / build_macos (push) Has been cancelled
349 lines
8.7 KiB
Dart
349 lines
8.7 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:madari_client/features/connections/service/base_connection_service.dart';
|
|
import 'package:madari_client/features/watch_history/service/base_watch_history.dart';
|
|
import 'package:media_kit/media_kit.dart';
|
|
import 'package:media_kit_video/media_kit_video.dart';
|
|
|
|
import '../../../utils/load_language.dart';
|
|
import '../../connections/types/stremio/stremio_base.types.dart' as types;
|
|
import '../../connections/widget/stremio/stremio_season_selector.dart';
|
|
import '../../trakt/service/trakt.service.dart';
|
|
import '../../trakt/types/common.dart';
|
|
import '../../watch_history/service/zeee_watch_history.dart';
|
|
import '../types/doc_source.dart';
|
|
import 'video_viewer/video_viewer_ui.dart';
|
|
|
|
class VideoViewer extends StatefulWidget {
|
|
final DocSource source;
|
|
final LibraryItem? meta;
|
|
final BaseConnectionService? service;
|
|
final String? currentSeason;
|
|
final String? library;
|
|
|
|
const VideoViewer({
|
|
super.key,
|
|
required this.source,
|
|
this.meta,
|
|
this.service,
|
|
this.currentSeason,
|
|
this.library,
|
|
});
|
|
|
|
@override
|
|
State<VideoViewer> createState() => _VideoViewerState();
|
|
}
|
|
|
|
class _VideoViewerState extends State<VideoViewer> {
|
|
final zeeeWatchHistory = ZeeeWatchHistoryStatic.service;
|
|
Timer? _timer;
|
|
late final Player player = Player(
|
|
configuration: const PlayerConfiguration(
|
|
title: "Madari",
|
|
),
|
|
);
|
|
final Logger _logger = Logger('VideoPlayer');
|
|
|
|
double get currentProgressInPercentage {
|
|
final duration = player.state.duration.inSeconds;
|
|
final position = player.state.position.inSeconds;
|
|
return duration > 0 ? (position / duration * 100) : 0;
|
|
}
|
|
|
|
Future<List<TraktProgress>>? traktProgress;
|
|
|
|
Future<void> saveWatchHistory() async {
|
|
final duration = player.state.duration.inSeconds;
|
|
|
|
if (duration <= 30) {
|
|
_logger.info('Video is too short to track.');
|
|
return;
|
|
}
|
|
|
|
final position = player.state.position.inSeconds;
|
|
final progress = duration > 0 ? (position / duration * 100).round() : 0;
|
|
|
|
if (progress == 0) {
|
|
_logger.info('No progress to save.');
|
|
return;
|
|
}
|
|
|
|
if (widget.meta is types.Meta && TraktService.instance != null) {
|
|
try {
|
|
if (player.state.playing) {
|
|
_logger.info('Starting scrobbling...');
|
|
await TraktService.instance!.startScrobbling(
|
|
meta: widget.meta as types.Meta,
|
|
progress: currentProgressInPercentage,
|
|
);
|
|
} else {
|
|
_logger.info('Stopping scrobbling...');
|
|
await TraktService.instance!.stopScrobbling(
|
|
meta: widget.meta as types.Meta,
|
|
progress: currentProgressInPercentage,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
_logger.severe('Error during scrobbling: $e');
|
|
TraktService.instance!.debugLogs.add(e.toString());
|
|
}
|
|
} else {
|
|
_logger.warning('Meta is not valid or TraktService is not initialized.');
|
|
}
|
|
|
|
await zeeeWatchHistory!.saveWatchHistory(
|
|
history: WatchHistory(
|
|
id: _source.id,
|
|
progress: progress,
|
|
duration: duration.toDouble(),
|
|
episode: _source.episode,
|
|
season: _source.season,
|
|
),
|
|
);
|
|
}
|
|
|
|
late final controller = VideoController(
|
|
player,
|
|
configuration: VideoControllerConfiguration(
|
|
enableHardwareAcceleration: !config.softwareAcceleration,
|
|
),
|
|
);
|
|
|
|
late DocSource _source;
|
|
|
|
bool canCallOnce = false;
|
|
|
|
int? traktId;
|
|
|
|
Future<void> setDurationFromTrakt() async {
|
|
if (player.state.duration.inSeconds < 2) {
|
|
return;
|
|
}
|
|
|
|
if (canCallOnce) {
|
|
return;
|
|
}
|
|
|
|
canCallOnce = true;
|
|
|
|
if (!TraktService.isEnabled() || traktProgress == null) {
|
|
player.play();
|
|
return;
|
|
}
|
|
|
|
final progress = await traktProgress;
|
|
|
|
if ((progress ?? []).isEmpty) {
|
|
player.play();
|
|
return;
|
|
}
|
|
|
|
traktId = progress!.first.traktId;
|
|
|
|
final duration = Duration(
|
|
seconds: calculateSecondsFromProgress(
|
|
player.state.duration.inSeconds.toDouble(),
|
|
progress.first.progress,
|
|
),
|
|
);
|
|
|
|
await player.seek(duration);
|
|
await player.play();
|
|
}
|
|
|
|
List<StreamSubscription> listener = [];
|
|
|
|
PlaybackConfig config = getPlaybackConfig();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_source = widget.source;
|
|
|
|
SystemChrome.setEnabledSystemUIMode(
|
|
SystemUiMode.immersiveSticky,
|
|
overlays: [],
|
|
);
|
|
|
|
_duration = player.stream.duration.listen((item) async {
|
|
if (item.inSeconds != 0) {
|
|
await setDurationFromTrakt();
|
|
await saveWatchHistory();
|
|
}
|
|
});
|
|
|
|
loadFile();
|
|
|
|
if (player.platform is NativePlayer && !kIsWeb) {
|
|
Future.microtask(() async {
|
|
await (player.platform as dynamic).setProperty('network-timeout', '60');
|
|
});
|
|
}
|
|
|
|
_timer = Timer.periodic(const Duration(seconds: 30), (timer) {
|
|
saveWatchHistory();
|
|
});
|
|
|
|
_streamListen = player.stream.playing.listen((playing) {
|
|
if (playing) {
|
|
saveWatchHistory();
|
|
}
|
|
});
|
|
|
|
if (widget.meta is types.Meta && TraktService.isEnabled()) {
|
|
traktProgress = TraktService.instance!.getProgress(
|
|
widget.meta as types.Meta,
|
|
);
|
|
}
|
|
}
|
|
|
|
loadFile() async {
|
|
_logger.info('Loading file for source: ${_source.id}');
|
|
|
|
final item = await zeeeWatchHistory!.getItemWatchHistory(
|
|
ids: [
|
|
WatchHistoryGetRequest(
|
|
id: _source.id,
|
|
season: _source.season,
|
|
episode: _source.episode,
|
|
),
|
|
],
|
|
);
|
|
|
|
final duration = Duration(
|
|
seconds: item.isEmpty
|
|
? 0
|
|
: calculateSecondsFromProgress(
|
|
item.first.duration,
|
|
item.first.progress.toDouble(),
|
|
),
|
|
);
|
|
|
|
switch (_source.runtimeType) {
|
|
case const (FileSource):
|
|
if (kIsWeb) {
|
|
return;
|
|
}
|
|
player.open(
|
|
Media(
|
|
(_source as FileSource).filePath,
|
|
start: duration,
|
|
),
|
|
play: false,
|
|
);
|
|
case const (URLSource):
|
|
case const (MediaURLSource):
|
|
case const (TorrentSource):
|
|
player.open(
|
|
Media(
|
|
(_source as URLSource).url,
|
|
httpHeaders: (_source as URLSource).headers,
|
|
start: duration,
|
|
),
|
|
play: false,
|
|
);
|
|
}
|
|
}
|
|
|
|
late StreamSubscription<bool> _streamComplete;
|
|
late StreamSubscription<bool> _streamListen;
|
|
late StreamSubscription<dynamic> _duration;
|
|
|
|
onLibrarySelect() async {
|
|
_logger.info('Library selection triggered.');
|
|
|
|
controller.player.pause();
|
|
|
|
final result = await showCupertinoDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text("Seasons"),
|
|
),
|
|
body: CustomScrollView(
|
|
slivers: [
|
|
StremioItemSeasonSelector(
|
|
service: widget.service,
|
|
meta: widget.meta as types.Meta,
|
|
shouldPop: true,
|
|
season: int.tryParse(widget.currentSeason!),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
|
|
if (result is MediaURLSource) {
|
|
_source = result;
|
|
|
|
loadFile();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_logger.info('Disposing VideoViewer...');
|
|
|
|
SystemChrome.setPreferredOrientations([
|
|
DeviceOrientation.portraitUp,
|
|
DeviceOrientation.portraitDown,
|
|
DeviceOrientation.landscapeLeft,
|
|
DeviceOrientation.landscapeRight,
|
|
]);
|
|
for (final item in listener) {
|
|
item.cancel();
|
|
}
|
|
SystemChrome.setEnabledSystemUIMode(
|
|
SystemUiMode.edgeToEdge,
|
|
overlays: [],
|
|
);
|
|
_timer?.cancel();
|
|
_streamListen.cancel();
|
|
_duration.cancel();
|
|
|
|
if (widget.meta is types.Meta && player.state.duration.inSeconds > 30) {
|
|
TraktService.instance!.stopScrobbling(
|
|
meta: widget.meta as types.Meta,
|
|
progress: currentProgressInPercentage,
|
|
shouldClearCache: true,
|
|
traktId: traktId,
|
|
);
|
|
}
|
|
|
|
player.dispose();
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: VideoViewerUi(
|
|
controller: controller,
|
|
player: player,
|
|
config: config,
|
|
source: _source,
|
|
onLibrarySelect: onLibrarySelect,
|
|
title: _source.title,
|
|
service: widget.service,
|
|
meta: widget.meta,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
int calculateSecondsFromProgress(
|
|
double duration,
|
|
double progressPercentage,
|
|
) {
|
|
final clampedProgress = progressPercentage.clamp(0.0, 100.0);
|
|
final currentSeconds = (duration * (clampedProgress / 100)).round();
|
|
return currentSeconds;
|
|
}
|