v0.4.0: scroll log, worker slots, disk skip, color gradient, parallel export
This commit is contained in:
@@ -1,10 +1,19 @@
|
||||
#ifndef PHOTOKIT_BRIDGE_H
|
||||
#define PHOTOKIT_BRIDGE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
int active;
|
||||
double progress;
|
||||
int64_t bytes_done;
|
||||
int64_t bytes_total;
|
||||
} export_progress_t;
|
||||
|
||||
int photos_request_access(void);
|
||||
|
||||
char *photos_list_albums_json(void);
|
||||
@@ -15,13 +24,15 @@ char *photos_export_preview_json(
|
||||
const char *asset_id,
|
||||
const char *output_dir,
|
||||
int target_size,
|
||||
int index
|
||||
int index,
|
||||
int slot_index
|
||||
);
|
||||
|
||||
char *photos_export_original_json(
|
||||
const char *asset_id,
|
||||
const char *output_dir,
|
||||
int index
|
||||
int index,
|
||||
int slot_index
|
||||
);
|
||||
|
||||
char *photos_list_tree_json(void);
|
||||
@@ -32,6 +43,10 @@ int photos_request_is_cancelled(void);
|
||||
|
||||
void photos_free_string(char *value);
|
||||
|
||||
export_progress_t *photos_get_progress_slots(void);
|
||||
int photos_get_progress_slot_count(void);
|
||||
void photos_reset_progress_slots(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+289
-32
@@ -1,10 +1,24 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
#import <objc/message.h>
|
||||
#import "photokit_bridge.h"
|
||||
|
||||
static volatile int photos_cancelled = 0;
|
||||
|
||||
#define PROGRESS_SLOT_COUNT 3
|
||||
static export_progress_t progress_slots[PROGRESS_SLOT_COUNT];
|
||||
|
||||
static void reset_slot(int slot_index) {
|
||||
if (slot_index >= 0 && slot_index < PROGRESS_SLOT_COUNT) {
|
||||
progress_slots[slot_index].active = 0;
|
||||
progress_slots[slot_index].progress = 0;
|
||||
progress_slots[slot_index].bytes_done = 0;
|
||||
progress_slots[slot_index].bytes_total = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static NSDictionary *make_error_dict(NSString *message) {
|
||||
return @{@"error": message};
|
||||
}
|
||||
@@ -192,6 +206,60 @@ char *photos_list_albums_json(void) {
|
||||
return json_from_object(@{@"albums": list});
|
||||
}
|
||||
|
||||
static NSString *resource_type_string(NSInteger type) {
|
||||
switch (type) {
|
||||
case 1: return @"photo";
|
||||
case 2: return @"video";
|
||||
case 3: return @"audio";
|
||||
case 4: return @"alternatePhoto";
|
||||
case 5: return @"fullSizePhoto";
|
||||
case 6: return @"fullSizeVideo";
|
||||
case 7: return @"adjustmentData";
|
||||
case 8: return @"adjustmentBasePhoto";
|
||||
case 9: return @"pairedVideo";
|
||||
case 10: return @"fullSizePairedVideo";
|
||||
case 11: return @"adjustmentBasePairedVideo";
|
||||
case 12: return @"adjustmentBaseVideo";
|
||||
default: return [NSString stringWithFormat:@"other(%ld)", (long)type];
|
||||
}
|
||||
}
|
||||
|
||||
static NSString *media_type_string(PHAssetMediaType type) {
|
||||
switch (type) {
|
||||
case PHAssetMediaTypeImage: return @"image";
|
||||
case PHAssetMediaTypeVideo: return @"video";
|
||||
case PHAssetMediaTypeAudio: return @"audio";
|
||||
default: return @"unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static NSString *iso8601_string(NSDate *date) {
|
||||
if (!date) return nil;
|
||||
static NSDateFormatter *fmt = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
fmt = [[NSDateFormatter alloc] init];
|
||||
fmt.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
|
||||
fmt.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZ";
|
||||
});
|
||||
return [fmt stringFromDate: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)];
|
||||
@@ -200,16 +268,17 @@ static NSString *asset_cloud_status_string(PHAsset *asset) {
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
}
|
||||
PHAssetResource *resource = [PHAssetResource assetResourcesForAsset:asset].firstObject;
|
||||
PHAssetResource *resource = nil;
|
||||
@try {
|
||||
resource = [PHAssetResource assetResourcesForAsset:asset].firstObject;
|
||||
} @catch (NSException *e) {
|
||||
resource = nil;
|
||||
}
|
||||
if (resource) {
|
||||
@try {
|
||||
id locallyAvailable = [resource performSelector:@selector(isLocallyAvailable)];
|
||||
if (locallyAvailable && [locallyAvailable boolValue]) {
|
||||
return @"local";
|
||||
}
|
||||
return @"cloud";
|
||||
} @catch (NSException *exception) {
|
||||
if (resource_is_locally_available(resource)) {
|
||||
return @"local";
|
||||
}
|
||||
return @"cloud";
|
||||
}
|
||||
return @"local";
|
||||
}
|
||||
@@ -236,16 +305,53 @@ char *photos_list_assets_json(const char *album_id) {
|
||||
PHAsset *asset = assets[i];
|
||||
NSString *lid = asset.localIdentifier;
|
||||
NSString *filename = nil;
|
||||
NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
|
||||
NSArray<PHAssetResource *> *resources = nil;
|
||||
@try {
|
||||
resources = [PHAssetResource assetResourcesForAsset:asset];
|
||||
} @catch (NSException *e) {
|
||||
resources = @[];
|
||||
}
|
||||
if (resources.count > 0) {
|
||||
filename = resources.firstObject.originalFilename;
|
||||
}
|
||||
NSString *cloudStatus = asset_cloud_status_string(asset);
|
||||
[list addObject:@{
|
||||
NSString *mediaTypeStr = media_type_string(asset.mediaType);
|
||||
|
||||
NSString *creationDateStr = iso8601_string(asset.creationDate);
|
||||
|
||||
NSMutableArray *resourcesList = [NSMutableArray arrayWithCapacity:resources.count];
|
||||
for (PHAssetResource *res in resources) {
|
||||
NSString *resTypeStr = resource_type_string(res.type);
|
||||
NSString *uti = res.uniformTypeIdentifier ?: @"";
|
||||
BOOL isLocal = resource_is_locally_available(res);
|
||||
[resourcesList addObject:@{
|
||||
@"type": resTypeStr,
|
||||
@"filename": res.originalFilename ?: @"",
|
||||
@"uti": uti,
|
||||
@"local": @(isLocal)
|
||||
}];
|
||||
}
|
||||
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:@{
|
||||
@"id": lid ?: @"",
|
||||
@"filename": filename ?: @"",
|
||||
@"cloud": cloudStatus
|
||||
@"cloud": cloudStatus,
|
||||
@"mediaType": mediaTypeStr,
|
||||
@"pixelWidth": @(asset.pixelWidth),
|
||||
@"pixelHeight": @(asset.pixelHeight),
|
||||
@"duration": @(asset.duration),
|
||||
@"isFavorite": @(asset.isFavorite)
|
||||
}];
|
||||
if (creationDateStr) {
|
||||
dict[@"creationDate"] = creationDateStr;
|
||||
}
|
||||
if (@available(macOS 12, *)) {
|
||||
dict[@"hasAdjustments"] = @(asset.hasAdjustments);
|
||||
}
|
||||
if (resourcesList.count > 0) {
|
||||
dict[@"resources"] = resourcesList;
|
||||
}
|
||||
[list addObject:dict];
|
||||
}
|
||||
|
||||
return json_from_object(@{@"assets": list, @"total": @(assets.count)});
|
||||
@@ -266,23 +372,32 @@ char *photos_list_tree_json(void) {
|
||||
}
|
||||
|
||||
|
||||
char *photos_export_preview_json(const char *asset_id, const char *output_dir, int target_size, int index) {
|
||||
if (!asset_id || !output_dir) return json_from_object(make_error_dict(@"asset_id and output_dir required"));
|
||||
#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) {
|
||||
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;
|
||||
|
||||
NSString *nsAssetId = [NSString stringWithUTF8String:asset_id];
|
||||
NSString *nsOutputDir = [NSString stringWithUTF8String:output_dir];
|
||||
if (!nsAssetId || !nsOutputDir) return json_from_object(make_error_dict(@"invalid UTF-8 in arguments"));
|
||||
if (!nsAssetId || !nsOutputDir) RETURN_PREVIEW(json_from_object(make_error_dict(@"invalid UTF-8 in arguments")));
|
||||
|
||||
if (!ensure_directory(nsOutputDir)) {
|
||||
return json_from_object(make_error_dict(@"failed to create output directory"));
|
||||
RETURN_PREVIEW(json_from_object(make_error_dict(@"failed to create output directory")));
|
||||
}
|
||||
|
||||
PHFetchResult<PHAsset *> *fetch = [PHAsset fetchAssetsWithLocalIdentifiers:@[nsAssetId] options:nil];
|
||||
if (fetch.count == 0) return json_from_object(make_error_dict(@"asset not found"));
|
||||
if (fetch.count == 0) RETURN_PREVIEW(json_from_object(make_error_dict(@"asset not found")));
|
||||
|
||||
PHAsset *asset = fetch.firstObject;
|
||||
|
||||
if (slot_index >= 0 && slot_index < PROGRESS_SLOT_COUNT) {
|
||||
progress_slots[slot_index].active = 1;
|
||||
progress_slots[slot_index].progress = 0;
|
||||
progress_slots[slot_index].bytes_done = 0;
|
||||
progress_slots[slot_index].bytes_total = 0;
|
||||
}
|
||||
|
||||
PHImageManager *im = [PHImageManager defaultManager];
|
||||
CGFloat scale = (CGFloat)target_size;
|
||||
CGSize targetCGSize = CGSizeMake(scale, scale);
|
||||
@@ -291,6 +406,11 @@ char *photos_export_preview_json(const char *asset_id, const char *output_dir, i
|
||||
imgOpts.resizeMode = PHImageRequestOptionsResizeModeExact;
|
||||
imgOpts.networkAccessAllowed = YES;
|
||||
imgOpts.synchronous = YES;
|
||||
imgOpts.progressHandler = ^(double progress, NSError * _Nullable error, BOOL * _Nonnull stop, NSDictionary * _Nullable info) {
|
||||
if (slot_index >= 0 && slot_index < PROGRESS_SLOT_COUNT) {
|
||||
progress_slots[slot_index].progress = progress;
|
||||
}
|
||||
};
|
||||
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
__block NSData *imageData = nil;
|
||||
@@ -313,21 +433,35 @@ char *photos_export_preview_json(const char *asset_id, const char *output_dir, i
|
||||
}];
|
||||
|
||||
if (!semaphore_wait_with_timeout(sem, 120)) {
|
||||
return 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(asset)}));
|
||||
}
|
||||
|
||||
if (!imageData) {
|
||||
return 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(asset)}));
|
||||
}
|
||||
|
||||
NSString *safe = sanitized_asset_identifier(asset.localIdentifier);
|
||||
NSString *filename = [NSString stringWithFormat:@"%04lu_%@.jpg", (unsigned long)index, safe];
|
||||
NSString *filepath = [nsOutputDir stringByAppendingPathComponent:filename];
|
||||
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
NSDictionary *existingAttrs = [fm attributesOfItemAtPath:filepath error:nil];
|
||||
if (existingAttrs) {
|
||||
NSNumber *existingSize = existingAttrs[NSFileSize];
|
||||
if (existingSize && existingSize.unsignedLongValue > 0) {
|
||||
RETURN_PREVIEW(json_from_object(@{
|
||||
@"filename": filename,
|
||||
@"size": existingSize,
|
||||
@"cloud": asset_cloud_status_string(asset),
|
||||
@"skipped": @YES
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
NSError *writeErr = nil;
|
||||
if (![imageData writeToFile:filepath options:NSDataWritingAtomic error:&writeErr]) {
|
||||
NSString *msg = writeErr ? writeErr.localizedDescription : @"unknown write error";
|
||||
return 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(asset)}));
|
||||
}
|
||||
|
||||
NSNumber *fileSize = nil;
|
||||
@@ -336,32 +470,47 @@ char *photos_export_preview_json(const char *asset_id, const char *output_dir, i
|
||||
fileSize = attrs[NSFileSize];
|
||||
}
|
||||
|
||||
return json_from_object(@{
|
||||
RETURN_PREVIEW(json_from_object(@{
|
||||
@"filename": filename,
|
||||
@"size": fileSize ?: @0,
|
||||
@"cloud": asset_cloud_status_string(asset)
|
||||
});
|
||||
}));
|
||||
}
|
||||
#undef RETURN_PREVIEW
|
||||
|
||||
char *photos_export_original_json(const char *asset_id, const char *output_dir, int index) {
|
||||
if (!asset_id || !output_dir) return json_from_object(make_error_dict(@"asset_id and output_dir required"));
|
||||
#define RETURN_ORIGINAL(x) do { reset_slot(slot_index); return (x); } while(0)
|
||||
|
||||
char *photos_export_original_json(const char *asset_id, const char *output_dir, int index, int slot_index) {
|
||||
if (!asset_id || !output_dir) RETURN_ORIGINAL(json_from_object(make_error_dict(@"asset_id and output_dir required")));
|
||||
|
||||
NSString *nsAssetId = [NSString stringWithUTF8String:asset_id];
|
||||
NSString *nsOutputDir = [NSString stringWithUTF8String:output_dir];
|
||||
if (!nsAssetId || !nsOutputDir) return json_from_object(make_error_dict(@"invalid UTF-8 in arguments"));
|
||||
if (!nsAssetId || !nsOutputDir) RETURN_ORIGINAL(json_from_object(make_error_dict(@"invalid UTF-8 in arguments")));
|
||||
|
||||
if (!ensure_directory(nsOutputDir)) {
|
||||
return json_from_object(make_error_dict(@"failed to create output directory"));
|
||||
RETURN_ORIGINAL(json_from_object(make_error_dict(@"failed to create output directory")));
|
||||
}
|
||||
|
||||
PHFetchResult<PHAsset *> *fetch = [PHAsset fetchAssetsWithLocalIdentifiers:@[nsAssetId] options:nil];
|
||||
if (fetch.count == 0) return json_from_object(make_error_dict(@"asset not found"));
|
||||
if (fetch.count == 0) RETURN_ORIGINAL(json_from_object(make_error_dict(@"asset not found")));
|
||||
|
||||
PHAsset *asset = fetch.firstObject;
|
||||
|
||||
NSArray<PHAssetResource *> *resources = [PHAssetResource assetResourcesForAsset:asset];
|
||||
if (slot_index >= 0 && slot_index < PROGRESS_SLOT_COUNT) {
|
||||
progress_slots[slot_index].active = 1;
|
||||
progress_slots[slot_index].progress = 0;
|
||||
progress_slots[slot_index].bytes_done = 0;
|
||||
progress_slots[slot_index].bytes_total = 0;
|
||||
}
|
||||
|
||||
NSArray<PHAssetResource *> *resources = nil;
|
||||
@try {
|
||||
resources = [PHAssetResource assetResourcesForAsset:asset];
|
||||
} @catch (NSException *e) {
|
||||
resources = @[];
|
||||
}
|
||||
if (resources.count == 0) {
|
||||
return 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(asset)}));
|
||||
}
|
||||
|
||||
PHAssetResource *resource = resources.firstObject;
|
||||
@@ -371,11 +520,38 @@ char *photos_export_original_json(const char *asset_id, const char *output_dir,
|
||||
}
|
||||
|
||||
NSString *filepath = unique_path_for_filename(nsOutputDir, filename, (NSUInteger)index);
|
||||
|
||||
NSFileManager *fm = [NSFileManager defaultManager];
|
||||
{
|
||||
NSString *checkPath = [nsOutputDir stringByAppendingPathComponent:filename];
|
||||
NSDictionary *existingAttrs = [fm attributesOfItemAtPath:checkPath error:nil];
|
||||
if (existingAttrs) {
|
||||
NSNumber *existingSize = existingAttrs[NSFileSize];
|
||||
if (existingSize && existingSize.unsignedLongValue > 0) {
|
||||
RETURN_ORIGINAL(json_from_object(@{
|
||||
@"filename": filename,
|
||||
@"size": existingSize,
|
||||
@"cloud": asset_cloud_status_string(asset),
|
||||
@"skipped": @YES
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSURL *fileURL = [NSURL fileURLWithPath:filepath];
|
||||
|
||||
if ([fm fileExistsAtPath:filepath]) {
|
||||
[fm removeItemAtPath:filepath error:nil];
|
||||
}
|
||||
|
||||
PHAssetResourceManager *rm = [PHAssetResourceManager defaultManager];
|
||||
PHAssetResourceRequestOptions *opts = [[PHAssetResourceRequestOptions alloc] init];
|
||||
opts.networkAccessAllowed = YES;
|
||||
opts.progressHandler = ^(double progress) {
|
||||
if (slot_index >= 0 && slot_index < PROGRESS_SLOT_COUNT) {
|
||||
progress_slots[slot_index].progress = progress;
|
||||
}
|
||||
};
|
||||
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
__block NSError *writeErr = nil;
|
||||
@@ -387,11 +563,79 @@ 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 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(asset)}));
|
||||
}
|
||||
|
||||
if (writeErr) {
|
||||
return json_from_object(@{@"error": [NSString stringWithFormat:@"write failed: %@", writeErr.localizedDescription], @"cloud": asset_cloud_status_string(asset)});
|
||||
PHImageRequestOptions *imgOpts = [[PHImageRequestOptions alloc] init];
|
||||
imgOpts.networkAccessAllowed = YES;
|
||||
imgOpts.synchronous = NO;
|
||||
|
||||
dispatch_semaphore_t sem2 = dispatch_semaphore_create(0);
|
||||
__block NSData *fallbackData = nil;
|
||||
__block NSError *fallbackErr = nil;
|
||||
__block NSString *fallbackUTI = nil;
|
||||
|
||||
[[PHImageManager defaultManager] requestImageDataAndOrientationForAsset:asset
|
||||
options:imgOpts
|
||||
resultHandler:^(NSData *imageData, NSString *dataUTI, CGImagePropertyOrientation orientation, NSDictionary *info) {
|
||||
if (info[PHImageErrorKey]) {
|
||||
fallbackErr = info[PHImageErrorKey];
|
||||
} else if (imageData) {
|
||||
fallbackData = imageData;
|
||||
fallbackUTI = dataUTI;
|
||||
} else {
|
||||
fallbackErr = [NSError errorWithDomain:@"photoscli" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"no data"}];
|
||||
}
|
||||
dispatch_semaphore_signal(sem2);
|
||||
}];
|
||||
|
||||
if (!semaphore_wait_with_timeout(sem2, 120)) {
|
||||
RETURN_ORIGINAL(json_from_object(@{@"error": @"timeout waiting for image data", @"cloud": asset_cloud_status_string(asset)}));
|
||||
}
|
||||
|
||||
if (fallbackErr || !fallbackData) {
|
||||
NSString *detail = writeErr.localizedDescription;
|
||||
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)}));
|
||||
}
|
||||
|
||||
NSString *ext = nil;
|
||||
if (fallbackUTI) {
|
||||
UTType *uti = [UTType typeWithIdentifier:fallbackUTI];
|
||||
if (uti && uti.preferredFilenameExtension) {
|
||||
ext = uti.preferredFilenameExtension;
|
||||
}
|
||||
}
|
||||
if (ext.length == 0) {
|
||||
ext = @"dng";
|
||||
}
|
||||
|
||||
NSString *baseFilename = [filename stringByDeletingPathExtension];
|
||||
if (baseFilename.length == 0) {
|
||||
baseFilename = sanitized_asset_identifier(asset.localIdentifier);
|
||||
}
|
||||
NSString *fallbackFilename = [NSString stringWithFormat:@"%@.%@", baseFilename, ext];
|
||||
NSString *fallbackPath = [nsOutputDir stringByAppendingPathComponent:fallbackFilename];
|
||||
|
||||
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)}));
|
||||
}
|
||||
|
||||
NSNumber *fileSize = nil;
|
||||
NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:fallbackPath error:nil];
|
||||
if (attrs) {
|
||||
fileSize = attrs[NSFileSize];
|
||||
}
|
||||
|
||||
RETURN_ORIGINAL(json_from_object(@{
|
||||
@"filename": fallbackFilename,
|
||||
@"size": fileSize ?: @0,
|
||||
@"cloud": asset_cloud_status_string(asset)
|
||||
}));
|
||||
}
|
||||
|
||||
NSString *writtenFilename = [filepath lastPathComponent];
|
||||
@@ -401,12 +645,13 @@ char *photos_export_original_json(const char *asset_id, const char *output_dir,
|
||||
fileSize = attrs[NSFileSize];
|
||||
}
|
||||
|
||||
return json_from_object(@{
|
||||
RETURN_ORIGINAL(json_from_object(@{
|
||||
@"filename": writtenFilename,
|
||||
@"size": fileSize ?: @0,
|
||||
@"cloud": asset_cloud_status_string(asset)
|
||||
});
|
||||
}));
|
||||
}
|
||||
#undef RETURN_ORIGINAL
|
||||
|
||||
void photos_free_string(char *value) {
|
||||
if (value) free(value);
|
||||
@@ -419,3 +664,15 @@ void photos_request_cancel(void) {
|
||||
int photos_request_is_cancelled(void) {
|
||||
return photos_cancelled;
|
||||
}
|
||||
|
||||
export_progress_t *photos_get_progress_slots(void) {
|
||||
return progress_slots;
|
||||
}
|
||||
|
||||
int photos_get_progress_slot_count(void) {
|
||||
return PROGRESS_SLOT_COUNT;
|
||||
}
|
||||
|
||||
void photos_reset_progress_slots(void) {
|
||||
memset(progress_slots, 0, sizeof(progress_slots));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "../bridge/photokit_bridge.h"
|
||||
|
||||
static export_progress_t stub_progress_slots[3];
|
||||
|
||||
static char *alloc_json(const char *s) {
|
||||
size_t len = strlen(s);
|
||||
@@ -46,18 +49,20 @@ 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) {
|
||||
char *photos_export_preview_json(const char *asset_id, const char *output_dir, int target_size, int index, int slot_index) {
|
||||
(void)asset_id;
|
||||
(void)output_dir;
|
||||
(void)target_size;
|
||||
(void)index;
|
||||
(void)slot_index;
|
||||
return maybe_alloc_json(stub_export_preview_json);
|
||||
}
|
||||
|
||||
char *photos_export_original_json(const char *asset_id, const char *output_dir, int index) {
|
||||
char *photos_export_original_json(const char *asset_id, const char *output_dir, int index, int slot_index) {
|
||||
(void)asset_id;
|
||||
(void)output_dir;
|
||||
(void)index;
|
||||
(void)slot_index;
|
||||
return maybe_alloc_json(stub_export_original_json);
|
||||
}
|
||||
|
||||
@@ -84,4 +89,16 @@ void photos_test_set_export_preview_json(const char *json) {
|
||||
|
||||
void photos_test_set_export_original_json(const char *json) {
|
||||
stub_export_original_json = json;
|
||||
}
|
||||
|
||||
export_progress_t *photos_get_progress_slots(void) {
|
||||
return stub_progress_slots;
|
||||
}
|
||||
|
||||
int photos_get_progress_slot_count(void) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
void photos_reset_progress_slots(void) {
|
||||
memset(stub_progress_slots, 0, sizeof(stub_progress_slots));
|
||||
}
|
||||
Reference in New Issue
Block a user