feat: ✨ add basic support for sonarr today's releases feed
This commit is contained in:
parent
f8816ff227
commit
0bca1b3041
5 changed files with 217 additions and 0 deletions
|
@ -32,6 +32,7 @@ var (
|
|||
TwitchGamesListTemplate = compileTemplate("twitch-games-list.html", "widget-base.html")
|
||||
TwitchChannelsTemplate = compileTemplate("twitch-channels.html", "widget-base.html")
|
||||
RepositoryTemplate = compileTemplate("repository.html", "widget-base.html")
|
||||
ArrReleasesTemplate = compileTemplate("arr-stack-today-releases.html", "widget-base.html")
|
||||
)
|
||||
|
||||
var globalTemplateFunctions = template.FuncMap{
|
||||
|
|
33
internal/assets/templates/arr-stack-today-releases.html
Normal file
33
internal/assets/templates/arr-stack-today-releases.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<ul class="list list-gap-14 list-collapsible">
|
||||
{{ range $i, $release := .Releases }}
|
||||
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
|
||||
<div class="flex gap-10 items-start thumbnail-container">
|
||||
<div class="anime-release-cover-container">
|
||||
{{ if $release.ImageCoverUrl }}
|
||||
<img class="anime-release-cover thumbnail" src="{{ $release.ImageCoverUrl }}" alt="Cover for {{ $release.Title }}" loading="lazy">
|
||||
{{ else }}
|
||||
<svg class="anime-release-cover thumbnail" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="shrink min-width-0">
|
||||
<strong class="size-h3 block text-truncate">{{ $release.Title }}</strong>
|
||||
<div>Air Date: {{ $release.AirDateUtc }}</div>
|
||||
{{ if $release.SeasonNumber }}<div>Season: {{ $release.SeasonNumber }}</div>{{ end }}
|
||||
{{ if $release.EpisodeNumber }}<div>Episode: {{ $release.EpisodeNumber }}</div>{{ end }}
|
||||
{{ if $release.Grabbed }}
|
||||
<div class="grabbed-label">Grabbed</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ if gt (len .Releases) $.CollapseAfter }}
|
||||
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
|
||||
{{ end }}
|
||||
{{ end }}
|
129
internal/feed/arr-stack.go
Normal file
129
internal/feed/arr-stack.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package feed
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type SonarrConfig struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Endpoint string `yaml:"endpoint"`
|
||||
ApiKey string `yaml:"apikey"`
|
||||
}
|
||||
|
||||
type ArrRelease struct {
|
||||
Title string
|
||||
ImageCoverUrl string
|
||||
AirDateUtc string
|
||||
SeasonNumber *int
|
||||
EpisodeNumber *int
|
||||
Grabbed bool
|
||||
}
|
||||
|
||||
type ArrReleases []ArrRelease
|
||||
|
||||
type SonarrReleaseResponse struct {
|
||||
HasFile bool `json:"hasFile"`
|
||||
SeasonNumber int `json:"seasonNumber"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
Series struct {
|
||||
Title string `json:"title"`
|
||||
Images []struct {
|
||||
CoverType string `json:"coverType"`
|
||||
RemoteUrl string `json:"remoteUrl"`
|
||||
} `json:"images"`
|
||||
} `json:"series"`
|
||||
AirDateUtc string `json:"airDateUtc"`
|
||||
}
|
||||
|
||||
func extractHostFromURL(apiEndpoint string) string {
|
||||
u, err := url.Parse(apiEndpoint)
|
||||
if err != nil {
|
||||
return "127.0.0.1"
|
||||
}
|
||||
return u.Host
|
||||
}
|
||||
|
||||
func FetchReleasesFromSonarr(SonarrEndpoint string, SonarrApiKey string) (ArrReleases, error) {
|
||||
if SonarrEndpoint == "" {
|
||||
return nil, fmt.Errorf("missing sonarr-endpoint config")
|
||||
}
|
||||
|
||||
if SonarrApiKey == "" {
|
||||
return nil, fmt.Errorf("missing sonarr-apikey config")
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
url := fmt.Sprintf("%s/api/v3/calendar?includeSeries=true", strings.TrimSuffix(SonarrEndpoint, "/"))
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("X-Api-Key", SonarrApiKey)
|
||||
req.Header.Set("Host", extractHostFromURL(SonarrEndpoint))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %v", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
|
||||
var sonarrReleases []SonarrReleaseResponse
|
||||
err = json.Unmarshal(body, &sonarrReleases)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
|
||||
}
|
||||
|
||||
var releases ArrReleases
|
||||
for _, release := range sonarrReleases {
|
||||
var imageCover string
|
||||
for _, image := range release.Series.Images {
|
||||
if image.CoverType == "poster" {
|
||||
imageCover = image.RemoteUrl
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
releases = append(releases, ArrRelease{
|
||||
Title: release.Series.Title,
|
||||
ImageCoverUrl: imageCover,
|
||||
AirDateUtc: release.AirDateUtc,
|
||||
SeasonNumber: &release.SeasonNumber,
|
||||
EpisodeNumber: &release.EpisodeNumber,
|
||||
Grabbed: release.HasFile,
|
||||
})
|
||||
}
|
||||
|
||||
return releases, nil
|
||||
}
|
||||
|
||||
func FetchReleasesFromArrStack(Sonarr SonarrConfig) (ArrReleases, error) {
|
||||
result := ArrReleases{}
|
||||
|
||||
// Call FetchReleasesFromSonarr and handle the result
|
||||
if Sonarr.Enable {
|
||||
sonarrReleases, err := FetchReleasesFromSonarr(Sonarr.Endpoint, Sonarr.ApiKey)
|
||||
if err != nil {
|
||||
slog.Warn("failed to fetch release", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result = sonarrReleases
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
52
internal/widget/arr-stack-today-releases.go
Normal file
52
internal/widget/arr-stack-today-releases.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package widget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"github.com/glanceapp/glance/internal/assets"
|
||||
"github.com/glanceapp/glance/internal/feed"
|
||||
)
|
||||
|
||||
type ArrReleases struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Releases feed.ArrReleases `yaml:"-"`
|
||||
Sonarr struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
Endpoint string `yaml:"endpoint"`
|
||||
ApiKey string `yaml:"apikey"`
|
||||
}
|
||||
CollapseAfter int `yaml:"collapse-after"`
|
||||
CacheDuration time.Duration `yaml:"cache-duration"`
|
||||
}
|
||||
|
||||
func (widget *ArrReleases) Initialize() error {
|
||||
widget.withTitle("Releasing Today")
|
||||
|
||||
// Set cache duration
|
||||
if widget.CacheDuration == 0 {
|
||||
widget.CacheDuration = time.Minute * 5
|
||||
}
|
||||
widget.withCacheDuration(widget.CacheDuration)
|
||||
|
||||
// Set collapse after default value
|
||||
if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
|
||||
widget.CollapseAfter = 5
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (widget *ArrReleases) Update(ctx context.Context) {
|
||||
releases, err := feed.FetchReleasesFromArrStack(widget.Sonarr)
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
}
|
||||
|
||||
widget.Releases = releases
|
||||
}
|
||||
|
||||
func (widget *ArrReleases) Render() template.HTML {
|
||||
return widget.render(widget, assets.ArrReleasesTemplate)
|
||||
}
|
|
@ -45,6 +45,8 @@ func New(widgetType string) (Widget, error) {
|
|||
return &TwitchChannels{}, nil
|
||||
case "repository":
|
||||
return &Repository{}, nil
|
||||
case "arr-stack-releases":
|
||||
return &ArrReleases{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue