commit ea8fff629c67b1b47d0ffec2f97ce461dada0b61
Author: Daniel Dada
Date: Tue Feb 24 16:34:39 2026 +0300
Initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a9440a8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/data
+/config/media
\ No newline at end of file
diff --git a/config/monitoring/glance/glance.yml b/config/monitoring/glance/glance.yml
new file mode 100644
index 0000000..3d022b1
--- /dev/null
+++ b/config/monitoring/glance/glance.yml
@@ -0,0 +1,97 @@
+# Glance – main config
+# See: https://github.com/glanceapp/glance/blob/main/docs/configuration.md
+
+server:
+ assets-path: /app/assets
+ proxied: true
+
+theme:
+ custom-css-file: /assets/user.css
+
+pages:
+ - name: Home
+ columns:
+ - size: small
+ widgets:
+ - type: clock
+ - type: calendar
+ first-day-of-week: monday
+ - type: releases
+ cache: 1d
+ repositories:
+ - glanceapp/glance
+ - immich-app/immich
+ - size: full
+ widgets:
+ - type: hacker-news
+ limit: 15
+ collapse-after: 5
+ - type: lobsters
+ sort-by: hot
+ limit: 15
+ collapse-after: 5
+ - type: reddit
+ subreddit: unixporn
+ limit: 15
+ collapse-after: 5
+ - size: small
+ widgets:
+ - type: bookmarks
+ title: Services
+ groups:
+ - same-tab: true
+ links:
+ - title: Jellyfin
+ url: https://jellyfin.home
+ icon: mdi:television-play
+ - title: Seer
+ url: https://jellyseerr.home
+ icon: mdi:movie-search
+ - title: Radarr
+ url: https://radarr.home
+ icon: mdi:film
+ - title: Sonarr
+ url: https://sonarr.home
+ icon: mdi:television
+ - title: Lidarr
+ url: https://lidarr.home
+ icon: mdi:music
+ - title: Prowlarr
+ url: https://prowlarr.home
+ icon: mdi:radar
+ - title: qBittorrent
+ url: https://qbit.home
+ icon: mdi:download
+ - title: Portainer
+ url: https://portainer.home
+ icon: mdi:docker
+ - title: Immich
+ url: https://photo.home
+ icon: mdi:image-multiple
+ - title: Navidrome
+ url: https://music.home
+ icon: mdi:music-box-multiple
+ - title: CopyParty
+ url: https://copyparty.home
+ icon: mdi:folder-multiple
+ - title: Gitea
+ url: https://git.home
+ icon: mdi:git
+ - title: Grafana
+ url: https://grafana.home
+ icon: mdi:chart-line
+ - name: Media
+ columns:
+ - size: full
+ widgets:
+ - type: group
+ widgets:
+ - $include: widgets/arr-radarr-upcoming.yml
+ - $include: widgets/arr-radarr-recent.yml
+ - $include: widgets/arr-radarr-missing.yml
+ - $include: widgets/arr-sonarr-upcoming.yml
+ - $include: widgets/arr-sonarr-recent.yml
+ - $include: widgets/arr-sonarr-missing.yml
+ - $include: widgets/monitor-media.yml
+ - $include: pages/services.yml
+ - $include: pages/gaming.yml
diff --git a/config/monitoring/glance/pages/gaming.yml b/config/monitoring/glance/pages/gaming.yml
new file mode 100644
index 0000000..5163a47
--- /dev/null
+++ b/config/monitoring/glance/pages/gaming.yml
@@ -0,0 +1,39 @@
+- name: Gaming
+ columns:
+ - size: small
+ widgets:
+ - type: twitch-top-games
+ limit: 20
+ collapse-after: 13
+ exclude:
+ - just-chatting
+ - pools-hot-tubs-and-beaches
+ - music
+ - art
+ - asmr
+
+ - size: full
+ widgets:
+ - type: group
+ widgets:
+ - type: reddit
+ show-thumbnails: true
+ subreddit: pcgaming
+ - type: reddit
+ subreddit: games
+
+ - type: videos
+ style: grid-cards
+ collapse-after-rows: 3
+ channels:
+ - UCNvzD7Z-g64bPXxGzaQaa4g # gameranx
+ - UCZ7AeeVbyslLM_8-nVy2B8Q # Skill Up
+ - UCHDxYLv8iovIbhrfl16CNyg # GameLinked
+ - UC9PBzalIcEQCsiIkq36PyUA # Digital Foundry
+
+ - size: small
+ widgets:
+ - type: reddit
+ subreddit: gamingnews
+ limit: 7
+ style: vertical-cards
\ No newline at end of file
diff --git a/config/monitoring/glance/pages/home.yml b/config/monitoring/glance/pages/home.yml
new file mode 100644
index 0000000..98db1d9
--- /dev/null
+++ b/config/monitoring/glance/pages/home.yml
@@ -0,0 +1,16 @@
+# Home: clock, calendar, quick links to all services
+name: Home
+columns:
+ - size: small
+ widgets:
+ - type: clock
+ - type: calendar
+ first-day-of-week: monday
+ - $include: ../widgets/bookmarks-services.yml
+ - size: full
+ widgets:
+ - type: releases
+ cache: 1d
+ repositories:
+ - glanceapp/glance
+ - immich-app/immich
diff --git a/config/monitoring/glance/pages/media.yml b/config/monitoring/glance/pages/media.yml
new file mode 100644
index 0000000..a09dcbc
--- /dev/null
+++ b/config/monitoring/glance/pages/media.yml
@@ -0,0 +1,15 @@
+# Media: Jellyfin (next up + latest), *arr wanted, status
+name: Media
+columns:
+ - size: full
+ widgets:
+ - type: group
+ widgets:
+ - $include: ../widgets/arr-radarr-upcoming.yml
+ - $include: ../widgets/arr-radarr-recent.yml
+ - $include: ../widgets/arr-radarr-missing.yml
+ - $include: ../widgets/arr-sonarr-upcoming.yml
+ - $include: ../widgets/arr-sonarr-recent.yml
+ - $include: ../widgets/arr-sonarr-missing.yml
+ - $include: ../widgets/jellyfin.yml
+ - $include: ../widgets/monitor-media.yml
diff --git a/config/monitoring/glance/pages/services.yml b/config/monitoring/glance/pages/services.yml
new file mode 100644
index 0000000..efad147
--- /dev/null
+++ b/config/monitoring/glance/pages/services.yml
@@ -0,0 +1,83 @@
+# Services: status monitor, Docker containers, Portainer
+- name: Services
+ columns:
+ - size: full
+ widgets:
+ - type: server-stats
+ title: Server
+ servers:
+ - type: local
+ name: Services
+ mountpoints:
+ "/":
+ name: Root
+ "/srv/media":
+ name: Media
+ "/backup":
+ name: Backup
+ - type: monitor
+ title: Service status
+ sites:
+ - title: Jellyfin
+ url: https://jellyfin.home
+ icon: mdi:television-play
+ - title: Seer
+ url: https://jellyseerr.home
+ icon: mdi:movie-search
+ - title: Radarr
+ url: https://radarr.home
+ icon: mdi:film
+ - title: Sonarr
+ url: https://sonarr.home
+ icon: mdi:television
+ - title: Lidarr
+ url: https://lidarr.home
+ icon: mdi:music
+ - title: Prowlarr
+ url: https://prowlarr.home
+ icon: mdi:radar
+ - title: qBittorrent
+ url: https://qbit.home
+ icon: mdi:download
+ - title: Portainer
+ url: https://portainer.home
+ icon: mdi:docker
+ - title: Immich
+ url: https://photo.home
+ icon: mdi:image-multiple
+ - title: Navidrome
+ url: https://music.home
+ icon: mdi:music-box-multiple
+ - title: CopyParty
+ url: https://copyparty.home
+ icon: mdi:folder-multiple
+ - title: Gitea
+ url: https://git.home
+ icon: mdi:git
+ - title: Grafana
+ url: https://grafana.home
+ icon: mdi:chart-line
+ - type: docker-containers
+ title: Docker
+ show-stats: true
+ - type: custom-api
+ title: Portainer
+ title-url: https://portainer.home
+ cache: 1h
+ url: ${PORTAINER_URL}/api/endpoints/${PORTAINER_ENDPOINT_ID}
+ headers:
+ X-API-Key: ${PORTAINER_API_KEY}
+ template: |
+
+
+ {{ .JSON.String "Name" }}
+ Containers: {{ .JSON.Int "Snapshots.0.ContainerCount" }}
+ Volumes: {{ .JSON.Int "Snapshots.0.VolumeCount" }}
+ Networks: {{ .JSON.Int "Snapshots.0.DockerSnapshotRaw.Networks.#" }}
+
+
+ - Running {{ .JSON.Int "Snapshots.0.RunningContainerCount" }}
+ - Healthy {{ .JSON.Int "Snapshots.0.HealthyContainerCount" }}
+ - Stopped {{ .JSON.Int "Snapshots.0.StoppedContainerCount" }}
+ - Unhealthy {{ .JSON.Int "Snapshots.0.DockerSnapshotRaw.Containers.#(Health=\"unhealthy\").#" }}
+
diff --git a/config/monitoring/glance/widgets/arr-radarr-missing.yml b/config/monitoring/glance/widgets/arr-radarr-missing.yml
new file mode 100644
index 0000000..5695d70
--- /dev/null
+++ b/config/monitoring/glance/widgets/arr-radarr-missing.yml
@@ -0,0 +1,450 @@
+# *arr community widget: Radarr wanted movies
+- type: custom-api
+ title: Wanted Movies
+ title-url: ${RADARR_URL}
+ cache: 30m
+ options:
+ service: radarr
+ type: missing
+ size: medium
+ collapse-after: 5
+ show-grabbed: false
+ interval: 20
+ api-base-url: ${RADARR_API_URL}
+ key: ${RADARR_KEY}
+ url: ${RADARR_URL}
+ template: |
+ {{ $collapseAfter := .Options.IntOr "collapse-after" 5 }}
+ {{ $showGrabbed := .Options.BoolOr "show-grabbed" false }}
+ {{ $sortTime := .Options.StringOr "sort-time" "desc" }}
+ {{ $coverProxy := .Options.StringOr "cover-proxy" "" }}
+ {{ $size := .Options.StringOr "size" "medium" }}
+ {{ $service := .Options.StringOr "service" "" }}
+ {{ $type := .Options.StringOr "type" "" }}
+ {{ $intervalH := .Options.IntOr "interval" 0 | mul 24 }}
+
+ {{ $now := now | formatTime "2006-01-02T15:04:05" }}
+ {{ $posInterval := (offsetNow (printf "+%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+ {{ $negInterval := (offsetNow (printf "-%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+
+ {{ $apiBaseUrl := .Options.StringOr "api-base-url" "" }}
+ {{ $key := .Options.StringOr "key" "" }}
+ {{ $url := .Options.StringOr "url" $apiBaseUrl }}
+
+ {{ if or (and (ne $service "sonarr") (ne $service "radarr") (ne $service "lidarr"))
+ (and (ne $type "upcoming") (ne $type "recent") (ne $type "missing"))
+ (eq $apiBaseUrl "") (eq $key "") (eq $url "") }}
+
+
+ Some options are not set or malformed
+
+ | service | {{ $service }} | must be sonarr, radarr, or lidarr |
+ | type | {{ $type }} | must be upcoming, recent, or missing |
+ | api-base-url | {{ $apiBaseUrl }} | should include http(s):// and port if needed |
+ | key | {{ $key }} | |
+
+
+ {{ else }}
+ {{ $requestUrl := "" }}
+
+ {{ if eq $service "sonarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeSeries=true&includeEpisode=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50&includeSeries=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?includeSeries=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "radarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeMovie=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "lidarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v1/history/since?includeArtist=true&includeAlbum=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v1/wanted/missing?page=1&pageSize=50&includeArtist=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v1/calendar?includeArtist=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+ {{ end }}
+
+ {{ $data := newRequest $requestUrl
+ | withHeader "Accept" "application/json"
+ | withHeader "X-Api-Key" $key
+ | getResponse }}
+
+
+ {{ $array := "" }}
+ {{ $sortByField := "" }}
+ {{ $itemDisplayed := false }}
+ {{ $itemDate := "" }}
+ {{ $isAvailable := false }}
+
+ {{ if eq $type "missing" }}
+ {{ $array = "records" }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ else if eq $type "recent" }}
+ {{ $sortByField = "date" }}
+ {{ else }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ end }}
+
+ {{ range $data.JSON.Array $array | sortByTime $sortByField "rfc3339" $sortTime }}
+
+ {{ if eq $service "sonarr" }}
+ {{ $itemDate = .String "airDateUtc" | parseTime "RFC3339" }}
+ {{ $itemDate = $itemDate.In now.Location | formatTime "2006-01-02T15:04:05" }}
+ {{ $isAvailable = true }}
+ {{ else if eq $service "radarr"}}
+ {{ $isAvailable = .Bool "isAvailable" }}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ else if eq $service "lidarr"}}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ $isAvailable = true }}
+ {{ end }}
+
+ {{ if or (eq $type "upcoming") (eq $type "recent") (and (or (and (gt $itemDate $negInterval) ((lt $itemDate $now ))) (eq $intervalH 0)) $isAvailable) }}
+ {{ $itemDisplayed = true }}
+ {{ $title := "" }}
+ {{ $subtitle := "" }}
+ {{ $coverUrl := "" }}
+ {{ $status := "" }}
+ {{ $coverBase := "" }}
+ {{ $height := "" }}
+ {{ $width := "" }}
+ {{ $popoverTitle := "" }}
+ {{ $popoverSubtitle := "" }}
+ {{ $popoverSummary := "" }}
+ {{ $summary := "" }}
+ {{ $link := "" }}
+ {{ $grabbed := false }}
+ {{ $date := now }}
+ {{ $datetype := "" }}
+ {{ $seString := "" }}
+ {{ $genres := "" }}
+ {{ $buttonJustify := "left" }}
+
+
+ {{ if eq $service "sonarr" }}
+ {{ $series := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "seriesId") $key }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "seriesId") }}
+ {{ end }}
+ {{ $title = .String "series.title" }}
+ {{ $link = printf "%s/series/%s#" $url (.String "series.titleSlug") }}
+ {{ $series = .Get "series" }}
+ {{ $genres = $series.Get "genres" }}
+
+ {{ if eq $type "recent" }}
+ {{ $date = .String "date" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "episode.title" }}
+ {{ $summary = .String "episode.overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "episode.seasonNumber") (.Int "episode.episodeNumber") }}
+ {{ $datetype = "Downloaded" }}
+
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "episode.title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "episode.overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+
+ {{ else if eq $type "missing" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Aired" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ else if eq $type "upcoming" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Airs" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "radarr" }}
+ {{ $movie := "" }}
+ {{ $status = .String "status" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+ {{ if eq $status "announced"}}
+ {{ if ne (.String "inCinemas") "" }}
+ {{ $date = (.String "inCinemas" | parseTime "RFC3339") }}
+ {{ $datetype = "In Cinemas" }}
+ {{ else }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ else if eq $status "inCinemas"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ else if eq $status "released"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Released" }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "movie.id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "movie.id") }}
+ {{ end }}
+ {{ $datetype = "Downloaded" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $title = .String "movie.title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "movie.overview" }}
+ {{ $popoverTitle = .String "movie.title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "movie.overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "movie.titleSlug") }}
+ {{ $movie = .Get "movie" }}
+ {{ $genres = $movie.Get "genres" }}
+ {{ if and $showGrabbed (gt (.Int "movie.movieFileId") 0) }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "id") }}
+ {{ end }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "titleSlug") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ $genres = .Get "genres" }}
+ {{ if eq $type "missing" }}
+ {{ if and $showGrabbed (.Bool "movie.hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if and $showGrabbed (.Bool "hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "lidarr" }}
+ {{ $artist := "" }}
+ {{ $album := "" }}
+ {{ $albumId := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v1/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ $album = .Get "album" }}
+ {{ $artist = $album.Get "artist" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase (.String "albumId") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase (.String "albumId") }}
+ {{ end }}
+ {{ $grabbed = true }}
+ {{ $title = $album.String "title" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $datetype = "Downloaded" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $summary = $album.String "overview" }}
+ {{ $popoverTitle = $album.String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = $artist.String "overview" }}
+
+ {{ else }}
+ {{ $artist = .Get "artist" }}
+ {{ $album = .Get "album" }}
+ {{ if eq $type "missing" }}
+ {{ $datetype = "Released" }}
+ {{ else }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ range .Array "releases" }}
+ {{ $albumId = .String "albumId" }}
+ {{ break }}
+ {{ end }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase $albumId $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase $albumId }}
+ {{ end }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $summary = $album.String "overview" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "artist.overview" }}
+ {{ end }}
+ {{ end }}
+
+ {{ if eq $size "small" }}
+ {{ $buttonJustify = "right" }}
+ {{ $height = "9rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "9rem" }}
+ {{ else }}
+ {{ $width = "6rem" }}
+ {{ end }}
+ {{ else if eq $size "medium" }}
+ {{ $height = "12rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "12rem" }}
+ {{ else }}
+ {{ $width = "8rem" }}
+ {{ end }}
+ {{ else if eq $size "large" }}
+ {{ $height = "15rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "15rem" }}
+ {{ else }}
+ {{ $width = "10rem" }}
+ {{ end }}
+ {{ else if eq $size "huge" }}
+ {{ $height = "18rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "18rem" }}
+ {{ else }}
+ {{ $width = "12rem" }}
+ {{ end }}
+ {{ end }}
+
+ -
+
+
+
+
+
+
{{ $popoverTitle }}
+
{{ $popoverSubtitle }}
+
+ {{ if ne $popoverSummary "" }}
+ {{ $popoverSummary }}
+ {{ else }}
+ TBA
+ {{ end }}
+
+ {{ if gt (len ($genres.Array "")) 0 }}
+
+ {{ range $genres.Array "" }}
+ - {{ .String "" }}
+ {{ end }}
+
+ {{ end }}
+
+
+

+
+
+
+
{{ $title }}
+
{{ $subtitle }}
+
+ {{ if eq $service "sonarr" }}
+
{{ $seString }} - {{ $datetype }} {{ $date.Format "1/2 03:04PM" }}
+ {{ else }}
+
{{ $datetype }} {{ $date.Format "1/2" }}
+ {{ end }}
+
+
+ {{ if $showGrabbed }}
+ {{ if eq $buttonJustify "right" }}
+
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+ {{ else }}
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+
+ {{ end }}
+ {{ else }}
+
+ {{ $summary }}
+
+ {{ end }}
+
+
+ {{ end }}
+ {{ end }}
+ {{ if not $itemDisplayed }}
+ - No items found.
+ {{ end }}
+
+ {{ end }}
+
diff --git a/config/monitoring/glance/widgets/arr-radarr-recent.yml b/config/monitoring/glance/widgets/arr-radarr-recent.yml
new file mode 100644
index 0000000..f7d4a8e
--- /dev/null
+++ b/config/monitoring/glance/widgets/arr-radarr-recent.yml
@@ -0,0 +1,449 @@
+# *arr community widget: Radarr recent movies
+- type: custom-api
+ title: Recent Movies
+ title-url: ${RADARR_URL}
+ cache: 30m
+ options:
+ service: radarr
+ type: recent
+ size: medium
+ collapse-after: 5
+ show-grabbed: false
+ interval: 20
+ api-base-url: ${RADARR_API_URL}
+ key: ${RADARR_KEY}
+ url: ${RADARR_URL}
+ template: |
+ {{ $collapseAfter := .Options.IntOr "collapse-after" 5 }}
+ {{ $showGrabbed := .Options.BoolOr "show-grabbed" false }}
+ {{ $sortTime := .Options.StringOr "sort-time" "desc" }}
+ {{ $coverProxy := .Options.StringOr "cover-proxy" "" }}
+ {{ $size := .Options.StringOr "size" "medium" }}
+ {{ $service := .Options.StringOr "service" "" }}
+ {{ $type := .Options.StringOr "type" "" }}
+ {{ $intervalH := .Options.IntOr "interval" 0 | mul 24 }}
+
+ {{ $now := now | formatTime "2006-01-02T15:04:05" }}
+ {{ $posInterval := (offsetNow (printf "+%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+ {{ $negInterval := (offsetNow (printf "-%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+
+ {{ $apiBaseUrl := .Options.StringOr "api-base-url" "" }}
+ {{ $key := .Options.StringOr "key" "" }}
+ {{ $url := .Options.StringOr "url" $apiBaseUrl }}
+
+ {{ if or (and (ne $service "sonarr") (ne $service "radarr") (ne $service "lidarr"))
+ (and (ne $type "upcoming") (ne $type "recent") (ne $type "missing"))
+ (eq $apiBaseUrl "") (eq $key "") (eq $url "") }}
+
+
+ Some options are not set or malformed
+
+ | service | {{ $service }} | must be sonarr, radarr, or lidarr |
+ | type | {{ $type }} | must be upcoming, recent, or missing |
+ | api-base-url | {{ $apiBaseUrl }} | should include http(s):// and port if needed |
+ | key | {{ $key }} | |
+
+
+ {{ else }}
+ {{ $requestUrl := "" }}
+
+ {{ if eq $service "sonarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeSeries=true&includeEpisode=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50&includeSeries=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?includeSeries=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "radarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeMovie=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "lidarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v1/history/since?includeArtist=true&includeAlbum=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v1/wanted/missing?page=1&pageSize=50&includeArtist=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v1/calendar?includeArtist=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+ {{ end }}
+
+ {{ $data := newRequest $requestUrl
+ | withHeader "Accept" "application/json"
+ | withHeader "X-Api-Key" $key
+ | getResponse }}
+
+
+ {{ $array := "" }}
+ {{ $sortByField := "" }}
+ {{ $itemDisplayed := false }}
+ {{ $itemDate := "" }}
+ {{ $isAvailable := false }}
+
+ {{ if eq $type "missing" }}
+ {{ $array = "records" }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ else if eq $type "recent" }}
+ {{ $sortByField = "date" }}
+ {{ else }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ end }}
+
+ {{ range $data.JSON.Array $array | sortByTime $sortByField "rfc3339" $sortTime }}
+
+ {{ if eq $service "sonarr" }}
+ {{ $itemDate = .String "airDateUtc" | parseTime "RFC3339" }}
+ {{ $itemDate = $itemDate.In now.Location | formatTime "2006-01-02T15:04:05" }}
+ {{ $isAvailable = true }}
+ {{ else if eq $service "radarr"}}
+ {{ $isAvailable = .Bool "isAvailable" }}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ else if eq $service "lidarr"}}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ $isAvailable = true }}
+ {{ end }}
+
+ {{ if or (eq $type "upcoming") (eq $type "recent") (and (or (and (gt $itemDate $negInterval) ((lt $itemDate $now ))) (eq $intervalH 0)) $isAvailable) }}
+ {{ $itemDisplayed = true }}
+ {{ $title := "" }}
+ {{ $subtitle := "" }}
+ {{ $coverUrl := "" }}
+ {{ $status := "" }}
+ {{ $coverBase := "" }}
+ {{ $height := "" }}
+ {{ $width := "" }}
+ {{ $popoverTitle := "" }}
+ {{ $popoverSubtitle := "" }}
+ {{ $popoverSummary := "" }}
+ {{ $summary := "" }}
+ {{ $link := "" }}
+ {{ $grabbed := false }}
+ {{ $date := now }}
+ {{ $datetype := "" }}
+ {{ $seString := "" }}
+ {{ $genres := "" }}
+ {{ $buttonJustify := "left" }}
+
+
+ {{ if eq $service "sonarr" }}
+ {{ $series := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "seriesId") $key }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "seriesId") }}
+ {{ end }}
+ {{ $title = .String "series.title" }}
+ {{ $link = printf "%s/series/%s#" $url (.String "series.titleSlug") }}
+ {{ $series = .Get "series" }}
+ {{ $genres = $series.Get "genres" }}
+
+ {{ if eq $type "recent" }}
+ {{ $date = .String "date" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "episode.title" }}
+ {{ $summary = .String "episode.overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "episode.seasonNumber") (.Int "episode.episodeNumber") }}
+ {{ $datetype = "Downloaded" }}
+
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "episode.title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "episode.overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+
+ {{ else if eq $type "missing" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Aired" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ else if eq $type "upcoming" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Airs" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "radarr" }}
+ {{ $movie := "" }}
+ {{ $status = .String "status" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+ {{ if eq $status "announced"}}
+ {{ if ne (.String "inCinemas") "" }}
+ {{ $date = (.String "inCinemas" | parseTime "RFC3339") }}
+ {{ $datetype = "In Cinemas" }}
+ {{ else }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ else if eq $status "inCinemas"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ else if eq $status "released"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Released" }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "movie.id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "movie.id") }}
+ {{ end }}
+ {{ $datetype = "Downloaded" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $title = .String "movie.title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "movie.overview" }}
+ {{ $popoverTitle = .String "movie.title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "movie.overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "movie.titleSlug") }}
+ {{ $movie = .Get "movie" }}
+ {{ $genres = $movie.Get "genres" }}
+ {{ if and $showGrabbed (gt (.Int "movie.movieFileId") 0) }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "id") }}
+ {{ end }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "titleSlug") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ $genres = .Get "genres" }}
+ {{ if eq $type "missing" }}
+ {{ if and $showGrabbed (.Bool "movie.hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if and $showGrabbed (.Bool "hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "lidarr" }}
+ {{ $artist := "" }}
+ {{ $album := "" }}
+ {{ $albumId := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v1/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ $album = .Get "album" }}
+ {{ $artist = $album.Get "artist" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase (.String "albumId") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase (.String "albumId") }}
+ {{ end }}
+ {{ $grabbed = true }}
+ {{ $title = $album.String "title" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $datetype = "Downloaded" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $summary = $album.String "overview" }}
+ {{ $popoverTitle = $album.String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = $artist.String "overview" }}
+
+ {{ else }}
+ {{ $artist = .Get "artist" }}
+ {{ $album = .Get "album" }}
+ {{ if eq $type "missing" }}
+ {{ $datetype = "Released" }}
+ {{ else }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ range .Array "releases" }}
+ {{ $albumId = .String "albumId" }}
+ {{ break }}
+ {{ end }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase $albumId $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase $albumId }}
+ {{ end }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $summary = $album.String "overview" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "artist.overview" }}
+ {{ end }}
+ {{ end }}
+
+ {{ if eq $size "small" }}
+ {{ $buttonJustify = "right" }}
+ {{ $height = "9rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "9rem" }}
+ {{ else }}
+ {{ $width = "6rem" }}
+ {{ end }}
+ {{ else if eq $size "medium" }}
+ {{ $height = "12rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "12rem" }}
+ {{ else }}
+ {{ $width = "8rem" }}
+ {{ end }}
+ {{ else if eq $size "large" }}
+ {{ $height = "15rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "15rem" }}
+ {{ else }}
+ {{ $width = "10rem" }}
+ {{ end }}
+ {{ else if eq $size "huge" }}
+ {{ $height = "18rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "18rem" }}
+ {{ else }}
+ {{ $width = "12rem" }}
+ {{ end }}
+ {{ end }}
+
+ -
+
+
+
+
+
+
{{ $popoverTitle }}
+
{{ $popoverSubtitle }}
+
+ {{ if ne $popoverSummary "" }}
+ {{ $popoverSummary }}
+ {{ else }}
+ TBA
+ {{ end }}
+
+ {{ if gt (len ($genres.Array "")) 0 }}
+
+ {{ range $genres.Array "" }}
+ - {{ .String "" }}
+ {{ end }}
+
+ {{ end }}
+
+
+

+
+
+
+
{{ $title }}
+
{{ $subtitle }}
+
+ {{ if eq $service "sonarr" }}
+
{{ $seString }} - {{ $datetype }} {{ $date.Format "1/2 03:04PM" }}
+ {{ else }}
+
{{ $datetype }} {{ $date.Format "1/2" }}
+ {{ end }}
+
+
+ {{ if $showGrabbed }}
+ {{ if eq $buttonJustify "right" }}
+
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+ {{ else }}
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+
+ {{ end }}
+ {{ else }}
+
+ {{ $summary }}
+
+ {{ end }}
+
+
+ {{ end }}
+ {{ end }}
+ {{ if not $itemDisplayed }}
+ - No items found.
+ {{ end }}
+
+ {{ end }}
diff --git a/config/monitoring/glance/widgets/arr-radarr-upcoming.yml b/config/monitoring/glance/widgets/arr-radarr-upcoming.yml
new file mode 100644
index 0000000..b45f318
--- /dev/null
+++ b/config/monitoring/glance/widgets/arr-radarr-upcoming.yml
@@ -0,0 +1,449 @@
+# *arr community widget: Radarr upcoming movies
+- type: custom-api
+ title: Upcoming Movies
+ title-url: ${RADARR_URL}
+ cache: 30m
+ options:
+ service: radarr
+ type: upcoming
+ size: medium
+ collapse-after: 5
+ show-grabbed: false
+ interval: 20
+ api-base-url: ${RADARR_API_URL}
+ key: ${RADARR_KEY}
+ url: ${RADARR_URL}
+ template: |
+ {{ $collapseAfter := .Options.IntOr "collapse-after" 5 }}
+ {{ $showGrabbed := .Options.BoolOr "show-grabbed" false }}
+ {{ $sortTime := .Options.StringOr "sort-time" "desc" }}
+ {{ $coverProxy := .Options.StringOr "cover-proxy" "" }}
+ {{ $size := .Options.StringOr "size" "medium" }}
+ {{ $service := .Options.StringOr "service" "" }}
+ {{ $type := .Options.StringOr "type" "" }}
+ {{ $intervalH := .Options.IntOr "interval" 0 | mul 24 }}
+
+ {{ $now := now | formatTime "2006-01-02T15:04:05" }}
+ {{ $posInterval := (offsetNow (printf "+%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+ {{ $negInterval := (offsetNow (printf "-%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+
+ {{ $apiBaseUrl := .Options.StringOr "api-base-url" "" }}
+ {{ $key := .Options.StringOr "key" "" }}
+ {{ $url := .Options.StringOr "url" $apiBaseUrl }}
+
+ {{ if or (and (ne $service "sonarr") (ne $service "radarr") (ne $service "lidarr"))
+ (and (ne $type "upcoming") (ne $type "recent") (ne $type "missing"))
+ (eq $apiBaseUrl "") (eq $key "") (eq $url "") }}
+
+
+ Some options are not set or malformed
+
+ | service | {{ $service }} | must be sonarr, radarr, or lidarr |
+ | type | {{ $type }} | must be upcoming, recent, or missing |
+ | api-base-url | {{ $apiBaseUrl }} | should include http(s):// and port if needed |
+ | key | {{ $key }} | |
+
+
+ {{ else }}
+ {{ $requestUrl := "" }}
+
+ {{ if eq $service "sonarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeSeries=true&includeEpisode=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50&includeSeries=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?includeSeries=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "radarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeMovie=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "lidarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v1/history/since?includeArtist=true&includeAlbum=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v1/wanted/missing?page=1&pageSize=50&includeArtist=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v1/calendar?includeArtist=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+ {{ end }}
+
+ {{ $data := newRequest $requestUrl
+ | withHeader "Accept" "application/json"
+ | withHeader "X-Api-Key" $key
+ | getResponse }}
+
+
+ {{ $array := "" }}
+ {{ $sortByField := "" }}
+ {{ $itemDisplayed := false }}
+ {{ $itemDate := "" }}
+ {{ $isAvailable := false }}
+
+ {{ if eq $type "missing" }}
+ {{ $array = "records" }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ else if eq $type "recent" }}
+ {{ $sortByField = "date" }}
+ {{ else }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ end }}
+
+ {{ range $data.JSON.Array $array | sortByTime $sortByField "rfc3339" $sortTime }}
+
+ {{ if eq $service "sonarr" }}
+ {{ $itemDate = .String "airDateUtc" | parseTime "RFC3339" }}
+ {{ $itemDate = $itemDate.In now.Location | formatTime "2006-01-02T15:04:05" }}
+ {{ $isAvailable = true }}
+ {{ else if eq $service "radarr"}}
+ {{ $isAvailable = .Bool "isAvailable" }}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ else if eq $service "lidarr"}}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ $isAvailable = true }}
+ {{ end }}
+
+ {{ if or (eq $type "upcoming") (eq $type "recent") (and (or (and (gt $itemDate $negInterval) ((lt $itemDate $now ))) (eq $intervalH 0)) $isAvailable) }}
+ {{ $itemDisplayed = true }}
+ {{ $title := "" }}
+ {{ $subtitle := "" }}
+ {{ $coverUrl := "" }}
+ {{ $status := "" }}
+ {{ $coverBase := "" }}
+ {{ $height := "" }}
+ {{ $width := "" }}
+ {{ $popoverTitle := "" }}
+ {{ $popoverSubtitle := "" }}
+ {{ $popoverSummary := "" }}
+ {{ $summary := "" }}
+ {{ $link := "" }}
+ {{ $grabbed := false }}
+ {{ $date := now }}
+ {{ $datetype := "" }}
+ {{ $seString := "" }}
+ {{ $genres := "" }}
+ {{ $buttonJustify := "left" }}
+
+
+ {{ if eq $service "sonarr" }}
+ {{ $series := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "seriesId") $key }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "seriesId") }}
+ {{ end }}
+ {{ $title = .String "series.title" }}
+ {{ $link = printf "%s/series/%s#" $url (.String "series.titleSlug") }}
+ {{ $series = .Get "series" }}
+ {{ $genres = $series.Get "genres" }}
+
+ {{ if eq $type "recent" }}
+ {{ $date = .String "date" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "episode.title" }}
+ {{ $summary = .String "episode.overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "episode.seasonNumber") (.Int "episode.episodeNumber") }}
+ {{ $datetype = "Downloaded" }}
+
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "episode.title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "episode.overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+
+ {{ else if eq $type "missing" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Aired" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ else if eq $type "upcoming" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Airs" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "radarr" }}
+ {{ $movie := "" }}
+ {{ $status = .String "status" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+ {{ if eq $status "announced"}}
+ {{ if ne (.String "inCinemas") "" }}
+ {{ $date = (.String "inCinemas" | parseTime "RFC3339") }}
+ {{ $datetype = "In Cinemas" }}
+ {{ else }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ else if eq $status "inCinemas"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ else if eq $status "released"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Released" }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "movie.id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "movie.id") }}
+ {{ end }}
+ {{ $datetype = "Downloaded" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $title = .String "movie.title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "movie.overview" }}
+ {{ $popoverTitle = .String "movie.title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "movie.overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "movie.titleSlug") }}
+ {{ $movie = .Get "movie" }}
+ {{ $genres = $movie.Get "genres" }}
+ {{ if and $showGrabbed (gt (.Int "movie.movieFileId") 0) }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "id") }}
+ {{ end }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "titleSlug") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ $genres = .Get "genres" }}
+ {{ if eq $type "missing" }}
+ {{ if and $showGrabbed (.Bool "movie.hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if and $showGrabbed (.Bool "hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "lidarr" }}
+ {{ $artist := "" }}
+ {{ $album := "" }}
+ {{ $albumId := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v1/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ $album = .Get "album" }}
+ {{ $artist = $album.Get "artist" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase (.String "albumId") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase (.String "albumId") }}
+ {{ end }}
+ {{ $grabbed = true }}
+ {{ $title = $album.String "title" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $datetype = "Downloaded" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $summary = $album.String "overview" }}
+ {{ $popoverTitle = $album.String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = $artist.String "overview" }}
+
+ {{ else }}
+ {{ $artist = .Get "artist" }}
+ {{ $album = .Get "album" }}
+ {{ if eq $type "missing" }}
+ {{ $datetype = "Released" }}
+ {{ else }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ range .Array "releases" }}
+ {{ $albumId = .String "albumId" }}
+ {{ break }}
+ {{ end }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase $albumId $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase $albumId }}
+ {{ end }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $summary = $album.String "overview" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "artist.overview" }}
+ {{ end }}
+ {{ end }}
+
+ {{ if eq $size "small" }}
+ {{ $buttonJustify = "right" }}
+ {{ $height = "9rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "9rem" }}
+ {{ else }}
+ {{ $width = "6rem" }}
+ {{ end }}
+ {{ else if eq $size "medium" }}
+ {{ $height = "12rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "12rem" }}
+ {{ else }}
+ {{ $width = "8rem" }}
+ {{ end }}
+ {{ else if eq $size "large" }}
+ {{ $height = "15rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "15rem" }}
+ {{ else }}
+ {{ $width = "10rem" }}
+ {{ end }}
+ {{ else if eq $size "huge" }}
+ {{ $height = "18rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "18rem" }}
+ {{ else }}
+ {{ $width = "12rem" }}
+ {{ end }}
+ {{ end }}
+
+ -
+
+
+
+
+
+
{{ $popoverTitle }}
+
{{ $popoverSubtitle }}
+
+ {{ if ne $popoverSummary "" }}
+ {{ $popoverSummary }}
+ {{ else }}
+ TBA
+ {{ end }}
+
+ {{ if gt (len ($genres.Array "")) 0 }}
+
+ {{ range $genres.Array "" }}
+ - {{ .String "" }}
+ {{ end }}
+
+ {{ end }}
+
+
+

+
+
+
+
{{ $title }}
+
{{ $subtitle }}
+
+ {{ if eq $service "sonarr" }}
+
{{ $seString }} - {{ $datetype }} {{ $date.Format "1/2 03:04PM" }}
+ {{ else }}
+
{{ $datetype }} {{ $date.Format "1/2" }}
+ {{ end }}
+
+
+ {{ if $showGrabbed }}
+ {{ if eq $buttonJustify "right" }}
+
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+ {{ else }}
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+
+ {{ end }}
+ {{ else }}
+
+ {{ $summary }}
+
+ {{ end }}
+
+
+ {{ end }}
+ {{ end }}
+ {{ if not $itemDisplayed }}
+ - No items found.
+ {{ end }}
+
+ {{ end }}
diff --git a/config/monitoring/glance/widgets/arr-sonarr-missing.yml b/config/monitoring/glance/widgets/arr-sonarr-missing.yml
new file mode 100644
index 0000000..188ef75
--- /dev/null
+++ b/config/monitoring/glance/widgets/arr-sonarr-missing.yml
@@ -0,0 +1,450 @@
+# *arr community widget: Sonarr wanted episodes
+- type: custom-api
+ title: Wanted Episodes
+ title-url: ${SONARR_URL}
+ cache: 30m
+ options:
+ service: sonarr
+ type: missing
+ size: medium
+ collapse-after: 5
+ show-grabbed: false
+ interval: 20
+ api-base-url: ${SONARR_API_URL}
+ key: ${SONARR_KEY}
+ url: ${SONARR_URL}
+ template: |
+ {{ $collapseAfter := .Options.IntOr "collapse-after" 5 }}
+ {{ $showGrabbed := .Options.BoolOr "show-grabbed" false }}
+ {{ $sortTime := .Options.StringOr "sort-time" "desc" }}
+ {{ $coverProxy := .Options.StringOr "cover-proxy" "" }}
+ {{ $size := .Options.StringOr "size" "medium" }}
+ {{ $service := .Options.StringOr "service" "" }}
+ {{ $type := .Options.StringOr "type" "" }}
+ {{ $intervalH := .Options.IntOr "interval" 0 | mul 24 }}
+
+ {{ $now := now | formatTime "2006-01-02T15:04:05" }}
+ {{ $posInterval := (offsetNow (printf "+%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+ {{ $negInterval := (offsetNow (printf "-%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+
+ {{ $apiBaseUrl := .Options.StringOr "api-base-url" "" }}
+ {{ $key := .Options.StringOr "key" "" }}
+ {{ $url := .Options.StringOr "url" $apiBaseUrl }}
+
+ {{ if or (and (ne $service "sonarr") (ne $service "radarr") (ne $service "lidarr"))
+ (and (ne $type "upcoming") (ne $type "recent") (ne $type "missing"))
+ (eq $apiBaseUrl "") (eq $key "") (eq $url "") }}
+
+
+ Some options are not set or malformed
+
+ | service | {{ $service }} | must be sonarr, radarr, or lidarr |
+ | type | {{ $type }} | must be upcoming, recent, or missing |
+ | api-base-url | {{ $apiBaseUrl }} | should include http(s):// and port if needed |
+ | key | {{ $key }} | |
+
+
+ {{ else }}
+ {{ $requestUrl := "" }}
+
+ {{ if eq $service "sonarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeSeries=true&includeEpisode=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50&includeSeries=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?includeSeries=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "radarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeMovie=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "lidarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v1/history/since?includeArtist=true&includeAlbum=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v1/wanted/missing?page=1&pageSize=50&includeArtist=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v1/calendar?includeArtist=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+ {{ end }}
+
+ {{ $data := newRequest $requestUrl
+ | withHeader "Accept" "application/json"
+ | withHeader "X-Api-Key" $key
+ | getResponse }}
+
+
+ {{ $array := "" }}
+ {{ $sortByField := "" }}
+ {{ $itemDisplayed := false }}
+ {{ $itemDate := "" }}
+ {{ $isAvailable := false }}
+
+ {{ if eq $type "missing" }}
+ {{ $array = "records" }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ else if eq $type "recent" }}
+ {{ $sortByField = "date" }}
+ {{ else }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ end }}
+
+ {{ range $data.JSON.Array $array | sortByTime $sortByField "rfc3339" $sortTime }}
+
+ {{ if eq $service "sonarr" }}
+ {{ $itemDate = .String "airDateUtc" | parseTime "RFC3339" }}
+ {{ $itemDate = $itemDate.In now.Location | formatTime "2006-01-02T15:04:05" }}
+ {{ $isAvailable = true }}
+ {{ else if eq $service "radarr"}}
+ {{ $isAvailable = .Bool "isAvailable" }}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ else if eq $service "lidarr"}}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ $isAvailable = true }}
+ {{ end }}
+
+ {{ if or (eq $type "upcoming") (eq $type "recent") (and (or (and (gt $itemDate $negInterval) ((lt $itemDate $now ))) (eq $intervalH 0)) $isAvailable) }}
+ {{ $itemDisplayed = true }}
+ {{ $title := "" }}
+ {{ $subtitle := "" }}
+ {{ $coverUrl := "" }}
+ {{ $status := "" }}
+ {{ $coverBase := "" }}
+ {{ $height := "" }}
+ {{ $width := "" }}
+ {{ $popoverTitle := "" }}
+ {{ $popoverSubtitle := "" }}
+ {{ $popoverSummary := "" }}
+ {{ $summary := "" }}
+ {{ $link := "" }}
+ {{ $grabbed := false }}
+ {{ $date := now }}
+ {{ $datetype := "" }}
+ {{ $seString := "" }}
+ {{ $genres := "" }}
+ {{ $buttonJustify := "left" }}
+
+
+ {{ if eq $service "sonarr" }}
+ {{ $series := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "seriesId") $key }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "seriesId") }}
+ {{ end }}
+ {{ $title = .String "series.title" }}
+ {{ $link = printf "%s/series/%s#" $url (.String "series.titleSlug") }}
+ {{ $series = .Get "series" }}
+ {{ $genres = $series.Get "genres" }}
+
+ {{ if eq $type "recent" }}
+ {{ $date = .String "date" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "episode.title" }}
+ {{ $summary = .String "episode.overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "episode.seasonNumber") (.Int "episode.episodeNumber") }}
+ {{ $datetype = "Downloaded" }}
+
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "episode.title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "episode.overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+
+ {{ else if eq $type "missing" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Aired" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ else if eq $type "upcoming" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Airs" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "radarr" }}
+ {{ $movie := "" }}
+ {{ $status = .String "status" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+ {{ if eq $status "announced"}}
+ {{ if ne (.String "inCinemas") "" }}
+ {{ $date = (.String "inCinemas" | parseTime "RFC3339") }}
+ {{ $datetype = "In Cinemas" }}
+ {{ else }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ else if eq $status "inCinemas"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ else if eq $status "released"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Released" }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "movie.id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "movie.id") }}
+ {{ end }}
+ {{ $datetype = "Downloaded" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $title = .String "movie.title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "movie.overview" }}
+ {{ $popoverTitle = .String "movie.title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "movie.overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "movie.titleSlug") }}
+ {{ $movie = .Get "movie" }}
+ {{ $genres = $movie.Get "genres" }}
+ {{ if and $showGrabbed (gt (.Int "movie.movieFileId") 0) }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "id") }}
+ {{ end }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "titleSlug") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ $genres = .Get "genres" }}
+ {{ if eq $type "missing" }}
+ {{ if and $showGrabbed (.Bool "movie.hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if and $showGrabbed (.Bool "hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "lidarr" }}
+ {{ $artist := "" }}
+ {{ $album := "" }}
+ {{ $albumId := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v1/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ $album = .Get "album" }}
+ {{ $artist = $album.Get "artist" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase (.String "albumId") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase (.String "albumId") }}
+ {{ end }}
+ {{ $grabbed = true }}
+ {{ $title = $album.String "title" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $datetype = "Downloaded" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $summary = $album.String "overview" }}
+ {{ $popoverTitle = $album.String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = $artist.String "overview" }}
+
+ {{ else }}
+ {{ $artist = .Get "artist" }}
+ {{ $album = .Get "album" }}
+ {{ if eq $type "missing" }}
+ {{ $datetype = "Released" }}
+ {{ else }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ range .Array "releases" }}
+ {{ $albumId = .String "albumId" }}
+ {{ break }}
+ {{ end }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase $albumId $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase $albumId }}
+ {{ end }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $summary = $album.String "overview" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "artist.overview" }}
+ {{ end }}
+ {{ end }}
+
+ {{ if eq $size "small" }}
+ {{ $buttonJustify = "right" }}
+ {{ $height = "9rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "9rem" }}
+ {{ else }}
+ {{ $width = "6rem" }}
+ {{ end }}
+ {{ else if eq $size "medium" }}
+ {{ $height = "12rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "12rem" }}
+ {{ else }}
+ {{ $width = "8rem" }}
+ {{ end }}
+ {{ else if eq $size "large" }}
+ {{ $height = "15rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "15rem" }}
+ {{ else }}
+ {{ $width = "10rem" }}
+ {{ end }}
+ {{ else if eq $size "huge" }}
+ {{ $height = "18rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "18rem" }}
+ {{ else }}
+ {{ $width = "12rem" }}
+ {{ end }}
+ {{ end }}
+
+ -
+
+
+
+
+
+
{{ $popoverTitle }}
+
{{ $popoverSubtitle }}
+
+ {{ if ne $popoverSummary "" }}
+ {{ $popoverSummary }}
+ {{ else }}
+ TBA
+ {{ end }}
+
+ {{ if gt (len ($genres.Array "")) 0 }}
+
+ {{ range $genres.Array "" }}
+ - {{ .String "" }}
+ {{ end }}
+
+ {{ end }}
+
+
+

+
+
+
+
{{ $title }}
+
{{ $subtitle }}
+
+ {{ if eq $service "sonarr" }}
+
{{ $seString }} - {{ $datetype }} {{ $date.Format "1/2 03:04PM" }}
+ {{ else }}
+
{{ $datetype }} {{ $date.Format "1/2" }}
+ {{ end }}
+
+
+ {{ if $showGrabbed }}
+ {{ if eq $buttonJustify "right" }}
+
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+ {{ else }}
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+
+ {{ end }}
+ {{ else }}
+
+ {{ $summary }}
+
+ {{ end }}
+
+
+ {{ end }}
+ {{ end }}
+ {{ if not $itemDisplayed }}
+ - No items found.
+ {{ end }}
+
+ {{ end }}
+
diff --git a/config/monitoring/glance/widgets/arr-sonarr-recent.yml b/config/monitoring/glance/widgets/arr-sonarr-recent.yml
new file mode 100644
index 0000000..e866c99
--- /dev/null
+++ b/config/monitoring/glance/widgets/arr-sonarr-recent.yml
@@ -0,0 +1,450 @@
+# *arr community widget: Sonarr recent episodes
+- type: custom-api
+ title: Recent Episodes
+ title-url: ${SONARR_URL}
+ cache: 30m
+ options:
+ service: sonarr
+ type: recent
+ size: medium
+ collapse-after: 5
+ show-grabbed: false
+ interval: 20
+ api-base-url: ${SONARR_API_URL}
+ key: ${SONARR_KEY}
+ url: ${SONARR_URL}
+ template: |
+ {{ $collapseAfter := .Options.IntOr "collapse-after" 5 }}
+ {{ $showGrabbed := .Options.BoolOr "show-grabbed" false }}
+ {{ $sortTime := .Options.StringOr "sort-time" "desc" }}
+ {{ $coverProxy := .Options.StringOr "cover-proxy" "" }}
+ {{ $size := .Options.StringOr "size" "medium" }}
+ {{ $service := .Options.StringOr "service" "" }}
+ {{ $type := .Options.StringOr "type" "" }}
+ {{ $intervalH := .Options.IntOr "interval" 0 | mul 24 }}
+
+ {{ $now := now | formatTime "2006-01-02T15:04:05" }}
+ {{ $posInterval := (offsetNow (printf "+%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+ {{ $negInterval := (offsetNow (printf "-%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+
+ {{ $apiBaseUrl := .Options.StringOr "api-base-url" "" }}
+ {{ $key := .Options.StringOr "key" "" }}
+ {{ $url := .Options.StringOr "url" $apiBaseUrl }}
+
+ {{ if or (and (ne $service "sonarr") (ne $service "radarr") (ne $service "lidarr"))
+ (and (ne $type "upcoming") (ne $type "recent") (ne $type "missing"))
+ (eq $apiBaseUrl "") (eq $key "") (eq $url "") }}
+
+
+ Some options are not set or malformed
+
+ | service | {{ $service }} | must be sonarr, radarr, or lidarr |
+ | type | {{ $type }} | must be upcoming, recent, or missing |
+ | api-base-url | {{ $apiBaseUrl }} | should include http(s):// and port if needed |
+ | key | {{ $key }} | |
+
+
+ {{ else }}
+ {{ $requestUrl := "" }}
+
+ {{ if eq $service "sonarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeSeries=true&includeEpisode=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50&includeSeries=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?includeSeries=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "radarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeMovie=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "lidarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v1/history/since?includeArtist=true&includeAlbum=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v1/wanted/missing?page=1&pageSize=50&includeArtist=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v1/calendar?includeArtist=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+ {{ end }}
+
+ {{ $data := newRequest $requestUrl
+ | withHeader "Accept" "application/json"
+ | withHeader "X-Api-Key" $key
+ | getResponse }}
+
+
+ {{ $array := "" }}
+ {{ $sortByField := "" }}
+ {{ $itemDisplayed := false }}
+ {{ $itemDate := "" }}
+ {{ $isAvailable := false }}
+
+ {{ if eq $type "missing" }}
+ {{ $array = "records" }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ else if eq $type "recent" }}
+ {{ $sortByField = "date" }}
+ {{ else }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ end }}
+
+ {{ range $data.JSON.Array $array | sortByTime $sortByField "rfc3339" $sortTime }}
+
+ {{ if eq $service "sonarr" }}
+ {{ $itemDate = .String "airDateUtc" | parseTime "RFC3339" }}
+ {{ $itemDate = $itemDate.In now.Location | formatTime "2006-01-02T15:04:05" }}
+ {{ $isAvailable = true }}
+ {{ else if eq $service "radarr"}}
+ {{ $isAvailable = .Bool "isAvailable" }}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ else if eq $service "lidarr"}}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ $isAvailable = true }}
+ {{ end }}
+
+ {{ if or (eq $type "upcoming") (eq $type "recent") (and (or (and (gt $itemDate $negInterval) ((lt $itemDate $now ))) (eq $intervalH 0)) $isAvailable) }}
+ {{ $itemDisplayed = true }}
+ {{ $title := "" }}
+ {{ $subtitle := "" }}
+ {{ $coverUrl := "" }}
+ {{ $status := "" }}
+ {{ $coverBase := "" }}
+ {{ $height := "" }}
+ {{ $width := "" }}
+ {{ $popoverTitle := "" }}
+ {{ $popoverSubtitle := "" }}
+ {{ $popoverSummary := "" }}
+ {{ $summary := "" }}
+ {{ $link := "" }}
+ {{ $grabbed := false }}
+ {{ $date := now }}
+ {{ $datetype := "" }}
+ {{ $seString := "" }}
+ {{ $genres := "" }}
+ {{ $buttonJustify := "left" }}
+
+
+ {{ if eq $service "sonarr" }}
+ {{ $series := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "seriesId") $key }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "seriesId") }}
+ {{ end }}
+ {{ $title = .String "series.title" }}
+ {{ $link = printf "%s/series/%s#" $url (.String "series.titleSlug") }}
+ {{ $series = .Get "series" }}
+ {{ $genres = $series.Get "genres" }}
+
+ {{ if eq $type "recent" }}
+ {{ $date = .String "date" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "episode.title" }}
+ {{ $summary = .String "episode.overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "episode.seasonNumber") (.Int "episode.episodeNumber") }}
+ {{ $datetype = "Downloaded" }}
+
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "episode.title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "episode.overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+
+ {{ else if eq $type "missing" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Aired" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ else if eq $type "upcoming" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Airs" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "radarr" }}
+ {{ $movie := "" }}
+ {{ $status = .String "status" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+ {{ if eq $status "announced"}}
+ {{ if ne (.String "inCinemas") "" }}
+ {{ $date = (.String "inCinemas" | parseTime "RFC3339") }}
+ {{ $datetype = "In Cinemas" }}
+ {{ else }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ else if eq $status "inCinemas"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ else if eq $status "released"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Released" }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "movie.id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "movie.id") }}
+ {{ end }}
+ {{ $datetype = "Downloaded" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $title = .String "movie.title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "movie.overview" }}
+ {{ $popoverTitle = .String "movie.title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "movie.overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "movie.titleSlug") }}
+ {{ $movie = .Get "movie" }}
+ {{ $genres = $movie.Get "genres" }}
+ {{ if and $showGrabbed (gt (.Int "movie.movieFileId") 0) }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "id") }}
+ {{ end }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "titleSlug") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ $genres = .Get "genres" }}
+ {{ if eq $type "missing" }}
+ {{ if and $showGrabbed (.Bool "movie.hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if and $showGrabbed (.Bool "hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "lidarr" }}
+ {{ $artist := "" }}
+ {{ $album := "" }}
+ {{ $albumId := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v1/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ $album = .Get "album" }}
+ {{ $artist = $album.Get "artist" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase (.String "albumId") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase (.String "albumId") }}
+ {{ end }}
+ {{ $grabbed = true }}
+ {{ $title = $album.String "title" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $datetype = "Downloaded" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $summary = $album.String "overview" }}
+ {{ $popoverTitle = $album.String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = $artist.String "overview" }}
+
+ {{ else }}
+ {{ $artist = .Get "artist" }}
+ {{ $album = .Get "album" }}
+ {{ if eq $type "missing" }}
+ {{ $datetype = "Released" }}
+ {{ else }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ range .Array "releases" }}
+ {{ $albumId = .String "albumId" }}
+ {{ break }}
+ {{ end }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase $albumId $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase $albumId }}
+ {{ end }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $summary = $album.String "overview" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "artist.overview" }}
+ {{ end }}
+ {{ end }}
+
+ {{ if eq $size "small" }}
+ {{ $buttonJustify = "right" }}
+ {{ $height = "9rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "9rem" }}
+ {{ else }}
+ {{ $width = "6rem" }}
+ {{ end }}
+ {{ else if eq $size "medium" }}
+ {{ $height = "12rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "12rem" }}
+ {{ else }}
+ {{ $width = "8rem" }}
+ {{ end }}
+ {{ else if eq $size "large" }}
+ {{ $height = "15rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "15rem" }}
+ {{ else }}
+ {{ $width = "10rem" }}
+ {{ end }}
+ {{ else if eq $size "huge" }}
+ {{ $height = "18rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "18rem" }}
+ {{ else }}
+ {{ $width = "12rem" }}
+ {{ end }}
+ {{ end }}
+
+ -
+
+
+
+
+
+
{{ $popoverTitle }}
+
{{ $popoverSubtitle }}
+
+ {{ if ne $popoverSummary "" }}
+ {{ $popoverSummary }}
+ {{ else }}
+ TBA
+ {{ end }}
+
+ {{ if gt (len ($genres.Array "")) 0 }}
+
+ {{ range $genres.Array "" }}
+ - {{ .String "" }}
+ {{ end }}
+
+ {{ end }}
+
+
+

+
+
+
+
{{ $title }}
+
{{ $subtitle }}
+
+ {{ if eq $service "sonarr" }}
+
{{ $seString }} - {{ $datetype }} {{ $date.Format "1/2 03:04PM" }}
+ {{ else }}
+
{{ $datetype }} {{ $date.Format "1/2" }}
+ {{ end }}
+
+
+ {{ if $showGrabbed }}
+ {{ if eq $buttonJustify "right" }}
+
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+ {{ else }}
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+
+ {{ end }}
+ {{ else }}
+
+ {{ $summary }}
+
+ {{ end }}
+
+
+ {{ end }}
+ {{ end }}
+ {{ if not $itemDisplayed }}
+ - No items found.
+ {{ end }}
+
+ {{ end }}
+
diff --git a/config/monitoring/glance/widgets/arr-sonarr-upcoming.yml b/config/monitoring/glance/widgets/arr-sonarr-upcoming.yml
new file mode 100644
index 0000000..952f39f
--- /dev/null
+++ b/config/monitoring/glance/widgets/arr-sonarr-upcoming.yml
@@ -0,0 +1,450 @@
+# *arr community widget: Sonarr upcoming episodes
+- type: custom-api
+ title: Upcoming Episodes
+ title-url: ${SONARR_URL}
+ cache: 30m
+ options:
+ service: sonarr
+ type: upcoming
+ size: medium
+ collapse-after: 5
+ show-grabbed: false
+ interval: 20
+ api-base-url: ${SONARR_API_URL}
+ key: ${SONARR_KEY}
+ url: ${SONARR_URL}
+ template: |
+ {{ $collapseAfter := .Options.IntOr "collapse-after" 5 }}
+ {{ $showGrabbed := .Options.BoolOr "show-grabbed" false }}
+ {{ $sortTime := .Options.StringOr "sort-time" "desc" }}
+ {{ $coverProxy := .Options.StringOr "cover-proxy" "" }}
+ {{ $size := .Options.StringOr "size" "medium" }}
+ {{ $service := .Options.StringOr "service" "" }}
+ {{ $type := .Options.StringOr "type" "" }}
+ {{ $intervalH := .Options.IntOr "interval" 0 | mul 24 }}
+
+ {{ $now := now | formatTime "2006-01-02T15:04:05" }}
+ {{ $posInterval := (offsetNow (printf "+%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+ {{ $negInterval := (offsetNow (printf "-%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+
+ {{ $apiBaseUrl := .Options.StringOr "api-base-url" "" }}
+ {{ $key := .Options.StringOr "key" "" }}
+ {{ $url := .Options.StringOr "url" $apiBaseUrl }}
+
+ {{ if or (and (ne $service "sonarr") (ne $service "radarr") (ne $service "lidarr"))
+ (and (ne $type "upcoming") (ne $type "recent") (ne $type "missing"))
+ (eq $apiBaseUrl "") (eq $key "") (eq $url "") }}
+
+
+ Some options are not set or malformed
+
+ | service | {{ $service }} | must be sonarr, radarr, or lidarr |
+ | type | {{ $type }} | must be upcoming, recent, or missing |
+ | api-base-url | {{ $apiBaseUrl }} | should include http(s):// and port if needed |
+ | key | {{ $key }} | |
+
+
+ {{ else }}
+ {{ $requestUrl := "" }}
+
+ {{ if eq $service "sonarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeSeries=true&includeEpisode=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50&includeSeries=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?includeSeries=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "radarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeMovie=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "lidarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v1/history/since?includeArtist=true&includeAlbum=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v1/wanted/missing?page=1&pageSize=50&includeArtist=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v1/calendar?includeArtist=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+ {{ end }}
+
+ {{ $data := newRequest $requestUrl
+ | withHeader "Accept" "application/json"
+ | withHeader "X-Api-Key" $key
+ | getResponse }}
+
+
+ {{ $array := "" }}
+ {{ $sortByField := "" }}
+ {{ $itemDisplayed := false }}
+ {{ $itemDate := "" }}
+ {{ $isAvailable := false }}
+
+ {{ if eq $type "missing" }}
+ {{ $array = "records" }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ else if eq $type "recent" }}
+ {{ $sortByField = "date" }}
+ {{ else }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ end }}
+
+ {{ range $data.JSON.Array $array | sortByTime $sortByField "rfc3339" $sortTime }}
+
+ {{ if eq $service "sonarr" }}
+ {{ $itemDate = .String "airDateUtc" | parseTime "RFC3339" }}
+ {{ $itemDate = $itemDate.In now.Location | formatTime "2006-01-02T15:04:05" }}
+ {{ $isAvailable = true }}
+ {{ else if eq $service "radarr"}}
+ {{ $isAvailable = .Bool "isAvailable" }}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ else if eq $service "lidarr"}}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ $isAvailable = true }}
+ {{ end }}
+
+ {{ if or (eq $type "upcoming") (eq $type "recent") (and (or (and (gt $itemDate $negInterval) ((lt $itemDate $now ))) (eq $intervalH 0)) $isAvailable) }}
+ {{ $itemDisplayed = true }}
+ {{ $title := "" }}
+ {{ $subtitle := "" }}
+ {{ $coverUrl := "" }}
+ {{ $status := "" }}
+ {{ $coverBase := "" }}
+ {{ $height := "" }}
+ {{ $width := "" }}
+ {{ $popoverTitle := "" }}
+ {{ $popoverSubtitle := "" }}
+ {{ $popoverSummary := "" }}
+ {{ $summary := "" }}
+ {{ $link := "" }}
+ {{ $grabbed := false }}
+ {{ $date := now }}
+ {{ $datetype := "" }}
+ {{ $seString := "" }}
+ {{ $genres := "" }}
+ {{ $buttonJustify := "left" }}
+
+
+ {{ if eq $service "sonarr" }}
+ {{ $series := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "seriesId") $key }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "seriesId") }}
+ {{ end }}
+ {{ $title = .String "series.title" }}
+ {{ $link = printf "%s/series/%s#" $url (.String "series.titleSlug") }}
+ {{ $series = .Get "series" }}
+ {{ $genres = $series.Get "genres" }}
+
+ {{ if eq $type "recent" }}
+ {{ $date = .String "date" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "episode.title" }}
+ {{ $summary = .String "episode.overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "episode.seasonNumber") (.Int "episode.episodeNumber") }}
+ {{ $datetype = "Downloaded" }}
+
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "episode.title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "episode.overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+
+ {{ else if eq $type "missing" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Aired" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ else if eq $type "upcoming" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Airs" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "radarr" }}
+ {{ $movie := "" }}
+ {{ $status = .String "status" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+ {{ if eq $status "announced"}}
+ {{ if ne (.String "inCinemas") "" }}
+ {{ $date = (.String "inCinemas" | parseTime "RFC3339") }}
+ {{ $datetype = "In Cinemas" }}
+ {{ else }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ else if eq $status "inCinemas"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ else if eq $status "released"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Released" }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "movie.id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "movie.id") }}
+ {{ end }}
+ {{ $datetype = "Downloaded" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $title = .String "movie.title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "movie.overview" }}
+ {{ $popoverTitle = .String "movie.title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "movie.overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "movie.titleSlug") }}
+ {{ $movie = .Get "movie" }}
+ {{ $genres = $movie.Get "genres" }}
+ {{ if and $showGrabbed (gt (.Int "movie.movieFileId") 0) }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "id") }}
+ {{ end }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "titleSlug") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ $genres = .Get "genres" }}
+ {{ if eq $type "missing" }}
+ {{ if and $showGrabbed (.Bool "movie.hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if and $showGrabbed (.Bool "hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "lidarr" }}
+ {{ $artist := "" }}
+ {{ $album := "" }}
+ {{ $albumId := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v1/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ $album = .Get "album" }}
+ {{ $artist = $album.Get "artist" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase (.String "albumId") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase (.String "albumId") }}
+ {{ end }}
+ {{ $grabbed = true }}
+ {{ $title = $album.String "title" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $datetype = "Downloaded" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $summary = $album.String "overview" }}
+ {{ $popoverTitle = $album.String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = $artist.String "overview" }}
+
+ {{ else }}
+ {{ $artist = .Get "artist" }}
+ {{ $album = .Get "album" }}
+ {{ if eq $type "missing" }}
+ {{ $datetype = "Released" }}
+ {{ else }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ range .Array "releases" }}
+ {{ $albumId = .String "albumId" }}
+ {{ break }}
+ {{ end }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase $albumId $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase $albumId }}
+ {{ end }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $summary = $album.String "overview" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "artist.overview" }}
+ {{ end }}
+ {{ end }}
+
+ {{ if eq $size "small" }}
+ {{ $buttonJustify = "right" }}
+ {{ $height = "9rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "9rem" }}
+ {{ else }}
+ {{ $width = "6rem" }}
+ {{ end }}
+ {{ else if eq $size "medium" }}
+ {{ $height = "12rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "12rem" }}
+ {{ else }}
+ {{ $width = "8rem" }}
+ {{ end }}
+ {{ else if eq $size "large" }}
+ {{ $height = "15rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "15rem" }}
+ {{ else }}
+ {{ $width = "10rem" }}
+ {{ end }}
+ {{ else if eq $size "huge" }}
+ {{ $height = "18rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "18rem" }}
+ {{ else }}
+ {{ $width = "12rem" }}
+ {{ end }}
+ {{ end }}
+
+ -
+
+
+
+
+
+
{{ $popoverTitle }}
+
{{ $popoverSubtitle }}
+
+ {{ if ne $popoverSummary "" }}
+ {{ $popoverSummary }}
+ {{ else }}
+ TBA
+ {{ end }}
+
+ {{ if gt (len ($genres.Array "")) 0 }}
+
+ {{ range $genres.Array "" }}
+ - {{ .String "" }}
+ {{ end }}
+
+ {{ end }}
+
+
+

+
+
+
+
{{ $title }}
+
{{ $subtitle }}
+
+ {{ if eq $service "sonarr" }}
+
{{ $seString }} - {{ $datetype }} {{ $date.Format "1/2 03:04PM" }}
+ {{ else }}
+
{{ $datetype }} {{ $date.Format "1/2" }}
+ {{ end }}
+
+
+ {{ if $showGrabbed }}
+ {{ if eq $buttonJustify "right" }}
+
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+ {{ else }}
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+
+ {{ end }}
+ {{ else }}
+
+ {{ $summary }}
+
+ {{ end }}
+
+
+ {{ end }}
+ {{ end }}
+ {{ if not $itemDisplayed }}
+ - No items found.
+ {{ end }}
+
+ {{ end }}
+
diff --git a/config/monitoring/glance/widgets/arr-template.txt b/config/monitoring/glance/widgets/arr-template.txt
new file mode 100644
index 0000000..ed29533
--- /dev/null
+++ b/config/monitoring/glance/widgets/arr-template.txt
@@ -0,0 +1,433 @@
+{{ $collapseAfter := .Options.IntOr "collapse-after" 5 }}
+{{ $showGrabbed := .Options.BoolOr "show-grabbed" false }}
+{{ $sortTime := .Options.StringOr "sort-time" "desc" }}
+{{ $coverProxy := .Options.StringOr "cover-proxy" "" }}
+{{ $size := .Options.StringOr "size" "medium" }}
+{{ $service := .Options.StringOr "service" "" }}
+{{ $type := .Options.StringOr "type" "" }}
+{{ $intervalH := .Options.IntOr "interval" 0 | mul 24 }}
+
+{{ $now := now | formatTime "2006-01-02T15:04:05" }}
+{{ $posInterval := (offsetNow (printf "+%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+{{ $negInterval := (offsetNow (printf "-%dh" $intervalH)) | formatTime "2006-01-02T15:04:05" }}
+
+{{ $apiBaseUrl := .Options.StringOr "api-base-url" "" }}
+{{ $key := .Options.StringOr "key" "" }}
+{{ $url := .Options.StringOr "url" $apiBaseUrl }}
+
+{{ if or (and (ne $service "sonarr") (ne $service "radarr") (ne $service "lidarr"))
+ (and (ne $type "upcoming") (ne $type "recent") (ne $type "missing"))
+ (eq $apiBaseUrl "") (eq $key "") (eq $url "") }}
+
+
+ Some options are not set or malformed
+
+ | service | {{ $service }} | must be sonarr, radarr, or lidarr |
+ | type | {{ $type }} | must be upcoming, recent, or missing |
+ | api-base-url | {{ $apiBaseUrl }} | should include http(s):// and port if needed |
+ | key | {{ $key }} | |
+
+
+{{ else }}
+ {{ $requestUrl := "" }}
+
+ {{ if eq $service "sonarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeSeries=true&includeEpisode=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50&includeSeries=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?includeSeries=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "radarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v3/history/since?includeMovie=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v3/wanted/missing?page=1&pageSize=50" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v3/calendar?start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+
+ {{ else if eq $service "lidarr" }}
+ {{ if eq $type "recent" }}
+ {{ $requestUrl = printf "%s/api/v1/history/since?includeArtist=true&includeAlbum=true&eventType=grabbed&date=%s" $apiBaseUrl $negInterval }}
+ {{ else if eq $type "missing" }}
+ {{ $requestUrl = printf "%s/api/v1/wanted/missing?page=1&pageSize=50&includeArtist=true" $apiBaseUrl }}
+ {{ else if eq $type "upcoming" }}
+ {{ $requestUrl = printf "%s/api/v1/calendar?includeArtist=true&start=%s&end=%s" $apiBaseUrl $now $posInterval }}
+ {{ end }}
+ {{ end }}
+
+ {{ $data := newRequest $requestUrl
+ | withHeader "Accept" "application/json"
+ | withHeader "X-Api-Key" $key
+ | getResponse }}
+
+
+ {{ $array := "" }}
+ {{ $sortByField := "" }}
+ {{ $itemDisplayed := false }}
+ {{ $itemDate := "" }}
+ {{ $isAvailable := false }}
+
+ {{ if eq $type "missing" }}
+ {{ $array = "records" }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ else if eq $type "recent" }}
+ {{ $sortByField = "date" }}
+ {{ else }}
+ {{ if eq $service "sonarr" }}
+ {{ $sortByField = "airDateUtc" }}
+ {{ else }}
+ {{ $sortByField = "releaseDate" }}
+ {{ end }}
+ {{ end }}
+
+ {{ range $data.JSON.Array $array | sortByTime $sortByField "rfc3339" $sortTime }}
+
+ {{ if eq $service "sonarr" }}
+ {{ $itemDate = .String "airDateUtc" | parseTime "RFC3339" }}
+ {{ $itemDate = $itemDate.In now.Location | formatTime "2006-01-02T15:04:05" }}
+ {{ $isAvailable = true }}
+ {{ else if eq $service "radarr"}}
+ {{ $isAvailable = .Bool "isAvailable" }}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ else if eq $service "lidarr"}}
+ {{ $itemDate = .String "releaseDate" }}
+ {{ $isAvailable = true }}
+ {{ end }}
+
+ {{ if or (eq $type "upcoming") (eq $type "recent") (and (or (and (gt $itemDate $negInterval) ((lt $itemDate $now ))) (eq $intervalH 0)) $isAvailable) }}
+ {{ $itemDisplayed = true }}
+ {{ $title := "" }}
+ {{ $subtitle := "" }}
+ {{ $coverUrl := "" }}
+ {{ $status := "" }}
+ {{ $coverBase := "" }}
+ {{ $height := "" }}
+ {{ $width := "" }}
+ {{ $popoverTitle := "" }}
+ {{ $popoverSubtitle := "" }}
+ {{ $popoverSummary := "" }}
+ {{ $summary := "" }}
+ {{ $link := "" }}
+ {{ $grabbed := false }}
+ {{ $date := now }}
+ {{ $datetype := "" }}
+ {{ $seString := "" }}
+ {{ $genres := "" }}
+ {{ $buttonJustify := "left" }}
+
+
+ {{ if eq $service "sonarr" }}
+ {{ $series := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "seriesId") $key }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "seriesId") }}
+ {{ end }}
+ {{ $title = .String "series.title" }}
+ {{ $link = printf "%s/series/%s#" $url (.String "series.titleSlug") }}
+ {{ $series = .Get "series" }}
+ {{ $genres = $series.Get "genres" }}
+
+ {{ if eq $type "recent" }}
+ {{ $date = .String "date" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "episode.title" }}
+ {{ $summary = .String "episode.overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "episode.seasonNumber") (.Int "episode.episodeNumber") }}
+ {{ $datetype = "Downloaded" }}
+
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "episode.title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "episode.overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+
+ {{ else if eq $type "missing" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Aired" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "episode.hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ else if eq $type "upcoming" }}
+ {{ $date = .String "airDateUtc" | parseLocalTime "RFC3339" }}
+ {{ $date = $date.In now.Location }}
+ {{ $subtitle = .String "title" }}
+ {{ $summary = .String "overview" }}
+ {{ $seString = printf "S%02dE%02d" (.Int "seasonNumber") (.Int "episodeNumber") }}
+ {{ $datetype = "Airs" }}
+ {{ if $showGrabbed }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = $seString }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ if .Bool "hasFile" }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ $popoverTitle = .String "series.title" }}
+ {{ $popoverSummary = .String "series.overview" }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "radarr" }}
+ {{ $movie := "" }}
+ {{ $status = .String "status" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v3/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+ {{ if eq $status "announced"}}
+ {{ if ne (.String "inCinemas") "" }}
+ {{ $date = (.String "inCinemas" | parseTime "RFC3339") }}
+ {{ $datetype = "In Cinemas" }}
+ {{ else }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ else if eq $status "inCinemas"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Releases" }}
+ {{ else if eq $status "released"}}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $datetype = "Released" }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "movie.id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "movie.id") }}
+ {{ end }}
+ {{ $datetype = "Downloaded" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $title = .String "movie.title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "movie.overview" }}
+ {{ $popoverTitle = .String "movie.title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "movie.overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "movie.titleSlug") }}
+ {{ $movie = .Get "movie" }}
+ {{ $genres = $movie.Get "genres" }}
+ {{ if and $showGrabbed (gt (.Int "movie.movieFileId") 0) }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg?apikey=%s" $coverBase (.String "id") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/%s/poster-500.jpg" $coverBase (.String "id") }}
+ {{ end }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = "" }}
+ {{ $summary = .String "overview" }}
+ {{ $link = printf "%s/movie/%s#" $url (.String "titleSlug") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "overview" }}
+ {{ $genres = .Get "genres" }}
+ {{ if eq $type "missing" }}
+ {{ if and $showGrabbed (.Bool "movie.hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ else }}
+ {{ if and $showGrabbed (.Bool "hasFile") }}
+ {{ $grabbed = true }}
+ {{ end }}
+ {{ end }}
+ {{ end }}
+
+
+ {{ else if eq $service "lidarr" }}
+ {{ $artist := "" }}
+ {{ $album := "" }}
+ {{ $albumId := "" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverBase = printf "%s/api/v1/mediacover" $apiBaseUrl }}
+ {{ else }}
+ {{ $coverBase = $coverProxy }}
+ {{ end }}
+
+ {{ if eq $type "recent" }}
+ {{ $album = .Get "album" }}
+ {{ $artist = $album.Get "artist" }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase (.String "albumId") $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase (.String "albumId") }}
+ {{ end }}
+ {{ $grabbed = true }}
+ {{ $title = $album.String "title" }}
+ {{ $date = (.String "date" | parseTime "RFC3339") }}
+ {{ $datetype = "Downloaded" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $summary = $album.String "overview" }}
+ {{ $popoverTitle = $album.String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = $artist.String "overview" }}
+
+ {{ else }}
+ {{ $artist = .Get "artist" }}
+ {{ $album = .Get "album" }}
+ {{ if eq $type "missing" }}
+ {{ $datetype = "Released" }}
+ {{ else }}
+ {{ $datetype = "Releases" }}
+ {{ end }}
+ {{ range .Array "releases" }}
+ {{ $albumId = .String "albumId" }}
+ {{ break }}
+ {{ end }}
+ {{ if eq $coverProxy "" }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg?apikey=%s" $coverBase $albumId $key }}
+ {{ else }}
+ {{ $coverUrl = printf "%s/album/%s/cover-500.jpg" $coverBase $albumId }}
+ {{ end }}
+ {{ $date = (.String "releaseDate" | parseTime "RFC3339") }}
+ {{ $title = .String "title" }}
+ {{ $subtitle = $artist.String "artistName" }}
+ {{ $summary = $album.String "overview" }}
+ {{ $genres = $artist.Get "genres" }}
+ {{ $link = printf "%s/artist/%s#" $url ($artist.String "foreignArtistId") }}
+ {{ $popoverTitle = .String "title" }}
+ {{ $popoverSubtitle = printf "%s %s" $datetype ($date | formatTime "1/2/2006") }}
+ {{ $popoverSummary = .String "artist.overview" }}
+ {{ end }}
+ {{ end }}
+
+ {{ if eq $size "small" }}
+ {{ $buttonJustify = "right" }}
+ {{ $height = "9rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "9rem" }}
+ {{ else }}
+ {{ $width = "6rem" }}
+ {{ end }}
+ {{ else if eq $size "medium" }}
+ {{ $height = "12rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "12rem" }}
+ {{ else }}
+ {{ $width = "8rem" }}
+ {{ end }}
+ {{ else if eq $size "large" }}
+ {{ $height = "15rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "15rem" }}
+ {{ else }}
+ {{ $width = "10rem" }}
+ {{ end }}
+ {{ else if eq $size "huge" }}
+ {{ $height = "18rem" }}
+ {{ if eq $service "lidarr" }}
+ {{ $width = "18rem" }}
+ {{ else }}
+ {{ $width = "12rem" }}
+ {{ end }}
+ {{ end }}
+
+ -
+
+
+
+
+
+
{{ $popoverTitle }}
+
{{ $popoverSubtitle }}
+
+ {{ if ne $popoverSummary "" }}
+ {{ $popoverSummary }}
+ {{ else }}
+ TBA
+ {{ end }}
+
+ {{ if gt (len ($genres.Array "")) 0 }}
+
+ {{ range $genres.Array "" }}
+ - {{ .String "" }}
+ {{ end }}
+
+ {{ end }}
+
+
+

+
+
+
+
{{ $title }}
+
{{ $subtitle }}
+
+ {{ if eq $service "sonarr" }}
+
{{ $seString }} - {{ $datetype }} {{ $date.Format "1/2 03:04PM" }}
+ {{ else }}
+
{{ $datetype }} {{ $date.Format "1/2" }}
+ {{ end }}
+
+
+ {{ if $showGrabbed }}
+ {{ if eq $buttonJustify "right" }}
+
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+ {{ else }}
+
+ {{ if $grabbed }}Grabbed{{ else }}Missing{{ end }}
+
+
+ {{ end }}
+ {{ else }}
+
+ {{ $summary }}
+
+ {{ end }}
+
+
+ {{ end }}
+ {{ end }}
+ {{ if not $itemDisplayed }}
+ - No items found.
+ {{ end }}
+
+{{ end }}
diff --git a/config/monitoring/glance/widgets/monitor-media.yml b/config/monitoring/glance/widgets/monitor-media.yml
new file mode 100644
index 0000000..0a35ee6
--- /dev/null
+++ b/config/monitoring/glance/widgets/monitor-media.yml
@@ -0,0 +1,27 @@
+- type: monitor
+ title: Media
+ sites:
+ - title: Jellyfin
+ url: https://jellyfin.home
+ icon: mdi:television-play
+ - title: Jellyseerr
+ url: https://jellyseerr.home
+ icon: mdi:movie-search
+ - title: Radarr
+ url: https://radarr.home
+ icon: mdi:film
+ - title: Sonarr
+ url: https://sonarr.home
+ icon: mdi:television
+ - title: Lidarr
+ url: https://lidarr.home
+ icon: mdi:music
+ - title: Prowlarr
+ url: https://prowlarr.home
+ icon: mdi:radar
+ - title: qBittorrent
+ url: https://qbit.home
+ icon: mdi:download
+ - title: TorrServe
+ url: https://torrserve.home
+ icon: mdi:server-network
diff --git a/config/monitoring/grafana/provisioning/dashboards/dashboards.yaml b/config/monitoring/grafana/provisioning/dashboards/dashboards.yaml
new file mode 100644
index 0000000..4d8b6a8
--- /dev/null
+++ b/config/monitoring/grafana/provisioning/dashboards/dashboards.yaml
@@ -0,0 +1,10 @@
+apiVersion: 1
+
+providers:
+ - name: homelab
+ folder: Homelab
+ type: file
+ disableDeletion: false
+ editable: true
+ options:
+ path: /etc/grafana/provisioning/dashboards/json
diff --git a/config/monitoring/grafana/provisioning/datasources/datasources.yaml b/config/monitoring/grafana/provisioning/datasources/datasources.yaml
new file mode 100644
index 0000000..789d5d4
--- /dev/null
+++ b/config/monitoring/grafana/provisioning/datasources/datasources.yaml
@@ -0,0 +1,16 @@
+apiVersion: 1
+
+datasources:
+ - name: Prometheus
+ type: prometheus
+ access: proxy
+ url: http://prometheus:9090
+ isDefault: false
+ editable: true
+
+ - name: Loki
+ type: loki
+ access: proxy
+ url: http://loki:3100
+ isDefault: true
+ editable: true
diff --git a/config/monitoring/loki-config.yaml b/config/monitoring/loki-config.yaml
new file mode 100644
index 0000000..7aa27f1
--- /dev/null
+++ b/config/monitoring/loki-config.yaml
@@ -0,0 +1,49 @@
+auth_enabled: false
+
+server:
+ http_listen_port: 3100
+ log_level: info
+
+ingester:
+ wal:
+ dir: /loki/wal
+ lifecycler:
+ ring:
+ kvstore:
+ store: inmemory
+ replication_factor: 1
+ chunk_idle_period: 5m
+ chunk_target_size: 1048576
+ max_transfer_retries: 0
+
+schema_config:
+ configs:
+ - from: 2020-10-24
+ store: boltdb
+ object_store: filesystem
+ schema: v11
+ index:
+ prefix: index_
+ period: 168h
+
+storage_config:
+ boltdb:
+ directory: /loki/index
+ filesystem:
+ directory: /loki/chunks
+
+limits_config:
+ enforce_metric_name: false
+ reject_old_samples: true
+ reject_old_samples_max_age: 168h
+ split_queries_by_interval: 15m
+
+querier:
+ max_outstanding_per_tenant: 4096
+
+chunk_store_config:
+ max_look_back_period: 0s
+
+table_manager:
+ retention_deletes_enabled: true
+ retention_period: 168h
diff --git a/config/monitoring/prometheus.yml b/config/monitoring/prometheus.yml
new file mode 100644
index 0000000..3e97ca9
--- /dev/null
+++ b/config/monitoring/prometheus.yml
@@ -0,0 +1,24 @@
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+scrape_configs:
+ - job_name: "node"
+ static_configs:
+ - targets: ["192.168.1.70:9100"]
+
+ - job_name: "cadvisor"
+ static_configs:
+ - targets: ["cadvisor:8080"]
+
+ - job_name: "prometheus"
+ static_configs:
+ - targets: ["localhost:9090"]
+
+ - job_name: "caddy"
+ scheme: https
+ metrics_path: /metrics
+ tls_config:
+ insecure_skip_verify: true
+ static_configs:
+ - targets: ["metrics.home:443"]
diff --git a/config/monitoring/promtail-config.yaml b/config/monitoring/promtail-config.yaml
new file mode 100644
index 0000000..764f6b3
--- /dev/null
+++ b/config/monitoring/promtail-config.yaml
@@ -0,0 +1,48 @@
+server:
+ http_listen_port: 9080
+ grpc_listen_port: 0
+
+positions:
+ filename: /tmp/positions.yaml
+
+clients:
+ - url: http://loki:3100/loki/api/v1/push
+
+scrape_configs:
+ - job_name: varlogs
+ static_configs:
+ - targets: [localhost]
+ labels:
+ job: varlogs
+ host: cyberdeck
+ __path__: /var/log/**/*.log
+
+ - job_name: docker
+ static_configs:
+ - targets: [localhost]
+ labels:
+ job: docker
+ host: cyberdeck
+ __path__: /var/lib/docker/containers/*/*-json.log
+ pipeline_stages:
+ - docker: {}
+ relabel_configs:
+ - source_labels: [__path__]
+ regex: "/var/lib/docker/containers/([^/]+)/.*"
+ target_label: container
+ replacement: "$1"
+
+ - job_name: journald
+ journal:
+ path: /var/log/journal
+ max_age: 12h
+ labels:
+ job: journald
+ host: cyberdeck
+ relabel_configs:
+ - source_labels: [__journal__systemd_unit]
+ target_label: unit
+ - source_labels: [__journal__hostname]
+ target_label: host
+ - source_labels: [__journal__priority_keyword]
+ target_label: level
diff --git a/config/storage/copyparty/copyparty.conf b/config/storage/copyparty/copyparty.conf
new file mode 100644
index 0000000..bede08e
--- /dev/null
+++ b/config/storage/copyparty/copyparty.conf
@@ -0,0 +1,14 @@
+[global]
+ i: 0.0.0.0
+ rproxy: -1
+ xff-src: 172.20.0.0/16
+
+[accounts]
+ dan: 44435777
+
+[/]
+/srv/media/share
+ accs:
+ r: * # everyone gets read-access, but
+ rw: dan # the user "ed" gets read-write
+ d: dan
diff --git a/config/storage/copyparty/initcfg b/config/storage/copyparty/initcfg
new file mode 100644
index 0000000..42ec2da
--- /dev/null
+++ b/config/storage/copyparty/initcfg
@@ -0,0 +1 @@
+# Override image default to avoid duplicate [/] volume (see copyparty.conf for volume definition).
diff --git a/stacks/media/arr.yaml b/stacks/media/arr.yaml
new file mode 100644
index 0000000..e95e955
--- /dev/null
+++ b/stacks/media/arr.yaml
@@ -0,0 +1,83 @@
+# When *arr apps connect to Prowlarr, use HTTP and Docker service names:
+# Lidarr: http://lidarr:8686 Radarr: http://radarr:7878
+# Sonarr: http://sonarr:8989 Prowlarr: http://prowlarr:9696
+services:
+ sonarr:
+ image: lscr.io/linuxserver/sonarr:latest
+ container_name: sonarr
+ environment:
+ - PUID=1000
+ - PGID=1000
+ - TZ=Europe/Moscow
+ ports:
+ - "8989:8989"
+ volumes:
+ - /srv/homelab/config/media/sonarr:/config
+ - /srv/media:/srv/media
+ - /srv/media/downloads:/downloads
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ radarr:
+ image: lscr.io/linuxserver/radarr:latest
+ container_name: radarr
+ environment:
+ - PUID=1000
+ - PGID=1000
+ - TZ=Europe/Moscow
+ ports:
+ - "7878:7878"
+ volumes:
+ - /srv/homelab/config/media/radarr:/config
+ - /srv/media:/srv/media
+ - /srv/media/downloads:/downloads
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ lidarr:
+ image: lscr.io/linuxserver/lidarr:latest
+ container_name: lidarr
+ environment:
+ - PUID=1000
+ - PGID=1000
+ - TZ=Europe/Moscow
+ ports:
+ - "8686:8686"
+ volumes:
+ - /srv/homelab/config/media/lidarr:/config
+ - /srv/media:/srv/media
+ - /srv/media/downloads:/downloads
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ prowlarr:
+ image: lscr.io/linuxserver/prowlarr:latest
+ container_name: prowlarr
+ environment:
+ - PUID=1000
+ - PGID=1000
+ - TZ=Europe/Moscow
+ ports:
+ - "9696:9696"
+ volumes:
+ - /srv/homelab/config/media/prowlarr:/config
+ - /srv/media:/srv/media
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
diff --git a/stacks/media/compose.yaml b/stacks/media/compose.yaml
new file mode 100644
index 0000000..284672e
--- /dev/null
+++ b/stacks/media/compose.yaml
@@ -0,0 +1,9 @@
+name: media
+
+include:
+ - ./arr.yaml
+ - ./qbittorrent.yaml
+ - ./jellyfin.yaml
+ - ./seer.yaml
+ - ./navidrome.yaml
+ - ./immich.yaml
diff --git a/stacks/media/immich.env b/stacks/media/immich.env
new file mode 100644
index 0000000..7f89982
--- /dev/null
+++ b/stacks/media/immich.env
@@ -0,0 +1,10 @@
+TZ=Europe/Moscow
+IMMICH_VERSION=v2
+DB_HOSTNAME=immich-postgres
+REDIS_HOSTNAME=immich-redis
+DB_PASSWORD=change_me
+DB_USERNAME=postgres
+DB_DATABASE_NAME=immich
+POSTGRES_PASSWORD=change_me
+POSTGRES_USER=postgres
+POSTGRES_DB=immich
diff --git a/stacks/media/immich.yaml b/stacks/media/immich.yaml
new file mode 100644
index 0000000..5d95ad4
--- /dev/null
+++ b/stacks/media/immich.yaml
@@ -0,0 +1,67 @@
+services:
+ immich-server:
+ container_name: immich_server
+ image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-v2}
+ volumes:
+ - /srv/media/immich:/data
+ - /etc/localtime:/etc/localtime:ro
+ env_file:
+ - ./immich.env
+ ports:
+ - "2283:2283"
+ depends_on:
+ - immich-redis
+ - immich-postgres
+ restart: unless-stopped
+ healthcheck:
+ disable: false
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ immich-machine-learning:
+ container_name: immich_machine_learning
+ image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-v2}
+ volumes:
+ - /srv/homelab/data/media/immich/model-cache:/cache
+ env_file:
+ - ./immich.env
+ restart: unless-stopped
+ healthcheck:
+ disable: false
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ immich-redis:
+ container_name: immich_redis
+ image: docker.io/valkey/valkey:9@sha256:546304417feac0874c3dd576e0952c6bb8f06bb4093ea0c9ca303c73cf458f63
+ healthcheck:
+ test: redis-cli ping || exit 1
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ immich-postgres:
+ container_name: immich_postgres
+ image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23
+ env_file:
+ - ./immich.env
+ shm_size: 128mb
+ volumes:
+ - /srv/homelab/data/media/immich/postgres:/var/lib/postgresql/data
+ restart: unless-stopped
+ healthcheck:
+ disable: false
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
diff --git a/stacks/media/jellyfin.yaml b/stacks/media/jellyfin.yaml
new file mode 100644
index 0000000..dec7f58
--- /dev/null
+++ b/stacks/media/jellyfin.yaml
@@ -0,0 +1,21 @@
+services:
+ jellyfin:
+ image: lscr.io/linuxserver/jellyfin:latest
+ container_name: jellyfin
+ environment:
+ - PUID=1000
+ - PGID=1000
+ - TZ=Europe/Moscow
+ ports:
+ - "8096:8096"
+ volumes:
+ - /srv/homelab/config/media/jellyfin:/config
+ - /srv/homelab/data/media/jellyfin/cache:/cache
+ - /srv/media:/srv/media
+ - /srv/media:/media
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
diff --git a/stacks/media/navidrome.yaml b/stacks/media/navidrome.yaml
new file mode 100644
index 0000000..5569c9d
--- /dev/null
+++ b/stacks/media/navidrome.yaml
@@ -0,0 +1,16 @@
+services:
+ navidrome:
+ image: deluan/navidrome:latest
+ container_name: navidrome
+ user: "1000:1000"
+ ports:
+ - "4533:4533"
+ volumes:
+ - /srv/homelab/data/media/navidrome:/data
+ - /srv/media/music:/music:ro
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
diff --git a/stacks/media/qbittorrent.yaml b/stacks/media/qbittorrent.yaml
new file mode 100644
index 0000000..13eb071
--- /dev/null
+++ b/stacks/media/qbittorrent.yaml
@@ -0,0 +1,24 @@
+services:
+ qbittorrent:
+ image: lscr.io/linuxserver/qbittorrent:latest
+ container_name: qbittorrent
+ environment:
+ - PUID=1000
+ - PGID=1000
+ - TZ=Europe/Moscow
+ - WEBUI_PORT=8081
+ ports:
+ - "8081:8081"
+ - "6881:6881"
+ - "6881:6881/udp"
+ volumes:
+ - /srv/homelab/config/media/qbittorrent:/config
+ - /srv/media/downloads:/downloads
+ - /srv/media/movies:/movies
+ - /srv/media/shows:/shows
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
diff --git a/stacks/media/seer.yaml b/stacks/media/seer.yaml
new file mode 100644
index 0000000..747695f
--- /dev/null
+++ b/stacks/media/seer.yaml
@@ -0,0 +1,18 @@
+services:
+ seer:
+ image: ghcr.io/seerr-team/seerr:latest
+ container_name: seer
+ environment:
+ - LOG_LEVEL=info
+ - TZ=Europe/Moscow
+ - PORT=5055
+ ports:
+ - "5055:5055"
+ volumes:
+ - /srv/homelab/config/media/seer:/app/config
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
diff --git a/stacks/monitoring/compose.yaml b/stacks/monitoring/compose.yaml
new file mode 100644
index 0000000..c949791
--- /dev/null
+++ b/stacks/monitoring/compose.yaml
@@ -0,0 +1,6 @@
+name: monitoring
+
+include:
+ - ./grafana.yaml
+ - ./glance.yaml
+ - ./portainer.yaml
diff --git a/stacks/monitoring/glance.env b/stacks/monitoring/glance.env
new file mode 100644
index 0000000..0cc786f
--- /dev/null
+++ b/stacks/monitoring/glance.env
@@ -0,0 +1,22 @@
+# Variables for config (re-run docker compose up after changes).
+
+# Auth (optional)
+MY_SECRET_TOKEN=123456
+
+# *arr stack – API base URL and key (http from container)
+RADARR_URL=http://radarr.home
+RADARR_API_URL=http://radarr.home
+RADARR_KEY=512cab3acfe84420957a9c8585560f8f
+
+SONARR_URL=http://sonarr.home
+SONARR_API_URL=http://sonarr.home
+SONARR_KEY=b3a84ef407ca42d99f5bc22fb8afc401
+
+LIDARR_URL=http://lidarr.home
+LIDARR_API_URL=http://lidarr.home
+LIDARR_KEY=e0862cb9aa8c4c0f8115ec794c5bebc1
+
+# Portainer (Settings → API → Enable API; endpoint ID from URL e.g. !#/2/docker → 2)
+PORTAINER_URL=http://portainer.home
+PORTAINER_ENDPOINT_ID=3
+PORTAINER_API_KEY=ptr_zmLlSaAbw8nQRoJlDRb006IDVtJLojneToSVNWJxo3c=
diff --git a/stacks/monitoring/glance.env.example b/stacks/monitoring/glance.env.example
new file mode 100644
index 0000000..8280251
--- /dev/null
+++ b/stacks/monitoring/glance.env.example
@@ -0,0 +1,13 @@
+MY_SECRET_TOKEN=change_me
+RADARR_URL=http://radarr.home
+RADARR_API_URL=http://radarr.home
+RADARR_KEY=change_me
+SONARR_URL=http://sonarr.home
+SONARR_API_URL=http://sonarr.home
+SONARR_KEY=change_me
+LIDARR_URL=http://lidarr.home
+LIDARR_API_URL=http://lidarr.home
+LIDARR_KEY=change_me
+PORTAINER_URL=http://portainer.home
+PORTAINER_ENDPOINT_ID=1
+PORTAINER_API_KEY=change_me
diff --git a/stacks/monitoring/glance.yaml b/stacks/monitoring/glance.yaml
new file mode 100644
index 0000000..00ea1c3
--- /dev/null
+++ b/stacks/monitoring/glance.yaml
@@ -0,0 +1,24 @@
+services:
+ glance:
+ container_name: glance
+ image: glanceapp/glance
+ restart: unless-stopped
+ volumes:
+ - /srv/homelab/config/monitoring/glance:/app/config
+ - /srv/homelab/data/monitoring/glance/assets:/app/assets
+ - /etc/localtime:/etc/localtime:ro
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/custom-ca.crt:ro
+ - /srv/media:/srv/media:ro
+ - /backup:/backup:ro
+ ports:
+ - "9090:8080"
+ env_file:
+ - ./glance.env
+ environment:
+ - SSL_CERT_FILE=/etc/ssl/certs/custom-ca.crt
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
diff --git a/stacks/monitoring/grafana.yaml b/stacks/monitoring/grafana.yaml
new file mode 100644
index 0000000..a29c89e
--- /dev/null
+++ b/stacks/monitoring/grafana.yaml
@@ -0,0 +1,98 @@
+# One-time permissions (run on host):
+# sudo chown -R 65534:65534 /srv/homelab/data/monitoring/prometheus
+# sudo chown -R 10001:10001 /srv/homelab/data/monitoring/loki
+# sudo chown -R 472:472 /srv/homelab/data/monitoring/grafana
+#
+services:
+ cadvisor:
+ image: gcr.io/cadvisor/cadvisor:latest
+ container_name: cadvisor
+ ports:
+ - "8088:8080"
+ volumes:
+ - /:/rootfs:ro
+ - /var/run:/var/run:ro
+ - /sys:/sys:ro
+ - /var/lib/docker:/var/lib/docker:ro
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ prometheus:
+ image: prom/prometheus:latest
+ container_name: prometheus
+ extra_hosts:
+ - "metrics.home:192.168.1.70"
+ ports:
+ - "9094:9090"
+ volumes:
+ - /srv/homelab/config/monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
+ - /srv/homelab/data/monitoring/prometheus:/prometheus
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ grafana:
+ image: grafana/grafana:latest
+ container_name: grafana
+ user: "472:65534"
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+ environment:
+ - GF_FEATURE_TOGGLES_KUBERNETESDASHBOARDS=false
+ depends_on:
+ - loki
+ - prometheus
+ ports:
+ - "3034:3000"
+ volumes:
+ - /srv/homelab/data/monitoring/grafana:/var/lib/grafana
+ - /srv/homelab/config/monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ loki:
+ image: grafana/loki:2.9.0
+ container_name: loki
+ ports:
+ - "3100:3100"
+ command: -config.file=/etc/loki/config.yaml
+ volumes:
+ - /srv/homelab/config/monitoring/loki-config.yaml:/etc/loki/config.yaml:ro
+ - /srv/homelab/data/monitoring/loki:/loki
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
+
+ promtail:
+ image: grafana/promtail:2.9.0
+ container_name: promtail
+ command: -config.file=/etc/promtail/config.yaml
+ depends_on:
+ - loki
+ volumes:
+ - /srv/homelab/config/monitoring/promtail-config.yaml:/etc/promtail/config.yaml:ro
+ - /var/log:/var/log:ro
+ - /etc/machine-id:/etc/machine-id:ro
+ - /var/lib/docker/containers:/var/lib/docker/containers:ro
+ - /run/log/journal:/run/log/journal:ro
+ - /var/log/journal:/var/log/journal:ro
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
diff --git a/stacks/monitoring/portainer.yaml b/stacks/monitoring/portainer.yaml
new file mode 100644
index 0000000..bf91f78
--- /dev/null
+++ b/stacks/monitoring/portainer.yaml
@@ -0,0 +1,18 @@
+services:
+ portainer:
+ container_name: portainer
+ image: portainer/portainer-ce:lts
+ restart: unless-stopped
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - /srv/homelab/data/portainer/data:/data
+ ports:
+ - "9443:9443"
+ - "8000:8000"
+ environment:
+ - TRUSTED_ORIGINS=portainer.home
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
diff --git a/stacks/storage/compose.yaml b/stacks/storage/compose.yaml
new file mode 100644
index 0000000..5dff8e5
--- /dev/null
+++ b/stacks/storage/compose.yaml
@@ -0,0 +1,5 @@
+name: storage
+
+include:
+ - ./copyparty.yaml
+ - ./gitea.yaml
diff --git a/stacks/storage/copyparty.yaml b/stacks/storage/copyparty.yaml
new file mode 100644
index 0000000..ea8b197
--- /dev/null
+++ b/stacks/storage/copyparty.yaml
@@ -0,0 +1,18 @@
+services:
+ copyparty:
+ image: copyparty/ac:latest
+ container_name: copyparty
+ command: ["-c", "/cfg/copyparty.conf"]
+ ports:
+ - "3923:3923"
+ volumes:
+ - /srv/homelab/config/storage/copyparty:/cfg
+ - /srv/homelab/config/storage/copyparty/initcfg:/z/initcfg:ro
+ - /srv/media:/srv/media
+ - /srv/homelab/data/storage/copyparty:/w
+ restart: unless-stopped
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "5"
diff --git a/stacks/storage/gitea.yaml b/stacks/storage/gitea.yaml
new file mode 100644
index 0000000..fb34c55
--- /dev/null
+++ b/stacks/storage/gitea.yaml
@@ -0,0 +1,21 @@
+networks:
+ gitea:
+ external: false
+
+services:
+ server:
+ image: docker.gitea.com/gitea:1.25.4
+ container_name: gitea
+ environment:
+ - USER_UID=1000
+ - USER_GID=1000
+ restart: always
+ networks:
+ - gitea
+ volumes:
+ - /srv/homelab/data/storage/gitea:/data
+ - /etc/timezone:/etc/timezone:ro
+ - /etc/localtime:/etc/localtime:ro
+ ports:
+ - "3000:3000"
+ - "222:22"