Add scripts for PostgreSQL migration and SQLite removal; update project references
- Introduced `publish-with-sql.ps1` to publish Jellyfin with SQL files. - Added `remove-sqlite.ps1` to facilitate the removal of SQLite dependencies for PostgreSQL-only deployment. - Created `rollback-to-net10.ps1` to revert .NET 11 changes back to .NET 10. - Implemented `test_api.py` for testing API interactions. - Added `verify-migration.ps1` to verify PostgreSQL migration steps. - Updated various `.csproj` files to include `Microsoft.Kiota.Abstractions` package. - Enhanced `JellyfinDbContext` to handle concurrency exceptions during save operations. - Updated tests to include new package references and ensure compatibility with changes.
This commit is contained in:
@@ -50,6 +50,7 @@
|
|||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="11.0.0-preview.1.26104.118" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="11.0.0-preview.1.26104.118" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="11.0.0-preview.1.26104.118" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="11.0.0-preview.1.26104.118" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="11.0.0-preview.1.26104.118" />
|
<PackageVersion Include="Microsoft.Extensions.Options" Version="11.0.0-preview.1.26104.118" />
|
||||||
|
<PackageVersion Include="Microsoft.Kiota.Abstractions" Version="1.22.0" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||||
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
<PackageVersion Include="MimeTypes" Version="2.5.2" />
|
||||||
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
|
<PackageVersion Include="Morestachio" Version="5.0.1.670" />
|
||||||
|
|||||||
@@ -32,6 +32,10 @@
|
|||||||
<Compile Include="../SharedVersion.cs" />
|
<Compile Include="../SharedVersion.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
|
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
|
||||||
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
|
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="TagLibSharp" />
|
<PackageReference Include="TagLibSharp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" />
|
||||||
<PackageReference Include="DotNet.Glob" />
|
<PackageReference Include="DotNet.Glob" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" />
|
<PackageReference Include="Swashbuckle.AspNetCore.ReDoc" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncKeyedLock" />
|
<PackageReference Include="AsyncKeyedLock" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Queries;
|
using Jellyfin.Database.Implementations;
|
||||||
using Jellyfin.Extensions.Dapper;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Plugins;
|
using MediaBrowser.Controller.Plugins;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Server.Implementations.ScheduledTasks
|
namespace Jellyfin.Server.Implementations.ScheduledTasks
|
||||||
@@ -19,17 +19,17 @@ namespace Jellyfin.Server.Implementations.ScheduledTasks
|
|||||||
public class PostgresPeriodicTuner : IScheduledTask, IConfigurableScheduledTask
|
public class PostgresPeriodicTuner : IScheduledTask, IConfigurableScheduledTask
|
||||||
{
|
{
|
||||||
private readonly ILogger<PostgresPeriodicTuner> _logger;
|
private readonly ILogger<PostgresPeriodicTuner> _logger;
|
||||||
private readonly IDbContext _dbContext;
|
private readonly IDbContextFactory<JellyfinDbContext> _dbContextFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PostgresPeriodicTuner"/> class.
|
/// Initializes a new instance of the <see cref="PostgresPeriodicTuner"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="dbContext">The database context.</param>
|
/// <param name="dbContextFactory">The database context factory.</param>
|
||||||
public PostgresPeriodicTuner(ILogger<PostgresPeriodicTuner> logger, IDbContext dbContext)
|
public PostgresPeriodicTuner(ILogger<PostgresPeriodicTuner> logger, IDbContextFactory<JellyfinDbContext> dbContextFactory)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_dbContext = dbContext;
|
_dbContextFactory = dbContextFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -56,15 +56,17 @@ namespace Jellyfin.Server.Implementations.ScheduledTasks
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the task.
|
/// Executes the task.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <param name="progress">The progress.</param>
|
/// <param name="progress">The progress.</param>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
public async Task ExecuteAsync(CancellationToken cancellationToken, IProgress<double> progress)
|
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("PostgreSQL Periodic Tuner task started.");
|
_logger.LogInformation("PostgreSQL Periodic Tuner task started.");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
const string DeadTupleQuery = @"
|
const string DeadTupleQuery = @"
|
||||||
SELECT
|
SELECT
|
||||||
relname AS TableName,
|
relname AS TableName,
|
||||||
@@ -76,7 +78,7 @@ namespace Jellyfin.Server.Implementations.ScheduledTasks
|
|||||||
ORDER BY
|
ORDER BY
|
||||||
n_dead_tup DESC;";
|
n_dead_tup DESC;";
|
||||||
|
|
||||||
var tablesToVacuum = (await _dbContext.QueryAsync<(string TableName, long DeadTuples)>(DeadTupleQuery)).ToList();
|
var tablesToVacuum = (await dbContext.Database.SqlQueryRaw<(string TableName, long DeadTuples)>(DeadTupleQuery).ToListAsync(cancellationToken).ConfigureAwait(false)).ToList();
|
||||||
|
|
||||||
if (tablesToVacuum.Count == 0)
|
if (tablesToVacuum.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -96,7 +98,7 @@ namespace Jellyfin.Server.Implementations.ScheduledTasks
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _dbContext.ExecuteAsync($"VACUUM ANALYZE \"{table.TableName}\"");
|
await dbContext.Database.ExecuteSqlInterpolatedAsync($"VACUUM ANALYZE \"{table.TableName}\"", cancellationToken).ConfigureAwait(false);
|
||||||
_logger.LogInformation("Successfully vacuumed and analyzed table {TableName}.", table.TableName);
|
_logger.LogInformation("Successfully vacuumed and analyzed table {TableName}.", table.TableName);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -129,7 +131,7 @@ namespace Jellyfin.Server.Implementations.ScheduledTasks
|
|||||||
{
|
{
|
||||||
new TaskTriggerInfo
|
new TaskTriggerInfo
|
||||||
{
|
{
|
||||||
Type = TaskTriggerInfo.TriggerInterval,
|
Type = TaskTriggerInfoType.IntervalTrigger,
|
||||||
IntervalTicks = TimeSpan.FromHours(24).Ticks
|
IntervalTicks = TimeSpan.FromHours(24).Ticks
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandLineParser" />
|
<PackageReference Include="CommandLineParser" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
|
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Morestachio" />
|
<PackageReference Include="Morestachio" />
|
||||||
<PackageReference Include="prometheus-net" />
|
<PackageReference Include="prometheus-net" />
|
||||||
<PackageReference Include="prometheus-net.AspNetCore" />
|
<PackageReference Include="prometheus-net.AspNetCore" />
|
||||||
@@ -121,16 +122,26 @@
|
|||||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
<Pack>false</Pack>
|
<Pack>false</Pack>
|
||||||
</None>
|
</None>
|
||||||
|
<!-- Utility scripts -->
|
||||||
|
<None Include="scripts\**\*">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
|
<Pack>false</Pack>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Post-build event to ensure SQL files are copied -->
|
<!-- Post-build event to ensure SQL and scripts files are copied -->
|
||||||
<Target Name="CopySQLFiles" AfterTargets="AfterBuild">
|
<Target Name="CopySQLFiles" AfterTargets="AfterBuild">
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<SQLFiles Include="$(MSBuildProjectDirectory)\sql\**\*.sql" />
|
<SQLFiles Include="$(MSBuildProjectDirectory)\sql\**\*.sql" />
|
||||||
|
<ScriptFiles Include="$(MSBuildProjectDirectory)\scripts\**\*" Exclude="$(MSBuildProjectDirectory)\scripts\**\*.git*" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Message Importance="high" Text="Copying SQL files to $(OutDir)sql\" />
|
<Message Importance="high" Text="Copying SQL files to $(OutDir)sql\" />
|
||||||
<Copy SourceFiles="@(SQLFiles)" DestinationFiles="@(SQLFiles->'$(OutDir)sql\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
|
<Copy SourceFiles="@(SQLFiles)" DestinationFiles="@(SQLFiles->'$(OutDir)sql\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
|
||||||
<Message Importance="high" Text="✅ SQL files copied to output directory: $(OutDir)sql\" />
|
<Message Importance="high" Text="✅ SQL files copied to output directory: $(OutDir)sql\" />
|
||||||
|
<Message Importance="high" Text="Copying script files to $(OutDir)scripts\" />
|
||||||
|
<Copy SourceFiles="@(ScriptFiles)" DestinationFiles="@(ScriptFiles->'$(OutDir)scripts\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
|
||||||
|
<Message Importance="high" Text="✅ Script files copied to output directory: $(OutDir)scripts\" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,393 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# SYNOPSIS
|
||||||
|
# Automated weekly performance monitoring for Jellyfin PostgreSQL database
|
||||||
|
#
|
||||||
|
# DESCRIPTION
|
||||||
|
# This script:
|
||||||
|
# 1. Runs comprehensive diagnostics
|
||||||
|
# 2. Analyzes slow queries
|
||||||
|
# 3. Tracks index usage over time
|
||||||
|
# 4. Generates weekly performance reports
|
||||||
|
# 5. Optionally runs VACUUM ANALYZE on tables with high dead tuples
|
||||||
|
# 6. Can be scheduled via cron
|
||||||
|
#
|
||||||
|
# USAGE
|
||||||
|
# ./Weekly-Performance-Monitor.sh [OPTIONS]
|
||||||
|
#
|
||||||
|
# OPTIONS
|
||||||
|
# -o, --output-directory DIR Directory to save reports (default: reports/weekly)
|
||||||
|
# -e, --email-report If specified, emails the report (requires email configuration)
|
||||||
|
# -c, --compare-with-previous Compare current stats with previous week
|
||||||
|
# -a, --auto-vacuum-analyze Enable auto vacuum analyze
|
||||||
|
# --dead-tuple-threshold NUM Minimum dead tuples to trigger vacuum (default: 100)
|
||||||
|
# --dead-tuple-percent-threshold NUM Minimum dead tuple percentage to trigger vacuum (default: 0.02)
|
||||||
|
#
|
||||||
|
# EXAMPLE
|
||||||
|
# ./Weekly-Performance-Monitor.sh
|
||||||
|
# ./Weekly-Performance-Monitor.sh -o "/reports/jellyfin" -c
|
||||||
|
# ./Weekly-Performance-Monitor.sh -a --dead-tuple-threshold 10000 --dead-tuple-percent-threshold 20
|
||||||
|
#
|
||||||
|
# CRON JOB EXAMPLE
|
||||||
|
# # Run every Monday at 6am
|
||||||
|
# 0 6 * * 1 /path/to/Weekly-Performance-Monitor.sh -a
|
||||||
|
#
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Default Configuration
|
||||||
|
# ============================================================================
|
||||||
|
OUTPUT_DIRECTORY="reports/weekly"
|
||||||
|
EMAIL_REPORT=false
|
||||||
|
COMPARE_WITH_PREVIOUS=false
|
||||||
|
AUTO_VACUUM_ANALYZE=false
|
||||||
|
DEAD_TUPLE_THRESHOLD=100
|
||||||
|
DEAD_TUPLE_PERCENT_THRESHOLD=0.02
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Helper Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Color definitions
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GRAY='\033[0;90m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
write_color_output() {
|
||||||
|
local message="$1"
|
||||||
|
local color="$2"
|
||||||
|
echo -e "${color}${message}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_report_timestamp() {
|
||||||
|
date +"%Y-%m-%d_%H%M%S"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_week_number() {
|
||||||
|
date +%V
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Script Argument Parsing
|
||||||
|
# ============================================================================
|
||||||
|
while [[ "$#" -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-o|--output-directory) OUTPUT_DIRECTORY="$2"; shift ;;
|
||||||
|
-e|--email-report) EMAIL_REPORT=true ;;
|
||||||
|
-c|--compare-with-previous) COMPARE_WITH_PREVIOUS=true ;;
|
||||||
|
-a|--auto-vacuum-analyze) AUTO_VACUUM_ANALYZE=true ;;
|
||||||
|
--dead-tuple-threshold) DEAD_TUPLE_THRESHOLD="$2"; shift ;;
|
||||||
|
--dead-tuple-percent-threshold) DEAD_TUPLE_PERCENT_THRESHOLD="$2"; shift ;;
|
||||||
|
*) write_color_output "Unknown parameter passed: $1" "$RED"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
# Load database configuration
|
||||||
|
source "./db-config.sh"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
test_database_connection() {
|
||||||
|
write_color_output "\nTesting database connection..." "$CYAN"
|
||||||
|
|
||||||
|
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "SELECT version();" > /dev/null 2>&1; then
|
||||||
|
write_color_output "[OK] Database connection successful" "$GREEN"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
write_color_output "[ERROR] Database connection failed" "$RED"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
invoke_diagnostic_report() {
|
||||||
|
local output_file="$1"
|
||||||
|
write_color_output "\nRunning comprehensive diagnostics..." "$CYAN"
|
||||||
|
|
||||||
|
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$PROJECT_ROOT/sql/diagnostics.sql" > "$output_file" 2>&1; then
|
||||||
|
write_color_output "[OK] Diagnostics completed: $output_file" "$GREEN"
|
||||||
|
else
|
||||||
|
write_color_output "[WARN] Diagnostics completed with warnings" "$YELLOW"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
invoke_query_analysis() {
|
||||||
|
local output_file="$1"
|
||||||
|
write_color_output "\nAnalyzing slow queries..." "$CYAN"
|
||||||
|
|
||||||
|
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$PROJECT_ROOT/sql/query-analysis.sql" > "$output_file" 2>&1; then
|
||||||
|
write_color_output "[OK] Query analysis completed: $output_file" "$GREEN"
|
||||||
|
else
|
||||||
|
write_color_output "[WARN] Query analysis completed with warnings" "$YELLOW"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_index_usage_stats() {
|
||||||
|
local output_file="$1"
|
||||||
|
write_color_output "\nGathering index usage statistics..." "$CYAN"
|
||||||
|
|
||||||
|
local query="
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
relname AS tablename,
|
||||||
|
indexrelname,
|
||||||
|
idx_scan,
|
||||||
|
idx_tup_read,
|
||||||
|
idx_tup_fetch,
|
||||||
|
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
|
||||||
|
FROM pg_stat_user_indexes
|
||||||
|
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
|
||||||
|
ORDER BY schemaname, relname, idx_scan DESC;
|
||||||
|
"
|
||||||
|
|
||||||
|
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "$query" > "$output_file" 2>&1; then
|
||||||
|
write_color_output "[OK] Index stats gathered: $output_file" "$GREEN"
|
||||||
|
else
|
||||||
|
write_color_output "[WARN] Index stats completed with warnings" "$YELLOW"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_table_size_stats() {
|
||||||
|
local output_file="$1"
|
||||||
|
write_color_output "\nGathering table size statistics..." "$CYAN"
|
||||||
|
|
||||||
|
local query="
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
relname,
|
||||||
|
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) AS total_size,
|
||||||
|
pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) AS table_size,
|
||||||
|
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname) - pg_relation_size(schemaname||'.'||relname)) AS index_size,
|
||||||
|
n_live_tup AS row_count,
|
||||||
|
n_dead_tup AS dead_rows,
|
||||||
|
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
|
||||||
|
ORDER BY pg_total_relation_size(schemaname||'.'||relname) DESC;
|
||||||
|
"
|
||||||
|
|
||||||
|
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "$query" > "$output_file" 2>&1; then
|
||||||
|
write_color_output "[OK] Table size stats gathered: $output_file" "$GREEN"
|
||||||
|
else
|
||||||
|
write_color_output "[WARN] Table size stats completed with warnings" "$YELLOW"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
invoke_conditional_vacuum_analyze() {
|
||||||
|
local min_dead_tuples="$1"
|
||||||
|
local min_dead_percent="$2"
|
||||||
|
|
||||||
|
write_color_output "\nChecking for tables that need VACUUM ANALYZE..." "$CYAN"
|
||||||
|
write_color_output "Thresholds: dead tuples >= $min_dead_tuples and dead percent >= $min_dead_percent" "$GRAY"
|
||||||
|
|
||||||
|
local table_query="
|
||||||
|
SELECT
|
||||||
|
format('%I.%I', schemaname, relname) AS qualified_table,
|
||||||
|
n_dead_tup,
|
||||||
|
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
|
||||||
|
AND n_live_tup > 0
|
||||||
|
AND n_dead_tup >= $min_dead_tuples
|
||||||
|
AND (100.0 * n_dead_tup / NULLIF(n_live_tup, 0)) >= $min_dead_percent
|
||||||
|
ORDER BY n_dead_tup DESC;
|
||||||
|
"
|
||||||
|
|
||||||
|
tables=$(PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -t -A -F "|" -c "$table_query")
|
||||||
|
|
||||||
|
if [ -z "$tables" ]; then
|
||||||
|
write_color_output "[OK] No tables exceeded dead tuple thresholds" "$GREEN"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_color_output "Found $(echo "$tables" | wc -l) table(s) requiring VACUUM ANALYZE" "$YELLOW"
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
qualified_table=$(echo "$line" | cut -d'|' -f1)
|
||||||
|
dead_tuples=$(echo "$line" | cut -d'|' -f2)
|
||||||
|
dead_percent=$(echo "$line" | cut -d'|' -f3)
|
||||||
|
|
||||||
|
write_color_output "Running VACUUM ANALYZE on $qualified_table (dead tuples: $dead_tuples, dead percent: $dead_percent)" "$CYAN"
|
||||||
|
|
||||||
|
if PGPASSWORD=$DB_PASS $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "VACUUM (ANALYZE, VERBOSE) $qualified_table;"; then
|
||||||
|
write_color_output "[OK] VACUUM ANALYZE completed for $qualified_table" "$GREEN"
|
||||||
|
else
|
||||||
|
write_color_output "[ERROR] VACUUM ANALYZE failed for $qualified_table" "$RED"
|
||||||
|
fi
|
||||||
|
done <<< "$tables"
|
||||||
|
}
|
||||||
|
|
||||||
|
compare_with_previous() {
|
||||||
|
local current_report="$1"
|
||||||
|
local output_dir="$2"
|
||||||
|
|
||||||
|
write_color_output "\nComparing with previous week's report..." "$CYAN"
|
||||||
|
|
||||||
|
previous_report=$(find "$output_dir" -name "diagnostics_*.txt" ! -name "$(basename "$current_report")" -printf '%T@ %p\n' | sort -n | tail -1 | cut -d' ' -f2-)
|
||||||
|
|
||||||
|
if [ -z "$previous_report" ]; then
|
||||||
|
write_color_output "[WARN] No previous reports found for comparison" "$YELLOW"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_color_output "Comparing with: $(basename "$previous_report")" "$CYAN"
|
||||||
|
|
||||||
|
# TODO: Add detailed comparison logic
|
||||||
|
write_color_output "[OK] Comparison data available" "$GREEN"
|
||||||
|
write_color_output " Current: $current_report" "$GRAY"
|
||||||
|
write_color_output " Previous: $previous_report" "$GRAY"
|
||||||
|
}
|
||||||
|
|
||||||
|
new_summary_report() {
|
||||||
|
local output_dir="$1"
|
||||||
|
local timestamp="$2"
|
||||||
|
|
||||||
|
write_color_output "\nGenerating summary report..." "$CYAN"
|
||||||
|
|
||||||
|
local summary_file="$output_dir/summary_$timestamp.txt"
|
||||||
|
|
||||||
|
cat > "$summary_file" <<EOF
|
||||||
|
================================================================================
|
||||||
|
JELLYFIN DATABASE WEEKLY PERFORMANCE SUMMARY
|
||||||
|
================================================================================
|
||||||
|
Report Date: $(date +"%Y-%m-%d %H:%M:%S")
|
||||||
|
Database: $DB_NAME at $DB_HOST
|
||||||
|
Week Number: $(get_week_number)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
REPORTS GENERATED:
|
||||||
|
* diagnostics_$timestamp.txt : Full diagnostic report
|
||||||
|
* query_analysis_$timestamp.txt : Slow query analysis
|
||||||
|
* index_usage_$timestamp.txt : Index usage statistics
|
||||||
|
* table_sizes_$timestamp.txt : Table size and bloat info
|
||||||
|
* summary_$timestamp.txt : This summary
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
QUICK HEALTH CHECK:
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
To view critical issues check diagnostics file for:
|
||||||
|
* Section 5: TABLES WITH HIGH SEQUENTIAL SCANS
|
||||||
|
* Section 8: UNUSED OR RARELY USED INDEXES
|
||||||
|
* Section 10: SLOWEST QUERIES
|
||||||
|
|
||||||
|
To identify slow queries check query_analysis file for:
|
||||||
|
* Section 2: TOP 20 SLOWEST QUERIES (by max execution time)
|
||||||
|
* Section 5: BASEITEMS TABLE QUERIES ANALYSIS
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
RECOMMENDED ACTIONS:
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
* Review slow queries taking longer than 10 seconds
|
||||||
|
* Check if new indexes are being utilized
|
||||||
|
* Look for tables with many sequential scans
|
||||||
|
* Monitor cache hit ratio (should be above 95 percent)
|
||||||
|
* Check for tables with high dead tuples percentage (need VACUUM)
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
AUTOMATION SETUP:
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
To schedule this script weekly, add the following to your crontab:
|
||||||
|
0 6 * * 1 /path/to/$(basename "$0")
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
NEXT REPORT: $(date -d "+7 days" +"%Y-%m-%d")
|
||||||
|
================================================================================
|
||||||
|
EOF
|
||||||
|
|
||||||
|
write_color_output "[OK] Summary generated: $summary_file" "$GREEN"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Execution
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
write_color_output "\n${CYAN}========================================${NC}"
|
||||||
|
write_color_output "Jellyfin Database Weekly Monitor" "$CYAN"
|
||||||
|
write_color_output "========================================${NC}"
|
||||||
|
write_color_output "Database: $DB_NAME" "$YELLOW"
|
||||||
|
write_color_output "Host: $DB_HOST" "$YELLOW"
|
||||||
|
write_color_output "Week: $(get_week_number)" "$YELLOW"
|
||||||
|
write_color_output "Date: $(date +'%Y-%m-%d %H:%M:%S')" "$YELLOW"
|
||||||
|
write_color_output "========================================\n${NC}"
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
report_dir="./$OUTPUT_DIRECTORY"
|
||||||
|
mkdir -p "$report_dir"
|
||||||
|
write_color_output "Created report directory: $report_dir" "$GREEN"
|
||||||
|
|
||||||
|
# Generate timestamp for this run
|
||||||
|
timestamp=$(get_report_timestamp)
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
if ! test_database_connection; then
|
||||||
|
write_color_output "\n[ERROR] Cannot connect to database. Exiting." "$RED"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run diagnostics
|
||||||
|
diagnostics_file="$report_dir/diagnostics_$timestamp.txt"
|
||||||
|
invoke_diagnostic_report "$diagnostics_file"
|
||||||
|
|
||||||
|
# Run query analysis
|
||||||
|
query_analysis_file="$report_dir/query_analysis_$timestamp.txt"
|
||||||
|
invoke_query_analysis "$query_analysis_file"
|
||||||
|
|
||||||
|
# Gather index usage stats
|
||||||
|
index_usage_file="$report_dir/index_usage_$timestamp.txt"
|
||||||
|
get_index_usage_stats "$index_usage_file"
|
||||||
|
|
||||||
|
# Gather table size stats
|
||||||
|
table_sizes_file="$report_dir/table_sizes_$timestamp.txt"
|
||||||
|
get_table_size_stats "$table_sizes_file"
|
||||||
|
|
||||||
|
# Optionally vacuum/analyze tables
|
||||||
|
if [ "$AUTO_VACUUM_ANALYZE" = true ]; then
|
||||||
|
invoke_conditional_vacuum_analyze "$DEAD_TUPLE_THRESHOLD" "$DEAD_TUPLE_PERCENT_THRESHOLD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compare with previous week
|
||||||
|
if [ "$COMPARE_WITH_PREVIOUS" = true ]; then
|
||||||
|
compare_with_previous "$diagnostics_file" "$report_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generate summary
|
||||||
|
new_summary_report "$report_dir" "$timestamp"
|
||||||
|
|
||||||
|
# Cleanup old reports (keep last 12 weeks)
|
||||||
|
write_color_output "\nCleaning up old reports (keeping last 12 weeks)..." "$CYAN"
|
||||||
|
find "$report_dir" -type f -mtime +84 -delete
|
||||||
|
write_color_output "[OK] Cleanup complete" "$GREEN"
|
||||||
|
|
||||||
|
write_color_output "\n${GREEN}========================================${NC}"
|
||||||
|
write_color_output "[OK] WEEKLY MONITORING COMPLETE" "$GREEN"
|
||||||
|
write_color_output "========================================${NC}"
|
||||||
|
write_color_output "Reports saved to: $report_dir" "$YELLOW"
|
||||||
|
write_color_output "Summary: summary_$timestamp.txt" "$YELLOW"
|
||||||
|
write_color_output "\nNext steps:" "$CYAN"
|
||||||
|
write_color_output "1. Review summary_$timestamp.txt" "$NC"
|
||||||
|
write_color_output "2. Check for critical issues in diagnostics report" "$NC"
|
||||||
|
write_color_output "3. Analyze slow queries in query_analysis report" "$NC"
|
||||||
|
write_color_output "========================================\n${NC}"
|
||||||
|
|
||||||
|
# Email report if requested
|
||||||
|
if [ "$EMAIL_REPORT" = true ]; then
|
||||||
|
write_color_output "[WARN] Email functionality not yet implemented" "$YELLOW"
|
||||||
|
write_color_output " Configure a mail client like 'mail' or 'sendmail'" "$GRAY"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Database Configuration
|
||||||
|
# Edit this file to change which database commands run against
|
||||||
|
|
||||||
|
# Database connection settings
|
||||||
|
PSQL_PATH="/usr/bin/psql"
|
||||||
|
DB_USER="jellyfin"
|
||||||
|
DB_NAME="jellyfin_test2" # ← Change this to switch databases
|
||||||
|
DB_HOST="192.168.129.253"
|
||||||
|
DB_PORT="6432"
|
||||||
|
|
||||||
|
# Export for use in other scripts
|
||||||
|
export PSQL_PATH
|
||||||
|
export DB_USER
|
||||||
|
export DB_NAME
|
||||||
|
export DB_HOST
|
||||||
|
export DB_PORT
|
||||||
|
|
||||||
|
# Helper function to run psql commands
|
||||||
|
invoke_psql() {
|
||||||
|
local query=""
|
||||||
|
local file=""
|
||||||
|
local output_file=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-q|--query)
|
||||||
|
query="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-f|--file)
|
||||||
|
file="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-o|--output-file)
|
||||||
|
output_file="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
local base_cmd="$PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME"
|
||||||
|
local cmd=""
|
||||||
|
|
||||||
|
if [ -n "$file" ]; then
|
||||||
|
cmd="$base_cmd -f \"$file\""
|
||||||
|
elif [ -n "$query" ]; then
|
||||||
|
cmd="$base_cmd -c \"$query\""
|
||||||
|
else
|
||||||
|
echo "Must provide either --query or --file parameter"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$output_file" ]; then
|
||||||
|
cmd+=" > \"$output_file\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\e[36mConnecting to: $DB_NAME@$DB_HOST as $DB_USER\e[0m"
|
||||||
|
eval "$cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Display current configuration
|
||||||
|
echo -e "\e[36m========================================\e[0m"
|
||||||
|
echo -e "\e[36mDatabase Configuration Loaded\e[0m"
|
||||||
|
echo -e "\e[36m========================================\e[0m"
|
||||||
|
echo -e "\e[33mDatabase: $DB_NAME\e[0m"
|
||||||
|
echo -e "\e[33mUser: $DB_USER\e[0m"
|
||||||
|
echo -e "\e[33mHost: $DB_HOST\e[0m"
|
||||||
|
echo -e "\e[33mPort: $DB_PORT\e[0m"
|
||||||
|
echo -e "\e[36m========================================\e[0m"
|
||||||
|
echo ""
|
||||||
|
echo -e "\e[90mTo change database, edit: db-config.sh\e[0m"
|
||||||
|
echo -e "\e[90mThen run: source db-config.sh\e[0m"
|
||||||
|
echo ""
|
||||||
@@ -60,4 +60,8 @@
|
|||||||
</AssemblyAttribute>
|
</AssemblyAttribute>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<PackageReference Include="BitFaster.Caching" />
|
<PackageReference Include="BitFaster.Caching" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -37,5 +37,8 @@
|
|||||||
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
<PackageReference Include="libse" />
|
<PackageReference Include="libse" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="UTF.Unknown" />
|
<PackageReference Include="UTF.Unknown" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="MimeTypes">
|
<PackageReference Include="MimeTypes">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<PackageReference Include="LrcParser" />
|
<PackageReference Include="LrcParser" />
|
||||||
<PackageReference Include="MetaBrainz.MusicBrainz" />
|
<PackageReference Include="MetaBrainz.MusicBrainz" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Newtonsoft.Json" />
|
<PackageReference Include="Newtonsoft.Json" />
|
||||||
<PackageReference Include="PlaylistsNET" />
|
<PackageReference Include="PlaylistsNET" />
|
||||||
<PackageReference Include="z440.atl.core" />
|
<PackageReference Include="z440.atl.core" />
|
||||||
|
|||||||
@@ -37,5 +37,8 @@
|
|||||||
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Linux script to rebuild the Jellyfin solution properly
|
||||||
|
# This script cleans and rebuilds in the correct order to resolve CS0006 metadata errors
|
||||||
|
|
||||||
|
echo -e "\e[36mStarting Jellyfin solution rebuild...\e[0m"
|
||||||
|
|
||||||
|
# Step 1: Clean the solution
|
||||||
|
echo -e "\n\e[33m[1/3] Cleaning solution...\e[0m"
|
||||||
|
dotnet clean Jellyfin.sln --configuration Debug
|
||||||
|
|
||||||
|
# Remove obj and bin directories to ensure clean state
|
||||||
|
echo -e "\n\e[33m[2/3] Removing build artifacts...\e[0m"
|
||||||
|
find . -name bin -type d -exec rm -rf {} +
|
||||||
|
find . -name obj -type d -exec rm -rf {} +
|
||||||
|
|
||||||
|
# Step 3: Restore NuGet packages
|
||||||
|
echo -e "\n\e[33m[3/3] Restoring NuGet packages...\e[0m"
|
||||||
|
dotnet restore Jellyfin.sln
|
||||||
|
|
||||||
|
# Step 4: Build the solution
|
||||||
|
echo -e "\n\e[33m[4/4] Building solution...\e[0m"
|
||||||
|
dotnet build Jellyfin.sln --configuration Debug --no-restore
|
||||||
|
|
||||||
|
echo -e "\n\e[32mBuild complete!\e[0m"
|
||||||
|
echo -e "\e[36mIf you still see errors, try building specific projects in this order:\e[0m"
|
||||||
|
echo -e "\e[90m 1. Jellyfin.Database.Implementations\e[0m"
|
||||||
|
echo -e "\e[90m 2. Jellyfin.Database.Providers.Postgres\e[0m"
|
||||||
|
echo -e "\e[90m 3. Jellyfin.Server.Implementations\e[0m"
|
||||||
|
echo -e "\e[90m 4. Emby.Server.Implementations\e[0m"
|
||||||
|
echo -e "\e[90m 5. Jellyfin.Server\e[0m"
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
#Requires -RunAsAdministrator
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Sets up automated weekly performance monitoring via Windows Task Scheduler
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Creates a scheduled task to run Weekly-Performance-Monitor.ps1 every Monday at 6 AM.
|
||||||
|
Can optionally enable conditional VACUUM ANALYZE based on dead tuple thresholds.
|
||||||
|
|
||||||
|
.PARAMETER Uninstall
|
||||||
|
Remove the scheduled task
|
||||||
|
|
||||||
|
.PARAMETER TaskTime
|
||||||
|
Time to run the task (default: 6am)
|
||||||
|
|
||||||
|
.PARAMETER DayOfWeek
|
||||||
|
Day of week to run (default: Monday)
|
||||||
|
|
||||||
|
.PARAMETER EnableAutoVacuumAnalyze
|
||||||
|
Enable conditional VACUUM ANALYZE for tables above dead tuple thresholds
|
||||||
|
|
||||||
|
.PARAMETER DeadTupleThreshold
|
||||||
|
Minimum dead tuples before VACUUM ANALYZE runs (default: 10000)
|
||||||
|
|
||||||
|
.PARAMETER DeadTuplePercentThreshold
|
||||||
|
Minimum dead tuple percentage before VACUUM ANALYZE runs (default: 20)
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Setup-Weekly-Monitor.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Setup-Weekly-Monitor.ps1 -TaskTime "3am" -DayOfWeek Sunday
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Setup-Weekly-Monitor.ps1 -EnableAutoVacuumAnalyze -DeadTupleThreshold 10000 -DeadTuplePercentThreshold 20
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Setup-Weekly-Monitor.ps1 -Uninstall
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$Uninstall,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$TaskTime = "6am",
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[ValidateSet('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday')]
|
||||||
|
[string]$DayOfWeek = 'Monday',
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$EnableAutoVacuumAnalyze,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[ValidateRange(1, [int]::MaxValue)]
|
||||||
|
[int]$DeadTupleThreshold = 10000,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[ValidateRange(0.01, 100.0)]
|
||||||
|
[double]$DeadTuplePercentThreshold = 20.0
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$TaskName = "Jellyfin Database Weekly Monitor"
|
||||||
|
$ScriptRoot = Split-Path -Parent $PSCommandPath
|
||||||
|
$MonitorScript = Join-Path $ScriptRoot "Weekly-Performance-Monitor.ps1"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
function Write-ColorOutput {
|
||||||
|
param(
|
||||||
|
[string]$Message,
|
||||||
|
[ConsoleColor]$Color = 'White'
|
||||||
|
)
|
||||||
|
Write-Host $Message -ForegroundColor $Color
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-IsAdmin {
|
||||||
|
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
|
||||||
|
return $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
Write-ColorOutput "`n========================================" -Color Cyan
|
||||||
|
Write-ColorOutput "Jellyfin DB Monitor Setup" -Color Cyan
|
||||||
|
Write-ColorOutput "========================================`n" -Color Cyan
|
||||||
|
|
||||||
|
# Check if running as administrator
|
||||||
|
if (-not (Test-IsAdmin)) {
|
||||||
|
Write-ColorOutput "❌ This script must be run as Administrator" -Color Red
|
||||||
|
Write-ColorOutput " Right-click PowerShell and select 'Run as Administrator'" -Color Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Uninstall if requested
|
||||||
|
if ($Uninstall) {
|
||||||
|
Write-ColorOutput "Removing scheduled task..." -Color Yellow
|
||||||
|
|
||||||
|
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($task) {
|
||||||
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||||
|
Write-ColorOutput "✅ Scheduled task removed successfully" -Color Green
|
||||||
|
} else {
|
||||||
|
Write-ColorOutput "⚠️ Scheduled task not found" -Color Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if monitor script exists
|
||||||
|
if (-not (Test-Path $MonitorScript)) {
|
||||||
|
Write-ColorOutput "❌ Monitor script not found: $MonitorScript" -Color Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-ColorOutput "Configuration:" -Color Cyan
|
||||||
|
Write-ColorOutput " Task Name: $TaskName" -Color White
|
||||||
|
Write-ColorOutput " Script: $MonitorScript" -Color White
|
||||||
|
Write-ColorOutput " Schedule: Every $DayOfWeek at $TaskTime" -Color White
|
||||||
|
if ($EnableAutoVacuumAnalyze) {
|
||||||
|
Write-ColorOutput " Auto VACUUM ANALYZE: Enabled (dead tuples >= $DeadTupleThreshold, dead percent >= $DeadTuplePercentThreshold)" -Color White
|
||||||
|
} else {
|
||||||
|
Write-ColorOutput " Auto VACUUM ANALYZE: Disabled" -Color White
|
||||||
|
}
|
||||||
|
Write-ColorOutput ""
|
||||||
|
|
||||||
|
# Remove existing task if present
|
||||||
|
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||||
|
if ($existingTask) {
|
||||||
|
Write-ColorOutput "⚠️ Existing task found. Removing..." -Color Yellow
|
||||||
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create scheduled task action
|
||||||
|
Write-ColorOutput "Creating scheduled task..." -Color Cyan
|
||||||
|
|
||||||
|
$scriptArguments = "-NoProfile -ExecutionPolicy Bypass -File `"$MonitorScript`""
|
||||||
|
if ($EnableAutoVacuumAnalyze) {
|
||||||
|
$scriptArguments += " -AutoVacuumAnalyze -DeadTupleThreshold $DeadTupleThreshold -DeadTuplePercentThreshold $DeadTuplePercentThreshold"
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = New-ScheduledTaskAction `
|
||||||
|
-Execute "PowerShell.exe" `
|
||||||
|
-Argument $scriptArguments
|
||||||
|
|
||||||
|
# Create trigger
|
||||||
|
$trigger = New-ScheduledTaskTrigger `
|
||||||
|
-Weekly `
|
||||||
|
-DaysOfWeek $DayOfWeek `
|
||||||
|
-At $TaskTime
|
||||||
|
|
||||||
|
# Task settings
|
||||||
|
$settings = New-ScheduledTaskSettingsSet `
|
||||||
|
-AllowStartIfOnBatteries `
|
||||||
|
-DontStopIfGoingOnBatteries `
|
||||||
|
-StartWhenAvailable `
|
||||||
|
-RunOnlyIfNetworkAvailable `
|
||||||
|
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||||
|
|
||||||
|
# Task principal (run as current user)
|
||||||
|
$principal = New-ScheduledTaskPrincipal `
|
||||||
|
-UserId $env:USERNAME `
|
||||||
|
-LogonType S4U `
|
||||||
|
-RunLevel Highest
|
||||||
|
|
||||||
|
# Register the task
|
||||||
|
try {
|
||||||
|
Register-ScheduledTask `
|
||||||
|
-TaskName $TaskName `
|
||||||
|
-Action $action `
|
||||||
|
-Trigger $trigger `
|
||||||
|
-Settings $settings `
|
||||||
|
-Principal $principal `
|
||||||
|
-Description "Automated weekly performance monitoring for Jellyfin PostgreSQL database" | Out-Null
|
||||||
|
|
||||||
|
Write-ColorOutput "✅ Scheduled task created successfully!" -Color Green
|
||||||
|
Write-ColorOutput ""
|
||||||
|
|
||||||
|
# Verify task
|
||||||
|
$task = Get-ScheduledTask -TaskName $TaskName
|
||||||
|
$nextRun = $task.Triggers[0].StartBoundary
|
||||||
|
|
||||||
|
Write-ColorOutput "Task Details:" -Color Cyan
|
||||||
|
Write-ColorOutput " Status: $($task.State)" -Color White
|
||||||
|
Write-ColorOutput " Next Run: $nextRun" -Color White
|
||||||
|
Write-ColorOutput ""
|
||||||
|
|
||||||
|
# Test run option
|
||||||
|
Write-ColorOutput "Would you like to test the task now? (y/n): " -Color Yellow -NoNewline
|
||||||
|
$response = Read-Host
|
||||||
|
|
||||||
|
if ($response -eq 'y' -or $response -eq 'Y') {
|
||||||
|
Write-ColorOutput "`nRunning test..." -Color Cyan
|
||||||
|
Start-ScheduledTask -TaskName $TaskName
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
|
||||||
|
$taskInfo = Get-ScheduledTask -TaskName $TaskName | Get-ScheduledTaskInfo
|
||||||
|
Write-ColorOutput "Last Run Time: $($taskInfo.LastRunTime)" -Color White
|
||||||
|
Write-ColorOutput "Last Result: $($taskInfo.LastTaskResult)" -Color White
|
||||||
|
|
||||||
|
if ($taskInfo.LastTaskResult -eq 0) {
|
||||||
|
Write-ColorOutput "✅ Test run completed successfully!" -Color Green
|
||||||
|
} else {
|
||||||
|
Write-ColorOutput "⚠️ Test run completed with code: $($taskInfo.LastTaskResult)" -Color Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
Write-ColorOutput "❌ Failed to create scheduled task: $_" -Color Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-ColorOutput "`n========================================" -Color Green
|
||||||
|
Write-ColorOutput "Setup Complete!" -Color Green
|
||||||
|
Write-ColorOutput "========================================" -Color Green
|
||||||
|
Write-ColorOutput ""
|
||||||
|
Write-ColorOutput "Monitoring is now automated!" -Color Cyan
|
||||||
|
Write-ColorOutput ""
|
||||||
|
Write-ColorOutput "To manage the task:" -Color White
|
||||||
|
Write-ColorOutput " View: Get-ScheduledTask -TaskName '$TaskName'" -Color Gray
|
||||||
|
Write-ColorOutput " Run now: Start-ScheduledTask -TaskName '$TaskName'" -Color Gray
|
||||||
|
Write-ColorOutput " Disable: Disable-ScheduledTask -TaskName '$TaskName'" -Color Gray
|
||||||
|
Write-ColorOutput " Remove: .\Setup-Weekly-Monitor.ps1 -Uninstall" -Color Gray
|
||||||
|
Write-ColorOutput ""
|
||||||
|
Write-ColorOutput "Reports will be saved to: reports\weekly" -Color Yellow
|
||||||
|
Write-ColorOutput "========================================`n" -Color Green
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Automated weekly performance monitoring for Jellyfin PostgreSQL database
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This script:
|
||||||
|
1. Runs comprehensive diagnostics
|
||||||
|
2. Analyzes slow queries
|
||||||
|
3. Tracks index usage over time
|
||||||
|
4. Generates weekly performance reports
|
||||||
|
5. Optionally runs VACUUM ANALYZE on tables with high dead tuples
|
||||||
|
6. Can be scheduled via Task Scheduler
|
||||||
|
|
||||||
|
.PARAMETER OutputDirectory
|
||||||
|
Directory to save reports (default: reports/weekly)
|
||||||
|
|
||||||
|
.PARAMETER EmailReport
|
||||||
|
If specified, emails the report (requires email configuration)
|
||||||
|
|
||||||
|
.PARAMETER CompareWithPrevious
|
||||||
|
Compare current stats with previous week
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Weekly-Performance-Monitor.ps1
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Weekly-Performance-Monitor.ps1 -OutputDirectory "C:\Reports\Jellyfin" -CompareWithPrevious
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\Weekly-Performance-Monitor.ps1 -AutoVacuumAnalyze -DeadTupleThreshold 10000 -DeadTuplePercentThreshold 20
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# Schedule weekly via Task Scheduler
|
||||||
|
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-File C:\Path\To\Weekly-Performance-Monitor.ps1"
|
||||||
|
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 6am
|
||||||
|
Register-ScheduledTask -TaskName "Jellyfin DB Monitor" -Action $action -Trigger $trigger
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Author: Generated for Jellyfin PostgreSQL Project
|
||||||
|
Date: 2026-03-05
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter()]
|
||||||
|
[string]$OutputDirectory = "reports\weekly",
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$EmailReport,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$CompareWithPrevious,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$AutoVacuumAnalyze,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[ValidateRange(1, [int]::MaxValue)]
|
||||||
|
[int]$DeadTupleThreshold = 100,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[ValidateRange(0.01, 100.0)]
|
||||||
|
[double]$DeadTuplePercentThreshold = 0.02
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$ScriptRoot = Split-Path -Parent $PSCommandPath
|
||||||
|
$ProjectRoot = Split-Path -Parent $ScriptRoot
|
||||||
|
|
||||||
|
# Load database configuration
|
||||||
|
. "$ProjectRoot\scripts\db-config.ps1"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
function Write-ColorOutput {
|
||||||
|
param(
|
||||||
|
[string]$Message,
|
||||||
|
[ConsoleColor]$Color = 'White'
|
||||||
|
)
|
||||||
|
Write-Host $Message -ForegroundColor $Color
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ReportTimestamp {
|
||||||
|
return Get-Date -Format "yyyy-MM-dd_HHmmss"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-WeekNumber {
|
||||||
|
$date = Get-Date
|
||||||
|
$culture = [System.Globalization.CultureInfo]::CurrentCulture
|
||||||
|
return $culture.Calendar.GetWeekOfYear($date,
|
||||||
|
[System.Globalization.CalendarWeekRule]::FirstDay,
|
||||||
|
[DayOfWeek]::Monday)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-DatabaseConnection {
|
||||||
|
Write-ColorOutput "`nTesting database connection..." -Color Cyan
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = & $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "SELECT version();" 2>&1
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-ColorOutput "[OK] Database connection successful" -Color Green
|
||||||
|
return $true
|
||||||
|
} else {
|
||||||
|
Write-ColorOutput "[ERROR] Database connection failed: $result" -Color Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-ColorOutput "[ERROR] Database connection error: $_" -Color Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-DiagnosticReport {
|
||||||
|
param([string]$OutputFile)
|
||||||
|
|
||||||
|
Write-ColorOutput "`nRunning comprehensive diagnostics..." -Color Cyan
|
||||||
|
|
||||||
|
try {
|
||||||
|
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$ProjectRoot\sql\diagnostics.sql" > $OutputFile 2>&1
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-ColorOutput "[OK] Diagnostics completed: $OutputFile" -Color Green
|
||||||
|
return $true
|
||||||
|
} else {
|
||||||
|
Write-ColorOutput "[WARN] Diagnostics completed with warnings" -Color Yellow
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-ColorOutput "[ERROR] Diagnostics failed: $_" -Color Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-QueryAnalysis {
|
||||||
|
param([string]$OutputFile)
|
||||||
|
|
||||||
|
Write-ColorOutput "`nAnalyzing slow queries..." -Color Cyan
|
||||||
|
|
||||||
|
try {
|
||||||
|
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -f "$ProjectRoot\sql\query-analysis.sql" > $OutputFile 2>&1
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-ColorOutput "[OK] Query analysis completed: $OutputFile" -Color Green
|
||||||
|
return $true
|
||||||
|
} else {
|
||||||
|
Write-ColorOutput "[WARN] Query analysis completed with warnings" -Color Yellow
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-ColorOutput "[ERROR] Query analysis failed: $_" -Color Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-IndexUsageStats {
|
||||||
|
param([string]$OutputFile)
|
||||||
|
|
||||||
|
Write-ColorOutput "`nGathering index usage statistics..." -Color Cyan
|
||||||
|
|
||||||
|
$query = @"
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
relname AS tablename,
|
||||||
|
indexrelname,
|
||||||
|
idx_scan,
|
||||||
|
idx_tup_read,
|
||||||
|
idx_tup_fetch,
|
||||||
|
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
|
||||||
|
FROM pg_stat_user_indexes
|
||||||
|
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
|
||||||
|
ORDER BY schemaname, relname, idx_scan DESC;
|
||||||
|
"@
|
||||||
|
|
||||||
|
try {
|
||||||
|
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c $query > $OutputFile 2>&1
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-ColorOutput "[OK] Index stats gathered: $OutputFile" -Color Green
|
||||||
|
return $true
|
||||||
|
} else {
|
||||||
|
Write-ColorOutput "[WARN] Index stats completed with warnings" -Color Yellow
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-ColorOutput "[ERROR] Index stats failed: $_" -Color Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-TableSizeStats {
|
||||||
|
param([string]$OutputFile)
|
||||||
|
|
||||||
|
Write-ColorOutput "`nGathering table size statistics..." -Color Cyan
|
||||||
|
|
||||||
|
$query = @"
|
||||||
|
SELECT
|
||||||
|
schemaname,
|
||||||
|
relname,
|
||||||
|
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) AS total_size,
|
||||||
|
pg_size_pretty(pg_relation_size(schemaname||'.'||relname)) AS table_size,
|
||||||
|
pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname) - pg_relation_size(schemaname||'.'||relname)) AS index_size,
|
||||||
|
n_live_tup AS row_count,
|
||||||
|
n_dead_tup AS dead_rows,
|
||||||
|
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
|
||||||
|
ORDER BY pg_total_relation_size(schemaname||'.'||relname) DESC;
|
||||||
|
"@
|
||||||
|
|
||||||
|
try {
|
||||||
|
& $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c $query > $OutputFile 2>&1
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-ColorOutput "[OK] Table size stats gathered: $OutputFile" -Color Green
|
||||||
|
return $true
|
||||||
|
} else {
|
||||||
|
Write-ColorOutput "[WARN] Table size stats completed with warnings" -Color Yellow
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-ColorOutput "[ERROR] Table size stats failed: $_" -Color Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-ConditionalVacuumAnalyze {
|
||||||
|
param(
|
||||||
|
[int]$MinDeadTuples,
|
||||||
|
[double]$MinDeadPercent
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-ColorOutput "`nChecking for tables that need VACUUM ANALYZE..." -Color Cyan
|
||||||
|
Write-ColorOutput "Thresholds: dead tuples >= $MinDeadTuples and dead percent >= $MinDeadPercent" -Color Gray
|
||||||
|
|
||||||
|
$tableQuery = @"
|
||||||
|
SELECT
|
||||||
|
format('%I.%I', schemaname, relname) AS qualified_table,
|
||||||
|
n_dead_tup,
|
||||||
|
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup, 0), 2) AS dead_pct
|
||||||
|
FROM pg_stat_user_tables
|
||||||
|
WHERE schemaname IN ('library', 'users', 'authentication', 'displaypreferences', 'activitylog')
|
||||||
|
AND n_live_tup > 0
|
||||||
|
AND n_dead_tup >= $MinDeadTuples
|
||||||
|
AND (100.0 * n_dead_tup / NULLIF(n_live_tup, 0)) >= $MinDeadPercent
|
||||||
|
ORDER BY n_dead_tup DESC;
|
||||||
|
"@
|
||||||
|
|
||||||
|
try {
|
||||||
|
$tables = & $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -t -A -F "|" -c $tableQuery 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-ColorOutput "[ERROR] Failed to evaluate dead tuple thresholds: $tables" -Color Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableLines = @($tables | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||||
|
if ($tableLines.Count -eq 0) {
|
||||||
|
Write-ColorOutput "[OK] No tables exceeded dead tuple thresholds" -Color Green
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-ColorOutput "Found $($tableLines.Count) table(s) requiring VACUUM ANALYZE" -Color Yellow
|
||||||
|
|
||||||
|
foreach ($line in $tableLines) {
|
||||||
|
$parts = $line -split "\|", 3
|
||||||
|
$qualifiedTable = $parts[0].Trim()
|
||||||
|
$deadTuples = if ($parts.Count -ge 2) { $parts[1].Trim() } else { "unknown" }
|
||||||
|
$deadPercent = if ($parts.Count -ge 3) { $parts[2].Trim() } else { "unknown" }
|
||||||
|
|
||||||
|
Write-ColorOutput "Running VACUUM ANALYZE on $qualifiedTable (dead tuples: $deadTuples, dead percent: $deadPercent)" -Color Cyan
|
||||||
|
|
||||||
|
$vacuumResult = & $PSQL_PATH -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME -c "VACUUM (ANALYZE, VERBOSE) $qualifiedTable;" 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-ColorOutput "[ERROR] VACUUM ANALYZE failed for $qualifiedTable : $vacuumResult" -Color Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-ColorOutput "[OK] VACUUM ANALYZE completed for $qualifiedTable" -Color Green
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
Write-ColorOutput "[ERROR] Conditional VACUUM ANALYZE failed: $_" -Color Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Compare-WithPrevious {
|
||||||
|
param(
|
||||||
|
[string]$CurrentReport,
|
||||||
|
[string]$OutputDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-ColorOutput "`nComparing with previous week's report..." -Color Cyan
|
||||||
|
|
||||||
|
# Find most recent previous report
|
||||||
|
$previousReports = Get-ChildItem "$OutputDirectory\diagnostics_*.txt" -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.Name -ne (Split-Path $CurrentReport -Leaf) } |
|
||||||
|
Sort-Object LastWriteTime -Descending
|
||||||
|
|
||||||
|
if ($previousReports.Count -eq 0) {
|
||||||
|
Write-ColorOutput "[WARN] No previous reports found for comparison" -Color Yellow
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$previousReport = $previousReports[0].FullName
|
||||||
|
Write-ColorOutput "Comparing with: $($previousReports[0].Name)" -Color Cyan
|
||||||
|
|
||||||
|
# TODO: Add detailed comparison logic
|
||||||
|
# For now, just note that both reports exist
|
||||||
|
Write-ColorOutput "[OK] Comparison data available" -Color Green
|
||||||
|
Write-ColorOutput " Current: $CurrentReport" -Color Gray
|
||||||
|
Write-ColorOutput " Previous: $previousReport" -Color Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-SummaryReport {
|
||||||
|
param(
|
||||||
|
[string]$OutputDirectory,
|
||||||
|
[string]$Timestamp
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-ColorOutput "`nGenerating summary report..." -Color Cyan
|
||||||
|
|
||||||
|
$summaryFile = Join-Path $OutputDirectory "summary_$Timestamp.txt"
|
||||||
|
|
||||||
|
$summary = @"
|
||||||
|
================================================================================
|
||||||
|
JELLYFIN DATABASE WEEKLY PERFORMANCE SUMMARY
|
||||||
|
================================================================================
|
||||||
|
Report Date: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
|
||||||
|
Database: $DB_NAME at $DB_HOST
|
||||||
|
Week Number: $(Get-WeekNumber)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
REPORTS GENERATED:
|
||||||
|
* diagnostics_$Timestamp.txt : Full diagnostic report
|
||||||
|
* query_analysis_$Timestamp.txt : Slow query analysis
|
||||||
|
* index_usage_$Timestamp.txt : Index usage statistics
|
||||||
|
* table_sizes_$Timestamp.txt : Table size and bloat info
|
||||||
|
* summary_$Timestamp.txt : This summary
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
QUICK HEALTH CHECK:
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
To view critical issues check diagnostics file for:
|
||||||
|
* Section 5: TABLES WITH HIGH SEQUENTIAL SCANS
|
||||||
|
* Section 8: UNUSED OR RARELY USED INDEXES
|
||||||
|
* Section 10: SLOWEST QUERIES
|
||||||
|
|
||||||
|
To identify slow queries check query_analysis file for:
|
||||||
|
* Section 2: TOP 20 SLOWEST QUERIES (by max execution time)
|
||||||
|
* Section 5: BASEITEMS TABLE QUERIES ANALYSIS
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
RECOMMENDED ACTIONS:
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
* Review slow queries taking longer than 10 seconds
|
||||||
|
* Check if new indexes are being utilized
|
||||||
|
* Look for tables with many sequential scans
|
||||||
|
* Monitor cache hit ratio (should be above 95 percent)
|
||||||
|
* Check for tables with high dead tuples percentage (need VACUUM)
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
AUTOMATION SETUP:
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
To schedule this script weekly via Task Scheduler run Setup-Weekly-Monitor.ps1
|
||||||
|
as Administrator. This will create a scheduled task to run every Monday at 6am.
|
||||||
|
|
||||||
|
To disable the scheduled task run Setup-Weekly-Monitor.ps1 with -Uninstall flag.
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
NEXT REPORT: $(Get-Date).AddDays(7).ToString("yyyy-MM-dd")
|
||||||
|
================================================================================
|
||||||
|
"@
|
||||||
|
|
||||||
|
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
|
||||||
|
Write-ColorOutput "[OK] Summary generated: $summaryFile" -Color Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Main Execution
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
Write-ColorOutput "`n========================================" -Color Cyan
|
||||||
|
Write-ColorOutput "Jellyfin Database Weekly Monitor" -Color Cyan
|
||||||
|
Write-ColorOutput "========================================" -Color Cyan
|
||||||
|
Write-ColorOutput "Database: $DB_NAME" -Color Yellow
|
||||||
|
Write-ColorOutput "Host: $DB_HOST" -Color Yellow
|
||||||
|
Write-ColorOutput "Week: $(Get-WeekNumber)" -Color Yellow
|
||||||
|
Write-ColorOutput "Date: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -Color Yellow
|
||||||
|
Write-ColorOutput "========================================`n" -Color Cyan
|
||||||
|
|
||||||
|
# Create output directory
|
||||||
|
$reportDir = Join-Path $ProjectRoot $OutputDirectory
|
||||||
|
if (-not (Test-Path $reportDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $reportDir -Force | Out-Null
|
||||||
|
Write-ColorOutput "Created report directory: $reportDir" -Color Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate timestamp for this run
|
||||||
|
$timestamp = Get-ReportTimestamp
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
if (-not (Test-DatabaseConnection)) {
|
||||||
|
Write-ColorOutput "`n[ERROR] Cannot connect to database. Exiting." -Color Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run diagnostics
|
||||||
|
$diagnosticsFile = Join-Path $reportDir "diagnostics_$timestamp.txt"
|
||||||
|
$success = Invoke-DiagnosticReport -OutputFile $diagnosticsFile
|
||||||
|
|
||||||
|
if (-not $success) {
|
||||||
|
Write-ColorOutput "`n[WARN] Some reports failed. Check output directory." -Color Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run query analysis
|
||||||
|
$queryAnalysisFile = Join-Path $reportDir "query_analysis_$timestamp.txt"
|
||||||
|
Invoke-QueryAnalysis -OutputFile $queryAnalysisFile | Out-Null
|
||||||
|
|
||||||
|
# Gather index usage stats
|
||||||
|
$indexUsageFile = Join-Path $reportDir "index_usage_$timestamp.txt"
|
||||||
|
Get-IndexUsageStats -OutputFile $indexUsageFile | Out-Null
|
||||||
|
|
||||||
|
# Gather table size stats
|
||||||
|
$tableSizesFile = Join-Path $reportDir "table_sizes_$timestamp.txt"
|
||||||
|
Get-TableSizeStats -OutputFile $tableSizesFile | Out-Null
|
||||||
|
|
||||||
|
# Optionally vacuum/analyze tables that exceed dead tuple thresholds
|
||||||
|
if ($AutoVacuumAnalyze) {
|
||||||
|
$vacuumSuccess = Invoke-ConditionalVacuumAnalyze `
|
||||||
|
-MinDeadTuples $DeadTupleThreshold `
|
||||||
|
-MinDeadPercent $DeadTuplePercentThreshold
|
||||||
|
|
||||||
|
if (-not $vacuumSuccess) {
|
||||||
|
Write-ColorOutput "[WARN] Conditional VACUUM ANALYZE encountered errors" -Color Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compare with previous week if requested
|
||||||
|
if ($CompareWithPrevious) {
|
||||||
|
Compare-WithPrevious -CurrentReport $diagnosticsFile -OutputDirectory $reportDir
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate summary
|
||||||
|
New-SummaryReport -OutputDirectory $reportDir -Timestamp $timestamp
|
||||||
|
|
||||||
|
# Cleanup old reports (keep last 12 weeks)
|
||||||
|
Write-ColorOutput "`nCleaning up old reports (keeping last 12 weeks)..." -Color Cyan
|
||||||
|
Get-ChildItem $reportDir -File |
|
||||||
|
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-84) } |
|
||||||
|
Remove-Item -Force
|
||||||
|
Write-ColorOutput "[OK] Cleanup complete" -Color Green
|
||||||
|
|
||||||
|
Write-ColorOutput "`n========================================" -Color Green
|
||||||
|
Write-ColorOutput "[OK] WEEKLY MONITORING COMPLETE" -Color Green
|
||||||
|
Write-ColorOutput "========================================" -Color Green
|
||||||
|
Write-ColorOutput "Reports saved to: $reportDir" -Color Yellow
|
||||||
|
Write-ColorOutput "Summary: summary_$timestamp.txt" -Color Yellow
|
||||||
|
Write-ColorOutput "`nNext steps:" -Color Cyan
|
||||||
|
Write-ColorOutput "1. Review summary_$timestamp.txt" -Color White
|
||||||
|
Write-ColorOutput "2. Check for critical issues in diagnostics report" -Color White
|
||||||
|
Write-ColorOutput "3. Analyze slow queries in query_analysis report" -Color White
|
||||||
|
Write-ColorOutput "========================================`n" -Color Green
|
||||||
|
|
||||||
|
# Email report if requested (placeholder)
|
||||||
|
if ($EmailReport) {
|
||||||
|
Write-ColorOutput "[WARN] Email functionality not yet implemented" -Color Yellow
|
||||||
|
Write-ColorOutput " Configure Send-MailMessage with your SMTP settings" -Color Gray
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Database Configuration
|
||||||
|
# Edit this file to change which database commands run against
|
||||||
|
|
||||||
|
# Database connection settings
|
||||||
|
$PSQL_PATH = "C:\Program Files\PostgreSQL\18\bin\psql.exe"
|
||||||
|
$DB_USER = "jellyfin"
|
||||||
|
$DB_NAME = "jellyfin_test2" # ← Change this to switch databases
|
||||||
|
$DB_HOST = "192.168.129.253"
|
||||||
|
$DB_PORT = "6432"
|
||||||
|
|
||||||
|
# Export for use in other scripts
|
||||||
|
$global:PSQL_PATH = $PSQL_PATH
|
||||||
|
$global:DB_USER = $DB_USER
|
||||||
|
$global:DB_NAME = $DB_NAME
|
||||||
|
$global:DB_HOST = $DB_HOST
|
||||||
|
$global:DB_PORT = $DB_PORT
|
||||||
|
|
||||||
|
# Helper function to run psql commands
|
||||||
|
function Invoke-PSQL {
|
||||||
|
param(
|
||||||
|
[string]$Query,
|
||||||
|
[string]$File,
|
||||||
|
[string]$OutputFile
|
||||||
|
)
|
||||||
|
|
||||||
|
$baseCmd = "& `"$PSQL_PATH`" -h $DB_HOST -p $DB_PORT -U $DB_USER -d $DB_NAME"
|
||||||
|
|
||||||
|
if ($File) {
|
||||||
|
$cmd = "$baseCmd -f `"$File`""
|
||||||
|
} elseif ($Query) {
|
||||||
|
$cmd = "$baseCmd -c `"$Query`""
|
||||||
|
} else {
|
||||||
|
Write-Error "Must provide either -Query or -File parameter"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($OutputFile) {
|
||||||
|
$cmd += " > `"$OutputFile`""
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Connecting to: $DB_NAME@$DB_HOST as $DB_USER" -ForegroundColor Cyan
|
||||||
|
Invoke-Expression $cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
# Display current configuration
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host "Database Configuration Loaded" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host "Database: $DB_NAME" -ForegroundColor Yellow
|
||||||
|
Write-Host "User: $DB_USER" -ForegroundColor Yellow
|
||||||
|
Write-Host "Host: $DB_HOST" -ForegroundColor Yellow
|
||||||
|
Write-Host "Port: $DB_PORT" -ForegroundColor Yellow
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "To change database, edit: db-config.ps1" -ForegroundColor Gray
|
||||||
|
Write-Host "Then run: . .\db-config.ps1" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -350,6 +350,21 @@ public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILog
|
|||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
catch (DbUpdateConcurrencyException ex)
|
||||||
|
{
|
||||||
|
// When marking items as played in quick succession, concurrency conflicts can occur.
|
||||||
|
// To resolve this, we'll reload the conflicting entities from the database and retry.
|
||||||
|
logger.LogWarning(ex, "Concurrency exception, retrying operation.");
|
||||||
|
|
||||||
|
foreach (var entry in ex.Entries)
|
||||||
|
{
|
||||||
|
// Reload the entity from the database to get the latest version
|
||||||
|
await entry.ReloadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry saving changes after resolving conflicts
|
||||||
|
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
catch (DbUpdateException ex)
|
catch (DbUpdateException ex)
|
||||||
{
|
{
|
||||||
// Check if it's a constraint violation (works across all database providers)
|
// Check if it's a constraint violation (works across all database providers)
|
||||||
|
|||||||
+1
@@ -12,6 +12,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<PackageReference Include="BlurHashSharp.SkiaSharp" />
|
<PackageReference Include="BlurHashSharp.SkiaSharp" />
|
||||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" />
|
<PackageReference Include="HarfBuzzSharp.NativeAssets.Linux" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="SkiaSharp" />
|
<PackageReference Include="SkiaSharp" />
|
||||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
|
||||||
<PackageReference Include="SkiaSharp.HarfBuzz" />
|
<PackageReference Include="SkiaSharp.HarfBuzz" />
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncKeyedLock" />
|
<PackageReference Include="AsyncKeyedLock" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<PackageReference Include="Diacritics" />
|
<PackageReference Include="Diacritics" />
|
||||||
<PackageReference Include="ICU4N.Transliterator" />
|
<PackageReference Include="ICU4N.Transliterator" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<PackageReference Include="AsyncKeyedLock" />
|
<PackageReference Include="AsyncKeyedLock" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
<PackageReference Include="Jellyfin.XmlTv" />
|
<PackageReference Include="Jellyfin.XmlTv" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="NEbml" />
|
<PackageReference Include="NEbml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@
|
|||||||
<Compile Include="..\..\SharedVersion.cs" />
|
<Compile Include="..\..\SharedVersion.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
<ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||||
<ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
<ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||||
<PackageReference Include="AutoFixture.Xunit2" />
|
<PackageReference Include="AutoFixture.Xunit2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio">
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio">
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="Moq" />
|
<PackageReference Include="Moq" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio">
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<PackageReference Include="AutoFixture" />
|
<PackageReference Include="AutoFixture" />
|
||||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="Moq" />
|
<PackageReference Include="Moq" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio">
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
|||||||
+1
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio">
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<PackageReference Include="AutoFixture.Xunit2" />
|
<PackageReference Include="AutoFixture.Xunit2" />
|
||||||
<PackageReference Include="coverlet.collector" />
|
<PackageReference Include="coverlet.collector" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="Moq" />
|
<PackageReference Include="Moq" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="Moq" />
|
<PackageReference Include="Moq" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="Moq" />
|
<PackageReference Include="Moq" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio">
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||||
<PackageReference Include="AutoFixture.Xunit2" />
|
<PackageReference Include="AutoFixture.Xunit2" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="Moq" />
|
<PackageReference Include="Moq" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
|
|||||||
+1
@@ -23,6 +23,7 @@
|
|||||||
<PackageReference Include="AutoFixture.AutoMoq" />
|
<PackageReference Include="AutoFixture.AutoMoq" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="Moq" />
|
<PackageReference Include="Moq" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<PackageReference Include="AutoFixture.Xunit2" />
|
<PackageReference Include="AutoFixture.Xunit2" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio">
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<PackageReference Include="AutoFixture.Xunit2" />
|
<PackageReference Include="AutoFixture.Xunit2" />
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio">
|
<PackageReference Include="xunit.runner.visualstudio">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.Sdk" />
|
<PackageReference Include="Jellyfin.Sdk" />
|
||||||
|
<PackageReference Include="Microsoft.Kiota.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||||
<PackageReference Include="Moq" />
|
<PackageReference Include="Moq" />
|
||||||
<PackageReference Include="xunit" />
|
<PackageReference Include="xunit" />
|
||||||
|
|||||||
Reference in New Issue
Block a user