jnfilter @ 1fee46153555f3fe6b80aa7ddd975d78d62f2351

  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}