1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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)
}
|