Files
achmad 316ae2f9db 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.
2026-05-13 23:11:26 +07:00

224 lines
5.9 KiB
Go

// 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"))
}