316ae2f9db
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.
224 lines
5.9 KiB
Go
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"))
|
|
}
|