aboutsummaryrefslogtreecommitdiff
path: root/internal/tradlit/tradlit.go
blob: edb9ce014eb2ace167bce74edd452f71a3cac106 (plain) (blame)
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)
}