feature: Ask for playback type when media is downloaded (#361)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-06-01 17:05:16 +02:00 committed by GitHub
parent 563d267566
commit 5ef7936c33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 194 additions and 148 deletions

View file

@ -1,5 +1,7 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:chopper/chopper.dart';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -16,6 +18,7 @@ import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/playback/direct_playback_model.dart';
import 'package:fladder/models/playback/offline_playback_model.dart';
import 'package:fladder/models/playback/playback_options_dialogue.dart';
import 'package:fladder/models/playback/transcode_playback_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/models/video_stream_model.dart';
@ -49,10 +52,10 @@ extension PlaybackModelExtension on PlaybackModel? {
AudioStreamModel? get defaultAudioStream =>
this?.audioStreams?.firstWhereOrNull((element) => element.index == this?.mediaStreams?.defaultAudioStreamIndex);
String? get label => switch (this) {
DirectPlaybackModel _ => PlaybackType.directStream.name,
TranscodePlaybackModel _ => PlaybackType.transcode.name,
OfflinePlaybackModel _ => PlaybackType.offline.name,
String? label(BuildContext context) => switch (this) {
DirectPlaybackModel _ => PlaybackType.directStream.name(context),
TranscodePlaybackModel _ => PlaybackType.transcode.name(context),
OfflinePlaybackModel _ => PlaybackType.offline.name(context),
_ => null
};
}
@ -118,12 +121,12 @@ class PlaybackModelHelper {
ref.read(videoPlayerProvider).pause();
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(buffering: true));
final currentModel = ref.read(playBackModel);
final newModel = (await createServerPlaybackModel(
newItem,
final newModel = (await createPlaybackModel(
null,
newItem,
oldModel: currentModel,
)) ??
await createOfflinePlaybackModel(
await _createOfflinePlaybackModel(
newItem,
ref.read(syncProvider.notifier).getSyncedItem(newItem),
oldModel: currentModel,
@ -133,7 +136,7 @@ class PlaybackModelHelper {
return newModel;
}
Future<OfflinePlaybackModel?> createOfflinePlaybackModel(
Future<OfflinePlaybackModel?> _createOfflinePlaybackModel(
ItemBaseModel item,
SyncedItem? syncedItem, {
PlaybackModel? oldModel,
@ -157,46 +160,92 @@ class PlaybackModelHelper {
);
}
Future<EpisodeModel?> getNextUpEpisode(String itemId) async {
final response = await api.showsNextUpGet(parentId: itemId, fields: [ItemFields.overview]);
final episode = response.body?.items?.firstOrNull;
if (episode == null) {
return null;
Future<PlaybackModel?> createPlaybackModel(
BuildContext? context,
ItemBaseModel? item, {
PlaybackModel? oldModel,
List<ItemBaseModel>? libraryQueue,
bool showPlaybackOptions = false,
Duration? startPosition,
}) async {
if (item == null) return null;
final userId = ref.read(userProvider)?.id;
if (userId?.isEmpty == true) return null;
final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item);
final firstItemToPlay = switch (item) {
SeriesModel _ || SeasonModel _ => (queue.whereType<EpisodeModel>().toList().nextUp),
_ => item,
};
if (firstItemToPlay == null) return null;
final fullItem = (await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id)).body;
if (fullItem == null) return null;
SyncedItem? syncedItem = ref.read(syncProvider.notifier).getSyncedItem(fullItem);
final firstItemIsSynced = syncedItem != null && syncedItem.status == SyncStatus.complete;
final options = {
PlaybackType.directStream,
PlaybackType.transcode,
if (firstItemIsSynced) PlaybackType.offline,
};
if ((showPlaybackOptions || firstItemIsSynced) && context != null) {
final playbackType = await showPlaybackTypeSelection(
context: context,
options: options,
);
if (!context.mounted) return null;
return switch (playbackType) {
PlaybackType.directStream || PlaybackType.transcode => await _createServerPlaybackModel(
fullItem,
playbackType,
oldModel: oldModel,
libraryQueue: queue,
startPosition: startPosition,
),
PlaybackType.offline => await _createOfflinePlaybackModel(fullItem, syncedItem),
null => null
};
} else {
return EpisodeModel.fromBaseDto(episode, ref);
return (await _createServerPlaybackModel(
fullItem,
PlaybackType.directStream,
startPosition: startPosition,
oldModel: oldModel,
libraryQueue: queue,
)) ??
await _createOfflinePlaybackModel(fullItem, syncedItem);
}
}
Future<PlaybackModel?> createServerPlaybackModel(
ItemBaseModel? item,
Future<PlaybackModel?> _createServerPlaybackModel(
ItemBaseModel item,
PlaybackType? type, {
PlaybackModel? oldModel,
List<ItemBaseModel>? libraryQueue,
required List<ItemBaseModel> libraryQueue,
Duration? startPosition,
}) async {
try {
if (item == null) return null;
final userId = ref.read(userProvider)?.id;
if (userId?.isEmpty == true) return null;
final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item);
final firstItemToPlay = switch (item) {
SeriesModel _ || SeasonModel _ => (await getNextUpEpisode(item.id) ?? queue.first),
_ => item,
};
final fullItem = await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id);
Map<Bitrate, bool> qualityOptions = getVideoQualityOptions(
VideoQualitySettings(
maxBitRate: ref.read(videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate)),
videoBitRate: firstItemToPlay.streamModel?.videoStreams.firstOrNull?.bitRate ?? 0,
videoCodec: firstItemToPlay.streamModel?.videoStreams.firstOrNull?.codec,
videoBitRate: item.streamModel?.videoStreams.firstOrNull?.bitRate ?? 0,
videoCodec: item.streamModel?.videoStreams.firstOrNull?.codec,
),
);
final streamModel = firstItemToPlay.streamModel;
final streamModel = item.streamModel;
final audioStreamIndex = selectAudioStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
oldModel?.mediaStreams?.currentAudioStream,
@ -209,7 +258,7 @@ class PlaybackModelHelper {
streamModel?.defaultSubStreamIndex);
final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: firstItemToPlay.id,
itemId: item.id,
body: PlaybackInfoDto(
startTimeTicks: startPosition?.toRuntimeTicks,
audioStreamIndex: audioStreamIndex,
@ -238,9 +287,9 @@ class PlaybackModelHelper {
defaultSubStreamIndex: subStreamIndex,
);
final mediaSegments = await api.mediaSegmentsGet(id: firstItemToPlay.id);
final trickPlay = (await api.getTrickPlay(item: fullItem.body, ref: ref))?.body;
final chapters = fullItem.body?.overview.chapters ?? [];
final mediaSegments = await api.mediaSegmentsGet(id: item.id);
final trickPlay = (await api.getTrickPlay(item: item, ref: ref))?.body;
final chapters = item.overview.chapters ?? [];
final mediaPath = isValidVideoUrl(mediaSource.path ?? "");
@ -263,8 +312,8 @@ class PlaybackModelHelper {
final playbackUrl = joinAll([ref.read(userProvider)!.server, "Videos", mediaSource.id!, "stream?$params"]);
return DirectPlaybackModel(
item: fullItem.body ?? item,
queue: queue,
item: item,
queue: libraryQueue,
mediaSegments: mediaSegments?.body,
chapters: chapters,
playbackInfo: playbackInfo,
@ -275,8 +324,8 @@ class PlaybackModelHelper {
);
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
return TranscodePlaybackModel(
item: fullItem.body ?? item,
queue: queue,
item: item,
queue: libraryQueue,
mediaSegments: mediaSegments?.body,
chapters: chapters,
trickPlay: trickPlay,

View file

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:fladder/models/video_stream_model.dart';
import 'package:fladder/screens/shared/adaptive_dialog.dart';
import 'package:fladder/util/localization_helper.dart';
Future<PlaybackType?> showPlaybackTypeSelection({
required BuildContext context,
required Set<PlaybackType> options,
}) async {
PlaybackType? playbackType;
await showDialogAdaptive(
context: context,
builder: (context) {
return PlaybackDialogue(
options: options,
onClose: (type) {
playbackType = type;
Navigator.of(context).pop();
},
);
},
);
return playbackType;
}
class PlaybackDialogue extends StatelessWidget {
final Set<PlaybackType> options;
final Function(PlaybackType type) onClose;
const PlaybackDialogue({required this.options, required this.onClose, super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).add(const EdgeInsets.only(top: 16, bottom: 8)),
child: Text(
context.localized.playbackType,
style: Theme.of(context).textTheme.titleLarge,
),
),
const Divider(),
...options.map((type) => ListTile(
title: Text(type.name(context)),
leading: Icon(type.icon),
onTap: () {
onClose(type);
},
))
],
);
}
}