all repos

rss-tools @ 8c0c68e023cfb8e513f5d9d072ab47ac61b5bb65

get rss feed from sources that(i need and) dont provide one
2 files changed, 93 insertions(+), 139 deletions(-)
moviefeed: dont make any requests to the actual api
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-05-23 15:55:44 +0300
Authored at: 2026-05-23 15:51:15 +0300
Change ID: vlovrwlosknrznutqnotuxyqlkuzqlsr
Parent: 71f9578
M sources/moviefeed/moviefeed.go
···
        13
        13
         )

      
        14
        14
         

      
        15
        15
         type moviefeed struct {

      
        16
        
        -	api   *TMDBAPI

      
        
        16
        +	api   episodeFetcher

      
        17
        17
         	shows []string

      
        
        18
        +}

      
        
        19
        +

      
        
        20
        +type episodeFetcher interface {

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

      
        18
        22
         }

      
        19
        23
         

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

      ···
        63
        67
         

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

      
        65
        69
         	feed := atom.NewFeed("moviefeed", "moviefeed")

      
        66
        
        -

      
        67
        70
         	for i := len(episodes) - 1; i >= 0; i-- {

      
        68
        71
         		ep := episodes[i]

      
        69
        72
         		airDate, _ := time.Parse(dateFormat, ep.AirDate)

      
M sources/moviefeed/moviefeed_test.go
···
        2
        2
         

      
        3
        3
         import (

      
        4
        4
         	"encoding/xml"

      
        5
        
        -	"fmt"

      
        
        5
        +	"errors"

      
        6
        6
         	"net/http"

      
        7
        7
         	"net/http/httptest"

      
        8
        
        -	"net/url"

      
        9
        8
         	"strings"

      
        10
        9
         	"testing"

      
        11
        10
         	"time"

      ···
        14
        13
         	"olexsmir.xyz/x/is"

      
        15
        14
         )

      
        16
        15
         

      
        
        16
        +type fakeEpisodeAPI struct {

      
        
        17
        +	episodes map[string][]TMDBEpisode

      
        
        18
        +	errs     map[string]error

      
        
        19
        +}

      
        
        20
        +

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

      
        
        22
        +	if err, ok := f.errs[showID]; ok {

      
        
        23
        +		return nil, err

      
        
        24
        +	}

      
        
        25
        +	if episodes, ok := f.episodes[showID]; ok {

      
        
        26
        +		return episodes, nil

      
        
        27
        +	}

      
        
        28
        +	return nil, nil

      
        
        29
        +}

      
        
        30
        +

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

      
        18
        
        -	server, client := newTMDBStub(t)

      
        19
        
        -	defer server.Close()

      
        
        32
        +	episodes := []TMDBEpisode{

      
        
        33
        +		{

      
        
        34
        +			ID:            1001,

      
        
        35
        +			Name:          "Episode 1",

      
        
        36
        +			Overview:      "E1",

      
        
        37
        +			AirDate:       "2026-04-20",

      
        
        38
        +			EpisodeNumber: 1,

      
        
        39
        +			SeasonNumber:  1,

      
        
        40
        +			StillPath:     "/e1.jpg",

      
        
        41
        +			ShowName:      "Test Show",

      
        
        42
        +			ShowID:        "101",

      
        
        43
        +		},

      
        
        44
        +		{

      
        
        45
        +			ID:            1002,

      
        
        46
        +			Name:          "Episode 2",

      
        
        47
        +			Overview:      "E2",

      
        
        48
        +			AirDate:       "2026-04-21",

      
        
        49
        +			EpisodeNumber: 2,

      
        
        50
        +			SeasonNumber:  1,

      
        
        51
        +			StillPath:     "",

      
        
        52
        +			ShowName:      "Test Show",

      
        
        53
        +			ShowID:        "101",

      
        
        54
        +		},

      
        
        55
        +	}

      
        20
        56
         

      
        21
        57
         	mf := &moviefeed{

      
        22
        
        -		api:   NewTMDBAPI("test-key", client),

      
        
        58
        +		api: fakeEpisodeAPI{

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

      
        
        60
        +				"tt123": episodes,

      
        
        61
        +			},

      
        
        62
        +		},

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

      
        24
        64
         	}

      
        25
        65
         

      ···
        38
        78
         	var feed atom.Feed

      
        39
        79
         	is.Err(t, xml.NewDecoder(rr.Body).Decode(&feed), nil)

      
        40
        80
         	is.Equal(t, feed.Title, "moviefeed")

      
        41
        
        -	is.Equal(t, feed.Subtitle, "Latest episodes from followed shows")

      
        42
        81
         	is.Equal(t, len(feed.Entry), 2)

      
        43
        82
         	is.Equal(t, strings.Contains(feed.Entry[0].Title, "S1E2"), true)

      
        44
        83
         	is.Equal(t, feed.Entry[0].Content.Type, "text")

      ···
        51
        90
         }

      
        52
        91
         

      
        53
        92
         func TestHandleMoviesContinuesWhenOneShowFails(t *testing.T) {

      
        54
        
        -	server, client := newTMDBStub(t)

      
        55
        
        -	defer server.Close()

      
        
        93
        +	episodes := []TMDBEpisode{

      
        
        94
        +		{

      
        
        95
        +			ID:            1001,

      
        
        96
        +			Name:          "Episode 1",

      
        
        97
        +			Overview:      "E1",

      
        
        98
        +			AirDate:       "2026-04-20",

      
        
        99
        +			EpisodeNumber: 1,

      
        
        100
        +			SeasonNumber:  1,

      
        
        101
        +			StillPath:     "/e1.jpg",

      
        
        102
        +			ShowName:      "Test Show",

      
        
        103
        +			ShowID:        "101",

      
        
        104
        +		},

      
        
        105
        +		{

      
        
        106
        +			ID:            1002,

      
        
        107
        +			Name:          "Episode 2",

      
        
        108
        +			Overview:      "E2",

      
        
        109
        +			AirDate:       "2026-04-21",

      
        
        110
        +			EpisodeNumber: 2,

      
        
        111
        +			SeasonNumber:  1,

      
        
        112
        +			StillPath:     "",

      
        
        113
        +			ShowName:      "Test Show",

      
        
        114
        +			ShowID:        "101",

      
        
        115
        +		},

      
        
        116
        +	}

      
        56
        117
         

      
        57
        118
         	mf := &moviefeed{

      
        58
        
        -		api:   NewTMDBAPI("test-key", client),

      
        
        119
        +		api: fakeEpisodeAPI{

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

      
        
        121
        +				"tt123": episodes,

      
        
        122
        +			},

      
        
        123
        +			errs: map[string]error{

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

      
        
        125
        +			},

      
        
        126
        +		},

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

      
        60
        128
         	}

      
        61
        129
         

      ···
        73
        141
         	is.Equal(t, len(feed.Entry), 2)

      
        74
        142
         }

      
        75
        143
         

      
        76
        
        -func TestFetchEpisodesForShowFiltersRecentAndMapsFields(t *testing.T) {

      
        77
        
        -	server, client := newTMDBStub(t)

      
        78
        
        -	defer server.Close()

      
        
        144
        +func TestFilterRecentEpisodes(t *testing.T) {

      
        
        145
        +	now := time.Now()

      
        
        146
        +	recent := now.AddDate(0, 0, -5).Format(dateFormat)

      
        
        147
        +	old := now.AddDate(0, 0, -40).Format(dateFormat)

      
        79
        148
         

      
        80
        
        -	api := NewTMDBAPI("test-key", client)

      
        81
        
        -	episodes, err := api.FetchEpisodesForShow("tt123")

      
        82
        
        -	is.Err(t, err, nil)

      
        
        149
        +	episodes := filterRecentEpisodes([]TMDBEpisode{

      
        
        150
        +		{AirDate: recent, Name: "recent"},

      
        
        151
        +		{AirDate: old, Name: "old"},

      
        
        152
        +		{AirDate: "", Name: "missing"},

      
        
        153
        +	})

      
        83
        154
         

      
        84
        
        -	is.Equal(t, len(episodes), 2)

      
        85
        
        -	is.Equal(t, episodes[0].ShowID, "101")

      
        86
        
        -	is.Equal(t, episodes[0].ShowName, "Test Show")

      
        87
        
        -}

      
        88
        
        -

      
        89
        
        -func TestFetchEpisodesForShowSeasonErrorDoesNotFailShow(t *testing.T) {

      
        90
        
        -	server, client := newTMDBStubWithSeasonError(t)

      
        91
        
        -	defer server.Close()

      
        92
        
        -

      
        93
        
        -	api := NewTMDBAPI("test-key", client)

      
        94
        
        -	episodes, err := api.FetchEpisodesForShow("tt123")

      
        95
        
        -	is.Err(t, err, nil)

      
        96
        
        -	is.Equal(t, len(episodes), 0)

      
        
        155
        +	is.Equal(t, 1, len(episodes))

      
        
        156
        +	is.Equal(t, "recent", episodes[0].Name)

      
        97
        157
         }

      
        98
        158
         

      
        99
        159
         func TestEpisodeContentIncludesImageInBody(t *testing.T) {

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

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

      
        110
        170
         }

      
        111
        
        -

      
        112
        
        -func newTMDBStub(t *testing.T) (*httptest.Server, *http.Client) {

      
        113
        
        -	t.Helper()

      
        114
        
        -

      
        115
        
        -	recentDay := time.Now().AddDate(0, 0, -7).Format(dateFormat)

      
        116
        
        -	newestDay := time.Now().AddDate(0, 0, -1).Format(dateFormat)

      
        117
        
        -	oldDay := time.Now().AddDate(0, 0, -45).Format(dateFormat)

      
        118
        
        -

      
        119
        
        -	mux := http.NewServeMux()

      
        120
        
        -	mux.HandleFunc("/3/find/tt123", func(w http.ResponseWriter, r *http.Request) {

      
        121
        
        -		if got := r.URL.Query().Get("external_source"); got != "imdb_id" {

      
        122
        
        -			t.Fatalf("unexpected external_source query: %q", got)

      
        123
        
        -		}

      
        124
        
        -		if got := r.URL.Query().Get("api_key"); got != "test-key" {

      
        125
        
        -			t.Fatalf("unexpected api_key query: %q", got)

      
        126
        
        -		}

      
        127
        
        -		_, _ = w.Write([]byte(`{"tv_results":[{"id":101}]}`))

      
        128
        
        -	})

      
        129
        
        -

      
        130
        
        -	mux.HandleFunc("/3/find/bad-show", func(w http.ResponseWriter, r *http.Request) {

      
        131
        
        -		if got := r.URL.Query().Get("external_source"); got != "imdb_id" {

      
        132
        
        -			t.Fatalf("unexpected external_source query: %q", got)

      
        133
        
        -		}

      
        134
        
        -		if got := r.URL.Query().Get("api_key"); got != "test-key" {

      
        135
        
        -			t.Fatalf("unexpected api_key query: %q", got)

      
        136
        
        -		}

      
        137
        
        -		_, _ = w.Write([]byte(`{"tv_results":[]}`))

      
        138
        
        -	})

      
        139
        
        -

      
        140
        
        -	mux.HandleFunc("/3/tv/101", func(w http.ResponseWriter, r *http.Request) {

      
        141
        
        -		if got := r.URL.Query().Get("api_key"); got != "test-key" {

      
        142
        
        -			t.Fatalf("unexpected api_key query: %q", got)

      
        143
        
        -		}

      
        144
        
        -		_, _ = w.Write([]byte(`{"id":101,"name":"Test Show","number_of_seasons":1}`))

      
        145
        
        -	})

      
        146
        
        -

      
        147
        
        -	mux.HandleFunc("/3/tv/101/season/1", func(w http.ResponseWriter, r *http.Request) {

      
        148
        
        -		if got := r.URL.Query().Get("api_key"); got != "test-key" {

      
        149
        
        -			t.Fatalf("unexpected api_key query: %q", got)

      
        150
        
        -		}

      
        151
        
        -		body := fmt.Sprintf(`{

      
        152
        
        -			"episodes": [

      
        153
        
        -				{"id": 1001, "name": "Episode 1", "overview": "E1", "air_date": %q, "episode_number": 1, "season_number": 1, "still_path": "/e1.jpg"},

      
        154
        
        -				{"id": 1002, "name": "Episode 2", "overview": "E2", "air_date": %q, "episode_number": 2, "season_number": 1, "still_path": ""},

      
        155
        
        -				{"id": 1003, "name": "Episode old", "overview": "old", "air_date": %q, "episode_number": 3, "season_number": 1, "still_path": ""}

      
        156
        
        -			]

      
        157
        
        -		}`, recentDay, newestDay, oldDay)

      
        158
        
        -		_, _ = w.Write([]byte(body))

      
        159
        
        -	})

      
        160
        
        -

      
        161
        
        -	server := httptest.NewServer(mux)

      
        162
        
        -	target, err := url.Parse(server.URL)

      
        163
        
        -	is.Err(t, err, nil)

      
        164
        
        -

      
        165
        
        -	client := &http.Client{

      
        166
        
        -		Transport: rewriteTransport{target: target},

      
        167
        
        -	}

      
        168
        
        -	return server, client

      
        169
        
        -}

      
        170
        
        -

      
        171
        
        -func newTMDBStubWithSeasonError(t *testing.T) (*httptest.Server, *http.Client) {

      
        172
        
        -	t.Helper()

      
        173
        
        -

      
        174
        
        -	mux := http.NewServeMux()

      
        175
        
        -	mux.HandleFunc("/3/find/tt123", func(w http.ResponseWriter, r *http.Request) {

      
        176
        
        -		if got := r.URL.Query().Get("external_source"); got != "imdb_id" {

      
        177
        
        -			t.Fatalf("unexpected external_source query: %q", got)

      
        178
        
        -		}

      
        179
        
        -		if got := r.URL.Query().Get("api_key"); got != "test-key" {

      
        180
        
        -			t.Fatalf("unexpected api_key query: %q", got)

      
        181
        
        -		}

      
        182
        
        -		_, _ = w.Write([]byte(`{"tv_results":[{"id":101}]}`))

      
        183
        
        -	})

      
        184
        
        -

      
        185
        
        -	mux.HandleFunc("/3/tv/101", func(w http.ResponseWriter, r *http.Request) {

      
        186
        
        -		if got := r.URL.Query().Get("api_key"); got != "test-key" {

      
        187
        
        -			t.Fatalf("unexpected api_key query: %q", got)

      
        188
        
        -		}

      
        189
        
        -		_, _ = w.Write([]byte(`{"id":101,"name":"Test Show","number_of_seasons":1}`))

      
        190
        
        -	})

      
        191
        
        -

      
        192
        
        -	mux.HandleFunc("/3/tv/101/season/1", func(w http.ResponseWriter, r *http.Request) {

      
        193
        
        -		w.WriteHeader(http.StatusInternalServerError)

      
        194
        
        -		_, _ = w.Write([]byte(`bad gateway`))

      
        195
        
        -	})

      
        196
        
        -

      
        197
        
        -	server := httptest.NewServer(mux)

      
        198
        
        -	target, err := url.Parse(server.URL)

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

      
        200
        
        -

      
        201
        
        -	client := &http.Client{

      
        202
        
        -		Transport: rewriteTransport{target: target},

      
        203
        
        -	}

      
        204
        
        -	return server, client

      
        205
        
        -}

      
        206
        
        -

      
        207
        
        -type rewriteTransport struct {

      
        208
        
        -	target *url.URL

      
        209
        
        -}

      
        210
        
        -

      
        211
        
        -func (t rewriteTransport) RoundTrip(req *http.Request) (*http.Response, error) {

      
        212
        
        -	clone := req.Clone(req.Context())

      
        213
        
        -	copiedURL := *clone.URL

      
        214
        
        -	copiedURL.Scheme = t.target.Scheme

      
        215
        
        -	copiedURL.Host = t.target.Host

      
        216
        
        -	clone.URL = &copiedURL

      
        217
        
        -	clone.Host = t.target.Host

      
        218
        
        -	return http.DefaultTransport.RoundTrip(clone)

      
        219
        
        -}