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