#!/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