diff options
Diffstat (limited to 'internal/liturgy/store.go')
| -rw-r--r-- | internal/liturgy/store.go | 215 |
1 files changed, 0 insertions, 215 deletions
diff --git a/internal/liturgy/store.go b/internal/liturgy/store.go deleted file mode 100644 index 5bf7ef0..0000000 --- a/internal/liturgy/store.go +++ /dev/null @@ -1,215 +0,0 @@ -package liturgy - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "strings" - "time" -) - -// harvestRetries is how many times Harvest attempts a single date's fetch -// before treating it as a genuine network failure rather than transient -// hiccup; harvestRetryDelay is the backoff slept between attempts. Both are -// package vars so tests can shrink the delay instead of waiting on it. -var ( - harvestRetries = 3 - harvestRetryDelay = 2 * time.Second -) - -// siglaRow is one line of the sigla TSV: a date's section label and the -// scripture citation extracted from its heading. -type siglaRow struct { - label, citation string -} - -// siglaPath is the persistent sigla store written by Harvest and read by -// LoadOffline: ${XDG_DATA_HOME:-~/.local/share}/lectio/sigla.tsv -func siglaPath() string { - base := os.Getenv("XDG_DATA_HOME") - if base == "" { - home, err := os.UserHomeDir() - if err != nil { - home = "." - } - base = filepath.Join(home, ".local", "share") - } - return filepath.Join(base, "lectio", "sigla.tsv") -} - -// sectionLabel derives the short label Harvest stores alongside a citation -// from a section's full heading, e.g. "Ewangelia (J 20, 1. 11-18)" -> -// "Ewangelia". It strips the same trailing "(...)" citation ExtractCitation -// reads, so the two stay in sync. -func sectionLabel(heading string) string { - loc := citationRe.FindStringIndex(heading) - if loc == nil { - return strings.TrimSpace(heading) - } - return strings.TrimSpace(heading[:loc[0]]) -} - -// readSigla loads the sigla TSV into date -> rows. A missing file is not an -// error -- it just means nothing has been harvested yet. -func readSigla(path string) (map[string][]siglaRow, error) { - data, err := os.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - return map[string][]siglaRow{}, nil - } - return nil, err - } - rows := map[string][]siglaRow{} - for _, line := range strings.Split(string(data), "\n") { - if line == "" { - continue - } - fields := strings.SplitN(line, "\t", 3) - if len(fields) != 3 { - continue - } - date := fields[0] - rows[date] = append(rows[date], siglaRow{label: fields[1], citation: fields[2]}) - } - return rows, nil -} - -// writeSigla writes date -> rows back out as the sigla TSV, sorted by date -// for a deterministic, diffable file. -func writeSigla(path string, byDate map[string][]siglaRow) error { - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { - return err - } - dates := make([]string, 0, len(byDate)) - for d := range byDate { - dates = append(dates, d) - } - sort.Strings(dates) - - var b strings.Builder - for _, date := range dates { - for _, r := range byDate[date] { - fmt.Fprintf(&b, "%s\t%s\t%s\n", date, r.label, r.citation) - } - } - return os.WriteFile(path, []byte(b.String()), 0o644) -} - -// Harvest walks dates forward from fromDate, fetching and parsing each day's -// page and recording every section's citation to the sigla TSV, for up to -// maxDays days (0 = walk until the unpublished horizon). It stops cleanly -// (nil error) the first time a date fails to PARSE -- niedziela.pl's -// "Przykro nam" placeholder (or any other parse failure) marks the horizon -// the site hasn't published past yet, not an error to report. A date that -// fails to FETCH, by contrast, is a transient network problem, not the -// horizon: Harvest retries it a few times (harvestRetries, backing off -// harvestRetryDelay between attempts) and, if it still fails, stops and -// returns an error -- but only after saving whatever was harvested up to -// that point, so the caller never loses progress to a blip. -// -// Re-harvesting a date replaces its rows in the TSV rather than duplicating -// them, so running Harvest again over an already-harvested range is safe. -// It also warms the HTML/JSON cache for every date it successfully harvests. -// -// It returns how many days were harvested and the furthest (most recent) -// date reached, alongside any fetch error (nil on a clean parse-horizon -// stop or on reaching maxDays). -func Harvest(fromDate string, maxDays int) (added int, furthest string, err error) { - start, err := time.Parse("2006-01-02", fromDate) - if err != nil { - return 0, "", fmt.Errorf("invalid date %q: %w", fromDate, err) - } - - path := siglaPath() - byDate, err := readSigla(path) - if err != nil { - return 0, "", err - } - - dir := CacheDir() - day := start - var harvestErr error - for i := 0; maxDays == 0 || i < maxDays; i++ { - dateStr := day.Format("2006-01-02") - - var page string - var ferr error - for attempt := 1; attempt <= harvestRetries; attempt++ { - page, ferr = fetch(dateStr) - if ferr == nil { - break - } - if attempt < harvestRetries { - time.Sleep(harvestRetryDelay) - } - } - if ferr != nil { - // A genuine network/transport error, not the horizon: don't - // silently stop as if the site simply hadn't published this - // date yet. Record it and stop walking, but writeSigla below - // still runs so progress made so far isn't lost. - harvestErr = fmt.Errorf("harvest interrupted at %s: %w", dateStr, ferr) - break - } - secs, perr := Parse(page) - if perr != nil { - break // unpublished horizon (or unparsable page): stop walking, cleanly - } - - var rows []siglaRow - for _, s := range secs { - citation, cerr := ExtractCitation(s.Heading) - if cerr != nil { - continue - } - rows = append(rows, siglaRow{label: sectionLabel(s.Heading), citation: citation}) - } - byDate[dateStr] = rows - - if publishedRe.MatchString(page) { - if mkErr := os.MkdirAll(dir, 0o755); mkErr == nil { - _ = os.WriteFile(filepath.Join(dir, dateStr+".html"), []byte(page), 0o644) - writeCache(filepath.Join(dir, dateStr+".json"), secs, ParseDayInfo(page)) - } - } - - added++ - furthest = dateStr - day = day.AddDate(0, 0, 1) - } - - werr := writeSigla(path, byDate) - if harvestErr != nil { - return added, furthest, harvestErr - } - if werr != nil { - return added, furthest, werr - } - return added, furthest, nil -} - -// LoadOffline builds a day's sections purely from the harvested sigla TSV: -// Heading is the stored section label, Citation the stored citation, and -// Paragraphs empty (no reading text is harvested, only the scripture -// reference). It errors clearly if the date has not been harvested. -func LoadOffline(date string) ([]Section, error) { - byDate, err := readSigla(siglaPath()) - if err != nil { - return nil, err - } - rows, ok := byDate[date] - if !ok || len(rows) == 0 { - return nil, fmt.Errorf("%s not harvested; run 'lectio update' while online first", date) - } - secs := make([]Section, 0, len(rows)) - czytanieCount := 0 - for _, r := range rows { - secs = append(secs, Section{ - Heading: r.label, - Citation: r.citation, - PartID: partID(r.label, &czytanieCount), - }) - } - return secs, nil -} |
