Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
pichouk committed Oct 6, 2023
0 parents commit 0b4aeb4
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 0 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Build and push Docker image

on:
push:
branches: ["main"]

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha
type=raw, value=latest, enable={{ is_default_branch }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM python:3.11-alpine

COPY requirements.txt /app/requirements.txt
RUN pip install -r /app/requirements.txt
COPY main.py /app/main.py

EXPOSE 8000
CMD /app/main.py
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# A simple Jellyfin prometheus exporter in Python

This project aims at providing a [Prometheus](https://prometheus.io/) exporter for the [Jellyfin](https://jellyfin.org/) streaming server.

### Configuration

You can use environment variables when starting the container:

| Variable | Value |
| ---------------- | ------------------------------------------------------------------ |
| `JELLYFIN_URL` | the URL to the Jellyfin instance (default `http://localhost:8096`) |
| `JELLYFIN_TOKEN` | Jellyfin API token |

The exporter is listenning on port 8000.
166 changes: 166 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env python3

# Imports
import os
import sys
import time
import traceback
from prometheus_client import start_http_server, Gauge, REGISTRY, PROCESS_COLLECTOR, PLATFORM_COLLECTOR, GC_COLLECTOR
import requests

# Number of seconds between 2 metrics collection
COLLECT_INTERVAL = os.getenv('EXPORTER_COLLECT_INTERVAL', 30)
# Prefix for all metrics
METRIC_NAMESPACE = os.getenv('METRIC_NAMESPACE', 'jellyfin')

# Get Jellyfin information from env
JELLYFIN_URL = os.getenv('JELLYFIN_URL', 'http://localhost:8096')
JELLYFIN_TOKEN = os.getenv('JELLYFIN_TOKEN')
if JELLYFIN_TOKEN is None:
print("JELLYFIN_TOKEN must be defined")
sys.exit(1)

# Remove unwanted Prometheus metrics
REGISTRY.unregister(GC_COLLECTOR)
REGISTRY.unregister(PLATFORM_COLLECTOR)
REGISTRY.unregister(PROCESS_COLLECTOR)

# Start Prometheus exporter server
start_http_server(8000)


#########################################
##### Initialize Prometheus metrics #####
#########################################

# User gauges
users_gauge = Gauge(f'{METRIC_NAMESPACE}_users_count', 'Count of user account')
items_gauge= Gauge(f'{METRIC_NAMESPACE}_items_count', 'Count of media items by type', ['type'])
active_streams_gauge= Gauge(f'{METRIC_NAMESPACE}_active_streams_count', 'The total number of streams', ['user'])
active_direct_streams_gauge = Gauge(f'{METRIC_NAMESPACE}_active_streams_direct_count', 'The number of streams which are currently being direct streams')
active_transcode_streams_gauge = Gauge(f'{METRIC_NAMESPACE}_active_streams_transcode_count', 'The number of streams which are currently being transcoded')
streams_bandwidth_gauge = Gauge(f'{METRIC_NAMESPACE}_streams_bandwidth_bits', 'The total bandwidth currently being streamed')


def get_users():
"""
Get all Jellyfin users from API
"""
url = JELLYFIN_URL + "/Users"
headers = {"X-Emby-Token": JELLYFIN_TOKEN}
try:
r = requests.get(url, headers=headers)
users = r.json()
except Exception:
print(traceback.format_exc())
return []
return users

def get_items():
"""
Get all Jellyfin items from API
"""
url = JELLYFIN_URL + "/Items/Counts"
headers = {"X-Emby-Token": JELLYFIN_TOKEN}
try:
r = requests.get(url, headers=headers)
items = r.json()
except Exception:
print(traceback.format_exc())
return []
return items

def get_session():
"""
Get current sessions from Jellyfin API
"""
url = JELLYFIN_URL + "/Sessions?ActiveWithinSeconds=" + str(COLLECT_INTERVAL)
headers = {"X-Emby-Token": JELLYFIN_TOKEN}
try:
r = requests.get(url, headers=headers)
sessions = r.json()
except Exception:
print(traceback.format_exc())
return []
return sessions

def get_sessions_active_count(sessions):
"""
Count active sessions per user from a list of sessions returned from Jellyfin API
"""
sessions_per_user = {}
for session in sessions:
try:
if session["PlayState"]["IsPaused"] == False and "NowPlayingItem" in session.keys():
if session["UserName"] not in sessions_per_user.keys():
sessions_per_user[session["UserName"]] = 1
else:
sessions_per_user[session["UserName"]] += 1
except Exception:
print(traceback.format_exc())
return sessions_per_user

def get_total_bandwidth(sessions):
"""
Count total bandwidth being streamed from sessions list from Jellyfin API
"""
bandwidth_total = 0.0
for session in sessions:
try:
if session["PlayState"]["IsPaused"] == False and "NowPlayingItem" in session.keys():
for stream in session["NowPlayingItem"]["MediaStreams"]:
if "BitRate" in stream.keys() and isinstance(stream["BitRate"], int):
bandwidth_total += stream["BitRate"]
except Exception:
print(traceback.format_exc())
return bandwidth_total

def get_stream_types(sessions):
"""
Count direct and transcoded streams count from sessions list
"""
transcoded = 0
direct = 0
for session in sessions:
try:
if session["PlayState"]["IsPaused"] == False and "NowPlayingItem" in session.keys():
if "TranscodingInfo" in session.keys() and (session["TranscodingInfo"]["IsVideoDirect"] or session["TranscodingInfo"]["TranscodeReasons"] is None):
direct += 1
else:
transcoded += 1
except Exception:
print(traceback.format_exc())
return direct, transcoded


def refresh_metrics():
"""
Refresh all Prometheus metrics from Jellyfin API
"""

# Get data from Jellyfin API
users = get_users()
items = get_items()
sessions = get_session()

# Process data
active_sessions = get_sessions_active_count(sessions)
bandwidth_total = get_total_bandwidth(sessions)
direct_streams, transcoded_streams = get_stream_types(sessions)

# Refresh gauges
users_gauge.set(len(users))
for item in items:
items_gauge.labels(type=item).set(items[item])
active_streams_gauge._metrics.clear()
for active_user in active_sessions:
active_streams_gauge.labels(user=active_user).set(active_sessions[active_user])
streams_bandwidth_gauge.set(bandwidth_total)
active_direct_streams_gauge.set(direct_streams)
active_transcode_streams_gauge.set(transcoded_streams)

# Loop forever
while True:
refresh_metrics()
# Wait before next metrics collection
time.sleep(COLLECT_INTERVAL)
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
requests==2.31.0
prometheus-client==0.17.1

0 comments on commit 0b4aeb4

Please sign in to comment.