4.6 KiB
4.6 KiB
RoyalRoad Updates — Project Plan
Overview
A Go CLI tool that reads RoyalRoad story URLs from a PocketBase collection, fetches each story's last-update time from RoyalRoad, and prints a sorted plain-text table showing story names and how recently each was updated.
Behavior
- Authenticate to PocketBase using credentials baked in at compile time.
- Query the
bookmarkscollection, filteringcategory = "Geschichten". - Extract the
urlfield from each matching record. - For each URL, sequentially fetch the RoyalRoad fiction page.
- Extract the fiction name and
dateModifiedfrom the embedded JSON-LD<script type="application/ld+json">block. - Sort results by
dateModifieddescending (most recent first). - Print a plain-text table with:
Story NamecolumnLast Updatecolumn showing relative time (e.g., "3 hours ago")
- When
--jsonis passed, output machine-readable JSON with actual ISO 8601 dates.
URL Filtering Rules
- Skip chapter URLs silently. A chapter URL contains
/chapter/in its path. If a bookmark points to a specific chapter, the user is not yet caught up; no update notification is wanted. - Skip non-RoyalRoad URLs silently. Only URLs whose host ends with
royalroad.com(with or withoutwww.) are processed. - Skip silently on any fetch or parse failure. A single bad URL must not crash the tool or clutter output.
Error Handling
| Scenario | Behaviour |
|---|---|
| PocketBase unreachable or auth fails | Exit code ≠ 0, error message on stderr |
| Zero valid RoyalRoad stories found after filtering | Exit code 0, empty table |
| Individual RoyalRoad page fails to fetch or parse | Silent skip (no output, no error) |
Tech Stack
| Layer | Choice |
|---|---|
| Language | Go |
| Build orchestration | Makefile |
| Go compiler | Official golang:1.22 Docker image (no local toolchain) |
| HTTP client | net/http (standard library) |
| JSON parsing | encoding/json (standard library) |
| HTML extraction | Regex/string search for <script type="application/ld+json"> |
| Date parsing | time.Parse(time.RFC3339, ...) (standard library) |
| Relative-time formatting | Custom helper from time.Time |
No external Go dependencies. The project is deliberately small and uses only the standard library.
Compile-Time Configuration
The three PocketBase connection values are compiled into the binary via -ldflags:
PB_URL=https://pocketbase.example.com
PB_USER=username
PB_PASSWORD=password
These are read from a .env file at build time. The resulting binary runs
standalone without any environment variables present.
Output Formats
Default (plain-text table)
Story Name Last Update
----------------------------------- -----------
A Fractured Truth 3 hours ago
Mother of Learning 6 years ago
--json
[
{
"name": "A Fractured Truth",
"date": "2024-12-11T08:55:49Z"
},
{
"name": "Mother of Learning",
"date": "2023-07-06T17:47:41Z"
}
]
Dates are always sortable ISO 8601 / RFC3339 strings in JSON output.
Data Flow
PocketBase (bookmarks collection)
└── Filter: category = "Geschichten"
└── Extract: url field
└── For each URL:
├── Skip if not royalroad.com
├── Skip if /chapter/ in path
├── Fetch fiction page (sequential, 30s timeout)
├── Extract JSON-LD script tag
├── Parse name + dateModified
└── Skip silently on any error
└── Collect valid results
└── Sort by dateModified descending
└── Print table or JSON
Development Loop
make check runs the full pipeline:
gofmt -d— fail if any file is unformatted.go vet ./...— static analysis.go test ./...— unit tests plus end-to-end test:- Compiles the binary using the local
.envcredentials. - Runs
./royalroadupdates --json. - Asserts:
- exit code 0
- stderr empty
- stdout is valid JSON
- JSON array contains ≥ 3 entries
- Each entry has
nameanddatefields
- Compiles the binary using the local
- If any step fails: fix issues, rerun
make check.
Open Decisions / Future Tuning
- Rate limiting: Requests are sequential to be gentle on RoyalRoad. If
Cloudflare blocks the tool, add a per-request delay (e.g.,
time.Sleep). - Relative time precision: Currently shows minutes/hours/days/years as coarse buckets. This can be refined if desired.