1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
|
.Dd August 24, 2026
.Dt WHATCD-RESOLVER 1
.Os
.Sh NAME
.Nm whatcd-resolver
.Nd browse, rank and download music from a Gazelle tracker
.Sh SYNOPSIS
.Nm whatcd-resolver-go
.Sh DESCRIPTION
.Nm
is a web UI over the Redacted API.
It mirrors search results into PostgreSQL, ranks the torrents of each release
group so that one of them can be called
.Dq the best ,
hands that one to a local Transmission daemon, and then streams the downloaded
audio files and cover art back out over HTTP.
.Pp
There is no command line interface: all configuration is through the
environment, and everything else happens in the browser.
.Ss Two implementations
This directory contains two implementations of the same program.
The original is in Haskell
.Pq Pa Main.hs , Pa src/*.hs ,
built as the
.Va whatcd-resolver
attribute; the Go port is in the top-level
.Pa *.go
files, built as
.Va whatcd-resolver-go .
They share no code, only
.Pa static/ .
Both builds list their sources explicitly, so neither sees the other's files.
.Pp
This page documents the Go port.
The most visible differences from the Haskell version are that it does not
start its own PostgreSQL
.Pq see Sx DATABASE
and that it does not export traces over the OTLP protobuf encoding
.Pq see Sx TRACING .
Everything the user sees is intended to be identical, so the two can be run
against the same database and compared.
.Sh ENVIRONMENT
.Bl -tag -width WHATCD_RESOLVER_TRANSMISSION_DOWNLOAD_DIRECTORY
.It Ev WHATCD_RESOLVER_DATABASE_URL
PostgreSQL connection URL, e.g.
.Pa postgres://user@localhost:5433/whatcd_resolver .
Required; the process exits immediately without it.
.It Ev WHATCD_RESOLVER_REDACTED_API_KEY
API key for Redacted.
If unset,
.Xr pass 1
is consulted for
.Pa internet/redacted/api-keys/whatcd-resolver ,
as in the Haskell version.
.It Ev WHATCD_RESOLVER_LISTEN
Address to bind, default
.Pa 127.0.0.1:9094 .
The Haskell version listens on 9093; the default differs on purpose so both can
run at once.
.It Ev WHATCD_RESOLVER_TRANSMISSION_HOST , Ev WHATCD_RESOLVER_TRANSMISSION_PORT
Transmission RPC endpoint, default
.Pa localhost
and
.Pa 9091 .
.It Ev WHATCD_RESOLVER_TRANSMISSION_DOWNLOAD_DIRECTORY
Where Transmission puts its downloads.
When unset or not a directory, file streaming and cover art are disabled and the
rest of the UI still works.
.It Ev WHATCD_RESOLVER_TOOLS
Directory of helper binaries; only
.Pa exiftool
is looked up there.
Falls back to
.Ev PATH .
.It Ev OTEL_EXPORTER_OTLP_ENDPOINT
Trace collector, default
.Pa http://localhost:4318 .
.It Ev OTEL_SERVICE_NAME
Service name in traces, default
.Pa whatcd-resolver .
.It Ev WHATCD_RESOLVER_DISABLE_TRACING
When set to any value, no traces are exported and no collector is contacted.
.El
.Sh DATABASE
The Haskell version starts a private PostgreSQL 14 through
.Pa tmp-postgres ,
under
.Pa $XDG_DATA_HOME/whatcd-resolver/database
on port 5431.
The Go port does not: it connects to a server that is already running, named by
.Ev WHATCD_RESOLVER_DATABASE_URL .
.Pp
The schema is applied at every startup and is idempotent.
It is ported verbatim, which matters more than it might appear:
.Va seeding_weight
and
.Va artist_ids
are
.Sy STORED
generated columns computed by a
.Xr plpgsql 7
function from the API JSON.
Changing that function, or the JSON that feeds it, silently re-ranks every
torrent in the database rather than failing.
For the same reason the JSON normalisation in the API client
.Pq renaming Dq snatched No to Dq snatches , and the per-endpoint torrent id key to Dq torrentId
is load-bearing and must not be tidied away.
.Ss Development database
The authoritative data lives on
.Pa haku .
To work against a copy of it locally, dump it read-only and restore it into a
local cluster:
.Bd -literal -offset indent
ssh haku 'sudo -u whatcd-resolver pg_dump \e
-h /var/lib/whatcd-resolver/.local/share/whatcd-resolver/database-socket \e
-p 5431 -d postgres -Fc -Z6 -f /tmp/whatcd-dump.pgc'
scp haku:/tmp/whatcd-dump.pgc ./tmp/
pg_restore -d whatcd_resolver --no-owner --no-privileges ./tmp/whatcd-dump.pgc
.Ed
.Pp
The deployed database contains two structures that no code refers to:
.Va redacted.torrent_artists ,
a denormalised artist-to-torrent join table that is roughly half stale, and
.Va redacted.artist_filter_cache ,
a materialised view of the artist filter subqueries.
Both are abandoned optimisation experiments and should be dropped from any
restored copy.
.Pp
The tests skip themselves unless
.Ev WHATCD_RESOLVER_DATABASE_URL
is set, so
.Ic go test ./...
works on a fresh checkout.
With it set, they run against the restored copy, checking among other things
that the migration does not disturb existing data and that every stored
.Pa .torrent
still decodes.
.Sh TRACING
Spans are exported to an OTLP collector, in practice the
.Pa jaeger-all-in-one
that runs beside the service.
Attribute names carry a
.Dq _.
prefix, as in the Haskell version, so existing saved queries keep matching.
.Pp
The OTLP payload is encoded as JSON by
.Pa otel.go
rather than using the official exporter.
That exporter imports
.Pa go.opentelemetry.io/proto/otlp ,
which pulls in grpc, genproto and grpc-gateway
.Pq about 45 packages
even when only the HTTP transport is used, and every one of those would have to
be pinned by hand in
.Pa go-deps.nix ,
since
.Xr buildGo 7
does no module resolution.
The JSON encoding of OTLP is part of the specification and Jaeger accepts it on
the same endpoint, so the dependency buys nothing here.
.Sh SEE ALSO
.Xr transmission-daemon 1 ,
.Xr buildGo 7
.Pp
The Redacted API is documented on the tracker's own wiki, which requires an
account.
.Sh AUTHORS
.An Profpatsch
.Sh CAVEATS
.Bl -bullet
.It
htmx and howler are fetched from the network at startup and their SHA-384
computed, so the process needs network access to start and will fail without it.
A failed prefetch is fatal, which is not hypothetical: the third asset,
.Pa stylize.css ,
had to be vendored into
.Pa resources/
because its upstream repository was deleted from GitHub, at which point neither
implementation could start any more.
The deployed instance survived only because it had been running since before the
deletion and still held the file in memory, which is where the vendored copy was
recovered from.
See
.Pa resources/README.md .
.Pp
Note what the
.Va integrity
attribute does and does not do for the two assets that are still fetched: the
hash is computed from whatever was just downloaded, so it guarantees the browser
sees the same bytes the server did, but it does not pin the upstream content.
Pinning would mean writing the expected hash next to the URL and refusing to
start on a mismatch.
.It
Recommendations are effectively untested against real data:
.Va redacted.similar_artists
is empty in the deployed database, so the feature has never run there.
The query is ported verbatim and is known to execute, but its output has not
been compared against the Haskell version's.
.It
Torrents removed from Transmission are treated as never downloaded: both the
hash and the stored
.Pa .torrent
are cleared, and the UI offers to fetch the file again.
.It
Fetching recommendations makes one API call per favourite artist, serially, and
can take minutes.
It is deliberately synchronous, as in the Haskell version.
.El
|