all repos

rss-tools @ af1c5cee5bc11dd4ee70578b375976ded01073a0

get rss feed from sources that(i need and) dont provide one
3 files changed, 238 insertions(+), 8 deletions(-)
imrpove movies id  parsing
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-05-27 20:05:08 +0300
Authored at: 2026-05-27 20:02:36 +0300
Change ID: ooyttkkmtrnrnotyqppzssmzmmqpmyol
Parent: 70f8cb6
M sources/moviefeed/api.go
···
        44
        44
         	TvResults []tmdbShow `json:"tv_results"`

      
        45
        45
         }

      
        46
        46
         

      
        
        47
        +type tmdbSearchResponse struct {

      
        
        48
        +	Results []tmdbShow `json:"results"`

      
        
        49
        +}

      
        
        50
        +

      
        47
        51
         type tmdbSeasonResponse struct {

      
        48
        52
         	Episodes []TMDBEpisode `json:"episodes"`

      
        49
        53
         }

      ···
        105
        109
         		return fmt.Sprintf("%d", result.TvResults[0].ID), nil

      
        106
        110
         	}

      
        107
        111
         	return showID, nil

      
        
        112
        +}

      
        
        113
        +

      
        
        114
        +func (a *TMDBAPI) SearchShow(query string) (*tmdbShow, error) {

      
        
        115
        +	encoded := url.QueryEscape(query)

      
        
        116
        +	result, err := makeRequest[tmdbSearchResponse](a, "/search/tv?query=%s", encoded)

      
        
        117
        +	if err != nil {

      
        
        118
        +		return nil, err

      
        
        119
        +	}

      
        
        120
        +	if len(result.Results) == 0 {

      
        
        121
        +		return nil, fmt.Errorf("no TMDB show found for %q", query)

      
        
        122
        +	}

      
        
        123
        +	return &result.Results[0], nil

      
        108
        124
         }

      
        109
        125
         

      
        110
        126
         func makeRequest[T any](a *TMDBAPI, endpoint string, args ...any) (*T, error) {

      
M sources/moviefeed/moviefeed.go
···
        13
        13
         )

      
        14
        14
         

      
        15
        15
         type moviefeed struct {

      
        16
        
        -	api   episodeFetcher

      
        17
        
        -	shows []string

      
        
        16
        +	api       episodeFetcher

      
        
        17
        +	shows     []string

      
        
        18
        +	nameCache map[string]string

      
        18
        19
         }

      
        19
        20
         

      
        20
        21
         type episodeFetcher interface {

      
        21
        22
         	FetchEpisodesForShow(showID string) ([]TMDBEpisode, error)

      
        
        23
        +	SearchShow(query string) (*tmdbShow, error)

      
        22
        24
         }

      
        23
        25
         

      
        24
        26
         func Register(a *app.App) error {

      ···
        27
        29
         	}

      
        28
        30
         

      
        29
        31
         	mf := &moviefeed{

      
        30
        
        -		api:   NewTMDBAPI(a.Config.MoviefeedAPIKey, a.Client),

      
        31
        
        -		shows: a.Config.MoviefeedShows,

      
        
        32
        +		api:       NewTMDBAPI(a.Config.MoviefeedAPIKey, a.Client),

      
        
        33
        +		shows:     a.Config.MoviefeedShows,

      
        
        34
        +		nameCache: map[string]string{},

      
        32
        35
         	}

      
        33
        36
         

      
        34
        37
         	a.Route("GET /movies", mf.handleMovies)

      ···
        54
        57
         

      
        55
        58
         func (mf *moviefeed) fetchNewEpisodes() ([]TMDBEpisode, error) {

      
        56
        59
         	var allEpisodes []TMDBEpisode

      
        57
        
        -	for _, showID := range mf.shows {

      
        
        60
        +	for _, entry := range mf.shows {

      
        
        61
        +		showID, err := mf.resolveShowID(entry)

      
        
        62
        +		if err != nil {

      
        
        63
        +			slog.Warn("failed to resolve show", "entry", entry, "err", err)

      
        
        64
        +			continue

      
        
        65
        +		}

      
        
        66
        +

      
        58
        67
         		episodes, err := mf.api.FetchEpisodesForShow(showID)

      
        59
        68
         		if err != nil {

      
        60
        
        -			slog.Warn("failed to fetch episodes for show", "show", showID, "err", err)

      
        
        69
        +			slog.Warn("failed to fetch episodes for show", "show", showID, "entry", entry, "err", err)

      
        61
        70
         			continue

      
        62
        71
         		}

      
        63
        72
         		allEpisodes = append(allEpisodes, episodes...)

      
        64
        73
         	}

      
        65
        74
         	return allEpisodes, nil

      
        
        75
        +}

      
        
        76
        +

      
        
        77
        +func (mf *moviefeed) resolveShowID(entry string) (string, error) {

      
        
        78
        +	if cached, ok := mf.nameCache[entry]; ok {

      
        
        79
        +		return cached, nil

      
        
        80
        +	}

      
        
        81
        +

      
        
        82
        +	label, id, hasSep := strings.Cut(entry, "::")

      
        
        83
        +	if hasSep && id != "" {

      
        
        84
        +		mf.nameCache[entry] = id

      
        
        85
        +		return id, nil

      
        
        86
        +	}

      
        
        87
        +

      
        
        88
        +	name := strings.TrimSpace(label)

      
        
        89
        +	if isDirectID(name) {

      
        
        90
        +		return name, nil

      
        
        91
        +	}

      
        
        92
        +

      
        
        93
        +	show, err := mf.api.SearchShow(name)

      
        
        94
        +	if err != nil {

      
        
        95
        +		return "", fmt.Errorf("searching %q: %w", name, err)

      
        
        96
        +	}

      
        
        97
        +

      
        
        98
        +	tmdbID := fmt.Sprintf("%d", show.ID)

      
        
        99
        +	mf.nameCache[entry] = tmdbID

      
        
        100
        +	slog.Info("resolved show name", "name", name, "tmdb_id", tmdbID, "first_air", show.FirstAirDate)

      
        
        101
        +	return tmdbID, nil

      
        
        102
        +}

      
        
        103
        +

      
        
        104
        +func isDirectID(s string) bool {

      
        
        105
        +	if strings.HasPrefix(s, "tt") {

      
        
        106
        +		return true

      
        
        107
        +	}

      
        
        108
        +	for _, c := range s {

      
        
        109
        +		if c < '0' || c > '9' {

      
        
        110
        +			return false

      
        
        111
        +		}

      
        
        112
        +	}

      
        
        113
        +	return len(s) > 0

      
        66
        114
         }

      
        67
        115
         

      
        68
        116
         func generateFeed(episodes []TMDBEpisode) *atom.Feed {

      
M sources/moviefeed/moviefeed_test.go
···
        3
        3
         import (

      
        4
        4
         	"encoding/xml"

      
        5
        5
         	"errors"

      
        
        6
        +	"fmt"

      
        6
        7
         	"net/http"

      
        7
        8
         	"net/http/httptest"

      
        8
        9
         	"strings"

      ···
        16
        17
         type fakeEpisodeAPI struct {

      
        17
        18
         	episodes map[string][]TMDBEpisode

      
        18
        19
         	errs     map[string]error

      
        
        20
        +	searches map[string]tmdbShow

      
        19
        21
         }

      
        20
        22
         

      
        21
        23
         func (f fakeEpisodeAPI) FetchEpisodesForShow(showID string) ([]TMDBEpisode, error) {

      ···
        28
        30
         	return nil, nil

      
        29
        31
         }

      
        30
        32
         

      
        
        33
        +func (f fakeEpisodeAPI) SearchShow(query string) (*tmdbShow, error) {

      
        
        34
        +	if f.searches == nil {

      
        
        35
        +		return nil, fmt.Errorf("no search results for %q", query)

      
        
        36
        +	}

      
        
        37
        +	s, ok := f.searches[query]

      
        
        38
        +	if !ok {

      
        
        39
        +		return nil, fmt.Errorf("no search results for %q", query)

      
        
        40
        +	}

      
        
        41
        +	return &s, nil

      
        
        42
        +}

      
        
        43
        +

      
        31
        44
         func TestHandleMoviesRendersFeedFromConfiguredShows(t *testing.T) {

      
        32
        45
         	episodes := []TMDBEpisode{

      
        33
        46
         		{

      ···
        60
        73
         				"tt123": episodes,

      
        61
        74
         			},

      
        62
        75
         		},

      
        63
        
        -		shows: []string{"tt123"},

      
        
        76
        +		shows:     []string{"tt123"},

      
        
        77
        +		nameCache: map[string]string{},

      
        64
        78
         	}

      
        65
        79
         

      
        66
        80
         	mux := http.NewServeMux()

      ···
        124
        138
         				"bad-show": errors.New("boom"),

      
        125
        139
         			},

      
        126
        140
         		},

      
        127
        
        -		shows: []string{"bad-show", "tt123"},

      
        
        141
        +		shows:     []string{"bad-show", "tt123"},

      
        
        142
        +		nameCache: map[string]string{},

      
        128
        143
         	}

      
        129
        144
         

      
        130
        145
         	mux := http.NewServeMux()

      ···
        168
        183
         	is.Equal(t, strings.Contains(content, `<img src="https://image.tmdb.org/t/p/w500/e1.jpg" alt="Episode 1"`), true)

      
        169
        184
         	is.Equal(t, strings.Contains(content, "</body>"), true)

      
        170
        185
         }

      
        
        186
        +

      
        
        187
        +func TestIsDirectID(t *testing.T) {

      
        
        188
        +	is.Equal(t, true, isDirectID("tt1190634"))

      
        
        189
        +	is.Equal(t, true, isDirectID("101"))

      
        
        190
        +	is.Equal(t, true, isDirectID("1"))

      
        
        191
        +	is.Equal(t, false, isDirectID(""))

      
        
        192
        +	is.Equal(t, false, isDirectID("The Boys"))

      
        
        193
        +	is.Equal(t, false, isDirectID("101a"))

      
        
        194
        +}

      
        
        195
        +

      
        
        196
        +func TestResolveShowIDDirectPassthrough(t *testing.T) {

      
        
        197
        +	mf := &moviefeed{nameCache: map[string]string{}, api: fakeEpisodeAPI{}}

      
        
        198
        +	id, err := mf.resolveShowID("tt123")

      
        
        199
        +	is.Err(t, err, nil)

      
        
        200
        +	is.Equal(t, "tt123", id)

      
        
        201
        +

      
        
        202
        +	id, err = mf.resolveShowID("42")

      
        
        203
        +	is.Err(t, err, nil)

      
        
        204
        +	is.Equal(t, "42", id)

      
        
        205
        +}

      
        
        206
        +

      
        
        207
        +func TestResolveShowIDNameIDSkipsSearch(t *testing.T) {

      
        
        208
        +	mf := &moviefeed{nameCache: map[string]string{}, api: fakeEpisodeAPI{}}

      
        
        209
        +	id, err := mf.resolveShowID("The Boys::101")

      
        
        210
        +	is.Err(t, err, nil)

      
        
        211
        +	is.Equal(t, "101", id)

      
        
        212
        +}

      
        
        213
        +

      
        
        214
        +func TestResolveShowIDNameIDSkipsSearchWithIMDB(t *testing.T) {

      
        
        215
        +	mf := &moviefeed{nameCache: map[string]string{}, api: fakeEpisodeAPI{}}

      
        
        216
        +	id, err := mf.resolveShowID("The Boys::tt1190634")

      
        
        217
        +	is.Err(t, err, nil)

      
        
        218
        +	is.Equal(t, "tt1190634", id)

      
        
        219
        +}

      
        
        220
        +

      
        
        221
        +func TestResolveShowIDSearchesAndCaches(t *testing.T) {

      
        
        222
        +	mf := &moviefeed{

      
        
        223
        +		nameCache: map[string]string{},

      
        
        224
        +		api: fakeEpisodeAPI{

      
        
        225
        +			searches: map[string]tmdbShow{

      
        
        226
        +				"The Boys": {ID: 1398, Name: "The Boys"},

      
        
        227
        +			},

      
        
        228
        +		},

      
        
        229
        +	}

      
        
        230
        +

      
        
        231
        +	id, err := mf.resolveShowID("The Boys")

      
        
        232
        +	is.Err(t, err, nil)

      
        
        233
        +	is.Equal(t, "1398", id)

      
        
        234
        +

      
        
        235
        +	cached, ok := mf.nameCache["The Boys"]

      
        
        236
        +	is.Equal(t, true, ok)

      
        
        237
        +	is.Equal(t, "1398", cached)

      
        
        238
        +}

      
        
        239
        +

      
        
        240
        +func TestResolveShowIDCacheHit(t *testing.T) {

      
        
        241
        +	mf := &moviefeed{

      
        
        242
        +		nameCache: map[string]string{"The Boys": "1398"},

      
        
        243
        +		api:       fakeEpisodeAPI{}, // empty — would fail if search called

      
        
        244
        +	}

      
        
        245
        +

      
        
        246
        +	id, err := mf.resolveShowID("The Boys")

      
        
        247
        +	is.Err(t, err, nil)

      
        
        248
        +	is.Equal(t, "1398", id)

      
        
        249
        +}

      
        
        250
        +

      
        
        251
        +func TestResolveShowIDSearchFails(t *testing.T) {

      
        
        252
        +	mf := &moviefeed{

      
        
        253
        +		nameCache: map[string]string{},

      
        
        254
        +		api:       fakeEpisodeAPI{}, // no searches registered

      
        
        255
        +	}

      
        
        256
        +

      
        
        257
        +	_, err := mf.resolveShowID("Nonexistent Show")

      
        
        258
        +	if err == nil {

      
        
        259
        +		t.Fatal("expected error for unresolvable name")

      
        
        260
        +	}

      
        
        261
        +}

      
        
        262
        +

      
        
        263
        +func TestFetchNewEpisodesResolvesNames(t *testing.T) {

      
        
        264
        +	episodes := []TMDBEpisode{

      
        
        265
        +		{

      
        
        266
        +			ID:            1001,

      
        
        267
        +			Name:          "E1",

      
        
        268
        +			AirDate:       time.Now().AddDate(0, 0, -2).Format(dateFormat),

      
        
        269
        +			EpisodeNumber: 1,

      
        
        270
        +			SeasonNumber:  1,

      
        
        271
        +			ShowName:      "The Boys",

      
        
        272
        +			ShowID:        "1398",

      
        
        273
        +		},

      
        
        274
        +	}

      
        
        275
        +

      
        
        276
        +	mf := &moviefeed{

      
        
        277
        +		nameCache: map[string]string{},

      
        
        278
        +		api: fakeEpisodeAPI{

      
        
        279
        +			searches: map[string]tmdbShow{

      
        
        280
        +				"The Boys": {ID: 1398, Name: "The Boys"},

      
        
        281
        +			},

      
        
        282
        +			episodes: map[string][]TMDBEpisode{

      
        
        283
        +				"1398": episodes,

      
        
        284
        +			},

      
        
        285
        +		},

      
        
        286
        +		shows: []string{"The Boys"},

      
        
        287
        +	}

      
        
        288
        +

      
        
        289
        +	got, err := mf.fetchNewEpisodes()

      
        
        290
        +	is.Err(t, err, nil)

      
        
        291
        +	is.Equal(t, 1, len(got))

      
        
        292
        +	is.Equal(t, 1001, got[0].ID)

      
        
        293
        +}

      
        
        294
        +

      
        
        295
        +func TestFetchNewEpisodesNameIDFormat(t *testing.T) {

      
        
        296
        +	episodes := []TMDBEpisode{

      
        
        297
        +		{

      
        
        298
        +			ID:            1001,

      
        
        299
        +			Name:          "E1",

      
        
        300
        +			AirDate:       time.Now().AddDate(0, 0, -2).Format(dateFormat),

      
        
        301
        +			EpisodeNumber: 1,

      
        
        302
        +			SeasonNumber:  1,

      
        
        303
        +			ShowName:      "The Boys",

      
        
        304
        +			ShowID:        "1398",

      
        
        305
        +		},

      
        
        306
        +	}

      
        
        307
        +

      
        
        308
        +	mf := &moviefeed{

      
        
        309
        +		nameCache: map[string]string{},

      
        
        310
        +		api: fakeEpisodeAPI{

      
        
        311
        +			episodes: map[string][]TMDBEpisode{

      
        
        312
        +				"1398": episodes,

      
        
        313
        +			},

      
        
        314
        +		},

      
        
        315
        +		shows: []string{"The Boys::1398"},

      
        
        316
        +	}

      
        
        317
        +

      
        
        318
        +	got, err := mf.fetchNewEpisodes()

      
        
        319
        +	is.Err(t, err, nil)

      
        
        320
        +	is.Equal(t, 1, len(got))

      
        
        321
        +	is.Equal(t, 1001, got[0].ID)

      
        
        322
        +}

      
        
        323
        +

      
        
        324
        +func TestFetchNewEpisodesContinuesOnResolveError(t *testing.T) {

      
        
        325
        +	mf := &moviefeed{

      
        
        326
        +		nameCache: map[string]string{},

      
        
        327
        +		api: fakeEpisodeAPI{

      
        
        328
        +			episodes: map[string][]TMDBEpisode{},

      
        
        329
        +		},

      
        
        330
        +		shows: []string{"Does Not Exist", "tt1190634"},

      
        
        331
        +	}

      
        
        332
        +

      
        
        333
        +	got, err := mf.fetchNewEpisodes()

      
        
        334
        +	is.Err(t, err, nil)

      
        
        335
        +	is.Equal(t, 0, len(got))

      
        
        336
        +}