Merge remote-tracking branch 'origin/main' into auth-fixes

This commit is contained in:
Prateek Sunal
2024-07-17 15:41:40 +05:30
9 changed files with 1658 additions and 76 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@ import "package:photos/models/location/location.dart";
class FFProbeProps {
Map<String, dynamic>? prodData;
Location? location;
DateTime? creationTime;
DateTime? creationTimeUTC;
String? bitrate;
String? majorBrand;
String? fps;
@@ -45,7 +45,7 @@ class FFProbeProps {
return buffer.toString();
}
static fromJson(Map<dynamic, dynamic>? json) {
static parseData(Map<dynamic, dynamic>? json) {
final Map<String, dynamic> parsedData = {};
final FFProbeProps result = FFProbeProps();
@@ -76,6 +76,7 @@ class FFProbeProps {
break;
case FFProbeKeys.creationTime:
parsedData[stringKey] = _formatDate(json[key] ?? "");
result.creationTimeUTC = _getUTCDateTime(json[key] ?? "");
break;
case FFProbeKeys.durationMicros:
parsedData[stringKey] = formatPreciseDuration(
@@ -112,7 +113,20 @@ class FFProbeProps {
}
// iterate through the streams
final List<dynamic> streams = json["streams"];
final List<dynamic> newStreams = [];
final Map<String, dynamic> metadata = {};
for (final stream in streams) {
if (stream['type'] == 'metadata') {
for (final key in stream.keys) {
if (key == FFProbeKeys.frameCount && stream[key]?.toString() == "1") {
continue;
}
metadata[key] = stream[key];
}
metadata.remove(FFProbeKeys.index);
} else {
newStreams.add(stream);
}
for (final key in stream.keys) {
if (key == FFProbeKeys.rFrameRate) {
result.fps = _formatFPS(stream[key]);
@@ -126,6 +140,10 @@ class FFProbeProps {
}
}
}
if (metadata.isNotEmpty) {
newStreams.add(metadata);
}
parsedData["streams"] = newStreams;
result.prodData = parsedData;
return result;
}
@@ -171,6 +189,16 @@ class FFProbeProps {
return formatDateTime(newDate, 'en_US', false);
}
static DateTime? _getUTCDateTime(String value) {
final dateInUtc = DateTime.tryParse(value);
if (dateInUtc == null) return null;
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
if (dateInUtc == epoch) return null;
return DateTime.fromMicrosecondsSinceEpoch(
dateInUtc.microsecondsSinceEpoch,
);
}
// input example: '00:00:05.408000000' or '5.408000'
static Duration? _parseDuration(String? value) {
if (value == null) return null;

View File

@@ -3,7 +3,6 @@ import "dart:developer";
import "dart:io";
import "package:exif/exif.dart";
import "package:ffmpeg_kit_flutter_min/ffprobe_kit.dart";
import "package:flutter/foundation.dart";
import "package:flutter/material.dart";
import "package:logging/logging.dart";
@@ -15,6 +14,7 @@ import "package:photos/models/ffmpeg/ffprobe_props.dart";
import "package:photos/models/file/extensions/file_props.dart";
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file/file_type.dart';
import "package:photos/models/location/location.dart";
import "package:photos/models/metadata/file_magic.dart";
import "package:photos/service_locator.dart";
import "package:photos/services/file_magic_service.dart";
@@ -33,7 +33,6 @@ import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart";
import "package:photos/ui/viewer/file_details/location_tags_widget.dart";
import "package:photos/ui/viewer/file_details/video_exif_item.dart";
import "package:photos/utils/exif_util.dart";
import "package:photos/utils/ffprobe_util.dart";
import "package:photos/utils/file_util.dart";
import "package:photos/utils/local_settings.dart";
@@ -89,7 +88,15 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
_exifNotifier.addListener(() {
if (_exifNotifier.value != null && !widget.file.hasLocation) {
_updateLocationFromExif(_exifNotifier.value!).ignore();
_updateLocationFromExif(locationFromExif(_exifNotifier.value!))
.ignore();
}
});
_videoMetadataNotifier.addListener(() {
if (_videoMetadataNotifier.value?.location != null &&
!widget.file.hasLocation) {
_updateLocationFromExif(_videoMetadataNotifier.value?.location)
.ignore();
}
});
@@ -117,21 +124,11 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
Future<void> getMediaInfo() async {
final File? originFile = await getFile(widget.file, isOrigin: true);
if (originFile == null) return;
final session = await FFprobeKit.getMediaInformation(originFile.path);
final mediaInfo = session.getMediaInformation();
if (mediaInfo == null) {
final failStackTrace = await session.getFailStackTrace();
final output = await session.getOutput();
_logger.severe(
'failed to get video metadata failStackTrace=$failStackTrace, output=$output',
);
return;
}
final properties = await FFProbeUtil.getProperties(mediaInfo);
final properties = await getVideoPropsAsync(originFile);
_videoMetadataNotifier.value = properties;
if (kDebugMode) {
log("videoCustomProps ${properties.toString()}");
log("PropData ${properties.prodData.toString()}");
log("PropData ${properties?.prodData.toString()}");
}
setState(() {});
}
@@ -343,14 +340,13 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
//This code is for updating the location of files in which location data is
//missing and the EXIF has location data. This is only happens for a
//certain specific minority of devices.
Future<void> _updateLocationFromExif(Map<String, IfdTag> exif) async {
Future<void> _updateLocationFromExif(Location? locationDataFromExif) async {
// If the file is not uploaded or the file is not owned by the current user
// then we don't need to update the location.
if (!widget.file.isUploaded || widget.file.ownerID! != _currentUserID) {
return;
}
try {
final locationDataFromExif = locationFromExif(exif);
if (locationDataFromExif?.latitude != null &&
locationDataFromExif?.longitude != null) {
widget.file.location = locationDataFromExif;

View File

@@ -1,8 +1,12 @@
import "dart:io";
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file/file_type.dart';
import "package:photos/ui/viewer/file/video_widget.dart";
import "package:photos/ui/viewer/file/video_widget_new.dart";
import "package:photos/ui/viewer/file/zoomable_live_image_new.dart";
class FileWidget extends StatelessWidget {
@@ -38,7 +42,16 @@ class FileWidget extends StatelessWidget {
key: key ?? ValueKey(fileKey),
);
} else if (file.fileType == FileType.video) {
return VideoWidget(
// use old video widget on iOS simulator as the new one crashes while
// playing certain videos on iOS simulator
if (kDebugMode && Platform.isIOS) {
return VideoWidget(
file,
tagPrefix: tagPrefix,
playbackCallback: playbackCallback,
);
}
return VideoWidgetNew(
file,
tagPrefix: tagPrefix,
playbackCallback: playbackCallback,

View File

@@ -25,12 +25,17 @@ class VideoExifDialog extends StatelessWidget {
}
Widget _buildSection(BuildContext context, String title, Widget content) {
return ExpansionTile(
initiallyExpanded: true,
title: Text(title, style: getEnteTextTheme(context).largeFaint),
childrenPadding: const EdgeInsets.symmetric(vertical: 2),
tilePadding: const EdgeInsets.symmetric(vertical: 4),
children: [content],
return Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
initiallyExpanded: false,
title: Text(title, style: getEnteTextTheme(context).largeFaint),
childrenPadding: EdgeInsets.zero, // Remove padding around children
tilePadding: EdgeInsets.zero,
collapsedShape: const Border(), // Remove border when collapsed
shape: const Border(),
children: [content],
),
);
}
@@ -61,6 +66,9 @@ class VideoExifDialog extends StatelessWidget {
for (final key in stream.keys) {
final dynamic value = stream[key];
if (value is List) {
continue;
}
// print type of value
if (value is int ||
value is double ||

View File

@@ -1,12 +1,20 @@
import "dart:async";
import "dart:developer";
import "dart:io";
import "package:computer/computer.dart";
import 'package:exif/exif.dart';
import "package:ffmpeg_kit_flutter_min/ffprobe_kit.dart";
import "package:ffmpeg_kit_flutter_min/media_information.dart";
import "package:ffmpeg_kit_flutter_min/media_information_session.dart";
import "package:flutter/foundation.dart";
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import "package:photos/models/ffmpeg/ffprobe_props.dart";
import 'package:photos/models/file/file.dart';
import "package:photos/models/location/location.dart";
import "package:photos/services/location_service.dart";
import "package:photos/utils/ffprobe_util.dart";
import 'package:photos/utils/file_util.dart';
const kDateTimeOriginal = "EXIF DateTimeOriginal";
@@ -48,6 +56,52 @@ Future<Map<String, IfdTag>?> getExifFromSourceFile(File originFile) async {
}
}
Future<FFProbeProps?> getVideoPropsAsync(File originalFile) async {
try {
final Map<int, StringBuffer> logs = {};
final completer = Completer<MediaInformation?>();
final session = await FFprobeKit.getMediaInformationAsync(
originalFile.path,
(MediaInformationSession session) async {
// This callback is called when the session is complete
final mediaInfo = session.getMediaInformation();
if (mediaInfo == null) {
_logger.warning("Failed to get video metadata");
final failStackTrace = await session.getFailStackTrace();
final output = await session.getOutput();
_logger.warning(
'Failed to get video metadata. failStackTrace=$failStackTrace, output=$output',
);
}
completer.complete(mediaInfo);
},
(log) {
// put log messages into a map
logs.putIfAbsent(log.getSessionId(), () => StringBuffer());
logs[log.getSessionId()]!.write(log.getMessage());
},
);
// Wait for the session to complete
await session.getReturnCode();
final mediaInfo = await completer.future;
if (kDebugMode) {
logs.forEach((key, value) {
log("log for session $key: $value", name: "FFprobeKit");
});
}
if (mediaInfo == null) {
return null;
}
final properties = await FFProbeUtil.getProperties(mediaInfo);
return properties;
} catch (e, s) {
_logger.severe("Failed to getVideoProps", e, s);
return null;
}
}
Future<DateTime?> getCreationTimeFromEXIF(
File? file,
Map<String, IfdTag>? exifData,

View File

@@ -17,7 +17,7 @@ class FFProbeUtil {
final properties = await getMetadata(mediaInformation);
try {
return FFProbeProps.fromJson(properties);
return FFProbeProps.parseData(properties);
} catch (e, stackTrace) {
_logger.severe(
"Error parsing FFProbe properties: $properties",

View File

@@ -15,12 +15,15 @@ import 'package:photo_manager/photo_manager.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/errors.dart';
import "package:photos/models/ffmpeg/ffprobe_props.dart";
import "package:photos/models/file/extensions/file_props.dart";
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file/file_type.dart';
import "package:photos/models/location/location.dart";
import "package:photos/models/metadata/file_magic.dart";
import "package:photos/services/file_magic_service.dart";
import 'package:photos/utils/crypto_util.dart';
import "package:photos/utils/exif_util.dart";
import 'package:photos/utils/file_util.dart';
import "package:uuid/uuid.dart";
import 'package:video_thumbnail/video_thumbnail.dart';
@@ -110,7 +113,7 @@ Future<MediaUploadData> _getMediaUploadDataFromAssetFile(EnteFile file) async {
);
}
// h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
await _decorateEnteFileData(file, asset);
await _decorateEnteFileData(file, asset, sourceFile);
fileHash = CryptoUtil.bin2base64(await CryptoUtil.getHash(sourceFile));
if (file.fileType == FileType.livePhoto && Platform.isIOS) {
@@ -266,7 +269,11 @@ void _assertFileType(AssetEntity asset, EnteFile file) {
);
}
Future<void> _decorateEnteFileData(EnteFile file, AssetEntity asset) async {
Future<void> _decorateEnteFileData(
EnteFile file,
AssetEntity asset,
File sourceFile,
) async {
// h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads
if (file.location == null ||
(file.location!.latitude == 0 && file.location!.longitude == 0)) {
@@ -274,7 +281,12 @@ Future<void> _decorateEnteFileData(EnteFile file, AssetEntity asset) async {
file.location =
Location(latitude: latLong.latitude, longitude: latLong.longitude);
}
if (!file.hasLocation && file.isVideo && Platform.isAndroid) {
final FFProbeProps? props = await getVideoPropsAsync(sourceFile);
if (props != null && props.location != null) {
file.location = props.location;
}
}
if (file.title == null || file.title!.isEmpty) {
_logger.warning("Title was missing ${file.tag}");
file.title = await asset.titleAsync;

View File

@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.9.8+908
version: 0.9.9+909
publish_to: none
environment: