Compare commits

..

1 Commits

Author SHA1 Message Date
Andrew Rabert
3d0b84ce48 ci: use workflow_run pattern for PR workflows 2026-02-01 15:28:27 -05:00
19 changed files with 209 additions and 232 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "10.0.3",
"version": "10.0.2",
"commands": [
"dotnet-ef"
]

View File

@@ -1,31 +1,17 @@
{
"name": "Development Jellyfin Server",
"image": "mcr.microsoft.com/devcontainers/dotnet:10.0-noble",
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
"postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
// The previous way of installing extensions via the vs command dont work on selfhosted devcontainers
"customizations": {
"vscode": {
"extensions": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.csdevkit",
"alexcvzz.vscode-sqlite",
"streetsidesoftware.code-spell-checker",
"eamodio.gitlens",
"redhat.vscode-xml"
]
}
},
// reads the extensions list and installs them
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
"dotnetRuntimeVersions": "10.0",
"aspNetCoreRuntimeVersions": "10.0"
"dotnetRuntimeVersions": "9.0",
"aspNetCoreRuntimeVersions": "9.0"
},
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"preserve_apt_list": false,

View File

@@ -28,13 +28,13 @@ jobs:
dotnet-version: '10.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/autobuild@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0

55
.github/workflows/ci-compat-build.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: ABI Compatibility Build
on:
pull_request:
permissions: {}
jobs:
abi-head:
name: ABI - HEAD
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Build
run: dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: abi-head
retention-days: 1
if-no-files-found: error
path: out/
abi-base:
name: ABI - BASE
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Build
run: dotnet build Jellyfin.Server -o ./out
- name: Upload Base
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: abi-base
retention-days: 1
if-no-files-found: error
path: out/

View File

@@ -1,87 +1,20 @@
name: ABI Compatibility
name: ABI Compatibility
on:
pull_request_target:
workflow_run:
workflows: ["ABI Compatibility Build"]
types: [completed]
permissions: {}
jobs:
abi-head:
name: ABI - HEAD
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Build
run: |
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: abi-head
retention-days: 14
if-no-files-found: error
path: out/
abi-base:
name: ABI - BASE
if: ${{ github.base_ref != '' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Build
run: |
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: abi-base
retention-days: 14
if-no-files-found: error
path: out/
abi-diff:
permissions:
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
pull-requests: write
name: ABI - Difference
if: ${{ github.event_name == 'pull_request_target' }}
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
runs-on: ubuntu-latest
needs:
- abi-head
- abi-base
steps:
- name: Download abi-head
@@ -89,12 +22,16 @@ jobs:
with:
name: abi-head
path: abi-head
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download abi-base
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: abi-base
path: abi-base
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup ApiCompat
run: |
@@ -118,7 +55,7 @@ jobs:
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
issue-number: ${{ github.event.workflow_run.pull_requests[0].number }}
direction: last
body-includes: abi-diff-workflow-comment
@@ -126,7 +63,7 @@ jobs:
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
issue-number: ${{ github.event.workflow_run.pull_requests[0].number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -145,7 +82,7 @@ jobs:
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
issue-number: ${{ github.event.workflow_run.pull_requests[0].number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
token: ${{ secrets.JF_BOT_TOKEN }}

55
.github/workflows/ci-openapi-build.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: OpenAPI Build
on:
pull_request:
permissions: {}
jobs:
openapi-head:
name: OpenAPI - HEAD
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-head
retention-days: 1
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
openapi-base:
name: OpenAPI - BASE
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-base
retention-days: 1
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json

View File

@@ -1,30 +1,31 @@
name: OpenAPI
on:
push:
branches:
- master
tags:
- 'v*'
pull_request_target:
workflow_run:
workflows: ["OpenAPI Build"]
types: [completed]
permissions: {}
jobs:
openapi-head:
name: OpenAPI - HEAD
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
@@ -36,65 +37,29 @@ jobs:
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
openapi-base:
name: OpenAPI - BASE
if: ${{ github.base_ref != '' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
with:
dotnet-version: '10.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net10.0/openapi.json
openapi-diff:
permissions:
pull-requests: write
name: OpenAPI - Difference
if: ${{ github.event_name == 'pull_request_target' }}
if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
runs-on: ubuntu-latest
needs:
- openapi-head
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: openapi-head
path: openapi-head
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download openapi-base
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: openapi-base
path: openapi-base
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Detect OpenAPI changes
id: openapi-diff
@@ -105,11 +70,12 @@ jobs:
markdown: openapi-changelog.md
add-pr-comment: true
github-token: ${{ secrets.GITHUB_TOKEN }}
pr-number: ${{ github.event.workflow_run.pull_requests[0].number }}
publish-unstable:
name: OpenAPI - Publish Unstable Spec
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
if: ${{ github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- openapi-head
@@ -170,7 +136,7 @@ jobs:
publish-stable:
name: OpenAPI - Publish Stable Spec
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- openapi-head

View File

@@ -4,7 +4,7 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="8.0.1" />
<PackageVersion Include="AsyncKeyedLock" Version="8.0.0" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
@@ -74,12 +74,12 @@
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="[3.116.1]" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.4.1" />
<PackageVersion Include="Svg.Skia" Version="3.2.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.9.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.3.2" />
<PackageVersion Include="System.Text.Json" Version="10.0.2" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.11.0" />
<PackageVersion Include="z440.atl.core" Version="7.10.0" />
<PackageVersion Include="TMDbLib" Version="2.3.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />

View File

@@ -37,25 +37,15 @@ namespace Emby.Server.Implementations.Library
while (attributeIndex > -1 && attributeIndex < maxIndex)
{
var attributeEnd = attributeIndex + attribute.Length;
if (attributeIndex > 0)
if (attributeIndex > 0
&& str[attributeIndex - 1] == '['
&& (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
{
var attributeOpener = str[attributeIndex - 1];
var attributeCloser = attributeOpener switch
var closingIndex = str[attributeEnd..].IndexOf(']');
// Must be at least 1 character before the closing bracket.
if (closingIndex > 1)
{
'[' => ']',
'(' => ')',
'{' => '}',
_ => '\0'
};
if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
{
var closingIndex = str[attributeEnd..].IndexOf(attributeCloser);
// Must be at least 1 character before the closing bracket.
if (closingIndex > 1)
{
return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
}
return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
}
}

View File

@@ -3,7 +3,7 @@
"Playlists": "Плэй-лісты",
"Latest": "Апошняе",
"LabelIpAddressValue": "IP-адрас: {0}",
"ItemAddedWithName": "{0} дададзены ў бібліятэку",
"ItemAddedWithName": "{0} даданы ў бібліятэку",
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне праграмы ўсталявана",
"PluginInstalledWithName": "{0} быў усталяваны",
@@ -14,7 +14,7 @@
"Channels": "Каналы",
"ChapterNameValue": "Раздзел {0}",
"Collections": "Калекцыі",
"Default": радвызначана",
"Default": а змаўчанні",
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
"Folders": "Папкі",
"Favorites": "Абранае",
@@ -81,8 +81,8 @@
"NotificationOptionInstallationFailed": "Збой усталёўкі",
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыя спынена",
"NotificationOptionAudioPlayback": "Прайграванне аўдыя пачалося",
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена",
"NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося",
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
"NotificationOptionPluginError": "Збой плагіна",
"NotificationOptionPluginUninstalled": "Плагін выдалены",

View File

@@ -104,7 +104,7 @@
"TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja dels registres",
"TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
"TaskRefreshLibrary": "Escaneja la mediateca",
"TaskRefreshLibrary": "Escaneig de les mediateques",
"TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
"TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
"TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",

View File

@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
"Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}",
"Collections": "Zbirke",
"Collections": "Kolekcije",
"DeviceOfflineWithName": "{0} je prekinuo vezu",
"DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
@@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto",
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
"Shows": "Emisije",
"Shows": "Serije",
"Songs": "Pjesme",
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",

View File

@@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"Application": "Applicazione",
"Artists": "Artisti",
"AuthenticationSucceededWithUserName": "{0} autenticato correttamente",
"AuthenticationSucceededWithUserName": "{0} autenticato con successo",
"Books": "Libri",
"CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}",
"Channels": "Canali",
@@ -11,36 +11,36 @@
"Collections": "Collezioni",
"DeviceOfflineWithName": "{0} si è disconnesso",
"DeviceOnlineWithName": "{0} è connesso",
"FailedLoginAttemptWithUserName": "Tentativo di accesso non riuscito da {0}",
"FailedLoginAttemptWithUserName": "Tentativo di accesso fallito da {0}",
"Favorites": "Preferiti",
"Folders": "Cartelle",
"Genres": "Generi",
"HeaderAlbumArtists": "Artisti dell'album",
"HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album preferiti",
"HeaderFavoriteArtists": "Artisti preferiti",
"HeaderFavoriteEpisodes": "Episodi preferiti",
"HeaderFavoriteShows": "Serie TV preferite",
"HeaderFavoriteSongs": "Brani preferiti",
"HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti",
"HeaderFavoriteEpisodes": "Episodi Preferiti",
"HeaderFavoriteShows": "Serie TV Preferite",
"HeaderFavoriteSongs": "Brani Preferiti",
"HeaderLiveTV": "Diretta TV",
"HeaderNextUp": "Prossimo",
"HeaderRecordingGroups": "Gruppi di registrazione",
"HomeVideos": "Video personali",
"HeaderRecordingGroups": "Gruppi di Registrazione",
"HomeVideos": "Video Personali",
"Inherit": "Eredita",
"ItemAddedWithName": "{0} è stato aggiunto alla libreria",
"ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
"LabelIpAddressValue": "Indirizzo IP: {0}",
"LabelRunningTimeValue": "Durata: {0}",
"Latest": "Novità",
"MessageApplicationUpdated": "Jellyfin Server è stato aggiornato",
"MessageApplicationUpdated": "Il Server Jellyfin è stato aggiornato",
"MessageApplicationUpdatedTo": "Jellyfin Server è stato aggiornato a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La sezione {0} della configurazione server è stata aggiornata",
"MessageServerConfigurationUpdated": "La configurazione del server è stata aggiornata",
"MixedContent": "Contenuto misto",
"Movies": "Film",
"Music": "Musica",
"MusicVideos": "Video musicali",
"NameInstallFailed": "{0} installazione non riuscita",
"MusicVideos": "Video Musicali",
"NameInstallFailed": "{0} installazione fallita",
"NameSeasonNumber": "Stagione {0}",
"NameSeasonUnknown": "Stagione sconosciuta",
"NewVersionIsAvailable": "Una nuova versione di Jellyfin Server è disponibile per il download.",
@@ -49,37 +49,37 @@
"NotificationOptionAudioPlayback": "La riproduzione audio è iniziata",
"NotificationOptionAudioPlaybackStopped": "La riproduzione audio è stata interrotta",
"NotificationOptionCameraImageUploaded": "Immagine fotocamera caricata",
"NotificationOptionInstallationFailed": "Installazione non riuscita",
"NotificationOptionInstallationFailed": "Installazione fallita",
"NotificationOptionNewLibraryContent": "Nuovo contenuto aggiunto",
"NotificationOptionPluginError": "Errore del plugin",
"NotificationOptionPluginInstalled": "Plugin installato",
"NotificationOptionPluginUninstalled": "Plugin disinstallato",
"NotificationOptionPluginUpdateInstalled": "Aggiornamento plugin installato",
"NotificationOptionServerRestartRequired": "Riavvio del server necessario",
"NotificationOptionTaskFailed": "Operazione pianificata non riuscita",
"NotificationOptionTaskFailed": "Operazione pianificata fallita",
"NotificationOptionUserLockedOut": "Utente bloccato",
"NotificationOptionVideoPlayback": "Riproduzione video iniziata",
"NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
"Photos": "Foto",
"Playlists": "Scalette",
"Playlists": "Playlist",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} è stato installato",
"PluginInstalledWithName": "{0} è stato Installato",
"PluginUninstalledWithName": "{0} è stato disinstallato",
"PluginUpdatedWithName": "{0} è stato aggiornato",
"ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} non riuscito",
"ScheduledTaskFailedWithName": "{0} fallito",
"ScheduledTaskStartedWithName": "{0} avviato",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Serie TV",
"Songs": "Brani",
"StartupEmbyServerIsLoading": "Jellyfin Server si sta avviando. Riprova più tardi.",
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
"SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}",
"Sync": "Sincronizza",
"System": "Sistema",
"TvShows": "Serie TV",
"User": "Utente",
"UserCreatedWithName": "L'utente {0} è stato creato",
"UserDeletedWithName": "L'utente {0} è stato eliminato",
"UserDeletedWithName": "L'utente {0} è stato rimosso",
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
"UserOfflineFromDevice": "{0} si è disconnesso da {1}",
@@ -114,20 +114,20 @@
"TasksLibraryCategory": "Libreria",
"TasksMaintenanceCategory": "Manutenzione",
"TaskCleanActivityLog": "Attività di Registro Completate",
"TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie dell'età configurata.",
"Undefined": "Non specificato",
"TaskCleanActivityLogDescription": "Elimina le voci del registro delle attività più vecchie delletà configurata.",
"Undefined": "Non Definito",
"Forced": "Forzato",
"Default": "Predefinito",
"TaskOptimizeDatabaseDescription": "Compatta database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altre modifiche inerenti il database potrebbe aumentarne le prestazioni.",
"TaskOptimizeDatabase": "Ottimizza database",
"TaskKeyframeExtractor": "Estrattore di Keyframe",
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori scalette HLS. Questa procedura potrebbe richiedere molto tempo.",
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
"External": "Esterno",
"HearingImpaired": "Non udenti",
"HearingImpaired": "Non Udenti",
"TaskRefreshTrickplayImages": "Genera immagini Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate.",
"TaskCleanCollectionsAndPlaylists": "Ripulisci le collezioni e le scalette",
"TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle scalette che non esistono più.",
"TaskCleanCollectionsAndPlaylists": "Ripulire le collezioni e le playlist",
"TaskCleanCollectionsAndPlaylistsDescription": "Rimuove gli elementi dalle collezioni e dalle playlist che non esistono più.",
"TaskAudioNormalization": "Normalizzazione dell'audio",
"TaskAudioNormalizationDescription": "Scansiona i file alla ricerca dei dati per la normalizzazione dell'audio.",
"TaskDownloadMissingLyricsDescription": "Scarica testi per le canzoni",

View File

@@ -124,8 +124,8 @@
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
"External": "Externo",
"HearingImpaired": "Surdo",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskRefreshTrickplayImages": "Gerar Imagens de Trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria ficheiros de trickplay para vídeos nas bibliotecas ativas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",

View File

@@ -124,8 +124,8 @@
"HearingImpaired": "Problemas auditivos",
"TaskKeyframeExtractor": "Extrator de quadro-chave",
"TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
"TaskRefreshTrickplayImages": "Gerar imagens de trickplay",
"TaskRefreshTrickplayImagesDescription": "Cria pré-visualizações de trickplay para vídeos nas bibliotecas ativadas.",
"TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
"TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas.",
"TaskCleanCollectionsAndPlaylistsDescription": "Remove itens de coleções e listas de reprodução que já não existem.",
"TaskCleanCollectionsAndPlaylists": "Limpar coleções e listas de reprodução",
"TaskAudioNormalizationDescription": "Analisa os ficheiros para obter dados de normalização de áudio.",

View File

@@ -191,17 +191,9 @@ public class DisplayPreferencesController : BaseJellyfinApiController
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{
var viewType = displayPreferences.CustomPrefs[key];
if (string.IsNullOrEmpty(viewType))
if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out _))
{
displayPreferences.CustomPrefs.Remove(key);
continue;
}
if (!Enum.TryParse<ViewType>(viewType, true, out _))
{
_logger.LogError("Invalid ViewType: {LandingScreenOption}", viewType);
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
displayPreferences.CustomPrefs.Remove(key);
}
}

View File

@@ -74,10 +74,9 @@ public class PeopleRepository(IDbContextFactory<JellyfinDbContext> dbProvider, I
/// <inheritdoc />
public void UpdatePeople(Guid itemId, IReadOnlyList<PersonInfo> people)
{
foreach (var person in people)
foreach (var item in people.Where(e => e.Role is null))
{
person.Name = person.Name.Trim();
person.Role = person.Role?.Trim() ?? string.Empty;
item.Role = string.Empty;
}
// multiple metadata providers can provide the _same_ person

View File

@@ -117,6 +117,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode);
languages.Add(preferredLanguage);
if (preferredLanguage.Length == 5) // Like en-US
{
// Currently, TMDb supports 2-letter language codes only.
// They are planning to change this in the future, thus we're
// supplying both codes if we're having a 5-letter code.
languages.Add(preferredLanguage.Substring(0, 2));
}
}
languages.Add("null");

View File

@@ -11,29 +11,21 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid=tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid-tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)]
[InlineData("Superman: Red Son", "something", null)]
[InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid1=tt11111111}(imdbid=tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid1-tt11111111)[imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid1-tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355]{imdbid-tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (tmdbid-618355)[imdbid-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355][imdbid-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
[InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
[InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
[InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
[InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
[InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
[InlineData("Superman: Red Son {tmdbid=3}", "tmdbid", "3")]
[InlineData("Superman: Red Son (tvdbid-6)", "tvdbid", "6")]
[InlineData("[tmdbid=618355]", "tmdbid", "618355")]
[InlineData("{tmdbid=618355}", "tmdbid", "618355")]
[InlineData("(tmdbid=618355)", "tmdbid", "618355")]
[InlineData("[tmdbid-618355]", "tmdbid", "618355")]
[InlineData("{tmdbid-618355)", "tmdbid", null)]
[InlineData("[tmdbid-618355}", "tmdbid", null)]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
[InlineData("tmdbid=618355]", "tmdbid", null)]
@@ -44,9 +36,6 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)]
[InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)]
[InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")]
[InlineData("{tmdbid=}{imdbid=tt10985510}", "tmdbid", null)]
[InlineData("(tmdbid-)(imdbid-tt10985510)", "tmdbid", null)]
[InlineData("Superman: Red Son {tmdbid-618355}{tmdbid=1234567}", "tmdbid", "618355")]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));