Starting in July 2021, Google Photos will no longer offer unlimited storage for photos and videos. I was lucky enough to never rely on it completely and managed my media files collection in GNOME Shotwell. Why not to take it a bit further and allow network access from portable devices? It could provide media availability and almost completely substitute Google Photos.

The following components are required:

  • A Rapberry Pi 4 as a web server
  • The USB HDD with the media files collection
  • Tinc VPN set up on the server and every device that would be accessing the gallery
  • PiGallery2 or any other directory based media gallery viewer
  • A way to extract useful information from the Shotwell database to present the gallery in a user friendly manner
  • It may also be convenient to expose the media files via NFC to enable remote management of the collection with Shotwell.

Shotwell stores its metadata into a Sqlite database. We need to group the media files by events just like they are organized in the application. So why not to reflect these relations as a file system directory hierarchy, using symlinks to point to the physical files?

#!/usr/bin/env python

"""Create picture hierarchy based on the information from Shotwell.

Symlinks to the actual files will be put into the directories that
correspond to the descriptive events. The photo/video collection
will be suitable for browsing and viewing with PiGallery2
(https://github.com/bpatrik/PiGallery2).
"""

import sqlite3
from datetime import datetime
import pathlib
import os
import shutil


QUERY = """
SELECT f.filename, f.exposure_time, e.id, e.name FROM
(SELECT p.filename, p.exposure_time, p.event_id FROM PhotoTable p
UNION
SELECT v.filename, v.exposure_time, v.event_id FROM VideoTable v) AS f
JOIN EventTable e ON f.event_id = e.id
ORDER BY f.exposure_time;
"""

PHOTO_DB = "/home/sakhnik/Pictures/shotwell/data/photo.db"
ROOT = "/home/sakhnik/Pictures2"

# Remove the old view
shutil.rmtree(ROOT)

events = {}  # eid -> path

with sqlite3.connect(PHOTO_DB) as conn:
    c = conn.cursor()
    for (fname, timestamp, eid, ename) in c.execute(QUERY):
        # print(fname, timestamp, eid, ename)
        dir_path = events.get(eid)
        if not dir_path:
            dt = datetime.fromtimestamp(timestamp)
            if ename:
                dir_path = f"{ROOT}/{dt.year}/{dt.year}-{dt.month:02d}-{dt.day:02d} {ename}"
            else:
                dir_path = f"{ROOT}/{dt.year}/{dt.year}-{dt.month:02d}-{dt.day:02d}"
            events[eid] = dir_path
            pathlib.Path(dir_path).mkdir(parents=True, exist_ok=True)
        try:
            os.symlink(fname, f"{dir_path}/{timestamp}_{os.path.basename(fname)}")
        except Exception as e:
            print(e)

Here is a sample of the directory populated:

/home/sakhnik/Pictures2/
├── 1995
│   ├── 1995-05-25
│   │   ├── 801385896_scan0012.jpg -> /home/sakhnik/Pictures/1990/1995_05_25/scan0012.jpg
│   │   └── 801385912_scan0013.jpg -> /home/sakhnik/Pictures/1990/1995_05_25/scan0013.jpg
│   └── 1995-09-01 УФМЛ КНУ 9-Б клас
│       ├── 809941402_Untitled.jpg -> /home/sakhnik/Pictures/1990/1995_09_01/Untitled.jpg
│       └── 809982959_1995-9B.jpg -> /home/sakhnik/Pictures/1990/1995_09_04/1995-9B.jpg
...
├── 2009
│   ├── 2009-01-04 Лижний біг
│   │   ├── 1231057698_imgp3745.jpg -> /home/sakhnik/Pictures/2009/2009_01_04-Winter/imgp3745.jpg
│   │   ├── 1231057703_imgp3746.jpg -> /home/sakhnik/Pictures/2009/2009_01_04-Winter/imgp3746.jpg
│   │   ├── 1231060514_imgp3749.jpg -> /home/sakhnik/Pictures/2009/2009_01_04-Winter/imgp3749.jpg
│   │   ├── 1231061012_imgp3751.jpg -> /home/sakhnik/Pictures/2009/2009_01_04-Winter/imgp3751.jpg

Here is the result on the mobile screen:

PiGallery2 on the mobile screen