package moviefeed import ( "fmt" "html" "log/slog" "net/http" "strings" "time" "olexsmir.xyz/rss-tools/app" ) type moviefeed struct { api *TMDBAPI shows []string } func Register(a *app.App) error { if a.Config.MoviefeedAPIKey == "" { return nil } mf := &moviefeed{ api: NewTMDBAPI(a.Config.MoviefeedAPIKey, a.Client), shows: a.Config.MoviefeedShows, } a.Route("GET /movies", mf.handleMovies) a.Route("GET /movies/", mf.handleMovies) a.Logger.Info("moviefeed source registered") return nil } func (mf *moviefeed) handleMovies(w http.ResponseWriter, r *http.Request) { episodes, err := mf.fetchNewEpisodes() if err != nil { slog.Error("failed to fetch episodes", "err", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } feed := generateFeed(episodes) if err := feed.Render(w); err != nil { http.Error(w, "failed to render feed", http.StatusInternalServerError) } } func (mf *moviefeed) fetchNewEpisodes() ([]TMDBEpisode, error) { var allEpisodes []TMDBEpisode for _, showID := range mf.shows { episodes, err := mf.api.FetchEpisodesForShow(showID) if err != nil { slog.Warn("failed to fetch episodes for show", "show", showID, "err", err) continue } allEpisodes = append(allEpisodes, episodes...) } return allEpisodes, nil } func generateFeed(episodes []TMDBEpisode) *app.FeedBuilder { feed := app.NewFeed("moviefeed", "moviefeed"). WithSubtitle("Latest episodes from followed shows") for i := len(episodes) - 1; i >= 0; i-- { ep := episodes[i] airDate, _ := time.Parse(dateFormat, ep.AirDate) content, contentType := episodeContent(ep) links := []app.FeedLink{ { Rel: "alternate", Href: fmt.Sprintf("https://www.themoviedb.org/tv/episode/%d", ep.ID), }, } if ep.StillPath != "" { links = append(links, app.FeedLink{ Rel: "enclosure", Type: "image/jpeg", Length: "0", Href: tmdbImageBaseURL + ep.StillPath, }) } feed.Add(app.FeedEntry{ ID: fmt.Sprintf("%s-%d-%d", ep.ShowID, ep.SeasonNumber, ep.EpisodeNumber), Title: fmt.Sprintf( "%s S%dE%d: %s", ep.ShowName, ep.SeasonNumber, ep.EpisodeNumber, ep.Name, ), Content: content, ContentType: contentType, Updated: airDate, Links: links, }) } return feed } func episodeContent(ep TMDBEpisode) (string, string) { if ep.StillPath == "" { return ep.Overview, "" } imageURL := tmdbImageBaseURL + ep.StillPath parts := make([]string, 0, 4) parts = append(parts, "
") if text := strings.TrimSpace(ep.Overview); text != "" { parts = append(parts, ""+html.EscapeString(text)+"
") } parts = append(parts, fmt.Sprintf(`