feature: Re-implemented syncing

This commit is contained in:
PartyDonut 2025-07-27 10:54:29 +02:00
parent c5c7f71b84
commit 86ff355e21
51 changed files with 3067 additions and 1147 deletions

View file

@ -72,20 +72,16 @@ class EpisodeDetailsProvider extends StateNotifier<EpisodeDetailModel> {
}
}
void _tryToCreateOfflineState(ItemBaseModel item) {
Future<void> _tryToCreateOfflineState(ItemBaseModel item) async {
final syncNotifier = ref.read(syncProvider.notifier);
final episodeModel = syncNotifier.getSyncedItem(item)?.createItemModel(ref) as EpisodeModel?;
final episodeModel = (await syncNotifier.getSyncedItem(item))?.itemModel as EpisodeModel?;
if (episodeModel == null) return;
final seriesSyncedItem = syncNotifier.getSyncedItem(episodeModel.parentBaseModel);
final seriesSyncedItem = await syncNotifier.getSyncedItem(episodeModel.parentBaseModel);
if (seriesSyncedItem == null) return;
final seriesModel = seriesSyncedItem.createItemModel(ref) as SeriesModel?;
final seriesModel = seriesSyncedItem.itemModel as SeriesModel?;
if (seriesModel == null) return;
final episodes = syncNotifier
.getNestedChildren(seriesSyncedItem)
.map(
(e) => e.createItemModel(ref),
)
.nonNulls
final episodes = (await syncNotifier.getNestedChildren(seriesSyncedItem))
.map((e) => e.itemModel)
.whereType<EpisodeModel>()
.toList();
state = state.copyWith(

View file

@ -35,11 +35,11 @@ class MovieDetails extends _$MovieDetails {
}
}
void _tryToCreateOfflineState(ItemBaseModel item) {
void _tryToCreateOfflineState(ItemBaseModel item) async {
final syncNotifier = ref.read(syncProvider.notifier);
final syncedItem = syncNotifier.getParentItem(item.id);
final syncedItem = await syncNotifier.getParentItem(item.id);
if (syncedItem == null) return;
final movieModel = syncedItem.createItemModel(ref) as MovieModel;
final movieModel = syncedItem.itemModel as MovieModel?;
state = movieModel;
}

View file

@ -6,7 +6,7 @@ part of 'movies_details_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$movieDetailsHash() => r'872ea61464ef8493c7e6c559c526377f1c8f6a6d';
String _$movieDetailsHash() => r'a9d8d2eeb7fa37652f25c1820b5e346efeeb59fc';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -29,7 +29,12 @@ class SeasonDetailsNotifier extends StateNotifier<SeasonModel?> {
seriesId: newState?.seriesId ?? "",
seasonId: newState?.id,
season: newState?.season,
fields: [ItemFields.overview],
fields: [
ItemFields.overview,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.parentid,
],
);
newState = newState?.copyWith(episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref).toList());
state = newState;

View file

@ -38,6 +38,7 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
ItemFields.mediasources,
ItemFields.overview,
ItemFields.candownload,
ItemFields.childcount,
]);
final newEpisodes = EpisodeModel.episodesFromDto(
@ -45,10 +46,19 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
ref,
);
final episodesCanDownload = newEpisodes.any((episode) => episode.canDownload == true);
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id);
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id, fields: [
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.overview,
ItemFields.candownload,
ItemFields.childcount,
]);
newState = newState.copyWith(
seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref)
.map((element) => element.copyWith(canDownload: true))
.map((element) => element.copyWith(
canDownload: true,
episodes: newEpisodes.where((episode) => episode.season == element.season).toList(),
))
.toList(),
);
@ -68,20 +78,16 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
Future<void> _tryToCreateOfflineState(ItemBaseModel series) async {
final syncNotifier = ref.read(syncProvider.notifier);
final syncedItem = syncNotifier.getSyncedItem(series);
final syncedItem = await syncNotifier.getSyncedItem(series);
if (syncedItem == null) return;
final seriesModel = syncedItem.createItemModel(ref) as SeriesModel;
final allChildren = syncedItem
.getNestedChildren(ref)
.map(
(e) => e.createItemModel(ref),
)
.nonNulls
.toList();
state = seriesModel.copyWith(
availableEpisodes: allChildren.whereType<EpisodeModel>().toList(),
seasons: allChildren.whereType<SeasonModel>().toList(),
);
final seriesModel = syncedItem.itemModel as SeriesModel;
final allChildren = (await syncedItem.getNestedChildren(ref)).map((e) => e.itemModel).toList();
if (mounted) {
state = seriesModel.copyWith(
availableEpisodes: allChildren.whereType<EpisodeModel>().toList(),
seasons: allChildren.whereType<SeasonModel>().toList(),
);
}
return;
}

View file

@ -536,7 +536,10 @@ class JellyService {
return api.showsSeriesIdEpisodesGet(
seriesId: seriesId,
userId: account?.id,
fields: fields,
fields: [
...?fields,
ItemFields.parentid,
],
isMissing: isMissing,
limit: limit,
sortBy: sortBy,
@ -694,7 +697,10 @@ class JellyService {
seriesId: seriesId,
isMissing: isMissing,
enableUserData: enableUserData,
fields: fields,
fields: [
...?fields,
ItemFields.parentid,
],
);
Future<Response<QueryFilters>> itemsFilters2Get({
@ -903,37 +909,37 @@ class JellyService {
Future<Response<dynamic>> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId);
Future<UserConfiguration?> _updateUserConfiguration(UserConfiguration newUserConfiguration) async {
if (account?.id == null) return null;
Future<UserConfiguration?> _updateUserConfiguration(UserConfiguration newUserConfiguration) async {
if (account?.id == null) return null;
final response = await api.usersConfigurationPost(
userId: account!.id,
body: newUserConfiguration,
);
final response = await api.usersConfigurationPost(
userId: account!.id,
body: newUserConfiguration,
);
if (response.isSuccessful) {
return newUserConfiguration;
if (response.isSuccessful) {
return newUserConfiguration;
}
return null;
}
return null;
}
Future<UserConfiguration?> updateRememberAudioSelections() {
final currentUserConfiguration = account?.userConfiguration;
if (currentUserConfiguration == null) return Future.value(null);
Future<UserConfiguration?> updateRememberAudioSelections() {
final currentUserConfiguration = account?.userConfiguration;
if (currentUserConfiguration == null) return Future.value(null);
final updated = currentUserConfiguration.copyWith(
rememberAudioSelections: !(currentUserConfiguration.rememberAudioSelections ?? false),
);
return _updateUserConfiguration(updated);
}
final updated = currentUserConfiguration.copyWith(
rememberAudioSelections: !(currentUserConfiguration.rememberAudioSelections ?? false),
);
return _updateUserConfiguration(updated);
}
Future<UserConfiguration?> updateRememberSubtitleSelections() {
final current = account?.userConfiguration;
if (current == null) return Future.value(null);
Future<UserConfiguration?> updateRememberSubtitleSelections() {
final current = account?.userConfiguration;
if (current == null) return Future.value(null);
final updated = current.copyWith(
rememberSubtitleSelections: !(current.rememberSubtitleSelections ?? false),
);
return _updateUserConfiguration(updated);
}
final updated = current.copyWith(
rememberSubtitleSelections: !(current.rememberSubtitleSelections ?? false),
);
return _updateUserConfiguration(updated);
}
}

View file

@ -1,43 +1,34 @@
import 'package:isar/isar.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/syncing/download_stream.dart';
import 'package:fladder/models/syncing/i_synced_item.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync_provider.dart';
part 'sync_provider_helpers.g.dart';
@riverpod
class SyncChildren extends _$SyncChildren {
@override
List<SyncedItem> build(SyncedItem root) {
final isar = ref.watch(syncProvider.notifier).isar;
final syncPath = ref.read(syncProvider.notifier).syncPath ?? "";
if (isar == null) return [];
final all = <SyncedItem>[];
List<SyncedItem> toProcess = [root];
while (toProcess.isNotEmpty) {
final parentIds = toProcess.map((e) => e.id).toList();
final children = <ISyncedItem>[];
for (final id in parentIds) {
final results = isar.iSyncedItems.where().parentIdEqualTo(id).sortBySortName().findAll();
children.addAll(results);
}
if (children.isEmpty) break;
final wrapped = children.map((e) => SyncedItem.fromIsar(e, syncPath)).toList();
all.addAll(wrapped);
toProcess = wrapped;
}
return all;
Stream<SyncedItem?> syncedItem(Ref ref, ItemBaseModel? item) {
final id = item?.id;
if (id == null || id.isEmpty) {
return Stream.value(null);
}
return ref.watch(syncProvider.notifier).db.getItem(id).watchSingleOrNull();
}
@riverpod
class SyncedChildren extends _$SyncedChildren {
@override
FutureOr<List<SyncedItem>> build(SyncedItem item) => ref.read(syncProvider.notifier).getChildren(item);
}
@riverpod
class SyncedNestedChildren extends _$SyncedNestedChildren {
@override
FutureOr<List<SyncedItem>> build(SyncedItem item) => ref.read(syncProvider.notifier).getNestedChildren(item);
}
@riverpod
@ -55,49 +46,33 @@ class SyncDownloadStatus extends _$SyncDownloadStatus {
int downloadCount = 0;
double fullProgress = mainStream.hasDownload ? mainStream.progress : 0.0;
int fullySyncedChildren = 0;
for (var i = 0; i < nestedChildren.length; i++) {
final childItem = nestedChildren[i];
final downloadStream = ref.read(downloadTasksProvider(childItem.id));
if (childItem.videoFile.existsSync()) {
fullySyncedChildren++;
}
if (downloadStream.hasDownload) {
downloadCount++;
fullProgress += downloadStream.progress;
mainStream = mainStream.copyWith(status: downloadStream.status);
mainStream = mainStream.copyWith(
status: mainStream.status != TaskStatus.running ? downloadStream.status : mainStream.status,
);
}
}
int syncAbleChildren = nestedChildren.where((element) => element.hasVideoFile).length;
var fullySynced = nestedChildren.isNotEmpty ? fullySyncedChildren == syncAbleChildren : arg.videoFile.existsSync();
return mainStream.copyWith(
status: fullySynced ? TaskStatus.complete : mainStream.status,
progress: fullProgress / downloadCount.clamp(1, double.infinity).toInt(),
);
}
}
@riverpod
class SyncStatuses extends _$SyncStatuses {
@override
FutureOr<SyncStatus> build(SyncedItem arg, List<SyncedItem>? children) async {
final nestedChildren = children;
ref.watch(downloadTasksProvider(arg.id));
if (nestedChildren != null) {
for (var element in nestedChildren) {
ref.watch(downloadTasksProvider(element.id));
}
for (var i = 0; i < nestedChildren.length; i++) {
final item = nestedChildren[i];
if (item.hasVideoFile && !await item.videoFile.exists()) {
return SyncStatus.partially;
}
}
}
if (arg.hasVideoFile && !await arg.videoFile.exists()) {
return SyncStatus.partially;
}
return SyncStatus.complete;
}
}
@riverpod
class SyncSize extends _$SyncSize {
@override
@ -112,7 +87,9 @@ class SyncSize extends _$SyncSize {
ref.watch(downloadTasksProvider(element.id));
}
for (var element in nestedChildren) {
size += element.fileSize ?? 0;
if (element.videoFile.existsSync()) {
size += element.fileSize ?? 0;
}
}
}

View file

@ -6,7 +6,7 @@ part of 'sync_provider_helpers.dart';
// RiverpodGenerator
// **************************************************************************
String _$syncChildrenHash() => r'c5a90d630d49f59ad4fbaacb5154f1205799f5ab';
String _$syncedItemHash() => r'7b1178ba78529ebf65425aa4cb8b239a28c7914b';
/// Copied from Dart SDK
class _SystemHash {
@ -29,39 +29,30 @@ class _SystemHash {
}
}
abstract class _$SyncChildren
extends BuildlessAutoDisposeNotifier<List<SyncedItem>> {
late final SyncedItem root;
/// See also [syncedItem].
@ProviderFor(syncedItem)
const syncedItemProvider = SyncedItemFamily();
List<SyncedItem> build(
SyncedItem root,
);
}
/// See also [syncedItem].
class SyncedItemFamily extends Family<AsyncValue<SyncedItem?>> {
/// See also [syncedItem].
const SyncedItemFamily();
/// See also [SyncChildren].
@ProviderFor(SyncChildren)
const syncChildrenProvider = SyncChildrenFamily();
/// See also [SyncChildren].
class SyncChildrenFamily extends Family<List<SyncedItem>> {
/// See also [SyncChildren].
const SyncChildrenFamily();
/// See also [SyncChildren].
SyncChildrenProvider call(
SyncedItem root,
/// See also [syncedItem].
SyncedItemProvider call(
ItemBaseModel? item,
) {
return SyncChildrenProvider(
root,
return SyncedItemProvider(
item,
);
}
@override
SyncChildrenProvider getProviderOverride(
covariant SyncChildrenProvider provider,
SyncedItemProvider getProviderOverride(
covariant SyncedItemProvider provider,
) {
return call(
provider.root,
provider.item,
);
}
@ -77,81 +68,75 @@ class SyncChildrenFamily extends Family<List<SyncedItem>> {
_allTransitiveDependencies;
@override
String? get name => r'syncChildrenProvider';
String? get name => r'syncedItemProvider';
}
/// See also [SyncChildren].
class SyncChildrenProvider
extends AutoDisposeNotifierProviderImpl<SyncChildren, List<SyncedItem>> {
/// See also [SyncChildren].
SyncChildrenProvider(
SyncedItem root,
/// See also [syncedItem].
class SyncedItemProvider extends AutoDisposeStreamProvider<SyncedItem?> {
/// See also [syncedItem].
SyncedItemProvider(
ItemBaseModel? item,
) : this._internal(
() => SyncChildren()..root = root,
from: syncChildrenProvider,
name: r'syncChildrenProvider',
(ref) => syncedItem(
ref as SyncedItemRef,
item,
),
from: syncedItemProvider,
name: r'syncedItemProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$syncChildrenHash,
dependencies: SyncChildrenFamily._dependencies,
: _$syncedItemHash,
dependencies: SyncedItemFamily._dependencies,
allTransitiveDependencies:
SyncChildrenFamily._allTransitiveDependencies,
root: root,
SyncedItemFamily._allTransitiveDependencies,
item: item,
);
SyncChildrenProvider._internal(
SyncedItemProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.root,
required this.item,
}) : super.internal();
final SyncedItem root;
final ItemBaseModel? item;
@override
List<SyncedItem> runNotifierBuild(
covariant SyncChildren notifier,
Override overrideWith(
Stream<SyncedItem?> Function(SyncedItemRef provider) create,
) {
return notifier.build(
root,
);
}
@override
Override overrideWith(SyncChildren Function() create) {
return ProviderOverride(
origin: this,
override: SyncChildrenProvider._internal(
() => create()..root = root,
override: SyncedItemProvider._internal(
(ref) => create(ref as SyncedItemRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
root: root,
item: item,
),
);
}
@override
AutoDisposeNotifierProviderElement<SyncChildren, List<SyncedItem>>
createElement() {
return _SyncChildrenProviderElement(this);
AutoDisposeStreamProviderElement<SyncedItem?> createElement() {
return _SyncedItemProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SyncChildrenProvider && other.root == root;
return other is SyncedItemProvider && other.item == item;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, root.hashCode);
hash = _SystemHash.combine(hash, item.hashCode);
return _SystemHash.finish(hash);
}
@ -159,22 +144,316 @@ class SyncChildrenProvider
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SyncChildrenRef on AutoDisposeNotifierProviderRef<List<SyncedItem>> {
/// The parameter `root` of this provider.
SyncedItem get root;
mixin SyncedItemRef on AutoDisposeStreamProviderRef<SyncedItem?> {
/// The parameter `item` of this provider.
ItemBaseModel? get item;
}
class _SyncChildrenProviderElement
extends AutoDisposeNotifierProviderElement<SyncChildren, List<SyncedItem>>
with SyncChildrenRef {
_SyncChildrenProviderElement(super.provider);
class _SyncedItemProviderElement
extends AutoDisposeStreamProviderElement<SyncedItem?> with SyncedItemRef {
_SyncedItemProviderElement(super.provider);
@override
SyncedItem get root => (origin as SyncChildrenProvider).root;
ItemBaseModel? get item => (origin as SyncedItemProvider).item;
}
String _$syncedChildrenHash() => r'2b6ce1611750785060df6317ce0ea25e2dc0aeb4';
abstract class _$SyncedChildren
extends BuildlessAutoDisposeAsyncNotifier<List<SyncedItem>> {
late final SyncedItem item;
FutureOr<List<SyncedItem>> build(
SyncedItem item,
);
}
/// See also [SyncedChildren].
@ProviderFor(SyncedChildren)
const syncedChildrenProvider = SyncedChildrenFamily();
/// See also [SyncedChildren].
class SyncedChildrenFamily extends Family<AsyncValue<List<SyncedItem>>> {
/// See also [SyncedChildren].
const SyncedChildrenFamily();
/// See also [SyncedChildren].
SyncedChildrenProvider call(
SyncedItem item,
) {
return SyncedChildrenProvider(
item,
);
}
@override
SyncedChildrenProvider getProviderOverride(
covariant SyncedChildrenProvider provider,
) {
return call(
provider.item,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'syncedChildrenProvider';
}
/// See also [SyncedChildren].
class SyncedChildrenProvider extends AutoDisposeAsyncNotifierProviderImpl<
SyncedChildren, List<SyncedItem>> {
/// See also [SyncedChildren].
SyncedChildrenProvider(
SyncedItem item,
) : this._internal(
() => SyncedChildren()..item = item,
from: syncedChildrenProvider,
name: r'syncedChildrenProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$syncedChildrenHash,
dependencies: SyncedChildrenFamily._dependencies,
allTransitiveDependencies:
SyncedChildrenFamily._allTransitiveDependencies,
item: item,
);
SyncedChildrenProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.item,
}) : super.internal();
final SyncedItem item;
@override
FutureOr<List<SyncedItem>> runNotifierBuild(
covariant SyncedChildren notifier,
) {
return notifier.build(
item,
);
}
@override
Override overrideWith(SyncedChildren Function() create) {
return ProviderOverride(
origin: this,
override: SyncedChildrenProvider._internal(
() => create()..item = item,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
item: item,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<SyncedChildren, List<SyncedItem>>
createElement() {
return _SyncedChildrenProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SyncedChildrenProvider && other.item == item;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, item.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SyncedChildrenRef
on AutoDisposeAsyncNotifierProviderRef<List<SyncedItem>> {
/// The parameter `item` of this provider.
SyncedItem get item;
}
class _SyncedChildrenProviderElement
extends AutoDisposeAsyncNotifierProviderElement<SyncedChildren,
List<SyncedItem>> with SyncedChildrenRef {
_SyncedChildrenProviderElement(super.provider);
@override
SyncedItem get item => (origin as SyncedChildrenProvider).item;
}
String _$syncedNestedChildrenHash() =>
r'ea8dd0e694efa6d6ec0c73d699b5fb3e933f9322';
abstract class _$SyncedNestedChildren
extends BuildlessAutoDisposeAsyncNotifier<List<SyncedItem>> {
late final SyncedItem item;
FutureOr<List<SyncedItem>> build(
SyncedItem item,
);
}
/// See also [SyncedNestedChildren].
@ProviderFor(SyncedNestedChildren)
const syncedNestedChildrenProvider = SyncedNestedChildrenFamily();
/// See also [SyncedNestedChildren].
class SyncedNestedChildrenFamily extends Family<AsyncValue<List<SyncedItem>>> {
/// See also [SyncedNestedChildren].
const SyncedNestedChildrenFamily();
/// See also [SyncedNestedChildren].
SyncedNestedChildrenProvider call(
SyncedItem item,
) {
return SyncedNestedChildrenProvider(
item,
);
}
@override
SyncedNestedChildrenProvider getProviderOverride(
covariant SyncedNestedChildrenProvider provider,
) {
return call(
provider.item,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'syncedNestedChildrenProvider';
}
/// See also [SyncedNestedChildren].
class SyncedNestedChildrenProvider extends AutoDisposeAsyncNotifierProviderImpl<
SyncedNestedChildren, List<SyncedItem>> {
/// See also [SyncedNestedChildren].
SyncedNestedChildrenProvider(
SyncedItem item,
) : this._internal(
() => SyncedNestedChildren()..item = item,
from: syncedNestedChildrenProvider,
name: r'syncedNestedChildrenProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$syncedNestedChildrenHash,
dependencies: SyncedNestedChildrenFamily._dependencies,
allTransitiveDependencies:
SyncedNestedChildrenFamily._allTransitiveDependencies,
item: item,
);
SyncedNestedChildrenProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.item,
}) : super.internal();
final SyncedItem item;
@override
FutureOr<List<SyncedItem>> runNotifierBuild(
covariant SyncedNestedChildren notifier,
) {
return notifier.build(
item,
);
}
@override
Override overrideWith(SyncedNestedChildren Function() create) {
return ProviderOverride(
origin: this,
override: SyncedNestedChildrenProvider._internal(
() => create()..item = item,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
item: item,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<SyncedNestedChildren,
List<SyncedItem>> createElement() {
return _SyncedNestedChildrenProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SyncedNestedChildrenProvider && other.item == item;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, item.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SyncedNestedChildrenRef
on AutoDisposeAsyncNotifierProviderRef<List<SyncedItem>> {
/// The parameter `item` of this provider.
SyncedItem get item;
}
class _SyncedNestedChildrenProviderElement
extends AutoDisposeAsyncNotifierProviderElement<SyncedNestedChildren,
List<SyncedItem>> with SyncedNestedChildrenRef {
_SyncedNestedChildrenProviderElement(super.provider);
@override
SyncedItem get item => (origin as SyncedNestedChildrenProvider).item;
}
String _$syncDownloadStatusHash() =>
r'1036352200e1138b4ef70e524c0baf13bb9cd452';
r'6ee039e094f1e007ebaeb20ae63430be829cdeb7';
abstract class _$SyncDownloadStatus
extends BuildlessAutoDisposeNotifier<DownloadStream?> {
@ -344,176 +623,7 @@ class _SyncDownloadStatusProviderElement
(origin as SyncDownloadStatusProvider).children;
}
String _$syncStatusesHash() => r'64a3499fc7b7bbdbd6594b1eec76cf42a119a041';
abstract class _$SyncStatuses
extends BuildlessAutoDisposeAsyncNotifier<SyncStatus> {
late final SyncedItem arg;
late final List<SyncedItem>? children;
FutureOr<SyncStatus> build(
SyncedItem arg,
List<SyncedItem>? children,
);
}
/// See also [SyncStatuses].
@ProviderFor(SyncStatuses)
const syncStatusesProvider = SyncStatusesFamily();
/// See also [SyncStatuses].
class SyncStatusesFamily extends Family<AsyncValue<SyncStatus>> {
/// See also [SyncStatuses].
const SyncStatusesFamily();
/// See also [SyncStatuses].
SyncStatusesProvider call(
SyncedItem arg,
List<SyncedItem>? children,
) {
return SyncStatusesProvider(
arg,
children,
);
}
@override
SyncStatusesProvider getProviderOverride(
covariant SyncStatusesProvider provider,
) {
return call(
provider.arg,
provider.children,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'syncStatusesProvider';
}
/// See also [SyncStatuses].
class SyncStatusesProvider
extends AutoDisposeAsyncNotifierProviderImpl<SyncStatuses, SyncStatus> {
/// See also [SyncStatuses].
SyncStatusesProvider(
SyncedItem arg,
List<SyncedItem>? children,
) : this._internal(
() => SyncStatuses()
..arg = arg
..children = children,
from: syncStatusesProvider,
name: r'syncStatusesProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$syncStatusesHash,
dependencies: SyncStatusesFamily._dependencies,
allTransitiveDependencies:
SyncStatusesFamily._allTransitiveDependencies,
arg: arg,
children: children,
);
SyncStatusesProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.arg,
required this.children,
}) : super.internal();
final SyncedItem arg;
final List<SyncedItem>? children;
@override
FutureOr<SyncStatus> runNotifierBuild(
covariant SyncStatuses notifier,
) {
return notifier.build(
arg,
children,
);
}
@override
Override overrideWith(SyncStatuses Function() create) {
return ProviderOverride(
origin: this,
override: SyncStatusesProvider._internal(
() => create()
..arg = arg
..children = children,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
arg: arg,
children: children,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<SyncStatuses, SyncStatus>
createElement() {
return _SyncStatusesProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SyncStatusesProvider &&
other.arg == arg &&
other.children == children;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, arg.hashCode);
hash = _SystemHash.combine(hash, children.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SyncStatusesRef on AutoDisposeAsyncNotifierProviderRef<SyncStatus> {
/// The parameter `arg` of this provider.
SyncedItem get arg;
/// The parameter `children` of this provider.
List<SyncedItem>? get children;
}
class _SyncStatusesProviderElement
extends AutoDisposeAsyncNotifierProviderElement<SyncStatuses, SyncStatus>
with SyncStatusesRef {
_SyncStatusesProviderElement(super.provider);
@override
SyncedItem get arg => (origin as SyncStatusesProvider).arg;
@override
List<SyncedItem>? get children => (origin as SyncStatusesProvider).children;
}
String _$syncSizeHash() => r'81797ecc4a6f600691b6f1fe0c16bae0228ec920';
String _$syncSizeHash() => r'eeb6ab8dc1fdf5696c06e53f04a0e54ad68c6748';
abstract class _$SyncSize extends BuildlessAutoDisposeNotifier<int?> {
late final SyncedItem arg;

View file

@ -8,10 +8,10 @@ import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart';
import 'package:drift_db_viewer/drift_db_viewer.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
import 'package:isar/isar.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
@ -20,13 +20,14 @@ import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/movie_model.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/syncing/database_item.dart';
import 'package:fladder/models/syncing/download_stream.dart';
import 'package:fladder/models/syncing/i_synced_item.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/models/syncing/sync_settings_model.dart';
import 'package:fladder/models/video_stream_model.dart';
@ -38,16 +39,27 @@ import 'package:fladder/providers/sync/background_download_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/migration/isar_drift_migration.dart';
final syncProvider = StateNotifierProvider<SyncNotifier, SyncSettingsModel>((ref) => throw UnimplementedError());
final downloadTasksProvider = StateProvider.family<DownloadStream, String>((ref, id) => DownloadStream.empty());
class SyncNotifier extends StateNotifier<SyncSettingsModel> {
SyncNotifier(this.ref, this.isar, this.mobileDirectory) : super(SyncSettingsModel()) {
SyncNotifier(this.ref, this.mobileDirectory) : super(SyncSettingsModel()) {
_init();
}
final Ref ref;
late final db = AppDatabase(ref);
final Directory mobileDirectory;
final String subPath = "Synced";
void migrateFromIsar() async {
await isarMigration(ref, db, mainDirectory.path);
_initializeQueryStream();
}
void _init() {
cleanupTemporaryFiles();
ref.listen(
@ -55,35 +67,29 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
(previous, next) {
if (previous?.id != next?.id) {
if (next?.id != null) {
_initializeQueryStream(next?.id ?? "");
_initializeQueryStream();
}
}
},
);
final userId = ref.read(userProvider)?.id;
if (userId != null) {
_initializeQueryStream(userId);
}
_initializeQueryStream();
migrateFromIsar();
}
void _initializeQueryStream(String userId) {
void _initializeQueryStream() async {
final userId = ref.read(userProvider)?.id;
if (userId == null) return;
_subscription?.cancel();
final queryStream = getParentSyncItems
?.userIdEqualTo(userId)
.watch()
.asyncMap((event) => event.map((e) => SyncedItem.fromIsar(e, syncPath ?? "")).toList());
final queryStream = db.getParentItems.watch();
final initItems = getParentSyncItems
?.userIdEqualTo(userId)
.findAll()
.mapIndexed((index, element) => SyncedItem.fromIsar(element, syncPath ?? ""))
.toList();
final initItems = await db.getParentItems.get();
state = state.copyWith(items: initItems ?? []);
state = state.copyWith(items: initItems);
_subscription = queryStream?.listen((items) {
_subscription = queryStream.listen((items) {
state = state.copyWith(items: items);
});
}
@ -117,15 +123,8 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
}
}
final Ref ref;
final Isar? isar;
final Directory mobileDirectory;
final String subPath = "Synced";
StreamSubscription<List<SyncedItem>>? _subscription;
IsarCollection<String, ISyncedItem>? get syncedItems => isar?.iSyncedItems;
late final JellyService api = ref.read(jellyApiProvider);
String? get _savePath => !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)
@ -151,9 +150,6 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
String? get syncPath => saveDirectory?.path;
QueryBuilder<ISyncedItem, ISyncedItem, QAfterFilterCondition>? get getParentSyncItems =>
syncedItems?.where().parentIdIsNull();
Future<int> get directorySize async {
if (saveDirectory == null) return 0;
var files = await saveDirectory!.list(recursive: true).toList();
@ -168,59 +164,62 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
}
Future<void> refresh() async {
state = state.copyWith(
items: (await getParentSyncItems?.userIdEqualTo(ref.read(userProvider)?.id).findAllAsync())
?.map((e) => SyncedItem.fromIsar(e, syncPath ?? ""))
.toList() ??
[]);
state = state.copyWith(items: (await db.getParentItems.get()));
}
List<SyncedItem> getNestedChildren(SyncedItem item) {
final allChildren = <SyncedItem>[];
List<SyncedItem> toProcess = [item];
while (toProcess.isNotEmpty) {
final currentLevel = toProcess.map(
(parent) {
final children = syncedItems?.where().parentIdEqualTo(parent.id).sortBySortName().findAll();
return children?.map((e) => SyncedItem.fromIsar(e, ref.read(syncProvider.notifier).syncPath ?? "")) ??
<SyncedItem>[];
},
);
allChildren.addAll(currentLevel.expand((list) => list));
toProcess = currentLevel.expand((list) => list).toList();
}
return allChildren;
}
Future<List<SyncedItem>> getNestedChildren(SyncedItem item) async => db.getNestedChildren(item);
List<SyncedItem> getChildren(SyncedItem item) {
return (syncedItems?.where().parentIdEqualTo(item.id).sortBySortName().findAll())
?.map(
(e) => SyncedItem.fromIsar(e, syncPath ?? ""),
)
.toList() ??
[];
}
Future<List<SyncedItem>> getChildren(SyncedItem root) async => await db.getChildren(root.id).get();
SyncedItem? getSyncedItem(ItemBaseModel? item) {
Future<SyncedItem?> getSyncedItem(ItemBaseModel? item) async {
final id = item?.id;
if (id == null) return null;
final newItem = syncedItems?.get(id);
if (newItem == null) return null;
return SyncedItem.fromIsar(newItem, syncPath ?? "");
return await db.getItem(id).getSingleOrNull();
}
SyncedItem? getParentItem(String id) {
ISyncedItem? newItem = syncedItems?.get(id);
while (newItem?.parentId != null) {
newItem = syncedItems?.get(newItem!.parentId!);
Future<SyncedItem?> getParentItem(String id) async => await db.getParent(id).getSingleOrNull();
Future<SyncedItem> refreshSyncItem(SyncedItem item) async {
List<SyncedItem> itemsToSync = await getNestedChildren(item);
itemsToSync = [item, ...itemsToSync];
SyncedItem parentItem = item;
List<SyncedItem> newItems = [];
for (var i = 0; i < itemsToSync.length; i++) {
final itemToSync = itemsToSync[i];
final itemResponse = await api.usersUserIdItemsItemIdGetBaseItem(
itemId: itemToSync.id,
);
final itemModel = ItemBaseModel.fromBaseDto(itemResponse.bodyOrThrow, ref);
final syncedParent = await db.getItem(itemToSync.parentId ?? "").getSingleOrNull();
SyncedItem newSyncedItem = await _syncItemData(syncedParent, itemModel, itemResponse.bodyOrThrow);
final updatedItem = itemToSync.copyWith(
itemModel: newSyncedItem.createItemModel(ref),
sortName: newSyncedItem.sortName,
syncing: false,
fImages: newSyncedItem.fImages,
fTrickPlayModel: newSyncedItem.fTrickPlayModel,
subtitles: newSyncedItem.subtitles,
userData: UserData.determineLastUserData([item.userData, newSyncedItem.userData]),
);
newItems.add(updatedItem);
if (itemToSync.id == parentItem.id) {
parentItem = updatedItem;
}
}
if (newItem == null) return null;
return SyncedItem.fromIsar(newItem, syncPath ?? "");
}
ItemBaseModel? getItem(SyncedItem? syncedItem) {
if (syncedItem == null) return null;
return syncedItem.createItemModel(ref);
await db.insertMultipleEntries(newItems);
return parentItem;
}
Future<void> addSyncItem(BuildContext? context, ItemBaseModel item) async {
@ -229,7 +228,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
if (saveDirectory == null) {
String? selectedDirectory =
await FilePicker.platform.getDirectoryPath(dialogTitle: context.localized.syncSelectDownloadsFolder);
if (selectedDirectory?.isEmpty == true) {
if (selectedDirectory?.isEmpty == true && context.mounted) {
fladderSnackbar(context, title: context.localized.syncNoFolderSetup);
return;
}
@ -244,18 +243,24 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
MovieModel movie => await syncMovie(movie),
_ => null
};
fladderSnackbar(context,
title: newSync != null
? context.localized.startedSyncingItem(item.detailedName(context) ?? "Unknown")
: context.localized.unableToSyncItem(item.detailedName(context) ?? "Unknown"));
if (context.mounted) {
fladderSnackbar(context,
title: newSync != null
? context.localized.startedSyncingItem(item.detailedName(context) ?? "Unknown")
: context.localized.unableToSyncItem(item.detailedName(context) ?? "Unknown"));
}
return;
}
void viewDatabase(BuildContext context) =>
Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (context) => DriftDbViewer(db)));
Future<bool> removeSync(BuildContext context, SyncedItem? item) async {
try {
if (item == null) return false;
final nestedChildren = getNestedChildren(item);
final nestedChildren = await getNestedChildren(item);
state = state.copyWith(
items: state.items
@ -268,13 +273,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
await ref.read(backgroundDownloaderProvider).cancelTaskWithId(item.taskId!);
}
final deleteFromDatabase = isar?.write((isar) => syncedItems?.deleteAll([...nestedChildren, item]
.map(
(e) => e.id,
)
.toList()));
if (deleteFromDatabase == 0) return false;
await db.deleteAllItems([...nestedChildren, item]);
for (var i = 0; i < nestedChildren.length; i++) {
final element = nestedChildren[i];
@ -396,12 +395,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
return data?.copyWith(path: fileName);
}
void updateItemSync(SyncedItem syncedItem) =>
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncedItem, syncPath ?? "")));
Future<void> updateItem(SyncedItem syncedItem) async {
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncedItem, syncPath ?? "")));
}
Future<void> updateItem(SyncedItem syncedItem) async => db.insertItem(syncedItem);
Future<SyncedItem> deleteFullSyncFiles(SyncedItem syncedItem, DownloadTask? task) async {
await syncedItem.deleteDatFiles(ref);
@ -512,7 +506,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
Future<void> clear() async {
await mainDirectory.delete(recursive: true);
isar?.write((isar) => syncedItems?.clear());
await db.clearDatabase();
state = state.copyWith(items: []);
}
@ -526,10 +520,24 @@ extension SyncNotifierHelpers on SyncNotifier {
Future<SyncedItem> createSyncItem(BaseItemDto response, {SyncedItem? parent}) async {
final ItemBaseModel item = ItemBaseModel.fromBaseDto(response, ref);
final existingSyncedItem = getSyncedItem(item);
final existingSyncedItem = await getSyncedItem(item);
if (existingSyncedItem != null) return existingSyncedItem;
SyncedItem syncItem = await _syncItemData(parent, item, response);
if (parent == null) {
await db.insertItem(syncItem);
}
return syncItem.copyWith(
fileSize: response.mediaSources?.firstOrNull?.size ?? 0,
syncing: false,
videoFileName: response.path?.split('/').lastOrNull ?? "",
);
}
Future<SyncedItem> _syncItemData(SyncedItem? parent, ItemBaseModel item, BaseItemDto response) async {
final Directory? parentDirectory = parent?.directory;
final directory = Directory(path.joinAll([(parentDirectory ?? saveDirectory)?.path ?? "", item.id]));
@ -550,16 +558,7 @@ extension SyncNotifierHelpers on SyncNotifier {
path: directory.path,
userData: item.userData,
);
if (parent == null) {
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath)));
}
return syncItem.copyWith(
fileSize: response.mediaSources?.firstOrNull?.size ?? 0,
syncing: false,
videoFileName: response.path?.split('/').lastOrNull ?? "",
);
return syncItem;
}
Future<SyncedItem?> syncMovie(ItemBaseModel item, {bool skipDownload = false}) async {
@ -574,9 +573,9 @@ extension SyncNotifierHelpers on SyncNotifier {
if (!syncItem.directory.existsSync()) return null;
await syncFile(syncItem, skipDownload);
await db.insertItem(syncItem);
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath)));
await syncFile(syncItem, skipDownload);
return syncItem;
}
@ -595,6 +594,7 @@ extension SyncNotifierHelpers on SyncNotifier {
if (!seriesItem.directory.existsSync()) return null;
final seasonsResponse = await api.showsSeriesIdSeasonsGet(
seriesId: item.id,
isMissing: false,
enableUserData: true,
fields: [
@ -615,7 +615,6 @@ extension SyncNotifierHelpers on SyncNotifier {
ItemFields.chapters,
ItemFields.trickplay,
],
seriesId: item.id,
);
final seasons = seasonsResponse.body?.items ?? [];
@ -660,23 +659,17 @@ extension SyncNotifierHelpers on SyncNotifier {
for (final (ep, newEpisode) in episodeResults) {
newItems.add(newEpisode);
if (episode?.id == ep.id || newSeason.id == season?.id) {
if (episode?.id == ep.id || newSeason.id == season?.id && !await newEpisode.videoFile.exists()) {
itemsToDownload.add(newEpisode);
}
}
}
isar?.write(
(isar) => syncedItems?.putAll(newItems
.map(
(e) => ISyncedItem.fromSynced(e, syncPath ?? ""),
)
.toList()),
);
await db.insertMultipleEntries(newItems);
for (var i = 0; i < itemsToDownload.length; i++) {
final item = itemsToDownload[i];
await syncFile(item, false);
syncFile(item, false);
}
return seriesItem;