get actions working
- ID
9786983- date
2023-05-13 12:04:38+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
3b652f6- message
get actions working- changed files
6 files, 394 additions, 25 deletions
Changed files
scripts/flag.swift (0) → scripts/flag.swift (1687)
diff --git a/scripts/flag.swift b/scripts/flag.swift
new file mode 100644
index 0000000..147317a
--- /dev/null
+++ b/scripts/flag.swift
@@ -0,0 +1,65 @@
+#!/usr/bin/env swift
+
+import Photos
+
+func getAlbumWith(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 thisAssetCollection != nil {
+ return thisAssetCollection!
+ } else {
+ fputs("Unable to find album with name: \(name).\n", stderr)
+ exit(1)
+ }
+}
+
+func getPhotoWith(uuid: String) -> PHAsset {
+ let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [uuid], options: nil)
+
+ if fetchResult.count == 1 {
+ return fetchResult.firstObject!
+ } else {
+ fputs("Unable to find photo with ID: \(uuid).\n", stderr)
+ exit(1)
+ }
+}
+
+let arguments = CommandLine.arguments
+
+guard arguments.count == 2 else {
+ fputs("Usage: \(arguments[0]) PHOTO_ID\n", stderr)
+ exit(1)
+}
+
+let flagged = getAlbumWith(name: "Flagged")
+let rejected = getAlbumWith(name: "Rejected")
+let needsAction = getAlbumWith(name: "Needs Action")
+
+let photo = getPhotoWith(uuid: arguments[1])
+
+try PHPhotoLibrary.shared().performChangesAndWait {
+ let changeFlagged =
+ PHAssetCollectionChangeRequest(for: flagged)!
+ let changeRejected =
+ PHAssetCollectionChangeRequest(for: rejected)!
+ let changeNeedsAction =
+ PHAssetCollectionChangeRequest(for: needsAction)!
+
+ let assets = [photo] as NSFastEnumeration
+
+ changeFlagged.addAssets(assets)
+ changeRejected.removeAssets(assets)
+ changeNeedsAction.removeAssets(assets)
+}
scripts/needs_action.swift (0) → scripts/needs_action.swift (1687)
diff --git a/scripts/needs_action.swift b/scripts/needs_action.swift
new file mode 100644
index 0000000..581543a
--- /dev/null
+++ b/scripts/needs_action.swift
@@ -0,0 +1,65 @@
+#!/usr/bin/env swift
+
+import Photos
+
+func getAlbumWith(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 thisAssetCollection != nil {
+ return thisAssetCollection!
+ } else {
+ fputs("Unable to find album with name: \(name).\n", stderr)
+ exit(1)
+ }
+}
+
+func getPhotoWith(uuid: String) -> PHAsset {
+ let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [uuid], options: nil)
+
+ if fetchResult.count == 1 {
+ return fetchResult.firstObject!
+ } else {
+ fputs("Unable to find photo with ID: \(uuid).\n", stderr)
+ exit(1)
+ }
+}
+
+let arguments = CommandLine.arguments
+
+guard arguments.count == 2 else {
+ fputs("Usage: \(arguments[0]) PHOTO_ID\n", stderr)
+ exit(1)
+}
+
+let flagged = getAlbumWith(name: "Flagged")
+let rejected = getAlbumWith(name: "Rejected")
+let needsAction = getAlbumWith(name: "Needs Action")
+
+let photo = getPhotoWith(uuid: arguments[1])
+
+try PHPhotoLibrary.shared().performChangesAndWait {
+ let changeFlagged =
+ PHAssetCollectionChangeRequest(for: flagged)!
+ let changeRejected =
+ PHAssetCollectionChangeRequest(for: rejected)!
+ let changeNeedsAction =
+ PHAssetCollectionChangeRequest(for: needsAction)!
+
+ let assets = [photo] as NSFastEnumeration
+
+ changeFlagged.removeAssets(assets)
+ changeRejected.removeAssets(assets)
+ changeNeedsAction.addAssets(assets)
+}
scripts/reject.swift (0) → scripts/reject.swift (1687)
diff --git a/scripts/reject.swift b/scripts/reject.swift
new file mode 100644
index 0000000..68aea71
--- /dev/null
+++ b/scripts/reject.swift
@@ -0,0 +1,65 @@
+#!/usr/bin/env swift
+
+import Photos
+
+func getAlbumWith(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 thisAssetCollection != nil {
+ return thisAssetCollection!
+ } else {
+ fputs("Unable to find album with name: \(name).\n", stderr)
+ exit(1)
+ }
+}
+
+func getPhotoWith(uuid: String) -> PHAsset {
+ let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [uuid], options: nil)
+
+ if fetchResult.count == 1 {
+ return fetchResult.firstObject!
+ } else {
+ fputs("Unable to find photo with ID: \(uuid).\n", stderr)
+ exit(1)
+ }
+}
+
+let arguments = CommandLine.arguments
+
+guard arguments.count == 2 else {
+ fputs("Usage: \(arguments[0]) PHOTO_ID\n", stderr)
+ exit(1)
+}
+
+let flagged = getAlbumWith(name: "Flagged")
+let rejected = getAlbumWith(name: "Rejected")
+let needsAction = getAlbumWith(name: "Needs Action")
+
+let photo = getPhotoWith(uuid: arguments[1])
+
+try PHPhotoLibrary.shared().performChangesAndWait {
+ let changeFlagged =
+ PHAssetCollectionChangeRequest(for: flagged)!
+ let changeRejected =
+ PHAssetCollectionChangeRequest(for: rejected)!
+ let changeNeedsAction =
+ PHAssetCollectionChangeRequest(for: needsAction)!
+
+ let assets = [photo] as NSFastEnumeration
+
+ changeFlagged.removeAssets(assets)
+ changeRejected.addAssets(assets)
+ changeNeedsAction.removeAssets(assets)
+}
server.py (2691) → server.py (5465)
diff --git a/server.py b/server.py
index b4137da..9889919 100755
--- a/server.py
+++ b/server.py
@@ -3,6 +3,7 @@
import functools
import json
import subprocess
+import sys
from flask import Flask, redirect, render_template, request, send_file, url_for
@@ -11,11 +12,27 @@ app = Flask(__name__)
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 24 * 60 * 60
+def get_asset_state(asset):
+ state_albums = [alb for alb in asset['albums'] if alb in {'Flagged', 'Rejected', 'Needs Action'}]
+
+ assert len(state_albums) <= 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):
data = json.loads(subprocess.check_output(['swift', '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']
@@ -23,21 +40,88 @@ class PhotosData:
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['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]
+
+ return render_template('index.html', assets=all_assets, position=position, prev_five=prev_five, this_asset=this_asset, next_five=next_five)
+
+ def flag(self, local_identifier):
+ subprocess.check_call(['swift', 'scripts/flag.swift', local_identifier])
+
+ this_asset = self.all_assets[self.all_positions[local_identifier]]
+
+ try:
+ this_asset['albums'].remove('Rejected')
+ except KeyError:
+ pass
+
+ try:
+ this_asset['albums'].remove('Needs Action')
+ except KeyError:
+ pass
+
+ this_asset['albums'].add('Flagged')
+
+ this_asset['state'] = get_asset_state(this_asset)
+
+ self.get_response.cache_clear()
+
+ def reject(self, local_identifier):
+ subprocess.check_call(['swift', 'scripts/reject.swift', local_identifier])
+
+ this_asset = self.all_assets[self.all_positions[local_identifier]]
+
+ try:
+ this_asset['albums'].remove('Flagged')
+ except KeyError:
+ pass
+
+ try:
+ this_asset['albums'].remove('Needs Action')
+ except KeyError:
+ pass
+
+ this_asset['albums'].add('Rejected')
+
+ this_asset['state'] = get_asset_state(this_asset)
+
+ self.get_response.cache_clear()
+
+ def needs_action(self, local_identifier):
+ subprocess.check_call(['swift', 'scripts/needs_action.swift', local_identifier])
+
+ this_asset = self.all_assets[self.all_positions[local_identifier]]
+
+ try:
+ this_asset['albums'].remove('Flagged')
+ except KeyError:
+ pass
+
+ try:
+ this_asset['albums'].remove('Rejected')
+ except KeyError:
+ pass
+
+ this_asset['albums'].add('Needs Action')
+
+ this_asset['state'] = get_asset_state(this_asset)
+
+ self.get_response.cache_clear()
- if 'Flagged' in asset['albums']:
- asset['state'] = 'Flagged'
- asset['albums'].remove('Flagged')
- elif 'Rejected' in asset['albums']:
- asset['state'] = 'Rejected'
- asset['albums'].remove('Rejected')
- else:
- asset['state'] = 'Unknown'
- assert 'Flagged' not in asset['albums']
- assert 'Rejected' not in asset['albums']
- self.all_assets = all_assets
photos_data = PhotosData()
@@ -45,20 +129,13 @@ photos_data = PhotosData()
@app.route("/")
def index():
- all_assets = photos_data.all_assets
-
try:
local_identifier = request.args['localIdentifier']
except KeyError:
+ all_assets = photos_data.all_assets
return redirect(url_for('index', localIdentifier=all_assets[-1]['localIdentifier']))
- position = next(i for i, asset in enumerate(all_assets) if asset['localIdentifier'] == local_identifier)
-
- prev_five = all_assets[position - 5:position]
- this_asset = all_assets[position]
- next_five = all_assets[position + 1:position + 6]
-
- return render_template('index.html', assets=all_assets, position=position, prev_five=prev_five, this_asset=this_asset, next_five=next_five)
+ return photos_data.get_response(local_identifier)
@functools.cache
@@ -79,6 +156,35 @@ def get_image_path(local_identifier):
return subprocess.check_output(['swift', 'get_asset_jpeg.swift', local_identifier, '2048']).decode('utf8')
+@app.route('/actions/flag')
+def flag():
+ local_identifier = request.args['localIdentifier']
+
+ photos_data.flag(local_identifier)
+
+ return redirect(url_for('index', localIdentifier=local_identifier))
+
+
+@app.route('/actions/reject')
+def reject():
+ local_identifier = request.args['localIdentifier']
+
+ photos_data.reject(local_identifier)
+
+ return redirect(url_for('index', localIdentifier=local_identifier))
+
+
+@app.route('/actions/needs_action')
+def needs_action():
+ local_identifier = request.args['localIdentifier']
+
+ photos_data.needs_action(local_identifier)
+
+ return redirect(url_for('index', localIdentifier=local_identifier))
+
+
+
+
@app.route('/image')
def image():
local_identifier = request.args['localIdentifier']
@@ -87,4 +193,4 @@ def image():
if __name__ == '__main__':
- app.run(debug=True)
+ app.run(debug="--debug" in sys.argv)
templates/index.html (6732) → templates/index.html (8334)
diff --git a/templates/index.html b/templates/index.html
index 049eff0..2329665 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -81,6 +81,16 @@
background: red;
}
+ #thumbnails div.thumbnail.state-Needs-Action {
+ border-color: blue;
+ border-width: 2px;
+ margin: 0;
+ }
+
+ #thumbnails div.thumbnail.state-Needs-Action .state {
+ background: blue;
+ }
+
#thumbnails div.this_asset {
display: inline-block;
}
@@ -118,8 +128,32 @@
/* 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;
+ }
</style>
+<details id="debug">
+ <summary>debug</summary>
+ <strong>this asset:</strong> {{ this_asset['localIdentifier'] }}
+</details>
+
<div id="thumbnails">
{% set placeholder_prev_five = 5 - prev_five|length %}
{% for _ in range(placeholder_prev_five) %}
@@ -148,10 +182,10 @@
</div>
<p class="metadata">
+ {{ this_asset['localIdentifier'] }} /
{% if this_asset['albums'] %}
<strong>albums:</strong> {{ this_asset['albums'] | join(', ') }} /
{% endif %}
- <strong>state:</strong> {{ this_asset['state'] }} /
<strong>creation date:</strong> {{ this_asset['creationDate'] }}
</p>
@@ -161,3 +195,35 @@
<!--{% for a in assets %}
<img src="{{ url_for('thumbnail', localIdentifier=a['localIdentifier']) }}">
{% endfor %} -->
+
+<script>
+function httpPOST(url)
+{
+ var xmlHttp = null;
+
+ xmlHttp = new XMLHttpRequest();
+ xmlHttp.open("POST", url, false);
+ xmlHttp.send(null);
+ return xmlHttp.responseText;
+}
+
+document.onkeydown = function(e) {
+ console.log(e);
+ console.log(e.key);
+ console.log(e.key === "2");
+ if (e.key === "ArrowLeft") {
+ window.location = "/?localIdentifier={{ prev_five[-1]['localIdentifier'] }}";
+ } else if (e.key === "ArrowRight") {
+ {% if next_five %}
+ window.location = "/?localIdentifier={{ next_five[0]['localIdentifier'] }}";
+ {% endif %}
+ } else if (e.key === "1") {
+ window.location = "/actions/flag?localIdentifier={{ this_asset['localIdentifier'] }}";
+ } else if (e.key === "2") {
+ window.location = "/actions/reject?localIdentifier={{ this_asset['localIdentifier'] }}";
+ } else if (e.key === "3") {
+ window.location = "/actions/needs_action?localIdentifier={{ this_asset['localIdentifier'] }}";
+ }
+}
+</script>
+
templates/thumbnail.html (455) → templates/thumbnail.html (579)
diff --git a/templates/thumbnail.html b/templates/thumbnail.html
index 143e700..98c4711 100644
--- a/templates/thumbnail.html
+++ b/templates/thumbnail.html
@@ -1,12 +1,14 @@
<a href="{{ url_for('index', localIdentifier=asset['localIdentifier'])}}">
- <div class="thumbnail state-{{ asset['state'] }}">
+ <div class="thumbnail state-{{ asset['state'] | replace(' ', '-') }}">
{% if asset['state'] == 'Flagged' %}
<div class="state">✓</div>
{% elif asset['state'] == 'Rejected' %}
<div class="state">✘</div>
+ {% elif asset['state'] == 'Needs Action' %}
+ <div class="state">ℹ</div>
{% endif %}
<img
src="{{ url_for('thumbnail', localIdentifier=asset['localIdentifier']) }}"
- class="thumbnail state-{{ asset['state'] }}">
+ class="thumbnail state-{{ asset['state'] | replace(' ', '-') }}">
</div>
</a>