package main import ( "context" "embed" "errors" "flag" "fmt" "io" "log/slog" "net/http" "os" "regexp" "strconv" "strings" "time" "github.com/beevik/etree" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" ) const ( feedUrl = "https://api.jovemnerd.com.br/feed-nerdcast/" ) type ( errorRequestHandler func(w http.ResponseWriter, r *http.Request) error ) var ( //go:embed static/* assets embed.FS serieRegex = regexp.MustCompile(`(?P.+) (?P[0-9abc]+) \- (?P.+)`) errNotFound = errors.New("not found") ) var ( regexCollection = map[string]string{ "nerdcast": "NerdCast [0-9]+[a-c]*", "empreendedor": "Empreendedor [0-9]+", "mamicas": "Caneca de Mamicas [0-9]+", "english": "Speak English [0-9]+", "nerdcash": "NerdCash [0-9]+", "bunker": "Lá do Bunker( LDB especial Oscar|) [0-9]+", "tech": "NerdTech [0-9]+", "genera": "Generacast [0-9]+", "rpg": "NerdCast RPG [0-9]+[a-c]*", "catar": "Vai te Catar [0-9]+", "cloud": "Nerd na Cloud [0-9]+", "contar": "Vou (T|t)e Contar [0-9]+", "parceiro": "Papo de Parceiro [0-9]+", } feedRequest = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "feed_request", Help: "How long jovemnerd takes to answer", Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}, }, []string{"status_code"}) httpRequest = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "http_request", Help: "How long the application takes to complete the request", Buckets: []float64{.01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}, }, []string{"status_code", "user_agent"}) seriesCount = promauto.NewCounterVec(prometheus.CounterOpts{ Name: "serie_count", Help: "How often a serie is called", }, []string{"serie"}) ) func getSeries(r *http.Request) []string { query := r.URL.Query().Get("q") var series []string for _, q := range strings.Split(query, ",") { if _, ok := regexCollection[q]; ok { series = append(series, q) } } if len(series) > 0 { return series } return []string{"nerdcast"} } func match(title string, series []string) bool { for _, s := range series { if ok, err := regexp.MatchString(regexCollection[s], title); err == nil && ok { return true } } return false } func fetchXML(_ context.Context) ([]byte, error) { t := time.Now() c := http.StatusInternalServerError defer func() { since := time.Since(t).Seconds() code := strconv.Itoa(c) feedRequest.WithLabelValues(code).Observe(since) }() res, err := http.Get(feedUrl) if err != nil { return nil, err } defer res.Body.Close() c = res.StatusCode if c == http.StatusOK { return io.ReadAll(res.Body) } return nil, errors.New("Invalid http code") } func appendTag(tag *etree.Element, ap string) { text := tag.Text() tag.SetText(text + ap) } func filterBySeries(series []string, xml []byte, temper bool) ([]byte, error) { doc := etree.NewDocument() err := doc.ReadFromBytes(xml) if err != nil { return nil, err } channel := doc.FindElement("//channel") if temper { tmp := strings.ToUpper(strings.Join(series, ",")) tmp = fmt.Sprintf(" [%s]", tmp) appendTag(channel.FindElement("title"), tmp) appendTag(channel.FindElement("description"), tmp) appendTag(channel.FindElement("link"), "?"+tmp) appendTag(channel.FindElement("author[namespace-prefix()='itunes']"), tmp) appendTag(channel.FindElement("subtitle[namespace-prefix()='itunes']"), tmp) appendTag(channel.FindElement("summary[namespace-prefix()='itunes']"), tmp) appendTag(channel.FindElement("author[namespace-prefix()='googleplay']"), tmp) } for _, tag := range channel.FindElements("item") { title := tag.FindElement("title").Text() if !match(title, series) { channel.RemoveChild(tag) } } return doc.WriteToBytes() } func handleError(next errorRequestHandler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := next(w, r); err != nil { slog.ErrorContext(r.Context(), "Error", "error", err.Error()) if errors.Is(err, errNotFound) { w.WriteHeader(http.StatusNotFound) } else { w.WriteHeader(http.StatusInternalServerError) } } } } func observe(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { t := time.Now() next(w, r) rw := w.(*responseWriter) since := time.Since(t).Seconds() code := strconv.Itoa(rw.Status()) userAgent := r.Header.Get("user-agent") httpRequest.WithLabelValues(code, userAgent).Observe(float64(since)) for _, s := range getSeries(r) { seriesCount.WithLabelValues(s).Inc() } } } func wrap(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { next(NewResponseWriter(w), r) } } func titles(w http.ResponseWriter, r *http.Request) error { xml, err := fetchXML(r.Context()) if err != nil { return err } doc := etree.NewDocument() err = doc.ReadFromBytes(xml) if err != nil { return err } series := getSeries(r) els := doc.FindElements("//channel/item") for _, e := range els { txt := e.FindElement("title").Text() + "\n" if match(txt, series) { _, err = w.Write([]byte(txt)) if err != nil { return err } } } return nil } func view(w http.ResponseWriter, r *http.Request) error { data, err := assets.ReadFile("static/index.html") if err != nil { return err } _, err = w.Write(data) if err != nil { return err } return nil } func podcast(w http.ResponseWriter, r *http.Request) error { if r.URL.Path != "/" { return errNotFound } xml, err := fetchXML(r.Context()) if err != nil { return err } series := getSeries(r) temper := r.URL.Query().Get("tag") == "true" filterdXML, err := filterBySeries(series, xml, temper) if err != nil { return err } _, err = w.Write(filterdXML) if err != nil { return err } return nil } func genSeries() error { xml, err := fetchXML(context.Background()) if err != nil { return err } doc := etree.NewDocument() err = doc.ReadFromBytes(xml) if err != nil { return err } unique := make(map[string]any) els := doc.FindElements("//channel/item") for _, e := range els { txt := e.FindElement("title").Text() res := serieRegex.FindStringSubmatch(txt) if len(res) > 1 { unique[res[1]] = nil } } for k := range unique { fmt.Println(k) } return nil } func main() { if len(os.Args) > 1 && os.Args[1] == "series" { err := genSeries() if err != nil { panic(err.Error()) } return } var ( addr = flag.String("addr", ":8080", "Server address") ) flag.Parse() mux := http.NewServeMux() mux.Handle("/metrics", promhttp.Handler()) mux.HandleFunc("/titles", wrap(handleError(titles))) mux.HandleFunc("/view", wrap(handleError(view))) mux.HandleFunc("/", wrap(observe(handleError(podcast)))) server := http.Server{ Handler: mux, Addr: *addr, } err := server.ListenAndServe() if err != nil { fmt.Printf("Server error: %s", err.Error()) } }