4 Commits

44 changed files with 32 additions and 4185 deletions
-1
View File
@@ -48,4 +48,3 @@ schema/
**/scenario.json
scenario.json
logs
build/packages/
@@ -1286,7 +1286,6 @@ namespace Emby.Server.Implementations.Library
var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
.Order()
.Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
.ToList();
}
@@ -1476,25 +1475,6 @@ namespace Emby.Server.Implementations.Library
return _itemRepository.GetItemCounts(query);
}
public async Task<ItemCounts> GetItemCountsAsync(InternalItemsQuery query, CancellationToken cancellationToken = default)
{
if (query.Recursive && !query.ParentId.IsEmpty())
{
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
SetTopParentIdsOrAncestors(query, [parent]);
}
}
if (query.User is not null)
{
AddUserToQuery(query, query.User);
}
return await _itemRepository.GetItemCountsAsync(query, cancellationToken).ConfigureAwait(false);
}
public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
{
SetTopParentIdsOrAncestors(query, parents);
@@ -53,7 +53,6 @@ namespace Emby.Server.Implementations.Library
var folders = _libraryManager.GetUserRootFolder()
.GetChildren(user, true)
.OfType<Folder>()
.OrderBy(f => f.Name)
.ToList();
var groupedFolders = new List<ICollectionFolder>();
+10 -12
View File
@@ -451,7 +451,7 @@ public class LibraryController : BaseJellyfinApiController
[HttpGet("Items/Counts")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<ItemCounts>> GetItemCounts(
public ActionResult<ItemCounts> GetItemCounts(
[FromQuery] Guid? userId,
[FromQuery] bool? isFavorite)
{
@@ -460,20 +460,18 @@ public class LibraryController : BaseJellyfinApiController
? null
: _userManager.GetUserById(userId.Value);
var query = new InternalItemsQuery(user)
var counts = new ItemCounts
{
Limit = 0,
Recursive = true,
IsVirtualItem = false,
IsFavorite = isFavorite,
DtoOptions = new DtoOptions(false)
{
EnableImages = false
}
AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite),
EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite),
MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite),
SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite),
SongCount = GetCount(BaseItemKind.Audio, user, isFavorite),
MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite),
BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite),
BookCount = GetCount(BaseItemKind.Book, user, isFavorite)
};
var counts = await _libraryManager.GetItemCountsAsync(query, HttpContext.RequestAborted).ConfigureAwait(false);
return counts;
}
@@ -450,22 +450,6 @@ public sealed class BaseItemRepository
// Apply ordering before grouping so we get the right items
dbQuery = ApplyOrder(dbQuery, filter, context);
// Optimize: Use database-level deduplication to avoid loading all items into memory
var enableGrouping = EnableGroupByPresentationUniqueKey(filter);
List<Guid> filteredIds;
if (enableGrouping && filter.GroupBySeriesPresentationUniqueKey)
{
// Use database-level grouping for TV Shows to avoid loading all episodes into memory
filteredIds = await ApplySeriesGroupingAtDatabaseLevel(dbQuery, context, cancellationToken).ConfigureAwait(false);
}
else if (enableGrouping)
{
// Use database-level grouping for movies, videos, etc. (by PresentationUniqueKey) to avoid loading duplicates into memory
filteredIds = await ApplyPresentationUniqueKeyGrouping(dbQuery, context, cancellationToken).ConfigureAwait(false);
}
else
{
// Get IDs only, without DistinctBy to avoid translation errors
var allIds = await dbQuery
.Select(e => new { e.Id, e.PresentationUniqueKey, SeriesPresentationUniqueKey = e.TvExtras!.SeriesPresentationUniqueKey })
@@ -473,8 +457,7 @@ public sealed class BaseItemRepository
.ConfigureAwait(false);
// Apply grouping/distinct in memory
filteredIds = ApplyGroupingInMemory(allIds, filter);
}
var filteredIds = ApplyGroupingInMemory(allIds, filter);
// Apply paging to IDs
var pagedIds = ApplyPagingToIds(filteredIds, filter);
@@ -519,22 +502,6 @@ public sealed class BaseItemRepository
// Apply ordering first, before grouping
dbQuery = ApplyOrder(dbQuery, filter, context);
// Optimize: Use database-level deduplication to avoid loading all items into memory
var enableGrouping = EnableGroupByPresentationUniqueKey(filter);
List<Guid> groupedIds;
if (enableGrouping && filter.GroupBySeriesPresentationUniqueKey)
{
// Use database-level grouping for TV Shows to avoid loading all episodes into memory
groupedIds = await ApplySeriesGroupingAtDatabaseLevel(dbQuery, context, cancellationToken).ConfigureAwait(false);
}
else if (enableGrouping)
{
// Use database-level grouping for movies, videos, etc. (by PresentationUniqueKey) to avoid loading duplicates into memory
groupedIds = await ApplyPresentationUniqueKeyGrouping(dbQuery, context, cancellationToken).ConfigureAwait(false);
}
else
{
// Get IDs and keys to memory WITHOUT DistinctBy (which can't be translated)
var itemsWithKeys = await dbQuery
.Select(e => new
@@ -552,8 +519,7 @@ public sealed class BaseItemRepository
}
// Apply grouping in memory
groupedIds = ApplyGroupingInMemory(itemsWithKeys, filter);
}
var groupedIds = ApplyGroupingInMemory(itemsWithKeys, filter);
// Apply paging to IDs
var pagedIds = ApplyPagingToIds(groupedIds, filter);
@@ -886,64 +852,6 @@ public sealed class BaseItemRepository
return filtered.Select(e => (Guid)idProp.GetValue(e)!).ToList();
}
/// <summary>
/// Applies series grouping at the database level using GROUP BY to avoid loading all episodes into memory.
/// This is crucial for TV Shows performance - instead of loading 10,000 episodes and deduplicating in-memory,
/// we group by SeriesPresentationUniqueKey at the database level and only load one episode per series.
/// IMPORTANT: Gets sorted IDs first to preserve sort order from the ORDER BY clause.
/// </summary>
private async Task<List<Guid>> ApplySeriesGroupingAtDatabaseLevel(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, CancellationToken cancellationToken)
{
// Get all IDs in sorted order FIRST (preserves the ORDER BY from ApplyOrder)
var sortedIds = await dbQuery
.Select(e => new { e.Id, SeriesPresentationUniqueKey = e.TvExtras!.SeriesPresentationUniqueKey })
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
// Deduplicate by SeriesPresentationUniqueKey while preserving the sorted order
var seenSeries = new HashSet<string>();
var groupedBySeriesIds = new List<Guid>();
foreach (var item in sortedIds)
{
if (seenSeries.Add(item.SeriesPresentationUniqueKey ?? string.Empty))
{
groupedBySeriesIds.Add(item.Id);
}
}
return groupedBySeriesIds;
}
/// <summary>
/// Applies presentation unique key grouping at the database level using GROUP BY to avoid loading duplicate items into memory.
/// This optimization applies to movies, music videos, and other media types that can have duplicate PresentationUniqueKey values.
/// Instead of loading all items and deduplicating in-memory, we group by PresentationUniqueKey at the database level.
/// IMPORTANT: Gets sorted IDs first to preserve sort order from the ORDER BY clause.
/// </summary>
private async Task<List<Guid>> ApplyPresentationUniqueKeyGrouping(IQueryable<BaseItemEntity> dbQuery, JellyfinDbContext context, CancellationToken cancellationToken)
{
// Get all IDs in sorted order FIRST (preserves the ORDER BY from ApplyOrder)
var sortedIds = await dbQuery
.Select(e => new { e.Id, e.PresentationUniqueKey })
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
// Deduplicate by PresentationUniqueKey while preserving the sorted order
var seenKeys = new HashSet<string>();
var groupedByPresentationKeyIds = new List<Guid>();
foreach (var item in sortedIds)
{
if (seenKeys.Add(item.PresentationUniqueKey ?? string.Empty))
{
groupedByPresentationKeyIds.Add(item.Id);
}
}
return groupedByPresentationKeyIds;
}
private List<Guid> ApplyPagingToIds(List<Guid> ids, InternalItemsQuery filter)
{
IEnumerable<Guid> paged = ids;
@@ -661,8 +661,6 @@ namespace MediaBrowser.Controller.Library
ItemCounts GetItemCounts(InternalItemsQuery query);
Task<ItemCounts> GetItemCountsAsync(InternalItemsQuery query, CancellationToken cancellationToken = default);
Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason);
BaseItem GetParentItem(Guid? parentId, Guid? userId);
-300
View File
@@ -1,300 +0,0 @@
#!/bin/bash
#############################################################################
# Jellyfin Linux Package Build Automation Script
#
# Builds both Debian (.deb) and Red Hat (.rpm) packages from the current
# publishing parameters.
#
# Usage:
# ./build-linux-packages.sh # Build both packages
# ./build-linux-packages.sh --deb-only # Build Debian only
# ./build-linux-packages.sh --rpm-only # Build Red Hat only
# ./build-linux-packages.sh --version X.Y.Z # Specify version
#
#############################################################################
set -e
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Determine project root (script can be in root or in scripts/ subdirectory)
if [ -f "$SCRIPT_DIR/SharedVersion.cs" ]; then
PROJECT_ROOT="$SCRIPT_DIR"
elif [ -f "$(dirname "$SCRIPT_DIR")/SharedVersion.cs" ]; then
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
else
echo "Error: Could not locate project root (SharedVersion.cs not found)" >&2
exit 1
fi
# Parse arguments
BUILD_DEB=true
BUILD_RPM=true
CUSTOM_VERSION=""
while [[ $# -gt 0 ]]; do
case $1 in
--deb-only)
BUILD_RPM=false
shift
;;
--rpm-only)
BUILD_DEB=false
shift
;;
--version)
CUSTOM_VERSION="$2"
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# Extract version from SharedVersion.cs (AssemblyVersion attribute)
if [ -z "$CUSTOM_VERSION" ]; then
VERSION=$(grep -oP 'AssemblyVersion\("\K[^"]+' "$PROJECT_ROOT/SharedVersion.cs" | head -1)
if [ -z "$VERSION" ]; then
echo "Error: Could not extract version from SharedVersion.cs" >&2
exit 1
fi
else
VERSION="$CUSTOM_VERSION"
fi
# Directory setup
PUBLISH_DIR="/tmp/jellyfin-publish-$$"
BUILD_OUTPUT_DIR="${PROJECT_ROOT}/build/packages"
TEMP_DIR="/tmp/jellyfin-build-$$"
mkdir -p "$TEMP_DIR"
# Log files
RESTORE_LOG="$TEMP_DIR/restore.log"
BUILD_LOG="$TEMP_DIR/build.log"
PUBLISH_LOG="$TEMP_DIR/publish.log"
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Helper functions
print_header() {
echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}${NC} Jellyfin Linux Package Builder"
echo -e "${BLUE}${NC} Version: $VERSION"
echo -e "${BLUE}${NC} Building: $([ "$BUILD_DEB" = true ] && echo -n "Debian ") $([ "$BUILD_RPM" = true ] && echo -n "Red Hat")"
echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════╝${NC}"
}
print_step() {
echo -e "\n${BLUE}[$(date +'%H:%M:%S')]${NC} $1"
}
print_success() {
echo -e "${GREEN}${NC} $1"
}
print_error() {
echo -e "${RED}${NC} $1"
}
print_info() {
echo -e "${YELLOW}${NC} $1"
}
cleanup() {
if [ -d "$PUBLISH_DIR" ]; then
rm -rf "$PUBLISH_DIR"
fi
if [ -d "$TEMP_DIR" ]; then
rm -rf "$TEMP_DIR"
fi
}
trap cleanup EXIT
# Main execution
print_header
# Step 1: Verify prerequisites
print_step "Checking prerequisites..."
if ! command -v dotnet &> /dev/null; then
print_error "dotnet CLI not found. Please install .NET SDK."
exit 1
fi
print_success ".NET SDK found"
if ! command -v fpm &> /dev/null; then
print_error "fpm not found. Installing..."
if command -v apt-get &> /dev/null; then
sudo apt-get update
sudo apt-get install -y ruby-dev
sudo gem install fpm
elif command -v dnf &> /dev/null; then
sudo dnf install -y ruby-devel
sudo gem install fpm
else
print_error "Could not install fpm. Please install manually."
exit 1
fi
fi
print_success "fpm found"
# Step 2: Publish the application
print_step "Publishing application for linux-x64..."
cd "$PROJECT_ROOT"
if ! dotnet restore Jellyfin.sln -r linux-x64 > "$RESTORE_LOG" 2>&1; then
print_error "dotnet restore failed"
if [ -s "$RESTORE_LOG" ]; then
print_error "Error output:"
tail -30 "$RESTORE_LOG"
fi
exit 1
fi
print_success "Dependencies restored"
if ! dotnet build Jellyfin.sln --configuration Release > "$BUILD_LOG" 2>&1; then
print_error "dotnet build failed"
if [ -s "$BUILD_LOG" ]; then
print_error "Error output (last 50 lines):"
tail -50 "$BUILD_LOG"
fi
exit 1
fi
print_success "Solution built in Release configuration"
if ! dotnet publish Jellyfin.Server/Jellyfin.Server.csproj \
--configuration Release \
--self-contained true \
--runtime linux-x64 \
--output "$PUBLISH_DIR" > "$PUBLISH_LOG" 2>&1; then
print_error "dotnet publish failed"
if [ -s "$PUBLISH_LOG" ]; then
print_error "Error output (last 50 lines):"
tail -50 "$PUBLISH_LOG"
fi
exit 1
fi
print_success "Published to $PUBLISH_DIR"
# Step 3: Create build output directory
mkdir -p "$BUILD_OUTPUT_DIR"
print_success "Output directory: $BUILD_OUTPUT_DIR"
# Step 4: Build Debian package
if [ "$BUILD_DEB" = true ]; then
print_step "Building Debian package..."
DEB_OUTPUT="$BUILD_OUTPUT_DIR/jellyfin-${VERSION}-amd64.deb"
DEB_LOG="$TEMP_DIR/deb.log"
if fpm \
-s dir \
-t deb \
-n jellyfin \
-v "$VERSION" \
--architecture x86_64 \
--description "Jellyfin Media Server" \
--url "https://jellyfin.org" \
--license "GPL-2.0" \
--maintainer "Jellyfin Team <jellyfin@jellyfin.org>" \
--depends "libssl3" \
--depends "libicu72" \
--depends "libfontconfig1" \
--after-install "${PROJECT_ROOT}/scripts/debian/postinst.sh" \
--before-remove "${PROJECT_ROOT}/scripts/debian/prerm.sh" \
--after-remove "${PROJECT_ROOT}/scripts/debian/postrm.sh" \
-C "$PUBLISH_DIR" \
-p "$DEB_OUTPUT" \
.=opt/jellyfin > "$DEB_LOG" 2>&1; then
print_success "Created: jellyfin-${VERSION}-amd64.deb"
DEB_SIZE=$(du -h "$DEB_OUTPUT" | cut -f1)
print_info "Size: $DEB_SIZE"
else
print_error "Failed to create Debian package"
if [ -s "$DEB_LOG" ]; then
print_error "Error output:"
tail -20 "$DEB_LOG"
fi
exit 1
fi
fi
# Step 5: Build Red Hat package
if [ "$BUILD_RPM" = true ]; then
print_step "Building Red Hat package..."
RPM_OUTPUT="$BUILD_OUTPUT_DIR/jellyfin-${VERSION}-1.x86_64.rpm"
RPM_LOG="$TEMP_DIR/rpm.log"
if fpm \
-s dir \
-t rpm \
-n jellyfin \
-v "$VERSION" \
--architecture x86_64 \
--description "Jellyfin Media Server" \
--url "https://jellyfin.org" \
--license "GPL-2.0" \
--maintainer "Jellyfin Team <jellyfin@jellyfin.org>" \
--depends "openssl-libs" \
--depends "libicu" \
--depends "fontconfig" \
--after-install "${PROJECT_ROOT}/scripts/redhat/post.sh" \
--before-remove "${PROJECT_ROOT}/scripts/redhat/preun.sh" \
--after-remove "${PROJECT_ROOT}/scripts/redhat/postun.sh" \
-C "$PUBLISH_DIR" \
-p "$RPM_OUTPUT" \
.=opt/jellyfin > "$RPM_LOG" 2>&1; then
print_success "Created: jellyfin-${VERSION}-1.x86_64.rpm"
RPM_SIZE=$(du -h "$RPM_OUTPUT" | cut -f1)
print_info "Size: $RPM_SIZE"
else
print_error "Failed to create Red Hat package"
if [ -s "$RPM_LOG" ]; then
print_error "Error output:"
tail -20 "$RPM_LOG"
fi
exit 1
fi
fi
# Summary
echo
echo -e "${BLUE}╔═══════════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}${NC} Build Complete!"
echo -e "${BLUE}╠═══════════════════════════════════════════════════════════════╣${NC}"
echo -e "${BLUE}${NC} Packages available in:"
echo -e "${BLUE}${NC} ${GREEN}$BUILD_OUTPUT_DIR${NC}"
echo -e "${BLUE}╠═══════════════════════════════════════════════════════════════╣${NC}"
if [ "$BUILD_DEB" = true ]; then
echo -e "${BLUE}${NC} ${GREEN}${NC} Debian: jellyfin-${VERSION}-amd64.deb"
fi
if [ "$BUILD_RPM" = true ]; then
echo -e "${BLUE}${NC} ${GREEN}${NC} Red Hat: jellyfin-${VERSION}-1.x86_64.rpm"
fi
echo -e "${BLUE}╠═══════════════════════════════════════════════════════════════╣${NC}"
echo -e "${BLUE}${NC} Next steps:"
echo -e "${BLUE}${NC} Debian: sudo dpkg -i jellyfin-${VERSION}-amd64.deb"
echo -e "${BLUE}${NC} Red Hat: sudo dnf install jellyfin-${VERSION}-1.x86_64.rpm"
echo -e "${BLUE}╠═══════════════════════════════════════════════════════════════╣${NC}"
echo -e "${BLUE}${NC} Build logs (if errors occurred):"
echo -e "${BLUE}${NC} $TEMP_DIR"
echo -e "${BLUE}${NC}"
echo -e "${BLUE}${NC} Log files:"
echo -e "${BLUE}${NC} restore.log - dotnet restore output"
echo -e "${BLUE}${NC} build.log - dotnet build output"
echo -e "${BLUE}${NC} publish.log - dotnet publish output"
echo -e "${BLUE}${NC} deb.log - Debian package creation output"
echo -e "${BLUE}${NC} rpm.log - Red Hat package creation output"
echo -e "${BLUE}╚═══════════════════════════════════════════════════════════════╝${NC}"
-439
View File
@@ -1,439 +0,0 @@
# Building a Debian Package
This document describes how to build a Debian (`.deb`) package from the Jellyfin project using the current publishing parameters.
## Overview
The Jellyfin project is configured to publish as a self-contained Linux application. These published files can be packaged into a Debian package for easy distribution and installation on Debian-based systems.
### Current Publishing Parameters
The project is configured with the following publishing settings (from [rebuild-solution.sh](rebuild-solution.sh)):
| Parameter | Value | Purpose |
|-----------|-------|---------|
| **Runtime** | `linux-x64` | Self-contained binary for 64-bit Linux |
| **Configuration** | `Release` | Optimized production build |
| **Self-contained** | `true` | Includes all .NET runtime dependencies |
| **Output Directory** | `/opt/jellyfin` | Standard Linux application directory |
## Prerequisites
Install the required tools on your system:
```bash
# Update package list
sudo apt-get update
# Install build tools
sudo apt-get install -y \
dotnet-sdk-11.0 \
ruby-dev \
build-essential
# Install FPM (Effing Package Manager) for easy package creation
sudo gem install fpm --no-document
```
## Method 1: Using FPM (Recommended)
FPM automates the package creation process and handles dependencies, scripts, and metadata.
### Step 1: Publish the Application
```bash
cd /home/wjones/projects/pgsql-jellyfin
# Restore dependencies
dotnet restore Jellyfin.sln -r linux-x64
# Build for Release
dotnet build Jellyfin.sln --configuration Release
# Publish as self-contained
dotnet publish Jellyfin.Server/Jellyfin.Server.csproj \
--configuration Release \
--self-contained true \
--runtime linux-x64 \
--output /tmp/jellyfin-build
```
**Expected output location**: `/tmp/jellyfin-build/`
### Step 2: Extract Version
Get the version from the SharedVersion.cs file:
```bash
# Extract version number
VERSION=$(grep -oP 'Version\s*=\s*"\K[^"]+' SharedVersion.cs)
echo "Building Jellyfin version: $VERSION"
```
### Step 3: Create the Debian Package
```bash
# Navigate to project root
cd /home/wjones/projects/pgsql-jellyfin
# Build the .deb package
fpm -s dir \
-t deb \
-n jellyfin \
-v "$VERSION" \
-C /tmp/jellyfin-build \
-p "jellyfin-${VERSION}_amd64.deb" \
--license "LICENSE" \
--vendor "Jellyfin Contributors" \
--maintainer "Your Name <your.email@example.com>" \
--description "Jellyfin Media Server - a free software media server" \
--url "https://jellyfin.org" \
--architecture x86_64 \
--depends "libssl3" \
--depends "libicu72" \
--depends "libfontconfig1" \
--depends "libc6 (>= 2.31)" \
opt/=opt/jellyfin \
etc/=etc/jellyfin
```
**Output**: `jellyfin-{VERSION}_amd64.deb`
### Step 4 (Optional): Add Service Files
To include systemd integration, create service files:
```bash
# Create directories for package contents
mkdir -p fpm-package/etc/jellyfin
mkdir -p fpm-package/usr/lib/systemd/system
mkdir -p fpm-package/opt/jellyfin
# Copy published files
cp -r /tmp/jellyfin-build/* fpm-package/opt/jellyfin/
# Create systemd service file
cat > fpm-package/usr/lib/systemd/system/jellyfin.service << 'EOF'
[Unit]
Description=Jellyfin Media Server
After=network.target
[Service]
Type=simple
User=jellyfin
Group=jellyfin
WorkingDirectory=/opt/jellyfin
ExecStart=/opt/jellyfin/jellyfin
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
# Update FPM command to include systemd service
fpm -s dir \
-t deb \
-n jellyfin \
-v "$VERSION" \
-C fpm-package \
-p "jellyfin-${VERSION}_amd64.deb" \
--after-install ./scripts/debian/postinst \
--before-remove ./scripts/debian/prerm \
--license "LICENSE" \
--description "Jellyfin Media Server" \
--url "https://jellyfin.org" \
--architecture x86_64 \
--depends "libssl3" \
--depends "libicu72" \
opt/ \
usr/
```
## Method 2: Using dpkg-deb (Manual)
For more control over the package structure, use `dpkg-deb` directly.
### Step 1-2: Same as Method 1
Publish the application (Steps 1-2 above).
### Step 3: Create Package Directory Structure
```bash
# Create control directory
mkdir -p jellyfin-pkg/DEBIAN
mkdir -p jellyfin-pkg/opt/jellyfin
mkdir -p jellyfin-pkg/etc/jellyfin
mkdir -p jellyfin-pkg/usr/lib/systemd/system
# Copy published application files
cp -r /tmp/jellyfin-build/* jellyfin-pkg/opt/jellyfin/
# Copy configuration template
cp jellyfin-setup.iss jellyfin-pkg/etc/jellyfin/jellyfin.conf.example
```
### Step 4: Create DEBIAN Control Files
Create `jellyfin-pkg/DEBIAN/control`:
```ini
Package: jellyfin
Version: 10.x.x
Architecture: amd64
Maintainer: Jellyfin Contributors <https://jellyfin.org>
Depends: libssl3, libicu72, libfontconfig1, libc6 (>= 2.31)
Homepage: https://jellyfin.org
Description: Jellyfin Media Server
Jellyfin is a free software media server application
that allows you to collect, manage, and share your
digital media (video, music, photos) anywhere.
```
Create `jellyfin-pkg/DEBIAN/postinst` (post-install script):
```bash
#!/bin/bash
set -e
# Create jellyfin user if it doesn't exist
if ! getent passwd jellyfin > /dev/null; then
useradd --system --home /var/lib/jellyfin --shell /bin/false jellyfin
fi
# Create necessary directories
mkdir -p /var/lib/jellyfin /var/log/jellyfin /etc/jellyfin
chown -R jellyfin:jellyfin /var/lib/jellyfin /var/log/jellyfin /etc/jellyfin
# Reload systemd
systemctl daemon-reload
# Start the service
systemctl enable jellyfin.service
systemctl start jellyfin.service
exit 0
```
Create `jellyfin-pkg/DEBIAN/prerm` (pre-remove script):
```bash
#!/bin/bash
set -e
# Stop the service
systemctl stop jellyfin.service || true
systemctl disable jellyfin.service || true
exit 0
```
Create `jellyfin-pkg/DEBIAN/postrm` (post-remove script):
```bash
#!/bin/bash
set -e
# Clean up user and directories (optional)
# userdel jellyfin || true
# rm -rf /var/lib/jellyfin
exit 0
```
Make scripts executable:
```bash
chmod 0755 jellyfin-pkg/DEBIAN/postinst
chmod 0755 jellyfin-pkg/DEBIAN/prerm
chmod 0755 jellyfin-pkg/DEBIAN/postrm
```
### Step 5: Build the Package
```bash
# Get version
VERSION=$(grep -oP 'Version\s*=\s*"\K[^"]+' SharedVersion.cs)
# Create the .deb package
dpkg-deb --build jellyfin-pkg "jellyfin-${VERSION}_amd64.deb"
# Verify the package
dpkg-deb --info "jellyfin-${VERSION}_amd64.deb"
```
**Output**: `jellyfin-{VERSION}_amd64.deb`
## Installation
Once you have created the `.deb` package:
```bash
# Install the package
sudo dpkg -i jellyfin-10.x.x_amd64.deb
# If dependencies are missing, install them
sudo apt-get install -f
# Check service status
sudo systemctl status jellyfin
# View logs
sudo journalctl -u jellyfin -f
```
## Verification
### Check Package Contents
```bash
# List files in the package
dpkg-deb -c jellyfin-10.x.x_amd64.deb
# View package metadata
dpkg-deb -I jellyfin-10.x.x_amd64.deb
```
### Test Installation
```bash
# Create a test environment
mkdir -p /tmp/jellyfin-test
cd /tmp/jellyfin-test
# Extract package contents
dpkg -x /path/to/jellyfin-10.x.x_amd64.deb .
dpkg -e /path/to/jellyfin-10.x.x_amd64.deb DEBIAN
# Review extracted files
ls -la
cat DEBIAN/control
```
## Distribution
### Options for Distribution
1. **Local Repository**: Host `.deb` files on a private apt repository
2. **GitHub Releases**: Upload to GitHub releases for easy download
3. **Package Repository**: Submit to Ubuntu/Debian repositories
4. **Direct Download**: Provide `.deb` file download link
### Example: Create Local APT Repository
```bash
# Create repository directory
mkdir -p /var/www/jellyfin-repo/pool/main
# Copy package
cp jellyfin-10.x.x_amd64.deb /var/www/jellyfin-repo/pool/main/
# Generate package index (requires apt-utils)
cd /var/www/jellyfin-repo
dpkg-scanpackages pool/main /dev/null | gzip > Packages.gz
# Users can then add to their apt sources:
# echo "deb file:///var/www/jellyfin-repo /" | sudo tee /etc/apt/sources.list.d/jellyfin.list
```
## Troubleshooting
### Common Issues
**Issue**: `dpkg-deb: error: unable to open jellyfin-pkg/DEBIAN/control (No such file or directory)`
**Solution**: Ensure the DEBIAN directory exists and control file is properly created:
```bash
mkdir -p jellyfin-pkg/DEBIAN
# Recreate control file with proper permissions
```
**Issue**: Package installs but service won't start
**Solution**: Check if jellyfin user exists and has proper permissions:
```bash
sudo systemctl status jellyfin
sudo journalctl -u jellyfin -n 50
sudo ls -la /opt/jellyfin
sudo ls -la /var/lib/jellyfin
```
**Issue**: Missing dependencies warning
**Solution**: Ensure all required libraries are listed in the `Depends:` field:
```bash
ldd /opt/jellyfin/jellyfin | grep "not found"
```
## Automation Script
Create `scripts/linux/build-deb-package.sh`:
```bash
#!/bin/bash
set -e
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
VERSION=$(grep -oP 'Version\s*=\s*"\K[^"]+' "$PROJECT_ROOT/SharedVersion.cs")
BUILD_DIR="/tmp/jellyfin-build-$$"
OUTPUT_DIR="$PROJECT_ROOT/dist"
echo "Building Jellyfin Debian package v$VERSION..."
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Build and publish
cd "$PROJECT_ROOT"
dotnet restore Jellyfin.sln -r linux-x64
dotnet build Jellyfin.sln --configuration Release
dotnet publish Jellyfin.Server/Jellyfin.Server.csproj \
--configuration Release \
--self-contained true \
--runtime linux-x64 \
--output "$BUILD_DIR"
# Create package
fpm -s dir \
-t deb \
-n jellyfin \
-v "$VERSION" \
-C "$BUILD_DIR" \
-p "$OUTPUT_DIR/jellyfin-${VERSION}_amd64.deb" \
--license "LICENSE" \
--description "Jellyfin Media Server" \
--url "https://jellyfin.org" \
--architecture x86_64 \
--depends "libssl3" \
--depends "libicu72" \
opt/=opt/jellyfin
# Cleanup
rm -rf "$BUILD_DIR"
echo "Package created: $OUTPUT_DIR/jellyfin-${VERSION}_amd64.deb"
dpkg-deb -I "$OUTPUT_DIR/jellyfin-${VERSION}_amd64.deb"
```
Make it executable:
```bash
chmod +x scripts/linux/build-deb-package.sh
```
Run it:
```bash
./scripts/linux/build-deb-package.sh
```
## References
- [Debian New Maintainers' Guide](https://www.debian.org/doc/manuals/maint-guide/)
- [Packaging with fpm](https://github.com/jordansissel/fpm)
- [dpkg-deb Manual](https://manpages.debian.org/dpkg-deb)
- [systemd Unit Files](https://www.freedesktop.org/software/systemd/man/systemd.unit.html)
-160
View File
@@ -1,160 +0,0 @@
# Dashboard Performance Fix - Web UI Speed Optimization
## Problem Statement
The Jellyfin web dashboard was slow when loading movie and series counts, particularly noticeable when opening the web UI or refreshing the dashboard page.
## Root Cause Analysis
The `/Items/Counts` API endpoint was **inefficient** - it was making **8 separate database queries** instead of a single batched query:
```
GET /Items/Counts
├─ Query 1: Count AlbumCount
├─ Query 2: Count EpisodeCount
├─ Query 3: Count MovieCount
├─ Query 4: Count SeriesCount (SLOW)
├─ Query 5: Count SongCount
├─ Query 6: Count MusicVideoCount
├─ Query 7: Count BoxSetCount
└─ Query 8: Count BookCount
```
## Solution Implemented
Optimized the endpoint to use an **efficient single grouped query** that was already available in the codebase but not being used.
### Changes Made
#### 1. Interface Update - [MediaBrowser.Controller/Library/ILibraryManager.cs](../MediaBrowser.Controller/Library/ILibraryManager.cs)
Added async method to the interface:
```csharp
Task<ItemCounts> GetItemCountsAsync(InternalItemsQuery query, CancellationToken cancellationToken = default);
```
#### 2. Implementation - [Emby.Server.Implementations/Library/LibraryManager.cs](../Emby.Server.Implementations/Library/LibraryManager.cs)
Added implementation that properly applies user filtering before delegating to the repository:
```csharp
public async Task<ItemCounts> GetItemCountsAsync(InternalItemsQuery query, CancellationToken cancellationToken = default)
{
if (query.Recursive && !query.ParentId.IsEmpty())
{
var parent = GetItemById(query.ParentId);
if (parent is not null)
{
SetTopParentIdsOrAncestors(query, [parent]);
}
}
if (query.User is not null)
{
AddUserToQuery(query, query.User);
}
return await _itemRepository.GetItemCountsAsync(query, cancellationToken).ConfigureAwait(false);
}
```
#### 3. API Controller - [Jellyfin.Api/Controllers/LibraryController.cs](../Jellyfin.Api/Controllers/LibraryController.cs)
Made the endpoint async to avoid blocking thread pool:
```csharp
[HttpGet("Items/Counts")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<ItemCounts>> GetItemCounts(
[FromQuery] Guid? userId,
[FromQuery] bool? isFavorite)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.IsNullOrEmpty()
? null
: _userManager.GetUserById(userId.Value);
var query = new InternalItemsQuery(user)
{
Limit = 0,
Recursive = true,
IsVirtualItem = false,
IsFavorite = isFavorite,
DtoOptions = new DtoOptions(false)
{
EnableImages = false
}
};
var counts = await _libraryManager.GetItemCountsAsync(query, HttpContext.RequestAborted).ConfigureAwait(false);
return counts;
}
```
## Performance Impact
### Query Execution
| Aspect | Before | After | Improvement |
|--------|--------|-------|-------------|
| Database Queries | 8 separate queries | 1 GroupBy query | **8x faster** |
| Network Round-trips | 8 | 1 | **8x fewer** |
| Thread Pool Blocking | Yes (GetAwaiter().GetResult()) | No (async) | **Better scalability** |
| Query Type | Individual item counts | Aggregated GroupBy | **More efficient** |
### Expected Benefits
- ✅ Dashboard loads significantly faster
- ✅ Movie and series counts appear immediately
- ✅ Reduced database load during peak usage
- ✅ Reduced thread pool contention on the server
- ✅ Better scalability for multiple concurrent users
## Technical Details
### Query Optimization
The repository already had an efficient `GetItemCountsAsync()` method at [Jellyfin.Server.Implementations/Item/BaseItemRepository.cs:949](../Jellyfin.Server.Implementations/Item/BaseItemRepository.cs#L949):
```csharp
public async Task<ItemCounts> GetItemCountsAsync(InternalItemsQuery filter, CancellationToken cancellationToken = default)
{
// Single grouped query that aggregates all counts
var counts = await dbQuery
.GroupBy(x => x.Type)
.Select(x => new { x.Key, Count = x.Count() })
.ToArrayAsync(cancellationToken)
.ConfigureAwait(false);
// Map results to ItemCounts DTO
// Returns all counts in one object
}
```
This method was not being used because:
1. The controller was calling the synchronous `GetItemCounts()` method
2. The synchronous version was calling `.GetAwaiter().GetResult()` on the async method
3. No async endpoint was available at the controller level
### Why This Works
1. **Single Database Round-trip**: The GroupBy query aggregates all item types in one query execution
2. **No Thread Pool Blocking**: The async endpoint properly uses await instead of blocking
3. **Proper Cancellation**: Passes CancellationToken through the call chain
4. **User Filtering**: Maintains proper user access control before querying
## Testing Recommendations
### Manual Testing
1. Open web UI and check dashboard loads quickly
2. Verify movie and series counts appear immediately
3. Check multiple concurrent users don't cause delays
4. Monitor server logs for query execution time
### Performance Metrics to Monitor
- Average response time for GET /Items/Counts
- Database query count during dashboard load
- Server CPU usage with multiple concurrent users
- Thread pool thread count during peak usage
## Deployment Notes
- Build solution with: `dotnet build Jellyfin.sln -c Release`
- All projects compile successfully
- No database migration needed
- Backward compatible - no breaking changes
- Can be deployed as-is without any configuration changes
## Related Documentation
- [Query Flow Analysis](./ANALYSIS_SUMMARY.md) - For general query optimization details
- [Database Schema](./DATABASE_SCHEMA_CREATION.md) - For index recommendations
- [Query Optimization Guide](./database-query-optimization.md) - For additional optimization strategies
-665
View File
@@ -1,665 +0,0 @@
# Linux Package Build Guide
Complete documentation for building Debian (`.deb`) and Red Hat (`.rpm`) packages for Jellyfin using the current publishing parameters.
---
## Table of Contents
1. [Common Publishing Setup](#common-publishing-setup)
2. [Debian Package Build](#debian-package-build)
3. [Red Hat Package Build](#red-hat-package-build)
4. [Build Scripts](#build-scripts)
5. [Testing & Verification](#testing--verification)
---
## Common Publishing Setup
All package formats use the same .NET publishing parameters:
- **Runtime**: `linux-x64`
- **Configuration**: `Release`
- **Self-contained**: `true` (includes .NET runtime)
- **Output Directory**: `/opt/jellyfin` (standard FHS location)
### Prerequisites (All Distributions)
```bash
# Required for building
- .NET SDK (for publishing)
- Build tools (gcc, make, etc.)
# Package-specific requirements
Debian: ruby-dev, gem, fpm
Red Hat: rpm-build, ruby-devel, gem, fpm
```
### Step 1: Publish the Application
This step is identical for both Debian and Red Hat:
```bash
cd /home/wjones/projects/pgsql-jellyfin
# Restore dependencies
dotnet restore Jellyfin.sln -r linux-x64
# Build the solution
dotnet build Jellyfin.sln --configuration Release
# Publish for Linux x64 (self-contained)
dotnet publish Jellyfin.Server/Jellyfin.Server.csproj \
--configuration Release \
--self-contained true \
--runtime linux-x64 \
--output /tmp/jellyfin-publish
```
The published artifacts will be in `/tmp/jellyfin-publish/`.
---
## Debian Package Build
### Method 1: Using FPM (Recommended - Quickest)
FPM handles all the Debian metadata automatically.
#### Prerequisites
```bash
sudo apt-get install ruby-dev
sudo gem install fpm
```
#### Build the Package
```bash
#!/bin/bash
set -e
# Configuration
VERSION="11.0.0" # Get from SharedVersion.cs
PUBLISH_DIR="/tmp/jellyfin-publish"
PACKAGE_OUTPUT="jellyfin-${VERSION}-amd64.deb"
# Build FPM package
fpm \
-s dir \
-t deb \
-n jellyfin \
-v "$VERSION" \
--architecture x86_64 \
--description "Jellyfin Media Server - Free Software Media System" \
--url "https://jellyfin.org" \
--license "GPL-2.0" \
--maintainer "Jellyfin Team <jellyfin@jellyfin.org>" \
--depends "libssl3" \
--depends "libicu72" \
--depends "libfontconfig1" \
--after-install scripts/debian/postinst.sh \
--before-remove scripts/debian/prerm.sh \
--after-remove scripts/debian/postrm.sh \
-C "$PUBLISH_DIR" \
-p "$PACKAGE_OUTPUT" \
opt/=opt/jellyfin
echo "✓ Package created: $PACKAGE_OUTPUT"
```
### Method 2: Manual dpkg-deb (Complete Control)
For custom Debian configurations.
#### Create Package Structure
```bash
#!/bin/bash
set -e
VERSION="11.0.0"
PUBLISH_DIR="/tmp/jellyfin-publish"
PKG_DIR="jellyfin-${VERSION}-deb"
# Create directory structure
mkdir -p "$PKG_DIR/DEBIAN"
mkdir -p "$PKG_DIR/opt/jellyfin"
mkdir -p "$PKG_DIR/etc/systemd/system"
mkdir -p "$PKG_DIR/etc/jellyfin"
mkdir -p "$PKG_DIR/var/lib/jellyfin"
mkdir -p "$PKG_DIR/var/log/jellyfin"
# Copy application files
cp -r "$PUBLISH_DIR"/* "$PKG_DIR/opt/jellyfin/"
# Create control file
cat > "$PKG_DIR/DEBIAN/control" << 'EOF'
Package: jellyfin
Version: VERSION_PLACEHOLDER
Architecture: amd64
Installed-Size: $(du -s "$PKG_DIR" | cut -f1)
Depends: libssl3, libicu72, libfontconfig1
Recommends: ffmpeg
Maintainer: Jellyfin Team <jellyfin@jellyfin.org>
Homepage: https://jellyfin.org
Description: Jellyfin Media Server
Jellyfin is a Free Software Media System that puts you in control of managing
and streaming your media. Run the Jellyfin server and access it from a web
browser, mobile app, media player, or other client application.
EOF
sed -i "s/VERSION_PLACEHOLDER/$VERSION/g" "$PKG_DIR/DEBIAN/control"
# Create package
dpkg-deb --build "$PKG_DIR" "jellyfin-${VERSION}-amd64.deb"
```
#### Post-Install Script (`scripts/debian/postinst.sh`)
```bash
#!/bin/bash
set -e
# Create jellyfin user/group
if ! id "jellyfin" &>/dev/null; then
useradd -r -s /bin/false -d /var/lib/jellyfin jellyfin
fi
# Set permissions
chown -R jellyfin:jellyfin /opt/jellyfin
chown -R jellyfin:jellyfin /var/lib/jellyfin
chown -R jellyfin:jellyfin /var/log/jellyfin
# Set directory permissions
chmod 755 /opt/jellyfin
chmod 750 /var/lib/jellyfin
chmod 750 /var/log/jellyfin
# Enable and start service (if systemd is available)
if command -v systemctl &> /dev/null; then
systemctl daemon-reload || true
systemctl enable jellyfin || true
systemctl start jellyfin || true
fi
```
#### Pre-Remove Script (`scripts/debian/prerm.sh`)
```bash
#!/bin/bash
set -e
# Stop service
if command -v systemctl &> /dev/null; then
systemctl stop jellyfin || true
systemctl disable jellyfin || true
fi
```
#### Post-Remove Script (`scripts/debian/postrm.sh`)
```bash
#!/bin/bash
# Remove user/group
userdel jellyfin || true
```
#### Systemd Service File (`scripts/debian/jellyfin.service`)
Create `/etc/systemd/system/jellyfin.service`:
```ini
[Unit]
Description=Jellyfin Media Server
After=network.target
[Service]
Type=simple
User=jellyfin
Group=jellyfin
WorkingDirectory=/var/lib/jellyfin
ExecStart=/opt/jellyfin/jellyfin \
--datadir=/var/lib/jellyfin \
--logdir=/var/log/jellyfin
Restart=on-failure
RestartSec=5s
# Resource limits
LimitNOFILE=65536
MemoryMax=4G
[Install]
WantedBy=multi-user.target
EOF
```
### Install Debian Package
```bash
# Install
sudo dpkg -i jellyfin-11.0.0-amd64.deb
sudo apt-get install -f # Fix any dependency issues
# Verify
sudo systemctl status jellyfin
sudo journalctl -u jellyfin -n 50
```
---
## Red Hat Package Build
### Method 1: Using FPM (Recommended - Cross-Compatible)
FPM works on any Linux distribution to create RPM packages.
#### Prerequisites
```bash
# On Debian/Ubuntu (cross-build)
sudo apt-get install ruby-dev
sudo gem install fpm
# OR on Red Hat/CentOS/Fedora (native)
sudo dnf install ruby-devel
sudo gem install fpm
```
#### Build the Package
```bash
#!/bin/bash
set -e
# Configuration
VERSION="11.0.0"
PUBLISH_DIR="/tmp/jellyfin-publish"
PACKAGE_OUTPUT="jellyfin-${VERSION}-1.x86_64.rpm"
# Build FPM package
fpm \
-s dir \
-t rpm \
-n jellyfin \
-v "$VERSION" \
--architecture x86_64 \
--description "Jellyfin Media Server - Free Software Media System" \
--url "https://jellyfin.org" \
--license "GPL-2.0" \
--maintainer "Jellyfin Team <jellyfin@jellyfin.org>" \
--depends "openssl-libs" \
--depends "libicu" \
--depends "fontconfig" \
--depends "systemd" \
--after-install scripts/redhat/post.sh \
--before-remove scripts/redhat/preun.sh \
--after-remove scripts/redhat/postun.sh \
-C "$PUBLISH_DIR" \
-p "$PACKAGE_OUTPUT" \
opt/=opt/jellyfin
echo "✓ Package created: $PACKAGE_OUTPUT"
```
### Method 2: Using rpmbuild (Red Hat Native)
For building on Red Hat systems with native tooling.
#### Create SPEC File (`jellyfin.spec`)
```spec
Name: jellyfin
Version: 11.0.0
Release: 1%{?dist}
Summary: Jellyfin Media Server
License: GPL-2.0
URL: https://jellyfin.org
Source0: %{name}-%{version}.tar.gz
BuildRequires: rpm-build
Requires: openssl-libs libicu fontconfig systemd
%description
Jellyfin is a Free Software Media System that puts you in control of managing
and streaming your media. Run the Jellyfin server and access it from a web
browser, mobile app, media player, or other client application.
%prep
%setup -q
%build
# No build needed - we're packaging pre-built artifacts
%install
mkdir -p %{buildroot}/opt/jellyfin
cp -r * %{buildroot}/opt/jellyfin/
mkdir -p %{buildroot}/etc/systemd/system
mkdir -p %{buildroot}/var/lib/jellyfin
mkdir -p %{buildroot}/var/log/jellyfin
%pre
# Create jellyfin user/group
if ! id "jellyfin" &>/dev/null; then
useradd -r -s /bin/false -d /var/lib/jellyfin jellyfin
fi
%post
# Set permissions
chown -R jellyfin:jellyfin /opt/jellyfin
chown -R jellyfin:jellyfin /var/lib/jellyfin
chown -R jellyfin:jellyfin /var/log/jellyfin
chmod 755 /opt/jellyfin
chmod 750 /var/lib/jellyfin
chmod 750 /var/log/jellyfin
# Enable and start service
systemctl daemon-reload || true
systemctl enable jellyfin || true
systemctl start jellyfin || true
%preun
# Stop service before uninstall
systemctl stop jellyfin || true
systemctl disable jellyfin || true
%postun
# Clean up
userdel jellyfin || true
%files
/opt/jellyfin
%config(noreplace) /etc/jellyfin
%dir /var/lib/jellyfin
%dir /var/log/jellyfin
%changelog
* Sun Jul 13 2026 Jellyfin Team <jellyfin@jellyfin.org> - 11.0.0-1
- Initial release
```
#### Build RPM with rpmbuild
```bash
#!/bin/bash
set -e
VERSION="11.0.0"
PUBLISH_DIR="/tmp/jellyfin-publish"
# Create tarball of published artifacts
tar -czf /tmp/jellyfin-${VERSION}.tar.gz -C "$PUBLISH_DIR" .
# Set up rpmbuild structure
mkdir -p ~/rpmbuild/{SOURCES,SPECS,BUILD,RPMS,SRPMS}
cp /tmp/jellyfin-${VERSION}.tar.gz ~/rpmbuild/SOURCES/
cp jellyfin.spec ~/rpmbuild/SPECS/
# Build the RPM
rpmbuild -ba ~/rpmbuild/SPECS/jellyfin.spec
# Package location
echo "✓ Package created: ~/rpmbuild/RPMS/x86_64/jellyfin-${VERSION}-1.x86_64.rpm"
```
### Red Hat Service Files (`scripts/redhat/`)
#### Post-Install Script (`post.sh`)
```bash
#!/bin/bash
set -e
# Create jellyfin user/group
if ! id "jellyfin" &>/dev/null; then
useradd -r -s /bin/false -d /var/lib/jellyfin jellyfin
fi
# Set permissions
chown -R jellyfin:jellyfin /opt/jellyfin
chown -R jellyfin:jellyfin /var/lib/jellyfin
chown -R jellyfin:jellyfin /var/log/jellyfin
chmod 755 /opt/jellyfin
chmod 750 /var/lib/jellyfin
chmod 750 /var/log/jellyfin
# Enable and start service
systemctl daemon-reload || true
systemctl enable jellyfin || true
systemctl start jellyfin || true
```
#### Pre-Uninstall Script (`preun.sh`)
```bash
#!/bin/bash
set -e
# Stop service
systemctl stop jellyfin || true
systemctl disable jellyfin || true
```
#### Post-Uninstall Script (`postun.sh`)
```bash
#!/bin/bash
# Clean up user/group
userdel jellyfin || true
```
#### Systemd Service File (`jellyfin.service`)
```ini
[Unit]
Description=Jellyfin Media Server
After=network.target
[Service]
Type=simple
User=jellyfin
Group=jellyfin
WorkingDirectory=/var/lib/jellyfin
ExecStart=/opt/jellyfin/jellyfin \
--datadir=/var/lib/jellyfin \
--logdir=/var/log/jellyfin
Restart=on-failure
RestartSec=5s
# Resource limits
LimitNOFILE=65536
MemoryMax=4G
[Install]
WantedBy=multi-user.target
```
### Install Red Hat Package
```bash
# Install
sudo dnf install jellyfin-11.0.0-1.x86_64.rpm
# Verify
sudo systemctl status jellyfin
sudo journalctl -u jellyfin -n 50
```
---
## Build Scripts
### Complete Build Automation (`build-linux-packages.sh`)
Cross-distribution build script that creates both Debian and Red Hat packages:
```bash
#!/bin/bash
set -e
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
VERSION=$(grep -oP 'Version\s*=\s*"\K[^"]+' "$PROJECT_ROOT/SharedVersion.cs")
PUBLISH_DIR="/tmp/jellyfin-publish-$$"
BUILD_OUTPUT_DIR="${PROJECT_ROOT}/build/packages"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ Jellyfin Linux Package Builder ║"
echo "║ Version: $VERSION"
echo "╚═══════════════════════════════════════════════════════════════╝"
echo
# Step 1: Publish the application
echo "[1/4] Publishing application..."
cd "$PROJECT_ROOT"
dotnet restore Jellyfin.sln -r linux-x64 > /dev/null
dotnet build Jellyfin.sln --configuration Release > /dev/null
dotnet publish Jellyfin.Server/Jellyfin.Server.csproj \
--configuration Release \
--self-contained true \
--runtime linux-x64 \
--output "$PUBLISH_DIR" > /dev/null
echo " ✓ Published to $PUBLISH_DIR"
echo
# Step 2: Create build output directory
mkdir -p "$BUILD_OUTPUT_DIR"
# Step 3: Build Debian package
if command -v fpm &> /dev/null; then
echo "[2/4] Building Debian package..."
fpm \
-s dir \
-t deb \
-n jellyfin \
-v "$VERSION" \
--architecture x86_64 \
--description "Jellyfin Media Server" \
--url "https://jellyfin.org" \
--license "GPL-2.0" \
--maintainer "Jellyfin Team" \
--depends "libssl3" \
--depends "libicu72" \
--depends "libfontconfig1" \
--after-install "${PROJECT_ROOT}/scripts/debian/postinst.sh" \
--before-remove "${PROJECT_ROOT}/scripts/debian/prerm.sh" \
--after-remove "${PROJECT_ROOT}/scripts/debian/postrm.sh" \
-C "$PUBLISH_DIR" \
-p "$BUILD_OUTPUT_DIR/jellyfin-${VERSION}-amd64.deb" \
opt/=opt/jellyfin
echo " ✓ Created: jellyfin-${VERSION}-amd64.deb"
else
echo " ⚠ fpm not found - skipping Debian package"
fi
echo
# Step 4: Build Red Hat package
echo "[3/4] Building Red Hat package..."
fpm \
-s dir \
-t rpm \
-n jellyfin \
-v "$VERSION" \
--architecture x86_64 \
--description "Jellyfin Media Server" \
--url "https://jellyfin.org" \
--license "GPL-2.0" \
--maintainer "Jellyfin Team" \
--depends "openssl-libs" \
--depends "libicu" \
--depends "fontconfig" \
--after-install "${PROJECT_ROOT}/scripts/redhat/post.sh" \
--before-remove "${PROJECT_ROOT}/scripts/redhat/preun.sh" \
--after-remove "${PROJECT_ROOT}/scripts/redhat/postun.sh" \
-C "$PUBLISH_DIR" \
-p "$BUILD_OUTPUT_DIR/jellyfin-${VERSION}-1.x86_64.rpm" \
opt/=opt/jellyfin
echo " ✓ Created: jellyfin-${VERSION}-1.x86_64.rpm"
echo
# Cleanup
rm -rf "$PUBLISH_DIR"
# Summary
echo "[4/4] Build Complete!"
echo "╔═══════════════════════════════════════════════════════════════╗"
echo "║ Packages available in: $BUILD_OUTPUT_DIR"
echo "╠═══════════════════════════════════════════════════════════════╣"
echo "║ Debian: jellyfin-${VERSION}-amd64.deb"
echo "║ Red Hat: jellyfin-${VERSION}-1.x86_64.rpm"
echo "╚═══════════════════════════════════════════════════════════════╝"
```
---
## Testing & Verification
### Debian Testing
```bash
# Inspect package contents
dpkg -c jellyfin-11.0.0-amd64.deb
# Extract to temporary location
dpkg-deb -x jellyfin-11.0.0-amd64.deb /tmp/jellyfin-test
# Verify install scripts
dpkg-deb -e jellyfin-11.0.0-amd64.deb /tmp/jellyfin-control
# Install and test
sudo dpkg -i jellyfin-11.0.0-amd64.deb
sudo systemctl status jellyfin
```
### Red Hat Testing
```bash
# Inspect package contents
rpm -qpl jellyfin-11.0.0-1.x86_64.rpm
# Extract to temporary location
rpm2cpio jellyfin-11.0.0-1.x86_64.rpm | cpio -idmv
# Verify scripts
rpm -qp --scripts jellyfin-11.0.0-1.x86_64.rpm
# Install and test
sudo dnf install ./jellyfin-11.0.0-1.x86_64.rpm
sudo systemctl status jellyfin
```
### Cross-Distribution Validation
```bash
# Create Docker test environments
docker run --rm -it debian:bookworm dpkg -i jellyfin-11.0.0-amd64.deb
docker run --rm -it fedora:latest dnf install -y jellyfin-11.0.0-1.x86_64.rpm
docker run --rm -it rockylinux:9 dnf install -y jellyfin-11.0.0-1.x86_64.rpm
```
---
## Summary
| Aspect | Debian | Red Hat |
|--------|--------|---------|
| Package Format | `.deb` | `.rpm` |
| Install Command | `dpkg -i` / `apt install` | `dnf install` / `rpm -i` |
| Service File | `/etc/systemd/system/` | `/etc/systemd/system/` |
| User Dir | `/var/lib/jellyfin` | `/var/lib/jellyfin` |
| Log Dir | `/var/log/jellyfin` | `/var/log/jellyfin` |
| Publish Runtime | `linux-x64` (both) | `linux-x64` (both) |
Both distribution families use the same .NET publishing parameters, so you can maintain a single publish configuration and generate packages for both using the provided scripts.
-116
View File
@@ -1,116 +0,0 @@
# Linux Package Build Quick Reference
## One-Command Build
```bash
cd ~/projects/pgsql-jellyfin && chmod +x build-linux-packages.sh && ./build-linux-packages.sh
```
## Prerequisites
```bash
# Debian/Ubuntu
sudo apt-get install ruby-dev && sudo gem install fpm
# Red Hat/CentOS/Fedora
sudo dnf install ruby-devel && sudo gem install fpm
```
## Build Variations
```bash
# Both Debian and Red Hat
./build-linux-packages.sh
# Debian only
./build-linux-packages.sh --deb-only
# Red Hat only
./build-linux-packages.sh --rpm-only
# Custom version
./build-linux-packages.sh --version 12.0.0
```
## Installation
```bash
# Debian
sudo dpkg -i build/packages/jellyfin-11.0.0-amd64.deb
sudo apt-get install -f # Fix deps if needed
# Red Hat
sudo dnf install build/packages/jellyfin-11.0.0-1.x86_64.rpm
```
## Verification
```bash
# Check status
sudo systemctl status jellyfin
# View logs
sudo journalctl -u jellyfin -f
# List files in package
dpkg -L jellyfin # Debian
rpm -ql jellyfin # Red Hat
```
## Key Directories
| Purpose | Location |
|---------|----------|
| Application | `/opt/jellyfin` |
| Configuration | `/etc/jellyfin` |
| Data/Library | `/var/lib/jellyfin` |
| Logs | `/var/log/jellyfin` |
| Cache | `/var/cache/jellyfin` |
## Publishing Parameters
- Runtime: `linux-x64` (64-bit)
- Configuration: `Release` (optimized)
- Self-contained: `true` (.NET included)
- Both Debian and Red Hat use identical parameters
## Systemd Commands
```bash
sudo systemctl start jellyfin # Start service
sudo systemctl stop jellyfin # Stop service
sudo systemctl status jellyfin # Check status
sudo systemctl restart jellyfin # Restart service
sudo systemctl enable jellyfin # Auto-start on boot
sudo systemctl disable jellyfin # Disable auto-start
```
## Troubleshooting
| Issue | Solution |
|-------|----------|
| Permission denied | `chmod +x build-linux-packages.sh` |
| FPM not found | Install ruby-dev/ruby-devel and gem install fpm |
| Missing dependencies | `sudo apt-get install -f` (Debian) or dnf resolves automatically |
| Service won't start | Check logs: `sudo journalctl -u jellyfin -n 50` |
## Output
Packages created in: `./build/packages/`
- `jellyfin-VERSION-amd64.deb` (Debian)
- `jellyfin-VERSION-1.x86_64.rpm` (Red Hat)
## Scripts Location
```
scripts/
├── build-linux-packages.sh # Main build script
├── jellyfin.spec # Red Hat SPEC template
├── debian/ # Debian hooks
└── redhat/ # Red Hat hooks
```
## Full Documentation
See [LINUX_PACKAGE_BUILD_GUIDE.md](LINUX_PACKAGE_BUILD_GUIDE.md) for complete details
-327
View File
@@ -1,327 +0,0 @@
# Debian and Red Hat Package Building - Complete Summary
## Overview
Your Jellyfin project can now build packages for both Debian and Red Hat-based distributions using a single publishing configuration. Both distributions use:
- **Runtime**: `linux-x64`
- **Configuration**: `Release`
- **Self-contained**: `true` (includes .NET runtime)
The publishing step is identical; only the packaging format differs (`.deb` vs `.rpm`).
## Documentation Structure
### 📖 Main Documentation
**File**: [LINUX_PACKAGE_BUILD_GUIDE.md](LINUX_PACKAGE_BUILD_GUIDE.md)
Complete reference covering:
- Common publishing setup
- Debian package building (FPM method and manual dpkg-deb method)
- Red Hat package building (FPM method and native rpmbuild method)
- Installation and verification procedures
- Testing in Docker containers
### ⚡ Quick Reference
**File**: [LINUX_PACKAGE_BUILD_QUICK_REFERENCE.md](LINUX_PACKAGE_BUILD_QUICK_REFERENCE.md)
Quick commands for:
- One-command builds
- Prerequisites
- Installation variations
- Key directories and systemd commands
- Troubleshooting table
### 🛠️ Build Scripts
**Location**: `scripts/`
1. **build-linux-packages.sh** - Main automated build script
- Checks prerequisites
- Publishes application
- Creates both Debian and Red Hat packages
- Colored output with progress indicators
2. **jellyfin.spec** - Red Hat SPEC file template
- For native rpmbuild method
- Contains hooks and metadata
3. **debian/** - Debian-specific files
- postinst.sh - Post-install hook
- prerm.sh - Pre-remove hook
- postrm.sh - Post-remove hook
- jellyfin.service - Systemd service file
4. **redhat/** - Red Hat-specific files
- post.sh - Post-install hook
- preun.sh - Pre-uninstall hook
- postun.sh - Post-uninstall hook
- jellyfin.service - Systemd service file
5. **scripts/README.md** - Build script documentation
- Directory structure
- Prerequisites and installation
- Package specifications
- Hooks and lifecycle
- Customization options
## Quick Start
### Prerequisites (One-Time Setup)
**Debian/Ubuntu:**
```bash
sudo apt-get install ruby-dev
sudo gem install fpm
```
**Red Hat/CentOS/Fedora:**
```bash
sudo dnf install ruby-devel
sudo gem install fpm
```
### Build Both Packages
```bash
cd /home/wjones/projects/pgsql-jellyfin
chmod +x build-linux-packages.sh
./build-linux-packages.sh
```
Output: `build/packages/`
- `jellyfin-11.0.0-amd64.deb`
- `jellyfin-11.0.0-1.x86_64.rpm`
### Build Specific Distribution
```bash
./build-linux-packages.sh --deb-only # Debian only
./build-linux-packages.sh --rpm-only # Red Hat only
./build-linux-packages.sh --version 12.0.0 # Custom version
```
### Install Package
**Debian:**
```bash
sudo dpkg -i build/packages/jellyfin-11.0.0-amd64.deb
sudo apt-get install -f # Fix dependencies if needed
sudo systemctl start jellyfin
```
**Red Hat:**
```bash
sudo dnf install build/packages/jellyfin-11.0.0-1.x86_64.rpm
sudo systemctl start jellyfin
```
## Build Process Flow
```
┌─────────────────────────────────────────┐
│ User runs: ./build-linux-packages.sh │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ Check Prerequisites │
│ • .NET SDK │
│ • FPM (package manager) │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ Publish Application (linux-x64) │
│ • Restore dependencies │
│ • Build (Release config) │
│ • Self-contained publish │
│ → /tmp/jellyfin-publish-{pid} │
└────────────┬────────────────────────────┘
┌───────┴────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Build Debian │ │ Build Red Hat│
│ Package │ │ Package │
│ │ │ │
│ FPM: │ │ FPM: │
│ .deb format │ │ .rpm format │
└──────┬───────┘ └──────┬───────┘
│ │
└────────┬────────┘
┌─────────────────────────────────────────┐
│ Output to: build/packages/ │
│ • jellyfin-VERSION-amd64.deb │
│ • jellyfin-VERSION-1.x86_64.rpm │
└─────────────────────────────────────────┘
```
## Key Features
### Package Metadata
- **Version**: Automatically extracted from `SharedVersion.cs`
- **Architecture**: x86_64 (64-bit)
- **Dependencies**:
- Debian: libssl3, libicu72, libfontconfig1
- Red Hat: openssl-libs, libicu, fontconfig
### Installation Hooks
- **Post-install**: Creates user/group, sets permissions, enables service
- **Pre-remove**: Stops service
- **Post-remove**: Removes user/group
### Service File
- **Type**: Simple systemd service
- **Auto-restart**: On failure (5s delay)
- **Resource limits**: 4GB max memory, 65k file descriptors
- **Directories**:
- Config: `/var/lib/jellyfin`
- Logs: `/var/log/jellyfin`
- Cache: `/var/cache/jellyfin`
### User Management
- **Username**: `jellyfin` (system user, no shell)
- **Home**: `/var/lib/jellyfin`
- **Created automatically** during package installation
- **Removed automatically** during package removal
## Testing
### Manual Testing
```bash
# Extract and inspect package contents
dpkg -c jellyfin-11.0.0-amd64.deb | head -20
rpm -qpl jellyfin-11.0.0-1.x86_64.rpm | head -20
# Verify hooks
dpkg-deb -e jellyfin-11.0.0-amd64.deb /tmp/control
rpm -qp --scripts jellyfin-11.0.0-1.x86_64.rpm
```
### Docker Testing
```bash
# Test on Debian
docker run --rm -it -v $(pwd)/build/packages:/pkg debian:bookworm \
bash -c "dpkg -i /pkg/jellyfin-11.0.0-amd64.deb && systemctl status jellyfin"
# Test on Fedora
docker run --rm -it -v $(pwd)/build/packages:/pkg fedora:latest \
bash -c "dnf install -y /pkg/jellyfin-11.0.0-1.x86_64.rpm && systemctl status jellyfin"
# Test on Rocky Linux
docker run --rm -it -v $(pwd)/build/packages:/pkg rockylinux:9 \
bash -c "dnf install -y /pkg/jellyfin-11.0.0-1.x86_64.rpm && systemctl status jellyfin"
```
## Troubleshooting
### FPM Installation Issues
```bash
# Ensure Ruby development headers are installed
sudo apt-get install ruby-dev # Debian/Ubuntu
sudo dnf install ruby-devel # Red Hat
# Then install FPM
sudo gem install fpm
```
### Build Script Permissions
```bash
chmod +x build-linux-packages.sh
chmod +x scripts/debian/postinst.sh scripts/debian/prerm.sh scripts/debian/postrm.sh
chmod +x scripts/redhat/post.sh scripts/redhat/preun.sh scripts/redhat/postun.sh
```
### Service Won't Start
Check logs for errors:
```bash
sudo journalctl -u jellyfin -n 50
sudo journalctl -u jellyfin -f # Follow logs
```
Check permissions:
```bash
ls -la /opt/jellyfin
ls -la /var/lib/jellyfin
```
### Dependency Resolution
**Debian**: If dpkg reports missing dependencies:
```bash
sudo apt-get install -f
```
**Red Hat**: dnf resolves dependencies automatically
## No Red Hat System Required
The documentation and scripts can be created and tested on any Linux system:
- ✅ Debian/Ubuntu systems can create `.rpm` packages using FPM
- ✅ Red Hat systems can create `.deb` packages using FPM
- ✅ Cross-distribution testing via Docker
- ✅ Single .NET publishing configuration works for both
To actually run the packages on their respective distributions, you can:
1. Test locally using Docker containers
2. Deploy to VMs or servers running those distributions
3. Use CI/CD pipeline to test on actual systems
## File Manifest
```
/home/wjones/projects/pgsql-jellyfin/
├── docs/
│ ├── LINUX_PACKAGE_BUILD_GUIDE.md (Complete reference)
│ ├── LINUX_PACKAGE_BUILD_QUICK_REFERENCE.md (Quick commands)
│ └── PACKAGE_BUILDING_SUMMARY.md (This file)
├── scripts/
│ ├── README.md (Build scripts documentation)
│ ├── build-linux-packages.sh (Main build script)
│ ├── jellyfin.spec (Red Hat SPEC template)
│ ├── debian/
│ │ ├── postinst.sh
│ │ ├── prerm.sh
│ │ ├── postrm.sh
│ │ └── jellyfin.service
│ └── redhat/
│ ├── post.sh
│ ├── preun.sh
│ ├── postun.sh
│ └── jellyfin.service
└── build/
└── packages/ (Output location)
├── jellyfin-11.0.0-amd64.deb
└── jellyfin-11.0.0-1.x86_64.rpm
```
## Next Steps
1.**Read Documentation**
- Start with [LINUX_PACKAGE_BUILD_QUICK_REFERENCE.md](LINUX_PACKAGE_BUILD_QUICK_REFERENCE.md)
- Reference [LINUX_PACKAGE_BUILD_GUIDE.md](LINUX_PACKAGE_BUILD_GUIDE.md) for details
2.**Install Prerequisites**
- `sudo apt-get install ruby-dev && sudo gem install fpm` (or Red Hat equivalent)
3.**Build Packages**
- `./build-linux-packages.sh`
4.**Test Installation**
- Test locally or in Docker containers
- Verify service starts: `sudo systemctl status jellyfin`
5.**Deploy**
- Copy `.deb` to Debian systems
- Copy `.rpm` to Red Hat systems
- Install and verify
-195
View File
@@ -1,195 +0,0 @@
# TV Shows Media Library Performance Fix
## Problem Statement
The TV Shows media library was slow to load when users browsed or opened the TV Shows collection. Results appeared slowly compared to other media types.
## Root Cause Analysis
The performance issue was caused by **in-memory deduplication** of episodes when loading TV Shows:
1. **Database Query**: Fetches all matching episodes/items (could be 10,000+ for large libraries)
2. **Memory Load**: All results loaded into memory with `ToListAsync()`
3. **In-Memory Deduplication**: Uses LINQ `DistinctBy()` to group by `SeriesPresentationUniqueKey`
4. **Result**: To return 500 unique series, system loads 10,000+ episodes into memory (~20x overhead)
### Why This Happened
- EF Core cannot translate complex `DistinctBy` expressions to SQL
- Code fell back to in-memory deduplication
- No optimization existed for the critical TV Shows use case
## Solution Implemented
### Changes Made
**File**: [Jellyfin.Server.Implementations/Item/BaseItemRepository.cs](Jellyfin.Server.Implementations/Item/BaseItemRepository.cs)
#### 1. Optimized GetItemsAsync() (Line ~453)
- Detects when series-level grouping is needed
- Uses database-level `GroupBy()` instead of in-memory `DistinctBy()`
- Reduces memory usage from 10,000+ items to 500 items
- Adds call to new `ApplySeriesGroupingAtDatabaseLevel()` method
#### 2. Optimized GetItemListAsync() (Line ~517)
- Same optimization applied as GetItemsAsync()
- Ensures TV Shows load efficiently whether paging is enabled or not
#### 3. New Method: ApplySeriesGroupingAtDatabaseLevel() (Line ~869)
```csharp
private async Task<List<Guid>> ApplySeriesGroupingAtDatabaseLevel(
IQueryable<BaseItemEntity> dbQuery,
JellyfinDbContext context,
CancellationToken cancellationToken)
{
// Use database-level grouping for TV Shows to avoid
// loading all episodes into memory
var groupedBySeriesIds = await dbQuery
.GroupBy(e => e.TvExtras!.SeriesPresentationUniqueKey)
.Select(g => g.First().Id)
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
return groupedBySeriesIds;
}
```
This method:
- Uses SQL `GROUP BY` at the database level
- Returns only one ID per unique series
- EF Core translates this to efficient SQL GROUP BY query
- Avoids loading unnecessary episodes into memory
## Performance Impact
### Before & After Comparison
| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| Memory Usage | 10,000+ items | ~500 items | **20x reduction** |
| Database Query Type | All episodes | GROUP BY series | More efficient |
| Result Deduplication | In-memory | SQL-level | **Translated to SQL** |
| Load Time | Seconds | Milliseconds | **Significant speedup** |
### Example: 500 Series with 10,000 Episodes
- **Before**: 10,000 episodes loaded into memory → deduplicated with DistinctBy
- **After**: SQL executes `GROUP BY SeriesPresentationUniqueKey` → only ~500 rows returned
## Technical Details
### Query Optimization Path
**For TV Shows with GroupBySeriesPresentationUniqueKey enabled:**
```
Query Construction
ApplyOrder (sorting)
Check: GroupBySeriesPresentationUniqueKey?
├─ YES → ApplySeriesGroupingAtDatabaseLevel()
│ (SQL: GROUP BY SeriesPresentationUniqueKey)
│ ↓
│ Return deduplicated IDs (fast, minimal memory)
└─ NO → Load items to memory (legacy path)
→ ApplyGroupingInMemory() (DistinctBy)
Apply paging
Load full entities with navigations
```
### How SQL GROUP BY Works
```sql
-- PostgreSQL/EF Core translation
SELECT FIRST(Id)
FROM BaseItems
GROUP BY TvExtras.SeriesPresentationUniqueKey
```
This efficiently:
- Groups all episodes by series presentation key
- Returns one ID per group (first episode found)
- Executes at database level (no memory overhead)
- Works across SQL Server, PostgreSQL, and SQLite
## Testing Recommendations
### Manual Testing
1. Open Jellyfin web UI
2. Navigate to TV Shows media library
3. Observe load time vs before fix (should be significantly faster)
4. Verify all series display correctly
5. Check that series count matches expected number
### Performance Testing
```bash
# Monitor memory usage before/after
watch -n 1 'ps aux | grep jellyfin | grep -v grep'
# Check database query performance
EXPLAIN ANALYZE SELECT ... GROUP BY TvExtras.SeriesPresentationUniqueKey
```
### Test Cases
- [ ] Small library (< 100 series) - should load instantly
- [ ] Medium library (100-500 series) - should be < 1s
- [ ] Large library (1000+ series) - should be < 3s
- [ ] With user filters applied
- [ ] With search terms
- [ ] With different sort orders
## Database Considerations
### Existing Indexes
- `CreateIndex(x => x.Type)` - Helps Type filtering
- `CreateIndex(x => new { x.IsFolder, x.Type })` - Helps filtering
- `CreateIndex(x => x.TvExtras!.SeriesPresentationUniqueKey)` - Helps GROUP BY
### Recommended Future Optimizations
- Add index on `TvExtras.SeriesPresentationUniqueKey` if not present
- Add composite index `(Type, IsVirtualItem)` for faster filtering
- Consider index on `TvExtras.SeriesId` for episode queries
## Deployment Notes
- ✅ No database migration needed
- ✅ Backward compatible - no breaking changes
- ✅ Automatic optimization when TV Shows are loaded
- ✅ All projects compile successfully
- ✅ Can be deployed as-is without configuration
## Related Performance Fixes
1. **Dashboard Counts** - Reduced `/Items/Counts` from 8 queries to 1
- Similar principle: moved logic from client to database
- Result: 8x faster dashboard loading
2. **TV Shows Deduplication** - Moved from in-memory to database-level
- Uses SQL GROUP BY instead of LINQ DistinctBy
- Result: 20x memory reduction, significantly faster loading
## Performance Comparison with Other Fixes
| Issue | Type | Fix | Improvement |
|-------|------|-----|-------------|
| Dashboard Counts | API | Async + Single Query | 8x faster |
| TV Shows Loading | Query | Database GROUP BY | 20x memory, seconds→ms |
| Missing Indexes | Query | SQL Indexes | 2-10x faster |
| N+1 Queries | Query | Eager Loading | 10-100x faster |
All of these optimizations work together to dramatically improve overall Jellyfin performance.
## Implementation Summary
**Completed Changes:**
1. Modified GetItemsAsync() to detect and optimize series grouping
2. Modified GetItemListAsync() to use same optimization
3. Added ApplySeriesGroupingAtDatabaseLevel() method
4. Implemented conditional logic to use database-level grouping when applicable
5. All code compiles successfully with 0 errors, 0 warnings
**Performance Gains:**
- TV Shows library loads significantly faster
- Memory usage reduced by ~20x for large libraries
- Database queries more efficient with SQL GROUP BY
- Works seamlessly with existing codebase
@@ -1,157 +0,0 @@
# TV Shows Query Performance - Quick Summary
## Problem Statement
TV Shows queries are **10-100x slower** than other item types due to architectural limitations in how Series are queried and deduplicated.
## Critical Findings
### 1. Query Architecture
**Current Flow**:
1. Query database for matching Series (with TvExtras JOIN)
2. Load ALL results into memory (including episodes)
3. Deduplicate by `SeriesPresentationUniqueKey` in C#
4. Apply paging
5. Load full entities with all navigations (second query)
**Example Problem**:
- User has 500 TV Series with 10,000 Episodes total
- Query to display Series list:
- Query 1: Returns 10,000 Episode rows (all must be loaded to deduplicate)
- In-memory: Deduplicates to 500 unique Series
- Query 2: Loads 500 Series entities
### 2. Missing Database Indexes
From migration `20260226170000_AddBasePerformanceIndexes.cs`:
**What EXISTS:**
- `baseitems_parentid_idx``(ParentId, Type)`
- `baseitems_topparentid_idx``(TopParentId, Type)`
- `baseitems_seriespresentationuniquekey_idx``(SeriesPresentationUniqueKey, ...)`
**What's MISSING:**
- Standalone `Type` index
- Composite `(IsFolder, Type)` index
- Index on `TvExtras.SeriesId`
- No filtering on `IsSeries` column
### 3. Inefficient Queries for Series
**Line 2995-3000**: IsPlayed special case
```csharp
// For each Series, runs a correlated subquery to check if ANY episode was played
WHERE ... (
SELECT 1 FROM BaseItems
WHERE TvExtras.SeriesPresentationUniqueKey = e.PresentationUniqueKey
AND UserData.Played = true
)
```
**Impact**: 1 Series × (number of episodes to check) subqueries
**Line 3427-3438**: Tag inheritance for episodes
```csharp
// For each episode, must check both episode tags AND parent series tags
&& (!e.TvExtras.SeriesId.HasValue ||
!context.ItemValuesMap.Any(f =>
f.ItemId == e.TvExtras.SeriesId.Value
AND f.ItemValue.Type == ItemValueType.Tags))
```
**Impact**: Additional ItemValuesMap joins per episode with tag filters
### 4. In-Memory Deduplication
**Location**: Line 814-850 in BaseItemRepository.cs
Uses reflection on anonymous types to deduplicate results in C#:
```csharp
private List<Guid> ApplyGroupingInMemory<T>(List<T> items, InternalItemsQuery filter)
{
// Reflects properties, uses DistinctBy()
filtered = items.DistinctBy(e => seriesKeyProp.GetValue(e));
return filtered.Select(...).ToList();
}
```
**Why not database?** EF Core limitations - `DistinctBy()` on complex types can't be translated to SQL consistently
**Impact**:
- All matching rows must be loaded from database
- Temporary memory spike with 10,000+ item objects
- Slow reflection-based deduplication
### 5. Table Joins Required
For Series queries, ApplyNavigations (line 780-809) eagerly loads:
- `TvExtras` - Series/Season/Episode metadata
- `LiveTvExtras` - Live TV metadata
- `AudioExtras` - Audio metadata
- `Provider` - Provider IDs
- `LockedFields` - Locked metadata
- `UserData` - Watch history
- `Images` - Item images
- `TrailerTypes` - Trailer types (if requested)
**This creates a massive JOIN chain** instead of separate queries
### 6. Code Architecture Decisions
**Line 914**: `AsSingleQuery()`
- Forces EF Core to use one giant query instead of splitting
- **Good**: Avoids N+1 on navigations
- **Bad**: One huge JOIN with poor optimization potential
**Line 455-460**: Select only IDs and keys, then load full entities
- Meant to optimize but actually forces:
1. Full Series table scan with TvExtras JOIN
2. Select into memory
3. Deduplicate
4. Second query for full entities
## Performance Comparison
| Type | Bottleneck | Speed Relative to Movies |
|------|-----------|------------------------|
| Movies | Minimal dedup, direct Type filter | 1x (baseline) |
| Music Albums | Moderate dedup by Album | 2-5x slower |
| TV Shows | Massive dedup, correlated subqueries, tag inheritance | 10-100x slower |
## When TV Shows Are Slower
**❌ SLOW**:
1. Query for Episodes: `IncludeItemTypes=Episode` (loads all 10,000 to deduplicate)
2. Filter by tags: `Tags=Action` (must join parent Series tags)
3. Filter by played status: `IsPlayed=true` (correlated subquery per Series)
4. Browse large library with series+episodes mixed query
5. Any query requesting both Series AND Episodes
**✅ FASTER**:
1. Query for just Series: `IncludeItemTypes=Series` (no Episodes to load)
2. Query with TopParentId (uses composite index)
3. Direct queries with no deduplication needed
4. Small libraries with few episodes per series
## Why This Matters
**Scale Impact**:
- **100 Series × 10 Episodes each**: ~1000 rows loaded, deduplicated to 100 (10x overhead)
- **500 Series × 50 Episodes each**: ~25,000 rows loaded, deduplicated to 500 (50x overhead)
- **2000 Series × 100 Episodes each**: ~200,000 rows loaded, deduplicated to 2000 (100x overhead)
**For remote databases**: Even worse due to network latency × 50-100 overhead
## Recommended Fixes (Priority Order)
### High Priority (Quick Wins)
1. Add index on `Type` column
2. Add index on `(IsFolder, Type)`
3. Add index on `TvExtras.SeriesId`
4. Optimize IsPlayed query to avoid correlated subquery
### Medium Priority (Architecture)
1. Implement caching for Series child counts
2. Lazy-load TvExtras only when needed
3. Create view for Series with precomputed episode counts
### Low Priority (Long-term)
1. Denormalize episode count into Series row
2. Separate read replica for expensive queries
3. Move deduplication fully to database with DISTINCT ON
## Documentation
See **TV_SHOWS_QUERY_PERFORMANCE_ANALYSIS.md** for complete details with code references and SQL examples.
-585
View File
@@ -1,585 +0,0 @@
# TV Shows/Series Query Performance Analysis
## Executive Summary
TV Shows queries are **inherently slower** than other item types due to:
1. **Extra table joins** - TvExtras table required for Series grouping/deduplication
2. **Complex filtering logic** - Series-specific handling in IsPlayed filter and tag inheritance
3. **In-memory deduplication** - All matching episodes must be loaded before grouping (N+1 risk)
4. **Missing composite indexes** - No index on `(Type, IsFolder)` for efficient Series filtering
5. **SeriesPresentationUniqueKey deduplication** - Requires loading all results before grouping
---
## 1. TV Shows Query Logic in BaseItemRepository
### Location
[BaseItemRepository.cs](Jellyfin.Server.Implementations/Item/BaseItemRepository.cs#L421)
### Query Flow for TV Shows
**Step 1: PrepareItemQuery** (Line 911)
```csharp
private IQueryable<BaseItemEntity> PrepareItemQuery(JellyfinDbContext context, InternalItemsQuery filter)
{
IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking();
dbQuery = dbQuery.AsSingleQuery();
return dbQuery;
}
```
- Creates a base query from BaseItems table
- `AsSingleQuery()` - Prevents EF Core query splitting (important for TV Shows to avoid N+1)
**Step 2: TranslateQuery** (Line 2577)
- Applies filtering including:
- Type filtering: `e.Type != "Series"` (for excluding Series)
- IsSeries flag: `e.IsSeries == filter.IsSeries`
- IsPlayed special logic (line 2995-3000)
**Step 3: ApplyOrder** - Orders results
**Step 4: ApplyGroupingInMemory** (Line 814)
- **CRITICAL**: Groups results IN MEMORY (not in database)
- Uses `DistinctBy()` with reflection on anonymous types
- For Series: `DistinctBy(e => e.TvExtras!.SeriesPresentationUniqueKey)`
**Step 5: ApplyNavigations** (Line 780)
- Eagerly loads related data via `.Include()`:
```csharp
.Include(e => e.TvExtras) // TV Series metadata
.Include(e => e.LiveTvExtras) // Live TV metadata
.Include(e => e.AudioExtras) // Audio metadata
.Include(e => e.Provider) // Provider IDs
.Include(e => e.LockedFields) // Locked metadata fields
.Include(e => e.UserData) // Watch history (if EnableUserData=true)
.Include(e => e.Images) // Item images
.Include(e => e.TrailerTypes) // Trailer types (if requested)
```
---
## 2. Type Filtering for Series
### Location: Lines 2719-2748
**How Type Filtering Works:**
```csharp
var includeTypes = filter.IncludeItemTypes;
if (filter.IncludeItemTypes.Length == 0)
{
var excludeTypes = filter.ExcludeItemTypes;
if (excludeTypes.Length == 1)
{
baseQuery = baseQuery.Where(e => e.Type != excludeTypeName);
}
else if (excludeTypes.Length > 1)
{
var excludeTypeName = new List<string>();
foreach (var excludeType in excludeTypes)
{
if (_itemTypeLookup.BaseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
{
excludeTypeName.Add(baseItemKindName!);
}
}
baseQuery = baseQuery.Where(e => !excludeTypeName.Contains(e.Type));
}
}
else
{
string[] types = includeTypes.Select(f => _itemTypeLookup.BaseItemKindNames.GetValueOrDefault(f))
.Where(e => e != null).ToArray()!;
baseQuery = baseQuery.WhereOneOrMany(types, f => f.Type);
}
```
**Problem**: No index on `Type` column alone
- When filtering for Series: `e.Type == "Series"`
- Requires table scan or composite index
- **Index Status**: Only composite indexes exist:
- `baseitems_parentid_idx` → `(ParentId, Type)`
- `baseitems_topparentid_idx` → `(TopParentId, Type)`
- **Missing**: Standalone `Type` index or `(IsFolder, Type)` composite
### Workaround Optimization
The code uses `IsFolder` to optimize Series queries since Series are folders:
```csharp
if (filter.IsFolder.HasValue)
{
baseQuery = baseQuery.Where(e => e.IsFolder == filter.IsFolder);
}
```
**But**: This is manual and requires caller to specify `IsFolder=true` when filtering for Series.
---
## 3. Series-Specific Filtering: Child Count & Parent Relationships
### Location: Lines 3416-3438
**SeriesPresentationUniqueKey Filtering** (Line 3416-3419)
```csharp
if (!string.IsNullOrWhiteSpace(filter.SeriesPresentationUniqueKey))
{
baseQuery = baseQuery
.Where(e => e.TvExtras!.SeriesPresentationUniqueKey == filter.SeriesPresentationUniqueKey);
}
```
- Filters episodes by their parent Series
- Requires **join to TvExtras table**
**Tag Inheritance for Episodes** (Line 3424-3438)
```csharp
if (filter.ExcludeInheritedTags.Length > 0)
{
var excludedTags = filter.ExcludeInheritedTags;
baseQuery = baseQuery.Where(e =>
!e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
&& (!e.TvExtras!.SeriesId.HasValue || !context.ItemValuesMap.Any(f =>
f.ItemId == e.TvExtras!.SeriesId.Value &&
f.ItemValue.Type == ItemValueType.Tags &&
excludedTags.Contains(f.ItemValue.CleanValue))));
}
if (filter.IncludeInheritedTags.Length > 0)
{
var includeTags = filter.IncludeInheritedTags;
var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist;
baseQuery = baseQuery.Where(e =>
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
// For seasons and episodes, we also need to check the parent series' tags.
|| (e.TvExtras!.SeriesId.HasValue && context.ItemValuesMap.Any(f =>
f.ItemId == e.TvExtras!.SeriesId.Value &&
f.ItemValue.Type == ItemValueType.Tags &&
includeTags.Contains(f.ItemValue.CleanValue)))
|| (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
}
```
**Performance Issue**:
- **For each episode**, the query checks the parent Series' tags
- Requires correlated subqueries in tag filtering
- Forces joins to TvExtras, ItemValuesMap, and ItemValues tables
### IsPlayed Special Handling (Line 2995-3000)
```csharp
if (filter.IsPlayed.HasValue)
{
if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
{
// SPECIAL CASE: For Series queries, check if ANY episode was played
baseQuery = baseQuery.Where(e =>
context.BaseItems.Where(e => e.Id != EF.Constant(PlaceholderId))
.Where(e => e.IsFolder == false && e.IsVirtualItem == false)
.Where(f => f.UserData!.FirstOrDefault(e => e.UserId == filter.User!.Id && e.Played)!.Played)
.Any(f => f.TvExtras!.SeriesPresentationUniqueKey == e.PresentationUniqueKey) == filter.IsPlayed);
}
else
{
// Standard: Check if THIS item was played
baseQuery = baseQuery
.Select(e => new
{
IsPlayed = e.UserData!.Where(f => f.UserId == filter.User!.Id).Select(f => (bool?)f.Played).FirstOrDefault() ?? false,
Item = e
})
.Where(e => e.IsPlayed == filter.IsPlayed)
.Select(f => f.Item);
}
}
```
**The Problem**:
- **Correlated subquery inside WHERE clause**
- For each Series, database checks ALL episodes to see if ANY were played
- This is a **massive performance killer** for Series queries
- Example: 100 Series × (each checks all episodes in library) = thousands of subqueries
---
## 4. N+1 Loading Patterns Specific to TV Shows
### Location: Lines 450-530 (GetItemsAsync)
**The Two-Query Pattern:**
**Query 1: Get IDs with Keys** (Line 455-460)
```csharp
var allIds = await dbQuery
.Select(e => new { e.Id, e.PresentationUniqueKey, SeriesPresentationUniqueKey = e.TvExtras!.SeriesPresentationUniqueKey })
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
```
- Gets **all matching rows** (including duplicates)
- Must load TvExtras for grouping
**Query 2: Load Full Entities** (Line 475-481)
```csharp
var dbQueryWithNavs = ApplyNavigations(context.BaseItems.Where(e => pagedIds.Contains(e.Id)), filter);
var items = await dbQueryWithNavs
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
```
- **Second query**: Loads full entities including all navigations
**The N+1 Risk for TV Shows:**
1. User has 500 Series with 10,000 Episodes total
2. When browsing "TV Shows" page:
- User clicks "TV Shows" → wants to see list of Series (not episodes)
- Query 1 loads ALL 10,000 episodes (to deduplicate by SeriesPresentationUniqueKey)
- Only then groups to 500 Series
- Query 2 loads 500 Series entities with all their data
3. If subsequent calls ask for "child count" or "episode count" per Series → additional queries per Series
### Why This is Different Than Movies/Music
- **Movies**: No grouping needed, 1:1 with items displayed
- **Music Albums**: Grouped by Album, but typically fewer duplicates
- **TV Shows**: 1 Series + N Episodes = N+1 rows to load and deduplicate
---
## 5. In-Memory Deduplication (ApplyGroupingInMemory)
### Location: Lines 814-850
```csharp
private List<Guid> ApplyGroupingInMemory<T>(List<T> items, InternalItemsQuery filter)
where T : class
{
var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
var idProp = typeof(T).GetProperty("Id");
var presentationKeyProp = typeof(T).GetProperty("PresentationUniqueKey");
var seriesKeyProp = typeof(T).GetProperty("SeriesPresentationUniqueKey");
IEnumerable<T> filtered = items;
if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
{
filtered = items.DistinctBy(e => new
{
PresentationKey = presentationKeyProp.GetValue(e),
SeriesKey = seriesKeyProp.GetValue(e)
});
}
else if (enableGroupByPresentationUniqueKey)
{
filtered = items.DistinctBy(e => presentationKeyProp.GetValue(e));
}
else if (filter.GroupBySeriesPresentationUniqueKey)
{
filtered = items.DistinctBy(e => seriesKeyProp.GetValue(e));
}
else
{
filtered = items.DistinctBy(e => idProp.GetValue(e));
}
return filtered.Select(e => (Guid)idProp.GetValue(e)!).ToList();
}
```
**Why Not Database Grouping?**
- Comment in code: "See docs/query-optimization-complete-story.md"
- **EF Core limitation**: `DistinctBy()` on complex types cannot be translated to SQL
- **PostgreSQL issue**: UUID constraints prevent some grouping patterns
- **Workaround**: Use `DISTINCT ON` via `DistinctBy()` in EF Core (translates correctly)
- But `DistinctBy()` within complex SELECT is not always translatable
**Performance Impact for TV Shows:**
- Must load **ALL matching episodes** from database
- Then deduplicate in C# code using reflection
- For large libraries: 10,000+ rows loaded just to get 500 unique Series
- **Memory spike**: Temporary list of 10,000 items before deduplication
---
## 6. Database Indexes: What Exists vs. What's Missing
### Current Base Indexes on BaseItems
[Migration: 20260226170000_AddBasePerformanceIndexes.cs](src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/Migrations/20260226170000_AddBasePerformanceIndexes.cs)
**Indexes That Support Series Queries:**
| Index | Columns | Supports |
|-------|---------|----------|
| `baseitems_parentid_idx` | `(ParentId, Type)` | Series grouped by parent folder |
| `baseitems_topparentid_idx` | `(TopParentId, Type)` | Series filtered by library |
| `baseitems_seriespresentationuniquekey_idx` | `(SeriesPresentationUniqueKey, IndexNumber, ParentIndexNumber)` | ✅ Episodes by Series |
| `baseitems_sortname_idx` | `(SortName)` | Series sorted by name |
| `baseitems_premieredate_idx` | `(PremiereDate DESC)` | Series sorted by date |
**Critical Indexes Missing:**
| Index | Impact | Why Missing |
|-------|--------|------------|
| `Type` (standalone) | Full table scan when filtering by Type alone | Not included in migration |
| `(IsFolder, Type)` | Fast Series queries without ParentId filter | Not included in migration |
| `(Type, IsFolder)` | Fast queries: "all Series" without folder hierarchy | Not included in migration |
| `Type` with filter on `IsSeries=true` | IsSeries column not indexed | Not included in migration |
**Why This Matters:**
```sql
-- Current: Uses composite index (ParentId, Type) if available
SELECT * FROM BaseItems WHERE ParentId = $1 AND Type = 'Series';
-- Missing: NO index for this common query pattern
SELECT * FROM BaseItems WHERE Type = 'Series';
-- Missing: NO index for this optimization
SELECT * FROM BaseItems WHERE IsFolder = true AND Type = 'Series';
```
### TvExtras Table Indexes
- **Missing**: Index on `SeriesId` (needed for tag inheritance queries)
- **Missing**: Composite `(ItemId, SeriesPresentationUniqueKey)`
---
## 7. ApplyGroupingFilter Logic
### Location: Lines 740-778
```csharp
private IQueryable<BaseItemEntity> ApplyGroupingFilter(JellyfinDbContext context, IQueryable<BaseItemEntity> dbQuery, InternalItemsQuery filter)
{
var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter);
if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey)
{
// PERFORMANCE FIX: Use DistinctBy() instead of GroupBy().Select().FirstOrDefault()
dbQuery = dbQuery.DistinctBy(e => new { e.PresentationUniqueKey, SeriesPresentationUniqueKey = e.TvExtras!.SeriesPresentationUniqueKey });
}
else if (enableGroupByPresentationUniqueKey)
{
// PERFORMANCE FIX: Use DistinctBy() to generate DISTINCT ON instead of correlated subquery
dbQuery = dbQuery.DistinctBy(e => e.PresentationUniqueKey);
}
else if (filter.GroupBySeriesPresentationUniqueKey)
{
// PERFORMANCE FIX: Use DistinctBy() instead of GroupBy().Select().FirstOrDefault()
dbQuery = dbQuery.DistinctBy(e => e.TvExtras!.SeriesPresentationUniqueKey);
}
else
{
dbQuery = dbQuery.Distinct();
}
dbQuery = ApplyOrder(dbQuery, filter, context);
return dbQuery;
}
```
**Key Insight**:
- `DistinctBy()` translates to PostgreSQL's `DISTINCT ON` clause
- This is **much faster** than correlated subqueries
- But still must fetch all matching rows first
**For Series Queries:**
- `GroupBySeriesPresentationUniqueKey=true`
- Translates to: `SELECT DISTINCT ON (TvExtras.SeriesPresentationUniqueKey) ...`
- This means: "Give me one row per unique Series"
**The Problem:**
- Must ORDER BY before using DISTINCT ON (PostgreSQL requirement)
- With many Episodes, sorting 10,000 rows then deduplicating is slower than database-level grouping
---
## 8. Complete Series Query Execution Example
### Scenario: User Clicks "TV Shows" in Library
**URL**: `/Items?IncludeItemTypes=Series&ParentId=X`
**What Happens:**
```
1. GetItemsAsync() called with:
- IncludeItemTypes: [Series]
- ParentId: X (TV Shows folder)
- EnableTotalRecordCount: true
2. PrepareItemQuery()
└─ IQueryable<BaseItemEntity> dbQuery = context.BaseItems.AsNoTracking().AsSingleQuery()
3. TranslateQuery()
└─ WHERE Type = 'Series'
└─ WHERE ParentId = X
└─ (If indexed: uses baseitems_parentid_idx)
4. ApplyOrder()
└─ ORDER BY SortName (or other sort)
5. QUERY 1 (Lines 455-460):
SELECT Id, PresentationUniqueKey, TvExtras.SeriesPresentationUniqueKey
FROM BaseItems
LEFT JOIN TvExtras ON BaseItems.Id = TvExtras.ItemId
WHERE ParentId = X AND Type = 'Series'
ORDER BY SortName
└─ Result: 500 Series rows (1 row per Series, TvExtras joined)
6. ApplyGroupingInMemory()
└─ Already unique, no deduplication needed for Series-only queries
7. ApplyPagingToIds()
└─ Skip 0, Take 100 (if paginated)
8. QUERY 2 (Lines 475-481):
SELECT * FROM BaseItems
LEFT JOIN TvExtras ON BaseItems.Id = TvExtras.ItemId
LEFT JOIN Provider ON BaseItems.Id = Provider.ItemId
LEFT JOIN Images ON BaseItems.Id = Images.ItemId
LEFT JOIN UserData ON BaseItems.Id = UserData.ItemId
WHERE Id IN (500 Series IDs from paged list)
└─ Result: Full Series entities with all related data
9. Deserialize to DTOs
└─ Convert BaseItemEntity to BaseItemDto
└─ Map UserData if available
10. Return to client
```
**Total Queries: 2**
- **Query 1**: Get Series IDs (fast, uses index)
- **Query 2**: Get full entities (includes multiple JOINs)
**Bottleneck**:
- If there are 10,000 Episodes with same Parent, Query 1 returns 10,000 rows (one per episode), then deduplicates
- This is rare for direct Series queries, but happens when:
- Querying Episodes: `IncludeItemTypes=Episode&ParentId=X`
- Querying all content: `IncludeItemTypes=[Series,Episode,Movie]`
---
## 9. Performance Comparison: Series vs. Movies vs. Music
### Query Pattern Differences
**Movies** (Fast):
```csharp
// No special handling, 1:1 with display
WHERE Type = 'Movie'
// Result: 100 rows = 100 Movies shown
```
**Music Albums** (Medium):
```csharp
// Grouped by Album name
WHERE Type = 'MusicAlbum'
SELECT DISTINCT ON (Album) ...
// Result: 100 rows = 100 Albums
```
**TV Shows** (Slow):
```csharp
// Two cases:
// Case 1: Query for Series themselves
WHERE Type = 'Series'
// Result: 500 rows = 500 Series (GOOD)
// Case 2: Query for all Episodes (common in "latest" or "browse all")
WHERE Type = 'Episode'
SELECT DISTINCT ON (TvExtras.SeriesPresentationUniqueKey) ...
// Result: 10,000 rows → deduplicate to 500 → return 100
// PROBLEM: Loaded 10,000, returned 100
```
### Why Series Are Slower
| Aspect | Movies | TV Shows | Impact |
|--------|--------|----------|--------|
| Deduplication | None | Required (Series ID) | 10-100x more rows loaded |
| Table joins | 1 (BaseItems) | 3+ (BaseItems + TvExtras + Tags checks) | Multiple JOINs per query |
| Filter complexity | Simple | Complex (tag inheritance, IsPlayed special case) | More CPU, more conditions |
| Index usage | Direct on Type | Composite (ParentId, Type) usually needed | Fewer index options |
| Related data | Simple | Children (episodes), parent (series for episodes) | More eager-load navigations |
| In-memory ops | Minimal | Full list loaded for deduplication | Memory spike |
---
## 10. Special Handling That Slows TV Shows
### 1. IsPlayed Special Case (Line 2995)
```csharp
if (filter.IncludeItemTypes.Length == 1 && filter.IncludeItemTypes[0] == BaseItemKind.Series)
{
// Correlated subquery: For each Series, check if ANY episode was played
baseQuery = baseQuery.Where(e =>
context.BaseItems
.Where(f => f.TvExtras!.SeriesPresentationUniqueKey == e.PresentationUniqueKey)
.Any(f => f.UserData!.FirstOrDefault()!.Played));
}
```
**Impact**: Extra subquery per Series
### 2. Tag Inheritance for Episodes (Line 3427-3438)
```csharp
// Check episode's tags AND parent series' tags
&& (!e.TvExtras!.SeriesId.HasValue ||
!context.ItemValuesMap.Any(f =>
f.ItemId == e.TvExtras!.SeriesId.Value &&
f.ItemValue.Type == ItemValueType.Tags))
```
**Impact**: Joins to ItemValuesMap for every tag filter on episodes
### 3. AsSingleQuery() (Line 914)
```csharp
dbQuery = dbQuery.AsSingleQuery();
```
**Impact**: Prevents EF Core from splitting queries, but means all JOINs happen in one huge query
- Better: Avoids N+1
- Worse: One massive JOIN chain for all navigations
---
## 11. Recommendations for Optimization
### Immediate (No Schema Changes)
1. **Add Index on Type column**
```sql
CREATE INDEX CONCURRENTLY idx_baseitems_type
ON library."BaseItems" ("Type");
```
2. **Add Composite Index for IsFolder + Type**
```sql
CREATE INDEX CONCURRENTLY idx_baseitems_isfolder_type
ON library."BaseItems" ("IsFolder", "Type");
```
3. **Add Index on TvExtras.SeriesId**
```sql
CREATE INDEX CONCURRENTLY idx_baseitem_tv_extras_seriesid
ON library."BaseItemTvExtras" ("SeriesId");
```
### Short-term (Application Changes)
1. **Cache Series child counts** to avoid repeated queries
2. **Lazy-load TvExtras** only when needed (not always via Include)
3. **Optimize IsPlayed query** for Series - precompute in separate query
### Long-term (Architecture Changes)
1. **Denormalize Series child count** into BaseItems table
2. **Create materialized view** for Series with episode count, last played date
3. **Use read-only replica** for expensive Series queries
4. **Implement Series-specific cache layer** in BaseItemRepository
---
## Summary: Why TV Shows Are Slower
| Factor | Impact Level | Reason |
|--------|--------------|--------|
| **Extra Table Joins** | ⭐⭐⭐⭐⭐ | TvExtras, tag checks, user data |
| **In-Memory Deduplication** | ⭐⭐⭐⭐ | All episodes loaded, deduplicated in C# |
| **Missing Indexes** | ⭐⭐⭐⭐ | No Type or (IsFolder, Type) indexes |
| **Correlated Subqueries** | ⭐⭐⭐⭐ | IsPlayed, tag inheritance, child counts |
| **Complex Filtering** | ⭐⭐⭐ | Tag inheritance from parent Series |
| **AsSingleQuery()** | ⭐⭐ | One huge JOIN chain, not optimizable |
**Total Impact**: TV Shows queries can be **10-100x slower** than Movies, depending on library size and query type.
-292
View File
@@ -1,292 +0,0 @@
# TV Shows Actual SQL Query Patterns
## Generated SQL Examples
### Scenario 1: Get TV Shows List (When User Clicks TV Shows)
**API Call**: `/Items?IncludeItemTypes=Series&ParentId=abc123`
#### Query 1: Get Series IDs (Lines 455-460)
```sql
-- This is what gets generated with AsSingleQuery() and INCLUDE statements
SELECT
bi."Id",
bi."PresentationUniqueKey",
te."SeriesPresentationUniqueKey"
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
WHERE bi."ParentId" = 'abc123-uuid'::uuid
AND bi."Type" = 'Series'
ORDER BY bi."SortName"
LIMIT 1000;
-- Result: 500 rows (1 per Series, no duplicates because each Series has 1 row)
-- Then ApplyGroupingInMemory() does DistinctBy in C# (no-op here)
```
**Indexes Used**:
-`baseitems_parentid_idx` on `(ParentId, Type)` - **HIT**
#### Query 2: Load Full Series Entities (Lines 475-481)
```sql
-- After AsSingleQuery(), all INCLUDE statements are added to this query
SELECT
bi."Id",
bi."ParentId",
bi."Path",
bi."Name",
bi."SortName",
bi."IsFolder",
bi."Type",
bi."CommunityRating",
bi."IsLocked",
-- ... 50+ more columns ...
-- TvExtras included
te."SeriesId", te."SeasonId", te."SeriesName", te."SeasonName", te."SeriesPresentationUniqueKey",
-- Images included
img."Id" as "Images__Id", img."Path" as "Images__Path", img."ImageType" as "Images__ImageType",
-- UserData included
ud."Id" as "UserData__Id", ud."Played" as "UserData__Played", ud."IsFavorite" as "UserData__IsFavorite",
-- Provider included
pr."ProviderId" as "Provider__ProviderId", pr."ProviderValue" as "Provider__ProviderValue"
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
LEFT JOIN library."BaseItemImageInfos" img ON bi."Id" = img."ItemId"
LEFT JOIN library."UserData" ud ON bi."Id" = ud."ItemId" AND ud."UserId" = 'user-uuid'::uuid
LEFT JOIN library."BaseItemProviders" pr ON bi."Id" = pr."ItemId"
WHERE bi."Id" IN (
-- These are the 100 Series IDs from paging
'series-id-1', 'series-id-2', ... 'series-id-100'
)
ORDER BY bi."SortName";
-- Result: 1-100 rows (one per Series) with all related data as columns
```
**Indexes Used**:
- ⚠️ `PRIMARY KEY (Id)` for WHERE IN clause
- ⚠️ `UserData` index on `(UserId, ItemId)` - partial HIT
---
### Scenario 2: Get Recent Episodes (Slower - N+1 Example)
**API Call**: `/Items?IncludeItemTypes=Episode&ParentId=abc123&SortBy=DateCreated`
#### Query 1: Get Episode IDs (Lines 455-460)
```sql
-- Note: This returns EPISODES, not Series!
SELECT
bi."Id",
bi."PresentationUniqueKey",
te."SeriesPresentationUniqueKey" -- Each Episode linked to its Series
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
WHERE bi."ParentId" = 'season-id'::uuid -- Parent is Season
AND bi."Type" = 'Episode'
ORDER BY bi."DateCreated" DESC
LIMIT 1000;
-- Result: 10,000 rows (one per Episode!)
-- If you wanted to deduplicate by Series:
-- ApplyGroupingInMemory() → DistinctBy(SeriesPresentationUniqueKey)
-- Result: 500 unique Series, but 10,000 rows loaded from DB
```
**Indexes Used**:
-`baseitems_parentid_idx` on `(ParentId, Type)` - **HIT**
**Performance Problem**: Loaded 10,000 rows just to deduplicate to 500
#### Query 2: Load Full Episodes (Lines 475-481)
```sql
-- Loads full entities for paginated subset of 100 episodes
SELECT
bi."Id",
bi."Name",
bi."IndexNumber",
bi."ParentIndexNumber",
-- ... all columns ...
te."SeriesId", te."SeasonId", te."SeriesName",
-- ... images, userdata, provider ...
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
-- ... other JOINs ...
WHERE bi."Id" IN (
'episode-id-1', 'episode-id-2', ... 'episode-id-100'
);
-- Result: 100 rows (paged subset)
```
---
### Scenario 3: Filter By Tag (Slow - Correlated Subquery)
**API Call**: `/Items?IncludeItemTypes=Episode&Tags=Action`
#### Query 1: Get Episode IDs with Tag Inheritance (Line 3427-3438)
```sql
-- COMPLEX: Must check Episode's tags AND parent Series' tags
SELECT
bi."Id",
bi."PresentationUniqueKey",
te."SeriesPresentationUniqueKey"
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
WHERE bi."Type" = 'Episode'
AND (
-- Episode has tag
EXISTS (
SELECT 1 FROM library."ItemValuesMap" ivm
INNER JOIN library."ItemValues" iv ON ivm."ItemValueId" = iv."ItemValueId"
WHERE ivm."ItemId" = bi."Id"
AND iv."Type" = 1 -- Tags
AND iv."CleanValue" = 'action'
)
-- OR parent Series has tag
OR EXISTS (
SELECT 1 FROM library."ItemValuesMap" ivm
INNER JOIN library."ItemValues" iv ON ivm."ItemValueId" = iv."ItemValueId"
WHERE ivm."ItemId" = te."SeriesId"
AND iv."Type" = 1 -- Tags
AND iv."CleanValue" = 'action'
)
)
ORDER BY bi."DateCreated" DESC;
-- Result: Uncertain number of Episodes with tag or tagged Series
```
**Indexes Used**:
- ❌ NO index on `SeriesId` in TvExtras (MISSING!)
- ⚠️ ItemValuesMap indexes might be used if they exist
**Performance Problem**:
- No index on `TvExtras.SeriesId` forces table scan
- Complex EXISTS conditions can't be optimized well
---
### Scenario 4: Filter By IsPlayed=true on Series (Correlated Subquery)
**API Call**: `/Items?IncludeItemTypes=Series&IsPlayed=true`
#### Query 1: Get Series That Have Played Episodes (Line 2995-3000)
```sql
-- SPECIAL CASE for Series: Check if ANY episode was played
SELECT
bi."Id",
bi."PresentationUniqueKey",
te."SeriesPresentationUniqueKey"
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
WHERE bi."Type" = 'Series'
AND EXISTS (
-- Correlated subquery: For THIS series, check all episodes
SELECT 1 FROM library."BaseItems" episodes
LEFT JOIN library."UserData" ud ON episodes."Id" = ud."ItemId"
LEFT JOIN library."BaseItemTvExtras" epi_te ON episodes."Id" = epi_te."ItemId"
WHERE episodes."IsFolder" = false
AND episodes."IsVirtualItem" = false
AND epi_te."SeriesPresentationUniqueKey" = te."SeriesPresentationUniqueKey"
AND ud."Played" = true
AND ud."UserId" = 'user-uuid'::uuid
);
-- Result: Only Series with at least one played episode
-- PERFORMANCE: For each Series, this checks ALL episodes in library!
```
**Indexes Used**:
- ❌ Correlated subquery might not use indexes well
- ⚠️ `UserData (UserId, Played)` index exists but subquery is still expensive
**Performance Problem**:
- Multiplies query cost by number of Series × average episodes per Series
- 500 Series = 500 separate correlated subqueries
- If library has 10,000 episodes total: potential for 5,000,000 rows examined
---
### Scenario 5: MISSING INDEXES Example
**API Call**: `/Items?IncludeItemTypes=Series` (No ParentId specified)
#### Query Without Index
```sql
-- No ParentId filter, just type filter
SELECT
bi."Id",
bi."PresentationUniqueKey",
te."SeriesPresentationUniqueKey"
FROM library."BaseItems" bi
LEFT JOIN library."BaseItemTvExtras" te ON bi."Id" = te."ItemId"
WHERE bi."Type" = 'Series' -- ❌ NO INDEX ON TYPE ALONE!
ORDER BY bi."SortName";
-- Index lookup:
-- - baseitems_parentid_idx: (ParentId, Type) - CAN'T USE (no ParentId filter)
-- - baseitems_topparentid_idx: (TopParentId, Type) - CAN'T USE (no TopParentId filter)
-- - baseitems_sortname_idx: (SortName) - CAN'T USE (Type filter first)
--
-- Result: FULL TABLE SCAN of all BaseItems!
```
**Optimization**: Need index on `Type` or `(Type, SortName)`
---
### Index Usage Breakdown
**Current Indexes** (from migration 20260226170000):
| Index | Used When | Misses |
|-------|-----------|--------|
| `baseitems_parentid_idx (ParentId, Type)` | ParentId + Type filter | No Type-only queries |
| `baseitems_topparentid_idx (TopParentId, Type)` | TopParentId + Type filter | Type-only queries |
| `baseitems_sortname_idx (SortName)` | ORDER BY SortName | Can't use if Type filter first |
| `baseitems_seriespresentationuniquekey_idx (SeriesPresentationUniqueKey, ...)` | Episode queries | Series queries without this key |
**Missing Critical Indexes**:
```sql
-- 1. Type filter without Parent/TopParent
CREATE INDEX CONCURRENTLY idx_baseitems_type
ON library."BaseItems" ("Type");
-- 2. Series query with sorting
CREATE INDEX CONCURRENTLY idx_baseitems_type_sortname
ON library."BaseItems" ("Type", "SortName");
-- 3. Check if Series (IsFolder + Type)
CREATE INDEX CONCURRENTLY idx_baseitems_isfolder_type
ON library."BaseItems" ("IsFolder", "Type");
-- 4. Tag inheritance for Episodes
CREATE INDEX CONCURRENTLY idx_baseitems_tvextras_seriesid
ON library."BaseItemTvExtras" ("SeriesId", "ItemId");
-- 5. IsPlayed queries
CREATE INDEX CONCURRENTLY idx_userdata_userid_played
ON library."UserData" ("UserId", "Played");
```
---
## Summary: Why These Queries Are Slow
1. **In-memory deduplication**: Loads 10,000 rows to return 500
2. **Correlated subqueries**: IsPlayed × Series count = thousands of subqueries
3. **Missing indexes**: Type-only queries do full table scans
4. **Complex JOINs**: AsSingleQuery() creates massive JOIN chain
5. **Tag inheritance**: Must check two tables for every tag filter
6. **TvExtras required**: Every query must JOIN to TvExtras table
**Net Result**: TV Shows queries are **10-100x slower** than equivalent Movie queries
-262
View File
@@ -1,262 +0,0 @@
# Linux Package Building
This directory contains scripts and templates for building Jellyfin Linux packages for both Debian and Red Hat-based distributions.
## Quick Start
### Build Both Packages
```bash
cd /home/wjones/projects/pgsql-jellyfin
chmod +x build-linux-packages.sh
./build-linux-packages.sh
```
Packages will be created in `build/packages/`.
### Build Specific Distribution
```bash
# Debian only
./build-linux-packages.sh --deb-only
# Red Hat only
./build-linux-packages.sh --rpm-only
# Specific version
./build-linux-packages.sh --version 11.0.0
```
## Directory Structure
```
scripts/
├── build-linux-packages.sh # Main automated build script
├── jellyfin.spec # Red Hat SPEC file template
├── debian/
│ ├── postinst.sh # Post-install hook (Debian)
│ ├── prerm.sh # Pre-remove hook (Debian)
│ ├── postrm.sh # Post-remove hook (Debian)
│ └── jellyfin.service # Systemd service file (Debian)
└── redhat/
├── post.sh # Post-install hook (Red Hat)
├── preun.sh # Pre-uninstall hook (Red Hat)
├── postun.sh # Post-uninstall hook (Red Hat)
└── jellyfin.service # Systemd service file (Red Hat)
```
## Prerequisites
### All Systems
- .NET SDK (for publishing the application)
- `fpm` (Effing Package Manager) for cross-distribution building
### Install FPM
**Debian/Ubuntu:**
```bash
sudo apt-get install ruby-dev
sudo gem install fpm
```
**Red Hat/CentOS/Fedora:**
```bash
sudo dnf install ruby-devel
sudo gem install fpm
```
## Package Specifications
### Publishing Parameters
Both distributions use identical .NET publishing parameters:
- **Runtime**: `linux-x64` (64-bit Linux)
- **Configuration**: `Release` (optimized build)
- **Self-contained**: `true` (includes .NET runtime)
- **Output location**: `/opt/jellyfin` (standard FHS compliant)
### Package Details
| Component | Debian | Red Hat |
|-----------|--------|---------|
| Extension | `.deb` | `.rpm` |
| Install command | `dpkg -i` / `apt install` | `dnf install` |
| Application dir | `/opt/jellyfin` | `/opt/jellyfin` |
| Config dir | `/etc/jellyfin` | `/etc/jellyfin` |
| Data dir | `/var/lib/jellyfin` | `/var/lib/jellyfin` |
| Log dir | `/var/log/jellyfin` | `/var/log/jellyfin` |
| Service file | `/etc/systemd/system/` | `/etc/systemd/system/` |
| User | `jellyfin` (system user) | `jellyfin` (system user) |
## Installation
### Debian
```bash
# Install package
sudo dpkg -i jellyfin-11.0.0-amd64.deb
sudo apt-get install -f # Fix dependencies if needed
# Start service
sudo systemctl start jellyfin
sudo systemctl status jellyfin
# View logs
sudo journalctl -u jellyfin -f
```
### Red Hat
```bash
# Install package
sudo dnf install jellyfin-11.0.0-1.x86_64.rpm
# Start service
sudo systemctl start jellyfin
sudo systemctl status jellyfin
# View logs
sudo journalctl -u jellyfin -f
```
## Verification
### Inspect Package Contents
**Debian:**
```bash
dpkg -c jellyfin-11.0.0-amd64.deb
```
**Red Hat:**
```bash
rpm -qpl jellyfin-11.0.0-1.x86_64.rpm
```
### Test Install/Remove Cycle
```bash
# Install
sudo dpkg -i jellyfin-11.0.0-amd64.deb # or dnf install for Red Hat
# Verify
sudo systemctl status jellyfin
# Remove
sudo dpkg -r jellyfin # or dnf remove for Red Hat
```
## Build Details
### What the Build Script Does
1. **Checks prerequisites** - Ensures .NET SDK and fpm are available
2. **Restores dependencies** - `dotnet restore Jellyfin.sln -r linux-x64`
3. **Builds solution** - `dotnet build Jellyfin.sln --configuration Release`
4. **Publishes application** - Creates self-contained linux-x64 binary
5. **Generates Debian package** - Creates `.deb` with hooks and metadata
6. **Generates Red Hat package** - Creates `.rpm` with hooks and metadata
7. **Reports completion** - Displays package paths and sizes
### Build Artifacts
Output location: `<project-root>/build/packages/`
- `jellyfin-11.0.0-amd64.deb` - Debian package
- `jellyfin-11.0.0-1.x86_64.rpm` - Red Hat package
## Hooks and Lifecycle
Both packages include installation and removal hooks:
### Installation
- Creates `jellyfin` system user if not exists
- Creates required directories (lib, log, cache)
- Sets proper ownership and permissions
- Enables and optionally starts systemd service
### Removal
- Stops systemd service
- Disables service from auto-start
- Removes `jellyfin` user and group
## Service Configuration
The included `jellyfin.service` file provides:
- **Auto-restart** on failure (5s delay)
- **Resource limits** (4GB max memory, 65k file descriptors)
- **Proper logging** (journald integration)
- **Directory configuration**:
- `--datadir=/var/lib/jellyfin` (configuration and metadata)
- `--logdir=/var/log/jellyfin` (application logs)
- `--cachedir=/var/cache/jellyfin` (temporary cache)
## Customization
### Change Version
Edit `SharedVersion.cs` or use build script flag:
```bash
./build-linux-packages.sh --version 12.0.0
```
### Modify Dependencies
Edit install scripts or update FPM dependencies in `build-linux-packages.sh`:
```bash
--depends "libssl3" \
--depends "libicu72" \
```
### Add Custom Hooks
Edit the respective hook scripts in `debian/` or `redhat/` directories.
## Documentation
For detailed information about the build process, see:
- [LINUX_PACKAGE_BUILD_GUIDE.md](../docs/LINUX_PACKAGE_BUILD_GUIDE.md) - Complete guide with examples
## Troubleshooting
### FPM not found
```bash
# Debian/Ubuntu
sudo apt-get install ruby-dev && sudo gem install fpm
# Red Hat/CentOS
sudo dnf install ruby-devel && sudo gem install fpm
```
### Missing dependencies during install
For Debian, run:
```bash
sudo apt-get install -f
```
For Red Hat, the package manager will automatically resolve.
### Permission denied when running script
```bash
chmod +x build-linux-packages.sh
```
### Check build details
Edit `build-linux-packages.sh` and remove `> /dev/null 2>&1` to see verbose output.
## Related Files
- **Build Script**: `build-linux-packages.sh`
- **SPEC Template**: `scripts/jellyfin.spec`
- **Debian Hooks**: `scripts/debian/{postinst,prerm,postrm}.sh`
- **Red Hat Hooks**: `scripts/redhat/{post,preun,postun}.sh`
- **Service Files**: Both distributions in `scripts/{debian,redhat}/jellyfin.service`
- **Complete Guide**: `docs/LINUX_PACKAGE_BUILD_GUIDE.md`
-31
View File
@@ -1,31 +0,0 @@
[Unit]
Description=Jellyfin Media Server
After=network.target
[Service]
Type=simple
User=jellyfin
Group=jellyfin
WorkingDirectory=/var/lib/jellyfin
# Run jellyfin with proper configuration directories
ExecStart=/opt/jellyfin/jellyfin \
--datadir=/var/lib/jellyfin \
--logdir=/var/log/jellyfin \
--cachedir=/var/cache/jellyfin
# Restart behavior
Restart=on-failure
RestartSec=5s
# Resource limits
LimitNOFILE=65536
MemoryMax=4G
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=jellyfin
[Install]
WantedBy=multi-user.target
-83
View File
@@ -1,83 +0,0 @@
#!/bin/bash
# Jellyfin postinst script - runs after package installation
set -e
# Create jellyfin user if it doesn't exist
if ! getent passwd jellyfin > /dev/null; then
useradd --system \
--home /var/lib/jellyfin \
--shell /bin/false \
--comment "Jellyfin Media Server" \
jellyfin
fi
# Create necessary directories
mkdir -p /var/lib/jellyfin
mkdir -p /var/log/jellyfin
mkdir -p /var/cache/jellyfin
mkdir -p /etc/jellyfin
# Set proper ownership and permissions
chown -R jellyfin:jellyfin /var/lib/jellyfin
chown -R jellyfin:jellyfin /var/log/jellyfin
chown -R jellyfin:jellyfin /var/cache/jellyfin
chown -R jellyfin:jellyfin /etc/jellyfin
chmod 755 /var/lib/jellyfin
chmod 755 /var/log/jellyfin
chmod 755 /var/cache/jellyfin
chmod 755 /etc/jellyfin
# Set permissions on the application directory
chown -R jellyfin:jellyfin /opt/jellyfin
chmod -R 755 /opt/jellyfin
# Create systemd service file if it doesn't exist
if [[ ! -f /etc/systemd/system/jellyfin.service ]]; then
cat > /etc/systemd/system/jellyfin.service << 'EOF'
[Unit]
Description=Jellyfin Media Server
After=network.target
[Service]
Type=simple
User=jellyfin
Group=jellyfin
WorkingDirectory=/var/lib/jellyfin
ExecStart=/opt/jellyfin/jellyfin --webdir=/opt/jellyfin/wwwroot --logdir=/var/log/jellyfin --datadir=/var/lib/jellyfin --cachedir=/var/cache/jellyfin
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
[Install]
WantedBy=multi-user.target
EOF
fi
# Reload systemd daemon
systemctl daemon-reload
# Enable and start the service
if [[ -f /etc/systemd/system/jellyfin.service ]]; then
systemctl enable jellyfin.service
systemctl restart jellyfin.service || true
fi
# Display post-installation message
echo ""
echo "=========================================="
echo "Jellyfin Media Server installed!"
echo "=========================================="
echo ""
echo "Service status: systemctl status jellyfin"
echo "View logs: journalctl -u jellyfin -f"
echo "Configuration: /etc/jellyfin/"
echo "Data: /var/lib/jellyfin/"
echo "Logs: /var/log/jellyfin/"
echo ""
echo "Access Jellyfin at: http://localhost:8096"
echo ""
exit 0
-34
View File
@@ -1,34 +0,0 @@
#!/bin/bash
set -e
# Jellyfin Post-Installation Script (Debian)
# Runs after package is installed
# Create jellyfin user/group if not exists
if ! id "jellyfin" &>/dev/null; then
echo "Creating jellyfin user and group..."
useradd -r -s /bin/false -d /var/lib/jellyfin jellyfin
fi
# Create necessary directories
mkdir -p /var/lib/jellyfin
mkdir -p /var/log/jellyfin
mkdir -p /etc/jellyfin
# Set ownership and permissions
chown -R jellyfin:jellyfin /opt/jellyfin
chown -R jellyfin:jellyfin /var/lib/jellyfin
chown -R jellyfin:jellyfin /var/log/jellyfin
chmod 755 /opt/jellyfin
chmod 750 /var/lib/jellyfin
chmod 750 /var/log/jellyfin
# Enable and start service if systemd is available
if command -v systemctl &> /dev/null; then
systemctl daemon-reload || true
systemctl enable jellyfin.service || true
echo "To start Jellyfin, run: sudo systemctl start jellyfin"
fi
echo "Jellyfin post-install complete!"
-29
View File
@@ -1,29 +0,0 @@
#!/bin/bash
# Jellyfin postrm script - runs after package removal
set -e
# Remove systemd service file
if [[ -f /etc/systemd/system/jellyfin.service ]]; then
rm -f /etc/systemd/system/jellyfin.service
systemctl daemon-reload
fi
# Optional: Uncomment the lines below to completely remove user and data on uninstall
# WARNING: This will delete all Jellyfin data, libraries, and settings!
# Remove jellyfin user
# userdel jellyfin || true
# Remove data directories
# rm -rf /var/lib/jellyfin
# rm -rf /var/log/jellyfin
# rm -rf /var/cache/jellyfin
# rm -rf /etc/jellyfin
echo "Jellyfin Media Server uninstalled."
echo "Note: User data in /var/lib/jellyfin has been preserved."
echo "To completely remove all data, run:"
echo " sudo rm -rf /var/lib/jellyfin /var/log/jellyfin /var/cache/jellyfin /etc/jellyfin"
exit 0
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
# Jellyfin Post-Removal Script (Debian)
# Runs after package is removed
# Remove jellyfin user/group
if id "jellyfin" &>/dev/null; then
echo "Removing jellyfin user and group..."
userdel jellyfin || true
fi
echo "Jellyfin has been removed."
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
# Jellyfin prerm script - runs before package removal
set -e
# Stop the service
if [[ -f /etc/systemd/system/jellyfin.service ]]; then
systemctl stop jellyfin.service || true
systemctl disable jellyfin.service || true
fi
exit 0
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
set -e
# Jellyfin Pre-Removal Script (Debian)
# Runs before package is removed
# Stop service
if command -v systemctl &> /dev/null; then
echo "Stopping Jellyfin service..."
systemctl stop jellyfin.service || true
systemctl disable jellyfin.service || true
fi
-76
View File
@@ -1,76 +0,0 @@
Name: jellyfin
Version: 11.0.0
Release: 1%{?dist}
Summary: Jellyfin Media Server
License: GPL-2.0
URL: https://jellyfin.org
Source0: %{name}-%{version}.tar.gz
BuildArch: x86_64
BuildRequires: rpm-build
Requires: openssl-libs
Requires: libicu
Requires: fontconfig
Requires: systemd
%description
Jellyfin is a Free Software Media System that puts you in control of managing
and streaming your media. Run the Jellyfin server and access it from a web
browser, mobile app, media player, or other client application.
%prep
%setup -q
%build
# No build needed - we're packaging pre-built artifacts
%install
mkdir -p %{buildroot}/opt/jellyfin
mkdir -p %{buildroot}/var/lib/jellyfin
mkdir -p %{buildroot}/var/log/jellyfin
mkdir -p %{buildroot}/var/cache/jellyfin
mkdir -p %{buildroot}/etc/jellyfin
# Copy application files
cp -r . %{buildroot}/opt/jellyfin/
%pre
# Create jellyfin user/group if not exists
if ! id "jellyfin" &>/dev/null; then
useradd -r -s /bin/false -d /var/lib/jellyfin jellyfin
fi
%post
# Set ownership and permissions
chown -R jellyfin:jellyfin /opt/jellyfin
chown -R jellyfin:jellyfin /var/lib/jellyfin
chown -R jellyfin:jellyfin /var/log/jellyfin
chown -R jellyfin:jellyfin /var/cache/jellyfin
chmod 755 /opt/jellyfin
chmod 750 /var/lib/jellyfin
chmod 750 /var/log/jellyfin
chmod 750 /var/cache/jellyfin
# Enable and start service
systemctl daemon-reload || true
systemctl enable jellyfin.service || true
%preun
# Stop service before uninstall
systemctl stop jellyfin.service || true
systemctl disable jellyfin.service || true
%postun
# Remove user/group
userdel jellyfin || true
%files
/opt/jellyfin
%dir /var/lib/jellyfin
%dir /var/log/jellyfin
%dir /var/cache/jellyfin
%changelog
* Sun Jul 13 2026 Jellyfin Team <jellyfin@jellyfin.org> - 11.0.0-1
- Initial release
-156
View File
@@ -1,156 +0,0 @@
#!/bin/bash
# Jellyfin Debian Package Builder
# This script builds a .deb package from the current publishing configuration
# Usage: ./scripts/linux/build-deb-package.sh [--no-clean] [--output-dir PATH]
set -e
# Configuration
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
BUILD_TEMP_DIR=""
OUTPUT_DIR="${OUTPUT_DIR:-$PROJECT_ROOT/dist}"
CLEAN_AFTER=true
VERBOSE=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--no-clean)
CLEAN_AFTER=false
shift
;;
--output-dir)
OUTPUT_DIR="$2"
shift 2
;;
--verbose)
VERBOSE=true
shift
;;
*)
echo "Unknown option: $1"
echo "Usage: $0 [--no-clean] [--output-dir PATH] [--verbose]"
exit 1
;;
esac
done
# Functions
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
error() {
echo "[ERROR] $*" >&2
exit 1
}
cleanup() {
if [[ "$CLEAN_AFTER" == true ]] && [[ -n "$BUILD_TEMP_DIR" ]] && [[ -d "$BUILD_TEMP_DIR" ]]; then
log "Cleaning up temporary directory: $BUILD_TEMP_DIR"
rm -rf "$BUILD_TEMP_DIR"
fi
}
trap cleanup EXIT
# Extract version
log "Reading version from SharedVersion.cs..."
if [[ ! -f "$PROJECT_ROOT/SharedVersion.cs" ]]; then
error "SharedVersion.cs not found in $PROJECT_ROOT"
fi
VERSION=$(grep -oP 'Version\s*=\s*"\K[^"]+' "$PROJECT_ROOT/SharedVersion.cs")
if [[ -z "$VERSION" ]]; then
error "Could not extract version from SharedVersion.cs"
fi
log "Version: $VERSION"
# Create output directory
mkdir -p "$OUTPUT_DIR"
log "Output directory: $OUTPUT_DIR"
# Create temporary build directory
BUILD_TEMP_DIR=$(mktemp -d)
log "Temporary build directory: $BUILD_TEMP_DIR"
# Build and publish
cd "$PROJECT_ROOT"
log "Restoring dependencies..."
dotnet restore Jellyfin.sln -r linux-x64 > /dev/null 2>&1 || \
error "Failed to restore dependencies"
log "Building solution..."
dotnet build Jellyfin.sln --configuration Release > /dev/null 2>&1 || \
error "Failed to build solution"
log "Publishing Jellyfin.Server..."
if ! dotnet publish "Jellyfin.Server/Jellyfin.Server.csproj" \
--configuration Release \
--self-contained true \
--runtime linux-x64 \
--output "$BUILD_TEMP_DIR" > /dev/null 2>&1; then
error "Failed to publish Jellyfin.Server"
fi
# Verify published output
if [[ ! -f "$BUILD_TEMP_DIR/jellyfin" ]]; then
error "Published files not found in $BUILD_TEMP_DIR"
fi
log "Published files size: $(du -sh "$BUILD_TEMP_DIR" | cut -f1)"
# Create Debian package
PACKAGE_NAME="jellyfin-${VERSION}_amd64.deb"
PACKAGE_PATH="$OUTPUT_DIR/$PACKAGE_NAME"
log "Creating Debian package: $PACKAGE_NAME"
# Check if fpm is installed
if ! command -v fpm &> /dev/null; then
error "fpm is not installed. Install it with: sudo gem install fpm"
fi
# Build the package
if fpm \
-s dir \
-t deb \
-n jellyfin \
-v "$VERSION" \
-C "$BUILD_TEMP_DIR" \
-p "$PACKAGE_PATH" \
--license "LICENSE" \
--vendor "Jellyfin Contributors" \
--maintainer "Jellyfin Team <team@jellyfin.org>" \
--description "Jellyfin Media Server - a free software media server" \
--url "https://jellyfin.org" \
--architecture x86_64 \
--depends "libssl3" \
--depends "libicu72" \
--depends "libfontconfig1" \
--depends "libc6 (>= 2.31)" \
--before-remove "$PROJECT_ROOT/scripts/debian/prerm" \
--after-install "$PROJECT_ROOT/scripts/debian/postinst" \
--after-remove "$PROJECT_ROOT/scripts/debian/postrm" \
opt/=opt/jellyfin; then
log "Package created successfully!"
log "Package path: $PACKAGE_PATH"
log "Package size: $(du -sh "$PACKAGE_PATH" | cut -f1)"
# Display package information
log ""
log "Package Information:"
dpkg-deb -I "$PACKAGE_PATH" | sed 's/^/ /'
log ""
log "Installation command:"
log " sudo dpkg -i \"$PACKAGE_PATH\""
log ""
log "If dependencies are missing, run:"
log " sudo apt-get install -f"
else
error "Failed to create Debian package"
fi
-31
View File
@@ -1,31 +0,0 @@
[Unit]
Description=Jellyfin Media Server
After=network.target
[Service]
Type=simple
User=jellyfin
Group=jellyfin
WorkingDirectory=/var/lib/jellyfin
# Run jellyfin with proper configuration directories
ExecStart=/opt/jellyfin/jellyfin \
--datadir=/var/lib/jellyfin \
--logdir=/var/log/jellyfin \
--cachedir=/var/cache/jellyfin
# Restart behavior
Restart=on-failure
RestartSec=5s
# Resource limits
LimitNOFILE=65536
MemoryMax=4G
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=jellyfin
[Install]
WantedBy=multi-user.target
-37
View File
@@ -1,37 +0,0 @@
#!/bin/bash
set -e
# Jellyfin Post-Installation Script (Red Hat)
# Runs after package is installed
# Create jellyfin user/group if not exists
if ! id "jellyfin" &>/dev/null; then
echo "Creating jellyfin user and group..."
useradd -r -s /bin/false -d /var/lib/jellyfin jellyfin
fi
# Create necessary directories
mkdir -p /var/lib/jellyfin
mkdir -p /var/log/jellyfin
mkdir -p /var/cache/jellyfin
mkdir -p /etc/jellyfin
# Set ownership and permissions
chown -R jellyfin:jellyfin /opt/jellyfin
chown -R jellyfin:jellyfin /var/lib/jellyfin
chown -R jellyfin:jellyfin /var/log/jellyfin
chown -R jellyfin:jellyfin /var/cache/jellyfin
chmod 755 /opt/jellyfin
chmod 750 /var/lib/jellyfin
chmod 750 /var/log/jellyfin
chmod 750 /var/cache/jellyfin
# Enable and start service if systemd is available
if command -v systemctl &> /dev/null; then
systemctl daemon-reload || true
systemctl enable jellyfin.service || true
echo "To start Jellyfin, run: sudo systemctl start jellyfin"
fi
echo "Jellyfin post-install complete!"
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
# Jellyfin Post-Uninstall Script (Red Hat)
# Runs after package is removed
# Remove jellyfin user/group
if id "jellyfin" &>/dev/null; then
echo "Removing jellyfin user and group..."
userdel jellyfin || true
fi
echo "Jellyfin has been removed."
-12
View File
@@ -1,12 +0,0 @@
#!/bin/bash
set -e
# Jellyfin Pre-Uninstall Script (Red Hat)
# Runs before package is removed
# Stop service
if command -v systemctl &> /dev/null; then
echo "Stopping Jellyfin service..."
systemctl stop jellyfin.service || true
systemctl disable jellyfin.service || true
fi