A modern analytics dashboard for Jellyfin.
Jelana presents playback history, watch time, active users, library statistics, media profiles, recently added items, and play-count rankings in a responsive web dashboard.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
- Movie, TV series, episode, and user totals
- Playback count and watch time
- Most-played movies and TV series for 7 and 30 days
- Most-played users
- Daily watch-time chart
- Playback-method and client statistics
- Recently added media and library growth
- Storage usage by media library
- Video codec, resolution, and audio codec summaries
- Local poster proxy and cache
- Hourly JSON dashboard cache with locking and atomic writes
- Responsive interface with configurable sections
- Central configuration for branding, paths, database, cache, and timezone
- PHP 8.1 or later
- Apache, Nginx with PHP-FPM, or another PHP-capable web server
- PHP extensions:
curljsonmbstringpdo_sqlite
- Jellyfin server access
- Jellyfin Playback Reporting plugin
- Read access to the Playback Reporting SQLite database
- Read access to media paths when storage totals are enabled
.
├── .github/
│ ├── ISSUE_TEMPLATE/
│ └── PULL_REQUEST_TEMPLATE.md
├── bin/
│ └── refresh-dashboard.php
├── css/
│ └── style.css
├── inc/
│ ├── app.php
│ ├── dashboard.php
│ └── jellyfin.php
├── CHANGELOG.md
├── CONFIGURATION.md
├── CONTRIBUTING.md
├── HOURLY-CACHE-INSTALL.md
├── LICENSE
├── README.md
├── SECURITY.md
├── config.example.php
├── jelana.cron.example
├── image.php
└── index.php
config.php is excluded from version control because it contains local paths,
server details, and an API key.
A condensed step-by-step guide is available in INSTALL.md.
Clone or copy the project into the web root:
cd /var/www/html
git clone <repository-url> jelana
cd jelanaCreate the local configuration:
cp config.example.php config.phpEdit only config.php for installation-specific values:
<?php
declare(strict_types=1);
$appName = 'My Jellyfin';
$jellyfinServer = 'https://jellyfin.example.com:8920';
$apiKey = 'YOUR_API_KEY';
$brandHomeUrl = $jellyfinServer;
$brandLogoUrl = ''; // Optional logo URL
$playbackDatabase = '/var/lib/jellyfin/data/playback_reporting.db';
$mediaLibraries = [
'Movies' => '/mnt/media/movies',
'TV Series' => '/mnt/media/tv',
// 'Anime' => '/mnt/media/anime',
];
$cacheDirectory = '/var/cache/jelana';
$timezone = 'Europe/Stockholm';The array keys in $mediaLibraries become the storage labels on the dashboard.
Any number of media directories can be configured without editing application
source code.
The web server and refresh job require:
- Read access to
config.php - Read access to the Playback Reporting database
- Read access to the configured media directories
- Write access to the configured cache directory
Create a shared cache directory before running the web application or cron job:
sudo install -d -o www-data -g www-data -m 0770 /var/cache/jelana
sudo install -d -o www-data -g www-data -m 0770 /var/cache/jelana/postersA fixed path is used instead of the system temporary directory so Apache,
PHP-FPM, and cron all access the same cache even when systemd PrivateTmp is
enabled.
sudo -u www-data php bin/refresh-dashboard.phpA successful refresh prints a timestamp and elapsed time.
The dashboard cache is stored by default at:
/var/cache/jelana/dashboard.json
Poster files are stored separately at:
/var/cache/jelana/posters/
Install the included cron definition:
sudo cp jelana.cron.example /etc/cron.d/jelana
sudo chmod 644 /etc/cron.d/jelana
sudo touch /var/log/jelana.log
sudo chown www-data:www-data /var/log/jelana.logThe default schedule runs at the top of every hour:
0 * * * * www-data /usr/bin/php /var/www/html/bin/refresh-dashboard.php >> /var/log/jelana.log 2>&1See HOURLY-CACHE-INSTALL.md for verification and troubleshooting steps.
Browser
│
└── index.php
│
└── dashboard.json
Scheduled refresh
│
└── bin/refresh-dashboard.php
├── Jellyfin API
├── Playback Reporting SQLite database
└── dashboard.json
The refresh process:
- Obtains an exclusive lock.
- Collects Jellyfin and Playback Reporting data.
- Writes a temporary JSON file.
- Atomically renames the file into place.
- Releases the lock.
If the cache does not exist, the first page request builds it once.
- Never commit
config.php. - Use a dedicated Jellyfin API key and rotate it if exposed.
- Restrict filesystem permissions to the required database and media paths.
- Place the dashboard behind authentication or a trusted reverse proxy when it is accessible outside a trusted network.
- Review SECURITY.md before reporting a vulnerability.
Check every PHP file:
find . -name '*.php' -print0 | xargs -0 -n1 php -lRefresh the cache after backend changes:
sudo -u www-data php bin/refresh-dashboard.phpThe interface uses the following terms consistently:
- Playback count for the number of distinct playback sessions
- Watch time for accumulated playback duration
- TV Series for series-level media
- Recently Added for newly indexed library items
- Playback Methods for direct play, direct stream, and transcoding
- Media Profile for codec and resolution summaries
- Cached data for the prepared dashboard JSON
- Playback accuracy depends on the data recorded by the Playback Reporting plugin.
- Series rankings depend on Jellyfin metadata resolving episodes to their parent series.
- Storage totals require filesystem access to the configured media paths.
- The dashboard currently uses one configured Jellyfin server.
- The interface does not provide authentication by itself.
Potential future work:
- Configurable timezone and locale
- Optional application-level authentication
- Additional date ranges
- Exportable reports
- Automated tests
- Screenshot documentation
See CONTRIBUTING.md.
See CHANGELOG.md.
Stable versions are published through GitHub Releases and follow semantic versioning. For support requests and bug reports, include the version shown in the dashboard footer.
The first public Jelana release is v2.1.0.
jellyfin · analytics · dashboard · php · sqlite · media-server · statistics
Jelana is available under the MIT License.






