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