Files
Varaverk/Monitors/emby_session_report.sh
Gmer4Lfe e8b114094a Bring script headers onto the template and close safeguard gaps
Headers claimed protections the code never had, and several destructive paths had no
guard against a collapsed config value.
2026-08-01 20:37:59 -04:00

366 lines
16 KiB
Bash
Executable File

#!/bin/bash
# ==============================================================================================
# ============================= Emby Session Report ============================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Emby usage report via the Emby API. Scheduled weekly (Sunday 11am). Queries
# activity logs and session history to produce a summary of what was watched,
# by whom, and how often. No persistent state — queries fresh on every run.
#
# Reports: server info and uptime, active sessions and transcode ratio, library
# counts (movies/episodes/songs), activity history for the last EMBY_REPORT_DAYS
# days, top EMBY_REPORT_TOP_N content items, most active users, and ramdisk
# transcode status.
#
# Notifies only if transcoding exceeds 80% of streams — may indicate a client
# configuration issue. Silent on clean runs.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# 1. Reachability — Emby API responding; unreachable exits cleanly rather than
# reporting an empty library as a real result
# 2. Server info and uptime
# 3. Active sessions — count, and the transcode-to-direct-play ratio
# 4. Library counts — movies, episodes, songs
# 5. Activity history over the last EMBY_REPORT_DAYS
# 6. Top EMBY_REPORT_TOP_N items and most active users
# 7. Ramdisk transcode status, read from the shared transcode state
#
# Every figure is queried fresh. The only notification is the transcode-ratio warning;
# everything else is report output.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# No Persistent State
# Every report is generated fresh from the Emby API. No local database, no
# incremental tracking. A missed run leaves no gap — the next run simply
# covers its own window.
#
# Silent When Healthy
# The report goes to Discord/notification as a summary. Transcode alerts are
# the only proactive notification — high transcode ratios may indicate a
# misconfigured client that needs attention before it becomes a performance issue.
#
# Section Independence
# Each report section (sessions, library, activity, top content) guards its own
# API calls. A failure in one section does not abort the others — the report
# produces partial output rather than nothing.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Single Instance Lock
# acquire_lock prevents duplicate reports running simultaneously.
#
# API Connectivity Check
# check_api() verifies Emby is reachable before any queries. API failure in
# one section does not abort the others — each section guards itself.
#
# Tool Validation
# Checks for curl and jq at startup — exits with a clear error if either is missing.
#
# Per-Host Credentials
# detect_hosts() aliases HOST*_EMBY_URL and HOST*_EMBY_API_KEY → EMBY_URL / EMBY_API_KEY.
# Each server reports on its own Emby instance automatically.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# host*.conf
#
# HOST*_EMBY_URL
# Emby server URL for this host. Aliased by detect_hosts() → EMBY_URL.
#
# HOST*_EMBY_API_KEY
# Emby API key for this host. Aliased by detect_hosts() → EMBY_API_KEY.
# Generate via Emby UI → Settings → API Keys → New API Key.
#
# master.conf
#
# EMBY_REPORT_DAYS
# Number of days to include in the activity history section. (default: 7)
#
# EMBY_REPORT_TOP_N
# Number of top content items to show in the report. (default: 10)
#
# RAMDISK_PATH
# Ramdisk mount path — used for transcode status reporting.
#
# TRANSCODE_LINK
# Symlink path — used to determine current transcode location.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# emby_session_report.sh
# Generate and send Emby usage report.
#
# emby_session_report.sh --dry-run
# Test API connectivity and generate report output. No notification sent.
#
# emby_session_report.sh --status
# Show Emby URL, API key (masked), and report configuration. Then exit.
#
# emby_session_report.sh --log
# Verbose per-section output during report generation.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../load_config.sh"
parse_args "$@"
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_GEAR Setup ━━━"
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root"
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
error "curl not found — required for Emby API calls"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
error "jq not found — required for JSON parsing"
notify "Emby report failed on $(hostname) — jq not installed" "Emby Report" "warning"
exit 1
fi
acquire_lock
# detect_hosts() sets MY_ID and aliases EMBY_URL, EMBY_API_KEY
detect_hosts
require_var EMBY_URL
require_var EMBY_API_KEY
log "$ICON_GEAR Config: url=${EMBY_URL} period=${EMBY_REPORT_DAYS}d top=${EMBY_REPORT_TOP_N}"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — API queried but no notification sent"
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_EMBY Emby URL: $EMBY_URL"
echo "$ICON_TIME Period: Last ${EMBY_REPORT_DAYS} days"
echo "$ICON_EMBY Top N: ${EMBY_REPORT_TOP_N} items"
echo "$ICON_GEAR Dry Run: $DRY_RUN"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
# ==============================================================================================
# ── API HELPER ────────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
emby_api() {
local endpoint="$1"
local response http_code body
response=$(curl -sf \
--max-time 15 \
-H "X-Emby-Token: $EMBY_API_KEY" \
-w "\n%{http_code}" \
"${EMBY_URL}/${endpoint}" 2>/dev/null)
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | head -n -1)
if [[ "$http_code" != "200" ]]; then
error "Emby API HTTP $http_code for: $endpoint"
return 1
fi
echo "$body"
}
# ==============================================================================================
# ━━━ Emby Session Report ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_EMBY Emby Session Report — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
echo "$ICON_HOST $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_TIME Period: Last ${EMBY_REPORT_DAYS} days"
echo ""
START=$(date +%s)
# ── Connectivity and server info ──────────────────────────────────────────────────────────────
if ! check_api "$EMBY_URL" "Emby" 10; then
notify "Emby report failed on $(hostname) — cannot connect to Emby at $EMBY_URL" \
"Emby Report" "warning"
exit 1
fi
SYSTEM_INFO=$(emby_api "System/Info" 2>/dev/null) || {
error "Cannot connect to Emby at $EMBY_URL"
exit 1
}
SERVER_NAME=$(echo "$SYSTEM_INFO" | jq -r '.ServerName // "Unknown"' 2>/dev/null)
SERVER_VERSION=$(echo "$SYSTEM_INFO" | jq -r '.Version // "Unknown"' 2>/dev/null)
echo "$ICON_EMBY Connected to: $SERVER_NAME (v$SERVER_VERSION) ✅"
# ── Active Sessions ───────────────────────────────────────────────────────────────────────────
echo "━━━ $ICON_EMBY Active Sessions ━━━"
SESSIONS=$(emby_api "Sessions" 2>/dev/null) || { warn "Could not fetch sessions"; SESSIONS="[]"; }
ACTIVE_COUNT=$(echo "$SESSIONS" | \
jq '[.[] | select(.NowPlayingItem != null)] | length' 2>/dev/null || echo 0)
TRANSCODE_NOW=$(echo "$SESSIONS" | \
jq '[.[] | select(.NowPlayingItem != null) | select(.TranscodingInfo != null)] | length' \
2>/dev/null || echo 0)
DIRECT_NOW=$(( ACTIVE_COUNT - TRANSCODE_NOW ))
TRANSCODE_PCT=0
[[ "$ACTIVE_COUNT" -gt 0 ]] && TRANSCODE_PCT=$(( TRANSCODE_NOW * 100 / ACTIVE_COUNT ))
echo " $ICON_EMBY Active streams: $ACTIVE_COUNT"
echo " $ICON_EMBY Direct play: $DIRECT_NOW"
echo " $ICON_EMBY Transcoding: $TRANSCODE_NOW"
if [[ "$ACTIVE_COUNT" -gt 0 ]]; then
echo ""
echo " Now playing:"
echo "$SESSIONS" | jq -r '
.[] |
select(.NowPlayingItem != null) |
" \(.UserName // "Unknown") → \(.NowPlayingItem.Name // "Unknown") [\(if .TranscodingInfo != null then "transcode" else "direct" end)]"
' 2>/dev/null || true
fi
log "$ICON_EMBY Sessions: $ACTIVE_COUNT active ($DIRECT_NOW direct / $TRANSCODE_NOW transcode)"
echo ""
# ── Library Stats ─────────────────────────────────────────────────────────────────────────────
echo "━━━ $ICON_EMBY Library ━━━"
ITEMS=$(emby_api "Items/Counts" 2>/dev/null) || { warn "Could not fetch library counts"; ITEMS="{}"; }
MOVIE_COUNT=$(echo "$ITEMS" | jq '.MovieCount // 0' 2>/dev/null || echo 0)
EPISODE_COUNT=$(echo "$ITEMS" | jq '.EpisodeCount // 0' 2>/dev/null || echo 0)
SONG_COUNT=$(echo "$ITEMS" | jq '.SongCount // 0' 2>/dev/null || echo 0)
echo " $ICON_EMBY Movies: $MOVIE_COUNT"
echo " $ICON_EMBY Episodes: $EPISODE_COUNT"
echo " $ICON_EMBY Songs: $SONG_COUNT"
log "$ICON_EMBY Library: $MOVIE_COUNT movies $EPISODE_COUNT episodes $SONG_COUNT songs"
echo ""
# ── Activity History ──────────────────────────────────────────────────────────────────────────
# Query activity log for the configured period
echo "━━━ $ICON_EMBY Activity — Last ${EMBY_REPORT_DAYS} Days ━━━"
REPORT_START=$(date -d "${EMBY_REPORT_DAYS} days ago" '+%Y-%m-%dT00:00:00.000Z')
ACTIVITY=$(emby_api "System/ActivityLog/Entries?MinDate=${REPORT_START}&Limit=1000" \
2>/dev/null) || { warn "Could not fetch activity log"; ACTIVITY="{}"; }
# Emby 4.9 event types changed: VideoPlayback → playback.stop (completed session)
TOTAL_PLAYS=$(echo "$ACTIVITY" | \
jq '[.Items // [] | .[] | select(.Type == "playback.stop")] | length' \
2>/dev/null || echo 0)
echo " $ICON_EMBY Total play events: $TOTAL_PLAYS"
[[ "$TOTAL_PLAYS" -gt 0 ]] && log "$ICON_EMBY Activity: $TOTAL_PLAYS plays"
echo ""
# ── Top Content ───────────────────────────────────────────────────────────────────────────────
echo "━━━ $ICON_EMBY Top ${EMBY_REPORT_TOP_N} Content ━━━"
# Emby 4.9 requires a UserId on the Items endpoint — fetch admin ID first
EMBY_ADMIN_ID=$(emby_api "Users" 2>/dev/null | \
jq -r '[.[] | select(.Policy.IsAdministrator == true)] | first | .Id // empty' 2>/dev/null)
if [[ -z "$EMBY_ADMIN_ID" ]]; then
warn "Could not resolve admin UserId — skipping top content"
TOP_ITEMS="{}"
else
TOP_ITEMS=$(emby_api "Users/${EMBY_ADMIN_ID}/Items?SortBy=DatePlayed&SortOrder=Descending&Limit=${EMBY_REPORT_TOP_N}&Recursive=true&Fields=Overview&IncludeItemTypes=Movie,Episode" \
2>/dev/null) || { warn "Could not fetch top content"; TOP_ITEMS="{}"; }
fi
TOP_COUNT=$(echo "$TOP_ITEMS" | jq '.Items // [] | length' 2>/dev/null || echo 0)
if [[ "$TOP_COUNT" -gt 0 ]]; then
echo "$TOP_ITEMS" | jq -r '
.Items // [] |
to_entries[] |
" \(.key + 1). \(.value.Name // "Unknown") [\(.value.Type // "")]"
' 2>/dev/null || warn "Could not parse top content"
else
echo " No recent play history found"
fi
echo ""
# ── Most Active Users ─────────────────────────────────────────────────────────────────────────
echo "━━━ $ICON_EMBY Most Active Users ━━━"
USERS=$(emby_api "Users" 2>/dev/null) || { warn "Could not fetch users"; USERS="[]"; }
USER_COUNT=$(echo "$USERS" | jq 'length' 2>/dev/null || echo 0)
echo " $ICON_EMBY Total users: $USER_COUNT"
if [[ "$USER_COUNT" -gt 0 ]]; then
echo "$USERS" | jq -r '
sort_by(.LastActivityDate // "0") |
reverse |
.[:5][] |
" \(.Name // "Unknown") — last active: \(.LastActivityDate // "never" | split("T")[0])"
' 2>/dev/null || true
fi
echo ""
# ── Ramdisk / Transcode Status ────────────────────────────────────────────────────────────────
echo "━━━ $ICON_RAM Transcode Status ━━━"
if mountpoint -q "$RAMDISK_PATH" 2>/dev/null; then
RAMDISK_USED_KB=$(df "$RAMDISK_PATH" --output=used 2>/dev/null | tail -1 | tr -d ' ')
RAMDISK_USED_GB=$(kb_to_gb "$RAMDISK_USED_KB")
SYMLINK=$(readlink "$TRANSCODE_LINK" 2>/dev/null || echo "unknown")
echo " $ICON_RAM Ramdisk usage: ${RAMDISK_USED_GB}GB / ${RAMDISK_SIZE:-8G}"
echo " $ICON_LINK Symlink target: $SYMLINK"
if [[ "$SYMLINK" == *"ssd"* ]] || [[ "$SYMLINK" == *"cache"* ]]; then
warn "Transcode link pointing at SSD — ramdisk may be full"
fi
else
warn "Ramdisk not mounted at $RAMDISK_PATH"
fi
echo ""
END=$(date +%s)
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo "━━━━━ $ICON_SUMMARY EMBY REPORT SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_EMBY Server: $SERVER_NAME (v$SERVER_VERSION)"
echo "$ICON_EMBY Active: $ACTIVE_COUNT streams ($DIRECT_NOW direct / $TRANSCODE_NOW transcode, ${TRANSCODE_PCT}%)"
echo "$ICON_EMBY Library: $MOVIE_COUNT movies $EPISODE_COUNT episodes $SONG_COUNT songs"
echo "$ICON_EMBY Period: $TOTAL_PLAYS play events in last ${EMBY_REPORT_DAYS} days"
echo "$ICON_TIME Duration: $(format_duration $(( END - START )))"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Only notify on issues — high transcode rate may indicate config problem
if [[ "$DRY_RUN" == false ]]; then
if [[ "$TOTAL_PLAYS" -gt 10 && "$TRANSCODE_PCT" -gt 80 ]]; then
notify "Emby report on $(hostname) — high transcode rate: ${TRANSCODE_PCT}% of $TOTAL_PLAYS plays — check direct play config" \
"Emby Report" "warning"
fi
fi
exit 0