jnfilter @ f01369628016ba3038cccac77ba54bcd6be6630b

  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)
 73
 74func getSeries(r *http.Request) []string {
 75	query := r.URL.Query().Get("q")
 76
 77	var series []string
 78
 79	for _, q := range strings.Split(query, ",") {
 80		if _, ok := regexCollection[q]; ok {
 81			series = append(series, q)
 82		}
 83	}
 84
 85	if len(series) > 0 {
 86		return series
 87	}
 88
 89	return []string{"nerdcast"}
 90}
 91
 92func match(title string, series []string) bool {
 93	for _, s := range series {
 94		if ok, err := regexp.MatchString(regexCollection[s], title); err == nil && ok {
 95			return true
 96		}
 97	}
 98
 99	return false
100}
101
102func fetchXML(_ context.Context) ([]byte, error) {
103	t := time.Now()
104	c := http.StatusInternalServerError
105
106	defer func() {
107		since := time.Since(t).Seconds()
108		code := strconv.Itoa(c)
109		feedRequest.WithLabelValues(code).Observe(since)
110	}()
111
112	res, err := http.Get(feedUrl)
113	if err != nil {
114		return nil, err
115	}
116	defer res.Body.Close()
117
118	c = res.StatusCode
119
120	if c == http.StatusOK {
121		return io.ReadAll(res.Body)
122	}
123
124	return nil, errors.New("Invalid http code")
125}
126
127func appendTag(tag *etree.Element, ap string) {
128	text := tag.Text()
129	tag.SetText(text + ap)
130}
131
132func filterBySeries(series []string, xml []byte, temper bool) ([]byte, error) {
133	doc := etree.NewDocument()
134	err := doc.ReadFromBytes(xml)
135	if err != nil {
136		return nil, err
137	}
138
139	channel := doc.FindElement("//channel")
140
141	if temper {
142		tmp := strings.ToUpper(strings.Join(series, ","))
143		tmp = fmt.Sprintf(" [%s]", tmp)
144		appendTag(channel.FindElement("title"), tmp)
145		appendTag(channel.FindElement("description"), tmp)
146		appendTag(channel.FindElement("link"), "?"+tmp)
147		appendTag(channel.FindElement("author[namespace-prefix()='itunes']"), tmp)
148		appendTag(channel.FindElement("subtitle[namespace-prefix()='itunes']"), tmp)
149		appendTag(channel.FindElement("summary[namespace-prefix()='itunes']"), tmp)
150		appendTag(channel.FindElement("author[namespace-prefix()='googleplay']"), tmp)
151
152	}
153
154	for _, tag := range channel.FindElements("item") {
155		title := tag.FindElement("title").Text()
156		if !match(title, series) {
157			channel.RemoveChild(tag)
158		}
159	}
160
161	return doc.WriteToBytes()
162}
163
164func handleError(next errorRequestHandler) http.HandlerFunc {
165	return func(w http.ResponseWriter, r *http.Request) {
166		if err := next(w, r); err != nil {
167			slog.ErrorContext(r.Context(), "Error", "error", err.Error())
168
169			if errors.Is(err, errNotFound) {
170				w.WriteHeader(http.StatusNotFound)
171			} else {
172				w.WriteHeader(http.StatusInternalServerError)
173			}
174		}
175	}
176}
177
178func observe(next http.HandlerFunc) http.HandlerFunc {
179	return func(w http.ResponseWriter, r *http.Request) {
180		t := time.Now()
181
182		next(w, r)
183
184		rw := w.(*responseWriter)
185		since := time.Since(t).Seconds()
186		code := strconv.Itoa(rw.Status())
187		userAgent := r.Header.Get("user-agent")
188		httpRequest.WithLabelValues(code, userAgent).Observe(float64(since))
189
190		for _, s := range getSeries(r) {
191			seriesCount.WithLabelValues(s).Inc()
192		}
193	}
194}
195
196func wrap(next http.HandlerFunc) http.HandlerFunc {
197	return func(w http.ResponseWriter, r *http.Request) {
198		next(NewResponseWriter(w), r)
199	}
200}
201
202func titles(w http.ResponseWriter, r *http.Request) error {
203	xml, err := fetchXML(r.Context())
204	if err != nil {
205		return err
206	}
207
208	doc := etree.NewDocument()
209	err = doc.ReadFromBytes(xml)
210	if err != nil {
211		return err
212	}
213
214	series := getSeries(r)
215
216	els := doc.FindElements("//channel/item")
217	for _, e := range els {
218		txt := e.FindElement("title").Text() + "\n"
219		if match(txt, series) {
220			_, err = w.Write([]byte(txt))
221			if err != nil {
222				return err
223			}
224		}
225	}
226
227	return nil
228}
229
230func view(w http.ResponseWriter, r *http.Request) error {
231	data, err := assets.ReadFile("static/index.html")
232	if err != nil {
233		return err
234	}
235
236	_, err = w.Write(data)
237	if err != nil {
238		return err
239	}
240
241	return nil
242}
243
244func podcast(w http.ResponseWriter, r *http.Request) error {
245
246	if r.URL.Path != "/" {
247		return errNotFound
248
249	}
250
251	xml, err := fetchXML(r.Context())
252	if err != nil {
253		return err
254	}
255
256	series := getSeries(r)
257	filterdXML, err := filterBySeries(series, xml, true)
258	if err != nil {
259		return err
260	}
261
262	_, err = w.Write(filterdXML)
263	if err != nil {
264		return err
265	}
266
267	return nil
268}
269
270func genSeries() error {
271	xml, err := fetchXML(context.Background())
272	if err != nil {
273		return err
274	}
275
276	doc := etree.NewDocument()
277	err = doc.ReadFromBytes(xml)
278	if err != nil {
279		return err
280	}
281
282	unique := make(map[string]any)
283	els := doc.FindElements("//channel/item")
284	for _, e := range els {
285		txt := e.FindElement("title").Text()
286		res := serieRegex.FindStringSubmatch(txt)
287		if len(res) > 1 {
288			unique[res[1]] = nil
289		}
290	}
291
292	for k := range unique {
293		fmt.Println(k)
294	}
295
296	return nil
297}
298
299func main() {
300	if len(os.Args) > 1 && os.Args[1] == "series" {
301		err := genSeries()
302		if err != nil {
303			panic(err.Error())
304		}
305		return
306	}
307
308	var (
309		addr = flag.String("addr", ":8080", "Server address")
310	)
311
312	flag.Parse()
313
314	mux := http.NewServeMux()
315	mux.Handle("/metrics", promhttp.Handler())
316	mux.HandleFunc("/titles", wrap(handleError(titles)))
317	mux.HandleFunc("/view", wrap(handleError(view)))
318	mux.HandleFunc("/", wrap(observe(handleError(podcast))))
319
320	server := http.Server{
321		Handler: mux,
322		Addr:    *addr,
323	}
324
325	err := server.ListenAndServe()
326	if err != nil {
327		fmt.Printf("Server error: %s", err.Error())
328	}
329}