Просмотр исходного кода

Add BGG Hotness widget and associated doc entry

Taffaz 9 месяцев назад
Родитель
Сommit
15db5899f8

+ 24 - 0
docs/configuration.md

@@ -30,6 +30,7 @@
   - [Twitch Top Games](#twitch-top-games)
   - [iframe](#iframe)
   - [HTML](#html)
+  - [BGG Hotness](#bgghotness)
 
 ## Intro
 Configuration is done via a single YAML file and a server restart is required in order for any changes to take effect. Trying to start the server with an invalid config file will result in an error.
@@ -1665,3 +1666,26 @@ Example:
 ```
 
 Note the use of `|` after `source:`, this allows you to insert a multi-line string.
+
+### bgghotness
+Display the current games listed in the Board Game Geek Hotness list
+
+Example:
+
+```yaml
+- type: bgghotness
+  limit: 20
+  collapse-after-rows: 4
+```
+
+#### Properties
+| Name | Type | Required | Default |
+| ---- | ---- | -------- | ------- |
+| limit | integer | no | 20 |
+| collapse-after-rows| integer | no | 4 |
+
+##### `limit`
+The number of games to show. The BGG hotness has a maximum of 50 so any number greater than that will be clamped at 50
+
+##### `collapse-after-rows`
+How many rows of games are shown before the "SHOW MORE" button appears. Set to `-1` to never collapse.

+ 1 - 0
internal/assets/templates.go

@@ -39,6 +39,7 @@ var (
 	ExtensionTemplate             = compileTemplate("extension.html", "widget-base.html")
 	GroupTemplate                 = compileTemplate("group.html", "widget-base.html")
 	DNSStatsTemplate              = compileTemplate("dns-stats.html", "widget-base.html")
+    BGGHotnessTemplate            = compileTemplate("bgghotness.html", "widget-base.html")
 )
 
 var globalTemplateFunctions = template.FuncMap{

+ 17 - 0
internal/assets/templates/bgghotness.html

@@ -0,0 +1,17 @@
+{{ template "widget-base.html" . }}
+
+{{ define "widget-content" }}
+<div class="cards-grid collapsible-container" data-collapse-after-rows="{{ .CollapseAfterRows }}">
+    {{ range .Games }}
+    <div class="card widget-content-frame thumbnail-parent">
+        <img class="thumbnail" loading="lazy" src="{{ .ThumbnailUrl.PreFilter }}filters:strip_icc(){{ .ThumbnailUrl.PostFilter}}" alt="">
+        <div class="margin-top-10 margin-bottom-widget flex flex-column grow padding-inline-widget">
+            <a class="video-title color-primary-if-not-visited" href="{{ .BGGGameLink }}">{{ .Name }}</a>
+            <ul class="list-horizontal-text flex-nowrap margin-top-7">
+                <li class="min-width-0">Rank: {{ .Rank }}</li>
+            </ul>
+        </div>
+    </div>
+    {{ end }}
+</div>
+{{ end }}

+ 71 - 0
internal/feed/bgghotness.go

@@ -0,0 +1,71 @@
+package feed
+
+import (
+    "encoding/xml"
+    "io"
+    "net/http"
+    "strings"
+)
+
+type BGGFeedResponseXML struct {
+    XMLName xml.Name    `xml:"items"`
+    Items   []struct {
+        XMLName xml.Name    `xml:"item"`
+        ID string   `xml:"id,attr"`
+        Thumbnail   struct {
+            Value   string  `xml:"value,attr"`
+        }   `xml:"thumbnail"`
+        Name   struct {
+            Value   string  `xml:"value,attr"`
+        }   `xml:"name"`
+        YearPublished   struct {
+            Value   string  `xml:"value,attr"`
+        }   `xml:"yearpublished"`
+        Rank    string  `xml:"rank,attr"`
+    }   `xml:"item"`
+}
+
+func FetchBGGHotnessList() (BggBoardGames, error){
+    resp, err := http.Get("https://boardgamegeek.com/xmlapi2/hot?boardgame")
+    if err != nil {
+        return BggBoardGames{}, err
+    }
+
+    defer resp.Body.Close()
+
+    data, err := io.ReadAll(resp.Body)
+    if err != nil {
+        return BggBoardGames{}, err
+    }
+
+    var hotnessFeed BGGFeedResponseXML
+    err = xml.Unmarshal(data, &hotnessFeed)
+    if err != nil {
+        return BggBoardGames{}, err
+    }
+
+    bggGames := getItemsFromBGGFeedTask(hotnessFeed)
+    
+    return bggGames, nil
+}
+
+func getItemsFromBGGFeedTask(response BGGFeedResponseXML) (BggBoardGames) {
+    games := make(BggBoardGames, 0, len(response.Items))
+
+    for _, item := range response.Items {
+        splitUrl :=  strings.Split(item.Thumbnail.Value, "filters:strip_icc()")
+        thumbUrl := ThumbnailUrl { splitUrl[0], splitUrl[1]}
+        bggBoardGame := BggBoardGame {
+            ID:             item.ID,
+            ThumbnailUrl:   thumbUrl, 
+            Name:           item.Name.Value,
+            YearPublished:  item.YearPublished.Value,
+            Rank:           item.Rank,
+            BGGGameLink:    "https://boardgamegeek.com/boardgame/" + item.ID,
+        }
+
+        games = append(games, bggBoardGame)
+    }
+
+    return games
+}

+ 16 - 0
internal/feed/primitives.go

@@ -63,6 +63,22 @@ type Video struct {
 
 type Videos []Video
 
+type BggBoardGame struct {
+    ID              string
+    ThumbnailUrl    BggThumbnailUrl
+    Name            string
+    YearPublished   string
+    Rank            string
+    BGGGameLink     string
+}
+
+type BggBoardGames []BggBoardGame
+
+type BggThumbnailUrl struct {
+    PreFilter       string
+    PostFilter      string
+}
+
 var currencyToSymbol = map[string]string{
 	"USD": "$",
 	"EUR": "€",

+ 49 - 0
internal/widget/bgghotness.go

@@ -0,0 +1,49 @@
+package widget
+
+import (
+	"context"
+	"html/template"
+	"time"
+
+	"github.com/glanceapp/glance/internal/assets"
+	"github.com/glanceapp/glance/internal/feed"
+)
+
+type BGGHotness struct {
+    widgetBase  `yaml:",inline"`
+    Games       feed.BggBoardGames  `yaml:"`
+    CollapseAfterRows int         `yaml:"collapse-after-rows"`
+	Limit             int         `yaml:"limit"`
+}
+
+func (widget *BGGHotness) Initialize() error {
+    widget.withTitle("BGG Hotness").withCacheDuration(time.Hour)
+
+    if widget.Limit <= 0 {
+        widget.Limit = 20
+    }
+
+    if widget.CollapseAfterRows == 0 || widget.CollapseAfterRows < -1 {
+        widget.CollapseAfterRows = 4
+    }
+    return nil
+}
+
+func (widget *BGGHotness) Update(ctx context.Context) {
+    games, err := feed.FetchBGGHotnessList()
+
+    if !widget.canContinueUpdateAfterHandlingErr(err) {
+        return
+    }
+
+    if len(games) > widget.Limit {
+        games = games[:widget.Limit]
+    }
+
+    widget.Games = games
+}
+
+func (widget *BGGHotness) Render() template.HTML {
+    
+    return widget.render(widget, assets.BGGHotnessTemplate) 
+}

+ 3 - 1
internal/widget/widget.go

@@ -63,10 +63,12 @@ func New(widgetType string) (Widget, error) {
 		widget = &Search{}
 	case "extension":
 		widget = &Extension{}
-	case "group":
+    case "group":
 		widget = &Group{}
 	case "dns-stats":
 		widget = &DNSStats{}
+    case "bgghotness":
+        widget = &BGGHotness{}
 	default:
 		return nil, fmt.Errorf("unknown widget type: %s", widgetType)
 	}