1package main
2
3import (
4 "context"
5 "embed"
6 "errors"
7 "flag"
8 "fmt"
9 "io"
10 "log/slog"
11 "net/http"
12 "os"
13 "regexp"
14 "strconv"
15 "strings"
16 "time"
17
18 "github.com/beevik/etree"
19 "github.com/prometheus/client_golang/prometheus"
20 "github.com/prometheus/client_golang/prometheus/promauto"
21 "github.com/prometheus/client_golang/prometheus/promhttp"
22)
23
24const (
25 feedUrl = "https://api.jovemnerd.com.br/feed-nerdcast/"
26)
27
28type (
29 errorRequestHandler func(w http.ResponseWriter, r *http.Request) error
30)
31
32var (
33 //go:embed static/*
34 assets embed.FS
35 serieRegex = regexp.MustCompile(`(?P<serie>.+) (?P<number>[0-9abc]+) \- (?P<title>.+)`)
36 errNotFound = errors.New("not found")
37)
38
39var (
40 regexCollection = map[string]string{
41 "nerdcast": "NerdCast [0-9]+[a-c]*",
42 "empreendedor": "Empreendedor [0-9]+",
43 "mamicas": "Caneca de Mamicas [0-9]+",
44 "english": "Speak English [0-9]+",
45 "nerdcash": "NerdCash [0-9]+",
46 "bunker": "Lá do Bunker( LDB especial Oscar|) [0-9]+",
47 "tech": "NerdTech [0-9]+",
48 "genera": "Generacast [0-9]+",
49 "rpg": "NerdCast RPG [0-9]+[a-c]*",
50 "catar": "Vai te Catar [0-9]+",
51 "cloud": "Nerd na Cloud [0-9]+",
52 "contar": "Vou (T|t)e Contar [0-9]+",
53 "parceiro": "Papo de Parceiro [0-9]+",
54 }
55
56 feedRequest = promauto.NewHistogramVec(prometheus.HistogramOpts{
57 Name: "feed_request",
58 Help: "How long jovemnerd takes to answer",
59 Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
60 }, []string{"status_code"})
61
62 httpRequest = promauto.NewHistogramVec(prometheus.HistogramOpts{
63 Name: "http_request",
64 Help: "How long the application takes to complete the request",
65 Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
66 }, []string{"status_code", "user_agent"})
67
68 seriesCount = promauto.NewCounterVec(prometheus.CounterOpts{
69 Name: "serie_count",
70 Help: "How often a serie is called",
71 }, []string{"serie"})
72 panicCount = promauto.NewCounter(prometheus.CounterOpts{
73 Name: "panic",
74 Help: "How many times the application panic",
75 })
76)
77
78func getSeries(r *http.Request) []string {
79 query := r.URL.Query().Get("q")
80
81 var series []string
82
83 for _, q := range strings.Split(query, ",") {
84 if _, ok := regexCollection[q]; ok {
85 series = append(series, q)
86 }
87 }
88
89 if len(series) > 0 {
90 return series
91 }
92
93 return []string{"nerdcast"}
94}
95
96func match(title string, series []string) bool {
97 for _, s := range series {
98 if ok, err := regexp.MatchString(regexCollection[s], title); err == nil && ok {
99 return true
100 }
101 }
102
103 return false
104}
105
106func fetchXML(_ context.Context) ([]byte, error) {
107 t := time.Now()
108 c := http.StatusInternalServerError
109
110 defer func() {
111 since := time.Since(t).Seconds()
112 code := strconv.Itoa(c)
113 feedRequest.WithLabelValues(code).Observe(since)
114 }()
115
116 res, err := http.Get(feedUrl)
117 if err != nil {
118 return nil, err
119 }
120 defer res.Body.Close()
121
122 c = res.StatusCode
123
124 if c == http.StatusOK {
125 return io.ReadAll(res.Body)
126 }
127
128 return nil, errors.New("Invalid http code")
129}
130
131func appendTag(tag *etree.Element, ap string) {
132 if tag != nil {
133 text := tag.Text()
134 tag.SetText(text + ap)
135 }
136}
137
138func filterBySeries(series []string, xml []byte, temper bool) ([]byte, error) {
139 doc := etree.NewDocument()
140 err := doc.ReadFromBytes(xml)
141 if err != nil {
142 return nil, err
143 }
144
145 channel := doc.FindElement("//channel")
146
147 if temper {
148 tmp := strings.ToUpper(strings.Join(series, ","))
149 tmp = fmt.Sprintf(" [%s]", tmp)
150 appendTag(channel.FindElement("title"), tmp)
151 appendTag(channel.FindElement("description"), tmp)
152 appendTag(channel.FindElement("link"), "?"+tmp)
153 appendTag(channel.FindElement("author[namespace-prefix()='itunes']"), tmp)
154 appendTag(channel.FindElement("subtitle[namespace-prefix()='itunes']"), tmp)
155 appendTag(channel.FindElement("summary[namespace-prefix()='itunes']"), tmp)
156 appendTag(channel.FindElement("author[namespace-prefix()='googleplay']"), tmp)
157
158 }
159
160 for _, tag := range channel.FindElements("item") {
161 title := tag.FindElement("title").Text()
162 if !match(title, series) {
163 channel.RemoveChild(tag)
164 }
165 }
166
167 return doc.WriteToBytes()
168}
169
170func handleError(next errorRequestHandler) http.HandlerFunc {
171 return func(w http.ResponseWriter, r *http.Request) {
172 defer func() {
173 if perr := recover(); perr != nil {
174 w.WriteHeader(http.StatusInternalServerError)
175 slog.Error("Request panic", "error", perr)
176 panicCount.Inc()
177 }
178 }()
179
180 if err := next(w, r); err != nil {
181 slog.ErrorContext(r.Context(), "Error", "error", err.Error())
182
183 if errors.Is(err, errNotFound) {
184 w.WriteHeader(http.StatusNotFound)
185 } else {
186 w.WriteHeader(http.StatusInternalServerError)
187 }
188 }
189 }
190}
191
192func observe(next http.HandlerFunc) http.HandlerFunc {
193 return func(w http.ResponseWriter, r *http.Request) {
194 t := time.Now()
195
196 next(w, r)
197
198 rw := w.(*responseWriter)
199 since := time.Since(t).Seconds()
200 code := strconv.Itoa(rw.Status())
201 userAgent := r.Header.Get("user-agent")
202 httpRequest.WithLabelValues(code, userAgent).Observe(float64(since))
203
204 for _, s := range getSeries(r) {
205 seriesCount.WithLabelValues(s).Inc()
206 }
207 }
208}
209
210func wrap(next http.HandlerFunc) http.HandlerFunc {
211 return func(w http.ResponseWriter, r *http.Request) {
212 next(NewResponseWriter(w), r)
213 }
214}
215
216func titles(w http.ResponseWriter, r *http.Request) error {
217 xml, err := fetchXML(r.Context())
218 if err != nil {
219 return err
220 }
221
222 doc := etree.NewDocument()
223 err = doc.ReadFromBytes(xml)
224 if err != nil {
225 return err
226 }
227
228 series := getSeries(r)
229
230 els := doc.FindElements("//channel/item")
231 for _, e := range els {
232 txt := e.FindElement("title").Text() + "\n"
233 if match(txt, series) {
234 _, err = w.Write([]byte(txt))
235 if err != nil {
236 return err
237 }
238 }
239 }
240
241 return nil
242}
243
244func view(w http.ResponseWriter, r *http.Request) error {
245 data, err := assets.ReadFile("static/index.html")
246 if err != nil {
247 return err
248 }
249
250 _, err = w.Write(data)
251 if err != nil {
252 return err
253 }
254
255 return nil
256}
257
258func podcast(w http.ResponseWriter, r *http.Request) error {
259 if r.URL.Path != "/" {
260 return errNotFound
261 }
262
263 xml, err := fetchXML(r.Context())
264 if err != nil {
265 return err
266 }
267
268 series := getSeries(r)
269 temper := r.URL.Query().Get("tag") == "true"
270 filterdXML, err := filterBySeries(series, xml, temper)
271 if err != nil {
272 return err
273 }
274
275 _, err = w.Write(filterdXML)
276 if err != nil {
277 return err
278 }
279
280 return nil
281}
282
283func genSeries() error {
284 xml, err := fetchXML(context.Background())
285 if err != nil {
286 return err
287 }
288
289 doc := etree.NewDocument()
290 err = doc.ReadFromBytes(xml)
291 if err != nil {
292 return err
293 }
294
295 unique := make(map[string]any)
296 els := doc.FindElements("//channel/item")
297 for _, e := range els {
298 txt := e.FindElement("title").Text()
299 res := serieRegex.FindStringSubmatch(txt)
300 if len(res) > 1 {
301 unique[res[1]] = nil
302 }
303 }
304
305 for k := range unique {
306 fmt.Println(k)
307 }
308
309 return nil
310}
311
312func main() {
313 if len(os.Args) > 1 && os.Args[1] == "series" {
314 err := genSeries()
315 if err != nil {
316 panic(err.Error())
317 }
318 return
319 }
320
321 addr := flag.String("addr", ":8080", "Server address")
322
323 flag.Parse()
324
325 mux := http.NewServeMux()
326 mux.Handle("/metrics", promhttp.Handler())
327 mux.HandleFunc("/titles", wrap(handleError(titles)))
328 mux.HandleFunc("/view", wrap(handleError(view)))
329 mux.HandleFunc("/", wrap(observe(handleError(podcast))))
330
331 server := http.Server{
332 Handler: mux,
333 Addr: *addr,
334 }
335
336 slog.Info("Starting server", "addr", *addr)
337 err := server.ListenAndServe()
338 if err != nil {
339 fmt.Printf("Server error: %s", err.Error())
340 }
341}