npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chronicle.app/apple-photos

v0.5.0

Published

Chronicle plugin for your Photos library: what each photo and video is, who is in it, what you did with it, and what was shared with you in Messages

Readme

@chronicle.app/apple-photos

Your Photos library on a Mac: each photo and video, who you said is in it, and what you did with it, and the photos shared with you in Messages, with who sent them.

Usage

chronicle extract apple-photos
chronicle extract apple-photos activity --since 2026-01-01
chronicle extract apple-photos photos activity favorites shared-with-you
chronicle extract apple-photos --input "/path/to/Photos Library.photoslibrary"

The library is ~/Pictures/Photos Library.photoslibrary, and the plugin reads its database/Photos.sqlite in place, read-only, with built-in node:sqlite, while Photos runs. Your terminal needs Full Disk Access.

It reads what Photos knows about each photo, not the photo itself. Most originals in an iCloud library aren't on the Mac, so no file is read or downloaded, and no record has a contentPath.

| Record type | Default | What it is | | ----------------- | ------- | --------------------------------------------------------------------------------------------------- | | photos | yes | One record for each photo and video in your library | | activity | no | Views, shares, edits, caption and title changes, likes and unlikes, and adds to albums | | favorites | no | Your favorites that have no date, one like each | | shared-with-you | no | The photos and videos shared with you in Messages, with who sent them, from --syndication (below) |

Each kind comes out newest first, and --since and --until bound when each record happened.

shared-with-you reads the library Photos keeps of what was shared with you in Messages, ~/Library/Photos/Libraries/Syndication.photoslibrary (--syndication), and Messages' ~/Library/Messages/chat.db (--messages) for who sent each one and the chat it came in.

Schema

| Photos thing | Chronicle node | Key | Properties | | ------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Taking a photo or video | PhotographAction, RecordAction, or ScreenshotAction | @type, source, result.sourceId (the work it made) | agent (you), timestamp (when it was taken), instrument (the kind of camera), location, result (the work) | | Saving one from an app | AddAction | @type, source, object.sourceId (the work it added) | agent (you), timestamp (when it was added), instrument (the app), object (the work, with its location) | | The photo or video, as a work | StillImage or MovingImage | @type, source (apple-photos), sourceId (iCloud's ID for it, ZCLOUDASSETGUID) | name (its title), caption, tags (its keywords), depicts (the people you named in it), isPartOf (its albums), sourceFormat (the camera's mode it was taken in), rendition (each of its files) | | Each of its files | ImageObject or VideoObject | @type, source (apple-photos), sourceId (iCloud's fingerprint of the file, ZINTERNALRESOURCE.ZFINGERPRINT) | handle (its original file name), contentSize (its size in bytes), width, height, duration, mimeType, exifData (a photo's lens and exposure) | | A person you named | Person | @type, source (apple-photos), sourceId (Photos' ID for them, ZPERSONUUID) | name | | An album or a folder | Collection | @type, source (apple-photos), sourceId (iCloud's ID for it, ZCLOUDGUID) | name, isPartOf (its folder) | | The kind of camera | ProductModel | @type, source (exif), name (the make and model its files name, such as Apple iPhone 14 Pro) | | The app that saved it | SoftwareApplication | @type, source (apple-bundle-id), handle (its bundle identifier) | name | | Something you did with a photo | ViewAction, ShareAction, UpdateAction, LikeAction, UnlikeAction, or AddAction | @type, source, object.sourceId, timestamp, and for an AddAction target.sourceId | agent (you), object (the work, by key), target (the album, for an AddAction) | | A favorite with no date | LikeAction | @type, source, object.sourceId | agent (you), object (the work, by key), and no timestamp | | A photo shared in Messages | ShareAction | @type, source, object.sourceId (the work) | agent (who sent it), timestamp (when it was sent), object (the work, keyed by Messages' ID for the attachment, at_0_…, with depicts and location, and as its rendition the attachment as the imessage plugin keys it), target (in a group chat, the group) | | Who sent it | Person | @type, source (apple-photos), handle (their phone number or email address in Messages) | sameAs the participant as the imessage plugin keys them: an Agent keyed by source (icloud for iMessage, phone for SMS) and handle | | The group chat it was shared in | Channel | @type, source (imessage), sourceId (Apple's ID the group started with) | sameAs the group's ID now, when Messages replaced it, keyed the same way, as the imessage plugin keys the group | | You | Person | @type, source (icloud), sourceId (your account's DSID) | handle (your Apple Account email); sameAs @me |

SHAPES.md sketches the output, generated from the tests.

Decisions

What you did to make each photo. A photo from a camera is a PhotographAction and a video a RecordAction, with the work as its result, dated when it was taken. A screenshot is a ScreenshotAction, and a screen recording a RecordAction. A Live Photo taken during a FaceTime call is a PhotographAction with FaceTime as its instrument. Anything else an app added, such as a photo saved from WhatsApp, Instagram, or Messages, or AirDropped, is an AddAction with the work as its object, dated when it was added, and the app as its instrument when Photos names one.

A photo is a work, held in files. Each photo is a StillImage and each video a MovingImage: what it shows, with what you said about it, apart from the files that hold it. Each of its original files is a rendition of it, an ImageObject or a VideoObject. Most have one. A Live Photo has two: its image and its clip, a QuickTime movie with a length of its own. A file's size in bytes, dimensions, type, length, name, and EXIF are the file's, and the caption, people, keywords, albums, and the camera's mode are the work's. Only originals are read (ZINTERNALRESOURCE, version 0 with no recipe): an edited version, a preview, and an edit's data are left out. A file with no fingerprint, such as one in a library that isn't in iCloud, has no rendition, so its work has no size or type.

Where it was taken. A photo you took was taken where you were, so its position is the action's location. A photo an app saved was taken by someone, somewhere, before you saved it, so its position is the work's own location, not the save's. A Shared with You photo's is the photo's too.

  • The test for a camera photo is strict: the Camera app added it, or no app did, and its file names a camera. A photo of yours saved from Instagram is a save, which the Instagram plugin can claim.
  • Screenshots and screen recordings are told by their kind first, since many have no app.
  • A photo with no app and no camera data is a save. Most have no date in their file, and the video among them is mostly what Messages compresses to.

When it was taken is Photos' date. The date is ZDATECREATED, a UTC instant, exactly as Photos has it, including a date you changed in Photos. A photo whose file has no date has the date Photos gave it, usually when the file was saved.

Keys are iCloud's IDs. A photo's work is keyed by iCloud's ID for it (ZCLOUDASSETGUID), an album by its own (ZCLOUDGUID), and a person by Photos' ID for them (ZPERSONUUID). They stay the same when the library is rebuilt from iCloud and on every Mac on the same library, while the library's own IDs (ZUUID) don't. A photo that isn't in iCloud is keyed by the library's ID. A file is keyed by iCloud's fingerprint of it, which is the same for every copy of the file, so a file in two assets, such as one imported twice, is one node. An action is keyed through the work it made or acted on, so no two nodes share an ID: the work, its files, and what you did with it are told apart by their keys, not only by their types.

A photo links into Photos by its key. The plugin declares a deepLinks template for StillImage and MovingImage, photos://asset?identifier=<key>&cloud-identifier=<key>:001, which opens Photos with the photo selected. Photos finds a photo by iCloud's ID in cloud-identifier, with the :001 it gives every photo's ID, and by the library's ID in identifier, so one template covers a photo that isn't in iCloud too. Photos doesn't document the link. A Shared with You photo is keyed by Messages' ID, which Photos doesn't link by, so its link doesn't open it.

The kind of camera, not which camera. A camera photo's instrument is a ProductModel named by the make and model its file gives, such as Apple iPhone 14 Pro, keyed by its type and that name in the exif namespace, since it has no ID, so other sources reading the same files name the same one. A name isn't an ID the way Apple's model identifier is, so the key keeps its type, and a lens or a device of the same name stays apart from it. Photos can't tell two phones of one model apart, so there's no Device.

People you named in it. A photo depicts each person whose face in it you named: on its own, in a group of faces Photos showed you, or in iPhoto or Aperture, or a face you added yourself. Photos records how each face came to be its person's (ZDETECTEDFACE.ZNAMESOURCE, with ZMANUAL for a face you added), and most are its own matches, which are left out. A person merged into another is the one they were merged into. Pets, people you haven't named, and where faces are in the photo are left out. Few named people link to a contact, so they don't merge with contacts by themselves.

The camera's mode it was taken in. A photo or video taken in one of the camera's modes has it as its sourceFormat, as Photos' Media Types name them: livePhoto, portrait, panorama, burst, sloMo, timeLapse, or cinematic. A Portrait or a burst that's also a Live Photo is the Portrait or the burst. A screenshot and a screen recording are told apart by their action instead.

The lens and the exposure, as EXIF tags. The exifData of a photo's file is what the file records about how it was taken, as Photos keeps it, by EXIF 3.0's tag names: LensModel, FocalLength, FocalLengthIn35mmFilm, FNumber, ExposureTime, PhotographicSensitivity, ExposureBiasValue, MeteringMode, WhiteBalance, Flash, DigitalZoomRatio, and Orientation. Each value is EXIF's: a fraction as a decimal, to six significant digits, and a code as its number. Photos keeps only whether the flash fired, so Flash is 1 or 0. It's kept only for a photo whose file records its exposure, and so is an upright Orientation (1), since Photos records both for a screenshot too. A video's settings come from its own metadata, not EXIF, so a video has none. Photos' Photographic Styles and why it took a photo aren't EXIF, so they're left out. The make and model are the camera's name.

Only what you and your camera recorded. What Photos works out for itself is left out: the captions its model writes, the labels its search shows, the address it looks up for where a photo was taken, its guess at which person is you, and the faces it matches to people.

Albums are what you made. A photo isPartOf each album you made that it's in, and an album isPartOf its folder. Recently Deleted, shared albums, Memories, and the albums Photos makes are left out. Each run reads the library as it is now, so an album, keyword, or person taken off a photo closes while the photo has others. Taking off its last one doesn't close it: listing them in @asserts would, at the cost of a marker on every photo that has none, and few photos have any.

What's left out. Photos in Recently Deleted, the burst photos Photos hides, photos you hid, and photos in iCloud shared albums are left out, and so are Memories and the albums Photos makes. A photo in an iCloud Shared Library is yours when you added it: Photos records who added each one, and a photo someone else added is left out, since you didn't take or save it.

What you did with a photo is dated. Photos keeps only when a photo was last viewed, shared, and edited, so activity records each as it sees it, and history builds up over runs. The rest comes from the library's change log, Core Data's persistent history, which goes back as far as Photos has kept it:

  • A photo's favorite flag changed. The last change made it what it is now, and each change before turned it the other way, so a favorite is a LikeAction, and one you removed an UnlikeAction.
  • Its caption or title changed: an UpdateAction. A caption that arrived with the photo, such as on import, isn't an edit, and nor is a caption's row Photos adds with nothing in it.
  • It was added to an album: an AddAction with the album as its target. Adding photos to an album changes both the album and each photo, in one transaction. A photo added when several albums changed was added to whichever of them it's in now. A photo in none of them gets no add, and taking a photo out of an album isn't recorded, since the photo's snapshot closes it.

A photo that was in your library before this Mac had it, such as when the library is rebuilt from iCloud, arrives first, and its albums, favorite, caption, and title follow it over the next hour or so, each in a change of its own. So a change to such a photo within a day of it arriving is left out: it's the photo arriving, not something you did then. Its favorite then has no date, so it's in favorites.

A change made on this Mac is dated when it was made. One made on another device is dated when this Mac received it from iCloud: usually soon after, but later if the Mac was asleep.

The change log is read by property name. Each change has a bitmask of the properties it changed, and Core Data numbers them per entity, differently in each macOS version. The plugin reads the library's own model (Z_MODELCACHE) and lays the properties out as Core Data does, so the bits are looked up by name: a photo's favorite and albums, an album's assets, a caption's longDescription, and a title's title. A model without one of them, such as after a macOS release renames it, can't be read, and activity says so. Entity numbers come from Z_PRIMARYKEY, and columns from the tables themselves.

A favorite with no date is a like with no date. A favorite from before the change log begins, or one that arrived with its photo, has no date, so favorites gives each as its own LikeAction with no timestamp, keyed by the photo alone, and read as of when the library last changed. Removing one is a change the log dates, so it's an UnlikeAction in activity, which takes the photo out of your likes. An unchanged library gives the same records on every run.

Shared with You. The photos and videos shared with you in Messages are in a library of their own. Each is a ShareAction of a work keyed by Messages' ID for the attachment, which is how Photos knows it (ZSYNDICATIONIDENTIFIER). Its file is the attachment itself, its rendition, keyed as the imessage plugin keys it, so what Photos knows of the photo and what Messages knows of the attachment meet on one file. Photos says nothing of the file, which is Messages' to describe, and its library has no fingerprints. Who sent it comes from Messages: a Person keyed by their handle, sameAs Messages' participant. So does when: each is dated when its message was sent, since Photos dates a photo when it added it to the library, which is when the library was last rebuilt. Who it was sent to is left to the message, which the imessage plugin records with its recipients. A photo shared in a group chat targets the group's Channel, keyed by Apple's ID for the group, as the imessage plugin keys it. Messages says which chat it came in, since Photos doesn't record whether a conversation is a group. A one-to-one chat has no Channel, so a photo shared in one has no target, and nor does one shared in a group in an older Messages, which keeps no group IDs. Photos with camera data, videos, and screenshots are included. Other images, mostly stickers and images from the web, are left out, and so is a photo whose message is gone from Messages. So is a photo you sent: Photos keeps it as a copy with no link to the photo in your library, and activity already has your share of that photo.

The agent is your iCloud account, whose library this is, from @chronicle.app/apple. It's omitted when no account can be read. An explicit account object (or null) describes a copied library without a host account lookup.

Tests

The tests build a synthetic Photos library, with its own small Core Data model and change log, a Shared with You library, and a Messages database with node:sqlite, and pass an explicit iCloud account. They never read the host's Photos library, Messages, or iCloud account.