Files
glance/config/services.yml
T

1219 lines
58 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
- name: Services
columns:
- size: small
widgets:
- type: rss
title: SirBlob Blog
limit: 5
style: vertical-list
feeds:
- url: https://blog.sirblob.co/rss.xml
- type: custom-api
title: "Sync Status"
cache: 300s
headers:
X-API-Key: ${SYNCTHING_API_KEY}
url: "${SYNCTHING_URL}/rest/system/config"
subrequests:
<FOLDER_1_ID>:
url: "${SYNCTHING_URL}/rest/db/completion"
parameters: { folder: "<FOLDER_1_ID>" }
headers: { X-API-Key: ${SYNCTHING_API_KEY} }
<FOLDER_2_ID>:
url: "${SYNCTHING_URL}/rest/db/completion"
parameters: { folder: "<FOLDER_2_ID>" }
headers: { X-API-Key: ${SYNCTHING_API_KEY} }
options:
debug: false
folders:
- id: <FOLDER_1_ID>
name: "Backup Folder 1"
enabled: true
- id: <FOLDER_2_ID>
name: "Backup Folder 2"
enabled: true
template: |-
{{ $debug := or (.Options.debug) false }}
{{ $folders := .Options.folders }}
{{ if ne .Response.StatusCode 200 }}
<p class="text-danger">⚠️ Syncthing API unreachable ({{ .Response.Status }})</p>
{{ else }}
<ul class="list">
{{ range $folders }}
{{ $id := .id }}
{{ $name := .name }}
{{ if .enabled }}
{{ $r := $.Subrequest $id }}
{{ if eq $r.Response.StatusCode 200 }}
{{ $comp := $r.JSON.Float "completion" }}
<li>
<span class="{{ if eq $comp 100.0 }}text-success{{ else if gt $comp 0.0 }}text-info{{ else }}text-danger{{ end }}">
{{ if eq $comp 100.0 }}✔️
{{ else if gt $comp 0.0 }}🔄
{{ else }}⚠️
{{ end }}
{{ $name }}
{{ if eq $comp 100.0 }}
In Sync (100 %)
{{ else if gt $comp 0.0 }}
Syncing ({{ printf "%.1f" $comp }} %)
{{ else }}
Unreachable
{{ end }}
</span>
</li>
{{ else }}
<li><span class="text-danger">⚠️ {{ $name }} Unreachable</span></li>
{{ end }}
{{ end }}
{{ end }}
</ul>
{{ end }}
{{ if $debug }}
<details style="margin-top:0.5rem">
<summary>Debug raw Syncthing JSON</summary>
<pre style="font-size:10px">
{{ (printf "%+v" .JSON) }}
</pre>
</details>
{{ end }}
- size: full
widgets:
- type: monitor
cache: 1m
title: My Services
sites:
- title: Plex
url: "${PLEX_URL}/web/index.html#!/"
icon: si:plex
- title: Immich
url: https://immich.sirblob.co
icon: si:immich
- title: Blog
url: https://blog.sirblob.co/
icon: mdi:web
- title: MCSManager
url: http://panel.sirblob.co/
icon: mdi:server
- title: Coder
url: "${CODER_APP_URL}"
icon: mdi:code-braces
- title: Excalidraw
url: https://draw.sirblob.co/
icon: mdi:draw
- title: qBittorrent
url: https://torr.sirblob.co/
icon: si:qbittorrent
- title: Gitea
url: https://git.sirblob.co/
icon: si:gitea
- title: Nginx Proxy Manager # Added NPM
url: https://npm.home.lab/
icon: si:nginxproxymanager
- title: Seanime
url: https://sea.home.lab/
icon: mdi:television
- title: Docmost
url: https://notes.sirblob.co/
icon: mdi:note-text
- title: Stirling PDF
url: https://pdf.home.lab/
icon: mdi:file-pdf-box
- title: ConvertX
url: https://vertx.home.lab/
icon: mdi:file-swap
- title: Dockage
url: https://dock.home.lab/
icon: si:docker
- title: Paperless NGX
url: https://paperless.home.lab/
icon: si:paperlessngx
- title: RustFS
url: http://s3.home.lab/
icon: mdi:bucket
- title: Netbird
url: https://netbird.sirblob.co/
icon: si:netbird
- type: custom-api
title: Immich stats
cache: 10m
url: https://immich.sirblob.co/api/server/statistics
headers:
x-api-key: ${IMMICH_API_KEY}
Accept: application/json
template: |
<div class="flex justify-between text-center">
<div>
<div class="color-highlight size-h3">{{ .JSON.Int "photos" | formatNumber }}</div>
<div class="size-h6">PHOTOS</div>
</div>
<div>
<div class="color-highlight size-h3">{{ .JSON.Int "videos" | formatNumber }}</div>
<div class="size-h6">VIDEOS</div>
</div>
<div>
<div class="color-highlight size-h3">{{ div (.JSON.Int "usage" | toFloat) 1073741824 | toInt | formatNumber }}GB</div>
<div class="size-h6">USAGE</div>
</div>
</div>
- type: group
widgets:
- type: custom-api
title: mason.sirblob.co
cache: 30s
url: https://api.mcstatus.io/v2/status/java/mason.sirblob.co
template: &minecraft-widget-template |
<div style="display:flex; align-items:center; gap:12px;">
<div style="width:40px; height:40px; flex-shrink:0; border-radius:4px; display:flex; justify-content:center; align-items:center; overflow:hidden;">
{{ if .JSON.Bool "online" }}
<img src="{{ .JSON.String "icon" | safeURL }}" width="64" height="64" style="object-fit:contain;">
{{ else }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" style="width:32px; height:32px; opacity:0.5;">
<path fill-rule="evenodd" d="M1 5.25A2.25 2.25 0 0 1 3.25 3h13.5A2.25 2.25 0 0 1 19 5.25v9.5A2.25 2.25 0 0 1 16.75 17H3.25A2.25 2.25 0 0 1 1 14.75v-9.5Zm1.5 5.81v3.69c0 .414.336.75.75.75h13.5a.75.75 0 0 0 .75-.75v-2.69l-2.22-2.219a.75.75 0 0 0-1.06 0l-1.91 1.909.47.47a.75.75 0 1 1-1.06 1.06L6.53 8.091a.75.75 0 0 0-1.06 0l-2.97 2.97ZM12 7a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" clip-rule="evenodd" />
</svg>
{{ end }}
</div>
<div style="flex-grow:1; min-width:0;">
<a class="size-h4 block text-truncate color-highlight">
{{ .JSON.String "host" }}
{{ if .JSON.Bool "online" }}
<span style="width: 8px; height: 8px; border-radius: 50%; background-color: var(--color-positive); display: inline-block; vertical-align: middle;" data-popover-type="text" data-popover-text="Online"></span>
{{ else }}
<span style="width: 8px; height: 8px; border-radius: 50%; background-color: var(--color-negative); display: inline-block; vertical-align: middle;" data-popover-type="text" data-popover-text="Offline"></span>
{{ end }}
</a>
<ul class="list-horizontal-text">
<li>
{{ if .JSON.Bool "online" }}
<span>{{ .JSON.String "version.name_clean" }}</span>
{{ else }}
<span>Offline</span>
{{ end }}
</li>
{{ if .JSON.Bool "online" }}
<li data-popover-type="html">
<div data-popover-html>
{{ range .JSON.Array "players.list" }}{{ .String "name_clean" }}<br>{{ end }}
</div>
<p style="display:inline-flex;align-items:center;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6" style="height:1em;vertical-align:middle;margin-right:0.5em;"><path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" /></svg>
{{ .JSON.Int "players.online" | formatNumber }}/{{ .JSON.Int "players.max" | formatNumber }} players
</p>
</li>
{{ else }}
<li>
<p style="display:inline-flex;align-items:center;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6" style="height:1em;vertical-align:middle;margin-right:0.5em;opacity:0.5;"><path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd" /></svg>
0 players
</p>
</li>
{{ end }}
</ul>
</div>
</div>
- type: custom-api
title: cobble.sirblob.co
cache: 30s
url: https://api.mcstatus.io/v2/status/java/cobble.sirblob.co
template: *minecraft-widget-template
- type: custom-api
title: cgc.sirblob.co
cache: 30s
url: https://api.mcstatus.io/v2/status/java/cgc.sirblob.co
template: *minecraft-widget-template
- type: custom-api
title: presto.sirblob.co
cache: 30s
url: https://api.mcstatus.io/v2/status/java/presto.sirblob.co
template: *minecraft-widget-template
- type: custom-api
title: create.sirblob.co
cache: 30s
url: https://api.mcstatus.io/v2/status/java/create.sirblob.co
template: *minecraft-widget-template
- type: custom-api
title: qBittorrent
cache: 10s
options:
view: "detailed" # "basic" or "detailed"
mode: "default" # "default" or "upload"
subrequests:
transfer:
url: http://${QBITTORRENT_IP}/api/v2/transfer/info
seeding:
url: http://${QBITTORRENT_IP}/api/v2/torrents/info
parameters:
filter: seeding
leeching:
url: http://${QBITTORRENT_IP}/api/v2/torrents/info
parameters:
filter: downloading
template: |
{{ $transfer := .Subrequest "transfer" }}
{{ $seeding := .Subrequest "seeding" }}
{{ $leeching := .Subrequest "leeching" }}
{{ if and (eq $transfer.Response.StatusCode 200) (eq $seeding.Response.StatusCode 200) (eq $leeching.Response.StatusCode 200) }}
{{ $isDetailed := eq (.Options.StringOr "view" "detailed") "detailed" }}
{{ $mode := .Options.StringOr "mode" "default" }}
{{ if $isDetailed }}
<!-- Detailed View -->
<div class="list" style="--list-gap: 15px;">
<div class="flex justify-between text-center">
<div>
{{ $dlSpeed := $transfer.JSON.Float "dl_info_speed" }}
{{ if eq $mode "upload" }}
<div class="color-highlight size-h3">{{ printf "%.1f MB/s" (div $dlSpeed 1000000.0) }}</div>
{{ else }}
{{ if lt $dlSpeed 1048576.0 }}
<div class="color-highlight size-h3">{{ printf "%.0f KiB/s" (div $dlSpeed 1024.0) }}</div>
{{ else }}
<div class="color-highlight size-h3">{{ printf "%.1f MiB/s" (div $dlSpeed 1048576.0) }}</div>
{{ end }}
{{ end }}
<div class="size-h6">DOWNLOADING</div>
</div>
{{ if eq $mode "upload" }}
<div>
{{ $ulSpeed := $transfer.JSON.Float "up_info_speed" }}
<div class="color-highlight size-h3">{{ printf "%.1f MB/s" (div $ulSpeed 1000000.0) }}</div>
<div class="size-h6">UPLOADING</div>
</div>
{{ end }}
<div>
<div class="color-highlight size-h3">{{ len ($seeding.JSON.Array "") }}</div>
<div class="size-h6">SEEDING</div>
</div>
{{ if eq $mode "default" }}
<div>
<div class="color-highlight size-h3">{{ len ($leeching.JSON.Array "") }}</div>
<div class="size-h6">LEECHING</div>
</div>
{{ end }}
</div>
<!-- Downloading list -->
{{ $downloadingTorrents := $leeching.JSON.Array "" }}
{{ if gt (len $downloadingTorrents) 0 }}
<div style="margin-top: 15px;">
<ul class="list collapsible-container" data-collapse-after="0" style="--list-gap: 15px;">
{{ range $t := $downloadingTorrents }}
{{ $state := $t.String "state" }}
{{ $icon := "?" }}
{{ if ge ($t.Int "completed") ($t.Int "size") }}{{ $icon = "✔" }}
{{ else if eq $state "downloading" "forcedDL" }}{{ $icon = "↓" }}
{{ else if eq $state "pausedDL" "stoppedDL" "pausedUP" "stalledDL" "stalledUP" "queuedDL" "queuedUP" }}{{ $icon = "❚❚" }}
{{ else if eq $state "error" "missingFiles" }}{{ $icon = "!" }}
{{ else if eq $state "checkingDL" "checkingUP" "allocating" }}{{ $icon = "…" }}
{{ else if eq $state "checkingResumeData" }}{{ $icon = "⟳" }}
{{ end }}
<li class="flex items-center" style="gap: 10px;">
<div class="size-h4" style="flex-shrink: 0;">{{ $icon }}</div>
<div style="flex-grow: 1; min-width: 0;">
<div class="text-truncate color-highlight">{{ $t.String "name" }}</div>
<div title="{{ $t.Float "progress" | mul 100 | printf "%.1f" }}%" style="background: rgba(128, 128, 128, 0.2); border-radius: 5px; height: 6px; margin-top: 5px; overflow: hidden;">
<div style="width: {{ $t.Float "progress" | mul 100 }}%; background-color: var(--color-positive); height: 100%; border-radius: 5px;"></div>
</div>
</div>
<div style="flex-shrink: 0; text-align: right; width: 80px;">
{{ $dlSpeed := $t.Float "dlspeed" }}
<div class="size-sm color-paragraph">
{{ if eq $mode "upload" }}
{{ if lt $dlSpeed 1000.0 }}--{{ else }}{{ printf "%.1f MB/s" (div $dlSpeed 1000000.0) }}{{ end }}
{{ else }}
{{ if lt $dlSpeed 1024.0 }}--{{ else if lt $dlSpeed 1048576.0 }}{{ printf "%.0f KiB/s" (div $dlSpeed 1024.0) }}{{ else }}{{ printf "%.1f MiB/s" (div $dlSpeed 1048576.0) }}{{ end }}
{{ end }}
</div>
{{ $eta := $t.Int "eta" }}
<div class="size-sm color-paragraph">
{{ if eq $eta 8640000 }}∞
{{ else if gt $eta 3600 }}{{ printf "%dh %dm" (div $eta 3600) (mod (div $eta 60) 60) }}
{{ else if gt $eta 0 }}{{ printf "%dm" (div $eta 60) }}
{{ else }}--{{ end }}
</div>
</div>
</li>
{{ end }}
</ul>
</div>
{{ end }}
<!-- Seeding list -->
{{ if eq $mode "upload" }}
{{ $seedingTorrents := $seeding.JSON.Array "" }}
{{ if gt (len $seedingTorrents) 0 }}
<div style="margin-top: 20px;">
<ul class="list collapsible-container" data-collapse-after="0" style="--list-gap: 15px;">
{{ range $t := $seedingTorrents }}
{{ $state := $t.String "state" }}
{{ $icon := "↑" }}
{{ if eq $state "pausedUP" "stoppedUP" "stalledUP" "queuedUP" }}{{ $icon = "❚❚" }}
{{ else if eq $state "error" "missingFiles" }}{{ $icon = "!" }}
{{ else if eq $state "checkingUP" }}{{ $icon = "…" }}
{{ else if eq $state "checkingResumeData" }}{{ $icon = "⟳" }}
{{ end }}
<li class="flex items-center" style="gap: 10px;">
<div class="size-h4" style="flex-shrink: 0;">{{ $icon }}</div>
<div style="flex-grow: 1; min-width: 0;">
<div class="text-truncate color-highlight">{{ $t.String "name" }}</div>
<div class="size-sm color-paragraph">
Ratio: {{ printf "%.2f" ($t.Float "ratio") }} |
Size: {{ printf "%.1f GB" (div ($t.Float "size") 1073741824.0) }}
</div>
</div>
<div style="flex-shrink: 0; text-align: right; width: 80px;">
{{ $ulSpeed := $t.Float "upspeed" }}
<div class="size-sm color-paragraph">
{{ if lt $ulSpeed 1000.0 }}--{{ else }}{{ printf "%.1f MB/s" (div $ulSpeed 1000000.0) }}{{ end }}
</div>
<div class="size-sm color-paragraph">Upload</div>
</div>
</li>
{{ end }}
</ul>
</div>
{{ end }}
{{ end }}
</div>
{{ else }}
<!-- Basic View -->
<div class="flex justify-between text-center">
<div>
{{ $dlSpeed := $transfer.JSON.Float "dl_info_speed" }}
<div class="color-highlight size-h3">{{ printf "%.1f MB/s" (div $dlSpeed 1000000.0) }}</div>
<div class="size-h6">DOWNLOADING</div>
</div>
{{ if eq $mode "upload" }}
<div>
{{ $ulSpeed := $transfer.JSON.Float "up_info_speed" }}
<div class="color-highlight size-h3">{{ printf "%.1f MB/s" (div $ulSpeed 1000000.0) }}</div>
<div class="size-h6">UPLOADING</div>
</div>
{{ end }}
<div>
<div class="color-highlight size-h3">{{ len ($seeding.JSON.Array "") }}</div>
<div class="size-h6">SEEDING</div>
</div>
{{ if eq $mode "default" }}
<div>
<div class="color-highlight size-h3">{{ len ($leeching.JSON.Array "") }}</div>
<div class="size-h6">LEECHING</div>
</div>
{{ end }}
</div>
{{ end }}
{{ else }}
<div class="color-negative text-center">
<p>Error fetching qBittorrent data.</p>
<p class="size-sm">Check URL and authentication bypass settings.</p>
</div>
{{ end }}
- type: custom-api
title: Plex Now Playing
cache: 1m
options:
media-server: "plex"
base-url: ${PLEX_URL}
api-key: ${PLEX_TOKEN}
small-column: false
compact: false
play-state: "indicator"
show-thumbnail: true
full-thumbnail: false
show-paused: false
show-progress-bar: false
show-progress-info: true
time-format: "15:04"
template: |
{{ $mediaServer := .Options.StringOr "media-server" "" }}
{{ $baseURL := .Options.StringOr "base-url" "" }}
{{ $apiKey := .Options.StringOr "api-key" "" }}
{{ define "errorMsg" }}
<div class="widget-error-header">
<div class="color-negative size-h3">ERROR</div>
<svg class="widget-error-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"></path>
</svg>
</div>
<p class="break-all">{{ . }}</p>
{{ end }}
{{ if or
(eq $mediaServer "")
(eq $baseURL "")
(eq $apiKey "")
}}
{{ template "errorMsg" "Some required options are not set" }}
{{ else }}
{{ $isSmallColumn := .Options.BoolOr "small-column" false }}
{{ $isCompact := .Options.BoolOr "compact" true }}
{{ $playState := .Options.StringOr "play-state" "indicator" }}
{{ $showThumbnail := .Options.BoolOr "show-thumbnail" false }}
{{ $fullThumbnail := .Options.BoolOr "full-thumbnail" false }}
{{ $showPaused := .Options.BoolOr "show-paused" false }}
{{ $showProgressBar := .Options.BoolOr "show-progress-bar" false }}
{{ $showProgressInfo := .Options.BoolOr "show-progress-info" true }}
{{ $timeFormat := .Options.StringOr "time-format" "15:04" }}
{{ $userID := "" }}
{{ $sessionsRequestURL := "" }}
{{ $sessionsCall := "" }}
{{ $sessions := "" }}
{{ $activeSessions := 0 }}
{{ if eq $mediaServer "plex" }}
{{ $sessionsRequestURL = concat $baseURL "/status/sessions" }}
{{ $sessionsCall = newRequest $sessionsRequestURL
| withHeader "Accept" "application/json"
| withHeader "X-Plex-Token" $apiKey
| getResponse }}
{{ if $sessionsCall.JSON.Exists "MediaContainer" }}
{{ $sessions = $sessionsCall.JSON.Array "MediaContainer.Metadata" }}
{{ $activeSessions = len $sessions }}
{{ else }}
{{ template "errorMsg" (concat "Could not fetch " $mediaServer " API.") }}
{{ end }}
{{ else if eq $mediaServer "tautulli" }}
{{ $sessionsRequestURL = concat $baseURL "/api/v2" }}
{{ $sessionsCall = newRequest $sessionsRequestURL
| withParameter "apikey" $apiKey
| withParameter "cmd" "get_activity"
| withHeader "Accept" "application/json"
| getResponse }}
{{ if eq $sessionsCall.Response.StatusCode 200 }}
{{ $sessions = $sessionsCall.JSON.Array "response.data.sessions" }}
{{ $activeSessions = len $sessions }}
{{ else }}
{{ template "errorMsg" (concat "Could not fetch " $mediaServer " API.") }}
{{ end }}
{{ else if or (eq $mediaServer "jellyfin") (eq $mediaServer "emby") }}
{{ $sessionsRequestURL = concat $baseURL "/Sessions" }}
{{ $sessionsCall = newRequest $sessionsRequestURL
| withParameter "api_key" $apiKey
| withParameter "activeWithinSeconds" "30"
| withHeader "Accept" "application/json"
| getResponse }}
{{ if eq $sessionsCall.Response.StatusCode 200 }}
{{ $sessions = $sessionsCall.JSON.Array "" }}
{{ if eq $mediaServer "emby" }}
{{ range $session := $sessions }}
{{ if $session.Bool "PlayState.CanSeek" }}
{{ $activeSessions = 1 }}
{{ break }}
{{ end }}
{{ end }}
{{ else }}
{{ $activeSessions = len $sessions }}
{{ end }}
{{ else }}
{{ template "errorMsg" (concat "Could not fetch " $mediaServer " API.") }}
{{ end }}
{{ end }}
{{ if and (eq $sessionsCall.Response.StatusCode 200) (eq $activeSessions 0) }}
<p>Nothing is playing right now.</p>
{{ else if $sessionsCall.JSON.Exists "MediaContainer" }}
<style>
.media-server-session-container--grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 768px) {
.media-server-session-container--grid {
display: flex;
flex-direction: column;
}
}
.media-server-progress-container {
height: 1rem;
max-width: 32rem;
border: 1px solid var(--color-text-base);
border-radius: var(--border-radius);
overflow: hidden;
}
.media-server-progress-bar {
height: 100%;
background: var(--color-primary);
border-radius: 3px;
transition: width 1s linear;
}
@keyframes progress-animation { to { width: 100%; } }
</style>
<div class="gap-10 {{ if $isSmallColumn }}flex flex-column{{ else }}media-server-session-container--grid{{ end }}">
{{ range $i, $session := $sessions }}
{{ $isClient := true }}
{{ $isPlaying := false }}
{{ $state := "playing" }}
{{ $isMovie := false }}
{{ $isShows := false }}
{{ $isMusic := false }}
{{ $userName := "" }}
{{ $title := "" }}
{{ $showTitle := "" }}
{{ $showSeason := "" }}
{{ $showEpisode := "" }}
{{ $artist := "" }}
{{ $albumTitle := "" }}
{{ $thumbURL := "" }}
{{ $now := now | formatTime "15:04:05" | parseLocalTime "15:04:05" }}
{{ $duration := 0 }}
{{ $offset := 0 }}
{{ $remainingSeconds := 0 }}
{{ if eq $mediaServer "plex" }}
{{ $isPlaying = eq ($session.String "Player.state") "playing" }}
{{ if not $isPlaying }}
{{ $state = "paused"}}
{{ end }}
{{ $mediaType := $session.String "type" }}
{{ $isMovie = eq $mediaType "movie" }}
{{ $isShows = eq $mediaType "episode" }}
{{ $isMusic = eq $mediaType "track" }}
{{ $userName = $session.String "User.title" }}
{{ $title = $session.String "title" }}
{{ $showTitle = $session.String "grandparentTitle" }}
{{ $showSeason = $session.String "parentIndex" }}
{{ $showEpisode = $session.String "index" }}
{{ $artist = $session.String "grandparentTitle" }}
{{ $albumTitle = $session.String "parentTitle" }}
{{ $thumbID := $session.String "thumb" }}
{{ if or $isShows $isMusic }}
{{ $thumbID = $session.String "parentThumb" }}
{{ end }}
{{ $thumbURL = concat $baseURL $thumbID "?X-Plex-Token=" $apiKey }}
{{ $duration = $session.Float "duration" }}
{{ $offset = $session.Float "viewOffset" }}
{{ $remainingSeconds = div (sub $duration $offset) 1000 | toInt }}
{{ else if eq $mediaServer "tautulli" }}
{{ $isPlaying = eq ($session.String "state") "playing" }}
{{ if not $isPlaying }}
{{ $state = "paused"}}
{{ end }}
{{ $mediaType := $session.String "media_type" }}
{{ $isMovie = eq $mediaType "movie" }}
{{ $isShows = eq $mediaType "episode" }}
{{ $isMusic = eq $mediaType "track" }}
{{ $userName = $session.String "user" }}
{{ $title = $session.String "title" }}
{{ $showTitle = $session.String "grandparent_title" }}
{{ $showSeason = $session.String "parent_media_index" }}
{{ $showEpisode = $session.String "media_index" }}
{{ $artist = $session.String "grandparent_title" }}
{{ $albumTitle = $session.String "parent_title" }}
{{ $thumbID := $session.String "thumb" }}
{{ if or $isShows $isMusic }}
{{ $thumbID = $session.String "parent_thumb" }}
{{ end }}
{{ $thumbURL = concat $baseURL "/api/v2?apikey=" $apiKey "&cmd=pms_image_proxy&img=" $thumbID }}
{{ $duration = $session.Float "duration" }}
{{ $offset = $session.Float "view_offset" }}
{{ $remainingSeconds = div (sub $duration $offset) 1000 | toInt }}
{{ else if or (eq $mediaServer "jellyfin") (eq $mediaServer "emby") }}
{{ if eq $mediaServer "emby" }}
{{ $isClient = $session.Bool "PlayState.CanSeek" }}
{{ end }}
{{ $isPlaying = and ($session.Exists "NowPlayingItem") (not ($session.Bool "PlayState.IsPaused")) }}
{{ if not $isPlaying }}
{{ $state = "paused"}}
{{ end }}
{{ $mediaType := $session.String "NowPlayingItem.Type" }}
{{ $isMovie = eq $mediaType "Movie" }}
{{ $isShows = eq $mediaType "Episode" }}
{{ $isMusic = eq $mediaType "Audio" }}
{{ $userName = $session.String "UserName" }}
{{ $title = $session.String "NowPlayingItem.Name" }}
{{ $showTitle = $session.String "NowPlayingItem.SeriesName" }}
{{ $showSeason = $session.String "NowPlayingItem.ParentIndexNumber" }}
{{ $showEpisode = $session.String "NowPlayingItem.IndexNumber" }}
{{ $artist = $session.String "NowPlayingItem.AlbumArtist" }}
{{ $albumTitle = $session.String "NowPlayingItem.Album" }}
{{ $thumbID := $session.String "NowPlayingItem.Id" }}
{{ if $isShows }}
{{ $thumbID = $session.String "NowPlayingItem.ParentId" }}
{{ end }}
{{ $thumbURL = concat $baseURL "/Items/" $thumbID "/Images/Primary?api_key=" $apiKey }}
{{ $duration = $session.Float "NowPlayingItem.RunTimeTicks" }}
{{ $offset = $session.Float "PlayState.PositionTicks" }}
{{ $remainingSeconds = div (sub $duration $offset) 10000000 | toInt }}
{{ end }}
{{ $progress := mul 100 (div $offset $duration) | toInt }}
{{ $endTime := $now.Add (duration (printf "%ds" $remainingSeconds)) | formatTime $timeFormat }}
{{ $showInfoFormat := concat "Season " $showSeason " Episode " $showEpisode}}
{{ if $isCompact }}
{{ $showInfoFormat = concat "S" $showSeason "E" $showEpisode}}
{{ end }}
{{ if and $isClient (or $isPlaying $showPaused) }}
<div class="card gap-5">
<div class="flex items-center gap-10 size-h3">
<span class="color-primary">{{ $userName }}</span>
{{ if eq $playState "text" }}
<span {{ if $isPlaying }}class="color-primary"{{ end }}>
[{{ $state }}]
</span>
{{ else if eq $playState "indicator" }}
<style>
.media-server-indicator {
height: .7rem;
width: .7rem;
border-radius: 100%;
{{ if $isPlaying }}
animation: pulse 5s infinite;
background: var(--color-primary);
{{ else }}
background: var(--color-text-base-muted);
{{ end }}
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 var(--color-text-base); }
40% { box-shadow: 0 0 0 4px transparent; }
100% { box-shadow: 0 0 0 4px transparent; }
}
</style>
<div class="media-server-indicator"></div>
{{ end }}
</div>
<hr class="margin-bottom-5" />
<div class="flex items-center gap-10" style="align-items: stretch;">
{{ if $showThumbnail }}
<img src="{{ $thumbURL | safeURL }}"
alt="{{ $title }} thumbnail"
class="shrink-0"
loading="lazy"
style="
max-width: 7.5rem;
border: 2px solid var(--color-primary);
border-radius: var(--border-radius);
object-fit: cover;
{{ if and $isCompact (not $fullThumbnail) }} aspect-ratio: 1; {{ end }} "
/>
{{ end }}
<ul class="flex flex-column grow justify-evenly" style="width: 0;">
{{ if $isCompact }}
{{ if $isShows }}
<ul class="list-horizontal-text flex-nowrap">
<li class="shrink-0">{{ concat "S" $showSeason "E" $showEpisode }}</li>
<li class="text-truncate">{{ $showTitle }}</li>
</ul>
{{ else if $isMusic }}
<ul class="list-horizontal-text flex-nowrap">
<li class="shrink-0">{{ $artist }}</li>
<li class="text-truncate">{{ $albumTitle }}</li>
</ul>
{{ end }}
<li class="text-truncate">{{ $title }}</li>
{{ else }}
{{ if $isShows }}
<li>{{ $showTitle }}</li>
<li>{{ concat "S" $showSeason "E" $showEpisode }}</li>
{{ else if $isMusic }}
<li>{{ $artist }}</li>
<li>{{ $albumTitle }}</li>
{{ end }}
<li>{{ $title }}</li>
{{ end }}
<li>
{{ if and $isPlaying $showProgressBar }}
<div class="flex gap-10 items-center">
<div class="media-server-progress-container grow">
<div
class ="media-server-progress-bar"
data-progress="{{ $progress }}"
data-remaining="{{ $remainingSeconds }}"
style="
width: {{ $progress }}%;
animation: progress-animation {{ $remainingSeconds }}s linear forwards;"
>
</div>
</div>
{{ if $showProgressInfo }}
<p>
{{ if and (not $isCompact) (not $isSmallColumn) }}
ends at
{{ end }}
{{ $endTime }}
</p>
{{ end }}
</div>
{{ end }}
</li>
</ul>
</div>
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
{{ end }}
- type: custom-api
title: Media Server History
frameless: true
cache: 5m
options:
media-server: "plex"
base-url: ${PLEX_URL}
api-key: ${PLEX_TOKEN}
history-length: "10"
small-column: false
compact: true
show-thumbnail: true
thumbnail-aspect-ratio: "default"
show-user: true
time-absolute: false
time-format: "Jan 02 15:04"
template: |
{{ $mediaServer := .Options.StringOr "media-server" "" }}
{{ $baseURL := .Options.StringOr "base-url" "" }}
{{ $apiKey := .Options.StringOr "api-key" "" }}
{{ $userName := .Options.StringOr "user-name" "" }}
{{ define "errorMsg" }}
<div class="widget-error-header">
<div class="color-negative size-h3">ERROR</div>
<svg class="widget-error-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"></path>
</svg>
</div>
<p class="break-all">{{ . }}</p>
{{ end }}
{{ if or
(eq $mediaServer "")
(eq $baseURL "")
(eq $apiKey "")
(and (eq $mediaServer "jellyfin") (eq $userName ""))
}}
{{ template "errorMsg" "Some required options are not set" }}
{{ else }}
{{ $historyLength := .Options.StringOr "history-length" "10" }}
{{ $mediaTypes := .Options.StringOr "media-types" "" }}
{{ if eq $mediaServer "tautulli" }}
{{ $mediaTypes = .Options.StringOr "media-types" "movie,episode,track" }}
{{ else if or (eq $mediaServer "jellyfin") (eq $mediaServer "emby") }}
{{ $mediaTypes = .Options.StringOr "media-types" "Movie,Episode,Audio" }}
{{ end }}
{{ $isSmallColumn := .Options.BoolOr "small-column" false }}
{{ $isCompact := .Options.BoolOr "compact" true }}
{{ $showThumbnail := .Options.BoolOr "show-thumbnail" false }}
{{ $thumbAspectRatio := .Options.StringOr "thumbnail-aspect-ratio" "" }}
{{ $showUser := .Options.BoolOr "show-user" true }}
{{ $timeAbsolute := .Options.BoolOr "time-absolute" false }}
{{ $timeFormat := .Options.StringOr "time-format" "Jan 02 15:04" }}
{{ $userID := "" }}
{{ $historyRequestURL := "" }}
{{ $usersRequestURL := "" }}
{{ $historyCall := "" }}
{{ $usersCall := "" }}
{{ $history := "" }}
{{ $users := "" }}
{{ if eq $mediaServer "plex" }}
{{ $historyRequestURL = concat $baseURL "/status/sessions/history/all" }}
{{ $historyCall = newRequest $historyRequestURL
| withParameter "limit" $historyLength
| withParameter "sort" "viewedAt:desc"
| withHeader "Accept" "application/json"
| withHeader "X-Plex-Token" $apiKey
| getResponse }}
{{ if $historyCall.JSON.Exists "MediaContainer" }}
{{ $history = $historyCall.JSON.Array "MediaContainer.Metadata" }}
{{ else }}
{{ template "errorMsg" (concat "Could not fetch " $mediaServer " API.") }}
{{ end }}
{{ $usersRequestURL = concat $baseURL "/accounts" }}
{{ $usersCall = newRequest $usersRequestURL
| withHeader "Accept" "application/json"
| withHeader "X-Plex-Token" $apiKey
| getResponse }}
{{ $users = $usersCall.JSON.Array "MediaContainer.Account" }}
{{ else if eq $mediaServer "tautulli" }}
{{ $historyRequestURL = concat $baseURL "/api/v2" }}
{{ $historyCall = newRequest $historyRequestURL
| withParameter "apikey" $apiKey
| withParameter "cmd" "get_history"
| withParameter "length" $historyLength
| withParameter "media_type" $mediaTypes
| withHeader "Accept" "application/json"
| getResponse }}
{{ if eq $historyCall.Response.StatusCode 200 }}
{{ $history = $historyCall.JSON.Array "response.data.data" }}
{{ else }}
{{ template "errorMsg" (concat "Could not fetch " $mediaServer " API.") }}
{{ end }}
{{ else if or (eq $mediaServer "jellyfin") (eq $mediaServer "emby") }}
{{ $usersRequestURL = concat $baseURL "/Users" }}
{{ $usersCall = newRequest $usersRequestURL
| withParameter "api_key" $apiKey
| withHeader "Accept" "application/json"
| getResponse }}
{{ $usersList := $usersCall.JSON.Array "" }}
{{ range $i, $user := $usersList }}
{{ if eq ($user.String "Name") $userName }}
{{ $userID = $user.String "Id" }}
{{ break }}
{{ end }}
{{ end }}
{{ if eq $userID "" }}
{{ template "errorMsg" (concat "User '" $userName "' not found.") }}
{{ end }}
{{ $historyRequestURL = concat $baseURL "/Users/" $userID "/Items" }}
{{ $historyCall = newRequest $historyRequestURL
| withParameter "api_key" $apiKey
| withParameter "Limit" $historyLength
| withParameter "IncludeItemTypes" $mediaTypes
| withParameter "Recursive" "true"
| withParameter "isPlayed" "true"
| withParameter "sortBy" "DatePlayed"
| withParameter "sortOrder" "Descending"
| withParameter "Fields" "UserDataLastPlayedDate"
| withHeader "Accept" "application/json"
| getResponse }}
{{ $history = $historyCall.JSON.Array "Items" }}
{{ end }}
{{ if and (eq $historyCall.Response.StatusCode 200) (eq (len $history) 0) }}
<p>Nothing has been played. Start streaming something!</p>
{{ else if $historyCall.JSON.Exists "MediaContainer" }}
<div class="carousel-container show-right-cutoff">
<div class="cards-horizontal carousel-items-container">
{{ range $n, $item := $history }}
{{ $mediaType := "" }}
{{ $isMovie := false }}
{{ $isShows := false }}
{{ $isMusic := false }}
{{ $title := "" }}
{{ $showTitle := "" }}
{{ $showSeason := "" }}
{{ $showEpisode := "" }}
{{ $artist := "" }}
{{ $albumTitle := "" }}
{{ $thumbURL := "" }}
{{ $playedAt := "" }}
{{ if eq $mediaServer "plex" }}
{{ $userID = $item.Int "accountID" }}
{{ range $n, $u := $users }}
{{ if eq $userID ($u.Int "id") }}
{{ $userName = $u.String "name" }}
{{ break }}
{{ end }}
{{ end }}
{{ $mediaType = $item.String "type" }}
{{ $isMovie = eq $mediaType "movie" }}
{{ $isShows = eq $mediaType "episode" }}
{{ $isMusic = eq $mediaType "track" }}
{{ $title = $item.String "title" }}
{{ if $isShows }}
{{ $showTitle = $item.String "grandparentTitle" }}
{{ $showSeason = $item.String "parentIndex" }}
{{ $showEpisode = $item.String "index" }}
{{ else if $isMusic }}
{{ $artist = $item.String "grandparentTitle" }}
{{ $albumTitle = $item.String "parentTitle" }}
{{ end }}
{{ $thumbID := $item.String "thumb" }}
{{ if or $isShows $isMusic}}
{{ $thumbID = $item.String "parentThumb" }}
{{ end }}
{{ $thumbURL = concat $baseURL $thumbID "?X-Plex-Token=" $apiKey }}
{{ $time := $item.String "viewedAt" }}
{{ if $timeAbsolute }}
{{ $playedAt = $time | parseLocalTime "unix" | formatTime $timeFormat }}
{{ else }}
{{ $playedAt = $time | parseRelativeTime "unix" }}
{{ end }}
{{ else if eq $mediaServer "tautulli" }}
{{ $userName = $item.String "user" }}
{{ $mediaType = $item.String "media_type" }}
{{ $isMovie = eq $mediaType "movie" }}
{{ $isShows = eq $mediaType "episode" }}
{{ $isMusic = eq $mediaType "track" }}
{{ $title = $item.String "title" }}
{{ if $isShows }}
{{ $showTitle = $item.String "grandparent_title" }}
{{ $showSeason = $item.String "parent_media_index" }}
{{ $showEpisode = $item.String "media_index" }}
{{ else if $isMusic }}
{{ $artist = $item.String "grandparent_title" }}
{{ $albumTitle = $item.String "parent_title" }}
{{ end }}
{{ $thumbID := $item.String "thumb" }}
{{ $thumbURL = concat $baseURL "/api/v2?apikey=" $apiKey "&cmd=pms_image_proxy&img=" $thumbID }}
{{ $time := $item.String "date" }}
{{ if $timeAbsolute }}
{{ $playedAt = $time | parseLocalTime "unix" | formatTime $timeFormat }}
{{ else }}
{{ $playedAt = $time | parseRelativeTime "unix" }}
{{ end }}
{{ else if or (eq $mediaServer "jellyfin") (eq $mediaServer "emby") }}
{{ $mediaType = $item.String "Type" }}
{{ $isMovie = eq $mediaType "Movie" }}
{{ $isShows = eq $mediaType "Episode" }}
{{ $isMusic = eq $mediaType "Audio" }}
{{ $title = $item.String "Name" }}
{{ if $isShows }}
{{ $showTitle = $item.String "SeriesName" }}
{{ $showSeason = $item.String "ParentIndexNumber" }}
{{ $showEpisode = $item.String "IndexNumber" }}
{{ else if $isMusic }}
{{ $artist = $item.String "AlbumArtist" }}
{{ $albumTitle = $item.String "Album" }}
{{ end }}
{{ $thumbID := $item.String "Id" }}
{{ if $isShows }}
{{ $thumbID = $item.String "SeasonId" }}
{{ end }}
{{ $thumbURL = concat $baseURL "/Items/" $thumbID "/Images/Primary?api_key=" $apiKey }}
{{ $time := $item.String "UserData.LastPlayedDate" }}
{{ if $timeAbsolute }}
{{ $playedAt = $time | parseLocalTime "rfc3339" | formatTime $timeFormat }}
{{ else }}
{{ $playedAt = $time | parseRelativeTime "rfc3339" }}
{{ end }}
{{ end }}
{{ $showInfoFormat := concat "Season " $showSeason " Episode " $showEpisode}}
{{ if $isCompact }}
{{ $showInfoFormat = concat "S" $showSeason "E" $showEpisode}}
{{ end }}
<div class="card widget-content-frame" style="max-width: 150px;">
{{ if $showThumbnail }}
{{ if $thumbURL }}
<img src="{{ $thumbURL | safeURL }}"
alt="{{ $title }} thumbnail"
loading="lazy"
class="media-server-thumbnail shrink-0"
style="
object-fit: cover;
border-radius: var(--border-radius) var(--border-radius) 0 0;
{{ if eq $thumbAspectRatio "square" }}
aspect-ratio: 1;
{{ else if eq $thumbAspectRatio "portrait" }}
aspect-ratio: 3/4;
{{ else if eq $thumbAspectRatio "landscape" }}
aspect-ratio: 4/3;
{{ else }}
aspect-ratio: initial;
{{ end }}
"
/>
{{ else }}
<div class="media-server-thumbnail-placeholder"
style="
width: 100%;
height: 150px; /* Adjust height as needed */
background-color: var(--color-background-alt);
border-radius: var(--border-radius) var(--border-radius) 0 0;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: var(--color-text-base-muted);
font-size: 0.8em;
text-align: center;
padding: 10px;
"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width: 40px; height: 40px; margin-bottom: 5px;">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75L12 6l9.75 9.75" />
</svg>
No Image
</div>
{{ end }}
{{ end }}
<div class="grow padding-inline-widget margin-top-10 margin-bottom-10">
<ul class="flex flex-column justify-evenly margin-bottom-3 {{if $isSmallColumn}}size-h6{{end}}" style="height: 100%;">
{{ if $isCompact }}
<ul class="list-horizontal-text flex-nowrap">
{{ if $showUser }}
<li class="color-primary text-truncate">{{ $userName }}</li>
{{ end }}
{{ if $timeAbsolute }}
<li class="text-truncate">{{ $playedAt }}</li>
{{ else }}
<li class="shrink-0">
<span {{ $playedAt }}></span>
{{ if not $showUser }}
<span> ago</span>
{{ end }}
</li>
{{ end }}
</ul>
{{ if $isShows }}
<ul class="list-horizontal-text flex-nowrap">
<li class="text-truncate">{{ $showInfoFormat }}</li>
<li class="text-truncate">{{ $showTitle }}</li>
</ul>
{{ else if $isMusic }}
<ul class="list-horizontal-text flex-nowrap">
<li class="text-truncate">{{ $artist }}</li>
<li class="text-truncate">{{ $albumTitle }}</li>
</ul>
{{ end }}
<li class="text-truncate">{{ $title }}</li>
{{ else }}
{{ if $showUser }}
<li class="color-primary text-truncate">{{ $userName }}</li>
{{ end }}
{{ if $timeAbsolute }}
<li class="text-truncate">{{ $playedAt }}</li>
{{ else }}
<li class="text-truncate">
<span {{ $playedAt }}></span>
<span> ago</span>
</li>
{{ end }}
{{ if $isShows }}
<li class="text-truncate">{{ $showTitle }}</li>
<li class="text-truncate">{{ $showInfoFormat }}</li>
{{ else if $isMusic }}
<li class="text-truncate">{{ $artist }}</li>
<li class="text-truncate">{{ $albumTitle }}</li>
{{ end }}
<li class="text-truncate">{{ $title }}</li>
{{ end }}
</ul>
</div>
</div>
{{ end }}
</div>
</div>
{{ end }}
{{ end }}
- size: small
widgets:
- type: custom-api
title: SirBlob Gitea
url: https://git.sirblob.co/api/v1/users/SirBlob/repos
parameters:
sort: updated
limit: 15
cache: 1m
template: |
<div class="gitea-repos">
<ul class="list list-gap-6 collapsible-container" data-collapse-after="8">
{{ range .JSON.Array "" }}
<li>
<a class="size-sm color-primary text-truncate block" href="{{ .String "html_url" }}" target="_blank">{{ .String "name" }}</a>
{{ if .String "description" }}
<p class="size-xs color-subdue line-clamp-1" style="margin-top: 2px;">{{ .String "description" }}</p>
{{ end }}
<ul class="list-horizontal-text size-xs margin-top-1">
<li class="color-subdue">{{ formatTime "Jan 02" ( parseTime "RFC3339" (.String "updated_at") ) }}</li>
{{ if .String "language" }}
<li class="color-subdue">{{ .String "language" }}</li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
</div>
- type: custom-api
title: Recent Activity
url: https://git.sirblob.co/api/v1/users/SirBlob/activities/feeds
parameters:
limit: 5
cache: 1m
headers:
Accept: application/json
template: |
<div class="activity-feed">
<ul class="list list-gap-6">
{{ range .JSON.Array "" }}
{{ $type := .String "op_type" }}
{{ $repo := .String "repo.full_name" }}
{{ $repoUrl := .String "repo.html_url" }}
{{ $date := .String "created" }}
<li>
<div class="size-xs color-subdue">{{ $date | parseTime "RFC3339" | formatTime "Jan 02 15:04" }}</div>
<div class="size-sm">
{{ if eq $type "commit_repo" }}
<span class="color-primary">Pushed</span> to <a href="{{ $repoUrl }}" class="color-highlight">{{ $repo }}</a>
{{ else if eq $type "create_repo" }}
<span class="color-primary">Created</span> <a href="{{ $repoUrl }}" class="color-highlight">{{ $repo }}</a>
{{ else }}
<span class="color-primary">{{ $type }}</span> on <a href="{{ $repoUrl }}" class="color-highlight">{{ $repo }}</a>
{{ end }}
</div>
</li>
{{ end }}
</ul>
</div>