# photo-of-the-day
+
+```
+usage: photo-of-the-day.py [-h] [--database DATABASE] [--verbose] [--debug] [photosDirectory ...]
+
+Find photos and return the path of one at random.
+
+positional arguments:
+ photosDirectory Directories containing photos. Needed for initial discovery and a cache refresh.
+
+options:
+ -h, --help show this help message and exit
+ --database, -db DATABASE
+ SQLite3 filename to use for the discovery cache.
+ --verbose, -v Output information on photo discovery and selection.
+ --debug, -vv Output debug information on photo discovery and selection.
+```
+
+```
+# Either of the following commands will initialize a database
+# named 'cache.sqlite3' in the current directory.
+python3 photo-of-the-day.py -v --database ./cache.sqlite3 /Volumes/Photos/2025
+python3 photo-of-the-day.py /Volumes/Photos/2025
+
+# Assuming an initialized database, the following command
+# will return the path of a photo at random.
+python3 photo-of-the-day.py
+
+# Assuming an initialized database, the following command
+# will discover additional photos and add their path to cache.
+python3 photo-of-the-day.py /Volumes/Photos/2026/2026.01
+
+# Assuming an initialized database containing paths /Volumes/Photos/2025
+# and /Volumes/Photos/2026, the following command will purge it from all
+# photos under the former prefix and perform discovery anew.
+# Photos with the latter prefix will not be affected and no new photos
+# will be discovered respectively.
+# This is how the cache should be refreshed when new photos are added
+# to the file system.
+python3 photo-of-the-day.py /Volumes/Photos/2025
+```
--- /dev/null
+#!/usr/bin/python3 -u
+
+# kontaxis 2025-09-26
+
+import argparse
+import os
+import random
+import re
+import sqlite3
+import sys
+import time
+
+class photoOfTheDay:
+ debug = False
+ verbose = False
+
+ _connCursor = None
+
+ def __init__(self, dbPath, debug=True, verbose=True):
+ if not dbPath:
+ return None
+
+ self.debug = debug
+ self.verbose = verbose
+
+ conn = sqlite3.connect(dbPath)
+ debug and conn.set_trace_callback(print)
+ conn.row_factory = sqlite3.Row
+ conn.text_factory = str
+ self._connCursor = conn.cursor()
+
+ def __db_is_null(self):
+ c = self._connCursor
+ c.execute("SELECT name FROM sqlite_master "
+ "WHERE type='table' AND name='version'")
+ hasVersion = c.fetchone()
+ if hasVersion:
+ return False
+ return True
+
+ def __initialize_db_if_needed(self):
+ if not self.__db_is_null():
+ return
+
+ self.debug and print("DEBUG: Will initialize the database")
+
+ c = self._connCursor
+ c.execute("CREATE TABLE version (epoch integer);")
+ c.execute('INSERT INTO version VALUES(:epoch)', {
+ "epoch": str(int(time.time()))
+ })
+
+ c.execute("CREATE TABLE tree ("
+ "parent text NOT NULL, child text NOT NULL UNIQUE);")
+ c.execute("CREATE INDEX tree_parent ON tree (parent);")
+ c.execute("CREATE UNIQUE INDEX tree_tuple ON tree (parent, child);")
+
+ c.execute("CREATE TABLE photos ("
+ "prefix text NOT NULL, name text NOT NULL);")
+ c.execute("CREATE INDEX photos_prefix ON photos (prefix);")
+ c.execute("CREATE UNIQUE INDEX photos_tuple ON photos (prefix, name);")
+
+ c.connection.commit()
+
+ def __prune_db(self, prefix):
+ self.debug and \
+ print("DEBUG: Cache will forget {prefix}".format(prefix = prefix))
+
+ c = self._connCursor
+ c.execute("SELECT child FROM tree where parent=:me", { "me": prefix })
+ for link in c.fetchall():
+ self.__prune_db(link["child"])
+
+ c.execute("DELETE FROM tree WHERE parent=:me", { "me": prefix })
+ c.execute("DELETE FROM photos WHERE prefix=:me", { "me": prefix })
+
+ def __find_photos(self, targetPath):
+ if not targetPath:
+ return
+
+ self.__initialize_db_if_needed()
+
+ c = self._connCursor
+
+ for dirpath, dirnames, filenames in os.walk(targetPath, topdown=True):
+ self.debug and \
+ print("DEBUG: "
+ "{me} has {ndirs} directories and {nfiles} files".format(
+ me = dirpath,
+ ndirs = len(dirnames),
+ nfiles = len(filenames)))
+
+ self.__prune_db(dirpath)
+
+ for dirname in dirnames:
+ path = os.path.join(dirpath, dirname)
+ c.execute("INSERT INTO tree VALUES(:parent, :child)",
+ { "parent": dirpath, "child": path })
+
+ photos = []
+ for filename in filenames:
+ match = re.search(r"\.(jpg)$", filename, re.IGNORECASE)
+ if not match:
+ continue
+ photos.append((dirpath, filename))
+ if photos:
+ self.verbose and \
+ print("{me} has {nphotos} matching photos".format(
+ me = dirpath, nphotos = len(photos)))
+ c.executemany("INSERT INTO photos VALUES(?,?)", photos)
+
+ c.connection.commit()
+
+ def fetch(self, targetPaths: []):
+ if targetPaths:
+ for targetPath in targetPaths:
+ self.__find_photos(targetPath)
+
+ if self.__db_is_null():
+ self.debug and print("DEBUG: Database is uninitialized")
+ return None
+
+ c = self._connCursor
+ c.execute("SELECT count(*) as cnt FROM photos")
+ totalPhotos = c.fetchone()["cnt"]
+ if totalPhotos < 1:
+ self.debug and print("DEBUG: Database has zero photos")
+ return None
+
+ randomPhotoIndex = random.randrange(0, totalPhotos)
+ self.debug and print("DEBUG: "
+ "Will return photo at index {index}. "
+ "Total photos: {totalPhotos}".format(
+ index = randomPhotoIndex,
+ totalPhotos = totalPhotos))
+
+ c.execute("SELECT prefix, name FROM photos LIMIT 1 OFFSET :offset",
+ { "offset": randomPhotoIndex })
+ photo = c.fetchone()
+ return os.path.join(photo["prefix"], photo["name"])
+
+
+if __name__ == "__main__":
+
+ parser = argparse.ArgumentParser(
+ description="Find photos and return the path of one at random.")
+
+ parser.add_argument("--database", "-db",
+ default = "cache.sqlite3",
+ help = "SQLite3 filename to use for the discovery cache.")
+
+ parser.add_argument("photosDirectory", nargs='*',
+ help = "Directories containing photos. "
+ "Needed for initial discovery and a cache refresh.")
+
+ parser.add_argument("--verbose", "-v",
+ action="store_const", const=True, default=False,
+ help = "Output information on photo discovery and selection.")
+
+ parser.add_argument("--debug", "-vv",
+ action="store_const", const=True, default=False,
+ help = "Output debug information on photo discovery and selection.")
+
+ args = parser.parse_args()
+
+ photo_db = photoOfTheDay(dbPath = args.database,
+ debug = args.debug,
+ verbose = args.verbose)
+ print(photo_db.fetch(args.photosDirectory))
+