aboutsummaryrefslogtreecommitdiff
path: root/internal/tradlit/tradlit.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 13:24:28 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-23 13:24:28 +0200
commitf8772261f0c4feca68eee0300b5d726fd168a534 (patch)
tree4824c91713d5e1e4a18d53b1fcf752043f247d21 /internal/tradlit/tradlit.go
parentfbb3345518872346a629e7ccbec81f09532765f9 (diff)
downloadlectio-f8772261f0c4feca68eee0300b5d726fd168a534.tar.gz
lectio-f8772261f0c4feca68eee0300b5d726fd168a534.zip
tradlit: missalemeum 1962 propers -> sections
Diffstat (limited to 'internal/tradlit/tradlit.go')
-rw-r--r--internal/tradlit/tradlit.go121
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)
+}