all repos

rss-tools @ 11fd04d4db9db13f1178670ca3f0f008409951bc

get rss feed from sources that(i need and) dont provide one
2 files changed, 92 insertions(+), 16 deletions(-)
update weather
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-05-08 20:32:52 +0300
Authored at: 2026-05-08 20:31:34 +0300
Change ID: tklwwrsooxmoqkwmuzpmnwsmpzlpwwlk
Parent: 50b546d
M sources/weather/weather.go
···
        65
        65
         		return

      
        66
        66
         	}

      
        67
        67
         

      
        68
        
        -	content, updated, err := buildMorningBriefing(forecast, air)

      
        
        68
        +	content, updated, err := buildWeatherBriefing(forecast, air)

      
        69
        69
         	if err != nil {

      
        70
        70
         		http.Error(rw, "failed to build weather briefing", http.StatusBadGateway)

      
        71
        71
         		return

      ···
        81
        81
         		WithUpdated(updated)

      
        82
        82
         

      
        83
        83
         	feed.Add(app.FeedEntry{

      
        84
        
        -		Title:       "Morning weather briefing",

      
        
        84
        +		Title:       "Weather briefing",

      
        85
        85
         		ID:          fmt.Sprintf("%s-%s", feedID, updated.Format("20060102")),

      
        86
        
        -		Content:     formatBriefingHTML(content),

      
        87
        
        -		ContentType: "html",

      
        
        86
        +		Content:     formatBriefingXHTML(content),

      
        
        87
        +		ContentType: "xhtml",

      
        88
        88
         		Updated:     updated,

      
        89
        89
         	})

      
        90
        90
         

      ···
        322
        322
         	return strings.TrimSpace(part)

      
        323
        323
         }

      
        324
        324
         

      
        325
        
        -func formatBriefingHTML(content string) string {

      
        326
        
        -	return "<pre>" + html.EscapeString(content) + "</pre>"

      
        
        325
        +func formatBriefingXHTML(content string) string {

      
        
        326
        +	blocks := strings.Split(content, "\n\n")

      
        
        327
        +	overview := []string{}

      
        
        328
        +	timeline := []string{}

      
        
        329
        +	meta := []string{}

      
        
        330
        +

      
        
        331
        +	if len(blocks) > 0 {

      
        
        332
        +		overview = nonEmptyLines(blocks[0])

      
        
        333
        +	}

      
        
        334
        +	if len(blocks) > 1 {

      
        
        335
        +		timeline = nonEmptyLines(blocks[1])

      
        
        336
        +	}

      
        
        337
        +	if len(blocks) > 2 {

      
        
        338
        +		meta = nonEmptyLines(strings.Join(blocks[2:], "\n\n"))

      
        
        339
        +	}

      
        
        340
        +

      
        
        341
        +	var b strings.Builder

      
        
        342
        +	b.WriteString("<article>")

      
        
        343
        +

      
        
        344
        +	if len(overview) > 0 {

      
        
        345
        +		b.WriteString("<section><h2>Overview</h2>")

      
        
        346
        +		b.WriteString("<p>" + html.EscapeString(overview[0]) + "</p>")

      
        
        347
        +		if len(overview) > 1 {

      
        
        348
        +			b.WriteString("<ul>")

      
        
        349
        +			for _, line := range overview[1:] {

      
        
        350
        +				b.WriteString("<li>" + html.EscapeString(line) + "</li>")

      
        
        351
        +			}

      
        
        352
        +			b.WriteString("</ul>")

      
        
        353
        +		}

      
        
        354
        +		b.WriteString("</section>")

      
        
        355
        +	}

      
        
        356
        +

      
        
        357
        +	if len(timeline) > 0 {

      
        
        358
        +		b.WriteString("<section><h2>Timeline</h2><ul>")

      
        
        359
        +		for _, line := range timeline {

      
        
        360
        +			timestamp, rest, ok := strings.Cut(strings.TrimSpace(line), "  ")

      
        
        361
        +			if ok {

      
        
        362
        +				b.WriteString("<li><strong>" + html.EscapeString(strings.TrimSpace(timestamp)) + "</strong> " + html.EscapeString(strings.TrimSpace(rest)) + "</li>")

      
        
        363
        +				continue

      
        
        364
        +			}

      
        
        365
        +			b.WriteString("<li>" + html.EscapeString(line) + "</li>")

      
        
        366
        +		}

      
        
        367
        +		b.WriteString("</ul></section>")

      
        
        368
        +	}

      
        
        369
        +

      
        
        370
        +	if len(meta) > 0 {

      
        
        371
        +		b.WriteString("<section><h2>Source</h2>")

      
        
        372
        +		for _, line := range meta {

      
        
        373
        +			b.WriteString("<p>" + html.EscapeString(line) + "</p>")

      
        
        374
        +		}

      
        
        375
        +		b.WriteString("</section>")

      
        
        376
        +	}

      
        
        377
        +

      
        
        378
        +	b.WriteString("</article>")

      
        
        379
        +	return b.String()

      
        327
        380
         }

      
        328
        381
         

      
        329
        
        -func buildMorningBriefing(forecast forecastResponse, air airQualityResponse) (string, time.Time, error) {

      
        
        382
        +func nonEmptyLines(block string) []string {

      
        
        383
        +	lines := strings.Split(block, "\n")

      
        
        384
        +	out := make([]string, 0, len(lines))

      
        
        385
        +	for _, line := range lines {

      
        
        386
        +		line = strings.TrimSpace(line)

      
        
        387
        +		if line == "" {

      
        
        388
        +			continue

      
        
        389
        +		}

      
        
        390
        +		out = append(out, line)

      
        
        391
        +	}

      
        
        392
        +	return out

      
        
        393
        +}

      
        
        394
        +

      
        
        395
        +func buildWeatherBriefing(forecast forecastResponse, air airQualityResponse) (string, time.Time, error) {

      
        330
        396
         	loc := time.Local

      
        331
        397
         	if tz := strings.TrimSpace(forecast.Timezone); tz != "" {

      
        332
        398
         		zone, err := time.LoadLocation(tz)

      
M sources/weather/weather_test.go
···
        67
        67
           }

      
        68
        68
         }`

      
        69
        69
         

      
        70
        
        -func TestWeatherHandlerRendersMorningBriefing(t *testing.T) {

      
        
        70
        +func TestWeatherHandlerRendersWeatherBriefing(t *testing.T) {

      
        71
        71
         	forecastSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

      
        72
        72
         		w.Header().Set("Content-Type", "application/json")

      
        73
        73
         		_, _ = w.Write([]byte(forecastFixture))

      ···
        105
        105
         		t.Fatalf("expected atom feed, got %q", got)

      
        106
        106
         	}

      
        107
        107
         

      
        
        108
        +	raw := rr.Body.String()

      
        108
        109
         	var feed app.AtomFeed

      
        109
        
        -	is.Err(t, xml.NewDecoder(rr.Body).Decode(&feed), nil)

      
        
        110
        +	is.Err(t, xml.NewDecoder(strings.NewReader(raw)).Decode(&feed), nil)

      
        110
        111
         	is.Equal(t, len(feed.Entries), 1)

      
        111
        112
         	is.Equal(t, feed.Title, "Weather forecast for Kyiv")

      
        112
        
        -	is.Equal(t, feed.Entries[0].Content.Type, "html")

      
        113
        
        -	is.Equal(t, strings.HasPrefix(feed.Entries[0].Content.Value, "<pre>"), true)

      
        
        113
        +	is.Equal(t, feed.Entries[0].Title, "Weather briefing")

      
        
        114
        +	is.Equal(t, feed.Entries[0].Content.Type, "xhtml")

      
        114
        115
         

      
        115
        
        -	content := feed.Entries[0].Content.Value

      
        
        116
        +	content := raw

      
        
        117
        +	if !strings.Contains(content, `<content type="xhtml">`) {

      
        
        118
        +		t.Fatalf("missing xhtml content wrapper in response:\n%s", content)

      
        
        119
        +	}

      
        
        120
        +	if !strings.Contains(content, "<article>") || !strings.Contains(content, "<h2>Overview</h2>") {

      
        
        121
        +		t.Fatalf("missing overview structure in content:\n%s", content)

      
        
        122
        +	}

      
        
        123
        +	if !strings.Contains(content, "<h2>Timeline</h2>") {

      
        
        124
        +		t.Fatalf("missing timeline structure in content:\n%s", content)

      
        
        125
        +	}

      
        116
        126
         	if !strings.Contains(content, "+13° / +22°  |  Cloudy with showers in the afternoon") {

      
        117
        127
         		t.Fatalf("missing summary line in content:\n%s", content)

      
        118
        128
         	}

      ···
        132
        142
         		t.Fatalf("missing air quality line in content:\n%s", content)

      
        133
        143
         	}

      
        134
        144
         	for _, line := range []string{

      
        135
        
        -		"08:00  +14°  ☁ Cloudy",

      
        136
        
        -		"12:00  +18°  🌥 Partly cloudy",

      
        137
        
        -		"16:00  +21°  🌧 Rain showers",

      
        138
        
        -		"20:00  +16°  🌥 Partly cloudy",

      
        
        145
        +		"<strong>08:00</strong> +14°  ☁ Cloudy",

      
        
        146
        +		"<strong>12:00</strong> +18°  🌥 Partly cloudy",

      
        
        147
        +		"<strong>16:00</strong> +21°  🌧 Rain showers",

      
        
        148
        +		"<strong>20:00</strong> +16°  🌥 Partly cloudy",

      
        139
        149
         	} {

      
        140
        150
         		if !strings.Contains(content, line) {

      
        141
        151
         			t.Fatalf("missing timeline line %q in content:\n%s", line, content)