feat: initial Phase 1 implementation — core framework + Docker
- Data types (SManga, SChapter, Page, MangasPage, all Filter variants) - Source interfaces (Source, CatalogueSource) with MD5-based ID generation matching Tachiyomi/Suwayomi - HTTP client with per-host rate limiting, cookie jar, and 429 retry - FlareSolverr v1 client (FLARESOLVERR_URL env) - Generic GraphQL POST helper - goquery HTML parser wrappers - Source registry (panics on duplicate ID) - Multi-stage Dockerfile (golang:1.26-alpine + distroless) and compose.yml (postgres, flaresolverr, app)
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"goyomi/internal/source"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
sources = map[int64]source.CatalogueSource{}
|
||||
)
|
||||
|
||||
// Register adds a source. Panics on duplicate ID — caught at startup.
|
||||
func Register(s source.CatalogueSource) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if _, exists := sources[s.ID()]; exists {
|
||||
panic(fmt.Sprintf("registry: duplicate source ID %d (%s/%s)", s.ID(), s.Name(), s.Lang()))
|
||||
}
|
||||
sources[s.ID()] = s
|
||||
}
|
||||
|
||||
func Get(id int64) (source.CatalogueSource, bool) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
s, ok := sources[id]
|
||||
return s, ok
|
||||
}
|
||||
|
||||
// All returns all registered sources sorted by ID.
|
||||
func All() []source.CatalogueSource {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
out := make([]source.CatalogueSource, 0, len(sources))
|
||||
for _, s := range sources {
|
||||
out = append(out, s)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].ID() < out[j].ID() })
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package registry_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"goyomi/internal/registry"
|
||||
"goyomi/internal/source"
|
||||
)
|
||||
|
||||
type mockSource struct {
|
||||
id int64
|
||||
name string
|
||||
lang string
|
||||
}
|
||||
|
||||
func (m *mockSource) ID() int64 { return m.id }
|
||||
func (m *mockSource) Name() string { return m.name }
|
||||
func (m *mockSource) Lang() string { return m.lang }
|
||||
func (m *mockSource) SupportsLatest() bool { return false }
|
||||
func (m *mockSource) GetPopularManga(page int) (source.MangasPage, error) { return source.MangasPage{}, nil }
|
||||
func (m *mockSource) GetLatestUpdates(page int) (source.MangasPage, error) { return source.MangasPage{}, nil }
|
||||
func (m *mockSource) GetSearchManga(page int, query string, filters []source.Filter) (source.MangasPage, error) {
|
||||
return source.MangasPage{}, nil
|
||||
}
|
||||
func (m *mockSource) GetMangaDetails(manga source.SManga) (source.SManga, error) { return manga, nil }
|
||||
func (m *mockSource) GetChapterList(manga source.SManga) ([]source.SChapter, error) { return nil, nil }
|
||||
func (m *mockSource) GetPageList(chapter source.SChapter) ([]source.Page, error) { return nil, nil }
|
||||
func (m *mockSource) GetImageURL(page source.Page) (string, error) { return page.ImageURL, nil }
|
||||
func (m *mockSource) GetFilterList() []source.Filter { return nil }
|
||||
|
||||
func TestDuplicateIDPanics(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic on duplicate source ID, got none")
|
||||
}
|
||||
}()
|
||||
registry.Register(&mockSource{id: 9999, name: "A", lang: "en"})
|
||||
registry.Register(&mockSource{id: 9999, name: "B", lang: "en"})
|
||||
}
|
||||
Reference in New Issue
Block a user