diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 0c2d779..bac990e 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -18,6 +18,18 @@ jobs:
build:
name: Build
runs-on: ubuntu-latest
+ services:
+ postgres:
+ image: postgres:17
+ env:
+ POSTGRES_PASSWORD: postgres
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 5s
+ --health-timeout 5s
+ --health-retries 10
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -25,7 +37,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: '9.0.x'
+ dotnet-version: '10.0.x'
- name: Restore
run: dotnet restore Jellyfin.Database.Providers.Postgres.sln
@@ -33,6 +45,11 @@ jobs:
- name: Build (Release)
run: dotnet build Jellyfin.Database.Providers.Postgres.sln -c Release --no-restore
+ - name: Suite de verificación
+ env:
+ POSTGRES_TEST_CONNECTION: Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres
+ run: dotnet test Tests/Jellyfin.Database.Providers.Postgres.Tests/Jellyfin.Database.Providers.Postgres.Tests.csproj -c Release --no-build --no-restore
+
- name: Publish
run: dotnet publish Jellyfin.Database.Providers.Postgres/Jellyfin.Database.Providers.Postgres.csproj -c Release --no-restore -o artifacts
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7f2ac1c..e5a90d5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -33,7 +33,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
- dotnet-version: '9.0.x'
+ dotnet-version: '10.0.x'
- name: Normalizar versión (4 partes)
id: version
@@ -79,15 +79,21 @@ jobs:
echo "checksum=$CHECKSUM" >> "$GITHUB_OUTPUT"
- name: Actualizar manifest.json (repo)
+ # El changelog llega por variable de entorno, no interpolado en el script: así los saltos de
+ # línea sobreviven, jq los escribe como \n reales y Jellyfin los muestra como párrafos.
+ env:
+ VERSION: ${{ steps.version.outputs.full }}
+ RELEASE_VERSION: ${{ github.event.inputs.release_version }}
+ CHECKSUM: ${{ steps.package.outputs.checksum }}
+ CHANGELOG: ${{ github.event.inputs.changelog }}
run: |
- VERSION="${{ steps.version.outputs.full }}"
+ # ABI que declara esta rama: 12.1.0.0 corresponde a Jellyfin 12.x.
+ TARGETABI="12.1.0.0"
ZIP="Jellyfin.Database.Providers.Postgres-${VERSION}.zip"
- URL="https://github.com/${{ github.repository }}/releases/download/v${{ github.event.inputs.release_version }}/${ZIP}"
- CHECKSUM="${{ steps.package.outputs.checksum }}"
+ URL="https://github.com/${{ github.repository }}/releases/download/v${RELEASE_VERSION}/${ZIP}"
TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
- CHANGELOG="${{ github.event.inputs.changelog }}"
- jq --arg v "$VERSION" --arg url "$URL" --arg sum "$CHECKSUM" --arg ts "$TS" --arg changelog "$CHANGELOG" \
- '.[0].versions = (([.[0].versions[] | select(.version != $v)]) + [{version: $v, changelog: $changelog, targetAbi: "10.11.0.0", sourceUrl: $url, checksum: $sum, timestamp: $ts}])' \
+ jq --arg v "$VERSION" --arg url "$URL" --arg sum "$CHECKSUM" --arg ts "$TS" --arg changelog "$CHANGELOG" --arg abi "$TARGETABI" \
+ '.[0].versions = (([.[0].versions[] | select(.version != $v)]) + [{version: $v, changelog: $changelog, targetAbi: $abi, sourceUrl: $url, checksum: $sum, timestamp: $ts}])' \
manifest.json > manifest.tmp.json && mv manifest.tmp.json manifest.json
cp manifest.json publish/manifest.json
@@ -99,8 +105,40 @@ jobs:
git diff --cached --quiet || git commit -m "chore: actualizar manifest a v${{ steps.version.outputs.full }}"
git push
+ # El catálogo que consulta Jellyfin es el manifest.json de main. Si el release se
+ # publica desde una rama (release/*), la entrada debe replicarse en main o la
+ # versión no aparece en el catálogo aunque el release exista.
+ - name: Publicar la entrada en el catálogo servido (main)
+ if: github.ref_name != 'main'
+ env:
+ TARGETABI: '12.1.0.0'
+ VERSION: ${{ steps.version.outputs.full }}
+ RELEASE_VERSION: ${{ github.event.inputs.release_version }}
+ CHECKSUM: ${{ steps.package.outputs.checksum }}
+ CHANGELOG: ${{ github.event.inputs.changelog }}
+ run: |
+ ZIP="Jellyfin.Database.Providers.Postgres-${VERSION}.zip"
+ URL="https://github.com/${{ github.repository }}/releases/download/v${RELEASE_VERSION}/${ZIP}"
+ TS=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+ git fetch origin main
+ git worktree add --detach ../catalogo origin/main
+ cd ../catalogo
+ jq --arg v "$VERSION" --arg url "$URL" --arg sum "$CHECKSUM" --arg ts "$TS" --arg changelog "$CHANGELOG" --arg abi "$TARGETABI" \
+ '.[0].versions = (([.[0].versions[] | select(.version != $v)]) + [{version: $v, changelog: $changelog, targetAbi: $abi, sourceUrl: $url, checksum: $sum, timestamp: $ts}])' \
+ manifest.json > manifest.tmp.json && mv manifest.tmp.json manifest.json
+ git add manifest.json
+ if git diff --cached --quiet; then
+ echo "manifest.json de main ya contiene v${VERSION}"
+ exit 0
+ fi
+ git -c user.name="github-actions[bot]" -c user.email="github-actions[bot]@users.noreply.github.com" \
+ commit -m "chore: publicar v${VERSION} en el catálogo (targetAbi ${TARGETABI})"
+ git push origin HEAD:main
+
- name: Preparar notas del release
- run: printf '%s' "${{ github.event.inputs.changelog }}" > release-notes.md
+ env:
+ CHANGELOG: ${{ github.event.inputs.changelog }}
+ run: printf '%s' "$CHANGELOG" > release-notes.md
- name: Crear release en GitHub
uses: softprops/action-gh-release@v2
diff --git a/.github/workflows/scan-codeql.yaml b/.github/workflows/scan-codeql.yaml
index 05216d6..bf95b0a 100644
--- a/.github/workflows/scan-codeql.yaml
+++ b/.github/workflows/scan-codeql.yaml
@@ -22,6 +22,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
diff --git a/.github/workflows/verification.yml b/.github/workflows/verification.yml
new file mode 100644
index 0000000..db8aa52
--- /dev/null
+++ b/.github/workflows/verification.yml
@@ -0,0 +1,66 @@
+name: Verificación
+
+# Ejecuta la suite de pruebas del plugin y publica el resumen ejecutivo generado a partir del resultado
+# real (docs/VERIFICACION-RESUMEN.md). El resumen queda en la pestaña Summary del job y como artefacto.
+on:
+ push:
+ branches:
+ - main
+ - 'release/**'
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ verify:
+ name: Pruebas y resumen ejecutivo
+ runs-on: ubuntu-latest
+
+ services:
+ postgres:
+ image: postgres:17
+ env:
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: postgres
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Configurar .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Ejecutar la suite y generar el resumen
+ shell: pwsh
+ env:
+ POSTGRES_TEST_CONNECTION: Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres
+ run: ./Tools/Generar-ResumenEjecutivo.ps1
+
+ - name: Publicar el resumen en el job
+ if: always()
+ shell: bash
+ run: |
+ if [ -f docs/VERIFICACION-RESUMEN.md ]; then
+ cat docs/VERIFICACION-RESUMEN.md >> "$GITHUB_STEP_SUMMARY"
+ fi
+
+ - name: Subir artefactos
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: verificacion
+ path: |
+ docs/VERIFICACION-RESUMEN.md
+ Tests/TestResults/*.trx
+ if-no-files-found: warn
diff --git a/.gitignore b/.gitignore
index 68856f0..c8cbcf6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,6 +38,10 @@ project.lock.json
project.fragment.lock.json
artifacts/
+# Test results (artefactos de la suite de verificacion)
+Tests/TestResults/
+*.trx
+
# Publish output
publish/
publish_out/
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 269e0b3..568a055 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,5 +4,9 @@
// jellyfinDataDir: directorio de datos de Jellyfin (configs, plugins, metadata)
"jellyfinDataDir": "${env:LOCALAPPDATA}/jellyfin",
// Nombre del plugin (debe coincidir con el AssemblyName)
- "pluginName": "Jellyfin.Database.Providers.Postgres"
+ "pluginName": "Jellyfin.Database.Providers.Postgres",
+ "sonarlint.connectedMode.project": {
+ "connectionId": "SAETICS",
+ "projectKey": "BORNIOS_Jellyfin-Database-Providers-Postgres"
+ },
}
diff --git a/Directory.Build.props b/Directory.Build.props
index fe2323d..948004c 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
- 2.0.0.0
- 2.0.0.0
- 2.0.0.0
+ 3.0.0.0
+ 3.0.0.0
+ 3.0.0.0
diff --git a/Jellyfin.Database.Providers.Postgres.sln b/Jellyfin.Database.Providers.Postgres.sln
index 21bb076..f0d2b3a 100644
--- a/Jellyfin.Database.Providers.Postgres.sln
+++ b/Jellyfin.Database.Providers.Postgres.sln
@@ -1,9 +1,14 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers.Postgres", "Jellyfin.Database.Providers.Postgres\Jellyfin.Database.Providers.Postgres.csproj", "{D4FAA44B-984A-4FB7-810E-EC3315821AE8}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Database.Providers.Postgres.Tests", "Tests\Jellyfin.Database.Providers.Postgres.Tests\Jellyfin.Database.Providers.Postgres.Tests.csproj", "{B4B57747-951A-4026-985F-772CE174A3AD}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -26,8 +31,23 @@ Global
{D4FAA44B-984A-4FB7-810E-EC3315821AE8}.Release|x64.Build.0 = Release|Any CPU
{D4FAA44B-984A-4FB7-810E-EC3315821AE8}.Release|x86.ActiveCfg = Release|Any CPU
{D4FAA44B-984A-4FB7-810E-EC3315821AE8}.Release|x86.Build.0 = Release|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Debug|x64.Build.0 = Debug|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Debug|x86.Build.0 = Debug|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Release|x64.ActiveCfg = Release|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Release|x64.Build.0 = Release|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Release|x86.ActiveCfg = Release|Any CPU
+ {B4B57747-951A-4026-985F-772CE174A3AD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {B4B57747-951A-4026-985F-772CE174A3AD} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
+ EndGlobalSection
EndGlobal
diff --git a/Jellyfin.Database.Providers.Postgres/Controllers/Models/QueryRequest.cs b/Jellyfin.Database.Providers.Postgres/Controllers/Models/QueryRequest.cs
new file mode 100644
index 0000000..31b19c8
--- /dev/null
+++ b/Jellyfin.Database.Providers.Postgres/Controllers/Models/QueryRequest.cs
@@ -0,0 +1,20 @@
+namespace Jellyfin.Database.Providers.Postgres.Controllers.Models;
+
+///
+/// Request of the ad-hoc SQL console. Only read-only statements are accepted; the service that validates
+/// and executes it is QueryConsoleService.
+///
+public class QueryRequest
+{
+ /// Gets or sets the statement to execute.
+ public string Sql { get; set; } = string.Empty;
+
+ /// Gets or sets the maximum number of rows to return.
+ public int MaxRows { get; set; } = 200;
+
+ ///
+ /// Gets or sets a value indicating whether the execution plan should be returned instead of the rows.
+ /// The plan is generated without ANALYZE, so the statement is only planned, never run for real.
+ ///
+ public bool Explain { get; set; }
+}
diff --git a/Jellyfin.Database.Providers.Postgres/Controllers/Models/SaveConfigRequest.cs b/Jellyfin.Database.Providers.Postgres/Controllers/Models/SaveConfigRequest.cs
index 1c335c8..65ced19 100644
--- a/Jellyfin.Database.Providers.Postgres/Controllers/Models/SaveConfigRequest.cs
+++ b/Jellyfin.Database.Providers.Postgres/Controllers/Models/SaveConfigRequest.cs
@@ -9,6 +9,15 @@ public sealed class SaveConfigRequest
/// Gets or sets the EF command timeout in seconds.
public int CommandTimeout { get; set; } = 60;
+ /// Gets or sets the minimum number of connections kept open in the Npgsql pool.
+ public int MinPoolSize { get; set; } = 4;
+
+ /// Gets or sets the maximum number of connections of the Npgsql pool.
+ public int MaxPoolSize { get; set; } = 100;
+
+ /// Gets or sets how many statements Npgsql keeps prepared per connection.
+ public int MaxAutoPrepare { get; set; } = 50;
+
/// Gets or sets the backup output directory on the server.
public string? BackupDirectory { get; set; }
diff --git a/Jellyfin.Database.Providers.Postgres/Controllers/PostgresController.cs b/Jellyfin.Database.Providers.Postgres/Controllers/PostgresController.cs
index dc9def1..9f49033 100644
--- a/Jellyfin.Database.Providers.Postgres/Controllers/PostgresController.cs
+++ b/Jellyfin.Database.Providers.Postgres/Controllers/PostgresController.cs
@@ -99,6 +99,9 @@ public ActionResult