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
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>