Preparing to abandon Google Photos
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: