Skip to content

Instantly share code, notes, and snippets.

@KireinaHoro
Created January 20, 2024 00:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save KireinaHoro/59495095680d80a0777d658c212e7363 to your computer and use it in GitHub Desktop.
Save KireinaHoro/59495095680d80a0777d658c212e7363 to your computer and use it in GitHub Desktop.
Update all photo dates from filename. Useful for bulk import into iCloud from Google Photos.
//
// 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