Add Jellyfin media server deployment #77
16 changed files with 1036 additions and 0 deletions
|
|
@ -78,6 +78,23 @@
|
|||
no_log: true
|
||||
tags: [caddy]
|
||||
|
||||
# Jellyfin API key for metrics collection
|
||||
- name: Fetch Jellyfin API key
|
||||
ansible.builtin.command:
|
||||
cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get ceywxkcd3z7najsy2nmmbs2vke --fields credential --reveal
|
||||
delegate_to: localhost
|
||||
register: _jellyfin_metrics_api_key
|
||||
changed_when: false
|
||||
no_log: true
|
||||
check_mode: false
|
||||
tags: [jellyfin_metrics]
|
||||
|
||||
- name: Set Jellyfin API key fact
|
||||
ansible.builtin.set_fact:
|
||||
jellyfin_metrics_api_key: "{{ _jellyfin_metrics_api_key.stdout }}"
|
||||
no_log: true
|
||||
tags: [jellyfin_metrics]
|
||||
|
||||
roles:
|
||||
- role: alloy
|
||||
tags: alloy
|
||||
|
|
@ -97,5 +114,9 @@
|
|||
tags: minikube_metrics
|
||||
- role: plex_metrics
|
||||
tags: plex_metrics
|
||||
- role: jellyfin
|
||||
tags: jellyfin
|
||||
- role: jellyfin_metrics
|
||||
tags: jellyfin_metrics
|
||||
- role: caddy
|
||||
tags: caddy
|
||||
|
|
|
|||
|
|
@ -72,6 +72,12 @@ alloy_mcquack_logs:
|
|||
- path: /Users/erichblume/Library/Logs/mcquack.zot.err.log
|
||||
service: zot
|
||||
stream: stderr
|
||||
- path: /Users/erichblume/Library/Logs/mcquack.jellyfin.out.log
|
||||
service: jellyfin
|
||||
stream: stdout
|
||||
- path: /Users/erichblume/Library/Logs/mcquack.jellyfin.err.log
|
||||
service: jellyfin
|
||||
stream: stderr
|
||||
|
||||
alloy_plex_logs:
|
||||
- path: /Users/erichblume/Library/Logs/Plex Media Server/Plex Media Server.log
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ caddy_services:
|
|||
- name: registry
|
||||
host: "registry.{{ caddy_domain }}"
|
||||
backend: "http://localhost:5050"
|
||||
- name: jellyfin
|
||||
host: "jellyfin.{{ caddy_domain }}"
|
||||
backend: "http://localhost:8096"
|
||||
|
||||
# K8s services (via Tailscale Ingress)
|
||||
# Caddy proxies to existing Tailscale endpoints - traffic stays local
|
||||
|
|
|
|||
23
ansible/roles/jellyfin/defaults/main.yml
Normal file
23
ansible/roles/jellyfin/defaults/main.yml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
# Jellyfin media server configuration
|
||||
|
||||
# Port Jellyfin listens on
|
||||
jellyfin_port: 8096
|
||||
|
||||
# Data directory (standard macOS location)
|
||||
jellyfin_data_dir: "{{ ansible_env.HOME }}/Library/Application Support/jellyfin"
|
||||
|
||||
# Media path (NFS mount from sifaka)
|
||||
jellyfin_media_path: /Volumes/allisonflix
|
||||
|
||||
# Homebrew cask application path
|
||||
jellyfin_cask_app_path: /Applications/Jellyfin.app
|
||||
|
||||
# Binary path inside the cask app
|
||||
jellyfin_binary: "{{ jellyfin_cask_app_path }}/Contents/MacOS/jellyfin"
|
||||
|
||||
# Web client path (different from binary location in Homebrew cask)
|
||||
jellyfin_webdir: "{{ jellyfin_cask_app_path }}/Contents/Resources/jellyfin-web"
|
||||
|
||||
# Log directory
|
||||
jellyfin_log_dir: "{{ ansible_env.HOME }}/Library/Logs"
|
||||
6
ansible/roles/jellyfin/handlers/main.yml
Normal file
6
ansible/roles/jellyfin/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Reload jellyfin
|
||||
ansible.builtin.shell: |
|
||||
launchctl unload ~/Library/LaunchAgents/mcquack.jellyfin.plist 2>/dev/null || true
|
||||
launchctl load ~/Library/LaunchAgents/mcquack.jellyfin.plist
|
||||
changed_when: true
|
||||
30
ansible/roles/jellyfin/tasks/main.yml
Normal file
30
ansible/roles/jellyfin/tasks/main.yml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
- name: Install Jellyfin via Homebrew cask
|
||||
community.general.homebrew_cask:
|
||||
name: jellyfin
|
||||
state: present
|
||||
|
||||
- name: Ensure Jellyfin data directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ jellyfin_data_dir }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Deploy Jellyfin LaunchAgent plist
|
||||
ansible.builtin.template:
|
||||
src: mcquack.jellyfin.plist.j2
|
||||
dest: ~/Library/LaunchAgents/mcquack.jellyfin.plist
|
||||
mode: '0644'
|
||||
notify: Reload jellyfin
|
||||
|
||||
- name: Check if Jellyfin LaunchAgent is loaded
|
||||
ansible.builtin.command: launchctl list mcquack.jellyfin
|
||||
register: jellyfin_launchctl_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Load Jellyfin LaunchAgent if not loaded
|
||||
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.jellyfin.plist
|
||||
when: jellyfin_launchctl_check.rc != 0
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
33
ansible/roles/jellyfin/templates/mcquack.jellyfin.plist.j2
Normal file
33
ansible/roles/jellyfin/templates/mcquack.jellyfin.plist.j2
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- {{ ansible_managed }} -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>mcquack.jellyfin</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/bin:/usr/bin:/bin</string>
|
||||
</dict>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ jellyfin_binary }}</string>
|
||||
<string>--service</string>
|
||||
<string>--datadir</string>
|
||||
<string>{{ jellyfin_data_dir }}</string>
|
||||
<string>--webdir</string>
|
||||
<string>{{ jellyfin_webdir }}</string>
|
||||
</array>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>{{ jellyfin_data_dir }}</string>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ jellyfin_log_dir }}/mcquack.jellyfin.err.log</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ jellyfin_log_dir }}/mcquack.jellyfin.out.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
20
ansible/roles/jellyfin_metrics/defaults/main.yml
Normal file
20
ansible/roles/jellyfin_metrics/defaults/main.yml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
# Jellyfin metrics collection configuration
|
||||
|
||||
# Jellyfin server URL
|
||||
jellyfin_metrics_url: "http://localhost:8096"
|
||||
|
||||
# Path to file containing Jellyfin API key (should have 600 permissions)
|
||||
jellyfin_metrics_api_key_file: "/Users/erichblume/.jellyfin-api-key"
|
||||
|
||||
# Metrics collection interval in seconds
|
||||
jellyfin_metrics_interval: 60
|
||||
|
||||
# Output directory for prometheus textfile collector
|
||||
jellyfin_metrics_dir: /opt/homebrew/var/node_exporter/textfile
|
||||
|
||||
# Script installation path
|
||||
jellyfin_metrics_script: /Users/erichblume/.local/bin/jellyfin-metrics
|
||||
|
||||
# Log directory for metrics script output
|
||||
jellyfin_metrics_log_dir: /opt/homebrew/var/log
|
||||
6
ansible/roles/jellyfin_metrics/handlers/main.yml
Normal file
6
ansible/roles/jellyfin_metrics/handlers/main.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
- name: Reload jellyfin-metrics
|
||||
ansible.builtin.shell: |
|
||||
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.jellyfin-metrics.plist 2>/dev/null || true
|
||||
launchctl load ~/Library/LaunchAgents/mcquack.eblume.jellyfin-metrics.plist
|
||||
changed_when: true
|
||||
55
ansible/roles/jellyfin_metrics/tasks/main.yml
Normal file
55
ansible/roles/jellyfin_metrics/tasks/main.yml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
- name: Fetch Jellyfin API key (when running with --tags jellyfin_metrics)
|
||||
ansible.builtin.command:
|
||||
cmd: op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get ceywxkcd3z7najsy2nmmbs2vke --fields credential --reveal
|
||||
delegate_to: localhost
|
||||
register: jellyfin_metrics_api_key_fallback
|
||||
changed_when: false
|
||||
no_log: true
|
||||
check_mode: false
|
||||
when: jellyfin_metrics_api_key is not defined
|
||||
|
||||
- name: Set Jellyfin API key fact (fallback)
|
||||
ansible.builtin.set_fact:
|
||||
jellyfin_metrics_api_key: "{{ jellyfin_metrics_api_key_fallback.stdout }}"
|
||||
no_log: true
|
||||
when: jellyfin_metrics_api_key is not defined
|
||||
|
||||
- name: Write Jellyfin API key file
|
||||
ansible.builtin.copy:
|
||||
content: "{{ jellyfin_metrics_api_key }}"
|
||||
dest: "{{ jellyfin_metrics_api_key_file }}"
|
||||
mode: '0600'
|
||||
no_log: true
|
||||
|
||||
- name: Ensure bin directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ jellyfin_metrics_script | dirname }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Deploy jellyfin metrics collection script
|
||||
ansible.builtin.template:
|
||||
src: jellyfin-metrics.sh.j2
|
||||
dest: "{{ jellyfin_metrics_script }}"
|
||||
mode: '0755'
|
||||
notify: Reload jellyfin-metrics
|
||||
|
||||
- name: Deploy jellyfin-metrics LaunchAgent plist
|
||||
ansible.builtin.template:
|
||||
src: jellyfin-metrics.plist.j2
|
||||
dest: ~/Library/LaunchAgents/mcquack.eblume.jellyfin-metrics.plist
|
||||
mode: '0644'
|
||||
notify: Reload jellyfin-metrics
|
||||
|
||||
- name: Check if jellyfin-metrics LaunchAgent is loaded
|
||||
ansible.builtin.command: launchctl list mcquack.eblume.jellyfin-metrics
|
||||
register: jellyfin_metrics_launchctl_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Load jellyfin-metrics LaunchAgent if not loaded
|
||||
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.jellyfin-metrics.plist
|
||||
when: jellyfin_metrics_launchctl_check.rc != 0
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- {{ ansible_managed }} -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>mcquack.eblume.jellyfin-metrics</string>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/bin:/usr/bin:/bin</string>
|
||||
</dict>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ jellyfin_metrics_script }}</string>
|
||||
</array>
|
||||
<key>StartInterval</key>
|
||||
<integer>{{ jellyfin_metrics_interval }}</integer>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ jellyfin_metrics_log_dir }}/jellyfin-metrics.err.log</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ jellyfin_metrics_log_dir }}/jellyfin-metrics.out.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
137
ansible/roles/jellyfin_metrics/templates/jellyfin-metrics.sh.j2
Normal file
137
ansible/roles/jellyfin_metrics/templates/jellyfin-metrics.sh.j2
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#!/bin/bash
|
||||
# {{ ansible_managed }}
|
||||
# Collects Jellyfin Media Server metrics for node_exporter textfile collector
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
JELLYFIN_URL="{{ jellyfin_metrics_url }}"
|
||||
API_KEY_FILE="{{ jellyfin_metrics_api_key_file }}"
|
||||
OUTPUT_FILE="{{ jellyfin_metrics_dir }}/jellyfin.prom"
|
||||
TEMP_FILE="${OUTPUT_FILE}.tmp"
|
||||
|
||||
# Read API key from file
|
||||
get_api_key() {
|
||||
if [ -f "$API_KEY_FILE" ]; then
|
||||
cat "$API_KEY_FILE" | tr -d '\n'
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# Make API request with optional API key
|
||||
api_request() {
|
||||
local endpoint="$1"
|
||||
local use_auth="${2:-true}"
|
||||
local api_key
|
||||
local url="${JELLYFIN_URL}${endpoint}"
|
||||
|
||||
if [ "$use_auth" = "true" ]; then
|
||||
api_key=$(get_api_key)
|
||||
if [ -n "$api_key" ]; then
|
||||
curl -s -H "Accept: application/json" -H "X-Emby-Token: $api_key" "$url" 2>/dev/null
|
||||
else
|
||||
curl -s -H "Accept: application/json" "$url" 2>/dev/null
|
||||
fi
|
||||
else
|
||||
curl -s -H "Accept: application/json" "$url" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Initialize metrics
|
||||
jellyfin_up=0
|
||||
jellyfin_version=""
|
||||
jellyfin_sessions_total=0
|
||||
jellyfin_sessions_playing=0
|
||||
jellyfin_sessions_paused=0
|
||||
jellyfin_transcode_sessions_total=0
|
||||
|
||||
# Library metrics will be built dynamically
|
||||
library_metrics=""
|
||||
|
||||
# Check server health (no auth required)
|
||||
health=$(api_request "/health" false)
|
||||
if [ "$health" = "Healthy" ]; then
|
||||
jellyfin_up=1
|
||||
fi
|
||||
|
||||
# Get system info for version (requires auth)
|
||||
if [ "$jellyfin_up" -eq 1 ] && [ -f "$API_KEY_FILE" ]; then
|
||||
system_info=$(api_request "/System/Info")
|
||||
if [ -n "$system_info" ]; then
|
||||
jellyfin_version=$(echo "$system_info" | jq -r '.Version // ""')
|
||||
fi
|
||||
|
||||
# Get library counts (virtual folders)
|
||||
libraries=$(api_request "/Library/VirtualFolders")
|
||||
if [ -n "$libraries" ] && echo "$libraries" | jq -e '.' > /dev/null 2>&1; then
|
||||
# Process each library
|
||||
while IFS=$'\t' read -r lib_name lib_type lib_id; do
|
||||
if [ -n "$lib_name" ] && [ -n "$lib_type" ]; then
|
||||
# Get item count for this library
|
||||
# Map collection type to item type for counting
|
||||
case "$lib_type" in
|
||||
movies) item_type="Movie" ;;
|
||||
tvshows) item_type="Series" ;;
|
||||
music) item_type="MusicAlbum" ;;
|
||||
*) item_type="" ;;
|
||||
esac
|
||||
|
||||
if [ -n "$item_type" ] && [ -n "$lib_id" ]; then
|
||||
items=$(api_request "/Items?parentId=${lib_id}&recursive=true&includeItemTypes=${item_type}&limit=0")
|
||||
item_count=$(echo "$items" | jq -r '.TotalRecordCount // 0' 2>/dev/null || echo "0")
|
||||
library_metrics="${library_metrics}jellyfin_library_items{library=\"${lib_name}\",type=\"${lib_type}\"} ${item_count}
|
||||
"
|
||||
fi
|
||||
fi
|
||||
done < <(echo "$libraries" | jq -r '.[] | [.Name, .CollectionType, .ItemId] | @tsv' 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
# Get active sessions
|
||||
sessions=$(api_request "/Sessions")
|
||||
if [ -n "$sessions" ] && echo "$sessions" | jq -e '.' > /dev/null 2>&1; then
|
||||
jellyfin_sessions_total=$(echo "$sessions" | jq -r 'length')
|
||||
|
||||
# Count playing sessions (NowPlayingItem is present and IsPaused is false)
|
||||
jellyfin_sessions_playing=$(echo "$sessions" | jq -r '[.[] | select(.NowPlayingItem != null and .PlayState.IsPaused == false)] | length')
|
||||
|
||||
# Count paused sessions
|
||||
jellyfin_sessions_paused=$(echo "$sessions" | jq -r '[.[] | select(.NowPlayingItem != null and .PlayState.IsPaused == true)] | length')
|
||||
|
||||
# Count transcode sessions (TranscodingInfo is present)
|
||||
jellyfin_transcode_sessions_total=$(echo "$sessions" | jq -r '[.[] | select(.TranscodingInfo != null)] | length')
|
||||
fi
|
||||
fi
|
||||
|
||||
# Write metrics
|
||||
cat > "$TEMP_FILE" << EOF
|
||||
# HELP jellyfin_up Jellyfin Media Server is up and responding
|
||||
# TYPE jellyfin_up gauge
|
||||
jellyfin_up ${jellyfin_up}
|
||||
|
||||
# HELP jellyfin_version_info Jellyfin Media Server version information
|
||||
# TYPE jellyfin_version_info gauge
|
||||
jellyfin_version_info{version="${jellyfin_version}"} 1
|
||||
|
||||
# HELP jellyfin_sessions_total Total number of active Jellyfin sessions
|
||||
# TYPE jellyfin_sessions_total gauge
|
||||
jellyfin_sessions_total ${jellyfin_sessions_total}
|
||||
|
||||
# HELP jellyfin_sessions_playing Number of sessions currently playing
|
||||
# TYPE jellyfin_sessions_playing gauge
|
||||
jellyfin_sessions_playing ${jellyfin_sessions_playing}
|
||||
|
||||
# HELP jellyfin_sessions_paused Number of sessions currently paused
|
||||
# TYPE jellyfin_sessions_paused gauge
|
||||
jellyfin_sessions_paused ${jellyfin_sessions_paused}
|
||||
|
||||
# HELP jellyfin_transcode_sessions_total Number of sessions being transcoded
|
||||
# TYPE jellyfin_transcode_sessions_total gauge
|
||||
jellyfin_transcode_sessions_total ${jellyfin_transcode_sessions_total}
|
||||
|
||||
# HELP jellyfin_library_items Number of items in each Jellyfin library
|
||||
# TYPE jellyfin_library_items gauge
|
||||
${library_metrics}
|
||||
EOF
|
||||
|
||||
# Atomic move
|
||||
mv "$TEMP_FILE" "$OUTPUT_FILE"
|
||||
|
|
@ -0,0 +1,634 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: grafana-dashboard-jellyfin
|
||||
namespace: monitoring
|
||||
labels:
|
||||
grafana_dashboard: "1"
|
||||
data:
|
||||
jellyfin.json: |
|
||||
{
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"options": {
|
||||
"0": { "color": "red", "index": 0, "text": "DOWN" }
|
||||
},
|
||||
"type": "value"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"1": { "color": "green", "index": 1, "text": "UP" }
|
||||
},
|
||||
"type": "value"
|
||||
}
|
||||
],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "red", "value": null },
|
||||
{ "color": "green", "value": 1 }
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 0 },
|
||||
"id": 1,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "jellyfin_up",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Jellyfin Status",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [{ "color": "blue", "value": null }]
|
||||
},
|
||||
"unit": "string"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 6, "x": 4, "y": 0 },
|
||||
"id": 2,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "/^version$/",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "value"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "jellyfin_version_info",
|
||||
"format": "table",
|
||||
"instant": true,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Version",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 3 },
|
||||
{ "color": "orange", "value": 5 }
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 10, "y": 0 },
|
||||
"id": 3,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "jellyfin_sessions_total",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Active Sessions",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{ "color": "green", "value": null },
|
||||
{ "color": "yellow", "value": 1 },
|
||||
{ "color": "orange", "value": 2 }
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 4, "x": 14, "y": 0 },
|
||||
"id": 4,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "jellyfin_transcode_sessions_total",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Transcoding",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [{ "color": "purple", "value": null }]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 },
|
||||
"id": 5,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "count(jellyfin_library_items)",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Libraries",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"fixedColor": "blue",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [{ "color": "blue", "value": null }]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 8, "x": 0, "y": 4 },
|
||||
"id": 6,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "sum(jellyfin_library_items{type=\"movies\"})",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Movies",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"fixedColor": "green",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [{ "color": "green", "value": null }]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 8, "x": 8, "y": 4 },
|
||||
"id": 7,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "sum(jellyfin_library_items{type=\"tvshows\"})",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "TV Shows",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"fixedColor": "orange",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [{ "color": "orange", "value": null }]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": { "h": 4, "w": 8, "x": 16, "y": 4 },
|
||||
"id": 8,
|
||||
"options": {
|
||||
"colorMode": "value",
|
||||
"graphMode": "none",
|
||||
"justifyMode": "auto",
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": ["lastNotNull"],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "sum(jellyfin_library_items{type=\"music\"})",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Music Albums",
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [{ "color": "green", "value": null }]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Playing" },
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": { "fixedColor": "green", "mode": "fixed" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Paused" },
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": { "fixedColor": "yellow", "mode": "fixed" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
|
||||
"id": 9,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": ["mean", "max"],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "jellyfin_sessions_playing",
|
||||
"legendFormat": "Playing",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "jellyfin_sessions_paused",
|
||||
"legendFormat": "Paused",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Sessions Over Time",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "prometheus"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [{ "color": "green", "value": null }]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": { "id": "byName", "options": "Transcode Sessions" },
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": { "fixedColor": "red", "mode": "fixed" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
|
||||
"id": 10,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": ["mean", "max"],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "10.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "prometheus", "uid": "prometheus" },
|
||||
"expr": "jellyfin_transcode_sessions_total",
|
||||
"legendFormat": "Transcode Sessions",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Transcode Sessions",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "loki",
|
||||
"uid": "loki"
|
||||
},
|
||||
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 },
|
||||
"id": 11,
|
||||
"options": {
|
||||
"dedupStrategy": "none",
|
||||
"enableLogDetails": true,
|
||||
"prettifyLogMessage": false,
|
||||
"showCommonLabels": false,
|
||||
"showLabels": false,
|
||||
"showTime": true,
|
||||
"sortOrder": "Descending",
|
||||
"wrapLogMessage": false
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": { "type": "loki", "uid": "loki" },
|
||||
"expr": "{service=\"jellyfin\"}",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Jellyfin Logs",
|
||||
"type": "logs"
|
||||
}
|
||||
],
|
||||
"refresh": "30s",
|
||||
"schemaVersion": 38,
|
||||
"tags": ["jellyfin", "media"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "Jellyfin Media Server",
|
||||
"uid": "jellyfin",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ resources:
|
|||
- dashboards/configmap-macos.yaml
|
||||
- dashboards/configmap-minikube.yaml
|
||||
- dashboards/configmap-plex.yaml
|
||||
- dashboards/configmap-jellyfin.yaml
|
||||
- dashboards/configmap-postgresql.yaml
|
||||
- dashboards/configmap-services.yaml
|
||||
- dashboards/configmap-zot.yaml
|
||||
|
|
|
|||
20
argocd/manifests/homepage/external-secret-jellyfin.yaml
Normal file
20
argocd/manifests/homepage/external-secret-jellyfin.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# ExternalSecret for Jellyfin API key
|
||||
# Used by Homepage Jellyfin widget
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: homepage-jellyfin
|
||||
namespace: homepage
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: onepassword-blumeops
|
||||
target:
|
||||
name: homepage-jellyfin
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: apikey
|
||||
remoteRef:
|
||||
key: jellyfin
|
||||
property: credential
|
||||
|
|
@ -32,6 +32,11 @@ env:
|
|||
secretKeyRef:
|
||||
name: homepage-openweathermap
|
||||
key: apikey
|
||||
- name: HOMEPAGE_VAR_JELLYFIN_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: homepage-jellyfin
|
||||
key: apikey
|
||||
|
||||
config:
|
||||
# Host services (non-k8s, on indri or LAN)
|
||||
|
|
@ -77,6 +82,16 @@ config:
|
|||
query: borgmatic_repo_deduplicated_size_bytes
|
||||
format:
|
||||
type: bytes
|
||||
- Jellyfin:
|
||||
href: https://jellyfin.ops.eblu.me
|
||||
icon: jellyfin
|
||||
description: Media server
|
||||
widget:
|
||||
type: jellyfin
|
||||
url: https://jellyfin.ops.eblu.me
|
||||
key: "{{HOMEPAGE_VAR_JELLYFIN_API_KEY}}"
|
||||
enableBlocks: true
|
||||
enableNowPlaying: true
|
||||
|
||||
# External bookmarks
|
||||
bookmarks:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue