feat: implement phase 4 batch — 54 base-class wrapper sources
Add 8 all/ sources (7 Masonry, 1 Madara) and 38 en/ sources spanning Madara, MangaThemesia, MadTheme, Keyoapp, and Guya bases, plus 8 earlier all/ standalone sources from the previous session (ahottie, akuma, allporncomicsco, asmhentai, baobua, beauty3600000, buondua, comicfury, comicgrowl, comicklive, comicsvalley, comikey, commitstrip, coomer). Also annotates phase4-standalone.md with base-class tags for 43 additional unimplemented en/ sources identified in a full scan.
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
// Package commitstrip implements the Commit Strip webcomic source.
|
||||
// Two language instances (en, fr). Popular is a static list of per-year entries
|
||||
// (2012 → current year). Chapters scraped from paginated year archives; one page per strip.
|
||||
package commitstrip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
|
||||
"goyomi/internal/httpclient"
|
||||
"goyomi/internal/registry"
|
||||
"goyomi/internal/source"
|
||||
)
|
||||
|
||||
const siteURL = "https://www.commitstrip.com"
|
||||
|
||||
var (
|
||||
dateRe = regexp.MustCompile(`\d{4}/\d{2}/\d{2}`)
|
||||
pageRe = regexp.MustCompile(`\d+`)
|
||||
)
|
||||
|
||||
type Source struct {
|
||||
name string
|
||||
lang string
|
||||
siteLang string
|
||||
client *httpclient.Client
|
||||
id int64
|
||||
}
|
||||
|
||||
func newSource(lang, siteLang string) *Source {
|
||||
return &Source{
|
||||
name: "Commit Strip",
|
||||
lang: lang,
|
||||
siteLang: siteLang,
|
||||
client: httpclient.NewClient(httpclient.WithRateLimit(2, 1)),
|
||||
id: source.GenerateSourceID("Commit Strip", lang),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Source) ID() int64 { return s.id }
|
||||
func (s *Source) Name() string { return s.name }
|
||||
func (s *Source) Lang() string { return s.lang }
|
||||
func (s *Source) SupportsLatest() bool { return false }
|
||||
|
||||
func (s *Source) get(ctx context.Context, rawURL string) (*goquery.Document, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("commitstrip: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return goquery.NewDocumentFromReader(resp.Body)
|
||||
}
|
||||
|
||||
func currentYear() int { return time.Now().Year() }
|
||||
|
||||
func (s *Source) thumbnail() string {
|
||||
if s.lang == "fr" {
|
||||
return "https://i.imgur.com/I7ps9zS.jpg"
|
||||
}
|
||||
return "https://i.imgur.com/HODJlt9.jpg"
|
||||
}
|
||||
|
||||
func (s *Source) author() string {
|
||||
if s.lang == "fr" {
|
||||
return "Thomas Gx"
|
||||
}
|
||||
return "Mark Nightingale"
|
||||
}
|
||||
|
||||
func (s *Source) summary(year int) string {
|
||||
note := fmt.Sprintf("\n\nNote: This entry includes all the chapters published in %d", year)
|
||||
if s.lang == "fr" {
|
||||
return "Le blog qui raconte la vie des codeurs" + note
|
||||
}
|
||||
return "The blog relating the daily life of web agency developers." + note
|
||||
}
|
||||
|
||||
func (s *Source) makeYearManga(year int) source.SManga {
|
||||
status := source.StatusOngoing
|
||||
if year != currentYear() {
|
||||
status = source.StatusCompleted
|
||||
}
|
||||
return source.SManga{
|
||||
URL: fmt.Sprintf("/%s/%d", s.siteLang, year),
|
||||
Title: fmt.Sprintf("Commit Strip (%d)", year),
|
||||
ThumbnailURL: s.thumbnail(),
|
||||
Author: s.author(),
|
||||
Artist: "Etienne Issartial",
|
||||
Status: status,
|
||||
Description: s.summary(year),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Source) GetPopularManga(page int) (source.MangasPage, error) {
|
||||
if page > 1 {
|
||||
return source.MangasPage{}, nil
|
||||
}
|
||||
cur := currentYear()
|
||||
mangas := make([]source.SManga, 0, cur-2011)
|
||||
for y := cur; y >= 2012; y-- {
|
||||
mangas = append(mangas, s.makeYearManga(y))
|
||||
}
|
||||
return source.MangasPage{Mangas: mangas, HasNextPage: false}, nil
|
||||
}
|
||||
|
||||
func (s *Source) GetLatestUpdates(page int) (source.MangasPage, error) {
|
||||
return source.MangasPage{}, fmt.Errorf("commitstrip: latest not supported")
|
||||
}
|
||||
|
||||
func (s *Source) GetSearchManga(page int, query string, _ []source.Filter) (source.MangasPage, error) {
|
||||
all, _ := s.GetPopularManga(1)
|
||||
if query == "" {
|
||||
return all, nil
|
||||
}
|
||||
q := strings.ToLower(query)
|
||||
var matched []source.SManga
|
||||
for _, m := range all.Mangas {
|
||||
if strings.Contains(strings.ToLower(m.Title), q) {
|
||||
matched = append(matched, m)
|
||||
}
|
||||
}
|
||||
return source.MangasPage{Mangas: matched, HasNextPage: false}, nil
|
||||
}
|
||||
|
||||
func (s *Source) GetMangaDetails(manga source.SManga) (source.SManga, error) {
|
||||
return manga, nil
|
||||
}
|
||||
|
||||
func (s *Source) GetChapterList(manga source.SManga) ([]source.SChapter, error) {
|
||||
yearURL := siteURL + manga.URL
|
||||
doc, err := s.get(context.Background(), yearURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find total pages from ".wp-pagenavi .pages" text (e.g. "Page 1 of 12").
|
||||
totalPages := 1
|
||||
if pagesText := doc.Find(".wp-pagenavi .pages").First().Text(); pagesText != "" {
|
||||
matches := pageRe.FindAllString(pagesText, -1)
|
||||
if len(matches) >= 2 {
|
||||
fmt.Sscanf(matches[len(matches)-1], "%d", &totalPages)
|
||||
}
|
||||
}
|
||||
|
||||
var chapters []source.SChapter
|
||||
collect := func(d *goquery.Document) {
|
||||
d.Find(".excerpt a").Each(func(_ int, a *goquery.Selection) {
|
||||
href := a.AttrOr("href", "")
|
||||
if href == "" {
|
||||
return
|
||||
}
|
||||
chURL := strings.TrimPrefix(href, siteURL)
|
||||
name := strings.TrimSpace(a.Find("span").Text())
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(a.Text())
|
||||
}
|
||||
var date int64
|
||||
if m := dateRe.FindString(chURL); m != "" {
|
||||
if t, err := time.Parse("2006/01/02", m); err == nil {
|
||||
date = t.UnixMilli()
|
||||
}
|
||||
}
|
||||
chapters = append(chapters, source.SChapter{URL: chURL, Name: name, DateUpload: date})
|
||||
})
|
||||
}
|
||||
|
||||
collect(doc)
|
||||
for pg := 2; pg <= totalPages; pg++ {
|
||||
pageDoc, err := s.get(context.Background(), fmt.Sprintf("%s/page/%d", yearURL, pg))
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
collect(pageDoc)
|
||||
}
|
||||
|
||||
// Deduplicate and assign chapter numbers.
|
||||
seen := make(map[string]bool)
|
||||
var unique []source.SChapter
|
||||
for _, ch := range chapters {
|
||||
if !seen[ch.URL] {
|
||||
seen[ch.URL] = true
|
||||
unique = append(unique, ch)
|
||||
}
|
||||
}
|
||||
total := len(unique)
|
||||
for i := range unique {
|
||||
unique[i].ChapterNumber = float32(total - i)
|
||||
}
|
||||
return unique, nil
|
||||
}
|
||||
|
||||
func (s *Source) GetPageList(chapter source.SChapter) ([]source.Page, error) {
|
||||
doc, err := s.get(context.Background(), siteURL+chapter.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
src := doc.Find(".entry-content p img").First().AttrOr("src", "")
|
||||
if src == "" {
|
||||
return nil, fmt.Errorf("commitstrip: image not found")
|
||||
}
|
||||
return []source.Page{{Index: 0, ImageURL: src}}, nil
|
||||
}
|
||||
|
||||
func (s *Source) GetImageURL(page source.Page) (string, error) { return page.ImageURL, nil }
|
||||
func (s *Source) GetFilterList() []source.Filter { return nil }
|
||||
|
||||
func init() {
|
||||
registry.Register(newSource("en", "en"))
|
||||
registry.Register(newSource("fr", "fr"))
|
||||
}
|
||||
Reference in New Issue
Block a user