Post

Importing Yamtrack Watch History into Silo with a Read-Only Plugin

I setup Silo earlier this week. I wanted to sync my watch history, but with Traktv completely crippling their API without a membership, I’ve been hosting Yamtrack for a while now.

So, I wanted my existing Yamtrack watch history into Silo, including movies, individual episodes, rewatches, TMDB identifiers, and watch dates. The safest design I could think of was just a read-only one-way Silo watch-sync plugin backed by a PostgreSQL account that physically could not modify Yamtrack.

The flow looks like this:

1
2
3
4
5
6
7
8
9
Yamtrack PostgreSQL
        │
        │ SELECT only
        ▼
Silo Yamtrack plugin
        │
        │ ListRemoteState()
        ▼
Silo watch-sync service

Yamtrack remains authoritative. The plugin never issues INSERT, UPDATE, or DELETE, and the database role receives no write privileges.

Silo is being actively developed and isn’t done. Here’s the current state of my apps:

  • Yamtrack v0.25.3 using PostgreSQL;
  • Silo with watch_sync_provider.v1 support from silo-plugin-sdk v0.13.0;
  • Docker Compose;
  • movies and individual TV episodes identified through TMDB.

What works

The plugin imports:

  • completed movies
  • watched episodes
  • movie and episode play counts
  • rewatches
  • last-watched timestamps
  • TMDB movie IDs
  • TMDB series IDs plus season and episode numbers

It intentionally does not import paused playback positions, ratings, notes, favorites, or watchlists. It also cannot export watched changes, remove Yamtrack history, or scrobble playback. The provider advertises import-only capabilities to Silo, and the PostgreSQL role provides a second independent safety boundary.

Downloads

Check the Docker host’s architecture:

1
uname -m

Then download the matching Silo plugin archive:

Here are some SHA-256 checksums for you:

1
2
3
4
5
cffd725d8c71248a5325a4fd9b796f8f75dadeeae200786fd98e43ae5cfad796  silo-yamtrack-watchsync-linux-amd64.zip

051a0653dcdab1049b1d7376cee6954ce093cdab3de5f5d4fde2c110f40d2b92  silo-yamtrack-watchsync-linux-arm64.zip

e68dc8ea80b71855a6841c1047303869d5598b9470d4fa09d679c300f505e51f  silo-plugin-yamtrack-milestone-3-source.zip

Verify a download with:

1
sha256sum silo-yamtrack-watchsync-linux-amd64.zip

Before starting

You will need:

  • Access to Yamtrack’s PostgreSQL administrator account
  • Silo administrator access
  • The numeric Yamtrack user ID whose history should be imported (mine was 1)
  • A backup of Silo if it already contains important watch-state data

The plugin cannot alter Yamtrack, but a successful sync does change watched state in the selected Silo profile. Start with one profile and spot-check the result.

1. Create a read-only Yamtrack role

Choose a new, randomly generated password. Do not reuse Yamtrack’s application password.

Create a file named create-readonly-role.sql:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE ROLE silo_sync WITH LOGIN PASSWORD 'CHANGE_ME_TO_A_STRONG_PASSWORD';

GRANT CONNECT ON DATABASE yamtrack TO silo_sync;
GRANT USAGE ON SCHEMA public TO silo_sync;
GRANT SELECT ON TABLE
    public.app_item,
    public.app_movie,
    public.app_season,
    public.app_episode
TO silo_sync;

-- Intentionally no INSERT, UPDATE, or DELETE grants.

Run it through the Yamtrack database container. Replace the container, administrator, and database names if your deployment differs:

1
2
3
docker exec -i yamtrack-db \
  psql -U yamtrack -d yamtrack \
  < create-readonly-role.sql

Expected output resembles:

1
2
3
4
CREATE ROLE
GRANT
GRANT
GRANT

If the role already exists, rotate its password instead of trying to create it again:

1
2
3
docker exec -it yamtrack-db \
  psql -U yamtrack -d yamtrack \
  -c "ALTER ROLE silo_sync WITH PASSWORD 'A_NEW_STRONG_PASSWORD';"

Keep the password private. If it appears in a screenshot, terminal transcript, issue, or chat, rotate it. The role is read-only, but credential hygiene still matters.

2. Find the Yamtrack user ID

Open PostgreSQL:

1
2
docker exec -it yamtrack-db \
  psql -U yamtrack -d yamtrack

Then run:

1
2
3
4
5
6
7
8
SELECT user_id, COUNT(*) AS tracked_rows
FROM (
  SELECT user_id FROM app_movie
  UNION ALL
  SELECT user_id FROM app_season
) x
GROUP BY user_id
ORDER BY tracked_rows DESC;

For a single-user installation, the result is often:

1
2
3
 user_id | tracked_rows
---------+-------------
       1 |        ...

Record the numeric ID. This guide uses 1.

3. Confirm the Docker network name

List the networks attached to Yamtrack’s database container:

1
2
docker inspect yamtrack-db \
  | jq -r '.[0].NetworkSettings.Networks | keys[]'

Use the exact result. In my installation, the network is named yamtrack, not yamtrack_yamtrack.

You can confirm it directly:

1
docker network inspect yamtrack

4. Run the diagnostic first

The diagnostic is optional but strongly recommended. It validates the connection, expected Yamtrack v0.25.3 schema, user ID, TMDB identities, play counts, and timestamps before Silo changes anything.

Extract the source package:

1
2
unzip silo-plugin-yamtrack-milestone-3-source.zip
cd silo-plugin-yamtrack

Export the read-only password and actual Docker network:

1
2
export YAMTRACK_SYNC_DB_PASSWORD='THE_silo_sync_PASSWORD'
export YAMTRACK_DOCKER_NETWORK='yamtrack'

If you previously exported the wrong network name, overwrite it as shown above. An exported shell value takes precedence over the Compose file’s default.

Build and run:

1
2
3
4
docker compose -f compose.example.yml build

docker compose -f compose.example.yml run --rm \
  yamtrack-sync-diagnose

Expected output looks like:

1
2
3
4
5
6
7
8
9
10
11
12
Connected to Yamtrack PostgreSQL ✓
Schema matches Yamtrack v0.25.3 expectations ✓
Yamtrack user ID: 1 ✓

Movies:   3229 unique watched, 7068 total plays, 3839 rewatches
Episodes: 18959 unique watched, 34552 total plays, 15593 rewatches

Recent movies (up to 10)
  Example Movie  TMDB=12345  plays=2  last=2026-08-01T20:00:00Z

Recent episodes (up to 10)
  Example Series  S02E03  TMDB=67890  plays=1  last=2026-08-02T21:00:00Z

Your counts will differ. Compare several recent records and at least one rewatched title with Yamtrack’s interface. This is how we caught semantic errors before letting Silo consume the data.

You can also save the complete normalized dataset locally:

1
2
3
docker compose -f compose.example.yml run --rm \
  yamtrack-sync-diagnose --json \
  > yamtrack-watch-state.json

There is nothing you need to do with the JSON file, it just contains your complete viewing history.

5. Put Silo on the Yamtrack network

The plugin runs inside Silo’s container network namespace. Silo must therefore be able to resolve yamtrack-db.

For a temporary test, attach the running Silo container:

1
docker network connect yamtrack YOUR_SILO_CONTAINER

For a durable configuration, add the existing external network to the Silo service in Silo’s Compose file:

1
2
3
4
5
6
7
8
9
10
11
services:
  silo:
    # Keep the rest of your existing Silo configuration.
    networks:
      - default
      - yamtrack

networks:
  yamtrack:
    external: true
    name: yamtrack

Use the actual Silo service name if it is not silo, then recreate that service normally. Do not create a second network with a similar name; both Silo and yamtrack-db must share the same network.

6. Upload the plugin to Silo

Open Admin → Plugins in Silo and upload the architecture-specific zip from the Community Plugins tab in the Manual section.

The archive contains exactly the two root entries Silo expects:

1
2
manifest.json
plugin

The manifest checksum is tied to that exact binary. Don’t unzip, modify, and recompress the archive before uploading it.

The plugin should appear as Yamtrack Watch Sync and expose one watch_sync_provider.v1 capability.

7. Configure the installed plugin

Open the plugin’s administrative configuration and enter:

1
2
3
4
5
6
Host:                    yamtrack-db
Port:                    5432
Database:                yamtrack
Read-only database user: silo_sync
SSL mode:                disable
Yamtrack user ID:        1

Do not place the PostgreSQL password in these global settings. The password goes into your user’s profile in the next step.

8. Connect the Silo profile

Do not use Admin → API Keys. That page generates credentials for clients accessing Silo itself.

Instead, open:

Profile → Settings → Watch Providers

Or navigate directly to:

1
/settings/watch-providers

Find the Yamtrack card and click Connect. Silo displays a password-style field labeled API key. For this local provider, paste the password belonging to the read-only silo_sync PostgreSQL role.

Silo encrypts the credential for the profile. The plugin receives it transiently, confirms that PostgreSQL accepts it, validates the expected schema and Yamtrack user ID, and returns the account identity to Silo.

When it succeeds, the card shows Connected:

Yamtrack connected before its first sync

9. Run the first sync

Click Sync now once.

The initial traversal is a complete snapshot divided into pages of at most 100 records. A library with about 22,000 unique watched items requires roughly 220 pages, but on a local Docker network it still finishes pretty darn quickly (a couple minutes).

The plugin returns stable provider keys and absolute play counts.

After a successful run, the card should show a nonzero Watched imported count while progress, favorites, watchlists, and exported records remain zero:

Yamtrack watched history successfully imported into Silo

The number imported by Silo may be lower than the diagnostic’s normalized total. Silo can only apply records it successfully matches to its catalog. Spot-check recent titles, older titles, several episodes, and a few heavily rewatched items before considering the migration validated.

What happens on later syncs?

Version 0.2.0 performs another complete snapshot each time. This is intentional for the initial release.

The full traversal:

  • remains read-only against Yamtrack
  • is idempotent in Silo
  • captures corrected historical dates
  • captures changed play counts
  • avoids cursor edge cases
  • was fast enough for a library of more than 22,000 unique records in testing

Incremental synchronization would reduce database reads, but it would also need careful handling for edited or removed historical records. Full snapshots are a reasonable default until performance becomes a real problem.

Troubleshooting

missing go.sum entry for module providing package github.com/jackc/pgx/v5/stdlib

Use the source archive linked above. It includes the generated go.sum, and its Dockerfile copies both go.mod and go.sum before downloading dependencies.

If an older extracted directory is still being used, replace it and rebuild:

1
docker compose -f compose.example.yml build --no-cache

network yamtrack_yamtrack declared as external, but could not be found

An old environment variable is probably overriding the Compose default:

1
2
3
4
export YAMTRACK_DOCKER_NETWORK='yamtrack'

docker compose -f compose.example.yml config \
  | grep -A3 '^networks:'

The rendered configuration should show:

1
2
3
4
networks:
  yamtrack:
    name: yamtrack
    external: true

Also check for an outdated value in .env:

1
grep YAMTRACK_DOCKER_NETWORK .env 2>/dev/null

Yamtrack does not appear under Watch Providers

Confirm that:

  • the plugin is installed and enabled under Admin → Plugins;
  • its configuration has been saved;
  • your Silo release supports silo-plugin-sdk v0.13.0 and watch_sync_provider.v1;
  • the plugin status shows no startup or manifest error.

The Connect button fails

Check that the Silo container—not merely the diagnostic container—is attached to the yamtrack network:

1
2
docker inspect YOUR_SILO_CONTAINER \
  | jq -r '.[0].NetworkSettings.Networks | keys[]'

Then confirm:

  • the host is yamtrack-db;
  • the database is yamtrack;
  • the user is silo_sync;
  • the selected Yamtrack user ID exists;
  • the password is current;
  • the four SELECT grants are present.

The import count is lower than the diagnostic count

The diagnostic reports normalized Yamtrack records. Silo reports records it imported into the profile. Catalog availability and matching can make the latter smaller. Verify specific TMDB IDs and SxxExx coordinates rather than judging success from one aggregate number.

PostgreSQL reports permission denied

Reapply only the connection, schema-usage, and four table-select grants from the role setup. Do not grant broad schema privileges and do not grant writes.

Why direct PostgreSQL access is acceptable here

Direct database integration is normally more fragile than an official API because migrations can change table or column names. The mitigation is to keep the adapter narrow and fail closed:

  • it verifies every required table and column before reading;
  • it targets a specific Yamtrack release schema;
  • it uses only four application tables;
  • it refuses to operate without the expected structure;
  • PostgreSQL itself blocks writes;
  • the Silo provider advertises no outbound operations.

If Yamtrack later exposes a stable public watch-history API, that can replace the database adapter without changing the current Silo mapping.

Final result

That combination let me migrate a couple years of family viewing history—including a truly impressive number of Polar Express rewatches—without handing experimental code the ability to modify the source data.

This post is licensed under CC BY 4.0 by the author.