Created
January 20, 2024 00:26
Update all photo dates from filename. Useful for bulk import into iCloud from Google Photos.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// main.swift | |
// photo-redate | |
// | |
// Created by Pengcheng on 19.01.2024. | |
// | |
import Foundation | |
import Photos | |
import os | |
@main | |
struct App { | |
static func main() async throws { | |
if PHPhotoLibrary.authorizationStatus() != .authorized { | |
let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite) | |
print("Authorization status: \(status)") | |
} else { | |
print("Already authorized") | |
} | |
let logger = Logger(subsystem: "moe.jsteward.photo-redate", category: "default") | |
// format date | |
let dateFormatter = DateFormatter() | |
dateFormatter.dateFormat = "dd.MM.yyyy" | |
let date = dateFormatter.date(from: "13.01.2024")! | |
print("Filtering for photos with creationDate = \(date)") | |
// create date range; https://stackoverflow.com/a/71482203/5520728 | |
let calendar = Calendar.current | |
let start = calendar.startOfDay(for: date) | |
let end = calendar.date(byAdding: .day, value: 1, to: start)! | |
// get all photos created that day | |
let allPhotosOptions = PHFetchOptions() | |
allPhotosOptions.predicate = NSPredicate(format: "creationDate >= %@ AND creationDate < %@", argumentArray: [start, end]) | |
allPhotosOptions.includeHiddenAssets = true | |
let assets = PHAsset.fetchAssets(with: allPhotosOptions) | |
print("Got \(assets.count) photos") | |
var changes = [(PHAsset, Date)]() | |
assets.enumerateObjects { asset, idx, _ in | |
print("#\(idx): ", terminator: "") | |
let resources = PHAssetResource.assetResources(for: asset) | |
print("\(resources.count) resources, ", terminator: "") | |
if (resources.count > 1) { | |
// most likely not imported photo | |
print("skipping") | |
return | |
} | |
let fname = resources.first!.originalFilename | |
print("filename: \(fname), ", terminator: "") | |
var actualDate: Date | |
let imgDateFormatter = DateFormatter() | |
imgDateFormatter.dateFormat = "yyyyMMdd_HHmmss" | |
// most pictures in GMT+8 | |
imgDateFormatter.timeZone = TimeZone(secondsFromGMT: 8 * 3600) | |
let imgDateFormatter2 = DateFormatter() | |
imgDateFormatter2.dateFormat = "yyyy-MM-dd HH.mm.ss" | |
imgDateFormatter2.timeZone = TimeZone(secondsFromGMT: 8 * 3600) | |
// check filename formats | |
if let match = fname.firstMatch(of: /(?:wx_camera_|mmexport|microMsg.|da_|IMG_|)(\d{13}).*.(?:jpg|mp4|jpeg)/) { | |
// UNIX timestamp | |
let sinceEpoch = Double(match.output.1)! | |
actualDate = Date(timeIntervalSince1970: TimeInterval(sinceEpoch / 1000.0)) | |
} else if let match = fname.firstMatch(of: /(?:IMG_|)(\d{8}_\d{6}).*.(?:jpg|gif)/) { | |
actualDate = imgDateFormatter.date(from: String(match.output.1))! | |
} else if let match = fname.firstMatch(of: /(\d{4}-\d{2}-\d{2} \d{2}.\d{2}.\d{2}).*.jpg/) { | |
actualDate = imgDateFormatter2.date(from: String(match.output.1))! | |
} else { | |
print("unsupported format!") | |
logger.warning("Unsupported filename: \(fname)") | |
return | |
} | |
print("parsed actual date: \(actualDate)") | |
// enqueue for request | |
changes.append((asset, actualDate)) | |
} | |
// send metadata update requests | |
for (asset, date) in changes { | |
try await PHPhotoLibrary.shared().performChanges { | |
let req = PHAssetChangeRequest(for: asset) | |
req.creationDate = date | |
} | |
print("Updated photo on \(date)") | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment