feat: add basic support for sonarr today's releases feed

This commit is contained in:
iwa 2024-05-25 17:32:08 +02:00
parent f8816ff227
commit 0bca1b3041
No known key found for this signature in database
GPG key ID: FF8265BD92510C0B
5 changed files with 217 additions and 0 deletions

View file

@ -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{

View 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
View 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
}

View 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)
}

View file

@ -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)
}