1563 lines
72 KiB
YAML
1563 lines
72 KiB
YAML
- name: Home
|
|
columns:
|
|
- size: small
|
|
widgets:
|
|
- type: calendar
|
|
first-day-of-week: sunday
|
|
- type: clock # Added Clock widget
|
|
hour-format: 12h # Assuming 12h based on weather widget preference
|
|
timezones:
|
|
- timezone: America/Los_Angeles # San Francisco uses this timezone
|
|
label: San Francisco
|
|
- timezone: America/New_York
|
|
label: New York
|
|
- timezone: Europe/London
|
|
label: London
|
|
- timezone: Europe/Amsterdam
|
|
label: Amsterdam
|
|
- timezone: Asia/Kolkata # New Delhi uses this timezone
|
|
label: New Delhi
|
|
- timezone: Asia/Tokyo
|
|
label: Tokyo
|
|
- type: custom-api
|
|
title: Astronomy Picture of the Day
|
|
cache: 1d
|
|
url: https://api.nasa.gov/planetary/apod?api_key=${NASA_API_KEY}
|
|
headers:
|
|
Accept: application/json
|
|
template: |
|
|
{{- if eq (.JSON.String "media_type") "image" -}}
|
|
<div style="display:flex; flex-direction:column; align-items:center; width:100%; padding:8px; box-sizing:border-box;">
|
|
<!-- Clickable title -->
|
|
<p class="color-primary" style="margin:0 0 8px; text-align:center;">
|
|
<a
|
|
href="https://apod.nasa.gov/apod/astropix.html"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style="color: inherit; text-decoration: none;"
|
|
>
|
|
{{ .JSON.String "title" }}
|
|
</a>
|
|
</p>
|
|
|
|
<!-- Image -->
|
|
<img
|
|
src="{{ .JSON.String "url" }}"
|
|
alt="{{ .JSON.String "title" }}"
|
|
style="max-width:100%; height:auto; display:block; border-radius:4px;"
|
|
/>
|
|
|
|
<!-- Explanation dropdown -->
|
|
<details style="width:100%; margin-top:12px;">
|
|
<summary class="color-highlight size-h5" style="cursor:pointer;">
|
|
Show Explanation
|
|
</summary>
|
|
<p class="color-highlight size-h5" style="margin-top:8px; text-align:left; line-height:1.4;">
|
|
{{ .JSON.String "explanation" }}
|
|
</p>
|
|
</details>
|
|
</div>
|
|
{{- else -}}
|
|
<p class="color-negative" style="text-align:center;">
|
|
No image available today.
|
|
</p>
|
|
{{- end }}
|
|
|
|
- size: full
|
|
widgets:
|
|
- type: search
|
|
search-engine: google
|
|
bangs: # Added bangs section
|
|
- title: YouTube
|
|
shortcut: "!yt"
|
|
url: https://www.youtube.com/results?search_query={QUERY}
|
|
- title: Wikipedia
|
|
shortcut: "!w"
|
|
url: https://en.wikipedia.org/wiki/Special:Search?search={QUERY}
|
|
- title: Amazon
|
|
shortcut: "!a"
|
|
url: https://www.amazon.com/s?k={QUERY}
|
|
- title: Reddit
|
|
shortcut: "!r"
|
|
url: https://www.reddit.com/search/?q={QUERY}
|
|
- title: GitHub
|
|
shortcut: "!gh"
|
|
url: https://github.com/search?q={QUERY}
|
|
- type: bookmarks # Rewritten Quick Links widget
|
|
title: Quick Links
|
|
groups:
|
|
- links:
|
|
- title: Google
|
|
url: https://www.google.com/
|
|
icon: si:google
|
|
- title: GMail
|
|
url: https://mail.google.com/
|
|
icon: si:gmail
|
|
- title: GDrive
|
|
url: https://drive.google.com/
|
|
icon: si:googledrive
|
|
- links:
|
|
- title: GCal
|
|
url: https://calendar.google.com/
|
|
icon: si:googlecalendar
|
|
- title: Youtube
|
|
url: https://www.youtube.com/
|
|
icon: si:youtube
|
|
- title: Amazon
|
|
url: https://www.amazon.com/
|
|
icon: si:amazon
|
|
- links:
|
|
- title: Github
|
|
url: https://github.com/
|
|
icon: si:github
|
|
- title: LinkedIn
|
|
url: https://www.linkedin.com/
|
|
icon: si:linkedin
|
|
- title: Plex
|
|
url: https://app.plex.tv/
|
|
icon: si:plex
|
|
- links:
|
|
- title: MCSManager
|
|
url: http://panel.sirblob.co/
|
|
icon: mdi:server
|
|
- title: Gitea
|
|
url: https://git.sirblob.co/
|
|
icon: si:gitea
|
|
- title: Coder
|
|
url: https://mcbugj8flucdg.pit-1.try.coder.app
|
|
icon: mdi:code-braces
|
|
- links:
|
|
- title: qBittorrent
|
|
url: https://torr.sirblob.co/
|
|
icon: si:qbittorrent
|
|
- title: Excalidraw
|
|
url: https://draw.sirblob.co/
|
|
icon: mdi:draw
|
|
- title: Immich
|
|
url: https://immich.sirblob.co/
|
|
icon: si:immich
|
|
- links:
|
|
- title: Canvas
|
|
url: https://canvas.gmu.edu/
|
|
icon: si:instructure # Instructure is the company behind Canvas
|
|
- title: Mason360
|
|
url: https://mason360.gmu.edu/
|
|
icon: mdi:school # Using a generic school icon
|
|
- title: Nginx Proxy Manager # Added NPM
|
|
url: https://npm.sirblob.co/
|
|
icon: si:nginxproxymanager
|
|
|
|
- type: monitor
|
|
cache: 1m
|
|
title: My Services
|
|
sites:
|
|
- title: Plex
|
|
url: https://plex1.sirblob.co/web/index.html#!/
|
|
icon: si:plex
|
|
- title: Immich
|
|
url: https://immich.sirblob.co
|
|
icon: si:immich
|
|
- title: MCSManager
|
|
url: http://panel.sirblob.co/
|
|
icon: mdi:server
|
|
- title: Coder
|
|
url: https://mcbugj8flucdg.pit-1.try.coder.app
|
|
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.sirblob.co/
|
|
icon: si:nginxproxymanager
|
|
|
|
- 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: event.sirblob.co
|
|
cache: 30s
|
|
url: https://api.mcstatus.io/v2/status/java/event.sirblob.co
|
|
template: *minecraft-widget-template
|
|
|
|
- type: custom-api
|
|
title: LeetCode Daily Question
|
|
cache: 6h
|
|
url: https://leetcode.com/graphql
|
|
method: POST
|
|
headers:
|
|
Accept: application/json
|
|
body-type: json
|
|
body:
|
|
query: |
|
|
query questionOfToday {
|
|
activeDailyCodingChallengeQuestion {
|
|
link
|
|
question {
|
|
questionId
|
|
title
|
|
difficulty
|
|
topicTags {
|
|
name
|
|
slug
|
|
}
|
|
}
|
|
}
|
|
}
|
|
operationName: questionOfToday
|
|
template: |
|
|
<div class="leetcode-card">
|
|
<style>
|
|
.leetcode-card {
|
|
max-width: 600px;
|
|
margin: 8px auto;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
.leetcode-card h1 {
|
|
font-size: 24px;
|
|
margin: 0 0 16px;
|
|
line-height: 1.3;
|
|
}
|
|
.leetcode-card h1 a {
|
|
color: #8CAAEE;
|
|
text-decoration: none;
|
|
transition: color 0.2s ease;
|
|
}
|
|
.leetcode-card h1 a:hover {
|
|
color: #BAC8FF;
|
|
text-decoration: underline;
|
|
}
|
|
.leetcode-card p {
|
|
margin: 8px 0;
|
|
color: #C6D0F5;
|
|
font-size: 16px;
|
|
}
|
|
.leetcode-card .difficulty {
|
|
font-weight: 600;
|
|
color: #A5ADCE;
|
|
}
|
|
.leetcode-card .difficulty.Easy {
|
|
color: #A6D189;
|
|
}
|
|
.leetcode-card .difficulty.Medium {
|
|
color: #E5C890;
|
|
}
|
|
.leetcode-card .difficulty.Hard {
|
|
color: #E78284;
|
|
}
|
|
.leetcode-card .topics {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin: 12px 0;
|
|
}
|
|
.leetcode-card .topic-tag {
|
|
background: #CA9EE6;
|
|
color: #303446;
|
|
padding: 4px 12px;
|
|
border-radius: 16px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
.leetcode-card .topic-tag:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
.leetcode-card .premium {
|
|
color: #E78284;
|
|
font-weight: 600;
|
|
margin-top: 12px;
|
|
}
|
|
@media (max-width: 600px) {
|
|
.leetcode-card {
|
|
padding: 16px;
|
|
margin: 8px;
|
|
}
|
|
.leetcode-card h1 {
|
|
font-size: 20px;
|
|
}
|
|
.leetcode-card p {
|
|
font-size: 14px;
|
|
}
|
|
.leetcode-card .topic-tag {
|
|
font-size: 12px;
|
|
padding: 3px 10px;
|
|
}
|
|
}
|
|
</style>
|
|
<h1>
|
|
<a href="https://leetcode.com{{ .JSON.String "data.activeDailyCodingChallengeQuestion.link" }}" target="_blank">
|
|
{{ .JSON.String "data.activeDailyCodingChallengeQuestion.question.questionId" }} - {{ .JSON.String "data.activeDailyCodingChallengeQuestion.question.title" }}
|
|
</a>
|
|
</h1>
|
|
<p class="difficulty {{ .JSON.String "data.activeDailyCodingChallengeQuestion.question.difficulty" }}"><strong>Difficulty:</strong> {{ .JSON.String "data.activeDailyCodingChallengeQuestion.question.difficulty" }}</p>
|
|
<p><strong>Topics:</strong></p>
|
|
<div class="topics">
|
|
{{ if .JSON.Exists "data.activeDailyCodingChallengeQuestion.question.topicTags" }}
|
|
{{ range .JSON.Array "data.activeDailyCodingChallengeQuestion.question.topicTags" }}
|
|
<span class="topic-tag">{{ .String "name" }}</span>
|
|
{{ end }}
|
|
{{ else }}
|
|
<span class="topic-tag">None</span>
|
|
{{ end }}
|
|
</div>
|
|
{{ if .JSON.Bool "data.activeDailyCodingChallengeQuestion.question.paidOnly" }}
|
|
<p class="premium">This is a Premium question</p>
|
|
{{ end }}
|
|
</div>
|
|
|
|
- size: small
|
|
widgets:
|
|
- type: weather
|
|
location: Great Falls, Virginia, United States
|
|
units: imperial
|
|
hour-format: 12h
|
|
|
|
- type: custom-api
|
|
title: Air Quality
|
|
cache: 10m
|
|
url: https://api.waqi.info/feed/geo:38.9979;-77.2894/?token=${AIR_TOKEN}
|
|
template: |
|
|
{{ $aqi := printf "%03s" (.JSON.String "data.aqi") }}
|
|
{{ $aqiraw := .JSON.String "data.aqi" }}
|
|
{{ $updated := .JSON.String "data.time.iso" }}
|
|
{{ $humidity := .JSON.String "data.iaqi.h.v" }}
|
|
{{ $ozone := .JSON.String "data.iaqi.o3.v" }}
|
|
{{ $pm25 := .JSON.String "data.iaqi.pm25.v" }}
|
|
{{ $pressure := .JSON.String "data.iaqi.p.v" }}
|
|
|
|
<div class="flex justify-between">
|
|
<div class="size-h5">
|
|
{{ if le $aqi "050" }}
|
|
<div class="color-positive">Good air quality</div>
|
|
{{ else if le $aqi "100" }}
|
|
<div class="color-primary">Moderate air quality</div>
|
|
{{ else }}
|
|
<div class="color-negative">Bad air quality</div>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-highlight size-h2">AQI: {{ $aqiraw }}</div>
|
|
<div style="border-bottom: 1px solid; margin-block: 10px;"></div>
|
|
|
|
<div class="margin-block-2">
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
|
|
|
<div>
|
|
<div class="size-h3 color-highlight">{{ $humidity }}%</div>
|
|
<div class="size-h6">HUMIDITY</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="size-h3 color-highlight">{{ $ozone }} μg/m³</div>
|
|
<div class="size-h6">OZONE</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="size-h3 color-highlight">{{ $pm25 }} μg/m³</div>
|
|
<div class="size-h6">PM2.5</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="size-h3 color-highlight">{{ $pressure }} hPa</div>
|
|
<div class="size-h6">PRESSURE</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="size-h6" style="margin-top: 10px;">Last Updated at {{ slice $updated 11 16 }}</div>
|
|
</div>
|
|
|
|
- type: custom-api
|
|
title: GitHub Repositories
|
|
url: https://api.github.com/users/SirBlobby/repos
|
|
cache: 30m
|
|
parameters:
|
|
affiliation: owner
|
|
sort: updated
|
|
visibility: all
|
|
headers:
|
|
Accept: application/vnd.github.v3+json
|
|
User-Agent: Glance-Dashboard
|
|
template: |
|
|
<div class="github-repos">
|
|
<ul class="list list-gap-14 collapsible-container" data-collapse-after="5">
|
|
{{ range .JSON.Array "" }}
|
|
<li>
|
|
<a class="size-h3 color-primary" href="{{ .String "html_url" }}">{{ .String "full_name" }}</a>
|
|
<h3><a href="" target="_blank"></a></h3>
|
|
<p>{{ .String "description" }}</p>
|
|
<ul class="list-horizontal-text device-info">
|
|
<li data-popover-type="html"><div data-popover-html="">Last Update</div>{{ formatTime "DateOnly" ( parseTime "RFC3339" (.String "updated_at") ) }}</li>
|
|
<li data-popover-type="html"><div data-popover-html="">Visibility</div>{{ .String "visibility" }}</li>
|
|
<li data-popover-type="html"><div data-popover-html="">Stars</div>{{ .Int "stargazers_count" }}✩</li>
|
|
{{ if .String "language" }}
|
|
<li data-popover-type="html"><div data-popover-html="">Language</div>{{ .String "language" }}</li>
|
|
{{ end }}
|
|
</ul>
|
|
</li>
|
|
{{ end }}
|
|
</ul>
|
|
</div>
|
|
|
|
- name: Media
|
|
columns:
|
|
- size: small # Added new small column on the left
|
|
widgets:
|
|
- type: hacker-news # Added Hacker News widget
|
|
limit: 15
|
|
collapse-after: 5
|
|
- size: full # Existing full column is now second
|
|
widgets:
|
|
- type: custom-api
|
|
title: Epic Games
|
|
cache: 1h
|
|
url: https://store-site-backend-static.ak.epicgames.com/freeGamesPromotions?locale=en&country=US&allowCountries=US
|
|
template: |
|
|
<div style="display: flex; justify-content: center;">
|
|
{{ if eq .Response.StatusCode 200 }}
|
|
<div class="horizontal-cards" style="display: flex; flex-wrap: wrap; gap: 1rem; justify-content: center;">
|
|
{{ range .JSON.Array "data.Catalog.searchStore.elements" }}
|
|
{{ $price := .String "price.totalPrice.discountPrice" }}
|
|
{{ $hasPromo := gt (len (.Array "promotions.promotionalOffers")) 0 }}
|
|
{{ if and $hasPromo (eq $price "0") }}
|
|
{{ $gamePage := .String "productSlug" }}
|
|
{{ if gt (len (.Array "offerMappings")) 0 }}
|
|
{{ $gamePage = .String "offerMappings.0.pageSlug" }}
|
|
{{end }}
|
|
<a href="https://store.epicgames.com/en-US/p/{{ $gamePage }}" target="_blank" class="card">
|
|
{{ $title := .String "title" }}
|
|
{{ range .Array "keyImages" }}
|
|
{{ if eq (.String "type") "OfferImageWide" }}
|
|
<img src="{{ .String "url" }}" alt="{{ $title }}" style="width: 100%; max-width: 300px; height: 150px; object-fit: cover; border-radius: var(--border-radius);">
|
|
{{ end }}
|
|
{{ end }}
|
|
<div class="card-content">
|
|
<span class="size-base color-primary">{{ $title }}</span><br>
|
|
<span class="size-h5 color-subdue">
|
|
{{ if $hasPromo }}
|
|
{{ $promotions := .Array "promotionalOffers" }}
|
|
{{ if gt (len $promotions) 0 }}
|
|
{{ $firstPromo := index $promotions 0 }}
|
|
{{ $offers := $firstPromo.Array "promotionalOffers" }}
|
|
{{ if gt (len $offers) 0 }}
|
|
{{ $firstOffer := index $offers 0 }}
|
|
Free until {{ slice ($firstOffer.String "endDate") 0 10 }}
|
|
{{ else }}
|
|
Free this week!
|
|
{{ end }}
|
|
{{ else }}
|
|
Free this week!
|
|
{{ end }}
|
|
{{ end }}
|
|
</span>
|
|
</div>
|
|
</a>
|
|
{{ end }}
|
|
{{ end }}
|
|
</div>
|
|
{{ else }}
|
|
<p class="color-negative">Error fetching Epic Games data.</p>
|
|
{{ end }}
|
|
</div>
|
|
|
|
- type: videos
|
|
title: For You
|
|
style: grid-cards
|
|
collapse-after-rows: 2
|
|
channels:
|
|
- UCwWhs_6x42TyRM4Wstoq8HA
|
|
- UCLuYADJ6hESLHX87JnsGbjA
|
|
- UC-gW4TeZAlKm7UATp24JsWQ
|
|
- UCETqJYEne9Tks-eZYIrFZvg
|
|
- UCOT2iLov0V7Re7ku_3UBtcQ
|
|
- UCFe2Kq8Hg15UomoVYdmRg_Q
|
|
|
|
- type: videos
|
|
title: Engineering
|
|
style: grid-cards
|
|
collapse-after-rows: 2
|
|
channels:
|
|
- UCUyeluBRhGPCW4rPe_UvBZQ
|
|
- UCFhXFikryT4aFcLkLw2LBLA
|
|
- UCsBjURrPoezykLs9EqgamOA
|
|
- UCHnyfMqiRRG1u-2MsSQLbXA
|
|
- UC6biysICWOJ-C3P4Tyeggzg
|
|
|
|
- name: Services
|
|
columns:
|
|
- size: full
|
|
widgets:
|
|
- type: monitor
|
|
cache: 1m
|
|
title: My Services
|
|
sites:
|
|
- title: Plex
|
|
url: https://plex1.sirblob.co/web/index.html#!/
|
|
icon: si:plex
|
|
- title: Immich
|
|
url: https://immich.sirblob.co
|
|
icon: si:immich
|
|
- title: MCSManager
|
|
url: http://panel.sirblob.co/
|
|
icon: mdi:server
|
|
- title: Coder
|
|
url: https://mcbugj8flucdg.pit-1.try.coder.app
|
|
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.sirblob.co/
|
|
icon: si:nginxproxymanager
|
|
|
|
- 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
|
|
- 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: https://plex1.sirblob.co
|
|
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 "https://plex1.sirblob.co" $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: https://plex1.sirblob.co
|
|
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 "https://plex1.sirblob.co" $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 }}
|
|
|