Remove metadata from album files

This help reduce the file size
This commit is contained in:
Ming Ming 2021-09-26 00:22:19 +08:00
parent e2152a7ebe
commit f01c64a155
19 changed files with 856 additions and 349 deletions

View file

@ -140,6 +140,7 @@ class AppDbAlbumEntry {
upgraderV1: AlbumUpgraderV1(),
upgraderV2: AlbumUpgraderV2(),
upgraderV3: AlbumUpgraderV3(),
upgraderV4: AlbumUpgraderV4(),
)!,
);
}

View file

@ -44,6 +44,7 @@ class Album with EquatableMixin {
required AlbumUpgraderV1? upgraderV1,
required AlbumUpgraderV2? upgraderV2,
required AlbumUpgraderV3? upgraderV3,
required AlbumUpgraderV4? upgraderV4,
}) {
final jsonVersion = json["version"];
JsonObj? result = json;
@ -68,6 +69,13 @@ class Album with EquatableMixin {
return null;
}
}
if (jsonVersion < 5) {
result = upgraderV4?.call(result);
if (result == null) {
_log.info("[fromJson] Version $jsonVersion not compatible");
return null;
}
}
return Album(
lastUpdated: result["lastUpdated"] == null
? null
@ -168,7 +176,7 @@ class Album with EquatableMixin {
final File? albumFile;
/// versioning of this class, use to upgrade old persisted album
static const version = 4;
static const version = 5;
}
class AlbumRepo {
@ -224,6 +232,7 @@ class AlbumRemoteDataSource implements AlbumDataSource {
upgraderV1: AlbumUpgraderV1(),
upgraderV2: AlbumUpgraderV2(),
upgraderV3: AlbumUpgraderV3(),
upgraderV4: AlbumUpgraderV4(),
)!
.copyWith(
lastUpdated: OrNull(null),

View file

@ -4,6 +4,7 @@ import 'package:equatable/equatable.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/list_extension.dart';
import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/type.dart';
List<AlbumItem> makeDistinctAlbumItems(List<AlbumItem> items) =>
@ -95,6 +96,10 @@ class AlbumFileItem extends AlbumItem with EquatableMixin {
};
}
AlbumFileItem minimize() => AlbumFileItem(
file: file.copyWith(metadata: OrNull(null)),
);
@override
get props => [
// file is handled separately, see [equals]

View file

@ -6,7 +6,6 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/type.dart';
@ -58,13 +57,54 @@ abstract class AlbumProvider with EquatableMixin {
static final _log = Logger("entity.album.provider.AlbumProvider");
}
class AlbumStaticProvider extends AlbumProvider {
abstract class AlbumProviderBase extends AlbumProvider {
const AlbumProviderBase({
this.latestItemTime,
});
@override
toString({bool isDeep = false}) {
return "$runtimeType {"
"latestItemTime: $latestItemTime, "
"}";
}
@override
toContentJson() {
return {
if (latestItemTime != null)
"latestItemTime": latestItemTime!.toUtc().toIso8601String(),
};
}
@override
AlbumProviderBase copyWith({
DateTime? latestItemTime,
});
@override
get props => [
latestItemTime,
];
@override
final DateTime? latestItemTime;
}
class AlbumStaticProvider extends AlbumProviderBase {
AlbumStaticProvider({
DateTime? latestItemTime,
required List<AlbumItem> items,
}) : items = UnmodifiableListView(items);
}) : items = UnmodifiableListView(items),
super(
latestItemTime: latestItemTime,
);
factory AlbumStaticProvider.fromJson(JsonObj json) {
return AlbumStaticProvider(
latestItemTime: json["latestItemTime"] == null
? null
: DateTime.parse(json["latestItemTime"]),
items: (json["items"] as List)
.map((e) => AlbumItem.fromJson(e.cast<String, dynamic>()))
.toList(),
@ -79,6 +119,7 @@ class AlbumStaticProvider extends AlbumProvider {
final itemsStr =
isDeep ? items.toReadableString() : "List {length: ${items.length}}";
return "$runtimeType {"
"super: ${super.toString(isDeep: isDeep)}, "
"items: $itemsStr, "
"}";
}
@ -86,36 +127,25 @@ class AlbumStaticProvider extends AlbumProvider {
@override
toContentJson() {
return {
...super.toContentJson(),
"items": items.map((e) => e.toJson()).toList(),
};
}
@override
AlbumStaticProvider copyWith({
DateTime? latestItemTime,
List<AlbumItem>? items,
}) {
return AlbumStaticProvider(
latestItemTime: latestItemTime ?? this.latestItemTime,
items: items ?? this.items,
);
}
@override
get latestItemTime {
try {
return items
.whereType<AlbumFileItem>()
.map((e) => e.file)
.where((element) => file_util.isSupportedFormat(element))
.sorted(compareFileDateTimeDescending)
.first
.bestDateTime;
} catch (_) {
return null;
}
}
@override
get props => [
...super.props,
items,
];
@ -125,55 +155,28 @@ class AlbumStaticProvider extends AlbumProvider {
static const _type = "static";
}
abstract class AlbumDynamicProvider extends AlbumProvider {
AlbumDynamicProvider({
abstract class AlbumDynamicProvider extends AlbumProviderBase {
const AlbumDynamicProvider({
DateTime? latestItemTime,
}) : _latestItemTime = latestItemTime;
@override
toString({bool isDeep = false}) {
return "$runtimeType {"
"latestItemTime: $_latestItemTime, "
"}";
}
@override
toContentJson() {
return {
"latestItemTime": _latestItemTime?.toUtc().toIso8601String(),
};
}
@override
AlbumDynamicProvider copyWith({
DateTime? latestItemTime,
});
@override
get latestItemTime => _latestItemTime;
@override
get props => [
_latestItemTime,
];
final DateTime? _latestItemTime;
}) : super(latestItemTime: latestItemTime);
}
class AlbumDirProvider extends AlbumDynamicProvider {
AlbumDirProvider({
const AlbumDirProvider({
required this.dirs,
DateTime? latestItemTime,
}) : super(latestItemTime: latestItemTime);
}) : super(
latestItemTime: latestItemTime,
);
factory AlbumDirProvider.fromJson(JsonObj json) {
return AlbumDirProvider(
dirs: (json["dirs"] as List)
.map((e) => File.fromJson(e.cast<String, dynamic>()))
.toList(),
latestItemTime: json["latestItemTime"] == null
? null
: DateTime.parse(json["latestItemTime"]),
dirs: (json["dirs"] as List)
.map((e) => File.fromJson(e.cast<String, dynamic>()))
.toList(),
);
}
@ -195,12 +198,12 @@ class AlbumDirProvider extends AlbumDynamicProvider {
@override
AlbumDirProvider copyWith({
List<File>? dirs,
DateTime? latestItemTime,
List<File>? dirs,
}) {
return AlbumDirProvider(
dirs: dirs ?? this.dirs,
latestItemTime: latestItemTime ?? this.latestItemTime,
dirs: dirs ?? this.dirs,
);
}

View file

@ -1,5 +1,8 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/entity/exif.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/type.dart';
import 'package:tuple/tuple.dart';
abstract class AlbumUpgrader {
JsonObj? call(JsonObj json);
@ -85,3 +88,72 @@ class AlbumUpgraderV3 implements AlbumUpgrader {
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV3");
}
/// Upgrade v4 Album to v5
class AlbumUpgraderV4 implements AlbumUpgrader {
AlbumUpgraderV4({
this.logFilePath,
});
@override
call(JsonObj json) {
_log.fine("[call] Upgrade v4 Album for file: $logFilePath");
final result = JsonObj.from(json);
try {
if (result["provider"]["type"] != "static") {
return result;
}
final latestItem = (result["provider"]["content"]["items"] as List)
.map((e) => e.cast<String, dynamic>())
.where((e) => e["type"] == "file")
.map((e) => e["content"]["file"] as JsonObj)
.map((e) {
final overrideDateTime = e["overrideDateTime"] == null
? null
: DateTime.parse(e["overrideDateTime"]);
final String? dateTimeOriginalStr =
e["metadata"]?["exif"]?["DateTimeOriginal"];
final dateTimeOriginal =
dateTimeOriginalStr == null || dateTimeOriginalStr.isEmpty
? null
: Exif.dateTimeFormat.parse(dateTimeOriginalStr).toUtc();
final lastModified = e["lastModified"] == null
? null
: DateTime.parse(e["lastModified"]);
final latestItemTime =
overrideDateTime ?? dateTimeOriginal ?? lastModified;
// remove metadata
e.remove("metadata");
if (latestItemTime != null) {
return Tuple2(latestItemTime, e);
} else {
return null;
}
})
.whereType<Tuple2<DateTime, JsonObj>>()
.sorted((a, b) => a.item1.compareTo(b.item1))
.lastOrNull;
if (latestItem != null) {
// save the latest item time
result["provider"]["content"]["latestItemTime"] =
latestItem.item1.toIso8601String();
if (result["coverProvider"]["type"] == "auto") {
// save the cover
result["coverProvider"]["content"]["coverFile"] =
Map.of(latestItem.item2);
}
}
} catch (e, stackTrace) {
// this upgrade is not a must, if it failed then just leave it and it'll
// be upgraded the next time the album is saved
_log.shout("[call] Failed while upgrade", e, stackTrace);
}
return result;
}
/// File path for logging only
final String? logFilePath;
static final _log = Logger("entity.album.upgrader.AlbumUpgraderV4");
}

View file

@ -49,4 +49,12 @@ extension IterableExtension<T> on Iterable<T> {
return null;
}
}
T? get lastOrNull {
try {
return last;
} on StateError catch (_) {
return null;
}
}
}

View file

@ -1,24 +1,43 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/use_case/resync_album.dart';
import 'package:nc_photos/use_case/update_album.dart';
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
class AddToAlbum {
AddToAlbum(this.albumRepo);
/// Add a list of AlbumItems to [album]
Future<void> call(Account account, Album album, List<AlbumItem> items) =>
UpdateAlbum(albumRepo)(
account,
album.copyWith(
provider: AlbumStaticProvider.of(album).copyWith(
items: makeDistinctAlbumItems([
...items,
...AlbumStaticProvider.of(album).items,
]),
),
));
Future<Album> call(
Account account, Album album, List<AlbumItem> items) async {
_log.info("[call] Add ${items.length} items to album '${album.name}'");
assert(album.provider is AlbumStaticProvider);
// resync is needed to work out album cover and latest item
final oldItems = await ResyncAlbum()(account, album);
final newItems = makeDistinctAlbumItems([
...items,
...oldItems,
]);
var newAlbum = album.copyWith(
provider: AlbumStaticProvider.of(album).copyWith(
items: newItems,
),
);
// UpdateAlbumWithActualItems only persists when there are changes to
// several properties, so we can't rely on it
newAlbum = await UpdateAlbumWithActualItems(null)(
account,
newAlbum,
newItems,
);
await UpdateAlbum(albumRepo)(account, newAlbum);
return newAlbum;
}
final AlbumRepo albumRepo;
static final _log = Logger("use_case.add_to_album.AddToAlbum");
}

View file

@ -3,8 +3,11 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/use_case/resync_album.dart';
import 'package:nc_photos/use_case/update_album.dart';
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
class RemoveFromAlbum {
RemoveFromAlbum(this.albumRepo);
@ -26,6 +29,18 @@ class RemoveFromAlbum {
items: newItems,
),
);
// check if any of the removed items was the latest item
if (items.whereType<AlbumFileItem>().any((element) =>
element.file.bestDateTime == album.provider.latestItemTime)) {
_log.info("[call] Resync as latest item is being removed");
// need to update the album properties
final newItemsSynced = await ResyncAlbum()(account, newAlbum);
newAlbum = await UpdateAlbumWithActualItems(null)(
account,
newAlbum,
newItemsSynced,
);
}
await UpdateAlbum(albumRepo)(account, newAlbum);
return newAlbum;
}

View file

@ -10,11 +10,11 @@ import 'package:nc_photos/entity/file_util.dart' as file_util;
/// Resync files inside an album with the file db
class ResyncAlbum {
Future<Album> call(Account account, Album album) async {
Future<List<AlbumItem>> call(Account account, Album album) async {
_log.info("[call] Resync album: ${album.name}");
if (album.provider is! AlbumStaticProvider) {
_log.warning(
"[call] Resync only make sense for static albums: ${album.name}");
return album;
throw ArgumentError(
"Resync only make sense for static albums: ${album.name}");
}
return await AppDb.use((db) async {
final transaction =
@ -38,7 +38,7 @@ class ResyncAlbum {
newItems.add(item);
}
}
return album.copyWith(provider: AlbumStaticProvider(items: newItems));
return newItems;
});
}

View file

@ -2,15 +2,33 @@ import 'package:event_bus/event_bus.dart';
import 'package:kiwi/kiwi.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/event/event.dart';
class UpdateAlbum {
UpdateAlbum(this.albumRepo);
Future<void> call(Account account, Album album) async {
await albumRepo.update(account, album);
final provider = album.provider;
if (provider is AlbumStaticProvider) {
await albumRepo.update(
account,
album.copyWith(
provider: provider.copyWith(
items: _minimizeItems(provider.items),
),
),
);
} else {
await albumRepo.update(account, album);
}
KiwiContainer().resolve<EventBus>().fire(AlbumUpdatedEvent(account, album));
}
List<AlbumItem> _minimizeItems(List<AlbumItem> items) {
return items.map((e) => e is AlbumFileItem ? e.minimize() : e).toList();
}
final AlbumRepo albumRepo;
}

View file

@ -0,0 +1,56 @@
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
class UpdateAlbumTime {
/// Update the latest item time of an album with unsorted items
///
/// If no updates are needed, return the same object
Album call(Album album, List<AlbumItem> items) {
if (album.provider is! AlbumProviderBase) {
return album;
} else {
final sortedItems =
const AlbumTimeSortProvider(isAscending: false).sort(items);
return _updateWithSortedItems(album, sortedItems);
}
}
/// Update the latest item time of an album with pre-sorted files
///
/// The album items are expected to be sorted by [AlbumTimeSortProvider] with
/// isAscending = false, otherwise please call the unsorted version. If no
/// updates are needed, return the same object
Album updateWithSortedItems(Album album, List<AlbumItem> sortedItems) {
if (album.provider is! AlbumProviderBase) {
return album;
} else {
return _updateWithSortedItems(album, sortedItems);
}
}
Album _updateWithSortedItems(Album album, List<AlbumItem> sortedItems) {
DateTime? latestItemTime;
try {
final latestFile = sortedItems
.whereType<AlbumFileItem>()
.map((e) => e.file)
.where((element) => file_util.isSupportedFormat(element))
.first;
latestItemTime = latestFile.bestDateTime;
} catch (_) {
latestItemTime = null;
}
if (latestItemTime != album.provider.latestItemTime) {
return album.copyWith(
provider: (album.provider as AlbumProviderBase).copyWith(
latestItemTime: latestItemTime,
),
);
}
return album;
}
}

View file

@ -0,0 +1,50 @@
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/use_case/update_album.dart';
import 'package:nc_photos/use_case/update_album_time.dart';
import 'package:nc_photos/use_case/update_auto_album_cover.dart';
class UpdateAlbumWithActualItems {
UpdateAlbumWithActualItems(this.albumRepo);
/// Update, if necessary, [album] after resynced/populated with actual items
///
/// If [albumRepo] is null, the modified album will not be saved
Future<Album> call(
Account account, Album album, List<AlbumItem> items) async {
final sortedItems =
const AlbumTimeSortProvider(isAscending: false).sort(items);
bool shouldUpdate = false;
final albumUpdatedCover =
UpdateAutoAlbumCover().updateWithSortedItems(album, sortedItems);
if (!identical(albumUpdatedCover, album)) {
_log.info("[call] Update album cover");
shouldUpdate = true;
}
album = albumUpdatedCover;
final albumUpdatedTime =
UpdateAlbumTime().updateWithSortedItems(album, sortedItems);
if (!identical(albumUpdatedTime, album)) {
_log.info(
"[call] Update album time: ${album.provider.latestItemTime} -> ${albumUpdatedTime.provider.latestItemTime}");
shouldUpdate = true;
}
album = albumUpdatedTime;
if (albumRepo != null && shouldUpdate) {
_log.info("[call] Persist album");
await UpdateAlbum(albumRepo!)(account, album);
}
return album;
}
final AlbumRepo? albumRepo;
static final _log = Logger(
"use_case.update_album_with_actual_items.UpdateAlbumWithActualItems");
}

View file

@ -0,0 +1,55 @@
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
class UpdateAutoAlbumCover {
/// Update the AlbumAutoCoverProvider of an album with unsorted items
///
/// If no updates are needed, return the same object
Album call(Album album, List<AlbumItem> items) {
if (album.coverProvider is! AlbumAutoCoverProvider) {
return album;
} else {
final sortedItems =
const AlbumTimeSortProvider(isAscending: false).sort(items);
return _updateWithSortedItems(album, sortedItems);
}
}
/// Update the AlbumAutoCoverProvider of an album with pre-sorted files
///
/// The album items are expected to be sorted by [AlbumTimeSortProvider] with
/// isAscending = false, otherwise please call the unsorted version. If no
/// updates are needed, return the same object
Album updateWithSortedItems(Album album, List<AlbumItem> sortedItems) {
if (album.coverProvider is! AlbumAutoCoverProvider) {
return album;
} else {
return _updateWithSortedItems(album, sortedItems);
}
}
Album _updateWithSortedItems(Album album, List<AlbumItem> sortedItems) {
try {
final coverFile = sortedItems
.whereType<AlbumFileItem>()
.map((e) => e.file)
.where((element) => file_util.isSupportedFormat(element))
.firstWhere((element) => element.hasPreview ?? false);
// cache the result for later use
if (coverFile.path !=
(album.coverProvider as AlbumAutoCoverProvider).coverFile?.path) {
return album.copyWith(
coverProvider: AlbumAutoCoverProvider(
coverFile: coverFile,
),
);
}
} on StateError catch (_) {
// no files
}
return album;
}
}

View file

@ -1,60 +0,0 @@
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/iterable_extension.dart';
class UpdateDynamicAlbumCover {
/// Update the cover of a dynamic album with unsorted items
///
/// If no updates are needed, return the same object
Album call(Album album, List<AlbumItem> populatedItems) {
if (album.provider is! AlbumDynamicProvider ||
album.coverProvider is! AlbumAutoCoverProvider) {
return album;
} else {
return _updateWithSortedFiles(
album,
populatedItems
.whereType<AlbumFileItem>()
.map((e) => e.file)
.where((element) => file_util.isSupportedFormat(element))
.sorted(compareFileDateTimeDescending));
}
}
/// Update the cover of a dynamic album with pre-sorted files
///
/// The album items are expected to be sorted by
/// [compareFileDateTimeDescending], otherwise please call the unsorted
/// version. If no updates are needed, return the same object
Album updateWithSortedFiles(Album album, List<File> sortedFiles) {
if (album.provider is! AlbumDynamicProvider ||
album.coverProvider is! AlbumAutoCoverProvider) {
return album;
} else {
return _updateWithSortedFiles(album, sortedFiles);
}
}
Album _updateWithSortedFiles(Album album, List<File> sortedFiles) {
try {
final coverFile =
sortedFiles.firstWhere((element) => element.hasPreview ?? false);
// cache the result for later use
if (coverFile.path !=
(album.coverProvider as AlbumAutoCoverProvider).coverFile?.path) {
return album.copyWith(
coverProvider: AlbumAutoCoverProvider(
coverFile: coverFile,
),
);
}
} on StateError catch (_) {
// no files
}
return album;
}
}

View file

@ -1,55 +0,0 @@
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/iterable_extension.dart';
class UpdateDynamicAlbumTime {
/// Update the latest item time of a dynamic album with unsorted items
///
/// If no updates are needed, return the same object
Album call(Album album, List<AlbumItem> populatedItems) {
if (album.provider is! AlbumDynamicProvider) {
return album;
} else {
return _updateWithSortedFiles(
album,
populatedItems
.whereType<AlbumFileItem>()
.map((e) => e.file)
.where((element) => file_util.isSupportedFormat(element))
.sorted(compareFileDateTimeDescending));
}
}
/// Update the latest item time of a dynamic album with pre-sorted files
///
/// The album items are expected to be sorted by
/// [compareFileDateTimeDescending], otherwise please call the unsorted
/// version. If no updates are needed, return the same object
Album updateWithSortedFiles(Album album, List<File> sortedFiles) {
if (album.provider is! AlbumDynamicProvider) {
return album;
} else {
return _updateWithSortedFiles(album, sortedFiles);
}
}
Album _updateWithSortedFiles(Album album, List<File> sortedFiles) {
DateTime? latestItemTime;
try {
latestItemTime = sortedFiles.first.bestDateTime;
} catch (_) {
latestItemTime = null;
}
if (latestItemTime != album.provider.latestItemTime) {
return album.copyWith(
provider: (album.provider as AlbumDynamicProvider).copyWith(
latestItemTime: latestItemTime,
),
);
}
return album;
}
}

View file

@ -14,8 +14,8 @@ import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/event/event.dart';
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/list_extension.dart';
import 'package:nc_photos/or_null.dart';
import 'package:nc_photos/platform/k.dart' as platform_k;
import 'package:nc_photos/session_storage.dart';
@ -25,6 +25,7 @@ import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/remove_from_album.dart';
import 'package:nc_photos/use_case/resync_album.dart';
import 'package:nc_photos/use_case/update_album.dart';
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
import 'package:nc_photos/widget/album_browser_mixin.dart';
import 'package:nc_photos/widget/draggable_item_list_mixin.dart';
import 'package:nc_photos/widget/fancy_option_picker.dart';
@ -32,7 +33,6 @@ import 'package:nc_photos/widget/photo_list_item.dart';
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
import 'package:nc_photos/widget/simple_input_dialog.dart';
import 'package:nc_photos/widget/viewer.dart';
import 'package:quiver/iterables.dart';
class AlbumBrowserArguments {
AlbumBrowserArguments(this.account, this.album);
@ -169,20 +169,10 @@ class _AlbumBrowserState extends State<AlbumBrowser>
}
}
void _initAlbum() {
assert(widget.album.provider is AlbumStaticProvider);
ResyncAlbum()(widget.account, widget.album).then((album) {
if (_shouldPropagateResyncedAlbum(album)) {
_propagateResyncedAlbum(album);
}
if (mounted) {
setState(() {
_album = album;
_transformItems();
initCover(widget.account, album);
});
}
});
Future<void> _initAlbum() async {
final albumRepo = AlbumRepo(AlbumCachedDataSource());
final album = await albumRepo.get(widget.account, widget.album.albumFile!);
await _setAlbum(album);
}
Widget _buildContent(BuildContext context) {
@ -495,13 +485,9 @@ class _AlbumBrowserState extends State<AlbumBrowser>
});
}
void _onAlbumUpdatedEvent(AlbumUpdatedEvent ev) {
Future<void> _onAlbumUpdatedEvent(AlbumUpdatedEvent ev) async {
if (ev.album.albumFile!.path == _album?.albumFile?.path) {
setState(() {
_album = ev.album;
_transformItems();
initCover(widget.account, ev.album);
});
await _setAlbum(ev.album);
}
}
@ -600,64 +586,29 @@ class _AlbumBrowserState extends State<AlbumBrowser>
draggableItemList = items;
}
void _propagateResyncedAlbum(Album album) {
final propagateItems =
zip([_getAlbumItemsOf(album), _getAlbumItemsOf(widget.album)]).map((e) {
if (e[0] is AlbumFileItem) {
final item = e[0] as AlbumFileItem;
if (!item.file.isOwned(widget.account.username)) {
// don't propagate shared file not owned by this user, this is to
// prevent multiple user having different properties to keep
// overriding each others
_log.info(
"[_propagateResyncedAlbum] Skip shared file: ${item.file.path}");
return e[1];
}
}
return e[0];
}).toList();
final propagateAlbum = album.copyWith(
Future<void> _setAlbum(Album album) async {
assert(album.provider is AlbumStaticProvider);
final items = await ResyncAlbum()(widget.account, album);
album = album.copyWith(
provider: AlbumStaticProvider.of(album).copyWith(
items: propagateItems,
items: items,
),
);
UpdateAlbum(AlbumRepo(AlbumCachedDataSource()))(
widget.account, propagateAlbum)
.catchError((e, stacktrace) {
_log.shout("[_propagateResyncedAlbum] Failed while updating album", e,
stacktrace);
});
album = await _updateAlbumPostResync(album, items);
if (mounted) {
setState(() {
_album = album;
_transformItems();
initCover(widget.account, album);
});
}
}
bool _shouldPropagateResyncedAlbum(Album album) {
final origItems = _getAlbumItemsOf(widget.album);
final resyncItems = _getAlbumItemsOf(album);
if (origItems.length != resyncItems.length) {
_log.info(
"[_shouldPropagateResyncedAlbum] Item length differ: ${origItems.length}, ${resyncItems.length}");
return true;
}
for (final z in zip([origItems, resyncItems])) {
final a = z[0], b = z[1];
bool isEqual;
if (a is AlbumFileItem && b is AlbumFileItem) {
if (!a.file.isOwned(widget.account.username)) {
// ignore shared files
continue;
}
// faster compare
isEqual = a.equals(b, isDeep: false);
} else {
isEqual = a == b;
}
if (!isEqual) {
_log.info(
"[_shouldPropagateResyncedAlbum] Item differ:\nOriginal: ${z[0]}\nResynced: ${z[1]}");
return true;
}
}
_log.info("[_shouldPropagateResyncedAlbum] false");
return false;
Future<Album> _updateAlbumPostResync(
Album album, List<AlbumItem> items) async {
final albumRepo = AlbumRepo(AlbumCachedDataSource());
return await UpdateAlbumWithActualItems(albumRepo)(
widget.account, album, items);
}
static List<AlbumItem> _getAlbumItemsOf(Album a) =>

View file

@ -8,11 +8,9 @@ import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc/list_importable_album.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/album/provider.dart';
import 'package:nc_photos/entity/album/sort_provider.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/exception_util.dart' as exception_util;
import 'package:nc_photos/iterable_extension.dart';
import 'package:nc_photos/k.dart' as k;
@ -20,8 +18,7 @@ import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/create_album.dart';
import 'package:nc_photos/use_case/populate_album.dart';
import 'package:nc_photos/use_case/update_dynamic_album_cover.dart';
import 'package:nc_photos/use_case/update_dynamic_album_time.dart';
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
import 'package:nc_photos/widget/processing_dialog.dart';
import 'package:path/path.dart' as path;
@ -239,15 +236,8 @@ class _AlbumImporterState extends State<AlbumImporter> {
_log.info("[_createAllAlbums] Creating dir album: $album");
final items = await PopulateAlbum()(widget.account, album);
final sortedFiles = items
.whereType<AlbumFileItem>()
.map((e) => e.file)
.where((element) => file_util.isSupportedFormat(element))
.sorted(compareFileDateTimeDescending);
album =
UpdateDynamicAlbumCover().updateWithSortedFiles(album, sortedFiles);
album =
UpdateDynamicAlbumTime().updateWithSortedFiles(album, sortedFiles);
album = await UpdateAlbumWithActualItems(null)(
widget.account, album, items);
final albumRepo = AlbumRepo(AlbumCachedDataSource());
await CreateAlbum(albumRepo)(widget.account, album);

View file

@ -27,8 +27,7 @@ import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/populate_album.dart';
import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/use_case/update_album.dart';
import 'package:nc_photos/use_case/update_dynamic_album_cover.dart';
import 'package:nc_photos/use_case/update_dynamic_album_time.dart';
import 'package:nc_photos/use_case/update_album_with_actual_items.dart';
import 'package:nc_photos/widget/album_browser_mixin.dart';
import 'package:nc_photos/widget/fancy_option_picker.dart';
import 'package:nc_photos/widget/photo_list_item.dart';
@ -159,58 +158,16 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
}
}
void _initAlbum() {
Future<void> _initAlbum() async {
assert(widget.album.provider is AlbumDynamicProvider);
PopulateAlbum()(widget.account, widget.album).then((items) {
if (mounted) {
setState(() {
_album = widget.album;
_transformItems(items);
initCover(widget.account, widget.album);
_updateAlbumPostPopulate(items);
});
}
});
}
Future<void> _updateAlbumPostPopulate(List<AlbumItem> items) async {
final List<File> timeDescSortedFiles;
if (widget.album.sortProvider is AlbumTimeSortProvider) {
if ((widget.album.sortProvider as AlbumTimeSortProvider).isAscending) {
timeDescSortedFiles = _backingFiles.reversed.toList();
} else {
timeDescSortedFiles = _backingFiles;
}
} else {
timeDescSortedFiles = const AlbumTimeSortProvider(isAscending: false)
.sort(items)
.whereType<AlbumFileItem>()
.map((e) => e.file)
.where((element) => file_util.isSupportedFormat(element))
.toList();
}
bool shouldUpdate = false;
final albumUpdatedCover = UpdateDynamicAlbumCover()
.updateWithSortedFiles(_album!, timeDescSortedFiles);
if (!identical(albumUpdatedCover, _album)) {
_log.info("[_updateAlbumPostPopulate] Update album cover");
shouldUpdate = true;
}
_album = albumUpdatedCover;
final albumUpdatedTime = UpdateDynamicAlbumTime()
.updateWithSortedFiles(_album!, timeDescSortedFiles);
if (!identical(albumUpdatedTime, _album)) {
_log.info(
"[_updateAlbumPostPopulate] Update album time: ${albumUpdatedTime.provider.latestItemTime}");
shouldUpdate = true;
}
_album = albumUpdatedTime;
if (shouldUpdate) {
await UpdateAlbum(AlbumRepo(AlbumCachedDataSource()))(
widget.account, _album!);
final items = await PopulateAlbum()(widget.account, widget.album);
final album = await _updateAlbumPostPopulate(widget.album, items);
if (mounted) {
setState(() {
_album = album;
_transformItems(items);
initCover(widget.account, widget.album);
});
}
}
@ -376,6 +333,7 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
widget.account,
_album!.copyWith(
provider: AlbumStaticProvider(
latestItemTime: _album!.provider.latestItemTime,
items: _sortedItems,
),
coverProvider: AlbumAutoCoverProvider(),
@ -513,12 +471,12 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
});
}
void _onAlbumUpdatedEvent(AlbumUpdatedEvent ev) {
Future<void> _onAlbumUpdatedEvent(AlbumUpdatedEvent ev) async {
if (ev.album.albumFile!.path == _album?.albumFile?.path) {
final album = await _updateAlbumPostPopulate(ev.album, _sortedItems);
setState(() {
_album = ev.album;
initCover(widget.account, ev.album);
_updateAlbumPostPopulate(_sortedItems);
_album = album;
initCover(widget.account, album);
});
}
}
@ -572,6 +530,13 @@ class _DynamicAlbumBrowserState extends State<DynamicAlbumBrowser>
.toList();
}
Future<Album> _updateAlbumPostPopulate(
Album album, List<AlbumItem> items) async {
final albumRepo = AlbumRepo(AlbumCachedDataSource());
return await UpdateAlbumWithActualItems(albumRepo)(
widget.account, album, items);
}
Album? _album;
var _sortedItems = <AlbumItem>[];
var _backingFiles = <File>[];

View file

@ -1,3 +1,4 @@
import 'package:intl/intl.dart';
import 'package:nc_photos/entity/album.dart';
import 'package:nc_photos/entity/album/cover_provider.dart';
import 'package:nc_photos/entity/album/item.dart';
@ -31,8 +32,13 @@ void main() {
},
};
expect(
Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album.fromJson(
json,
upgraderV1: null,
upgraderV2: null,
upgraderV3: null,
upgraderV4: null,
),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
@ -65,8 +71,13 @@ void main() {
},
};
expect(
Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album.fromJson(
json,
upgraderV1: null,
upgraderV2: null,
upgraderV3: null,
upgraderV4: null,
),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "album",
@ -117,8 +128,13 @@ void main() {
},
};
expect(
Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album.fromJson(
json,
upgraderV1: null,
upgraderV2: null,
upgraderV3: null,
upgraderV4: null,
),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
@ -165,8 +181,13 @@ void main() {
},
};
expect(
Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album.fromJson(
json,
upgraderV1: null,
upgraderV2: null,
upgraderV3: null,
upgraderV4: null,
),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
@ -208,8 +229,13 @@ void main() {
},
};
expect(
Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album.fromJson(
json,
upgraderV1: null,
upgraderV2: null,
upgraderV3: null,
upgraderV4: null,
),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
@ -248,8 +274,13 @@ void main() {
},
};
expect(
Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album.fromJson(
json,
upgraderV1: null,
upgraderV2: null,
upgraderV3: null,
upgraderV4: null,
),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
@ -287,8 +318,13 @@ void main() {
},
};
expect(
Album.fromJson(json,
upgraderV1: null, upgraderV2: null, upgraderV3: null),
Album.fromJson(
json,
upgraderV1: null,
upgraderV2: null,
upgraderV3: null,
upgraderV4: null,
),
Album(
lastUpdated: DateTime.utc(2020, 1, 2, 3, 4, 5, 678, 901),
name: "",
@ -942,5 +978,374 @@ void main() {
},
});
});
group("AlbumUpgraderV4", () {
test("Non AlbumFileItem", () {
final json = <String, dynamic>{
"version": 4,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [
<String, dynamic>{
"type": "label",
"content": <String, dynamic>{
"text": "123",
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
};
expect(AlbumUpgraderV4()(json), <String, dynamic>{
"version": 4,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [
<String, dynamic>{
"type": "label",
"content": <String, dynamic>{
"text": "123",
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
});
});
group("AlbumFileItem", () {
test("drop metadata", () {
final json = <String, dynamic>{
"version": 4,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
"metadata": <String, dynamic>{
"Make": "Super",
"Model": "A123",
},
},
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
};
expect(AlbumUpgraderV4()(json), <String, dynamic>{
"version": 4,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
});
});
test("lastModified as latestItemTime", () {
final json = <String, dynamic>{
"version": 4,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
"lastModified": "2020-01-02T03:04:05.678901Z",
},
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
};
expect(AlbumUpgraderV4()(json), <String, dynamic>{
"version": 4,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"latestItemTime": "2020-01-02T03:04:05.678901Z",
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
"lastModified": "2020-01-02T03:04:05.678901Z",
},
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{
"coverFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
"lastModified": "2020-01-02T03:04:05.678901Z",
},
},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
});
});
test("dateTimeOriginal as latestItemTime", () {
final json = <String, dynamic>{
"version": 4,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
"metadata": <String, dynamic>{
"exif": <String, dynamic>{
// convert 2020-01-02T03:04:05Z to local time
"DateTimeOriginal":
DateFormat("yyyy:MM:dd HH:mm:ss").format(
DateTime.utc(2020, 1, 2, 3, 4, 5)
.toLocal()),
},
},
},
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
};
expect(AlbumUpgraderV4()(json), <String, dynamic>{
"version": 4,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"latestItemTime": "2020-01-02T03:04:05.000Z",
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{
"coverFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
},
},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
});
});
test("overrideDateTime as latestItemTime", () {
final json = <String, dynamic>{
"version": 4,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
"overrideDateTime": "2020-01-02T03:04:05.678901Z",
},
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
};
expect(AlbumUpgraderV4()(json), <String, dynamic>{
"version": 4,
"lastUpdated": "2020-01-02T03:04:05.678901Z",
"provider": <String, dynamic>{
"type": "static",
"content": <String, dynamic>{
"latestItemTime": "2020-01-02T03:04:05.678901Z",
"items": [
<String, dynamic>{
"type": "file",
"content": <String, dynamic>{
"file": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
"overrideDateTime": "2020-01-02T03:04:05.678901Z",
},
},
},
],
},
},
"coverProvider": <String, dynamic>{
"type": "auto",
"content": <String, dynamic>{
"coverFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.jpg",
"overrideDateTime": "2020-01-02T03:04:05.678901Z",
},
},
},
"sortProvider": <String, dynamic>{
"type": "time",
"content": <String, dynamic>{
"isAscending": false,
},
},
"albumFile": <String, dynamic>{
"path": "remote.php/dav/files/admin/test1.json",
},
});
});
});
});
});
}