mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-16 16:18:06 +00:00
Compare commits
30 Commits
renovate/a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a9bb060eb | ||
|
|
6a7600a8c6 | ||
|
|
0892847c2f | ||
|
|
62e51fd00a | ||
|
|
cf9051c277 | ||
|
|
d270957c82 | ||
|
|
0ee872999d | ||
|
|
c4f4dcc181 | ||
|
|
22ee5113d0 | ||
|
|
e233eee07b | ||
|
|
185849b68a | ||
|
|
e62b6f8339 | ||
|
|
9931537d87 | ||
|
|
82c4df5cde | ||
|
|
103f556c8d | ||
|
|
582a1d9866 | ||
|
|
244757c92c | ||
|
|
0ff869dfcd | ||
|
|
a1e0e4fd9d | ||
|
|
4138214ac3 | ||
|
|
8a1129bbde | ||
|
|
706a8d2850 | ||
|
|
ba4dbcf5a1 | ||
|
|
bfae788a44 | ||
|
|
18dc32d735 | ||
|
|
85ff708597 | ||
|
|
e5fb071708 | ||
|
|
6c8395ff87 | ||
|
|
82b2e7773f | ||
|
|
336958318d |
11
.github/workflows/ci-codeql-analysis.yml
vendored
11
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -21,17 +21,20 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
|
||||
4
.github/workflows/ci-compat.yml
vendored
4
.github/workflows/ci-compat.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
|
||||
81
.github/workflows/ci-openapi.yml
vendored
81
.github/workflows/ci-openapi.yml
vendored
@@ -20,12 +20,15 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
with:
|
||||
dotnet-version: '9.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:
|
||||
@@ -46,6 +49,7 @@ jobs:
|
||||
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 }}
|
||||
@@ -54,12 +58,15 @@ jobs:
|
||||
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@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
with:
|
||||
dotnet-version: '9.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:
|
||||
@@ -70,7 +77,7 @@ jobs:
|
||||
|
||||
openapi-diff:
|
||||
permissions:
|
||||
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
|
||||
pull-requests: write
|
||||
|
||||
name: OpenAPI - Difference
|
||||
if: ${{ github.event_name == 'pull_request_target' }}
|
||||
@@ -84,67 +91,23 @@ jobs:
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
- name: Workaround openapi-diff issue
|
||||
run: |
|
||||
sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
|
||||
sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
|
||||
- name: Calculate OpenAPI difference
|
||||
uses: docker://openapitools/openapi-diff
|
||||
continue-on-error: true
|
||||
with:
|
||||
args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
|
||||
- id: read-diff
|
||||
name: Read openapi-diff output
|
||||
run: |
|
||||
# Read and fix markdown
|
||||
body=$(cat openapi-changes.md)
|
||||
# Write to workflow summary
|
||||
echo "$body" >> $GITHUB_STEP_SUMMARY
|
||||
# Set ApiChanged var
|
||||
if [ "$body" != '' ]; then
|
||||
echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
# Add header/footer for diff comment
|
||||
echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md
|
||||
echo "<details>" >> openapi-changes-reply.md
|
||||
echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md
|
||||
echo "" >> openapi-changes-reply.md
|
||||
echo "$body" >> openapi-changes-reply.md
|
||||
echo "" >> openapi-changes-reply.md
|
||||
echo "</details>" >> openapi-changes-reply.md
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
direction: last
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- name: Reply or edit difference comment (changed)
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body-path: openapi-changes-reply.md
|
||||
- name: Edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!--openapi-diff-workflow-comment-->
|
||||
|
||||
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
||||
- name: Detect OpenAPI changes
|
||||
id: openapi-diff
|
||||
uses: jellyfin/openapi-diff-action@9274f6bda9d01ab091942a4a8334baa53692e8a4 # v1.0.0
|
||||
with:
|
||||
old-spec: openapi-base/openapi.json
|
||||
new-spec: openapi-head/openapi.json
|
||||
markdown: openapi-changelog.md
|
||||
add-pr-comment: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
publish-unstable:
|
||||
name: OpenAPI - Publish Unstable Spec
|
||||
@@ -178,7 +141,6 @@ jobs:
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
key: "${{ secrets.REPO_KEY }}"
|
||||
debug: false
|
||||
script_stop: false
|
||||
script: |
|
||||
if ! test -d /run/workflows; then
|
||||
sudo mkdir -p /run/workflows
|
||||
@@ -240,7 +202,6 @@ jobs:
|
||||
username: "${{ secrets.REPO_USER }}"
|
||||
key: "${{ secrets.REPO_KEY }}"
|
||||
debug: false
|
||||
script_stop: false
|
||||
script: |
|
||||
if ! test -d /run/workflows; then
|
||||
sudo mkdir -p /run/workflows
|
||||
|
||||
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
- uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
with:
|
||||
dotnet-version: ${{ env.SDK_VERSION }}
|
||||
|
||||
|
||||
3
.github/workflows/commands.yml
vendored
3
.github/workflows/commands.yml
vendored
@@ -43,13 +43,16 @@ jobs:
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
|
||||
- name: install python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
|
||||
- name: install python packages
|
||||
run: pip install -r rename/requirements.txt
|
||||
|
||||
- name: run rename script
|
||||
run: python3 rename.py
|
||||
working-directory: ./rename
|
||||
|
||||
3
.github/workflows/issue-template-check.yml
vendored
3
.github/workflows/issue-template-check.yml
vendored
@@ -13,13 +13,16 @@ jobs:
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
|
||||
- name: install python
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: '3.14'
|
||||
cache: 'pip'
|
||||
|
||||
- name: install python packages
|
||||
run: pip install -r main-repo-triage/requirements.txt
|
||||
|
||||
- name: check and comment issue
|
||||
working-directory: ./main-repo-triage
|
||||
run: python3 single_issue_gha.py
|
||||
|
||||
1
.github/workflows/project-automation.yml
vendored
1
.github/workflows/project-automation.yml
vendored
@@ -21,6 +21,7 @@ jobs:
|
||||
with:
|
||||
project: Current Release
|
||||
action: delete
|
||||
column: In progress
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Add to 'Release Next' project
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
- [Kirill Nikiforov](https://github.com/allmazz)
|
||||
- [bjorntp](https://github.com/bjorntp)
|
||||
- [martenumberto](https://github.com/martenumberto)
|
||||
- [ZeusCraft10](https://github.com/ZeusCraft10)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="4.0.12" />
|
||||
<PackageVersion Include="LrcParser" Version="2025.623.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.11" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
|
||||
@@ -88,7 +88,7 @@
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.11" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.11" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="z440.atl.core" Version="7.9.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" />
|
||||
|
||||
@@ -39,22 +39,24 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
{
|
||||
if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
|
||||
{
|
||||
var iterations = GetIterationsParameter(hash);
|
||||
return hash.Hash.SequenceEqual(
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
hash.Salt,
|
||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||
iterations,
|
||||
HashAlgorithmName.SHA1,
|
||||
32));
|
||||
}
|
||||
|
||||
if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
|
||||
{
|
||||
var iterations = GetIterationsParameter(hash);
|
||||
return hash.Hash.SequenceEqual(
|
||||
Rfc2898DeriveBytes.Pbkdf2(
|
||||
password,
|
||||
hash.Salt,
|
||||
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
|
||||
iterations,
|
||||
HashAlgorithmName.SHA512,
|
||||
DefaultOutputLength));
|
||||
}
|
||||
@@ -62,6 +64,27 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
throw new NotSupportedException($"Can't verify hash with id: {hash.Id}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts and validates the iterations parameter from a password hash.
|
||||
/// </summary>
|
||||
/// <param name="hash">The password hash containing parameters.</param>
|
||||
/// <returns>The number of iterations.</returns>
|
||||
/// <exception cref="FormatException">Thrown when iterations parameter is missing or invalid.</exception>
|
||||
private static int GetIterationsParameter(PasswordHash hash)
|
||||
{
|
||||
if (!hash.Parameters.TryGetValue("iterations", out var iterationsStr))
|
||||
{
|
||||
throw new FormatException($"Password hash with id '{hash.Id}' is missing required 'iterations' parameter.");
|
||||
}
|
||||
|
||||
if (!int.TryParse(iterationsStr, CultureInfo.InvariantCulture, out var iterations))
|
||||
{
|
||||
throw new FormatException($"Password hash with id '{hash.Id}' has invalid 'iterations' parameter: '{iterationsStr}'.");
|
||||
}
|
||||
|
||||
return iterations;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public byte[] GenerateSalt()
|
||||
=> GenerateSalt(DefaultSaltLength);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"Collections": "Калекцыі",
|
||||
"Default": "Па змаўчанні",
|
||||
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
|
||||
"Folders": "Тэчкі",
|
||||
"Folders": "Папкі",
|
||||
"Favorites": "Абранае",
|
||||
"External": "Знешні",
|
||||
"Genres": "Жанры",
|
||||
@@ -95,7 +95,7 @@
|
||||
"ServerNameNeedsToBeRestarted": "{0} патрабуе перазапуску",
|
||||
"Shows": "Шоу",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
|
||||
"SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "Субцітры для {1} не ўдалося спампаваць з {0}",
|
||||
"TvShows": "Тэлепраграма",
|
||||
"Undefined": "Нявызначана",
|
||||
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
|
||||
@@ -104,7 +104,7 @@
|
||||
"UserStartedPlayingItemWithValues": "{0} прайграваецца {1} на {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
|
||||
"ValueSpecialEpisodeName": "Спецэпізод - {0}",
|
||||
"ValueSpecialEpisodeName": "Спецвыпуск - {0}",
|
||||
"VersionNumber": "Версія {0}",
|
||||
"TasksMaintenanceCategory": "Абслугоўванне",
|
||||
"TasksLibraryCategory": "Бібліятэка",
|
||||
@@ -114,7 +114,7 @@
|
||||
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
|
||||
"TaskRefreshChapterImages": "Вынуць выявы раздзелаў",
|
||||
"TaskRefreshLibrary": "Сканаваць бібліятэку",
|
||||
"TaskRefreshLibraryDescription": "Скануе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
|
||||
"TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метаданыя.",
|
||||
"TaskCleanLogs": "Ачысціць журнал",
|
||||
"TaskRefreshPeople": "Абнавіць выканаўцаў",
|
||||
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
|
||||
@@ -137,5 +137,5 @@
|
||||
"TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
|
||||
"TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
|
||||
"CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
|
||||
"CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
|
||||
"CleanupUserDataTaskDescription": "Ачышчае ўсе даныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"CameraImageUploadedFrom": "Mae delwedd camera newydd wedi'i lanlwytho o {0}",
|
||||
"Books": "Llyfrau",
|
||||
"AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus",
|
||||
"Artists": "Artistiaid",
|
||||
"Artists": "Crewyr",
|
||||
"AppDeviceValues": "Ap: {0}, Dyfais: {1}",
|
||||
"Albums": "Albwmau",
|
||||
"Genres": "Genres",
|
||||
@@ -67,7 +67,7 @@
|
||||
"NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain",
|
||||
"MessageServerConfigurationUpdated": "Mae gosodiadau gweinydd wedi'i ddiweddaru",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Mae adran gosodiadau gweinydd {0} wedi'i diweddaru",
|
||||
"FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu gan {0}",
|
||||
"FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu o {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau",
|
||||
"UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}",
|
||||
@@ -123,5 +123,14 @@
|
||||
"TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
|
||||
"TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
|
||||
"TaskCleanCache": "Gwaghau Ffolder Cache",
|
||||
"HearingImpaired": "Nam ar y clyw"
|
||||
"HearingImpaired": "Nam ar y clyw",
|
||||
"TaskAudioNormalization": "Gwastatau Sain",
|
||||
"TaskAudioNormalizationDescription": "Yn sganio ffeiliau am ddata gwastatau sain.",
|
||||
"TaskRefreshTrickplayImages": "Creuwch lluniau Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Creu rhagolygon Trickplay ar gyfer fideos mewn llyfrgelloedd gweithredol.",
|
||||
"TaskDownloadMissingLyrics": "Lawrlwytho geiriau coll",
|
||||
"TaskDownloadMissingLyricsDescription": "Lawrlwytho geiriau caneuon",
|
||||
"TaskCleanCollectionsAndPlaylists": "Glanhau casgliadau a rhestrau chwarae",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Dileu eitemau o gasgliadau a rhestrau chwarae sydd ddim yn bodoli bellach.",
|
||||
"TaskExtractMediaSegments": "Sganio Darnau Cyfryngau"
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
|
||||
"NewVersionIsAvailable": "Jellyfin serveri uus versioon on allalaadimiseks saadaval.",
|
||||
"NameSeasonUnknown": "Tundmatu hooaeg",
|
||||
"NameSeasonNumber": "Hooaeg {0}",
|
||||
"NameSeasonNumber": "{0}. hooaeg",
|
||||
"NameInstallFailed": "{0} paigaldamine nurjus",
|
||||
"MusicVideos": "Muusikavideod",
|
||||
"Music": "Muusika",
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"AppDeviceValues": "അപ്ലിക്കേഷൻ: {0}, ഉപകരണം: {1}",
|
||||
"Application": "അപ്ലിക്കേഷൻ",
|
||||
"AuthenticationSucceededWithUserName": "{0} വിജയകരമായി പ്രാമാണീകരിച്ചു",
|
||||
"CameraImageUploadedFrom": "Camera 0 from എന്നതിൽ നിന്ന് ഒരു പുതിയ ക്യാമറ ചിത്രം അപ്ലോഡുചെയ്തു",
|
||||
"CameraImageUploadedFrom": "{0} എന്നതിൽ നിന്ന് ഒരു പുതിയ ക്യാമറ ചിത്രം അപ്ലോഡുചെയ്തു",
|
||||
"ChapterNameValue": "അധ്യായം {0}",
|
||||
"DeviceOfflineWithName": "{0} വിച്ഛേദിച്ചു",
|
||||
"DeviceOnlineWithName": "{0} ബന്ധിപ്പിച്ചു",
|
||||
"FailedLoginAttemptWithUserName": "{0}ൽ നിന്നുള്ള പ്രവേശന ശ്രമം പരാജയപ്പെട്ടു",
|
||||
"Forced": "നിർബന്ധിച്ചു",
|
||||
"Forced": "നിർബന്ധിതമായി",
|
||||
"HeaderFavoriteAlbums": "പ്രിയപ്പെട്ട ആൽബങ്ങൾ",
|
||||
"HeaderFavoriteArtists": "പ്രിയപ്പെട്ട കലാകാരന്മാർ",
|
||||
"HeaderFavoriteEpisodes": "പ്രിയപ്പെട്ട എപ്പിസോഡുകൾ",
|
||||
@@ -114,7 +114,7 @@
|
||||
"Artists": "കലാകാരന്മാർ",
|
||||
"Shows": "ഷോകൾ",
|
||||
"Default": "സ്ഥിരസ്ഥിതി",
|
||||
"Favorites": "പ്രിയങ്കരങ്ങൾ",
|
||||
"Favorites": "പ്രിയപ്പെട്ടവ",
|
||||
"Books": "പുസ്തകങ്ങൾ",
|
||||
"Genres": "വിഭാഗങ്ങൾ",
|
||||
"Channels": "ചാനലുകൾ",
|
||||
|
||||
@@ -156,6 +156,11 @@ namespace Emby.Server.Implementations.Updates
|
||||
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
|
||||
return Array.Empty<PackageInfo>();
|
||||
}
|
||||
catch (NotSupportedException ex)
|
||||
{
|
||||
_logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}", manifest);
|
||||
return Array.Empty<PackageInfo>();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
|
||||
|
||||
@@ -754,7 +754,9 @@ public class DynamicHlsHelper
|
||||
{
|
||||
if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string? profile = state.GetRequestedProfiles("aac").FirstOrDefault();
|
||||
string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
|
||||
? state.AudioStream?.Profile : state.GetRequestedProfiles("aac").FirstOrDefault();
|
||||
|
||||
return HlsCodecStringHelpers.GetAACString(profile);
|
||||
}
|
||||
|
||||
@@ -788,6 +790,19 @@ public class DynamicHlsHelper
|
||||
return HlsCodecStringHelpers.GetOPUSString();
|
||||
}
|
||||
|
||||
if (string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HlsCodecStringHelpers.GetTRUEHDString();
|
||||
}
|
||||
|
||||
if (string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// lavc only support encoding DTS core profile
|
||||
string? profile = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) ? state.AudioStream?.Profile : "DTS";
|
||||
|
||||
return HlsCodecStringHelpers.GetDTSString(profile);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,11 @@ public static class HlsCodecStringHelpers
|
||||
/// </summary>
|
||||
public const string OPUS = "Opus";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for TRUEHD.
|
||||
/// </summary>
|
||||
public const string TRUEHD = "mlpa";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a MP3 codec string.
|
||||
/// </summary>
|
||||
@@ -59,7 +64,7 @@ public static class HlsCodecStringHelpers
|
||||
{
|
||||
StringBuilder result = new StringBuilder("mp4a", 9);
|
||||
|
||||
if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(profile, "HE-AAC", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.Append(".40.5");
|
||||
}
|
||||
@@ -117,6 +122,46 @@ public static class HlsCodecStringHelpers
|
||||
return OPUS;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an TRUEHD codec string.
|
||||
/// </summary>
|
||||
/// <returns>TRUEHD codec string.</returns>
|
||||
public static string GetTRUEHDString()
|
||||
{
|
||||
return TRUEHD;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an DTS codec string.
|
||||
/// </summary>
|
||||
/// <param name="profile">DTS profile.</param>
|
||||
/// <returns>DTS codec string.</returns>
|
||||
public static string GetDTSString(string? profile)
|
||||
{
|
||||
if (string.Equals(profile, "DTS", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(profile, "DTS-ES", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(profile, "DTS 96/24", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "dtsc";
|
||||
}
|
||||
|
||||
if (string.Equals(profile, "DTS-HD HRA", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(profile, "DTS-HD MA", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(profile, "DTS-HD MA + DTS:X", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(profile, "DTS-HD MA + DTS:X IMAX", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "dtsh";
|
||||
}
|
||||
|
||||
if (string.Equals(profile, "DTS Express", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "dtse";
|
||||
}
|
||||
|
||||
// Default to DTS core if profile is invalid
|
||||
return "dtsc";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a H.264 codec string.
|
||||
/// </summary>
|
||||
|
||||
@@ -277,7 +277,7 @@ public sealed class BaseItemRepository
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
|
||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
result.Items = dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
|
||||
result.StartIndex = filter.StartIndex ?? 0;
|
||||
return result;
|
||||
}
|
||||
@@ -297,7 +297,7 @@ public sealed class BaseItemRepository
|
||||
dbQuery = ApplyQueryPaging(dbQuery, filter);
|
||||
dbQuery = ApplyNavigations(dbQuery, filter);
|
||||
|
||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -341,7 +341,7 @@ public sealed class BaseItemRepository
|
||||
|
||||
mainquery = ApplyNavigations(mainquery, filter);
|
||||
|
||||
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).ToArray();
|
||||
return mainquery.AsEnumerable().Where(e => e is not null).Select(w => DeserializeBaseItem(w, filter.SkipDeserialization)).Where(dto => dto is not null).ToArray()!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -1159,7 +1159,7 @@ public sealed class BaseItemRepository
|
||||
return type.GetCustomAttribute<RequiresSourceSerialisationAttribute>() == null;
|
||||
}
|
||||
|
||||
private BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
|
||||
private BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, bool skipDeserialization = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(baseItemEntity, nameof(baseItemEntity));
|
||||
if (_serverConfigurationManager?.Configuration is null)
|
||||
@@ -1182,11 +1182,19 @@ public sealed class BaseItemRepository
|
||||
/// <param name="logger">Logger.</param>
|
||||
/// <param name="appHost">The application server Host.</param>
|
||||
/// <param name="skipDeserialization">If only mapping should be processed.</param>
|
||||
/// <returns>A mapped BaseItem.</returns>
|
||||
/// <exception cref="InvalidOperationException">Will be thrown if an invalid serialisation is requested.</exception>
|
||||
public static BaseItemDto DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
|
||||
/// <returns>A mapped BaseItem, or null if the item type is unknown.</returns>
|
||||
public static BaseItemDto? DeserializeBaseItem(BaseItemEntity baseItemEntity, ILogger logger, IServerApplicationHost? appHost, bool skipDeserialization = false)
|
||||
{
|
||||
var type = GetType(baseItemEntity.Type) ?? throw new InvalidOperationException("Cannot deserialize unknown type.");
|
||||
var type = GetType(baseItemEntity.Type);
|
||||
if (type is null)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Skipping item {ItemId} with unknown type '{ItemType}'. This may indicate a removed plugin or database corruption.",
|
||||
baseItemEntity.Id,
|
||||
baseItemEntity.Type);
|
||||
return null;
|
||||
}
|
||||
|
||||
BaseItemDto? dto = null;
|
||||
if (TypeRequiresDeserialization(type) && baseItemEntity.Data is not null && !skipDeserialization)
|
||||
{
|
||||
@@ -1353,10 +1361,9 @@ public sealed class BaseItemRepository
|
||||
.. resultQuery
|
||||
.AsEnumerable()
|
||||
.Where(e => e is not null)
|
||||
.Select(e =>
|
||||
{
|
||||
return (DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount);
|
||||
})
|
||||
.Select(e => (Item: DeserializeBaseItem(e.item, filter.SkipDeserialization), e.itemCount))
|
||||
.Where(e => e.Item is not null)
|
||||
.Select(e => (e.Item!, e.itemCount))
|
||||
];
|
||||
}
|
||||
else
|
||||
@@ -1367,10 +1374,9 @@ public sealed class BaseItemRepository
|
||||
.. query
|
||||
.AsEnumerable()
|
||||
.Where(e => e is not null)
|
||||
.Select<BaseItemEntity, (BaseItemDto, ItemCounts?)>(e =>
|
||||
{
|
||||
return (DeserializeBaseItem(e, filter.SkipDeserialization), null);
|
||||
})
|
||||
.Select(e => (Item: DeserializeBaseItem(e, filter.SkipDeserialization), ItemCounts: (ItemCounts?)null))
|
||||
.Where(e => e.Item is not null)
|
||||
.Select(e => (e.Item!, e.ItemCounts))
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2489,35 +2495,24 @@ public sealed class BaseItemRepository
|
||||
|
||||
if (filter.ExcludeInheritedTags.Length > 0)
|
||||
{
|
||||
var excludedTags = filter.ExcludeInheritedTags;
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
!e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||
&& (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue ||
|
||||
!context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))));
|
||||
!e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
|
||||
&& (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))));
|
||||
}
|
||||
|
||||
if (filter.IncludeInheritedTags.Length > 0)
|
||||
{
|
||||
// For seasons and episodes, we also need to check the parent series' tags.
|
||||
if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
|
||||
{
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||
|| (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
|
||||
}
|
||||
var includeTags = filter.IncludeInheritedTags;
|
||||
var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist;
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
|
||||
|
||||
// A playlist should be accessible to its owner regardless of allowed tags.
|
||||
else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
|
||||
{
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
|
||||
|| e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
|
||||
// d ^^ this is stupid it hate this.
|
||||
}
|
||||
else
|
||||
{
|
||||
baseQuery = baseQuery.Where(e =>
|
||||
e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
|
||||
}
|
||||
// For seasons and episodes, we also need to check the parent series' tags.
|
||||
|| (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue)))
|
||||
|
||||
// A playlist should be accessible to its owner regardless of allowed tags
|
||||
|| (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
|
||||
}
|
||||
|
||||
if (filter.SeriesStatuses.Length > 0)
|
||||
@@ -2671,6 +2666,6 @@ public sealed class BaseItemRepository
|
||||
.Where(e => artistNames.Contains(e.Name))
|
||||
.ToArray();
|
||||
|
||||
return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast<MusicArtist>().ToArray());
|
||||
return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast<MusicArtist>().ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1247,8 +1247,11 @@ internal class MigrateLibraryDb : IDatabaseMigrationRoutine
|
||||
}
|
||||
|
||||
var baseItem = BaseItemRepository.DeserializeBaseItem(entity, _logger, null, false);
|
||||
var dataKeys = baseItem.GetUserDataKeys();
|
||||
userDataKeys.AddRange(dataKeys);
|
||||
if (baseItem is not null)
|
||||
{
|
||||
var dataKeys = baseItem.GetUserDataKeys();
|
||||
userDataKeys.AddRange(dataKeys);
|
||||
}
|
||||
|
||||
return (entity, userDataKeys.ToArray());
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
query.AncestorWithPresentationUniqueKey = null;
|
||||
query.SeriesPresentationUniqueKey = seriesKey;
|
||||
query.IncludeItemTypes = new[] { BaseItemKind.Season };
|
||||
query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) };
|
||||
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
|
||||
|
||||
if (user is not null && !user.DisplayMissingEpisodes)
|
||||
{
|
||||
@@ -247,6 +247,10 @@ namespace MediaBrowser.Controller.Entities.TV
|
||||
|
||||
query.AncestorWithPresentationUniqueKey = null;
|
||||
query.SeriesPresentationUniqueKey = seriesKey;
|
||||
if (query.OrderBy.Count == 0)
|
||||
{
|
||||
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
|
||||
}
|
||||
|
||||
if (query.IncludeItemTypes.Length == 0)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
@@ -25,14 +27,11 @@ namespace MediaBrowser.Model.Extensions
|
||||
|
||||
return string.Create(
|
||||
str.Length,
|
||||
str,
|
||||
str.AsSpan(),
|
||||
(chars, buf) =>
|
||||
{
|
||||
chars[0] = char.ToUpperInvariant(buf[0]);
|
||||
for (int i = 1; i < chars.Length; i++)
|
||||
{
|
||||
chars[i] = buf[i];
|
||||
}
|
||||
buf.Slice(1).CopyTo(chars.Slice(1));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
@@ -83,7 +84,9 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
||||
if (!string.IsNullOrEmpty(releaseGroupId))
|
||||
{
|
||||
var releaseGroupResult = await _musicBrainzQuery.LookupReleaseGroupAsync(new Guid(releaseGroupId), Include.Releases, null, cancellationToken).ConfigureAwait(false);
|
||||
return GetReleaseGroupResult(releaseGroupResult.Releases);
|
||||
|
||||
// No need to pass the cancellation token to GetReleaseGroupResultAsync as we're already passing it to ToBlockingEnumerable
|
||||
return GetReleaseGroupResultAsync(releaseGroupResult.Releases, CancellationToken.None).ToBlockingEnumerable(cancellationToken);
|
||||
}
|
||||
|
||||
var artistMusicBrainzId = searchInfo.GetMusicBrainzArtistId();
|
||||
@@ -128,7 +131,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<RemoteSearchResult> GetReleaseGroupResult(IEnumerable<IRelease>? releaseSearchResults)
|
||||
private async IAsyncEnumerable<RemoteSearchResult> GetReleaseGroupResultAsync(IEnumerable<IRelease>? releaseSearchResults, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (releaseSearchResults is null)
|
||||
{
|
||||
@@ -138,7 +141,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu
|
||||
foreach (var result in releaseSearchResults)
|
||||
{
|
||||
// Fetch full release info, otherwise artists are missing
|
||||
var fullResult = _musicBrainzQuery.LookupRelease(result.Id, Include.Artists | Include.ReleaseGroups);
|
||||
var fullResult = await _musicBrainzQuery.LookupReleaseAsync(result.Id, Include.Artists | Include.ReleaseGroups, cancellationToken).ConfigureAwait(false);
|
||||
yield return GetReleaseResult(fullResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using Emby.Server.Implementations.Cryptography;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.Cryptography;
|
||||
|
||||
public class CryptographyProviderTests
|
||||
{
|
||||
private readonly CryptographyProvider _sut = new();
|
||||
|
||||
[Fact]
|
||||
public void CreatePasswordHash_WithPassword_ReturnsHashWithIterations()
|
||||
{
|
||||
var hash = _sut.CreatePasswordHash("testpassword");
|
||||
|
||||
Assert.Equal("PBKDF2-SHA512", hash.Id);
|
||||
Assert.True(hash.Parameters.ContainsKey("iterations"));
|
||||
Assert.NotEmpty(hash.Salt.ToArray());
|
||||
Assert.NotEmpty(hash.Hash.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_WithValidPassword_ReturnsTrue()
|
||||
{
|
||||
const string password = "testpassword";
|
||||
var hash = _sut.CreatePasswordHash(password);
|
||||
|
||||
Assert.True(_sut.Verify(hash, password));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_WithWrongPassword_ReturnsFalse()
|
||||
{
|
||||
var hash = _sut.CreatePasswordHash("correctpassword");
|
||||
|
||||
Assert.False(_sut.Verify(hash, "wrongpassword"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_PBKDF2_MissingIterations_ThrowsFormatException()
|
||||
{
|
||||
var hash = PasswordHash.Parse("$PBKDF2$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
|
||||
|
||||
var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
|
||||
Assert.Contains("missing required 'iterations' parameter", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_PBKDF2SHA512_MissingIterations_ThrowsFormatException()
|
||||
{
|
||||
var hash = PasswordHash.Parse("$PBKDF2-SHA512$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
|
||||
|
||||
var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
|
||||
Assert.Contains("missing required 'iterations' parameter", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_PBKDF2_InvalidIterationsFormat_ThrowsFormatException()
|
||||
{
|
||||
var hash = PasswordHash.Parse("$PBKDF2$iterations=abc$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
|
||||
|
||||
var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
|
||||
Assert.Contains("invalid 'iterations' parameter", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_PBKDF2SHA512_InvalidIterationsFormat_ThrowsFormatException()
|
||||
{
|
||||
var hash = PasswordHash.Parse("$PBKDF2-SHA512$iterations=notanumber$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
|
||||
|
||||
var exception = Assert.Throws<FormatException>(() => _sut.Verify(hash, "password"));
|
||||
Assert.Contains("invalid 'iterations' parameter", exception.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Verify_UnsupportedHashId_ThrowsNotSupportedException()
|
||||
{
|
||||
var hash = PasswordHash.Parse("$UNKNOWN$69F420$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D");
|
||||
|
||||
Assert.Throws<NotSupportedException>(() => _sut.Verify(hash, "password"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateSalt_ReturnsNonEmptyArray()
|
||||
{
|
||||
var salt = _sut.GenerateSalt();
|
||||
|
||||
Assert.NotEmpty(salt);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(16)]
|
||||
[InlineData(32)]
|
||||
[InlineData(64)]
|
||||
public void GenerateSalt_WithLength_ReturnsArrayOfSpecifiedLength(int length)
|
||||
{
|
||||
var salt = _sut.GenerateSalt(length);
|
||||
|
||||
Assert.Equal(length, salt.Length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Server.Implementations.Item;
|
||||
using MediaBrowser.Controller;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.Item;
|
||||
|
||||
public class BaseItemRepositoryTests
|
||||
{
|
||||
[Fact]
|
||||
public void DeserializeBaseItem_WithUnknownType_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var entity = new BaseItemEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = "NonExistent.Plugin.CustomItemType"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = BaseItemRepository.DeserializeBaseItem(entity, NullLogger.Instance, null, false);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeserializeBaseItem_WithUnknownType_LogsWarning()
|
||||
{
|
||||
// Arrange
|
||||
var entity = new BaseItemEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = "NonExistent.Plugin.CustomItemType"
|
||||
};
|
||||
var loggerMock = new Mock<ILogger>();
|
||||
|
||||
// Act
|
||||
BaseItemRepository.DeserializeBaseItem(entity, loggerMock.Object, null, false);
|
||||
|
||||
// Assert
|
||||
loggerMock.Verify(
|
||||
x => x.Log(
|
||||
LogLevel.Warning,
|
||||
It.IsAny<EventId>(),
|
||||
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains("unknown type", StringComparison.OrdinalIgnoreCase)),
|
||||
It.IsAny<Exception?>(),
|
||||
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeserializeBaseItem_WithKnownType_ReturnsItem()
|
||||
{
|
||||
// Arrange
|
||||
var entity = new BaseItemEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Type = "MediaBrowser.Controller.Entities.Movies.Movie"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = BaseItemRepository.DeserializeBaseItem(entity, NullLogger.Instance, null, false);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user