mirror of
https://github.com/glanceapp/glance.git
synced 2025-06-22 10:51:24 +02:00
Merge pull request #56 from jonasknobloch/lobsters-widget
Add Lobsters widget
This commit is contained in:
commit
8dc34ddfa7
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# https://docs.docker.com/build/building/context/#dockerignore-files
|
||||||
|
# Ignore all files by default
|
||||||
|
*
|
||||||
|
|
||||||
|
# Only add necessary files to the Docker build context (Dockerfiles are always included implicitly)
|
||||||
|
!/build/
|
||||||
|
!/internal/
|
||||||
|
!/go.mod
|
||||||
|
!/go.sum
|
||||||
|
!main.go
|
@ -1,7 +1,14 @@
|
|||||||
|
FROM golang:1.22.3-alpine3.19 AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . /app
|
||||||
|
RUN CGO_ENABLED=0 go build .
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.19
|
FROM alpine:3.19
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY build/glance /app/glance
|
COPY --from=builder /app/glance .
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
ENTRYPOINT ["/app/glance"]
|
ENTRYPOINT ["/app/glance"]
|
||||||
|
@ -94,12 +94,6 @@ go run .
|
|||||||
|
|
||||||
### Building Docker image
|
### Building Docker image
|
||||||
|
|
||||||
Build Glance with CGO disabled:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
CGO_ENABLED=0 go build -o build/glance .
|
|
||||||
```
|
|
||||||
|
|
||||||
Build the image:
|
Build the image:
|
||||||
|
|
||||||
**Make sure to replace "owner" with your name or organization.**
|
**Make sure to replace "owner" with your name or organization.**
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
- [RSS](#rss)
|
- [RSS](#rss)
|
||||||
- [Videos](#videos)
|
- [Videos](#videos)
|
||||||
- [Hacker News](#hacker-news)
|
- [Hacker News](#hacker-news)
|
||||||
|
- [Lobsters](#lobsters)
|
||||||
- [Reddit](#reddit)
|
- [Reddit](#reddit)
|
||||||
- [Search](#search-widget)
|
- [Search](#search-widget)
|
||||||
- [Weather](#weather)
|
- [Weather](#weather)
|
||||||
@ -533,6 +534,49 @@ Can be used to specify an additional sort which will be applied on top of the al
|
|||||||
|
|
||||||
The `engagement` sort tries to place the posts with the most points and comments on top, also prioritizing recent over old posts.
|
The `engagement` sort tries to place the posts with the most points and comments on top, also prioritizing recent over old posts.
|
||||||
|
|
||||||
|
### Lobsters
|
||||||
|
Display a list of posts from [Lobsters](https://lobste.rs).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- type: lobsters
|
||||||
|
sort-by: hot
|
||||||
|
tags:
|
||||||
|
- go
|
||||||
|
- security
|
||||||
|
- linux
|
||||||
|
limit: 15
|
||||||
|
collapse-after: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TODO: add preview
|
||||||
|
|
||||||
|
Preview:
|
||||||
|

|
||||||
|
-->
|
||||||
|
|
||||||
|
#### Properties
|
||||||
|
| Name | Type | Required | Default |
|
||||||
|
| ---- | ---- | -------- | ------- |
|
||||||
|
| limit | integer | no | 15 |
|
||||||
|
| collapse-after | integer | no | 5 |
|
||||||
|
| sort-by | string | no | hot |
|
||||||
|
| tags | array | no | |
|
||||||
|
|
||||||
|
##### `limit`
|
||||||
|
The maximum number of posts to show.
|
||||||
|
|
||||||
|
##### `collapse-after`
|
||||||
|
How many posts are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse.
|
||||||
|
|
||||||
|
##### `sort-by`
|
||||||
|
The sort order in which posts are returned. Possible options are `hot` and `new`.
|
||||||
|
|
||||||
|
##### `tags`
|
||||||
|
Limit to posts containing one of the given tags. **You cannot specify a sort order when filtering by tags, it will default to `hot`.**
|
||||||
|
|
||||||
### Reddit
|
### Reddit
|
||||||
Display a list of posts from a specific subreddit.
|
Display a list of posts from a specific subreddit.
|
||||||
|
|
||||||
@ -629,7 +673,7 @@ https://your.proxy/?url={REQUEST-URL}
|
|||||||
##### `sort-by`
|
##### `sort-by`
|
||||||
Can be used to specify the order in which the posts should get returned. Possible values are `hot`, `new`, `top` and `rising`.
|
Can be used to specify the order in which the posts should get returned. Possible values are `hot`, `new`, `top` and `rising`.
|
||||||
|
|
||||||
##### `top-perid`
|
##### `top-period`
|
||||||
Available only when `sort-by` is set to `top`. Possible values are `hour`, `day`, `week`, `month`, `year` and `all`.
|
Available only when `sort-by` is set to `top`. Possible values are `hour`, `day`, `week`, `month`, `year` and `all`.
|
||||||
|
|
||||||
##### `search`
|
##### `search`
|
||||||
|
81
internal/feed/lobsters.go
Normal file
81
internal/feed/lobsters.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package feed
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type lobstersPostResponseJson struct {
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
CommentCount int `json:"comment_count"`
|
||||||
|
CommentsURL string `json:"comments_url"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type lobstersFeedResponseJson []lobstersPostResponseJson
|
||||||
|
|
||||||
|
func getLobstersPostsFromFeed(feedUrl string) (ForumPosts, error) {
|
||||||
|
request, err := http.NewRequest("GET", feedUrl, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
feed, err := decodeJsonFromRequest[lobstersFeedResponseJson](defaultClient, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
posts := make(ForumPosts, 0, len(feed))
|
||||||
|
|
||||||
|
for i := range feed {
|
||||||
|
createdAt, _ := time.Parse(time.RFC3339, feed[i].CreatedAt)
|
||||||
|
|
||||||
|
posts = append(posts, ForumPost{
|
||||||
|
Title: feed[i].Title,
|
||||||
|
DiscussionUrl: feed[i].CommentsURL,
|
||||||
|
TargetUrl: feed[i].URL,
|
||||||
|
TargetUrlDomain: extractDomainFromUrl(feed[i].URL),
|
||||||
|
CommentCount: feed[i].CommentCount,
|
||||||
|
Score: feed[i].Score,
|
||||||
|
TimePosted: createdAt,
|
||||||
|
Tags: feed[i].Tags,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(posts) == 0 {
|
||||||
|
return nil, ErrNoContent
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FetchLobstersPosts(sortBy string, tags []string) (ForumPosts, error) {
|
||||||
|
var feedUrl string
|
||||||
|
|
||||||
|
if sortBy == "hot" {
|
||||||
|
sortBy = "hottest"
|
||||||
|
} else if sortBy == "new" {
|
||||||
|
sortBy = "newest"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tags) == 0 {
|
||||||
|
feedUrl = "https://lobste.rs/" + sortBy + ".json"
|
||||||
|
} else {
|
||||||
|
tags := strings.Join(tags, ",")
|
||||||
|
feedUrl = "https://lobste.rs/t/" + tags + ".json"
|
||||||
|
}
|
||||||
|
|
||||||
|
posts, err := getLobstersPostsFromFeed(feedUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return posts, nil
|
||||||
|
}
|
@ -16,6 +16,7 @@ type ForumPost struct {
|
|||||||
Score int
|
Score int
|
||||||
Engagement float64
|
Engagement float64
|
||||||
TimePosted time.Time
|
TimePosted time.Time
|
||||||
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForumPosts []ForumPost
|
type ForumPosts []ForumPost
|
||||||
|
56
internal/widget/lobsters.go
Normal file
56
internal/widget/lobsters.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package widget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"html/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/glanceapp/glance/internal/assets"
|
||||||
|
"github.com/glanceapp/glance/internal/feed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Lobsters struct {
|
||||||
|
widgetBase `yaml:",inline"`
|
||||||
|
Posts feed.ForumPosts `yaml:"-"`
|
||||||
|
Limit int `yaml:"limit"`
|
||||||
|
CollapseAfter int `yaml:"collapse-after"`
|
||||||
|
SortBy string `yaml:"sort-by"`
|
||||||
|
Tags []string `yaml:"tags"`
|
||||||
|
ShowThumbnails bool `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *Lobsters) Initialize() error {
|
||||||
|
widget.withTitle("Lobsters").withCacheDuration(30 * time.Minute)
|
||||||
|
|
||||||
|
if widget.SortBy == "" || (widget.SortBy != "hot" && widget.SortBy != "new") {
|
||||||
|
widget.SortBy = "hot"
|
||||||
|
}
|
||||||
|
|
||||||
|
if widget.Limit <= 0 {
|
||||||
|
widget.Limit = 15
|
||||||
|
}
|
||||||
|
|
||||||
|
if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
|
||||||
|
widget.CollapseAfter = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *Lobsters) Update(ctx context.Context) {
|
||||||
|
posts, err := feed.FetchLobstersPosts(widget.SortBy, widget.Tags)
|
||||||
|
|
||||||
|
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if widget.Limit < len(posts) {
|
||||||
|
posts = posts[:widget.Limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.Posts = posts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *Lobsters) Render() template.HTML {
|
||||||
|
return widget.render(widget, assets.ForumPostsTemplate)
|
||||||
|
}
|
@ -45,6 +45,8 @@ func New(widgetType string) (Widget, error) {
|
|||||||
return &TwitchGames{}, nil
|
return &TwitchGames{}, nil
|
||||||
case "twitch-channels":
|
case "twitch-channels":
|
||||||
return &TwitchChannels{}, nil
|
return &TwitchChannels{}, nil
|
||||||
|
case "lobsters":
|
||||||
|
return &Lobsters{}, nil
|
||||||
case "change-detection":
|
case "change-detection":
|
||||||
return &ChangeDetection{}, nil
|
return &ChangeDetection{}, nil
|
||||||
case "repository":
|
case "repository":
|
||||||
|
Loading…
x
Reference in New Issue
Block a user