This commit is contained in:
iwa 2024-05-25 18:11:36 +02:00 committed by GitHub
commit e81c0a1731
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 358 additions and 0 deletions

View file

@ -980,6 +980,23 @@ body {
background: linear-gradient(0deg, var(--color-widget-background) 10%, transparent);
}
.arr-release-cover {
border-radius: 0.4em;
max-width: 6em;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.arr-grabbed-label {
color: green; /* or any other color you prefer */
font-weight: bold;
padding: 2px 5px;
border: 1px solid green;
border-radius: 3px;
display: inline-block;
margin-top: 5px;
}
@media (max-width: 1190px) {
.header-container {
display: none;

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,31 @@
{{ 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="arr-release-cover thumbnail" src="{{ $release.ImageCoverUrl }}" alt="Cover for {{ $release.Title }}" loading="lazy">
{{ else }}
<svg class="arr-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 }} - S{{ $release.SeasonNumber }}E{{ $release.EpisodeNumber }}</strong>
<div>Airing on {{ $release.AirDateUtc }} (UTC)</div>
{{ if $release.Grabbed }}
<div class="arr-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 }}

250
internal/feed/arr-stack.go Normal file
View file

@ -0,0 +1,250 @@
package feed
import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
)
type SonarrConfig struct {
Enable bool `yaml:"enable"`
Endpoint string `yaml:"endpoint"`
ApiKey string `yaml:"apikey"`
}
type RadarrConfig struct {
Enable bool `yaml:"enable"`
Endpoint string `yaml:"endpoint"`
ApiKey string `yaml:"apikey"`
}
type ArrRelease struct {
Title string
ImageCoverUrl string
AirDateUtc string
SeasonNumber *string
EpisodeNumber *string
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"`
}
type RadarrReleaseResponse struct {
HasFile bool `json:"hasFile"`
Title string `json:"title"`
Images []struct {
CoverType string `json:"coverType"`
RemoteUrl string `json:"remoteUrl"`
} `json:"images"`
InCinemasDate string `json:"inCinemas"`
PhysicalReleaseDate string `json:"physicalRelease"`
DigitalReleaseDate string `json:"digitalRelease"`
}
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
}
}
airDate, err := time.Parse(time.RFC3339, release.AirDateUtc)
if err != nil {
return nil, fmt.Errorf("failed to parse air date: %v", err)
}
// Format the date as YYYY-MM-DD HH:MM:SS
formattedDate := airDate.Format("2006-01-02 15:04:05")
// Format SeasonNumber and EpisodeNumber with at least two digits
seasonNumber := fmt.Sprintf("%02d", release.SeasonNumber)
episodeNumber := fmt.Sprintf("%02d", release.EpisodeNumber)
releases = append(releases, ArrRelease{
Title: release.Series.Title,
ImageCoverUrl: imageCover,
AirDateUtc: formattedDate,
SeasonNumber: &seasonNumber,
EpisodeNumber: &episodeNumber,
Grabbed: release.HasFile,
})
}
return releases, nil
}
func FetchReleasesFromRadarr(RadarrEndpoint string, RadarrApiKey string) (ArrReleases, error) {
if RadarrEndpoint == "" {
return nil, fmt.Errorf("missing radarr-endpoint config")
}
if RadarrApiKey == "" {
return nil, fmt.Errorf("missing radarr-apikey config")
}
client := &http.Client{}
url := fmt.Sprintf("%s/api/v3/calendar", strings.TrimSuffix(RadarrEndpoint, "/"))
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", RadarrApiKey)
req.Header.Set("Host", extractHostFromURL(RadarrEndpoint))
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 radarrReleases []RadarrReleaseResponse
err = json.Unmarshal(body, &radarrReleases)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
}
var releases ArrReleases
for _, release := range radarrReleases {
var imageCover string
for _, image := range release.Images {
if image.CoverType == "poster" {
imageCover = image.RemoteUrl
break
}
}
// Choose the appropriate release date from Radarr's response
releaseDate := release.InCinemasDate
formattedDate := "In Cinemas: "
if release.PhysicalReleaseDate != "" {
releaseDate = release.PhysicalReleaseDate
formattedDate = "Physical Release: "
} else if release.DigitalReleaseDate != "" {
releaseDate = release.DigitalReleaseDate
formattedDate = "Digital Release: "
}
airDate, err := time.Parse("2006-01-02", releaseDate)
if err != nil {
return nil, fmt.Errorf("failed to parse release date: %v", err)
}
// Format the date as YYYY-MM-DD HH:MM:SS
formattedDate = formattedDate + airDate.Format("2006-01-02 15:04:05")
releases = append(releases, ArrRelease{
Title: release.Title,
ImageCoverUrl: imageCover,
AirDateUtc: formattedDate,
Grabbed: release.HasFile,
})
}
return releases, nil
}
func FetchReleasesFromArrStack(Sonarr SonarrConfig, Radarr RadarrConfig) (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 from sonarr", "error", err)
return nil, err
}
result = append(result, sonarrReleases...)
}
// Call FetchReleasesFromRadarr and handle the result
if Radarr.Enable {
radarrReleases, err := FetchReleasesFromRadarr(Radarr.Endpoint, Radarr.ApiKey)
if err != nil {
slog.Warn("failed to fetch release from radarr", "error", err)
return nil, err
}
result = append(result, radarrReleases...)
}
return result, nil
}

View file

@ -0,0 +1,57 @@
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"`
}
Radarr 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, widget.Radarr)
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)
}