A self-hosted microservice that serves random photos from a Google Photos album to your TRMNL device.
It caches your album's photo manifest in memory (default: 1 hour) so TRMNL never times out waiting for Google to paginate a large album, and every request returns an instantly randomized photo at the exact device resolution.
TRMNL Device ──GET /api/photo──▸ This Service ──▸ Google Photos API
(cached)
GET /api/photo— Returns a random photo URL + date from your album.GET /api/albums— Utility endpoint to look up your album's internal ID.GET /health— Health check with cache stats.
- A Google account with photos in at least one album.
- Docker & Docker Compose on your server.
- A local machine with a web browser (one-time OAuth setup).
- Go to the Google Cloud Console and create a new project.
- Navigate to APIs & Services → Library and enable the Photos Library API.
- Navigate to OAuth consent screen, configure as External, and add your email as a test user.
- Navigate to Credentials → Create Credentials → OAuth client ID.
- Select Desktop app as the application type.
- Download the JSON file and rename it to
credentials.json.
Your server is headless. You must generate token.json on a machine with a browser, then copy it to the server.
# Install dependencies
pip install google-auth-oauthlib google-api-python-client
# Place credentials.json in this directory, then run:
python auth.pyA browser window will open. Log in and authorize the app. This creates token.json in your working directory.
- Copy
token.jsoninto adata/subdirectory on your server:
your-project/
├── app.py
├── auth.py
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
├── .env
└── data/
└── token.json
- Create your
.envfile:
cp .env.example .env- Start the container:
docker compose up -d --buildThe API requires the internal 64-character Album ID, not the public web URL.
- Open
http://<your-server-ip>:5050/api/albumsin your browser. - Find your target album in the JSON and copy its
"id"value. - Paste it into your
.envfile asALBUM_ID. - Recreate the container:
docker compose up -d --force-recreateIn your TRMNL plugin settings, set:
- URL:
http://<your-server-ip>:5050/api/photo - Method:
GET - Body: (empty)
| Variable | Required | Default | Description |
|---|---|---|---|
ALBUM_ID |
Yes | — | Internal Google Photos album ID |
CACHE_TTL |
No | 3600 |
Seconds to cache the album manifest |
PHOTO_WIDTH |
No | 1072 |
Requested image width (px) |
PHOTO_HEIGHT |
No | 1448 |
Requested image height (px) |
TOKEN_PATH |
No | /data/token.json |
Path to the OAuth token inside the container |
Returns a random photo from the configured album.
Response:
{
"photo_url": "https://lh3.googleusercontent.com/...=w1072-h1448",
"relative_date": "2023-08-14"
}Lists all albums in the authenticated Google Photos account. Use this to find your ALBUM_ID.
{
"status": "ok",
"cached_items": 342
}Token expired / 401 errors
The service auto-refreshes expired access tokens. If it still fails, re-run python auth.py locally to generate a fresh token.json and replace the one in data/.
"No photos found in album"
Double-check that ALBUM_ID matches the id field from /api/albums (not the URL slug).
Container keeps restarting
Check logs: docker compose logs -f photos-agent
MIT