Skip to main content

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>