8.5 KiB
Executable File
8.5 KiB
Executable File
Phase 1 — Core Framework
All foundational types, interfaces, and helpers that every source implementation depends on.
Nothing in sources/ can be written until this phase is complete.
Reference:
- Tachiyomi source contract:
/Users/achmad/Documents/Belajar/Android/extensions-source/core/src/(HttpSource, CatalogueSource) - Kotlin originals:
/Users/achmad/Documents/Belajar/Android/extensions-source/lib-multisrc/madara/src/eu/kanade/tachiyomi/multisrc/madara/Madara.kt - Suwayomi source interface:
/Users/achmad/Documents/Belajar/Web/Suwayomi-Server/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/
1.1 Data Types — internal/source/types.go
SMangastructURL,Title,Artist,Author,Descriptionstring fieldsGenrestring (comma-separated, matches Tachiyomi convention)Statusint — 0=unknown, 1=ongoing, 2=completed, 3=licensed, 5=hiatus, 6=cancelledThumbnailURLstringInitializedbool (false untilGetMangaDetailshas been called)
SChapterstructURL,Name,Scanlatorstring fieldsDateUploadint64 — unix milliseconds (matches Tachiyomi)ChapterNumberfloat32
PagestructIndexintURLstring — page HTML URL or chapter URLImageURLstring — direct image URL (may be empty untilGetImageURLresolves it)
MangasPagestructMangas []SMangaHasNextPage bool
- Filter types
Filterinterface withName()andValue()methodsTextFilter— free-text inputCheckboxFilter— booleanTriStateFilter— ignore / include / exclude (0/1/2)SelectFilter— dropdown with named options (name + values []string)SortFilter— sortable list with ascending/descending stateGroupFilter— container of sub-filters
- Status constants (
StatusUnknown,StatusOngoing, etc.)
1.2 Source Interfaces — internal/source/interfaces.go
Sourceinterface (base)ID() int64Name() stringLang() string
CatalogueSourceinterface (embeds Source)SupportsLatest() boolGetPopularManga(page int) (MangasPage, error)GetLatestUpdates(page int) (MangasPage, error)GetSearchManga(page int, query string, filters []Filter) (MangasPage, error)GetMangaDetails(manga SManga) (SManga, error)GetChapterList(manga SManga) ([]SChapter, error)GetPageList(chapter SChapter) ([]Page, error)GetImageURL(page Page) (string, error)— no-op default for sources that embed image URLs directly in pagesGetFilterList() []Filter
- ID generation helper
GenerateSourceID(name, lang string) int64— MD5 of"${name.lowercase()}/$lang/1", first 8 bytes big-endian, sign bit cleared. Matches SuwayomiHttpSource.generateId. (The original plan incorrectly described JavaString.hashCode().)- Unit test against computed IDs (MangaDex/en, MangaDex/all, HeanCms/en)
1.3 HTTP Client — internal/httpclient/client.go
Clientstruct wrapping*http.Client- Persistent
cookiejar.Jar(one per source instance) - Per-host
rate.Limitermap (keyed byHost) withsync.Mutex - Configurable
RateLimit(requests/sec) andBurstper source - Configurable
Timeout(default 30s) - Default
User-Agentheader (Chrome/Android UA, matches Tachiyomi)
- Persistent
NewClient(opts ...Option) *Clientconstructor with functional optionsDo(req *http.Request) (*http.Response, error)— applies rate limit before executingGet(ctx context.Context, url string, headers map[string]string) (*http.Response, error)helperPost(ctx context.Context, url string, body io.Reader, contentType string, headers map[string]string) (*http.Response, error)helper- 429 retry logic
- Read
Retry-Afterheader (seconds or HTTP-date) - Sleep and retry up to N times (configurable, default 3)
- Read
- Optional
Refererinjection (configurable per client)
1.4 FlareSolverr — internal/httpclient/flaresolverr.go
FlareSolverrClientstructendpointstring (FlareSolverr v1 base URL, e.g.http://localhost:8191)- Underlying
*http.Clientfor talking to FlareSolverr itself
Get(ctx context.Context, url string, cookies []Cookie) (html string, cookies []Cookie, err error)- POST
{"cmd":"request.get","url":"...","maxTimeout":60000}to/v1 - Extract
solution.response(rendered HTML body) - Extract
solution.cookies→ convert to[]*http.Cookie
- POST
- Cookie reuse strategy — cookies returned to caller; caller injects into source's cookie jar
FlareSolverrResponseJSON struct (full response shape)- Config:
FLARESOLVERR_URLenv var (disable if empty —NewFlareSolverrClientreturns error)
1.5 GraphQL Helper — internal/httpclient/graphql.go
Used only to talk to upstream sources that expose GraphQL (mangahub, senkuro, allanime, luscious, stashapp). Our own API is plain REST.
GraphQLRequeststruct —Query string,Variables anyPost[T any](ctx context.Context, client *http.Client, url string, req GraphQLRequest, headers map[string]string) (T, error)- Marshal request to JSON
- POST with
Content-Type: application/json - Unmarshal
datafield of response into T - Surface
errors[]as a Go error if present
graphQLResponse[T any]envelope struct
1.6 Header Builders — internal/httpclient/headers.go
AndroidUA() string— Chrome/Android user-agent string (matches Tachiyomi default)DesktopUA() string— desktop Chrome user-agent (for sources requiring desktop)JSONHeaders() map[string]string—Content-Type: application/json,Accept: application/jsonFormHeaders() map[string]string—Content-Type: application/x-www-form-urlencodedWithRefererHeader(headers map[string]string, referer string) map[string]stringWithOrigin(headers map[string]string, origin string) map[string]string
1.7 HTML Parser — internal/parser/html.go
Thin wrappers over github.com/PuerkitoBio/goquery (Go equivalent of JSoup used in Tachiyomi extensions).
Parse(html string) (*goquery.Document, error)— parse raw HTML stringParseResponse(resp *http.Response) (*goquery.Document, error)— parse from HTTP response bodySelect(doc *goquery.Document, css string) *goquery.SelectionSelectFrom(sel *goquery.Selection, css string) *goquery.SelectionAttr(sel *goquery.Selection, name string) string— returns empty string if not foundAbsURL(sel *goquery.Selection, attr string, baseURL string) string— resolves relative URLsOwnText(sel *goquery.Selection) string— text of element excluding child elementsTextTrim(sel *goquery.Selection) string—.Text()with strings.TrimSpaceFirst(sel *goquery.Selection) *goquery.SelectionEach(sel *goquery.Selection, fn func(i int, s *goquery.Selection))— convenience wrapper
1.8 Registry — internal/registry/registry.go
- Package-level
map[int64]source.CatalogueSourceprotected bysync.RWMutex Register(s source.CatalogueSource)— panics on duplicate ID (caught at startup)Get(id int64) (source.CatalogueSource, bool)All() []source.CatalogueSource— returns sorted slice (by ID) for deterministic listing- Each source package must call
registry.Register(NewXxx())in itsinit()function - All source packages blank-imported in
cmd/server/main.go(done when sources exist)
1.9 Docker — Dockerfile + compose.yml
Dockerfile— multi-stage build (golang:1.26-alpinebuilder +distroless/static-debian12runtime)compose.yml— services:app,postgres,flaresolverrpostgres— official image, healthcheck, named volumeflaresolverr— ghcr.io/flaresolverr/flaresolverr, depends on nothingapp— builds fromDockerfile, depends on postgres healthcheck, env vars wired
Checklist: Phase 1 Done When
go build ./internal/...succeeds with no errorsGenerateSourceIDmatches Tachiyomi IDs for at least 3 known sourcesFlareSolverrClient.Getreturns rendered HTML for a Cloudflare-protected URL (manual test — requires running FlareSolverr)GraphQLPostworks against a public GraphQL endpoint (manual test)- Registry panics on duplicate source ID (unit test)