Augustus is a Kotlin Multiplatform desktop application that keeps a catalogue in a local PostgreSQL database. Each kind of media is a feature module with its own tab, database schema, forms and reports:
| Module | Contents |
|---|---|
feature-asian-media |
Manga, manhwa, manhua and novels |
feature-games |
Games, with series, categories, play time and ratings |
feature-books |
Books and authors |
feature-sandbox |
Every form control and report column, with no tables of its own |
feature-spotify |
A copy of the trash of its Spotify extension, with no tab |
A browser extension adds series from NovelUpdates and MangaUpdates, and repeats in the catalogue what you do on NovelUpdates and SteamDB. A Spicetify extension skips the songs and artists you never want Spotify to play, and keeps a copy of them.
The application is built on these parts:
| Part | What it does |
|---|---|
| Form Engine | Forms from a schema of controls |
| Report Engine | Tables with filters, sorting and saved layouts from a SQL query |
| Octavius for PostgreSQL | External library: wire-protocol driver, SQL client and migrator |
| Octavius I18n | External Gradle plugin: typed translation objects from JSON files |
Also used: Compose Multiplatform with Material 3, HikariCP, Koin, Ktor and kotlinx-serialization.
You need:
- JDK 25
- PostgreSQL 18 or newer, because the driver uses wire protocol 3.2 and older servers reject it
- a database named
augustus, with thepg_trgmextension available, because a migration creates it
./gradlew :desktop-app:runThe connection is configured in %APPDATA%\Augustus\settings.json, or in the home directory on systems without
APPDATA. By default it uses jdbc:octavius://localhost:5432/augustus and the user postgres.
Any failure while connecting or migrating opens the same window with connection settings and a retry button. The cause is
in the console and in desktop-app/logs/octavius.log.
Other commands:
./gradlew :form-engine:desktopTest # tests
./gradlew dokkaGenerateHtml # KDoc, into build/dokka/htmlAugustus/
├── desktop-app/ # Entry point: settings, migrations, main window
│
├── form-engine/ # Forms
├── report-engine/ # Tables
├── ui-core/ # Theme, dialogs, snackbars, shared components
├── navigation/ # Tabs and screen stacks
│
├── feature-asian-media/
├── feature-books/
├── feature-games/
├── feature-sandbox/
├── feature-spotify/ # Trash copy API and its Spicetify extension (spicetify/)
├── feature-contract/ # FeatureModule and ApiModule interfaces
│
├── api-server/ # Embedded Ktor server for the extension
├── shared-contract/ # Code shared by the application and the extension
└── browser-extension/
At startup desktop-app scans the classpath for objects under org.octavius that implement FeatureModule. Each one
supplies a tab, API routes, screens that can be opened from outside, and a database schema. settings.gradle.kts
includes every feature-* directory that has a build.gradle.kts, and desktop-app depends on all of them, so a new
feature is a new directory and nothing else changes.
SQL is written by hand with the Octavius client. Each feature with tables has its own schema, migration sequence and
history table. Migrations are SQL files in src/desktopMain/resources/db/migration/<schema>/, named
V<yyyy.MM.dd.HH.mm>__<description>.sql, and they run at startup: public first, then the feature schemas in feature
order.
Each module has i18n/{en,pl}.json in its resources and a generated typed object: BooksTr.Form.titlePl() reads the key
form.titlePl. The active language is global and comes from the application settings.
browser-extension/ is a Manifest V3 Chrome extension written in Kotlin/JS. It reads pages on the sites below and talks to
the running application at http://localhost:8080. The application does all the writing to the database. The extension
sends nothing to the sites it reads.
| Site | What you do | What happens in Augustus |
|---|---|---|
| NovelUpdates, MangaUpdates | Open the popup on a series page | The series is checked, and can be added, linked or opened |
| NovelUpdates | Move a series to another reading list | The status of the series changes |
| SteamDB | Click "Ignore" on a game | The status of the game becomes Trash |
./gradlew :browser-extension:assembleBrowserExtensionOpen chrome://extensions, turn on developer mode, choose "Load unpacked" and pick browser-extension/build/extension/.
After a rebuild, reload the extension on that page. The application has to be running for changes to reach the database.
Clicking the extension's icon on a series page reads the titles, the publication type, the original language and the site's id of the series. Titles in non-Latin scripts are dropped. The application looks for the series by that id, then by trigram similarity of the titles, and the popup offers one of these:
| Found | Buttons |
|---|---|
| A series with this id | Open in Augustus |
| A similar title | Link to that title, Add as a new title |
| Nothing | Add to Augustus |
Adding and linking store the site's id, so the next check recognises the series, and open the title's form in the application. Linking also appends the new titles and adds a publication of this type if the title has none. The popup follows the browser's language.
The service worker watches requests to updatelist.php with act=move, so a move counts wherever it was clicked: in the
series finder, on a series page or on a reading list. The content script describes the series from the page, and the
application decides:
- The series id is known: the status changes.
- Exactly one title has this name, it has no NovelUpdates id yet, and the match is on its main title or its language agrees: the id is linked and the status changes.
- No title has this name: a new title is created with a publication and the id.
- Anything else is ambiguous: nothing is saved, and the message lists the candidates.
Names are compared without regard to case. NovelUpdates has only novels, so only light, web and published novel publications change status, and a title with only a manga gets a new publication.
novelUpdatesListStatuses in AsianMediaApi maps list ids to statuses, and a list missing from it saves nothing. List ids
are separate for each NovelUpdates account.
This needs the SteamDB extension, whose "Ignore" button appears on game pages and in the tooltip over games on lists. The content script listens for that extension's reply and acts on a successful ignore. The application then decides:
- The appid is known: the status changes.
- Exactly one game has this name: the appid is linked and the status changes. A game can have several appids.
- No game has this name: a new game is created with the appid.
- Several games have this name: nothing is saved, and the message lists the candidates.
Names are compared by letters and digits alone, without regard to case, because Steam adds ™, ® and emoji. These are also removed from the name of a new game. The status is always Trash, and the game's play time and ratings are deleted.
Ignoring a game in the Steam store itself is not picked up.
Each repeated change shows a message in the corner of the page: green for 3 seconds when it was saved, red for 10 seconds when it was not.
When the application is not running, the change waits in chrome.storage.local and goes out with the next change or the
next start of the service worker. A change the application answers with an error status is reported once and not sent
again. A series or game the content script can't find on the page is reported and never sent.
| Module | Role |
|---|---|
popup |
Compose UI of the popup and the HTTP client for the check, add, link, open routes |
content-script |
Runs on the three sites: parses pages, listens to SteamDB, shows messages |
background |
Service worker: watches updatelist.php, holds the queue, sends changes |
chrome-api |
external declarations for the Chrome APIs in use |
feature-asian-media and feature-games also have a js target: commonMain holds the DTOs shared with the application
and jsMain holds the page parsers. The routes are defined in AsianMediaApi and GamesApi:
| Route | Called by | Effect |
|---|---|---|
POST /api/asian-media/check |
popup | Looks for a series by id or similar title |
POST /api/asian-media/add |
popup | Adds a title and opens its form |
POST /api/asian-media/link |
popup | Links a series to a title and opens its form |
POST /api/asian-media/open |
popup | Opens a title's form |
POST /api/asian-media/novelupdates/list-move |
background | Repeats a reading list move |
POST /api/games/steam/ignore |
background | Repeats an ignored game |
feature-spotify/spicetify/trashbin.js is a Spicetify extension that skips the songs and artists you put in its trash
and keeps a copy of the trash in Augustus. The idea, the storage keys and the storage format come from Spicetify's
Trashbin, so a trash that Trashbin left behind
keeps working.
./gradlew :feature-spotify:installTrashbincopies it to %APPDATA%\spicetify\Extensions and runs spicetify apply, which restarts Spotify. The extension has to be
enabled once with spicetify config extensions trashbin.js.
The extension's labels and messages are in Polish. "Wrzuć do kosza" (put in the trash) is in the context menu of songs and artists, in a button next to the heart in every tracklist row, and in the player bar for the song that is playing. A song in the trash, or by an artist in the trash, is skipped when it starts and when it goes to the trash while playing. Going back to it with the previous button plays it. The settings, under "Kosz" (trash) in the profile menu, turn the player bar button on and off and show the size of the trash.
The trash itself stays in Spotify's local storage, as bare URIs, because the extension has to skip a song at once, with or without the application. The copy in Augustus works like this:
- After every change, and 5 seconds after Spotify starts, the extension sends the whole trash. The application only adds to its copy, so losing the local storage, for example by reinstalling Spotify, leaves the copy as it was.
- Only "Wyjmij z kosza" (take out of the trash) deletes from the copy. A removal made while the application is down waits in local storage and goes out before the next sync.
- For entries the copy has no description of, the extension asks Spotify, with the same GraphQL queries the client uses, and sends the title, album and artists of each song and the name of each artist.
- "Przywróć z Augustusa" (restore from Augustus) in the settings adds back everything the copy has and local storage lacks.
| Route | Effect |
|---|---|
GET /api/spotify/trash |
URIs of everything in the trash |
POST /api/spotify/trash/sync |
Adds missing entries, returns the URIs without a description |
POST /api/spotify/trash/describe |
Saves titles, albums and artists |
POST /api/spotify/trash/remove |
Removes entries taken out of the trash |