diff --git a/docs/configuration.md b/docs/configuration.md
index ea8c76e..014aad9 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1827,14 +1827,14 @@ Preview:
| allow-insecure | bool | no | false |
| url | string | yes | |
| username | string | when service is `adguard` | |
-| password | string | when service is `adguard` | |
+| password | string | when service is `adguard` or `pihole-v6` | |
| token | string | when service is `pihole` | |
| hide-graph | bool | no | false |
| hide-top-domains | bool | no | false |
| hour-format | string | no | 12h |
##### `service`
-Either `adguard`, `pihole`, or `technitium`.
+Either `adguard`, `technitium`, or `pihole` (major version 5 and below) or `pihole-v6` (major version 6 and above).
##### `allow-insecure`
Whether to allow invalid/self-signed certificates when making the request to the service.
@@ -1846,10 +1846,14 @@ The base URL of the service.
Only required when using AdGuard Home. The username used to log into the admin dashboard.
##### `password`
-Only required when using AdGuard Home. The password used to log into the admin dashboard.
+Required when using AdGuard Home, where the password is the one used to log into the admin dashboard.
+
+Also required when using Pi-hole major version 6 and above, where the password is the one used to log into the admin dashboard or the application password, which can be found in `Settings -> Web Interface / API -> Configure app password`.
##### `token`
-Only required when using Pi-hole or Technitium. For Pi-hole, the API token which can be found in `Settings -> API -> Show API token`; for Technitium, an API token can be generated at `Administration -> Sessions -> Create Token`.
+Required when using Pi-hole major version 5 or earlier. The API token which can be found in `Settings -> API -> Show API token`.
+
+Also required when using Technitium, an API token can be generated at `Administration -> Sessions -> Create Token`.
##### `hide-graph`
Whether to hide the graph showing the number of queries over time.
diff --git a/docs/glance.yml b/docs/glance.yml
index 92b4de1..35dc7cb 100644
--- a/docs/glance.yml
+++ b/docs/glance.yml
@@ -66,9 +66,6 @@ pages:
# hide-location: true
- type: markets
- # The link to go to when clicking on the symbol in the UI,
- # {SYMBOL} will be substituded with the symbol for each market
- symbol-link-template: https://www.tradingview.com/symbols/{SYMBOL}/news
markets:
- symbol: SPY
name: S&P 500
diff --git a/internal/glance/static/main.css b/internal/glance/static/main.css
index c9c3c17..7b7b592 100644
--- a/internal/glance/static/main.css
+++ b/internal/glance/static/main.css
@@ -552,6 +552,10 @@ kbd:active {
z-index: 1;
}
+.summary::-webkit-details-marker {
+ display: none;
+}
+
.details[open] .summary {
margin-bottom: .8rem;
}
@@ -1128,7 +1132,7 @@ details[open] .summary::after {
.calendar-date {
padding: 0.4rem 0;
- color: var(--color-text-paragraph);
+ color: var(--color-text-base);
position: relative;
border-radius: var(--border-radius);
background: none;
@@ -1269,6 +1273,7 @@ details[open] .summary::after {
.dns-stats-graph-bar > .blocked {
background-color: var(--color-negative);
+ flex-basis: calc(var(--percent) - 1px);
}
.dns-stats-graph-column:nth-child(even) .dns-stats-graph-time {
diff --git a/internal/glance/templates/dns-stats.html b/internal/glance/templates/dns-stats.html
index feb90f9..bb4222c 100644
--- a/internal/glance/templates/dns-stats.html
+++ b/internal/glance/templates/dns-stats.html
@@ -59,8 +59,8 @@
{{ if ne $column.Queries $column.Blocked }}
{{ end }}
- {{ if or (gt $column.Blocked 0) (and (lt $column.PercentTotal 15) (lt $column.PercentBlocked 10)) }}
-
+ {{ if gt $column.PercentBlocked 0 }}
+
{{ end }}
{{ end }}
diff --git a/internal/glance/templates/docker-containers.html b/internal/glance/templates/docker-containers.html
index 66c79fd..aeb2f0f 100644
--- a/internal/glance/templates/docker-containers.html
+++ b/internal/glance/templates/docker-containers.html
@@ -1,10 +1,10 @@
{{ template "widget-base.html" . }}
{{- define "widget-content" }}
-
+
{{- range .Containers }}
-
-
+
+
{{ .Image }}
@@ -33,31 +33,33 @@
{{- end }}
-
+
{{ template "state-icon" .StateIcon }}
-
+
+
+
{{- else }}
No containers available to show.
{{- end }}
-
+
{{- end }}
{{- define "state-icon" }}
{{- if eq . "ok" }}
-
+
{{- else if eq . "warn" }}
-
+
{{- else if eq . "paused" }}
-
+
{{- else }}
-
+
{{- end }}
diff --git a/internal/glance/templates/videos-vertical-list.html b/internal/glance/templates/videos-vertical-list.html
index b7ea6b2..a735a74 100644
--- a/internal/glance/templates/videos-vertical-list.html
+++ b/internal/glance/templates/videos-vertical-list.html
@@ -6,7 +6,7 @@
-
{{ .Title }}
+
{{ .Title }}
diff --git a/internal/glance/utils.go b/internal/glance/utils.go
index 8455bfe..72d5a28 100644
--- a/internal/glance/utils.go
+++ b/internal/glance/utils.go
@@ -186,3 +186,7 @@ func ternary[T any](condition bool, a, b T) T {
return b
}
+
+// Having compile time errors about unused variables is cool and all, but I don't want to
+// have to constantly comment out my code while I'm working on it and testing things out
+func ItsUsedTrustMeBro(...any) {}
diff --git a/internal/glance/widget-custom-api.go b/internal/glance/widget-custom-api.go
index 32f47b4..a73dfc4 100644
--- a/internal/glance/widget-custom-api.go
+++ b/internal/glance/widget-custom-api.go
@@ -266,12 +266,12 @@ func (r *decoratedGJSONResult) String(key string) string {
return r.Get(key).String()
}
-func (r *decoratedGJSONResult) Int(key string) int64 {
+func (r *decoratedGJSONResult) Int(key string) int {
if key == "" {
- return r.Result.Int()
+ return int(r.Result.Int())
}
- return r.Get(key).Int()
+ return int(r.Get(key).Int())
}
func (r *decoratedGJSONResult) Float(key string) float64 {
@@ -292,11 +292,11 @@ func (r *decoratedGJSONResult) Bool(key string) bool {
var customAPITemplateFuncs = func() template.FuncMap {
funcs := template.FuncMap{
- "toFloat": func(a int64) float64 {
+ "toFloat": func(a int) float64 {
return float64(a)
},
- "toInt": func(a float64) int64 {
- return int64(a)
+ "toInt": func(a float64) int {
+ return int(a)
},
"add": func(a, b float64) float64 {
return a + b
diff --git a/internal/glance/widget-dns-stats.go b/internal/glance/widget-dns-stats.go
index 68b6ac2..be99294 100644
--- a/internal/glance/widget-dns-stats.go
+++ b/internal/glance/widget-dns-stats.go
@@ -1,14 +1,18 @@
package glance
import (
+ "bytes"
"context"
"encoding/json"
"errors"
+ "fmt"
"html/template"
+ "io"
"log/slog"
"net/http"
"sort"
"strings"
+ "sync"
"time"
)
@@ -23,8 +27,9 @@ const (
type dnsStatsWidget struct {
widgetBase `yaml:",inline"`
- TimeLabels [8]string `yaml:"-"`
- Stats *dnsStats `yaml:"-"`
+ TimeLabels [8]string `yaml:"-"`
+ Stats *dnsStats `yaml:"-"`
+ piholeSessionID string `yaml:"-"`
HourFormat string `yaml:"hour-format"`
HideGraph bool `yaml:"hide-graph"`
@@ -37,11 +42,18 @@ type dnsStatsWidget struct {
Password string `yaml:"password"`
}
+const (
+ dnsServiceAdguard = "adguard"
+ dnsServicePihole = "pihole"
+ dnsServiceTechnitium = "technitium"
+ dnsServicePiholeV6 = "pihole-v6"
+)
+
func makeDNSWidgetTimeLabels(format string) [8]string {
now := time.Now()
- var labels [8]string
+ var labels [dnsStatsBars]string
- for h := 24; h > 0; h -= 3 {
+ for h := dnsStatsHoursSpan; h > 0; h -= dnsStatsHoursPerBar {
labels[7-(h/3-1)] = strings.ToLower(now.Add(-time.Duration(h) * time.Hour).Format(format))
}
@@ -55,11 +67,12 @@ func (widget *dnsStatsWidget) initialize() error {
withCacheDuration(10 * time.Minute)
switch widget.Service {
- case "adguard":
- case "pihole":
- case "technitium":
+ case dnsServiceAdguard:
+ case dnsServicePiholeV6:
+ case dnsServicePihole:
+ case dnsServiceTechnitium:
default:
- return errors.New("service must be either 'adguard', 'pihole', or 'technitium'")
+ return fmt.Errorf("service must be one of: %s, %s, %s, %s", dnsServiceAdguard, dnsServicePihole, dnsServicePiholeV6, dnsServiceTechnitium)
}
return nil
@@ -70,12 +83,25 @@ func (widget *dnsStatsWidget) update(ctx context.Context) {
var err error
switch widget.Service {
- case "adguard":
+ case dnsServiceAdguard:
stats, err = fetchAdguardStats(widget.URL, widget.AllowInsecure, widget.Username, widget.Password, widget.HideGraph)
- case "pihole":
- stats, err = fetchPiholeStats(widget.URL, widget.AllowInsecure, widget.Token, widget.HideGraph)
- case "technitium":
+ case dnsServicePihole:
+ stats, err = fetchPihole5Stats(widget.URL, widget.AllowInsecure, widget.Token, widget.HideGraph)
+ case dnsServiceTechnitium:
stats, err = fetchTechnitiumStats(widget.URL, widget.AllowInsecure, widget.Token, widget.HideGraph)
+ case dnsServicePiholeV6:
+ var newSessionID string
+ stats, newSessionID, err = fetchPiholeStats(
+ widget.URL,
+ widget.AllowInsecure,
+ widget.Password,
+ widget.piholeSessionID,
+ !widget.HideGraph,
+ !widget.HideTopDomains,
+ )
+ if err == nil {
+ widget.piholeSessionID = newSessionID
+ }
}
if !widget.canContinueUpdateAfterHandlingErr(err) {
@@ -97,11 +123,11 @@ func (widget *dnsStatsWidget) Render() template.HTML {
type dnsStats struct {
TotalQueries int
- BlockedQueries int
+ BlockedQueries int // we don't actually use this anywhere in templates, maybe remove it later?
BlockedPercent int
ResponseTime int
DomainsBlocked int
- Series [8]dnsStatsSeries
+ Series [dnsStatsBars]dnsStatsSeries
TopBlockedDomains []dnsStatsBlockedDomain
}
@@ -136,13 +162,7 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor
request.SetBasicAuth(username, password)
- var client requestDoer
- if !allowInsecure {
- client = defaultHTTPClient
- } else {
- client = defaultInsecureHTTPClient
- }
-
+ var client = ternary(allowInsecure, defaultInsecureHTTPClient, defaultHTTPClient)
responseJson, err := decodeJsonFromRequest[adguardStatsResponse](client, request)
if err != nil {
return nil, err
@@ -163,7 +183,7 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor
stats.BlockedPercent = int(float64(responseJson.BlockedQueries) / float64(responseJson.TotalQueries) * 100)
- for i := 0; i < topBlockedDomainsCount; i++ {
+ for i := range topBlockedDomainsCount {
domain := responseJson.TopBlockedDomains[i]
var firstDomain string
@@ -206,11 +226,11 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor
maxQueriesInSeries := 0
- for i := 0; i < dnsStatsBars; i++ {
+ for i := range dnsStatsBars {
queries := 0
blocked := 0
- for j := 0; j < dnsStatsHoursPerBar; j++ {
+ for j := range dnsStatsHoursPerBar {
queries += queriesSeries[i*dnsStatsHoursPerBar+j]
blocked += blockedSeries[i*dnsStatsHoursPerBar+j]
}
@@ -229,35 +249,36 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor
}
}
- for i := 0; i < dnsStatsBars; i++ {
+ for i := range dnsStatsBars {
stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100)
}
return stats, nil
}
-type piholeStatsResponse struct {
- TotalQueries int `json:"dns_queries_today"`
- QueriesSeries piholeQueriesSeries `json:"domains_over_time"`
- BlockedQueries int `json:"ads_blocked_today"`
- BlockedSeries map[int64]int `json:"ads_over_time"`
- BlockedPercentage float64 `json:"ads_percentage_today"`
- TopBlockedDomains piholeTopBlockedDomains `json:"top_ads"`
- DomainsBlocked int `json:"domains_being_blocked"`
+// Legacy Pi-hole stats response (before v6)
+type pihole5StatsResponse struct {
+ TotalQueries int `json:"dns_queries_today"`
+ QueriesSeries pihole5QueriesSeries `json:"domains_over_time"`
+ BlockedQueries int `json:"ads_blocked_today"`
+ BlockedSeries map[int64]int `json:"ads_over_time"`
+ BlockedPercentage float64 `json:"ads_percentage_today"`
+ TopBlockedDomains pihole5TopBlockedDomains `json:"top_ads"`
+ DomainsBlocked int `json:"domains_being_blocked"`
}
// If the user has query logging disabled it's possible for domains_over_time to be returned as an
// empty array rather than a map which will prevent unmashalling the rest of the data so we use
// custom unmarshal behavior to fallback to an empty map.
// See https://github.com/glanceapp/glance/issues/289
-type piholeQueriesSeries map[int64]int
+type pihole5QueriesSeries map[int64]int
-func (p *piholeQueriesSeries) UnmarshalJSON(data []byte) error {
+func (p *pihole5QueriesSeries) UnmarshalJSON(data []byte) error {
temp := make(map[int64]int)
err := json.Unmarshal(data, &temp)
if err != nil {
- *p = make(piholeQueriesSeries)
+ *p = make(pihole5QueriesSeries)
} else {
*p = temp
}
@@ -267,16 +288,16 @@ func (p *piholeQueriesSeries) UnmarshalJSON(data []byte) error {
// If user has some level of privacy enabled on Pihole, `json:"top_ads"` is an empty array
// Use custom unmarshal behavior to avoid not getting the rest of the valid data when unmarshalling
-type piholeTopBlockedDomains map[string]int
+type pihole5TopBlockedDomains map[string]int
-func (p *piholeTopBlockedDomains) UnmarshalJSON(data []byte) error {
+func (p *pihole5TopBlockedDomains) UnmarshalJSON(data []byte) error {
// NOTE: do not change to piholeTopBlockedDomains type here or it will cause a stack overflow
// because of the UnmarshalJSON method getting called recursively
temp := make(map[string]int)
err := json.Unmarshal(data, &temp)
if err != nil {
- *p = make(piholeTopBlockedDomains)
+ *p = make(pihole5TopBlockedDomains)
} else {
*p = temp
}
@@ -284,7 +305,7 @@ func (p *piholeTopBlockedDomains) UnmarshalJSON(data []byte) error {
return nil
}
-func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGraph bool) (*dnsStats, error) {
+func fetchPihole5Stats(instanceURL string, allowInsecure bool, token string, noGraph bool) (*dnsStats, error) {
if token == "" {
return nil, errors.New("missing API token")
}
@@ -297,14 +318,8 @@ func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGr
return nil, err
}
- var client requestDoer
- if !allowInsecure {
- client = defaultHTTPClient
- } else {
- client = defaultInsecureHTTPClient
- }
-
- responseJson, err := decodeJsonFromRequest[piholeStatsResponse](client, request)
+ var client = ternary(allowInsecure, defaultInsecureHTTPClient, defaultHTTPClient)
+ responseJson, err := decodeJsonFromRequest[pihole5StatsResponse](client, request)
if err != nil {
return nil, err
}
@@ -348,7 +363,6 @@ func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGr
}
var lowestTimestamp int64 = 0
-
for timestamp := range responseJson.QueriesSeries {
if lowestTimestamp == 0 || timestamp < lowestTimestamp {
lowestTimestamp = timestamp
@@ -357,11 +371,11 @@ func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGr
maxQueriesInSeries := 0
- for i := 0; i < 8; i++ {
+ for i := range dnsStatsBars {
queries := 0
blocked := 0
- for j := 0; j < 18; j++ {
+ for j := range 18 {
index := lowestTimestamp + int64(i*10800+j*600)
queries += responseJson.QueriesSeries[index]
@@ -382,13 +396,287 @@ func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGr
}
}
- for i := 0; i < 8; i++ {
+ for i := range dnsStatsBars {
stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100)
}
return stats, nil
}
+func fetchPiholeStats(
+ instanceURL string,
+ allowInsecure bool,
+ password string,
+ sessionID string,
+ includeGraph bool,
+ includeTopDomains bool,
+) (*dnsStats, string, error) {
+ instanceURL = strings.TrimRight(instanceURL, "/")
+ var client = ternary(allowInsecure, defaultInsecureHTTPClient, defaultHTTPClient)
+
+ fetchNewSessionID := func() error {
+ newSessionID, err := fetchPiholeSessionID(instanceURL, client, password)
+ if err != nil {
+ return err
+ }
+ sessionID = newSessionID
+ return nil
+ }
+
+ if sessionID == "" {
+ if err := fetchNewSessionID(); err != nil {
+ slog.Error("Failed to fetch Pihole v6 session ID", "error", err)
+ return nil, "", fmt.Errorf("fetching session ID: %v", err)
+ }
+ } else {
+ isValid, err := checkPiholeSessionIDIsValid(instanceURL, client, sessionID)
+ if err != nil {
+ slog.Error("Failed to check Pihole v6 session ID validity", "error", err)
+ return nil, "", fmt.Errorf("checking session ID: %v", err)
+ }
+
+ if !isValid {
+ if err := fetchNewSessionID(); err != nil {
+ slog.Error("Failed to renew Pihole v6 session ID", "error", err)
+ return nil, "", fmt.Errorf("renewing session ID: %v", err)
+ }
+ }
+ }
+
+ var wg sync.WaitGroup
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ type statsResponseJson struct {
+ Queries struct {
+ Total int `json:"total"`
+ Blocked int `json:"blocked"`
+ PercentBlocked float64 `json:"percent_blocked"`
+ } `json:"queries"`
+ Gravity struct {
+ DomainsBlocked int `json:"domains_being_blocked"`
+ } `json:"gravity"`
+ }
+
+ statsRequest, _ := http.NewRequestWithContext(ctx, "GET", instanceURL+"/api/stats/summary", nil)
+ statsRequest.Header.Set("x-ftl-sid", sessionID)
+
+ var statsResponse statsResponseJson
+ var statsErr error
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ statsResponse, statsErr = decodeJsonFromRequest[statsResponseJson](client, statsRequest)
+ if statsErr != nil {
+ cancel()
+ }
+ }()
+
+ type seriesResponseJson struct {
+ History []struct {
+ Timestamp int64 `json:"timestamp"`
+ Total int `json:"total"`
+ Blocked int `json:"blocked"`
+ } `json:"history"`
+ }
+
+ var seriesResponse seriesResponseJson
+ var seriesErr error
+
+ if includeGraph {
+ seriesRequest, _ := http.NewRequestWithContext(ctx, "GET", instanceURL+"/api/history", nil)
+ seriesRequest.Header.Set("x-ftl-sid", sessionID)
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ seriesResponse, seriesErr = decodeJsonFromRequest[seriesResponseJson](client, seriesRequest)
+ }()
+ }
+
+ type topDomainsResponseJson struct {
+ Domains []struct {
+ Domain string `json:"domain"`
+ Count int `json:"count"`
+ } `json:"domains"`
+ TotalQueries int `json:"total_queries"`
+ BlockedQueries int `json:"blocked_queries"`
+ Took float64 `json:"took"`
+ }
+
+ var topDomainsResponse topDomainsResponseJson
+ var topDomainsErr error
+
+ if includeTopDomains {
+ topDomainsRequest, _ := http.NewRequestWithContext(ctx, "GET", instanceURL+"/api/stats/top_domains?blocked=true", nil)
+ topDomainsRequest.Header.Set("x-ftl-sid", sessionID)
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ topDomainsResponse, topDomainsErr = decodeJsonFromRequest[topDomainsResponseJson](client, topDomainsRequest)
+ }()
+ }
+
+ wg.Wait()
+ partialContent := false
+
+ if statsErr != nil {
+ return nil, "", fmt.Errorf("fetching stats: %v", statsErr)
+ }
+
+ if includeGraph && seriesErr != nil {
+ slog.Error("Failed to fetch Pihole v6 graph data", "error", seriesErr)
+ partialContent = true
+ }
+
+ if includeTopDomains && topDomainsErr != nil {
+ slog.Error("Failed to fetch Pihole v6 top domains", "error", topDomainsErr)
+ partialContent = true
+ }
+
+ stats := &dnsStats{
+ TotalQueries: statsResponse.Queries.Total,
+ BlockedQueries: statsResponse.Queries.Blocked,
+ BlockedPercent: int(statsResponse.Queries.PercentBlocked),
+ DomainsBlocked: statsResponse.Gravity.DomainsBlocked,
+ }
+
+ if includeGraph && seriesErr == nil {
+ if len(seriesResponse.History) != 145 {
+ slog.Error(
+ "Pihole v6 graph data has unexpected length",
+ "length", len(seriesResponse.History),
+ "expected", 145,
+ )
+ partialContent = true
+ } else {
+ // The API from v5 used to return 144 data points, but v6 returns 145.
+ // We only show data from the last 24 hours hours, Pihole returns data
+ // points in a 10 minute interval, 24*(60/10) = 144. Why is there an extra
+ // data point? I don't know, but we'll just ignore the first one since it's
+ // the oldest data point.
+ history := seriesResponse.History[1:]
+
+ const interval = 10
+ const dataPointsPerBar = dnsStatsHoursPerBar * (60 / interval)
+
+ maxQueriesInSeries := 0
+
+ for i := range dnsStatsBars {
+ queries := 0
+ blocked := 0
+ for j := range dataPointsPerBar {
+ index := i*dataPointsPerBar + j
+ queries += history[index].Total
+ blocked += history[index].Blocked
+ }
+ if queries > maxQueriesInSeries {
+ maxQueriesInSeries = queries
+ }
+ stats.Series[i] = dnsStatsSeries{
+ Queries: queries,
+ Blocked: blocked,
+ }
+ if queries > 0 {
+ stats.Series[i].PercentBlocked = int(float64(blocked) / float64(queries) * 100)
+ }
+ }
+
+ for i := range dnsStatsBars {
+ stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100)
+ }
+ }
+ }
+
+ if includeTopDomains && topDomainsErr == nil && len(topDomainsResponse.Domains) > 0 {
+ domains := make([]dnsStatsBlockedDomain, 0, len(topDomainsResponse.Domains))
+ for i := range topDomainsResponse.Domains {
+ d := &topDomainsResponse.Domains[i]
+ domains = append(domains, dnsStatsBlockedDomain{
+ Domain: d.Domain,
+ PercentBlocked: int(float64(d.Count) / float64(statsResponse.Queries.Blocked) * 100),
+ })
+ }
+
+ sort.Slice(domains, func(a, b int) bool {
+ return domains[a].PercentBlocked > domains[b].PercentBlocked
+ })
+ stats.TopBlockedDomains = domains[:min(len(domains), 5)]
+ }
+
+ return stats, sessionID, ternary(partialContent, errPartialContent, nil)
+}
+
+func fetchPiholeSessionID(instanceURL string, client *http.Client, password string) (string, error) {
+ requestBody := []byte(`{"password":"` + password + `"}`)
+
+ request, err := http.NewRequest("POST", instanceURL+"/api/auth", bytes.NewBuffer(requestBody))
+ if err != nil {
+ return "", fmt.Errorf("creating authentication request: %v", err)
+ }
+ request.Header.Set("Content-Type", "application/json")
+
+ response, err := client.Do(request)
+ if err != nil {
+ return "", fmt.Errorf("sending authentication request: %v", err)
+ }
+ defer response.Body.Close()
+
+ body, err := io.ReadAll(response.Body)
+ if err != nil {
+ return "", fmt.Errorf("reading authentication response: %v", err)
+ }
+
+ var jsonResponse struct {
+ Session struct {
+ SID string `json:"sid"`
+ Message string `json:"message"`
+ } `json:"session"`
+ }
+
+ if err := json.Unmarshal(body, &jsonResponse); err != nil {
+ return "", fmt.Errorf("parsing authentication response: %v", err)
+ }
+
+ if response.StatusCode != http.StatusOK {
+ return "", fmt.Errorf(
+ "authentication request returned status %s with message '%s'",
+ response.Status, jsonResponse.Session.Message,
+ )
+ }
+
+ if jsonResponse.Session.SID == "" {
+ return "", fmt.Errorf(
+ "authentication response returned empty session ID, status code %d, message '%s'",
+ response.StatusCode, jsonResponse.Session.Message,
+ )
+ }
+
+ return jsonResponse.Session.SID, nil
+}
+
+func checkPiholeSessionIDIsValid(instanceURL string, client *http.Client, sessionID string) (bool, error) {
+ request, err := http.NewRequest("GET", instanceURL+"/api/auth", nil)
+ if err != nil {
+ return false, fmt.Errorf("creating session ID check request: %v", err)
+ }
+ request.Header.Set("x-ftl-sid", sessionID)
+
+ response, err := client.Do(request)
+ if err != nil {
+ return false, err
+ }
+ defer response.Body.Close()
+
+ if response.StatusCode != http.StatusOK && response.StatusCode != http.StatusUnauthorized {
+ return false, fmt.Errorf("session ID check request returned status %s", response.Status)
+ }
+
+ return response.StatusCode == http.StatusOK, nil
+}
+
type technitiumStatsResponse struct {
Response struct {
Stats struct {
diff --git a/internal/glance/widget-videos.go b/internal/glance/widget-videos.go
index fdc654c..ff79864 100644
--- a/internal/glance/widget-videos.go
+++ b/internal/glance/widget-videos.go
@@ -56,7 +56,7 @@ func (widget *videosWidget) initialize() error {
widget.Channels = append(widget.Channels, make([]string, len(widget.Playlists))...)
for i := range widget.Playlists {
- widget.Channels[initialLen+i] = "playlist:" + widget.Playlists[i]
+ widget.Channels[initialLen+i] = videosWidgetPlaylistPrefix + widget.Playlists[i]
}
}