v0.5.0: manifests, filters, logging, docs
This commit is contained in:
@@ -24,6 +24,7 @@ char *photos_export_preview_json(
|
||||
const char *asset_id,
|
||||
const char *output_dir,
|
||||
int target_size,
|
||||
int quality,
|
||||
int index,
|
||||
int slot_index
|
||||
);
|
||||
@@ -44,6 +45,7 @@ int photos_request_is_cancelled(void);
|
||||
void photos_free_string(char *value);
|
||||
|
||||
export_progress_t *photos_get_progress_slots(void);
|
||||
export_progress_t photos_get_progress_slot(export_progress_t *slots, int index);
|
||||
int photos_get_progress_slot_count(void);
|
||||
void photos_reset_progress_slots(void);
|
||||
|
||||
|
||||
+103
-69
@@ -7,7 +7,7 @@
|
||||
|
||||
static volatile int photos_cancelled = 0;
|
||||
|
||||
#define PROGRESS_SLOT_COUNT 3
|
||||
#define PROGRESS_SLOT_COUNT 16
|
||||
static export_progress_t progress_slots[PROGRESS_SLOT_COUNT];
|
||||
|
||||
static void reset_slot(int slot_index) {
|
||||
@@ -42,7 +42,7 @@ static NSDictionary *collection_to_dict(PHCollection *collection) {
|
||||
if ([collection isKindOfClass:[PHCollectionList class]]) {
|
||||
PHCollectionList *list = (PHCollectionList *)collection;
|
||||
PHFetchResult<PHCollection *> *children = [PHCollectionList fetchCollectionsInCollectionList:list
|
||||
options:nil];
|
||||
options:nil];
|
||||
NSMutableArray *childList = [NSMutableArray arrayWithCapacity:children.count];
|
||||
for (NSUInteger i = 0; i < children.count; i++) {
|
||||
NSDictionary *child = collection_to_dict(children[i]);
|
||||
@@ -107,7 +107,7 @@ static BOOL ensure_directory(NSString *outputDir) {
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
NSError *dirErr = nil;
|
||||
BOOL ok = [fm createDirectoryAtPath:outputDir withIntermediateDirectories:YES
|
||||
attributes:nil error:&dirErr];
|
||||
attributes:nil error:&dirErr];
|
||||
if (!ok && dirErr) {
|
||||
NSLog(@"ensure_directory failed: %@", dirErr);
|
||||
}
|
||||
@@ -151,7 +151,6 @@ static NSString *unique_path_for_filename(NSString *outputDir, NSString *filenam
|
||||
return [outputDir stringByAppendingPathComponent:prefixed];
|
||||
}
|
||||
|
||||
// Must stay in sync with sanitizePathComponent in cmd/photoscli/main.go
|
||||
static NSString *sanitized_path_component(NSString *name) {
|
||||
NSString *source = name ?: @"Untitled";
|
||||
NSMutableString *safe = [[source stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] mutableCopy];
|
||||
@@ -170,6 +169,85 @@ static NSString *sanitized_path_component(NSString *name) {
|
||||
return safe;
|
||||
}
|
||||
|
||||
static BOOL resource_is_locally_available(PHAssetResource *res) {
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
SEL sel = @selector(isLocallyAvailable);
|
||||
if ([res respondsToSelector:sel]) {
|
||||
return ((BOOL (*)(id, SEL))objc_msgSend)(res, sel);
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (NSException *e) {}
|
||||
return NO;
|
||||
}
|
||||
|
||||
static NSArray<PHAssetResource *> *safe_asset_resources(PHAsset *asset) {
|
||||
@try {
|
||||
@autoreleasepool {
|
||||
NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
|
||||
return resources ?: @[];
|
||||
}
|
||||
} @catch (NSException *e) {
|
||||
NSLog(@"photoscli: exception getting resources for asset %@: %@", asset.localIdentifier, e.reason);
|
||||
return @[];
|
||||
}
|
||||
}
|
||||
|
||||
static NSString *asset_cloud_status_string_safe(PHAsset *asset) {
|
||||
@try {
|
||||
@autoreleasepool {
|
||||
if (@available(macOS 11, *)) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
SEL sel = @selector(isCloudShared);
|
||||
if ([asset respondsToSelector:sel]) {
|
||||
BOOL isShared = ((BOOL (*)(id, SEL))objc_msgSend)(asset, sel);
|
||||
if (isShared) return @"cloud";
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
NSLog(@"photoscli: exception checking isCloudShared for %@: %@", asset.localIdentifier, exception.reason);
|
||||
}
|
||||
|
||||
PHCloudIdentifier *cloudId = nil;
|
||||
@try {
|
||||
@autoreleasepool {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
cloudId = [asset performSelector:@selector(cloudIdentifier)];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
cloudId = nil;
|
||||
}
|
||||
if (cloudId && ![cloudId isEqual:[NSNull null]]) {
|
||||
return @"cloud";
|
||||
}
|
||||
|
||||
BOOL isInCloud = NO;
|
||||
@try {
|
||||
@autoreleasepool {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
SEL sel = @selector(isInCloud);
|
||||
if ([asset respondsToSelector:sel]) {
|
||||
isInCloud = ((BOOL (*)(id, SEL))objc_msgSend)(asset, sel);
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
isInCloud = NO;
|
||||
}
|
||||
if (isInCloud) {
|
||||
return @"cloud";
|
||||
}
|
||||
|
||||
return @"local";
|
||||
}
|
||||
|
||||
int photos_request_access(void) {
|
||||
__block PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
|
||||
if (status == PHAuthorizationStatusNotDetermined) {
|
||||
@@ -247,42 +325,6 @@ static NSString *iso8601_string(NSDate *date) {
|
||||
|
||||
#import <objc/message.h>
|
||||
|
||||
static BOOL resource_is_locally_available(PHAssetResource *res) {
|
||||
@try {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
SEL sel = @selector(isLocallyAvailable);
|
||||
if ([res respondsToSelector:sel]) {
|
||||
return ((BOOL (*)(id, SEL))objc_msgSend)(res, sel);
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
} @catch (NSException *e) {}
|
||||
return NO;
|
||||
}
|
||||
|
||||
static NSString *asset_cloud_status_string(PHAsset *asset) {
|
||||
@try {
|
||||
id cloudId = [asset performSelector:@selector(cloudIdentifier)];
|
||||
if (cloudId && ![cloudId isEqual:[NSNull null]]) {
|
||||
return @"cloud";
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
}
|
||||
PHAssetResource *resource = nil;
|
||||
@try {
|
||||
resource = [PHAssetResource assetResourcesForAsset:asset].firstObject;
|
||||
} @catch (NSException *e) {
|
||||
resource = nil;
|
||||
}
|
||||
if (resource) {
|
||||
if (resource_is_locally_available(resource)) {
|
||||
return @"local";
|
||||
}
|
||||
return @"cloud";
|
||||
}
|
||||
return @"local";
|
||||
}
|
||||
|
||||
char *photos_list_assets_json(const char *album_id) {
|
||||
if (!album_id) return json_from_object(make_error_dict(@"album_id is required"));
|
||||
|
||||
@@ -305,16 +347,11 @@ char *photos_list_assets_json(const char *album_id) {
|
||||
PHAsset *asset = assets[i];
|
||||
NSString *lid = asset.localIdentifier;
|
||||
NSString *filename = nil;
|
||||
NSArray<PHAssetResource *> *resources = nil;
|
||||
@try {
|
||||
resources = [PHAssetResource assetResourcesForAsset:asset];
|
||||
} @catch (NSException *e) {
|
||||
resources = @[];
|
||||
}
|
||||
NSArray<PHAssetResource *> *resources = safe_asset_resources(asset);
|
||||
if (resources.count > 0) {
|
||||
filename = resources.firstObject.originalFilename;
|
||||
}
|
||||
NSString *cloudStatus = asset_cloud_status_string(asset);
|
||||
NSString *cloudStatus = asset_cloud_status_string_safe(asset);
|
||||
NSString *mediaTypeStr = media_type_string(asset.mediaType);
|
||||
|
||||
NSString *creationDateStr = iso8601_string(asset.creationDate);
|
||||
@@ -374,9 +411,10 @@ char *photos_list_tree_json(void) {
|
||||
|
||||
#define RETURN_PREVIEW(x) do { reset_slot(slot_index); return (x); } while(0)
|
||||
|
||||
char *photos_export_preview_json(const char *asset_id, const char *output_dir, int target_size, int index, int slot_index) {
|
||||
char *photos_export_preview_json(const char *asset_id, const char *output_dir, int target_size, int quality, int index, int slot_index) {
|
||||
if (!asset_id || !output_dir) RETURN_PREVIEW(json_from_object(make_error_dict(@"asset_id and output_dir required")));
|
||||
if (target_size <= 0) target_size = 1024;
|
||||
if (quality <= 0 || quality > 100) quality = 85;
|
||||
|
||||
NSString *nsAssetId = [NSString stringWithUTF8String:asset_id];
|
||||
NSString *nsOutputDir = [NSString stringWithUTF8String:output_dir];
|
||||
@@ -427,17 +465,18 @@ char *photos_export_preview_json(const char *asset_id, const char *output_dir, i
|
||||
return;
|
||||
}
|
||||
if (result) {
|
||||
imageData = nsimage_to_jpeg(result, 0.85);
|
||||
CGFloat compression = (CGFloat)quality / 100.0;
|
||||
imageData = nsimage_to_jpeg(result, compression);
|
||||
}
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
|
||||
if (!semaphore_wait_with_timeout(sem, 120)) {
|
||||
RETURN_PREVIEW(json_from_object(@{@"error": @"timeout waiting for image", @"cloud": asset_cloud_status_string(asset)}));
|
||||
RETURN_PREVIEW(json_from_object(@{@"error": @"timeout waiting for image", @"cloud": asset_cloud_status_string_safe(asset)}));
|
||||
}
|
||||
|
||||
if (!imageData) {
|
||||
RETURN_PREVIEW(json_from_object(@{@"error": @"failed to render image", @"cloud": asset_cloud_status_string(asset)}));
|
||||
RETURN_PREVIEW(json_from_object(@{@"error": @"failed to render image", @"cloud": asset_cloud_status_string_safe(asset)}));
|
||||
}
|
||||
|
||||
NSString *safe = sanitized_asset_identifier(asset.localIdentifier);
|
||||
@@ -452,7 +491,7 @@ char *photos_export_preview_json(const char *asset_id, const char *output_dir, i
|
||||
RETURN_PREVIEW(json_from_object(@{
|
||||
@"filename": filename,
|
||||
@"size": existingSize,
|
||||
@"cloud": asset_cloud_status_string(asset),
|
||||
@"cloud": asset_cloud_status_string_safe(asset),
|
||||
@"skipped": @YES
|
||||
}));
|
||||
}
|
||||
@@ -461,7 +500,7 @@ char *photos_export_preview_json(const char *asset_id, const char *output_dir, i
|
||||
NSError *writeErr = nil;
|
||||
if (![imageData writeToFile:filepath options:NSDataWritingAtomic error:&writeErr]) {
|
||||
NSString *msg = writeErr ? writeErr.localizedDescription : @"unknown write error";
|
||||
RETURN_PREVIEW(json_from_object(@{@"error": [NSString stringWithFormat:@"write failed: %@", msg], @"cloud": asset_cloud_status_string(asset)}));
|
||||
RETURN_PREVIEW(json_from_object(@{@"error": [NSString stringWithFormat:@"write failed: %@", msg], @"cloud": asset_cloud_status_string_safe(asset)}));
|
||||
}
|
||||
|
||||
NSNumber *fileSize = nil;
|
||||
@@ -473,7 +512,7 @@ char *photos_export_preview_json(const char *asset_id, const char *output_dir, i
|
||||
RETURN_PREVIEW(json_from_object(@{
|
||||
@"filename": filename,
|
||||
@"size": fileSize ?: @0,
|
||||
@"cloud": asset_cloud_status_string(asset)
|
||||
@"cloud": asset_cloud_status_string_safe(asset)
|
||||
}));
|
||||
}
|
||||
#undef RETURN_PREVIEW
|
||||
@@ -503,14 +542,9 @@ char *photos_export_original_json(const char *asset_id, const char *output_dir,
|
||||
progress_slots[slot_index].bytes_total = 0;
|
||||
}
|
||||
|
||||
NSArray<PHAssetResource *> *resources = nil;
|
||||
@try {
|
||||
resources = [PHAssetResource assetResourcesForAsset:asset];
|
||||
} @catch (NSException *e) {
|
||||
resources = @[];
|
||||
}
|
||||
NSArray<PHAssetResource *> *resources = safe_asset_resources(asset);
|
||||
if (resources.count == 0) {
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": @"no resources", @"cloud": asset_cloud_status_string(asset)}));
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": @"no resources", @"cloud": asset_cloud_status_string_safe(asset)}));
|
||||
}
|
||||
|
||||
PHAssetResource *resource = resources.firstObject;
|
||||
@@ -531,7 +565,7 @@ char *photos_export_original_json(const char *asset_id, const char *output_dir,
|
||||
RETURN_ORIGINAL(json_from_object(@{
|
||||
@"filename": filename,
|
||||
@"size": existingSize,
|
||||
@"cloud": asset_cloud_status_string(asset),
|
||||
@"cloud": asset_cloud_status_string_safe(asset),
|
||||
@"skipped": @YES
|
||||
}));
|
||||
}
|
||||
@@ -563,7 +597,7 @@ char *photos_export_original_json(const char *asset_id, const char *output_dir,
|
||||
dispatch_semaphore_signal(sem);
|
||||
}];
|
||||
if (!semaphore_wait_with_timeout(sem, 120)) {
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": @"timeout waiting for resource", @"cloud": asset_cloud_status_string(asset)}));
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": @"timeout waiting for resource", @"cloud": asset_cloud_status_string_safe(asset)}));
|
||||
}
|
||||
|
||||
if (writeErr) {
|
||||
@@ -591,7 +625,7 @@ char *photos_export_original_json(const char *asset_id, const char *output_dir,
|
||||
}];
|
||||
|
||||
if (!semaphore_wait_with_timeout(sem2, 120)) {
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": @"timeout waiting for image data", @"cloud": asset_cloud_status_string(asset)}));
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": @"timeout waiting for image data", @"cloud": asset_cloud_status_string_safe(asset)}));
|
||||
}
|
||||
|
||||
if (fallbackErr || !fallbackData) {
|
||||
@@ -599,7 +633,7 @@ char *photos_export_original_json(const char *asset_id, const char *output_dir,
|
||||
if (fallbackErr) {
|
||||
detail = [NSString stringWithFormat:@"%@; fallback: %@", detail, fallbackErr.localizedDescription];
|
||||
}
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": [NSString stringWithFormat:@"write failed: %@", detail], @"cloud": asset_cloud_status_string(asset)}));
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": [NSString stringWithFormat:@"write failed: %@", detail], @"cloud": asset_cloud_status_string_safe(asset)}));
|
||||
}
|
||||
|
||||
NSString *ext = nil;
|
||||
@@ -622,7 +656,7 @@ char *photos_export_original_json(const char *asset_id, const char *output_dir,
|
||||
|
||||
NSError *writeFallbackErr = nil;
|
||||
if (![fallbackData writeToFile:fallbackPath options:NSDataWritingAtomic error:&writeFallbackErr]) {
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": [NSString stringWithFormat:@"write failed: %@", writeFallbackErr.localizedDescription], @"cloud": asset_cloud_status_string(asset)}));
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": [NSString stringWithFormat:@"write failed: %@", writeFallbackErr.localizedDescription], @"cloud": asset_cloud_status_string_safe(asset)}));
|
||||
}
|
||||
|
||||
NSNumber *fileSize = nil;
|
||||
@@ -634,7 +668,7 @@ char *photos_export_original_json(const char *asset_id, const char *output_dir,
|
||||
RETURN_ORIGINAL(json_from_object(@{
|
||||
@"filename": fallbackFilename,
|
||||
@"size": fileSize ?: @0,
|
||||
@"cloud": asset_cloud_status_string(asset)
|
||||
@"cloud": asset_cloud_status_string_safe(asset)
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -648,7 +682,7 @@ char *photos_export_original_json(const char *asset_id, const char *output_dir,
|
||||
RETURN_ORIGINAL(json_from_object(@{
|
||||
@"filename": writtenFilename,
|
||||
@"size": fileSize ?: @0,
|
||||
@"cloud": asset_cloud_status_string(asset)
|
||||
@"cloud": asset_cloud_status_string_safe(asset)
|
||||
}));
|
||||
}
|
||||
#undef RETURN_ORIGINAL
|
||||
@@ -675,4 +709,4 @@ int photos_get_progress_slot_count(void) {
|
||||
|
||||
void photos_reset_progress_slots(void) {
|
||||
memset(progress_slots, 0, sizeof(progress_slots));
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
#include <string.h>
|
||||
#include "../bridge/photokit_bridge.h"
|
||||
|
||||
static export_progress_t stub_progress_slots[3];
|
||||
static export_progress_t stub_progress_slots[16];
|
||||
static int stub_progress_slot_count = 16;
|
||||
static int stub_progress_slots_null = 0;
|
||||
|
||||
static char *alloc_json(const char *s) {
|
||||
size_t len = strlen(s);
|
||||
@@ -49,10 +51,11 @@ char *photos_list_assets_json(const char *album_id) {
|
||||
return alloc_json(stub_assets_json);
|
||||
}
|
||||
|
||||
char *photos_export_preview_json(const char *asset_id, const char *output_dir, int target_size, int index, int slot_index) {
|
||||
char *photos_export_preview_json(const char *asset_id, const char *output_dir, int target_size, int quality, int index, int slot_index) {
|
||||
(void)asset_id;
|
||||
(void)output_dir;
|
||||
(void)target_size;
|
||||
(void)quality;
|
||||
(void)index;
|
||||
(void)slot_index;
|
||||
return maybe_alloc_json(stub_export_preview_json);
|
||||
@@ -87,18 +90,43 @@ void photos_test_set_export_preview_json(const char *json) {
|
||||
stub_export_preview_json = json;
|
||||
}
|
||||
|
||||
void photos_test_set_export_preview_json_null(void) {
|
||||
stub_export_preview_json = NULL;
|
||||
}
|
||||
|
||||
void photos_test_set_export_original_json(const char *json) {
|
||||
stub_export_original_json = json;
|
||||
}
|
||||
|
||||
void photos_test_set_export_original_json_null(void) {
|
||||
stub_export_original_json = NULL;
|
||||
}
|
||||
|
||||
export_progress_t *photos_get_progress_slots(void) {
|
||||
if (stub_progress_slots_null) return NULL;
|
||||
return stub_progress_slots;
|
||||
}
|
||||
|
||||
export_progress_t photos_get_progress_slot(export_progress_t *slots, int index) {
|
||||
export_progress_t result = {0, 0.0, 0, 0};
|
||||
if (index >= 0 && index < 16) {
|
||||
result = slots[index];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int photos_get_progress_slot_count(void) {
|
||||
return 3;
|
||||
return stub_progress_slot_count;
|
||||
}
|
||||
|
||||
void photos_reset_progress_slots(void) {
|
||||
memset(stub_progress_slots, 0, sizeof(stub_progress_slots));
|
||||
}
|
||||
}
|
||||
|
||||
void photos_test_set_progress_slot_count(int count) {
|
||||
stub_progress_slot_count = count;
|
||||
}
|
||||
|
||||
void photos_test_set_progress_slots_null(int val) {
|
||||
stub_progress_slots_null = val;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user