diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-23 13:24:28 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-23 13:24:28 +0200 |
| commit | f8772261f0c4feca68eee0300b5d726fd168a534 (patch) | |
| tree | 4824c91713d5e1e4a18d53b1fcf752043f247d21 /internal/tradlit/tradlit.go | |
| parent | fbb3345518872346a629e7ccbec81f09532765f9 (diff) | |
| download | lectio-f8772261f0c4feca68eee0300b5d726fd168a534.tar.gz lectio-f8772261f0c4feca68eee0300b5d726fd168a534.zip | |
tradlit: missalemeum 1962 propers -> sections
Diffstat (limited to 'internal/tradlit/tradlit.go')
| -rw-r--r-- | internal/tradlit/tradlit.go | 121 |
1 files changed, 121 insertions, 0 deletions
diff --git a/internal/tradlit/tradlit.go b/internal/tradlit/tradlit.go new file mode 100644 index 0000000..edb9ce0 --- /dev/null +++ b/internal/tradlit/tradlit.go @@ -0,0 +1,121 @@ +// Package tradlit fetches and parses the Traditional (1962 Missal) Latin +// propers from the missalemeum JSON API into the shared liturgy.Section +// type, so the rest of the app can treat the traditional and modern +// (niedziela.pl) lectionaries interchangeably. +package tradlit + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "strings" + + "github.com/lukaszkasprzak/lectio/internal/liturgy" +) + +// baseURL is the missalemeum proper-of-the-day API template ("%s" is lang, +// then date, YYYY-MM-DD). It is a package var so tests can point it at an +// httptest server. +var baseURL = "https://www.missalemeum.com/%s/api/v5/proper/%s" + +// userAgent is sent on every fetch; matches the browser UA used elsewhere +// in this project (see the plan's Global Constraints). +const userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/124.0 Safari/537.36" + +// citationRe matches the first *...* marker in a section's body text, e.g. +// "*Luke 7:36-50*" or "*Ps 44:2*". +var citationRe = regexp.MustCompile(`\*([^*\n]+)\*`) + +// apiResponse mirrors the shape of the missalemeum proper-of-the-day API: +// a single-element list of { info, sections }. +type apiResponse struct { + Info struct { + Title string `json:"title"` + } `json:"info"` + Sections []apiSection `json:"sections"` +} + +type apiSection struct { + ID string `json:"id"` + Label string `json:"label"` + Body [][]string `json:"body"` +} + +// Parse decodes a missalemeum proper-of-the-day API response body into +// liturgy.Sections. Sections with an empty id or empty body are skipped. +func Parse(jsonBody []byte) ([]liturgy.Section, error) { + var resp []apiResponse + if err := json.Unmarshal(jsonBody, &resp); err != nil { + return nil, fmt.Errorf("tradlit: parse: %w", err) + } + if len(resp) == 0 { + return nil, fmt.Errorf("tradlit: parse: empty response") + } + + var out []liturgy.Section + for _, sec := range resp[0].Sections { + if sec.ID == "" || len(sec.Body) == 0 || len(sec.Body[0]) == 0 { + continue + } + + text := strings.Join(sec.Body[0], "\n") + + var lines []string + for _, line := range strings.Split(text, "\n") { + line = strings.TrimSpace(line) + if line != "" { + lines = append(lines, line) + } + } + + citation := "" + if m := citationRe.FindStringSubmatch(text); m != nil { + citation = strings.TrimSpace(m[1]) + } + + out = append(out, liturgy.Section{ + Heading: sec.Label, + Citation: citation, + PartID: strings.ToLower(sec.ID), + Paragraphs: [][]string{lines}, + }) + } + return out, nil +} + +// Load fetches a day's traditional propers from the missalemeum API +// (https://www.missalemeum.com/{lang}/api/v5/proper/{date}) and parses +// them. On HTTP 404 (no propers published for that date) it returns a +// clear error rather than attempting to parse. +func Load(date, lang string) ([]liturgy.Section, error) { + url := fmt.Sprintf(baseURL, lang, date) + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("tradlit: load: %w", err) + } + req.Header.Set("User-Agent", userAgent) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("tradlit: load %s: %w", date, err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("tradlit: no propers published for %s", date) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("tradlit: load %s: unexpected status %s", date, resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("tradlit: load %s: %w", date, err) + } + + return Parse(body) +} |
