Skip to main content

remove all the old app stuff

ID
946eeb7
date
2023-06-18 17:06:49+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
1352565
message
remove all the old app stuff
changed files
12 files, 1156 deletions

Changed files

actions/README.md (1708) → actions/README.md (0)

diff --git a/actions/README.md b/actions/README.md
deleted file mode 100644
index bd7d6df..0000000
--- a/actions/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-This folder contains all the code that actually interacts with the Photos library.
-If you're building your own project to interact with Photos, some of this might be useful/reusable.
-
-These scripts are built around [PHObject.localIdentifier], a persistent string that identifies objects (and assets in particular).
-In my experience, these identifiers are a UUID with some trailing info, e.g. `F011D947-B547-4FFC-92A1-31D197B5EF4E/L0/001`.
-
-[PHObject.localIdentifier]: https://developer.apple.com/documentation/photokit/phobject/1622400-localidentifier
-
-The scripts are as follows:
-
-<dl>
-  <dt><code>get_asset_jpeg.swift [LOCAL_IDENTIFIER] [SIZE]</code></dt>
-  <dd>
-    get a JPEG for a photo in my library.
-    It prints a path to the generated file.
-    <br/><br/>
-    This includes downloading the photo from iCloud Photo Library, if it isn’t already saved locally.
-    There are two potentially interesting functions in here: one to create an NSImage from a PHAsset, one to convert an NSImage into JPEG Data.
-  </dd>
-
-  <dt><code>get_structural_metadata.swift</code></dt>
-  <dd>
-    extract a bunch of information about my albums and assets, and print it as a JSON object.
-  </dd>
-
-  <dt><code>open_photos_app.applescript [LOCAL_IDENTIFIER]</code></dt>
-  <dd>
-    open the given photo in Photos.app.
-  </dd>
-
-  <dt><code>run_action.swift [LOCAL_IDENTIFIER] [ACTION_NAME]</code></dt>
-  <dd>
-    this script does all the modification of stuff in Photos.app.
-    This includes marking a photo as a favourite and adding/removing photos from albums.
-    <br/><br/>
-    This could be a bunch of separate scripts, but I collapsed them into a single script because there was a lot of similar code.
-  </dd>
-</dl>
\ No newline at end of file

actions/get_asset_jpeg.swift (4724) → actions/get_asset_jpeg.swift (0)

diff --git a/actions/get_asset_jpeg.swift b/actions/get_asset_jpeg.swift
deleted file mode 100644
index 7cd7f08..0000000
--- a/actions/get_asset_jpeg.swift
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/usr/bin/env swift
-/// This script creates a JPEG for a photo in my Photos Library.
-///
-/// It takes two arguments: the localIdentifier for the asset, and a
-/// target size.  It prints a path to the generated JPEG.
-///
-///     $ swift get_asset_jpeg.swift ADC872E4-A7B3-4E4F-95AE-BA96C359F532/L0/001 2048
-///     /tmp/photos-reviewer/A/ADC872E4-A7B3-4E4F-95AE-BA96C359F532/L0/001_2048.jpg⏎
-///
-
-import Cocoa
-import Photos
-
-/// Returns the PHAsset with the given identifier, or throws if it
-/// can't be found.
-func getPhoto(withLocalIdentifier localIdentifier: String) -> PHAsset {
-  let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
-
-  if fetchResult.count == 1 {
-    return fetchResult.firstObject!
-  } else {
-    fputs("Unable to find photo with ID: \(localIdentifier).\n", stderr)
-    exit(1)
-  }
-}
-
-extension PHAsset {
-  /// Create an NSImage at the given size.
-  func getImage(atSize size: Double) -> NSImage {
-    // This implementation is based on code in a Stack Overflow answer
-    // by Francois Nadeau: https://stackoverflow.com/a/48755517/1558022
-    //
-    // I've added more comments and error-handling logic.
-
-    let options = PHImageRequestOptions()
-    options.isSynchronous = true
-
-    // If i don't set this value, then sometimes I get an error like
-    // this in the `info` variable:
-    //
-    //      Error Domain=PHPhotosErrorDomain Code=3164 "(null)"
-    //
-    // This means that the asset is in the cloud, and by default Photos
-    // isn't allowed to download assets here.  Apple's documentation
-    // suggests adding this option as the fix.
-    //
-    // See https://developer.apple.com/documentation/photokit/phphotoserror/phphotoserrornetworkaccessrequired
-    options.isNetworkAccessAllowed = true
-
-    var image = NSImage()
-
-    PHImageManager.default()
-      .requestImage(
-        for: self,
-        targetSize: CGSize(width: size, height: size),
-        contentMode: .aspectFit,
-        options: options,
-        resultHandler: { (result, info) -> Void in
-
-          // If we fail to get a result, print a message to the user that
-          // includes the value of `info`.  For information about interpreting
-          // these keys, see Apple's documentation:
-          // https://developer.apple.com/documentation/photokit/phimagemanager/image_result_info_keys
-          switch (result, info) {
-          case let (result?, _):
-            image = result
-          case let (.none, info?):
-            fputs("Unable to create image:\n", stderr)
-            fputs("\(info)\n", stderr)
-            exit(1)
-          case (.none, .none):
-            fputs("Unable to create image:\n", stderr)
-            fputs("(unknown error)\n", stderr)
-            exit(1)
-          }
-        })
-
-    return image
-  }
-}
-
-extension NSImage {
-  // Based on https://gist.github.com/zappycode/3b5e151d4d98407901af5748745f5845
-  func jpegData() -> Data {
-    let cgImage = self.cgImage(forProposedRect: nil, context: nil, hints: nil)!
-    let bitmapRep = NSBitmapImageRep(cgImage: cgImage)
-    return bitmapRep.representation(using: NSBitmapImageRep.FileType.jpeg, properties: [:])!
-  }
-}
-
-/// Create the parent directory for a given file path.
-///
-/// This ensures that when you go to write a file to this path, you don't
-/// get a "file not found" error because the directory doesn't exist.
-func makeParentDirectory(forPath filePath: String) -> Void {
-  try! FileManager.default.createDirectory(
-    atPath: NSString(string: filePath).deletingLastPathComponent,
-    withIntermediateDirectories: true, attributes: nil)
-}
-
-struct Arguments {
-  var localIdentifier: String
-  var size: Int
-}
-
-func parseArgs() -> Arguments {
-  let arguments = CommandLine.arguments
-
-  guard arguments.count == 3 else {
-    fputs("Usage: \(arguments[0]) ASSET_ID SIZE\n", stderr)
-    exit(1)
-  }
-
-  let localIdentifier = arguments[1]
-  let size = Int(arguments[2])
-
-  guard size != nil else {
-    fputs("Unrecognised size: \(arguments[2])\n", stderr)
-    exit(1)
-  }
-
-  guard size! > 0 else {
-    fputs("Size must be greater than 0, got \(arguments[2])\n", stderr)
-    exit(1)
-  }
-
-  return Arguments(localIdentifier: localIdentifier, size: size!)
-}
-
-let args = parseArgs()
-let localIdentifier = args.localIdentifier
-let size = args.size
-
-let thumbnailPath =
-  "/tmp/photos-reviewer/\(localIdentifier.prefix(1))/\(localIdentifier)_\(size).jpg"
-
-if !FileManager.default.fileExists(atPath: thumbnailPath) {
-  makeParentDirectory(forPath: thumbnailPath)
-
-  let asset = getPhoto(withLocalIdentifier: localIdentifier)
-
-  try! asset
-    .getImage(atSize: Double(size))
-    .jpegData()
-    .write(to: URL(fileURLWithPath: thumbnailPath), options: [])
-}
-
-fputs(thumbnailPath, stdout)

actions/get_structural_metadata.swift (2011) → actions/get_structural_metadata.swift (0)

diff --git a/actions/get_structural_metadata.swift b/actions/get_structural_metadata.swift
deleted file mode 100644
index 66a1b0c..0000000
--- a/actions/get_structural_metadata.swift
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/env swift
-/// This script gets some metadata from my Photos Library, in particular:
-///
-///   - a list of all my albums
-///   - a list of all my photos
-///
-/// This data takes the form of the `Response` struct shown below, and is
-/// formatted as JSON printed to stdout.
-
-import Photos
-
-struct AlbumData: Codable {
-  var localIdentifier: String
-  var localizedTitle: String?
-  var assetIdentifiers: [String]
-}
-
-struct AssetData: Codable {
-  var localIdentifier: String
-  var creationDate: String?
-  var isFavorite: Bool
-}
-
-struct Response: Codable {
-  var albums: [AlbumData]
-  var assets: [AssetData]
-}
-
-/// Get data for all the albums in my library.
-func getAllAlbums() -> [AlbumData] {
-  var allAlbums: [AlbumData] = []
-
-  PHAssetCollection
-    .fetchAssetCollections(with: .album, subtype: .albumRegular, options: nil)
-    .enumerateObjects({ (album, _, _) in
-      var assetIdentifiers: [String] = []
-
-      PHAsset
-        .fetchAssets(in: album, options: nil)
-        .enumerateObjects({ (asset, _, _) in
-          assetIdentifiers.append(asset.localIdentifier)
-        })
-
-      allAlbums.append(
-        AlbumData(
-          localIdentifier: album.localIdentifier,
-          localizedTitle: album.localizedTitle,
-          assetIdentifiers: assetIdentifiers
-        )
-      )
-    })
-
-  return allAlbums
-}
-
-/// Gets data for all the photos in my library.
-func getAllAssets() -> [AssetData] {
-  var allPhotos: [AssetData] = []
-
-  PHAsset
-    .fetchAssets(with: PHAssetMediaType.image, options: nil)
-    .enumerateObjects({ (asset, _, _) in
-
-      allPhotos.append(
-        AssetData(
-          localIdentifier: asset.localIdentifier,
-          creationDate: asset.creationDate?.ISO8601Format(),
-          isFavorite: asset.isFavorite
-        )
-      )
-    })
-
-  return allPhotos
-}
-
-let jsonEncoder = JSONEncoder()
-let jsonData = try jsonEncoder.encode(
-  Response(albums: getAllAlbums(), assets: getAllAssets())
-)
-let json = String(data: jsonData, encoding: String.Encoding.utf8)
-print(json!)

actions/open_photos_app.applescript (331) → actions/open_photos_app.applescript (0)

diff --git a/actions/open_photos_app.applescript b/actions/open_photos_app.applescript
deleted file mode 100644
index b353d8b..0000000
--- a/actions/open_photos_app.applescript
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env osascript
--- This script brings Photos.app to the front, and opens the selected photo.
-
-on run argv
-  if (count of argv) ≠ 1 then
-    tell me to error "Usage: open_photos_app.applescript [PHOTO_ID]"
-  end if
-
-  tell application "Photos"
-    spotlight media item id (item 1 of argv)
-    activate
-  end tell
-end run

actions/run_action.swift (6346) → actions/run_action.swift (0)

diff --git a/actions/run_action.swift b/actions/run_action.swift
deleted file mode 100644
index 49c5cfe..0000000
--- a/actions/run_action.swift
+++ /dev/null
@@ -1,204 +0,0 @@
-#!/usr/bin/env swift
-/// This script makes changes to my Photos library.
-///
-/// It takes two argument: an asset ID, and the name of the action to
-/// perform in my Photos library.
-///
-/// The asset ID is the 'localIdentifier' property of a PHAsset.
-///
-/// The available actions are as follows:
-///
-///     toggle-favorite
-///       If an image is already a favorite, unmark it as such.
-///       If an image isn't a favorite, mark it as a favorite.
-///
-///     toggle-approved
-///     toggle-rejected
-///     toggle-needs-action
-///       When I review an image, it gets sorted into one of three buckets,
-///       which have corresponding albums in Photos: Approved, Rejected,
-///       Needs Action.
-///
-///       These actions add an asset to the appropriate album, and remove it
-///       from the other albums.  If you run it a second time, it gets
-///       removed from the album, resetting it to zero.
-///
-///     toggle-cross-stitch
-///       Toggles an image's inclusion in my "Cross stitch" album.
-///
-
-import Photos
-
-/// Looks up an album by name.
-///
-/// This assumes that album names are globally unique.
-func getAlbum(withName name: String) -> PHAssetCollection {
-  let collections =
-    PHAssetCollection
-    .fetchAssetCollections(with: .album, subtype: .albumRegular, options: nil)
-
-  var thisAssetCollection: PHAssetCollection? = nil
-
-  collections.enumerateObjects({ (album, index, stop) in
-    let assetCollection = album
-
-    if assetCollection.localizedTitle == Optional(name) {
-      thisAssetCollection = assetCollection
-    }
-  })
-
-  if let assetCollection = thisAssetCollection {
-    return assetCollection
-  } else {
-    fputs("Unable to find album with name: \(name).\n", stderr)
-    exit(1)
-  }
-}
-
-/// Returns the PHAsset with the given identifier, or throws if it
-/// can't be found.
-func getPhoto(withLocalIdentifier localIdentifier: String) -> PHAsset {
-  let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
-
-  if let firstObject = fetchResult.firstObject {
-    return firstObject
-  } else {
-    fputs("Unable to find photo with ID: \(localIdentifier).\n", stderr)
-    exit(1)
-  }
-}
-
-extension PHAsset {
-  func albums() -> [PHAssetCollection] {
-    var result: [PHAssetCollection] = []
-
-    PHAssetCollection
-      .fetchAssetCollectionsContaining(self, with: .album, options: nil)
-      .enumerateObjects({ (collection, index, stop) in
-        result.append(collection)
-      })
-
-    return result
-  }
-
-  /// Returns true if an asset is in the given album, false otherwise.
-  func isInAlbum(_ album: PHAssetCollection) -> Bool {
-    var result = false
-
-    PHAssetCollection
-      .fetchAssetCollectionsContaining(self, with: .album, options: nil)
-      .enumerateObjects({ (collection, index, stop) in
-        if (album == collection) {
-          result = true
-        }
-      })
-
-    return result
-  }
-
-  /// Remove a photo from an album.
-  ///
-  /// This expects to be run inside a performChangesAndWait change block;
-  /// see https://developer.apple.com/documentation/photokit/phphotolibrary/1620747-performchangesandwait.
-  func remove(fromAlbum album: PHAssetCollection) -> Void {
-    let changeAlbum =
-      PHAssetCollectionChangeRequest(for: album)!
-
-    changeAlbum.removeAssets([self] as NSFastEnumeration)
-  }
-
-  /// Add a photo to an album.
-  ///
-  /// This expects to be run inside a performChangesAndWait change block;
-  /// see https://developer.apple.com/documentation/photokit/phphotolibrary/1620747-performchangesandwait.
-  func add(toAlbum album: PHAssetCollection) -> Void {
-    let changeAlbum =
-      PHAssetCollectionChangeRequest(for: album)!
-
-    changeAlbum.addAssets([self] as NSFastEnumeration)
-  }
-
-  /// Toggle a photo's inclusion in an album.
-  ///
-  /// If the photo is already in the album, remove it.  If the photo isn't
-  /// in the album, add it.
-  ///
-  /// This expects to be run inside a performChangesAndWait change block;
-  /// see https://developer.apple.com/documentation/photokit/phphotolibrary/1620747-performchangesandwait.
-  func toggle(inAlbum album: PHAssetCollection) -> Void {
-    let changeAlbum =
-      PHAssetCollectionChangeRequest(for: album)!
-
-    let assets = [self] as NSFastEnumeration
-
-    if photo.isInAlbum(album) {
-      changeAlbum.removeAssets(assets)
-    } else {
-      changeAlbum.addAssets(assets)
-    }
-  }
-}
-
-let arguments = CommandLine.arguments
-
-guard arguments.count == 3 else {
-  fputs("Usage: \(arguments[0]) PHOTO_ID ACTION\n", stderr)
-  exit(1)
-}
-
-let action = arguments[2]
-
-let photo = getPhoto(withLocalIdentifier: arguments[1])
-
-try PHPhotoLibrary.shared().performChangesAndWait {
-  switch action {
-    case "toggle-favorite":
-      let changeAsset = PHAssetChangeRequest(for: photo)
-      changeAsset.isFavorite = !photo.isFavorite
-
-    case "toggle-approved", "toggle-rejected", "toggle-needs-action":
-      let approved = getAlbum(withName: "Approved")
-      let rejected = getAlbum(withName: "Rejected")
-      let needsAction = getAlbum(withName: "Needs Action")
-
-      let albums = photo.albums()
-
-      let isApproved = albums.contains(approved)
-      let isRejected = albums.contains(rejected)
-      let isNeedsAction = albums.contains(needsAction)
-
-      // Strictly speaking, the first condition is a combination of two:
-      //
-      //   1. The action is `toggle-approved` and the photo is approved,
-      //      in which case toggling means un-approving it.
-      //   2. The action is anything else and the photo is approved, in
-      //      which case setting the new status means removing approved.
-      //
-      // Similar logic applies for all three conditions.
-      if isApproved {
-        photo.remove(fromAlbum: approved)
-      } else if action == "toggle-approved" {
-        photo.add(toAlbum: approved)
-      }
-
-      if isRejected {
-        photo.remove(fromAlbum: rejected)
-      } else if action == "toggle-rejected" {
-        photo.add(toAlbum: rejected)
-      }
-
-      if isNeedsAction {
-        photo.remove(fromAlbum: needsAction)
-      } else if action == "toggle-needs-action" {
-        photo.add(toAlbum: needsAction)
-      }
-
-    case "toggle-cross-stitch":
-      let crossStitch = getAlbum(withName: "Cross stitch")
-      photo.toggle(inAlbum: crossStitch)
-
-    default:
-      fputs("Unrecognised action: \(action)\n", stderr)
-      exit(1)
-  }
-}

requirements.in (29) → requirements.in (0)

diff --git a/requirements.in b/requirements.in
deleted file mode 100644
index 68f4f62..0000000
--- a/requirements.in
+++ /dev/null
@@ -1,2 +0,0 @@
-Flask==2.0.1
-humanize==4.4.0

requirements.txt (359) → requirements.txt (0)

diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index d8356c8..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-#
-# This file is autogenerated by pip-compile
-# To update, run:
-#
-#    pip-compile
-#
-click==8.1.3
-    # via flask
-flask==2.0.1
-    # via -r requirements.in
-humanize==4.4.0
-    # via -r requirements.in
-itsdangerous==2.1.2
-    # via flask
-jinja2==3.1.2
-    # via flask
-markupsafe==2.1.2
-    # via
-    #   jinja2
-    #   werkzeug
-werkzeug==2.3.4
-    # via flask

server.py (8309) → server.py (0)

diff --git a/server.py b/server.py
deleted file mode 100755
index 9f731ff..0000000
--- a/server.py
+++ /dev/null
@@ -1,290 +0,0 @@
-#!/usr/bin/env python3
-
-import collections
-import concurrent.futures
-import functools
-import json
-import os
-import random
-import subprocess
-import sys
-
-from flask import Flask, redirect, render_template, request, send_file, url_for
-import humanize
-
-
-app = Flask(__name__)
-app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 24 * 60 * 60
-
-app.add_template_filter(humanize.intcomma)
-
-
-def get_asset_state(asset):
-    state_albums = [
-        alb
-        for alb in asset["albums"]
-        if alb in {"Approved", "Rejected", "Needs Action"}
-    ]
-
-    if len(state_albums) > 1:
-        print(
-            f"Photo {asset['localIdentifier']} has multiple states! {', '.join(state_albums)}",
-            file=sys.stderr,
-        )
-        subprocess.check_call(
-            [
-                "osascript",
-                "actions/open_photos_app.applescript",
-                asset["localIdentifier"],
-            ]
-        )
-        sys.exit(1)
-
-    asset["display_albums"] = [
-        alb for alb in asset["albums"] if alb not in state_albums
-    ]
-
-    if len(state_albums) == 1:
-        return state_albums[0]
-    elif len(state_albums) == 0:
-        return "Unknown"
-    else:
-        raise RuntimeError(
-            f'Asset {asset["localIdentifier"]} is in multiple states: {state_albums.join(", ")}'
-        )
-
-
-class PhotosData:
-    def __init__(self):
-        self.fetch_metadata()
-        self.executor = concurrent.futures.ThreadPoolExecutor()
-
-    def fetch_metadata(self):
-        print("Fetching metadata from Photos.app...")
-        data = json.loads(
-            subprocess.check_output(["swift", "actions/get_structural_metadata.swift"])
-        )
-
-        all_assets = sorted(data["assets"], key=lambda a: a["creationDate"])
-        self.all_positions = {
-            asset["localIdentifier"]: i for i, asset in enumerate(all_assets)
-        }
-
-        all_albums = data["albums"]
-
-        for alb in all_albums:
-            alb["assetIdentifiers"] = set(alb["assetIdentifiers"])
-
-        for asset in all_assets:
-            asset["albums"] = {
-                alb["localizedTitle"]
-                for alb in all_albums
-                if asset["localIdentifier"] in alb["assetIdentifiers"]
-            }
-            asset["state"] = get_asset_state(asset)
-
-        self.all_assets = all_assets
-
-    @functools.lru_cache(maxsize=0 if "--debug" in sys.argv else None)
-    def get_response(self, local_identifier):
-        all_assets = self.all_assets
-
-        position = self.all_positions[local_identifier]
-
-        prev_five = all_assets[position - 5 : position]
-        this_asset = all_assets[position]
-        next_five = all_assets[position + 1 : position + 6]
-
-        states = collections.Counter(asset["state"] for asset in self.all_assets)
-
-        for asset in [this_asset] + prev_five + next_five:
-            self.executor.submit(
-                lambda: get_image(asset['localIdentifier'])
-            )
-            self.executor.submit(
-                lambda: get_thumbnail(asset['localIdentifier'])
-            )
-
-        return render_template(
-            "index.html",
-            assets=all_assets,
-            position=position,
-            prev_five=prev_five,
-            this_asset=this_asset,
-            next_five=next_five,
-            states=states,
-        )
-
-    def run_action(self, local_identifier, action):
-        import time
-
-        t0 = time.time()
-        subprocess.check_call(
-            ["swift", "actions/run_action.swift", local_identifier, action]
-        )
-        print(time.time() - t0)
-
-        this_asset = self.all_assets[self.all_positions[local_identifier]]
-
-        if action == "toggle-favorite":
-            this_asset["isFavorite"] = not this_asset["isFavorite"]
-        elif action == "toggle-approved":
-            this_asset["albums"].discard("Rejected")
-            this_asset["albums"].discard("Needs Action")
-
-            try:
-                this_asset["albums"].remove("Approved")
-            except KeyError:
-                this_asset["albums"].add("Approved")
-        elif action == "toggle-rejected":
-            this_asset["albums"].discard("Approved")
-            this_asset["albums"].discard("Needs Action")
-
-            try:
-                this_asset["albums"].remove("Rejected")
-            except KeyError:
-                this_asset["albums"].add("Rejected")
-        elif action == "toggle-needs-action":
-            this_asset["albums"].discard("Approved")
-            this_asset["albums"].discard("Rejected")
-
-            try:
-                this_asset["albums"].remove("Needs Action")
-            except KeyError:
-                this_asset["albums"].add("Needs Action")
-        elif action == "toggle-cross-stitch":
-            try:
-                this_asset["albums"].remove("Cross stitch")
-            except KeyError:
-                this_asset["albums"].add("Cross stitch")
-
-        this_asset["state"] = get_asset_state(this_asset)
-
-        self.get_response.cache_clear()
-
-
-photos_data = PhotosData()
-
-
-@app.route("/")
-def index():
-    try:
-        local_identifier = request.args["localIdentifier"]
-    except KeyError:
-        all_assets = photos_data.all_assets
-        return redirect(
-            url_for("index", localIdentifier=all_assets[-1]["localIdentifier"])
-        )
-
-    return photos_data.get_response(local_identifier)
-
-
-@functools.cache
-def get_jpeg(local_identifier, *, size):
-    if os.path.exists(
-        f"/tmp/photos-reviewer/{local_identifier[0]}/{local_identifier}_{size}.jpg"
-    ):
-        return (
-            f"/tmp/photos-reviewer/{local_identifier[0]}/{local_identifier}_{size}.jpg"
-        )
-
-    return subprocess.check_output(
-        ["swift", "actions/get_asset_jpeg.swift", local_identifier, str(size)]
-    ).decode("utf8")
-
-
-@functools.cache
-def get_thumbnail(local_identifier):
-    return get_jpeg(local_identifier, size=85 * 2)
-
-
-@functools.cache
-def get_image(local_identifier):
-    return get_jpeg(local_identifier, size=2048)
-
-
-@app.route("/thumbnail")
-def thumbnail():
-    local_identifier = request.args["localIdentifier"]
-
-    return send_file(get_thumbnail(local_identifier))
-
-
-@app.route("/image")
-def image():
-    local_identifier = request.args["localIdentifier"]
-
-    return send_file(get_image(local_identifier))
-
-
-@app.route("/actions")
-def run_action():
-    local_identifier = request.args["localIdentifier"]
-    action = request.args["action"]
-
-    photos_data.run_action(local_identifier, action)
-
-    if action in {"toggle-favorite", "toggle-cross-stitch"}:
-        return redirect(url_for("index", localIdentifier=local_identifier))
-    elif action in {"toggle-approved", "toggle-rejected", "toggle-needs-action"}:
-        position = photos_data.all_positions[local_identifier]
-        redirect_to = photos_data.all_assets[position - 1]["localIdentifier"]
-        return redirect(url_for("index", localIdentifier=redirect_to))
-
-
-@app.route("/open", methods=["POST"])
-def open_photo():
-    local_identifier = request.args["localIdentifier"]
-
-    subprocess.check_call(
-        ["osascript", "actions/open_photos_app.applescript", local_identifier]
-    )
-
-    return b"", 204
-
-
-@app.route("/next-unreviewed")
-def next_unreviewed():
-    local_identifier = request.args["before"]
-
-    all_assets = photos_data.all_assets
-
-    position = photos_data.all_positions[local_identifier]
-
-    this_asset = photos_data.all_assets[position]
-
-    unreviewed_assets = [
-        asset
-        for i, asset in enumerate(photos_data.all_assets)
-        if i <= position and asset["state"] == "Unknown"
-    ]
-    try:
-        next_asset_id_to_review = unreviewed_assets[-1]["localIdentifier"]
-        return redirect(url_for("index", localIdentifier=next_asset_id_to_review))
-    except IndexError:
-        return b"", 404
-
-
-@app.route("/random-unreviewed")
-def random_unreviewed():
-    unreviewed_assets = [
-        asset["localIdentifier"]
-        for i, asset in enumerate(photos_data.all_assets)
-        if asset["state"] == "Unknown"
-    ]
-
-    try:
-        return redirect(url_for("index", localIdentifier=random.choice(unreviewed_assets)))
-    except IndexError:
-        return b"", 404
-
-
-@app.route("/refresh", methods=["POST"])
-def refresh():
-    photos_data.fetch_metadata()
-
-    return b"", 204
-
-
-if __name__ == "__main__":
-    app.run(debug="--debug" in sys.argv)

static/reviewer.js (1350) → static/reviewer.js (0)

diff --git a/static/reviewer.js b/static/reviewer.js
deleted file mode 100644
index 0d2c1a9..0000000
--- a/static/reviewer.js
+++ /dev/null
@@ -1,51 +0,0 @@
-function httpPOST(url) {
-  var xmlHttp = null;
-
-  xmlHttp = new XMLHttpRequest();
-  xmlHttp.open("POST", url, false);
-  xmlHttp.send(null);
-  return xmlHttp.responseText;
-}
-
-function handleKeyDown(event, thisIdentifier, nextIdentifier, prevIdentifier) {
-  switch(event.key) {
-    case "ArrowLeft":
-      window.location = `/?localIdentifier=${prevIdentifier}`;
-      break;
-
-    case "ArrowRight":
-      window.location = `/?localIdentifier=${nextIdentifier}`;
-      break;
-
-    case "1":
-      window.location = `/actions?localIdentifier=${thisIdentifier}&action=toggle-approved`;
-      break;
-
-    case "2":
-      window.location = `/actions?localIdentifier=${thisIdentifier}&action=toggle-rejected`;
-      break;
-
-    case "3":
-      window.location = `/actions?localIdentifier=${thisIdentifier}&action=toggle-needs-action`;
-      break;
-
-    case "f":
-      window.location = `/actions?localIdentifier=${thisIdentifier}&action=toggle-favorite`;
-      break;
-
-    case "c":
-      window.location = `/actions?localIdentifier=${thisIdentifier}&action=toggle-cross-stitch`;
-      break;
-
-    case "o":
-      httpPOST(`/open?localIdentifier=${thisIdentifier}`);
-      break;
-
-    case "u":
-      window.location = `/next-unreviewed?before=${thisIdentifier}`;
-      break;
-
-    case "?":
-      window.location = '/random-unreviewed';
-      break;
-  }}

static/style.css (4477) → static/style.css (0)

diff --git a/static/style.css b/static/style.css
deleted file mode 100644
index d530a9b..0000000
--- a/static/style.css
+++ /dev/null
@@ -1,218 +0,0 @@
-body {
-  text-align: center;
-  padding: 0;
-  margin: 10px;
-  font-family: -apple-system;
-}
-
-a {
-  text-decoration: none;
-}
-
-#thumbnails {
-  margin-bottom: 1em;
-  height: 85px;
-}
-
-#thumbnails div.thumbnail {
-  display: inline-block;
-  border: 1px solid lightgrey;
-  margin: 1px;
-  width: 65px;
-  height: 65px;
-  padding: 1px;
-  border-radius: 6px;
-}
-
-#thumbnails div.thumbnail img {
-  width: 65px;
-  height: 65px;
-  border-radius: 4px;
-}
-
-#thumbnails div.thumbnail .state {
-  position: absolute;
-  width: 15px;
-  height: 17px;
-  color: white;
-  text-align: left;
-  padding-left: 3px;
-  padding-top: 1px;
-  font-size: 13px;
-  line-height: 16px;
-  border-top-left-radius: 4px;
-  border-bottom-right-radius: 16px;
-  margin-left: -3px;
-  margin-top: -3px;
-  font-family: serif;
-  z-index: 10;
-}
-
-#thumbnails div.this_asset div.thumbnail .state {
-  width: 18px;
-  height: 21px;
-  color: white;
-  text-align: left;
-  padding-left: 4px;
-  padding-top: 2px;
-  font-size: 17px;
-  line-height: 16px;
-  border-bottom-right-radius: 19px;
-  border-width: 10px;
-  margin-left: -2px;
-  margin-top: -2px;
-  font-family: serif;
-}
-
-#thumbnails div.thumbnail:not(.state-Unknown) img {
-  mask-image:
-      radial-gradient(circle at top left, transparent 0, transparent 16px, black 16px),
-      radial-gradient(circle at top right, transparent 0, transparent 0px, black 0px),
-      radial-gradient(circle at bottom left, transparent 0, transparent 0px, black 0px),
-      radial-gradient(circle at bottom right, transparent 0, transparent 0px, black 0px);
-  mask-position: top left, top right, bottom left, bottom right;
-  mask-repeat: no-repeat;
-  mask-size: 50% 50%;
-}
-
-#thumbnails div.this_asset div:not(.state-Unknown) img {
-  mask-image:
-      radial-gradient(circle at top left, transparent 0, transparent 22px, black 22px),
-      radial-gradient(circle at top right, transparent 0, transparent 0px, black 0px),
-      radial-gradient(circle at bottom left, transparent 0, transparent 0px, black 0px),
-      radial-gradient(circle at bottom right, transparent 0, transparent 0px, black 0px);
-  mask-position: top left, top right, bottom left, bottom right;
-  mask-repeat: no-repeat;
-  mask-size: 50% 50%;
-}
-
-#thumbnails div.thumbnail:not(.state-Unknown) {
-  border-width: 2px;
-  margin: 0;
-}
-
-.this_asset {
-  margin-left: 3px;
-  margin-right: 3px;
-}
-
-#thumbnails .this_asset div.thumbnail:not(.state-Unknown) {
-  border-width: 3px;
-  margin: -1px;
-  border-radius: 8px;
-}
-
-#thumbnails div.thumbnail.state-Approved {
-  border-color: green;
-}
-
-#thumbnails div.thumbnail.state-Approved .state {
-  background: green;
-}
-
-#thumbnails div.thumbnail.state-Rejected {
-  border-color: red;
-}
-
-#thumbnails div.thumbnail.state-Rejected .state {
-  background: red;
-}
-
-#thumbnails :not(.this_asset) div.thumbnail.state-Rejected img {
-  opacity: 0.5;
-  filter: saturate(0%);
-  z-index: -10;
-}
-
-#thumbnails div.thumbnail.state-Needs-Action {
-  border-color: blue;
-}
-
-#thumbnails div.thumbnail.state-Needs-Action .state {
-  background: blue;
-}
-
-#thumbnails div.this_asset {
-  display: inline-block;
-}
-
-#thumbnails div.this_asset div.thumbnail {
-  width: 85px;
-  height: 85px;
-}
-
-#thumbnails div.this_asset div.thumbnail:not(.state-Unknown) img {
-  width: 83px;
-  height: 83px;
-  margin-top: 1px;
-}
-
-#thumbnails div.this_asset div.thumbnail img {
-  width: 85px;
-  height: 85px;
-}
-
-#thumbnails img {
-  object-fit: cover;
-  aspect-ratio: 1 / 1;
-}
-
-#thumbnails .placeholder {
-  width: 65px;
-  height: 65px;
-  display: inline-block;
-  border: 2px solid lightgrey;
-  opacity: 0.5;
-}
-
-.thumbnail_big {
-  width: 85px;
-  height: 85px;
-}
-
-img#big {
-  max-width: calc(100vw - 20px);
-  /* screen height - 85px (thumbnail bar) - 20px (body padding) -1em (margin below thumbnail bar) - 18px (metadata) - 1em (margin below metadata)*/
-  max-height: calc(100vh - 85px - 20px - 1em - 18px - 1em);
-}
-
-#debug {
-  position: absolute;
-  border: 2px solid darkgrey;
-  text-align: right;
-  opacity: 0.25;
-  padding: 5px 10px;
-  border-radius: 10px;
-  background: white;
-  right: 10px;
-  top: 10px;
-  line-height: 1.25em;
-}
-
-#debug:hover {
-  opacity: 1;
-  box-shadow: 0px 0px 3px darkgrey;
-  z-index: 1000;
-}
-
-#debug ul {
-  text-align: left;
-}
-
-.favorite {
-  text-align: right;
-  position: absolute;
-  z-index: 10000;
-  margin-top: 53px;
-  margin-left: 47px;
-  color: white;
-  display: inline;
-  text-shadow: 0px 0px 3px black;
-  line-height: 0px;
-}
-
-.this_asset .favorite {
-  font-size: 21px;
-  margin-top: 69px;
-  margin-left: 61px;
-}
\ No newline at end of file

templates/index.html (5245) → templates/index.html (0)

diff --git a/templates/index.html b/templates/index.html
deleted file mode 100644
index 6342c05..0000000
--- a/templates/index.html
+++ /dev/null
@@ -1,70 +0,0 @@
-<head>
-  <link rel="stylesheet" href="/static/style.css">
-</head>
-
-<details id="debug">
-  <summary>debug</summary>
-  <ul>
-    <li><strong>this asset:</strong> {{ this_asset['localIdentifier'] }}</li>
-    <li><strong>stats:</strong>
-      {{ states["Approved"] | intcomma}} approved,
-      {{ states["Rejected"] | intcomma }} rejected,
-      {{ states["Needs Action"] | intcomma }} need action,
-      {{ states["Unknown"] | intcomma }} remaining
-      ({{ states.values() | sum | intcomma }} total)
-    </li>
-    <li>
-      <a href="#" onclick="httpPOST('/refresh'); window.location.reload();">refresh photos metadata</a>
-    </li>
-  </ul>
-</details>
-
-<div id="thumbnails">
-  {% set placeholder_prev_five = 5 - prev_five|length %}
-  {% for _ in range(placeholder_prev_five) %}
-    <!-- from subtle patterns -->
-    <img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFwAXAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAAAQcC/8QAMxAAAQMDAwIEBAUEAwAAAAAAAQARIQIxQSIyURJhQnGBoVJicrEDEzPR8EORssEjc4L/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A2gdt3s6fTu9u7J5RXbqw6Z0sKjnB5QB8m727sn/XfvYjsgnZFXODyyfQ1JPOQgfQ3/rjsn0QM9XCfQwzOQn0aRcvkIH0wPF1cJ5W8XVdT6dNImoHhXygDcKpKBjinxdV0/x8T3dMcUjcDdPVqRuBu6B/jkZdP+T+kwGXu6Y+XIy6NWf06mHDoA48VurlL7TqMdXPKdvFbq5KSYpLEx1csgX2lqjnnnyS+zSbvyg1FqS1R8Qyybtp6SZflAd9mnPmEvsPRkvkJfbpz5oC+09Lai2QgPD06QJIMwnlpA3C6CZp0gSQJdB20imSAgY+GkXpu6ewEGnkpYPtFN6eUHNgINIQMcU26e6dNdX6dXSBDJ38Ig0906Kq5oqNIEMEBn0vJjqym4mmxt1ZjlRnej0fPr2V3E02w+Y5QBqPSIJzn1TfAek3cX9U3aZHcX9U3xIzH+0DdbTmMoNVtLSWTfyM6f5dN2CGnT/LoAmRpaSKZdBIcQKS5FNigmbdMtTlBMzplqbFAx1WFNwLFPmsKYIFig+L4cU2KC3V8MMLFAZg+Bp6cJ+Wa5pq6QIZMdXENhPy/wAyerpaGFvRAZ9HZu6M+hm75jlIOg7bd/NLvQYFvJA3vTI+481N8GMxf1V36TAHskV6TA7Y7IG+CGzCDXgxOn7eafqbhaY54TffExE8IA1T8M6cpfU234bFN0mTTIaH7ID1ajJBhodAuOr4bNYoMVZpgNYpfXc02aAU+e5Fmse6Bjra0dvNOgfiajU2B3T58iBw3KdAr1VVdP8AtA+Q7fsOXSDpqinztwkbTs9m5dL6aop+3CBu01RSPb903OK4H9m7JfTXFP24ZN0V2HoyBvivHox4TfulrYnhN29vWJ4Tdv8AR4nhA3TVJFsOeE3ajJG3DpumoORZ4nhLzVJG14coF2qvUNuHS7VXqFsOOWS83qG18oPivVg8hA+fxY8uWTpor1fiFj90+bxY5byRqDP4hY/dAix2ezful4qGj27Ml6hSdpqIZSnUQCIJIbhkFvFcU/xmS8fiW729EGogVTSXjhlKdRAqkEEscILf9THMSl9/MdXP7KUnq3AHT1ThAX3S1PUHwUFvumoberlL7pq8PVdR4JMmkOCUJiomTTIJQXgmavD1XKDnx4e7IYFRuaWYnuhjqNzSQATdA7+PHLfuh/L/AKparzn1T7irpfLLqiimsE1ByCzoP//Z" class="placeholder">
-  {% endfor %}
-
-  {% for asset in prev_five %}
-    {% include "thumbnail.html" %}
-  {% endfor %}
-
-
-  <div class="this_asset">
-    {% set asset = this_asset %}
-    {% include "thumbnail.html" %}
-  </div>
-
-  {% for asset in next_five %}
-    {% include "thumbnail.html" %}
-  {% endfor %}
-
-  {% set placeholder_next_five = 5 - next_five|length %}
-  {% for _ in range(placeholder_next_five) %}
-    <img src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBwgHBgkIBwgKCgkLDRYPDQwMDRsUFRAWIB0iIiAdHx8kKDQsJCYxJx8fLT0tMTU3Ojo6Iys/RD84QzQ5OjcBCgoKDQwNGg8PGjclHyU3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3N//AABEIAFwAXAMBIgACEQEDEQH/xAAXAAEBAQEAAAAAAAAAAAAAAAAAAQcC/8QAMxAAAQMDAwIEBAUEAwAAAAAAAQARIQIxQSIyURJhQnGBoVJicrEDEzPR8EORssEjc4L/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8A2gdt3s6fTu9u7J5RXbqw6Z0sKjnB5QB8m727sn/XfvYjsgnZFXODyyfQ1JPOQgfQ3/rjsn0QM9XCfQwzOQn0aRcvkIH0wPF1cJ5W8XVdT6dNImoHhXygDcKpKBjinxdV0/x8T3dMcUjcDdPVqRuBu6B/jkZdP+T+kwGXu6Y+XIy6NWf06mHDoA48VurlL7TqMdXPKdvFbq5KSYpLEx1csgX2lqjnnnyS+zSbvyg1FqS1R8Qyybtp6SZflAd9mnPmEvsPRkvkJfbpz5oC+09Lai2QgPD06QJIMwnlpA3C6CZp0gSQJdB20imSAgY+GkXpu6ewEGnkpYPtFN6eUHNgINIQMcU26e6dNdX6dXSBDJ38Ig0906Kq5oqNIEMEBn0vJjqym4mmxt1ZjlRnej0fPr2V3E02w+Y5QBqPSIJzn1TfAek3cX9U3aZHcX9U3xIzH+0DdbTmMoNVtLSWTfyM6f5dN2CGnT/LoAmRpaSKZdBIcQKS5FNigmbdMtTlBMzplqbFAx1WFNwLFPmsKYIFig+L4cU2KC3V8MMLFAZg+Bp6cJ+Wa5pq6QIZMdXENhPy/wAyerpaGFvRAZ9HZu6M+hm75jlIOg7bd/NLvQYFvJA3vTI+481N8GMxf1V36TAHskV6TA7Y7IG+CGzCDXgxOn7eafqbhaY54TffExE8IA1T8M6cpfU234bFN0mTTIaH7ID1ajJBhodAuOr4bNYoMVZpgNYpfXc02aAU+e5Fmse6Bjra0dvNOgfiajU2B3T58iBw3KdAr1VVdP8AtA+Q7fsOXSDpqinztwkbTs9m5dL6aop+3CBu01RSPb903OK4H9m7JfTXFP24ZN0V2HoyBvivHox4TfulrYnhN29vWJ4Tdv8AR4nhA3TVJFsOeE3ajJG3DpumoORZ4nhLzVJG14coF2qvUNuHS7VXqFsOOWS83qG18oPivVg8hA+fxY8uWTpor1fiFj90+bxY5byRqDP4hY/dAix2ezful4qGj27Ml6hSdpqIZSnUQCIJIbhkFvFcU/xmS8fiW729EGogVTSXjhlKdRAqkEEscILf9THMSl9/MdXP7KUnq3AHT1ThAX3S1PUHwUFvumoberlL7pq8PVdR4JMmkOCUJiomTTIJQXgmavD1XKDnx4e7IYFRuaWYnuhjqNzSQATdA7+PHLfuh/L/AKparzn1T7irpfLLqiimsE1ByCzoP//Z" class="placeholder">
-  {% endfor %}
-</div>
-
-<p class="metadata">
-  {% if this_asset['display_albums'] %}
-    <strong>albums:</strong> {{ this_asset['display_albums'] | join(', ') }} /
-  {% endif %}
-  <strong>creation date:</strong> {{ this_asset['creationDate'] }}
-</p>
-
-<img id="big" src="{{ url_for('image', localIdentifier=this_asset['localIdentifier']) }}">
-
-<script src="/static/reviewer.js"></script>
-
-<script>
-  document.onkeydown = function(event) {
-    return handleKeyDown(
-      event,
-      "{{ this_asset['localIdentifier'] }}",
-      {% if next_five %}"{{ next_five[0]['localIdentifier'] }}"{% else %}""{% endif %},
-      "{{ prev_five[-1]['localIdentifier'] }}",
-    )
-  }
-</script>
-

templates/thumbnail.html (666) → templates/thumbnail.html (0)

diff --git a/templates/thumbnail.html b/templates/thumbnail.html
deleted file mode 100644
index 89d1703..0000000
--- a/templates/thumbnail.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<a href="{{ url_for('index', localIdentifier=asset['localIdentifier'])}}">
-  <div class="thumbnail state-{{ asset['state'] | replace(' ', '-') }}">
-    {% if asset['state'] == 'Approved' %}
-      <div class="state">✓</div>
-    {% elif asset['state'] == 'Rejected' %}
-      <div class="state">✘</div>
-    {% elif asset['state'] == 'Needs Action' %}
-      <div class="state">ℹ</div>
-    {% endif %}
-
-    {% if asset.isFavorite %}
-      <div class="favorite">♥</div>
-    {% endif %}
-
-    <img
-      src="{{ url_for('thumbnail', localIdentifier=asset['localIdentifier']) }}"
-      class="thumbnail state-{{ asset['state'] | replace(' ', '-')  }}">
-  </div>
-</a>