~louis77/gemini.tunerapp.org

907be2976d10f36bb2517afe55b6fbd2cb80d690 — Louis Brauer 11 months ago 07051ac
Pin stations to home
M README.md => README.md +2 -1
@@ 11,9 11,10 @@ This is a rewrite of version 1, but as a dynamic service using Gig. Planned feat
- [X] Browse all stations (as in v1)
- [X] Browse by country (as in v1)
- [X] Station search
- [X] Proxy images to enable inlining
- [X] Users can pin stations to home with comment
- [ ] Browse by genre
- [ ] Add "Station of the day" ?
- [X] Proxy images to enable inlining
- [ ] Add CLI flags to force cache reload
- [ ] Add TTL to cache


M go.mod => go.mod +1 -0
@@ 6,6 6,7 @@ replace gitlab.com/AgentNemo/goradios => ../goradios

require (
	github.com/djherbis/times v1.2.0 // indirect
	github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026
	github.com/ilk/gobcache v1.0.0
	github.com/kahlys/flagenv v1.0.1
	github.com/pitr/gig v0.9.7

M go.sum => go.sum +2 -0
@@ 1,5 1,7 @@
github.com/djherbis/times v1.2.0 h1:xANXjsC/iBqbO00vkWlYwPWgBgEVU6m6AFYg0Pic+Mc=
github.com/djherbis/times v1.2.0/go.mod h1:CGMZlo255K5r4Yw0b9RRfFQpM2y7uOmxg4jm9HsaVf8=
github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 h1:BpJ2o0OR5FV7vrkDYfXYVJQeMNWa8RhklZOpW2ITAIQ=
github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE=
github.com/ilk/gobcache v1.0.0 h1:sDvSD0LUrGactxYmxHplmTwjRsK2sLChAfkXc1mFkok=
github.com/ilk/gobcache v1.0.0/go.mod h1:OTAPfJrmJkpe7utrYeWyeUGpc0P1fuATujX6Kh9NiB4=
github.com/kahlys/flagenv v1.0.1 h1:NdSMXQLtVVclrO2xnK/7t6Y2bCq9OBPMwOGP/UgNDoU=

M handlers/handlers.go => handlers/handlers.go +55 -22
@@ 25,6 25,7 @@ import (
	"time"

	"git.sr.ht/~louis77/gemini.tunerapp.org/proxy"
	"github.com/hako/durafmt"
	"github.com/pitr/gig"
	"gitlab.com/AgentNemo/goradios"
)


@@ 35,6 36,16 @@ func URLEscape(text string) string {
	return strings.ReplaceAll(template.URLQueryEscaper(strings.ToLower(strings.TrimSpace(text))), "+", "-")
}

// TimeFormat is a helper to format Pin time
func TimeFormat(t time.Time) string {
	return t.Format("2006-01-02 15:04:05")
}

func DurationFormat(t time.Time) string {
	duration := durafmt.ParseShort(time.Since(t))
	return duration.String()
}

type languagesData struct {
	Languages  []goradios.Language
	LastUpdate string


@@ 95,22 106,20 @@ type stationData struct {
func HandleStation(stations []goradios.Station) gig.HandlerFunc {
	return func(c gig.Context) error {
		requestedStation := strings.Replace(c.Param("id"), ".gmi", "", 1)
		for _, station := range stations {
			if station.StationUUID == requestedStation {
				// Download image
				_, err := proxy.CacheImage(station.Favicon)
				if err != nil {
					station.Favicon = ""
				}
		station, err := findStation(stations, requestedStation)
		if err != nil {
			return gig.ErrNotFound
		}

				return c.Render("station.tmpl", stationData{
					Station:    station,
					LastUpdate: time.Now().Format(time.RFC1123),
				})
			}
		_, err = proxy.CacheImage(station.Favicon)
		if err != nil {
			station.Favicon = ""
		}

		return gig.ErrNotFound
		return c.Render("station.tmpl", stationData{
			Station:    *station,
			LastUpdate: time.Now().Format(time.RFC1123),
		})
	}
}



@@ 186,6 195,7 @@ func HandleStations(stations []goradios.Station) gig.HandlerFunc {

type indexData struct {
	Top5Stations []goradios.Station
	Pins         []StationPin
	Count        int
	LastUpdate   string
}


@@ 195,6 205,7 @@ func HandleIndex(stations []goradios.Station, top5 []goradios.Station) gig.Handl
	return func(c gig.Context) error {
		return c.Render("index.tmpl", indexData{
			Top5Stations: top5,
			Pins:         pins,
			Count:        len(stations),
			LastUpdate:   time.Now().Format(time.RFC1123),
		})


@@ 205,16 216,38 @@ func HandleIndex(stations []goradios.Station, top5 []goradios.Station) gig.Handl
func HandleStationImage(stations []goradios.Station) gig.HandlerFunc {
	return func(c gig.Context) error {
		requestedStation := strings.Replace(c.Param("id"), ".png", "", 1)
		for _, station := range stations {
			if station.StationUUID == requestedStation {
				faviconFile, err := proxy.CacheImage(station.Favicon)
				if err != nil {
					return gig.ErrProxyError
				}
				return c.File(faviconFile)
			}
		station, err := findStation(stations, requestedStation)
		if err != nil {
			return gig.ErrNotFound
		}

		return gig.ErrNotFound
		faviconFile, err := proxy.CacheImage(station.Favicon)
		if err != nil {
			return gig.ErrProxyError
		}
		return c.File(faviconFile)
	}
}

// HandleStationPin handles the Pin station to home page request
func HandleStationPin(stations []goradios.Station) gig.HandlerFunc {
	return func(c gig.Context) error {
		query, err := c.QueryString()
		if err != nil {
			return err
		}
		log.Printf("Pin comment: %s\n", query)

		if len(query) == 0 {
			return c.NoContent(gig.StatusInput, "Enter comment (i.e. 'Love it!')")
		}

		station, err := findStation(stations, c.Param("id"))
		if err != nil {
			return gig.ErrNotFound
		}

		addStationPin(station, query)
		return c.NoContent(gig.StatusRedirectTemporary, "/")
	}
}

A handlers/model.go => handlers/model.go +89 -0
@@ 0,0 1,89 @@
package handlers

import (
	"fmt"
	"log"
	"time"

	"github.com/ilk/gobcache"
	"gitlab.com/AgentNemo/goradios"
)

var (
	pins         []StationPin
	pinCache     gobcache.Client
	pinCacheName string = "pins"
)

type StationPin struct {
	Station  goradios.Station
	Comments []Comment
}

type Comment struct {
	Comment string
	Created time.Time
}

func init() {
	pinCache = gobcache.NewCache(gobcache.Config{})
	var err error
	if err = pinCache.GetData(pinCacheName, &pins); err == nil {
		log.Println("Data loaded from local cache")
		return
	}

	log.Println("Pin Cache error: ", err)
	pins = []StationPin{}
}

func findStation(stations []goradios.Station, stationUUID string) (*goradios.Station, error) {
	for _, station := range stations {
		if station.StationUUID == stationUUID {
			return &station, nil
		}
	}
	return nil, fmt.Errorf("Not found")
}

func addStationPin(station *goradios.Station, comment string) {

	// Check if station is already pinned and append comment
	var found bool
	for i, pin := range pins {
		if pin.Station.StationUUID == station.StationUUID {
			pins[i].Comments = append([]Comment{
				{
					Comment: comment,
					Created: time.Now(),
				},
			}, pins[i].Comments...)
			found = true
		}
	}

	// Add a new pin
	currentLength := len(pins)
	if currentLength >= 5 {
		currentLength = 4
	}
	if !found {
		pins = append([]StationPin{
			{
				Station: *station,
				Comments: []Comment{
					{
						Comment: comment,
						Created: time.Now(),
					},
				},
			}}, pins[:currentLength]...)
	}

	if err := pinCache.SaveData(pinCacheName, pins); err != nil {
		log.Printf("error: %v", err)
		log.Println("Pin Cache not saved, will continue with in-memory data")
	} else {
		log.Println("Pin Cache saved")
	}
}

M main.go => main.go +5 -2
@@ 45,8 45,10 @@ var (
	refreshDataFlag = flagenv.Bool("refresh", "REFRESH", false, "Refresh cached radio-browser data")
	helpers         = template.FuncMap{
		// The name "title" is what the function will be called in the template text.
		"escape": handlers.URLEscape,
		"split":  strings.Split,
		"escape":         handlers.URLEscape,
		"split":          strings.Split,
		"timeformat":     handlers.TimeFormat,
		"durationformat": handlers.DurationFormat,
	}
)



@@ 116,6 118,7 @@ func main() {
	g.Static("/pages", "pages")

	g.Handle("/station-image/:id", handlers.HandleStationImage(data.Stations))
	g.Handle("/station-pin/:id", handlers.HandleStationPin(data.Stations))
	g.Handle("/", handlers.HandleIndex(data.Stations, data.Top5Stations))
	g.Handle("/all-stations.gmi", handlers.HandleStations(data.Stations))
	g.Handle("/countries.gmi", handlers.HandleCountries(data.Countries))

A pages/2021-02-12_Pin-station-to-home.gmi => pages/2021-02-12_Pin-station-to-home.gmi +17 -0
@@ 0,0 1,17 @@
# Pin stations to home

A little interactivity can't hurt - not even on Gemini which is very static by design. As of today, you can now pin stations to the home page and leave a comment. This way you can show your fellow users your favourite stations.

Just select "Pin to home" on the station detail page.

Please be friendly.

## New features

* ❤️ Pin to Home, max. 5 stations
* 🥳 Show last 5 checked stations

## Improvements

* New header
* New compact footer

M templates/footer.tmpl => templates/footer.tmpl +1 -0
@@ 1,5 1,6 @@
### News

=> /pages/2021-02-12_Pin-station-to-home.gmi [2021-02-12] ❤️ Pin stations to home
=> /pages/2021-02-11_New-release-v2.gmi [2021-02-11] A new release with search, inline images

### About

M templates/index.tmpl => templates/index.tmpl +4 -0
@@ 11,6 11,10 @@

{{ template "header.tmpl" }}

{{ with .Pins}}### Pinned stations from other users
{{ range . }}=> /stations/{{ escape .Station.StationUUID }}.gmi {{ .Station.Name }} {{ if .Station.Country }} 🌐 {{.Station.Country}}{{ end }}
{{ range .Comments }}🕑 {{ durationformat .Created }} ago 🗨️ {{ .Comment}}
{{ end }}{{ end }}{{ end }}
### Last 5 checked streams
{{ range .Top5Stations }}=> /stations/{{ escape .StationUUID }}.gmi {{ .Name }} {{ if .Country }} 🌐 {{.Country}}{{ end }}
{{ end }}

M templates/station.tmpl => templates/station.tmpl +2 -0
@@ 10,6 10,8 @@
{{ if .Favicon }}=> /station-image/{{ .StationUUID}}.png 🖼️ Station Image
{{ end }}=> {{ .URLResolved }} 🎵 Stream
{{ if .Homepage }}=> {{ .Homepage }} 🕸️ Website

=> /station-pin/{{ .StationUUID}} ❤️ Pin to Home
{{ end }}

{{ template "footer.tmpl" . }}
\ No newline at end of file