Merge branch 'master' into network-rewrite

pull/8147/head
Shadowghost 1 year ago
commit 6cc1203c1b

@ -30,9 +30,9 @@ body:
label: Jellyfin Version
description: What version of Jellyfin are you running?
options:
- 10.8.0
- 10.8.z
- 10.8.9
- 10.7.7
- 10.7.z
- 10.6.4
- Other
validations:
@ -47,13 +47,15 @@ body:
label: Environment
description: |
Examples:
- **OS**: [e.g. Debian, Windows]
- **OS**: [e.g. Debian 11, Windows 10]
- **Linux Kernel**: [e.g. none, 5.15, 6.1, etc.]
- **Virtualization**: [e.g. Docker, KVM, LXC]
- **Clients**: [Browser, Android, Fire Stick, etc.]
- **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
- **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
- **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
- **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example]
@ -61,12 +63,14 @@ body:
- **Storage**: [e.g. local, NFS, cloud]
value: |
- OS:
- Linux Kernel:
- Virtualization:
- Clients:
- Browser:
- FFmpeg Version:
- Playback Method:
- Hardware Acceleration:
- GPU Model:
- Plugins:
- Reverse Proxy:
- Base URL:
@ -84,8 +88,8 @@ body:
id: ffmpeg-logs
attributes:
label: FFmpeg logs
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log.
placeholder: This field is mandatory for debugging hardware transcoding issues. It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
render: shell
- type: textarea
id: browserlogs

@ -19,6 +19,7 @@ jobs:
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
repoToken: ${{ secrets.JF_BOT_TOKEN }}
project:

@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2
uses: github/codeql-action/init@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2
uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@168b99b3c22180941ae7dbdd5f5c9678ede476ba # v2
uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3

@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@ -51,14 +51,14 @@ jobs:
reactions: eyes
- name: Checkout the latest code
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@ -93,7 +93,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@ -108,7 +108,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}

@ -14,18 +14,18 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.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@607fce577a46308457984d59e4954e075820f10a # tag=v3
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: '7.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@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: openapi-head
retention-days: 14
@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@24cb9080177205b6e8c946b17badbe402adc938f # v3
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -51,13 +51,13 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # v3.0.3
with:
dotnet-version: '7.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@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: openapi-base
retention-days: 14
@ -76,12 +76,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: openapi-base
path: openapi-base
@ -103,14 +103,14 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
uses: peter-evans/find-comment@034abe94d3191f9c89d870519735beae326f2bdb # v2
uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.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@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@ -125,7 +125,7 @@ jobs:
</details>
- name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
uses: peter-evans/create-or-update-comment@ca08ebd5dc95aa0cd97021e9708fcd6b87138c9b # v3.0.1
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}

@ -1,4 +1,4 @@
name: Issue Stale Check
name: Stale Check
on:
schedule:
@ -7,12 +7,15 @@ on:
permissions:
issues: write
pull-requests: write
jobs:
stale:
issues:
name: Check issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
@ -28,3 +31,21 @@ jobs:
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
prs-conflicts:
name: Check PRs with merge conflicts
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
operations-per-run: 75
# The merge conflict action will remove the label when updated
remove-stale-when-updated: false
days-before-stale: -1
days-before-close: 90
days-before-issue-close: -1
stale-pr-label: merge conflict
close-pr-message: |-
This PR has been closed due to having unresolved merge conflicts.

@ -14,23 +14,23 @@
<PackageVersion Include="BlurHashSharp" Version="1.2.0" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
<PackageVersion Include="Diacritics" Version="3.3.14" />
<PackageVersion Include="Diacritics" Version="3.3.18" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.6" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.1" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.11" />
<PackageVersion Include="LrcParser" Version="2023.308.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.5" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
@ -39,8 +39,8 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.4" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.5" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.5" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
@ -65,7 +65,7 @@
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="2.3.0" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.0.1" />
<PackageVersion Include="SharpFuzz" Version="2.0.2" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.3" />

@ -10,6 +10,7 @@ using System.Text;
using System.Xml;
using Emby.Dlna.ContentDirectory;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@ -870,11 +871,11 @@ namespace Emby.Dlna.Didl
var types = new[]
{
PersonType.Director,
PersonType.Writer,
PersonType.Producer,
PersonType.Composer,
"creator"
PersonKind.Director,
PersonKind.Writer,
PersonKind.Producer,
PersonKind.Composer,
PersonKind.Creator
};
// Seeing some LG models locking up due content with large lists of people
@ -888,10 +889,13 @@ namespace Emby.Dlna.Didl
foreach (var actor in people)
{
var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
?? PersonType.Actor;
var type = types.FirstOrDefault(i => i == actor.Type || string.Equals(actor.Role, i.ToString(), StringComparison.OrdinalIgnoreCase));
if (type == PersonKind.Unknown)
{
type = PersonKind.Actor;
}
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
AddValue(writer, "upnp", type.ToString().ToLowerInvariant(), actor.Name, NsUpnp);
}
}

@ -116,7 +116,7 @@ namespace Emby.Dlna.PlayTo
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "")
{
var stateString = string.Empty;
@ -137,10 +137,10 @@ namespace Emby.Dlna.PlayTo
}
}
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary)
{
var stateString = string.Empty;
@ -150,9 +150,9 @@ namespace Emby.Dlna.PlayTo
{
stateString += BuildArgumentXml(arg, "0");
}
else if (dictionary.ContainsKey(arg.Name))
else if (dictionary.TryGetValue(arg.Name, out var argValue))
{
stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
stateString += BuildArgumentXml(arg, argValue);
}
else
{
@ -160,7 +160,7 @@ namespace Emby.Dlna.PlayTo
}
}
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")

@ -141,8 +141,7 @@ namespace Emby.Naming.Common
VideoFileStackingRules = new[]
{
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
};
CleanDateTimes = new[]
@ -157,7 +156,8 @@ namespace Emby.Naming.Common
@"^(?<cleaned>.+?)(\[.*\])",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
@"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
};
SubtitleFileExtensions = new[]
@ -270,7 +270,6 @@ namespace Emby.Naming.Common
".sfx",
".shn",
".sid",
".spc",
".stm",
".strm",
".ult",

@ -87,8 +87,7 @@ namespace Emby.Naming.Video
name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType is null
&& TryCleanString(name, namingOptions, out var newName))
if (TryCleanString(name, namingOptions, out var newName))
{
name = newName;
}

@ -627,6 +627,9 @@ namespace Emby.Server.Implementations
}
}
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);
@ -634,9 +637,6 @@ namespace Emby.Server.Implementations
SetStaticProperties();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
FindParts();
}

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Jellyfin.Extensions;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@ -27,9 +26,19 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets or sets the path to the DB file.
/// </summary>
/// <value>Path to the DB file.</value>
protected string DbFilePath { get; set; }
/// <summary>
/// Gets or sets the number of write connections to create.
/// </summary>
/// <value>Path to the DB file.</value>
protected int WriteConnectionsCount { get; set; } = 1;
/// <summary>
/// Gets or sets the number of read connections to create.
/// </summary>
protected int ReadConnectionsCount { get; set; } = 1;
/// <summary>
/// Gets the logger.
/// </summary>
@ -63,7 +72,7 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
/// </summary>
protected virtual string LockingMode => "EXCLUSIVE";
protected virtual string LockingMode => "NORMAL";
/// <summary>
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
@ -88,7 +97,7 @@ namespace Emby.Server.Implementations.Data
/// </summary>
/// <value>The temp store mode.</value>
/// <see cref="TempStoreMode"/>
protected virtual TempStoreMode TempStore => TempStoreMode.Default;
protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
/// <summary>
/// Gets the synchronous mode.
@ -101,83 +110,114 @@ namespace Emby.Server.Implementations.Data
/// Gets or sets the write lock.
/// </summary>
/// <value>The write lock.</value>
protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1);
protected ConnectionPool WriteConnections { get; set; }
/// <summary>
/// Gets or sets the write connection.
/// </summary>
/// <value>The write connection.</value>
protected SQLiteDatabaseConnection WriteConnection { get; set; }
protected ConnectionPool ReadConnections { get; set; }
protected ManagedConnection GetConnection(bool readOnly = false)
public virtual void Initialize()
{
WriteLock.Wait();
if (WriteConnection is not null)
WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection);
ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection);
// Configuration and pragmas can affect VACUUM so it needs to be last.
using (var connection = GetConnection())
{
return new ManagedConnection(WriteConnection, WriteLock);
connection.Execute("VACUUM");
}
}
protected ManagedConnection GetConnection(bool readOnly = false)
=> readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
WriteConnection = SQLite3.Open(
protected SQLiteDatabaseConnection CreateWriteConnection()
{
var writeConnection = SQLite3.Open(
DbFilePath,
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
null);
if (CacheSize.HasValue)
{
WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(LockingMode))
{
WriteConnection.Execute("PRAGMA locking_mode=" + LockingMode);
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode);
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
WriteConnection.Execute("PRAGMA journal_size_limit=" + (int)JournalSizeLimit.Value);
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
WriteConnection.Execute("PRAGMA page_size=" + PageSize.Value);
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
}
WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
// Configuration and pragmas can affect VACUUM so it needs to be last.
WriteConnection.Execute("VACUUM");
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
return new ManagedConnection(WriteConnection, WriteLock);
return writeConnection;
}
public IStatement PrepareStatement(ManagedConnection connection, string sql)
=> connection.PrepareStatement(sql);
protected SQLiteDatabaseConnection CreateReadConnection()
{
var connection = SQLite3.Open(
DbFilePath,
DefaultConnectionFlags | ConnectionFlags.ReadOnly,
null);
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
=> connection.PrepareStatement(sql);
if (CacheSize.HasValue)
{
connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql)
{
int len = sql.Count;
IStatement[] statements = new IStatement[len];
for (int i = 0; i < len; i++)
if (!string.IsNullOrWhiteSpace(LockingMode))
{
connection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
connection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
statements[i] = connection.PrepareStatement(sql[i]);
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
return statements;
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return connection;
}
public IStatement PrepareStatement(ManagedConnection connection, string sql)
=> connection.PrepareStatement(sql);
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
=> connection.PrepareStatement(sql);
protected bool TableExists(ManagedConnection connection, string name)
{
return connection.RunInTransaction(
@ -252,22 +292,10 @@ namespace Emby.Server.Implementations.Data
if (dispose)
{
WriteLock.Wait();
try
{
WriteConnection?.Dispose();
}
finally
{
WriteLock.Release();
}
WriteLock.Dispose();
WriteConnections.Dispose();
ReadConnections.Dispose();
}
WriteConnection = null;
WriteLock = null;
_disposed = true;
}
}

@ -0,0 +1,79 @@
using System;
using System.Collections.Concurrent;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data;
/// <summary>
/// A pool of SQLite Database connections.
/// </summary>
public sealed class ConnectionPool : IDisposable
{
private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new();
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionPool" /> class.
/// </summary>
/// <param name="count">The number of database connection to create.</param>
/// <param name="factory">Factory function to create the database connections.</param>
public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory)
{
for (int i = 0; i < count; i++)
{
_connections.Add(factory.Invoke());
}
}
/// <summary>
/// Gets a database connection from the pool if one is available, otherwise blocks.
/// </summary>
/// <returns>A database connection.</returns>
public ManagedConnection GetConnection()
{
if (_disposed)
{
ThrowObjectDisposedException();
}
return new ManagedConnection(_connections.Take(), this);
static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(nameof(ConnectionPool));
}
}
/// <summary>
/// Return a database connection to the pool.
/// </summary>
/// <param name="connection">The database connection to return.</param>
public void Return(SQLiteDatabaseConnection connection)
{
if (_disposed)
{
connection.Dispose();
return;
}
_connections.Add(connection);
}
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
foreach (var connection in _connections)
{
connection.Dispose();
}
_connections.Dispose();
_disposed = true;
}
}

@ -2,23 +2,22 @@
using System;
using System.Collections.Generic;
using System.Threading;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
public sealed class ManagedConnection : IDisposable
{
private readonly SemaphoreSlim _writeLock;
private readonly ConnectionPool _pool;
private SQLiteDatabaseConnection? _db;
private SQLiteDatabaseConnection _db;
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
{
_db = db;
_writeLock = writeLock;
_pool = pool;
}
public IStatement PrepareStatement(string sql)
@ -73,9 +72,9 @@ namespace Emby.Server.Implementations.Data
return;
}
_writeLock.Release();
_pool.Return(_db);
_db = null; // Don't dispose it
_db = null!; // Don't dispose it
_disposed = true;
}
}

@ -336,6 +336,7 @@ namespace Emby.Server.Implementations.Data
_jsonOptions = JsonDefaults.Options;
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
ReadConnectionsCount = Environment.ProcessorCount * 2;
}
/// <inheritdoc />
@ -347,10 +348,10 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Opens the connection to the database.
/// </summary>
/// <param name="userDataRepo">The user data repository.</param>
/// <param name="userManager">The user manager.</param>
public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager)
public override void Initialize()
{
base.Initialize();
const string CreateMediaStreamsTableCommand
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, DvVersionMajor INT NULL, DvVersionMinor INT NULL, DvProfile INT NULL, DvLevel INT NULL, RpuPresentFlag INT NULL, ElPresentFlag INT NULL, BlPresentFlag INT NULL, DvBlSignalCompatibilityId INT NULL, IsHearingImpaired BIT NULL, PRIMARY KEY (ItemId, StreamIndex))";
@ -551,8 +552,6 @@ namespace Emby.Server.Implementations.Data
connection.RunQueries(postQueries);
}
userDataRepo.Initialize(userManager, WriteLock, WriteConnection);
}
public void SaveImages(BaseItem item)
@ -624,14 +623,8 @@ namespace Emby.Server.Implementations.Data
private void SaveItemsInTransaction(IDatabaseConnection db, IEnumerable<(BaseItem Item, List<Guid> AncestorIds, BaseItem TopParent, string UserDataKey, List<string> InheritedTags)> tuples)
{
var statements = PrepareAll(db, new string[]
{
SaveItemCommandText,
"delete from AncestorIds where ItemId=@ItemId"
});
using (var saveItemStatement = statements[0])
using (var deleteAncestorsStatement = statements[1])
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
{
var requiresReset = false;
foreach (var tuple in tuples)
@ -1286,15 +1279,13 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
{
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
{
statement.TryBind("@guid", id);
statement.TryBind("@guid", id);
foreach (var row in statement.ExecuteQuery())
{
return GetItem(row, new InternalItemsQuery());
}
foreach (var row in statement.ExecuteQuery())
{
return GetItem(row, new InternalItemsQuery());
}
}
@ -1309,7 +1300,8 @@ namespace Emby.Server.Implementations.Data
{
return false;
}
else if (type == typeof(UserRootFolder))
if (type == typeof(UserRootFolder))
{
return false;
}
@ -1319,55 +1311,68 @@ namespace Emby.Server.Implementations.Data
{
return false;
}
else if (type == typeof(MusicArtist))
if (type == typeof(MusicArtist))
{
return false;
}
else if (type == typeof(Person))
if (type == typeof(Person))
{
return false;
}
else if (type == typeof(MusicGenre))
if (type == typeof(MusicGenre))
{
return false;
}
else if (type == typeof(Genre))
if (type == typeof(Genre))
{
return false;
}
else if (type == typeof(Studio))
if (type == typeof(Studio))
{
return false;
}
else if (type == typeof(PlaylistsFolder))
if (type == typeof(PlaylistsFolder))
{
return false;
}
else if (type == typeof(PhotoAlbum))
if (type == typeof(PhotoAlbum))
{
return false;
}
else if (type == typeof(Year))
if (type == typeof(Year))
{
return false;
}
else if (type == typeof(Book))
if (type == typeof(Book))
{
return false;
}
else if (type == typeof(LiveTvProgram))
if (type == typeof(LiveTvProgram))
{
return false;
}
else if (type == typeof(AudioBook))
if (type == typeof(AudioBook))
{
return false;
}
else if (type == typeof(Audio))
if (type == typeof(Audio))
{
return false;
}
else if (type == typeof(MusicAlbum))
if (type == typeof(MusicAlbum))
{
return false;
}
@ -1958,22 +1963,19 @@ namespace Emby.Server.Implementations.Data
{
CheckDisposed();
var chapters = new List<ChapterInfo>();
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
{
var chapters = new List<ChapterInfo>();
statement.TryBind("@ItemId", item.Id);
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
foreach (var row in statement.ExecuteQuery())
{
statement.TryBind("@ItemId", item.Id);
foreach (var row in statement.ExecuteQuery())
{
chapters.Add(GetChapter(row, item));
}
chapters.Add(GetChapter(row, item));
}
return chapters;
}
return chapters;
}
/// <inheritdoc />
@ -1982,16 +1984,14 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
{
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
{
statement.TryBind("@ItemId", item.Id);
statement.TryBind("@ChapterIndex", index);
statement.TryBind("@ItemId", item.Id);
statement.TryBind("@ChapterIndex", index);
foreach (var row in statement.ExecuteQuery())
{
return GetChapter(row, item);
}
foreach (var row in statement.ExecuteQuery())
{
return GetChapter(row, item);
}
}
@ -2378,7 +2378,7 @@ namespace Emby.Server.Implementations.Data
else
{
builder.Append(
@"(SELECT CASE WHEN InheritedParentalRatingValue=0
@"(SELECT CASE WHEN COALESCE(InheritedParentalRatingValue, 0)=0
THEN 0
ELSE 10.0 / (1.0 + ABS(InheritedParentalRatingValue - @InheritedParentalRatingValue))
END)");
@ -2392,6 +2392,7 @@ namespace Emby.Server.Implementations.Data
// genres, tags, studios, person, year?
builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from ItemValues where ItemId=@SimilarItemId))");
builder.Append("+ (Select count(1) * 10 from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId))");
if (item is MusicArtist)
{
@ -2843,13 +2844,10 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
var itemQueryStatement = PrepareStatement(db, itemQuery);
var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery);
if (!isReturningZeroItems)
{
using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
using (var statement = itemQueryStatement)
using (var statement = PrepareStatement(db, itemQuery))
{
if (EnableJoinUserData(query))
{
@ -2884,7 +2882,7 @@ namespace Emby.Server.Implementations.Data
if (query.EnableTotalRecordCount)
{
using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
using (var statement = totalRecordCountQueryStatement)
using (var statement = PrepareStatement(db, totalRecordCountQuery))
{
if (EnableJoinUserData(query))
{
@ -4753,22 +4751,20 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
commandText.Append(" LIMIT ").Append(query.Limit);
}
var list = new List<string>();
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
var list = new List<string>();
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
GetPeopleWhereClauses(query, statement);
// Run this again to bind the params
GetPeopleWhereClauses(query, statement);
foreach (var row in statement.ExecuteQuery())
{
list.Add(row.GetString(0));
}
foreach (var row in statement.ExecuteQuery())
{
list.Add(row.GetString(0));
}
return list;
}
return list;
}
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
@ -4793,23 +4789,20 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
commandText += " LIMIT " + query.Limit;
}
var list = new List<PersonInfo>();
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
var list = new List<PersonInfo>();
// Run this again to bind the params
GetPeopleWhereClauses(query, statement);
using (var statement = PrepareStatement(connection, commandText))
foreach (var row in statement.ExecuteQuery())
{
// Run this again to bind the params
GetPeopleWhereClauses(query, statement);
foreach (var row in statement.ExecuteQuery())
{
list.Add(GetPerson(row));
}
list.Add(GetPerson(row));
}
return list;
}
return list;
}
private List<string> GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement)
@ -5540,7 +5533,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@Name" + index, person.Name);
statement.TryBind("@Role" + index, person.Role);
statement.TryBind("@PersonType" + index, person.Type);
statement.TryBind("@PersonType" + index, person.Type.ToString());
statement.TryBind("@SortOrder" + index, person.SortOrder);
statement.TryBind("@ListOrder" + index, listIndex);
@ -5569,9 +5562,10 @@ AND Type = @InternalPersonType)");
item.Role = role;
}
if (reader.TryGetString(3, out var type))
if (reader.TryGetString(3, out var type)
&& Enum.TryParse(type, true, out PersonKind personKind))
{
item.Type = type;
item.Type = personKind;
}
if (reader.TryGetInt32(4, out var sortOrder))

@ -7,7 +7,7 @@ using System.Collections.Generic;
using System.IO;
using System.Threading;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
@ -18,33 +18,32 @@ namespace Emby.Server.Implementations.Data
{
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
{
private readonly IUserManager _userManager;
public SqliteUserDataRepository(
ILogger<SqliteUserDataRepository> logger,
IApplicationPaths appPaths)
IServerConfigurationManager config,
IUserManager userManager)
: base(logger)
{
DbFilePath = Path.Combine(appPaths.DataPath, "library.db");
_userManager = userManager;
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
}
/// <summary>
/// Opens the connection to the database.
/// </summary>
/// <param name="userManager">The user manager.</param>
/// <param name="dbLock">The lock to use for database IO.</param>
/// <param name="dbConnection">The connection to use for database IO.</param>
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
public override void Initialize()
{
WriteLock.Dispose();
WriteLock = dbLock;
WriteConnection?.Dispose();
WriteConnection = dbConnection;
base.Initialize();
using (var connection = GetConnection())
{
var userDatasTableExists = TableExists(connection, "UserDatas");
var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : userManager.Users;
var users = userDatasTableExists ? null : _userManager.Users;
connection.RunInTransaction(
db =>
@ -371,20 +370,5 @@ namespace Emby.Server.Implementations.Data
return userData;
}
#pragma warning disable CA2215
/// <inheritdoc/>
/// <remarks>
/// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and
/// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>.
/// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>.
/// </remarks>
protected override void Dispose(bool dispose)
{
// The write lock and connection for the item repository are shared with the user data repository
// since they point to the same database. The item repo has responsibility for disposing these two objects,
// so the user data repo should not attempt to dispose them as well
}
#pragma warning restore CA2215
}
}

@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
@ -523,32 +522,32 @@ namespace Emby.Server.Implementations.Dto
var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
.ThenBy(i =>
{
if (i.IsType(PersonType.Actor))
if (i.IsType(PersonKind.Actor))
{
return 0;
}
if (i.IsType(PersonType.GuestStar))
if (i.IsType(PersonKind.GuestStar))
{
return 1;
}
if (i.IsType(PersonType.Director))
if (i.IsType(PersonKind.Director))
{
return 2;
}
if (i.IsType(PersonType.Writer))
if (i.IsType(PersonKind.Writer))
{
return 3;
}
if (i.IsType(PersonType.Producer))
if (i.IsType(PersonKind.Producer))
{
return 4;
}
if (i.IsType(PersonType.Composer))
if (i.IsType(PersonKind.Composer))
{
return 4;
}
@ -572,9 +571,7 @@ namespace Emby.Server.Implementations.Dto
return null;
}
}).Where(i => i is not null)
.Where(i => user is null ?
true :
i.IsVisible(user))
.Where(i => user is null || i.IsVisible(user))
.DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);

@ -1503,6 +1503,12 @@ namespace Emby.Server.Implementations.Library
});
query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
// Prevent searching in all libraries due to empty filter
if (query.TopParentIds.Length == 0)
{
query.TopParentIds = new[] { Guid.NewGuid() };
}
}
}
@ -1879,7 +1885,7 @@ namespace Emby.Server.Implementations.Library
catch (Exception ex)
{
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
size = new ImageDimensions(0, 0);
size = default;
image.Width = 0;
image.Height = 0;
}
@ -2743,9 +2749,7 @@ namespace Emby.Server.Implementations.Library
}
})
.Where(i => i is not null)
.Where(i => query.User is null ?
true :
i.IsVisible(query.User))
.Where(i => query.User is null || i.IsVisible(query.User))
.ToList();
}

@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Library
// If file is strm or main media stream is missing, force a metadata refresh with remote probing
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
&& (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
|| (item.MediaType == MediaType.Video && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Video))
|| (item.MediaType == MediaType.Audio && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio))))
|| (item.MediaType == MediaType.Video && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Video))
|| (item.MediaType == MediaType.Audio && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Audio))))
{
await item.RefreshMetadata(
new MetadataRefreshOptions(_directoryService)

@ -78,7 +78,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
Set3DFormat(videoTmp);
return videoTmp;
}
else if (IsBluRayDirectory(filename))
if (IsBluRayDirectory(filename))
{
var videoTmp = new TVideoType
{

@ -4,6 +4,7 @@ using System.IO;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <summary>
/// Resolves a Path into a Video or Video subclass.
/// </summary>
internal class ExtraResolver
internal class ExtraResolver : BaseVideoResolver<Video>
{
private readonly NamingOptions _namingOptions;
private readonly IItemResolver[] _trailerResolvers;
@ -28,10 +29,16 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
/// <param name="directoryService">The directory service.</param>
public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
: base(logger, namingOptions, directoryService)
{
_namingOptions = namingOptions;
_trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
_videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions, directoryService) };
_videoResolvers = new IItemResolver[] { this };
}
protected override Video Resolve(ItemResolveArgs args)
{
return ResolveVideo<Video>(args, true);
}
/// <summary>

@ -627,10 +627,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_timerProvider.Update(existingTimer);
return Task.FromResult(existingTimer.Id);
}
else
{
throw new ArgumentException("A scheduled recording already exists for this program.");
}
throw new ArgumentException("A scheduled recording already exists for this program.");
}
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
@ -1866,8 +1864,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
string id;
if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out id))
if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var id))
{
await writer.WriteElementStringAsync(null, "id", null, id).ConfigureAwait(false);
}
@ -2032,7 +2029,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
var directors = people
.Where(i => IsPersonType(i, PersonType.Director))
.Where(i => i.IsType(PersonKind.Director))
.Select(i => i.Name)
.ToList();
@ -2042,7 +2039,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
var writers = people
.Where(i => IsPersonType(i, PersonType.Writer))
.Where(i => i.IsType(PersonKind.Writer))
.Select(i => i.Name)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
@ -2122,10 +2119,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
}
private static bool IsPersonType(PersonInfo person, string type)
=> string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase)
|| string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
private LiveTvProgram GetProgramInfoFromCache(string programId)
{
var query = new InternalItemsQuery

@ -415,14 +415,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
return null;
}
else if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
{
return uri;
}
else
{
return apiUrl + "/image/" + uri + "?token=" + token;
}
return apiUrl + "/image/" + uri + "?token=" + token;
}
private static double GetAspectRatio(ImageDataDto i)

@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)
{
using var client = new TcpClient();
await client.ConnectAsync(remoteIP, HdHomeRunPort).ConfigureAwait(false);
await client.ConnectAsync(remoteIP, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
using var stream = client.GetStream();
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);

@ -170,9 +170,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
string numberString = null;
string attributeValue;
if (attributes.TryGetValue("tvg-chno", out attributeValue)
if (attributes.TryGetValue("tvg-chno", out var attributeValue)
&& double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
{
numberString = attributeValue;

@ -1,27 +1,27 @@
{
"DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
"DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
"Collections": "সংগ্রহ",
"Collections": "সংগ্রহশালা",
"ChapterNameValue": "অধ্যায় {0}",
"Channels": "চ্যানেল",
"Channels": "চ্যানেলসমূহ",
"CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
"Books": "বই",
"Books": "পুস্তকসমূহ",
"AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
"Artists": "শিল্পীরা",
"Artists": "শিল্পীগণ",
"Application": "অ্যাপ্লিকেশন",
"Albums": "অ্যালবামগুলো",
"Albums": "অ্যালবামসমূহ",
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
"HeaderAlbumArtists": "লবাম শিল্পীবৃন্দ",
"Genres": "শৈলী",
"Folders": "ফোল্ডারগুলো",
"HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
"Genres": "শৈলীধারাসমূহ",
"Folders": "ফোল্ডারসমূহ",
"Favorites": "পছন্দসমূহ",
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
"AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
"VersionNumber": "সংস্করণ {0}",
"ValueSpecialEpisodeName": "বিশেষ - {0}",
"ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
"ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
"UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
"UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
@ -36,10 +36,10 @@
"User": "ব্যবহারকারী",
"TvShows": "টিভি শোগুলো",
"System": "সিস্টেম",
"Sync": "সিংক",
"Sync": "সমলয় স্থাপন",
"SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
"StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
"Songs": "গানগুলো",
"Songs": "সঙ্গীতসমূহ",
"Shows": "টিভি পর্ব",
"ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন",
"ScheduledTaskStartedWithName": "{0} শুরু হয়েছে",
@ -49,8 +49,8 @@
"PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
"PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
"Plugin": "প্লাগিন",
"Playlists": "প্লেলিস্ট",
"Photos": "ছবিগুলো",
"Playlists": "প্লে লিস্ট সমূহ",
"Photos": "চিত্রসমূহ",
"NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
"NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
"NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
@ -71,9 +71,9 @@
"NameSeasonUnknown": "সিজন অজানা",
"NameSeasonNumber": "সিজন {0}",
"NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
"MusicVideos": "গানের ভিডিও",
"MusicVideos": "সঙ্গীত ভিডিয়ো সমূহ",
"Music": "গান",
"Movies": "চলচ্চিত্র",
"Movies": "চলচ্চিত্রসমূহ",
"MixedContent": "মিশ্র কন্টেন্ট",
"MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
"HeaderRecordingGroups": "রেকর্ডিং দল",
@ -117,5 +117,11 @@
"Forced": "জোরকরে",
"TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
"TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
"Default": "প্রাথমিক"
"Default": "প্রাথমিক",
"HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
"TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
"External": "বাহ্যিক",
"TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
"TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
"TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।"
}

@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
"Books": "Llibres",
"CameraImageUploadedFrom": "S'ha pujat una nova imatge des de la camera desde {0}",
"CameraImageUploadedFrom": "S'ha pujat una nova imatge de càmera des de {0}",
"Channels": "Canals",
"ChapterNameValue": "Capítol {0}",
"Collections": "Col·leccions",
@ -16,65 +16,65 @@
"Folders": "Carpetes",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes de l'àlbum",
"HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits",
"HeaderFavoriteArtists": "Artistes Predilectes",
"HeaderFavoriteEpisodes": "Episodis Predilectes",
"HeaderFavoriteShows": "Sèries Predilectes",
"HeaderFavoriteSongs": "Cançons Predilectes",
"HeaderLiveTV": "TV en Directe",
"HeaderContinueWatching": "Continuar veient",
"HeaderFavoriteAlbums": "Àlbums preferits",
"HeaderFavoriteArtists": "Artistes preferits",
"HeaderFavoriteEpisodes": "Episodis preferits",
"HeaderFavoriteShows": "Sèries preferides",
"HeaderFavoriteSongs": "Cançons preferides",
"HeaderLiveTV": "TV en directe",
"HeaderNextUp": "A continuació",
"HeaderRecordingGroups": "Grups d'Enregistrament",
"HomeVideos": "Vídeos Domèstics",
"HeaderRecordingGroups": "Grups d'enregistrament",
"HomeVideos": "Vídeos domèstics",
"Inherit": "Hereta",
"ItemAddedWithName": "{0} ha estat afegit a la biblioteca",
"ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca",
"ItemAddedWithName": "{0} ha sigut afegit a la biblioteca",
"ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca",
"LabelIpAddressValue": "Adreça IP: {0}",
"LabelRunningTimeValue": "Temps en funcionament: {0}",
"Latest": "Darreres",
"MessageApplicationUpdated": "El Servidor de Jellyfin ha estat actualitzat",
"MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}",
"Latest": "Darrers",
"MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
"MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
"MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
"MixedContent": "Contingut barrejat",
"Movies": "Pel·lícules",
"Music": "Música",
"MusicVideos": "Vídeos Musicals",
"MusicVideos": "Videoclips",
"NameInstallFailed": "{0} instal·lació fallida",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconeguda",
"NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.",
"NotificationOptionApplicationUpdateAvailable": "Actualització d'aplicació disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualització d'aplicació instal·lada",
"NameSeasonUnknown": "Temporada desconeguda",
"NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
"NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada",
"NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
"NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
"NotificationOptionInstallationFailed": "Instal·lació fallida",
"NotificationOptionNewLibraryContent": "Nou contingut afegit",
"NotificationOptionPluginError": "Un connector ha fallat",
"NotificationOptionPluginInstalled": "Connector instal·lat",
"NotificationOptionPluginUninstalled": "Connector desinstal·lat",
"NotificationOptionPluginUpdateInstalled": "Actualització de connector instal·lada",
"NotificationOptionPluginError": "Un complement ha fallat",
"NotificationOptionPluginInstalled": "Complement instal·lat",
"NotificationOptionPluginUninstalled": "Complement desinstal·lat",
"NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada",
"NotificationOptionServerRestartRequired": "Reinici del servidor requerit",
"NotificationOptionTaskFailed": "Tasca programada fallida",
"NotificationOptionUserLockedOut": "Usuari tancat",
"NotificationOptionVideoPlayback": "Reproducció de video iniciada",
"NotificationOptionVideoPlaybackStopped": "Reproducció de video aturada",
"NotificationOptionUserLockedOut": "Usuari expulsat",
"NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
"NotificationOptionVideoPlaybackStopped": "Reproducció de vídeo aturada",
"Photos": "Fotos",
"Playlists": "Llistes de reproducció",
"Plugin": "Connector",
"Plugin": "Complement",
"PluginInstalledWithName": "{0} ha estat instal·lat",
"PluginUninstalledWithName": "{0} ha estat desinstal·lat",
"PluginUpdatedWithName": "{0} ha estat actualitzat",
"ProviderValue": "Proveïdor: {0}",
"ScheduledTaskFailedWithName": "{0} ha fallat",
"ScheduledTaskStartedWithName": "{0} iniciat",
"ScheduledTaskStartedWithName": "{0} s'ha iniciat",
"ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
"Shows": "Sèries",
"Songs": "Cançons",
"StartupEmbyServerIsLoading": "El Servidor de Jellyfin està carregant. Si et plau, prova de nou ben aviat.",
"StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho altre cop aviat.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}",
"SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
"Sync": "Sincronitzar",
"System": "Sistema",
"TvShows": "Sèries de TV",
@ -82,11 +82,11 @@
"UserCreatedWithName": "S'ha creat l'usuari {0}",
"UserDeletedWithName": "L'usuari {0} ha estat eliminat",
"UserDownloadingItemWithValues": "{0} està descarregant {1}",
"UserLockedOutWithName": "L'usuari {0} ha sigut tancat",
"UserLockedOutWithName": "L'usuari {0} ha sigut expulsat",
"UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
"UserOnlineFromDevice": "{0} està connectat des de {1}",
"UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}",
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per {0}",
"UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
"UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}",
"UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
"ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
@ -94,14 +94,14 @@
"VersionNumber": "Versió {0}",
"TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
"TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'Internet.",
"TaskRefreshChannels": "Actualitza Canals",
"TaskCleanTranscodeDescription": "Elimina els arxius temporals de transcodificacions que tinguin més d'un dia.",
"TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
"TaskRefreshChannels": "Actualitza els canals",
"TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
"TaskCleanTranscode": "Neteja les transcodificacions",
"TaskUpdatePluginsDescription": "Actualitza les extensions que estan configurades per actualitzar-se automàticament.",
"TaskUpdatePlugins": "Actualitza les extensions",
"TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.",
"TaskUpdatePlugins": "Actualitza els connectors",
"TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.",
"TaskRefreshPeople": "Actualitza Persones",
"TaskRefreshPeople": "Actualitza les persones",
"TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
"TaskCleanLogs": "Neteja els registres",
"TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
@ -110,12 +110,12 @@
"TaskRefreshChapterImages": "Extreure les imatges dels capítols",
"TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
"TaskCleanCache": "Elimina arxius temporals",
"TasksChannelsCategory": "Canals d'Internet",
"TasksChannelsCategory": "Canals d'internet",
"TasksApplicationCategory": "Aplicació",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manteniment",
"TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
"TaskCleanActivityLog": "Buidar Registre d'Activitat",
"TaskCleanActivityLog": "Buidar el registre d'activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
"Default": "Per defecte",
@ -124,5 +124,5 @@
"TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
"TaskKeyframeExtractor": "Extractor de fotogrames clau",
"External": "Extern",
"HearingImpaired": "Discapacitat Auditiva"
"HearingImpaired": "Discapacitat auditiva"
}

@ -28,7 +28,7 @@
"NameSeasonNumber": "Tymor {0}",
"MusicVideos": "Fideos Cerddoriaeth",
"MixedContent": "Cynnwys amrywiol",
"HomeVideos": "Fideos Cartref",
"HomeVideos": "Genres",
"HeaderNextUp": "Nesaf i Fyny",
"HeaderFavoriteArtists": "Ffefryn Artistiaid",
"HeaderFavoriteAlbums": "Ffefryn Albwmau",
@ -122,5 +122,6 @@
"TaskRefreshChapterImagesDescription": "Creu mân-luniau ar gyfer fideos sydd â phenodau.",
"TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
"TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
"TaskCleanCache": "Gwaghau Ffolder Cache"
"TaskCleanCache": "Gwaghau Ffolder Cache",
"HearingImpaired": "Nam ar y clyw"
}

@ -1,9 +1,9 @@
{
"Albums": "Albummer",
"Albums": "Album",
"AppDeviceValues": "App: {0}, Enhed: {1}",
"Application": "Applikation",
"Artists": "Kunstnere",
"AuthenticationSucceededWithUserName": "{0} succesfuldt autentificeret",
"AuthenticationSucceededWithUserName": "{0} er logget ind",
"Books": "Bøger",
"CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}",
"Channels": "Kanaler",
@ -11,17 +11,17 @@
"Collections": "Samlinger",
"DeviceOfflineWithName": "{0} har afbrudt forbindelsen",
"DeviceOnlineWithName": "{0} er forbundet",
"FailedLoginAttemptWithUserName": "Fejlet loginforsøg fra {0}",
"FailedLoginAttemptWithUserName": "Mislykket loginforsøg fra {0}",
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumkunstner",
"HeaderAlbumArtists": "Albums kunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
"HeaderFavoriteAlbums": "Favoritalbummer",
"HeaderFavoriteArtists": "Favoritkunstnere",
"HeaderFavoriteEpisodes": "Favoritepisoder",
"HeaderFavoriteShows": "Favoritserier",
"HeaderFavoriteSongs": "Favoritsange",
"HeaderFavoriteAlbums": "Favorit albummer",
"HeaderFavoriteArtists": "Favorit kunstnere",
"HeaderFavoriteEpisodes": "Favorit afsnit",
"HeaderFavoriteShows": "Favorit serier",
"HeaderFavoriteSongs": "Favorit sange",
"HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper",
@ -39,90 +39,90 @@
"MixedContent": "Blandet indhold",
"Movies": "Film",
"Music": "Musik",
"MusicVideos": "Musik videoer",
"MusicVideos": "Musikvideoer",
"NameInstallFailed": "{0} installationen mislykkedes",
"NameSeasonNumber": "Sæson {0}",
"NameSeasonUnknown": "Ukendt sæson",
"NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig til download.",
"NotificationOptionApplicationUpdateAvailable": "Opdatering til applikation tilgængelig",
"NotificationOptionApplicationUpdateInstalled": "Opdatering til applikation installeret",
"NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig.",
"NotificationOptionApplicationUpdateAvailable": "Opdatering til applikationen er tilgængelig",
"NotificationOptionApplicationUpdateInstalled": "Opdatering til applikationen blev installeret",
"NotificationOptionAudioPlayback": "Lydafspilning påbegyndt",
"NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet",
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
"NotificationOptionInstallationFailed": "Installationen fejlede",
"NotificationOptionInstallationFailed": "Installationen mislykkedes",
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
"NotificationOptionPluginError": "Pluginfejl",
"NotificationOptionPluginInstalled": "Plugin installeret",
"NotificationOptionPluginUninstalled": "Plugin afinstalleret",
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin installeret",
"NotificationOptionServerRestartRequired": "Genstart af server påkrævet",
"NotificationOptionTaskFailed": "Planlagt opgave fejlet",
"NotificationOptionUserLockedOut": "Bruger låst ude",
"NotificationOptionPluginError": "Plugin fejl",
"NotificationOptionPluginInstalled": "Plugin blev installeret",
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
"NotificationOptionServerRestartRequired": "Genstart af serveren er påkrævet",
"NotificationOptionTaskFailed": "Planlagt opgave er fejlet",
"NotificationOptionUserLockedOut": "Bruger er låst ude",
"NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
"NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet",
"Photos": "Fotoer",
"NotificationOptionVideoPlaybackStopped": "Videoafspilning blev stoppet",
"Photos": "Fotos",
"Playlists": "Afspilningslister",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} blev installeret",
"PluginUninstalledWithName": "{0} blev afinstalleret",
"PluginUpdatedWithName": "{0} blev opdateret",
"ProviderValue": "Udbyder: {0}",
"ScheduledTaskFailedWithName": "{0} fejlet",
"ScheduledTaskStartedWithName": "{0} påbegyndt",
"ScheduledTaskFailedWithName": "{0} mislykkedes",
"ScheduledTaskStartedWithName": "{0} påbegyndte",
"ServerNameNeedsToBeRestarted": "{0} skal genstartes",
"Shows": "Serier",
"Songs": "Sange",
"StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte op. Prøv venligst igen om lidt.",
"StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte. Forsøg igen om et øjeblik.",
"SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}",
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke downloades fra {0} til {1}",
"Sync": "Synk",
"SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
"Sync": "Synkroniser",
"System": "System",
"TvShows": "Tv-serier",
"TvShows": "TV-serier",
"User": "Bruger",
"UserCreatedWithName": "Bruger {0} er blevet oprettet",
"UserDeletedWithName": "Brugeren {0} er blevet slettet",
"UserDownloadingItemWithValues": "{0} downloader {1}",
"UserDeletedWithName": "Brugeren {0} er nu slettet",
"UserDownloadingItemWithValues": "{0} henter {1}",
"UserLockedOutWithName": "Brugeren {0} er blevet låst ude",
"UserOfflineFromDevice": "{0} har afbrudt fra {1}",
"UserOnlineFromDevice": "{0} er online fra {1}",
"UserPasswordChangedWithName": "Adgangskode er ændret for bruger {0}",
"UserPolicyUpdatedWithName": "Brugerpolitik er blevet opdateret for {0}",
"UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
"UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
"UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
"UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfiguration.",
"TaskDownloadMissingSubtitles": "Download manglende undertekster",
"TaskUpdatePluginsDescription": "Downloader og installere opdateringer for plugins som er konfigureret til at opdatere automatisk.",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins",
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gammle.",
"TaskCleanLogs": "Ryd Log Mappe",
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdaterer metadata.",
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
"TaskCleanLogs": "Ryd Log mappe",
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
"TaskRefreshLibrary": "Scan Medie Bibliotek",
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke har brug for længere.",
"TaskCleanCache": "Ryd Cache Mappe",
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
"TaskCleanCache": "Ryd Cache mappe",
"TasksChannelsCategory": "Internet Kanaler",
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedligeholdelse",
"TaskRefreshChapterImages": "Udtræk Kapitel billeder",
"TaskRefreshChapterImages": "Udtræk kapitel billeder",
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
"TaskRefreshChannelsDescription": "Genopfrisker internet kanal information.",
"TaskRefreshChannels": "Genopfrisk Kanaler",
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.",
"TaskCleanTranscode": "Rengør Transcode Mappen",
"TaskRefreshPeople": "Genopfrisk Personer",
"TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigureret alder.",
"TaskRefreshChannelsDescription": "Opdater internet kanal information.",
"TaskRefreshChannels": "Opdater Kanaler",
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
"TaskCleanTranscode": "Tøm Transcode mappen",
"TaskRefreshPeople": "Opdater Personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
"TaskCleanActivityLog": "Ryd Aktivitetslog",
"Undefined": "Udefineret",
"Forced": "Tvunget",
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Kompakter database og forkorter fri plads. Ved at køre denne proces efter at scanne biblioteket eller efter at ændre noget som kunne have indflydelse på databasen, kan forbedre ydeevne.",
"TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
"TaskOptimizeDatabase": "Optimér database",
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan godt tage lang tid.",
"TaskKeyframeExtractor": "Billedramme udtrækker",
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
"TaskKeyframeExtractor": "Nøglebillede udtræk",
"External": "Ekstern",
"HearingImpaired": "Hørehæmmet"
}

@ -31,7 +31,7 @@
"ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
"LabelIpAddressValue": "Dirección IP: {0}",
"LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
"Latest": "Último contenido en",
"Latest": "Últimas",
"MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
"MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada",

@ -118,7 +118,7 @@
"TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.",
"TaskCleanActivityLog": "Tyhjennä toimintahistoria",
"Undefined": "Määrittelemätön",
"TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastojen skannauksen tai muiden tietokantaan liittyvien muutoksien jälkeen voi parantaa suorituskykyä.",
"TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastopäivityksen tai muiden mahdollisten tietokantamuutosten jälkeen voi parantaa suorituskykyä.",
"TaskOptimizeDatabase": "Optimoi tietokanta",
"TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
"TaskKeyframeExtractor": "Avainkuvien purkain",

@ -119,5 +119,9 @@
"Undefined": "Hindi tiyak",
"Forced": "Sapilitan",
"TaskOptimizeDatabaseDescription": "Iko-compact ang database at ita-truncate ang free space. Ang pagpapatakbo ng gawaing ito pagkatapos ng pag-scan sa library o paggawa ng iba pang mga pagbabago na nagpapahiwatig ng mga pagbabago sa database ay maaaring magpa-improve ng performance.",
"TaskOptimizeDatabase": "I-optimize ang database"
"TaskOptimizeDatabase": "I-optimize ang database",
"HearingImpaired": "Bingi",
"TaskKeyframeExtractor": "Tagabunot ng Keyframe",
"TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.",
"External": "External"
}

@ -73,5 +73,36 @@
"Songs": "गाने",
"UserStartedPlayingItemWithValues": "{0} {2} पर {1} खेल रहे हैं",
"UserStoppedPlayingItemWithValues": "{0} ने {2} पर {1} खेलना खत्म किया",
"StartupEmbyServerIsLoading": "जेलीफ़िन सर्वर लोड हो रहा है। कृपया शीघ्र ही पुन: प्रयास करें।"
"StartupEmbyServerIsLoading": "जेलीफ़िन सर्वर लोड हो रहा है। कृपया शीघ्र ही पुन: प्रयास करें।",
"ServerNameNeedsToBeRestarted": "{0} रीस्टार्ट करने की आवश्यकता है",
"UserCreatedWithName": "उपयोगकर्ता {0} बनाया गया",
"UserDownloadingItemWithValues": "{0} डाउनलोड हो रहा है",
"UserOfflineFromDevice": "{0} {1} से डिस्कनेक्ट हो गया है",
"Undefined": "अनिर्धारित",
"UserOnlineFromDevice": "{0} {1} से ऑनलाइन है",
"Shows": "शो",
"UserPasswordChangedWithName": "उपयोगकर्ता {0} के लिए पासवर्ड बदल दिया गया है",
"UserDeletedWithName": "उपयोगकर्ता {0} हटा दिया गया",
"UserPolicyUpdatedWithName": "{0} के लिए उपयोगकर्ता नीति अपडेट कर दी गई है",
"User": "उपयोगकर्ता",
"SubtitleDownloadFailureFromForItem": "{1} के लिए {0} से उपशीर्षक डाउनलोड करने में विफल",
"ProviderValue": "प्रदाता: {0}",
"ScheduledTaskFailedWithName": "{0}असफल",
"UserLockedOutWithName": "उपयोगकर्ता {0} को लॉक आउट कर दिया गया है",
"System": "प्रणाली",
"TvShows": "टीवी शो",
"HearingImpaired": "मूक बधिर",
"ValueSpecialEpisodeName": "विशेष - {0}",
"TasksMaintenanceCategory": "रखरखाव",
"Sync": "समाकलयति",
"VersionNumber": "{0} पाठान्तर",
"ValueHasBeenAddedToLibrary": "{0} आपके माध्यम ग्रन्थालय में उपजात हो गया हैं",
"TasksLibraryCategory": "संग्रहालय",
"TaskOptimizeDatabase": "जानकारी प्रवृद्धि",
"TaskDownloadMissingSubtitles": "असमेत अनुलेख को अवाहरति करें",
"TaskRefreshLibrary": "माध्यम संग्राहत को छाने",
"TaskCleanActivityLog": "क्रियाकलाप लॉग साफ करें",
"TasksChannelsCategory": "इंटरनेट प्रणाली",
"TasksApplicationCategory": "अनुप्रयोग",
"TaskRefreshPeople": "लोगोकी जानकारी ताज़ी करें"
}

@ -37,8 +37,8 @@
"MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました",
"MessageServerConfigurationUpdated": "サーバー設定が更新されました",
"MixedContent": "ミックスコンテンツ",
"Movies": "ムービー",
"Music": "ミュージック",
"Movies": "映画",
"Music": "音楽",
"MusicVideos": "ミュージックビデオ",
"NameInstallFailed": "{0}のインストールに失敗しました",
"NameSeasonNumber": "シーズン {0}",

@ -120,5 +120,6 @@
"Default": "Noklusējuma",
"TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
"TaskOptimizeDatabase": "Optimizēt datubāzi",
"External": "Ārējais"
"External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem"
}

@ -0,0 +1,6 @@
{
"Albums": "辑册",
"Artists": "艺人",
"AuthenticationSucceededWithUserName": "{0} 授之权矣",
"Books": "册"
}

@ -122,5 +122,6 @@
"External": "बाहेरचा",
"DeviceOnlineWithName": "{0} कनेक्ट झाले",
"DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे",
"AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत"
"AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत",
"HearingImpaired": "कर्णबधीर"
}

@ -39,7 +39,7 @@
"MixedContent": "Kandungan campuran",
"Movies": "Filem-filem",
"Music": "Muzik",
"MusicVideos": "Video muzik",
"MusicVideos": "Video Muzik",
"NameInstallFailed": "{0} pemasangan gagal",
"NameSeasonNumber": "Musim {0}",
"NameSeasonUnknown": "Musim Tidak Diketahui",
@ -55,7 +55,7 @@
"NotificationOptionPluginInstalled": "Plugin telah dipasang",
"NotificationOptionPluginUninstalled": "Plugin telah dinyahpasang",
"NotificationOptionPluginUpdateInstalled": "Kemaskini plugin telah dipasang",
"NotificationOptionServerRestartRequired": "",
"NotificationOptionServerRestartRequired": "Perlu mulakan semula server",
"NotificationOptionTaskFailed": "Kegagalan tugas berjadual",
"NotificationOptionUserLockedOut": "Pengguna telah dikunci",
"NotificationOptionVideoPlayback": "Ulangmain video bermula",
@ -109,5 +109,20 @@
"TaskRefreshLibrary": "Imbas Perpustakaan Media",
"TaskRefreshChapterImagesDescription": "Membuat gambaran kecil untuk video yang mempunyai bab.",
"TaskRefreshChapterImages": "Ekstrak Gambar-gambar Bab",
"TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem."
"TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem.",
"HearingImpaired": "Lemah Pendengaran",
"TaskRefreshPeopleDescription": "Kemas kini metadata untuk pelakon dan pengarah di dalam perpustakaan media.",
"TaskUpdatePluginsDescription": "Muat turun dan kemas kini plugin yang dikonfigurasi secara automatik.",
"TaskDownloadMissingSubtitlesDescription": "Cari sari kata yang hilang di internet, berdasarkan konfigurasi metadata.",
"TaskOptimizeDatabaseDescription": "Mampatkan pangkalan data dan potong ruang kosong. Pelaksanaan tugas ini selepas pengimbasan perpustakaan boleh membantu membaiki prestasi.",
"TaskRefreshChannels": "Segarkan Saluran-saluran",
"TaskUpdatePlugins": "Kemas kini plugin",
"TaskDownloadMissingSubtitles": "Muat turn sari kata yang tiada",
"TaskCleanTranscodeDescription": "Padam fail transkod yang lebih lama dari satu hari.",
"TaskRefreshChannelsDescription": "Segarkan maklumat saluran internet.",
"TaskCleanTranscode": "Bersihkan direktori transkod",
"External": "Luaran",
"TaskOptimizeDatabase": "Optimumkan pangkalan data",
"TaskKeyframeExtractor": "Ekstrak bingkai kunci",
"TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang."
}

@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
"Channels": "Kanalen",
"ChapterNameValue": "Hoofdstuk {0}",
"Collections": "Verzamelingen",
"Collections": "Collecties",
"DeviceOfflineWithName": "Verbinding met {0} is verbroken",
"DeviceOnlineWithName": "{0} is verbonden",
"FailedLoginAttemptWithUserName": "Mislukte inlogpoging van {0}",
@ -114,7 +114,7 @@
"TasksApplicationCategory": "Toepassing",
"TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud",
"TaskCleanActivityLogDescription": "Verwijdert activiteiten logs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLogDescription": "Verwijdert activiteitenlogs ouder dan de ingestelde leeftijd.",
"TaskCleanActivityLog": "Activiteitenlogboek legen",
"Undefined": "Niet gedefinieerd",
"Forced": "Geforceerd",

@ -0,0 +1,4 @@
{
"External": "ବହିଃସ୍ଥ",
"Genres": "ଧରଣ"
}

@ -28,22 +28,22 @@
"ValueHasBeenAddedToLibrary": "{0} ਤੁਹਾਡੀ ਮੀਡੀਆ ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ",
"UserStoppedPlayingItemWithValues": "{0} ਨੇ {2} 'ਤੇ {1} ਖੇਡਣਾ ਪੂਰਾ ਕਰ ਲਿਆ ਹੈ",
"UserStartedPlayingItemWithValues": "{0} {2} 'ਤੇ {1} ਖੇਡ ਰਿਹਾ ਹੈ",
"UserPolicyUpdatedWithName": "ਉਪਭੋਗਤਾ ਨੀਤੀ ਨੂੰ {0} ਲਈ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
"UserPasswordChangedWithName": "ਪਾਸਵਰਡ ਯੂਜ਼ਰ ਲਈ ਬਦਲਿਆ ਗਿਆ ਹੈ {0}",
"UserOnlineFromDevice": "{0} ਤੋਂ isਨਲਾਈਨ ਹੈ {1}",
"UserPolicyUpdatedWithName": "ਵਰਤੋਂਕਾਰ ਨੀਤੀ ਨੂੰ {0} ਲਈ ਅਪਡੇਟ ਕੀਤਾ ਗਿਆ ਹੈ",
"UserPasswordChangedWithName": "{0} ਵਰਤੋਂਕਾਰ ਲਈ ਪਾਸਵਰਡ ਬਦਲਿਆ ਗਿਆ ਸੀ",
"UserOnlineFromDevice": "{0} ਨੂੰ {1} ਤੋਂ ਆਨਲਾਈਨ ਹੈ",
"UserOfflineFromDevice": "{0} ਤੋਂ ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ {1}",
"UserLockedOutWithName": "ਯੂਜ਼ਰ {0} ਨੂੰ ਲਾਕ ਆਉਟ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ",
"UserDownloadingItemWithValues": "{0} ਡਾ{ਨਲੋਡ ਕਰ ਰਿਹਾ ਹੈ {1}",
"UserDeletedWithName": "ਯੂਜ਼ਰ {0} ਨੂੰ ਮਿਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ",
"UserCreatedWithName": "ਯੂਜ਼ਰ {0} ਬਣਾਇਆ ਗਿਆ ਹੈ",
"User": "ਯੂਜ਼ਰ",
"UserLockedOutWithName": "ਵਰਤੋਂਕਾਰ {0} ਨੂੰ ਲਾਕ ਕੀਤਾ ਗਿਆ ਹੈ",
"UserDownloadingItemWithValues": "{0} {1} ਨੂੰ ਡਾਊਨਲੋਡ ਕਰ ਰਿਹਾ ਹੈ",
"UserDeletedWithName": "ਵਰਤੋਂਕਾਰ {0} ਨੂੰ ਹਟਾਇਆ ਗਿਆ",
"UserCreatedWithName": "ਵਰਤੋਂਕਾਰ {0} ਬਣਾਇਆ ਗਿਆ ਹੈ",
"User": "ਵਰਤੋਂਕਾਰ",
"Undefined": "ਪਰਿਭਾਸ਼ਤ",
"TvShows": "ਟੀਵੀ ਸ਼ੋਅਜ਼",
"TvShows": "ਟੀਵੀ ਸ਼ੋਅ",
"System": "ਸਿਸਟਮ",
"Sync": "ਸਿੰਕ",
"SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾ toਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ",
"StartupEmbyServerIsLoading": "ਜੈਲੀਫਿਨ ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ. ਕਿਰਪਾ ਕਰਕੇ ਜਲਦੀ ਹੀ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ.",
"Songs": "ਗਾਣੇ",
"SubtitleDownloadFailureFromForItem": "ਉਪਸਿਰਲੇਖ {1} ਲਈ {0} ਤੋਂ ਡਾਨਲੋਡ ਕਰਨ ਵਿੱਚ ਅਸਫਲ ਰਹੇ",
"StartupEmbyServerIsLoading": "Jellyfin ਸਰਵਰ ਲੋਡ ਹੋ ਰਿਹਾ ਹੈ। ਛੇਤੀ ਹੀ ਫ਼ੇਰ ਕੋਸ਼ਿਸ਼ ਕਰੋ।",
"Songs": "ਗਾਣੇ",
"Shows": "ਸ਼ੋਅ",
"ServerNameNeedsToBeRestarted": "{0} ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ",
"ScheduledTaskStartedWithName": "{0} ਸ਼ੁਰੂ ਹੋਇਆ",
@ -57,12 +57,12 @@
"Photos": "ਫੋਟੋਆਂ",
"NotificationOptionVideoPlaybackStopped": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਰੋਕਿਆ ਗਿਆ",
"NotificationOptionVideoPlayback": "ਵੀਡੀਓ ਪਲੇਬੈਕ ਸ਼ੁਰੂ ਹੋਇਆ",
"NotificationOptionUserLockedOut": "ਉਪਭੋਗਤਾ ਨੂੰ ਲਾਕ ਆਉਟ ਕੀਤਾ ਗਿਆ",
"NotificationOptionUserLockedOut": "ਵਰਤੋਂਕਾਰ ਨੂੰ ਲਾਕ ਕੀਤਾ",
"NotificationOptionTaskFailed": "ਨਿਰਧਾਰਤ ਕਾਰਜ ਅਸਫਲਤਾ",
"NotificationOptionServerRestartRequired": "ਸਰਵਰ ਨੂੰ ਮੁੜ ਚਾਲੂ ਕਰਨ ਦੀ ਲੋੜ ਹੈ",
"NotificationOptionPluginUpdateInstalled": "ਪਲੱਗਇਨ ਅਪਡੇਟ ਇੰਸਟੌਲ ਕੀਤਾ ਗਿਆ",
"NotificationOptionPluginUninstalled": "ਪਲੱਗਇਨ ਅਣਇੰਸਟੌਲ ਕੀਤਾ",
"NotificationOptionPluginInstalled": "ਪਲੱਗਇਨ ਸਥਾਪਿਤ ਕੀਤਾ",
"NotificationOptionPluginInstalled": "ਪਲੱਗਇਨ ਇੰਸਟਾਲ ਕੀਤੀ",
"NotificationOptionPluginError": "ਪਲੱਗਇਨ ਅਸਫਲ",
"NotificationOptionNewLibraryContent": "ਨਵੀਂ ਸਮੱਗਰੀ ਸ਼ਾਮਲ ਕੀਤੀ ਗਈ",
"NotificationOptionInstallationFailed": "ਇੰਸਟਾਲੇਸ਼ਨ ਅਸਫਲ",
@ -92,7 +92,7 @@
"HomeVideos": "ਘਰੇਲੂ ਵੀਡੀਓ",
"HeaderRecordingGroups": "ਰਿਕਾਰਡਿੰਗ ਸਮੂਹ",
"HeaderNextUp": "ਅੱਗੇ",
"HeaderLiveTV": "ਲਾਈਵ ਟੀ",
"HeaderLiveTV": "ਲਾਈਵ ਟੀਵੀ",
"HeaderFavoriteSongs": "ਮਨਪਸੰਦ ਗਾਣੇ",
"HeaderFavoriteShows": "ਮਨਪਸੰਦ ਸ਼ੋਅ",
"HeaderFavoriteEpisodes": "ਮਨਪਸੰਦ ਐਪੀਸੋਡ",
@ -102,20 +102,22 @@
"HeaderAlbumArtists": "ਐਲਬਮ ਕਲਾਕਾਰ",
"Genres": "ਸ਼ੈਲੀਆਂ",
"Forced": "ਮਜਬੂਰ",
"Folders": "ਫੋਲਡਰ",
"Folders": "ਫੋਲਡਰ",
"Favorites": "ਮਨਪਸੰਦ",
"FailedLoginAttemptWithUserName": "ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ {0}",
"FailedLoginAttemptWithUserName": "{0} ਤੋਂ ਲਾਗਇਨ ਕੋਸ਼ਿਸ਼ ਫੇਲ ਹੋਈ",
"DeviceOnlineWithName": "{0} ਜੁੜਿਆ ਹੋਇਆ ਹੈ",
"DeviceOfflineWithName": "{0} ਡਿਸਕਨੈਕਟ ਹੋ ਗਿਆ ਹੈ",
"Default": "ਡਿਫੌਲਟ",
"Collections": "ਸੰਗ੍ਰਹਿਣ",
"ChapterNameValue": "ਅਧਿਆਇ {0}",
"ChapterNameValue": "ਚੈਪਟਰ {0}",
"Channels": "ਚੈਨਲ",
"CameraImageUploadedFrom": "ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ {0}",
"CameraImageUploadedFrom": "{0} ਤੋਂ ਇੱਕ ਨਵਾਂ ਕੈਮਰਾ ਚਿੱਤਰ ਅਪਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ",
"Books": "ਕਿਤਾਬਾਂ",
"AuthenticationSucceededWithUserName": "{0} ਸਫਲਤਾਪੂਰਕ ਪ੍ਰਮਾਣਿਤ",
"Artists": "ਕਲਾਕਾਰ",
"Application": "ਐਪਲੀਕੇਸ਼ਨ",
"AppDeviceValues": "ਐਪ: {0}, ਜੰਤਰ: {1}",
"Albums": "ਐਲਬਮਾਂ"
"Albums": "ਐਲਬਮਾਂ",
"TaskOptimizeDatabase": "ਡਾਟਾਬੇਸ ਅਨੁਕੂਲ ਬਣਾਓ",
"External": "ਬਾਹਰੀ"
}

@ -121,5 +121,7 @@
"TaskOptimizeDatabase": "Otimizar base de dados",
"TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
"External": "Externo",
"HearingImpaired": "Problemas auditivos"
"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."
}

@ -42,7 +42,7 @@
"MusicVideos": "Муз. видео",
"NameInstallFailed": "Установка {0} неудачна",
"NameSeasonNumber": "Сезон {0}",
"NameSeasonUnknown": "Сезон неопознан",
"NameSeasonUnknown": "Сезон не опознан",
"NewVersionIsAvailable": "Новая версия Jellyfin Server доступна для загрузки.",
"NotificationOptionApplicationUpdateAvailable": "Имеется обновление приложения",
"NotificationOptionApplicationUpdateInstalled": "Обновление приложения установлено",
@ -96,7 +96,7 @@
"TaskRefreshChannels": "Обновление каналов",
"TaskCleanTranscode": "Очистка каталога перекодировки",
"TaskUpdatePlugins": "Обновление плагинов",
"TaskRefreshPeople": "Подновление людей",
"TaskRefreshPeople": "Обновление информации о персонах",
"TaskCleanLogs": "Очистка каталога журналов",
"TaskRefreshLibrary": "Сканирование медиатеки",
"TaskRefreshChapterImages": "Извлечение изображений сцен",

@ -12,7 +12,7 @@
"HeaderContinueWatching": "دیکھنا جاری رکھیں",
"Playlists": "پلے لسٹس",
"ValueSpecialEpisodeName": "خصوصی - {0}",
"Shows": "دکھاتا ہے۔",
"Shows": "دکھاتا ہے",
"Genres": "انواع",
"Artists": "فنکار",
"Sync": "مطابقت پذیری",
@ -123,5 +123,5 @@
"TaskCleanActivityLogDescription": "تشکیل شدہ عمر سے زیادہ پرانی سرگرمی لاگ اندراجات کو حذف کرتا ہے۔",
"External": "بیرونی",
"HearingImpaired": "قوت سماعت سے محروم",
"TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں۔"
"TaskCleanActivityLog": "سرگرمی لاگ کو صاف کریں"
}

@ -14,7 +14,7 @@
"FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
"Favorites": "我的最爱",
"Folders": "文件夹",
"Genres": "风格",
"Genres": "类型",
"HeaderAlbumArtists": "专辑艺术家",
"HeaderContinueWatching": "继续观看",
"HeaderFavoriteAlbums": "收藏的专辑",

@ -4,13 +4,13 @@
"Application": "應用程式",
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "{0} 授權成功",
"Books": "書",
"CameraImageUploadedFrom": "{0} 成功上傳一張新片",
"Books": "",
"CameraImageUploadedFrom": "{0} 成功上傳一張新片",
"Channels": "頻道",
"ChapterNameValue": "章節 {0}",
"Collections": "合輯",
"DeviceOfflineWithName": "{0} 已斷開連接",
"DeviceOnlineWithName": "{0} 已連接",
"ChapterNameValue": "第 {0} 章",
"Collections": "系列",
"DeviceOfflineWithName": "{0} 已斷開連接",
"DeviceOnlineWithName": "{0} 已連接",
"FailedLoginAttemptWithUserName": "{0} 登入失敗",
"Favorites": "我的最愛",
"Folders": "資料夾",
@ -23,105 +23,105 @@
"HeaderFavoriteShows": "最愛的節目",
"HeaderFavoriteSongs": "最愛的歌曲",
"HeaderLiveTV": "電視直播",
"HeaderNextUp": "接下來",
"HeaderNextUp": "接著播放",
"HeaderRecordingGroups": "錄製組",
"HomeVideos": "家庭影片",
"Inherit": "繼承",
"ItemAddedWithName": "{0} 已添加至媒體庫",
"ItemAddedWithName": "{0} 已添加至媒體庫",
"ItemRemovedWithName": "{0} 已從媒體庫移除",
"LabelIpAddressValue": "IP 地址: {0}",
"LabelRunningTimeValue": "運行時間: {0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin 伺服器已更新",
"MessageApplicationUpdatedTo": "Jellyfin 伺服器已更新至 {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已更新",
"MessageServerConfigurationUpdated": "伺服器設定已經更新",
"MessageApplicationUpdated": "Jellyfin 更新",
"MessageApplicationUpdatedTo": "Jellyfin 更新至 {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已更新",
"MessageServerConfigurationUpdated": "伺服器設定已經更新",
"MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
"MusicVideos": "音樂影片",
"MusicVideos": "MV",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
"NewVersionIsAvailable": "新版本的 Jellyfin 伺服器可供下載。",
"NameSeasonUnknown": "未知的季度",
"NewVersionIsAvailable": "有較新版本的 Jellyfin 可供下載。",
"NotificationOptionApplicationUpdateAvailable": "有可用的更新",
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
"NotificationOptionAudioPlayback": "開始播放音訊",
"NotificationOptionAudioPlaybackStopped": "停止播放音訊",
"NotificationOptionCameraImageUploaded": "相片已上傳",
"NotificationOptionAudioPlaybackStopped": "停止播放音訊",
"NotificationOptionCameraImageUploaded": "相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已添加新内容",
"NotificationOptionPluginError": "擴充元件錯誤",
"NotificationOptionPluginInstalled": "擴充元件已安裝",
"NotificationOptionPluginUninstalled": "擴充元件已移除",
"NotificationOptionPluginUpdateInstalled": "擴充元件更新已安裝",
"NotificationOptionServerRestartRequired": "伺服器需要重",
"NotificationOptionTaskFailed": "計劃任務失敗",
"NotificationOptionUserLockedOut": "用家已鎖定",
"NotificationOptionVideoPlayback": "開始播放視頻",
"NotificationOptionVideoPlaybackStopped": "已停止播放視頻",
"NotificationOptionPluginError": "插件出現錯誤",
"NotificationOptionPluginInstalled": "插件已被安裝",
"NotificationOptionPluginUninstalled": "插件已被移除",
"NotificationOptionPluginUpdateInstalled": "插件已被更新",
"NotificationOptionServerRestartRequired": "伺服器需要重",
"NotificationOptionTaskFailed": "排程任務執行失敗",
"NotificationOptionUserLockedOut": "用戶已被鎖定",
"NotificationOptionVideoPlayback": "開始播放影片",
"NotificationOptionVideoPlaybackStopped": "已停止播放影片",
"Photos": "相片",
"Playlists": "播放清單",
"Plugin": "插件",
"PluginInstalledWithName": "已安裝 {0}",
"PluginUninstalledWithName": "已移除 {0}",
"PluginUpdatedWithName": "已更新 {0}",
"ProviderValue": "提供者: {0}",
"ScheduledTaskFailedWithName": "{0} 任務失敗",
"ScheduledTaskStartedWithName": "{0} 任務開始",
"ServerNameNeedsToBeRestarted": "{0} 需要重",
"ProviderValue": "提供者{0}",
"ScheduledTaskFailedWithName": "{0} 執行失敗",
"ScheduledTaskStartedWithName": "{0} 開始執行",
"ServerNameNeedsToBeRestarted": "{0} 需要重",
"Shows": "節目",
"Songs": "歌曲",
"StartupEmbyServerIsLoading": "Jellyfin 伺服器載入中,請稍後再試。",
"StartupEmbyServerIsLoading": "正在載入 Jellyfin請稍後再試。",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕",
"Sync": "同步",
"System": "系統",
"TvShows": "電視節目",
"User": "使用者",
"UserCreatedWithName": "使用者 {0} 已創建",
"UserDeletedWithName": "使用者 {0} 已移除",
"User": "用戶",
"UserCreatedWithName": "用戶 {0} 已被建立",
"UserDeletedWithName": "用戶 {0} 已被移除",
"UserDownloadingItemWithValues": "{0} 正在下載 {1}",
"UserLockedOutWithName": "使用者 {0} 已被鎖定",
"UserOfflineFromDevice": "{0} 從 {1} 斷開",
"UserOnlineFromDevice": "{0} 已連綫,來自 {1}",
"UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
"UserOfflineFromDevice": "{0} 從 {1} 斷開連接",
"UserOnlineFromDevice": "{0} 從 {1} 連線",
"UserPasswordChangedWithName": "{0} 的密碼已被變改",
"UserPolicyUpdatedWithName": "使用者協議已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}",
"ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫",
"UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 上播放 {1}",
"ValueHasBeenAddedToLibrary": "已添加 {0} 到你的媒體庫",
"ValueSpecialEpisodeName": "特典 - {0}",
"VersionNumber": "版本{0}",
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
"VersionNumber": "版本 {0}",
"TaskDownloadMissingSubtitles": "下載缺少的字幕",
"TaskUpdatePlugins": "更新插件",
"TasksApplicationCategory": "應用程式",
"TaskRefreshLibraryDescription": "掃描媒體庫以查找新文件並刷新metadata。",
"TaskRefreshLibraryDescription": "掃描媒體庫以加入新增檔案及重新載入 metadata。",
"TasksMaintenanceCategory": "維護",
"TaskDownloadMissingSubtitlesDescription": "根據metadata配置在互聯網上搜索缺少的字幕。",
"TaskRefreshChannelsDescription": "刷新互聯網頻道信息。",
"TaskRefreshChannels": "刷新頻道",
"TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在互聯網上搜索缺少的字幕。",
"TaskRefreshChannelsDescription": "重新載入網絡頻道的資訊。",
"TaskRefreshChannels": "重新載入頻道",
"TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。",
"TaskCleanTranscode": "清理轉碼目錄",
"TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
"TaskUpdatePluginsDescription": "下載並更新能夠被自動更新的插件。",
"TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的元數據。",
"TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。",
"TaskCleanLogs": "清理日誌目錄",
"TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。",
"TaskCleanLogs": "清理紀錄檔目錄",
"TaskRefreshLibrary": "掃描媒體庫",
"TaskRefreshChapterImagesDescription": "為帶有章節的視頻創建縮略圖。",
"TaskRefreshChapterImagesDescription": "為帶有章節的影片建立縮圖。",
"TaskRefreshChapterImages": "提取章節圖像",
"TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。",
"TaskCleanCache": "清理緩存目錄",
"TasksChannelsCategory": "互聯網頻道",
"TasksChannelsCategory": "頻道",
"TasksLibraryCategory": "庫",
"TaskRefreshPeople": "刷新人物",
"TaskRefreshPeople": "重新載入人物",
"TaskCleanActivityLog": "清理活動記錄",
"Undefined": "未定義",
"Forced": "強制",
"Default": "預設",
"TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。",
"TaskOptimizeDatabase": "最佳化數據庫",
"TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。",
"TaskKeyframeExtractorDescription": "提取關鍵格以創建更準確的HLS播放列表。次指示可能用時很長。",
"TaskCleanActivityLogDescription": "刪除早於設定時間的活動記錄。",
"TaskKeyframeExtractorDescription": "提取關鍵幀以建立更準確的 HLS 播放列表。此工作或需要使用較長時間來完成。",
"TaskKeyframeExtractor": "關鍵幀提取器",
"External": "外部",
"HearingImpaired": "聽力障礙"

@ -91,14 +91,14 @@
"HeaderRecordingGroups": "錄製組",
"Inherit": "繼承",
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
"TaskDownloadMissingSubtitlesDescription": "透過中繼資料從網路上搜尋遺失的字幕。",
"TaskDownloadMissingSubtitlesDescription": "透過媒體資訊從網路上搜尋遺失的字幕。",
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
"TaskRefreshChannels": "重新整理頻道",
"TaskUpdatePlugins": "更新附加元件",
"TaskRefreshPeople": "更新人物",
"TaskCleanLogsDescription": "刪除超過 {0} 天的日誌文件。",
"TaskCleanLogs": "清空日誌資料夾",
"TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新中繼資料。",
"TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新媒體資訊。",
"TaskRefreshLibrary": "重新掃描媒體庫",
"TaskRefreshChapterImages": "擷取章節圖片",
"TaskCleanCacheDescription": "刪除系統已不需要的快取。",
@ -108,7 +108,7 @@
"TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
"TaskCleanTranscode": "清除轉碼資料夾",
"TaskUpdatePluginsDescription": "為已設置為自動更新的附加元件下載並安裝更新。",
"TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。",
"TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的資訊。",
"TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
"TasksChannelsCategory": "網路頻道",
"TasksApplicationCategory": "應用程式",

@ -198,25 +198,25 @@ namespace Emby.Server.Implementations.Localization
}
// Minimum rating possible
if (!ratings.Any(x => x.Value == 0))
if (ratings.All(x => x.Value != 0))
{
ratings.Add(new ParentalRating("Approved", 0));
}
// Matches PG (this has different age restrictions depending on country)
if (!ratings.Any(x => x.Value == 10))
if (ratings.All(x => x.Value != 10))
{
ratings.Add(new ParentalRating("10", 10));
}
// Matches PG-13
if (!ratings.Any(x => x.Value == 13))
if (ratings.All(x => x.Value != 13))
{
ratings.Add(new ParentalRating("13", 13));
}
// Matches TV-14
if (!ratings.Any(x => x.Value == 14))
if (ratings.All(x => x.Value != 14))
{
ratings.Add(new ParentalRating("14", 14));
}
@ -229,13 +229,13 @@ namespace Emby.Server.Implementations.Localization
}
// A lot of countries don't excplicitly have a seperate rating for adult content
if (!ratings.Any(x => x.Value == 1000))
if (ratings.All(x => x.Value != 1000))
{
ratings.Add(new ParentalRating("XXX", 1000));
}
// A lot of countries don't excplicitly have a seperate rating for banned content
if (!ratings.Any(x => x.Value == 1001))
if (ratings.All(x => x.Value != 1001))
{
ratings.Add(new ParentalRating("Banned", 1001));
}

@ -15,6 +15,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@ -62,23 +63,16 @@ namespace Emby.Server.Implementations.MediaEncoder
/// Determines whether [is eligible for chapter image extraction] [the specified video].
/// </summary>
/// <param name="video">The video.</param>
/// <param name="libraryOptions">The library options for the video.</param>
/// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns>
private bool IsEligibleForChapterImageExtraction(Video video)
private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions)
{
if (video.IsPlaceHolder)
{
return false;
}
var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (libraryOptions is not null)
{
if (!libraryOptions.EnableChapterImageExtraction)
{
return false;
}
}
else
if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction)
{
return false;
}
@ -99,7 +93,9 @@ namespace Emby.Server.Implementations.MediaEncoder
public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
{
if (!IsEligibleForChapterImageExtraction(video))
var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (!IsEligibleForChapterImageExtraction(video, libraryOptions))
{
extractImages = false;
}
@ -179,6 +175,12 @@ namespace Emby.Server.Implementations.MediaEncoder
chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
changesMade = true;
}
else if (libraryOptions?.EnableChapterImageExtraction != true)
{
// We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image
chapter.ImagePath = null;
changesMade = true;
}
}
if (saveChapters && changesMade)

@ -135,16 +135,8 @@ namespace Emby.Server.Implementations.Playlists
{
Name = name,
Path = path,
Shares = new[]
{
new Share
{
UserId = options.UserId.Equals(default)
? null
: options.UserId.ToString("N", CultureInfo.InvariantCulture),
CanEdit = true
}
}
OwnerUserId = options.UserId,
Shares = options.Shares ?? Array.Empty<Share>()
};
playlist.SetMediaType(options.MediaType);
@ -537,5 +529,55 @@ namespace Emby.Server.Implementations.Playlists
return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
_libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
}
/// <inheritdoc />
public async Task RemovePlaylistsAsync(Guid userId)
{
var playlists = GetPlaylists(userId);
foreach (var playlist in playlists)
{
// Update owner if shared
var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToArray();
if (rankedShares.Length > 0 && Guid.TryParse(rankedShares[0].UserId, out var guid))
{
playlist.OwnerUserId = guid;
playlist.Shares = rankedShares.Skip(1).ToArray();
await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (playlist.IsFile)
{
SavePlaylistFile(playlist);
}
}
else
{
// Remove playlist if not shared
_libraryManager.DeleteItem(
playlist,
new DeleteOptions
{
DeleteFileLocation = false,
DeleteFromExternalProvider = false
},
playlist.GetParent(),
false);
}
}
}
/// <inheritdoc />
public async Task UpdatePlaylistAsync(Playlist playlist)
{
var currentPlaylist = (Playlist)_libraryManager.GetItemById(playlist.Id);
currentPlaylist.OwnerUserId = playlist.OwnerUserId;
currentPlaylist.Shares = playlist.Shares;
await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
if (currentPlaylist.IsFile)
{
SavePlaylistFile(currentPlaylist);
}
}
}
}

@ -304,7 +304,7 @@ namespace Emby.Server.Implementations.Plugins
// If no version is given, return the current instance.
var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList();
plugin = plugins.FirstOrDefault(p => p.Instance is not null) ?? plugins.OrderByDescending(p => p.Version).FirstOrDefault();
plugin = plugins.FirstOrDefault(p => p.Instance is not null) ?? plugins.MaxBy(p => p.Version);
}
else
{

@ -100,7 +100,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
EnableImages = false
},
SourceTypes = new SourceType[] { SourceType.Library },
HasChapterImages = false,
IsVirtualItem = false
})
.OfType<Video>()

@ -606,7 +606,7 @@ namespace Emby.Server.Implementations.Session
}
catch (Exception ex)
{
_logger.LogDebug("Error calling OnPlaybackStopped", ex);
_logger.LogDebug(ex, "Error calling OnPlaybackStopped");
}
}
@ -953,7 +953,7 @@ namespace Emby.Server.Implementations.Session
}
catch (Exception ex)
{
_logger.LogError("Error closing live stream", ex);
_logger.LogError(ex, "Error closing live stream");
}
}

@ -69,9 +69,7 @@ namespace Emby.Server.Implementations.Session
T data,
CancellationToken cancellationToken)
{
var socket = GetActiveSockets()
.OrderByDescending(i => i.LastActivityDate)
.FirstOrDefault();
var socket = GetActiveSockets().MaxBy(i => i.LastActivityDate);
if (socket is null)
{

@ -620,10 +620,8 @@ namespace Emby.Server.Implementations.SyncPlay
RestartCurrentItem();
return true;
}
else
{
return false;
}
return false;
}
/// <inheritdoc />
@ -637,10 +635,8 @@ namespace Emby.Server.Implementations.SyncPlay
RestartCurrentItem();
return true;
}
else
{
return false;
}
return false;
}
/// <inheritdoc />

@ -339,10 +339,8 @@ namespace Emby.Server.Implementations.SyncPlay
{
return sessionsCounter > 0;
}
else
{
return false;
}
return false;
}
/// <summary>

@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Extensions;
using Jellyfin.MediaEncoding.Hls.Playlist;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
@ -19,7 +20,6 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
@ -1687,14 +1687,25 @@ public class DynamicHlsController : BaseJellyfinApiController
audioTranscodeParams += "-acodec " + audioCodec;
if (state.OutputAudioBitrate.HasValue)
var audioBitrate = state.OutputAudioBitrate;
var audioChannels = state.OutputAudioChannels;
if (audioBitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(state.ActualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
{
audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, audioBitrate.Value / (audioChannels ?? 2));
if (_encodingOptions.EnableAudioVbr && vbrParam is not null)
{
audioTranscodeParams += vbrParam;
}
else
{
audioTranscodeParams += " -ab " + audioBitrate.Value.ToString(CultureInfo.InvariantCulture);
}
}
if (state.OutputAudioChannels.HasValue)
if (audioChannels.HasValue)
{
audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
audioTranscodeParams += " -ac " + audioChannels.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
@ -1708,11 +1719,11 @@ public class DynamicHlsController : BaseJellyfinApiController
// dts, flac, opus and truehd are experimental in mp4 muxer
var strictArgs = string.Empty;
if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
{
strictArgs = " -strict -2";
}
@ -1746,10 +1757,17 @@ public class DynamicHlsController : BaseJellyfinApiController
}
var bitrate = state.OutputAudioBitrate;
if (bitrate.HasValue)
if (bitrate.HasValue && !EncodingHelper.LosslessAudioCodecs.Contains(actualOutputAudioCodec, StringComparison.OrdinalIgnoreCase))
{
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
var vbrParam = _encodingHelper.GetAudioVbrModeParam(audioCodec, bitrate.Value / (channels ?? 2));
if (_encodingOptions.EnableAudioVbr && vbrParam is not null)
{
args += vbrParam;
}
else
{
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
}
}
if (state.OutputAudioSampleRate.HasValue)
@ -2011,8 +2029,7 @@ public class DynamicHlsController : BaseJellyfinApiController
{
return fileSystem.GetFiles(folder, new[] { segmentExtension }, true, false)
.Where(i => Path.GetFileNameWithoutExtension(i.Name).StartsWith(filePrefix, StringComparison.OrdinalIgnoreCase))
.OrderByDescending(fileSystem.GetLastWriteTimeUtc)
.FirstOrDefault();
.MaxBy(fileSystem.GetLastWriteTimeUtc);
}
catch (IOException)
{

@ -1,6 +1,5 @@
using System;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;

@ -15,6 +15,7 @@ using Jellyfin.Api.Models.LibraryDtos;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@ -332,12 +333,26 @@ public class LibraryController : BaseJellyfinApiController
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteItem(Guid itemId)
{
var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
var user = !isApiKey && !userId.Equals(default)
? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
: null;
if (!isApiKey && user is null)
{
return Unauthorized("Unauthorized access");
}
var item = _libraryManager.GetItemById(itemId);
var user = _userManager.GetUserById(User.GetUserId());
if (item is null)
{
return NotFound();
}
if (!item.CanDelete(user))
if (user is not null && !item.CanDelete(user))
{
return Unauthorized("Unauthorized access");
}
@ -361,26 +376,31 @@ public class LibraryController : BaseJellyfinApiController
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
if (ids.Length == 0)
var isApiKey = User.GetIsApiKey();
var userId = User.GetUserId();
var user = !isApiKey && !userId.Equals(default)
? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
: null;
if (!isApiKey && user is null)
{
return NoContent();
return Unauthorized("Unauthorized access");
}
foreach (var i in ids)
{
var item = _libraryManager.GetItemById(i);
var user = _userManager.GetUserById(User.GetUserId());
if (!item.CanDelete(user))
if (item is null)
{
if (ids.Length > 1)
{
return Unauthorized("Unauthorized access");
}
return NotFound();
}
continue;
if (user is not null && !item.CanDelete(user))
{
return Unauthorized("Unauthorized access");
}
_libraryManager.DeleteItem(

@ -146,7 +146,7 @@ public class PluginsController : BaseJellyfinApiController
var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId)).ToList();
// Select the un-instanced one first.
var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
var plugin = plugins.FirstOrDefault(p => p.Instance is null) ?? plugins.MinBy(p => p.Manifest.Status);
if (plugin is not null)
{

@ -1,8 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;

@ -3,7 +3,6 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;

@ -533,10 +533,8 @@ public class SubtitleController : BaseJellyfinApiController
_logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize);
return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
}
else
{
_logger.LogWarning("The selected font is null or empty");
}
_logger.LogWarning("The selected font is null or empty");
}
else
{

@ -5,7 +5,6 @@ using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;

@ -15,6 +15,7 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.QuickConnect;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Configuration;
@ -41,6 +42,7 @@ public class UserController : BaseJellyfinApiController
private readonly IServerConfigurationManager _config;
private readonly ILogger _logger;
private readonly IQuickConnect _quickConnectManager;
private readonly IPlaylistManager _playlistManager;
/// <summary>
/// Initializes a new instance of the <see cref="UserController"/> class.
@ -53,6 +55,7 @@ public class UserController : BaseJellyfinApiController
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="quickConnectManager">Instance of the <see cref="IQuickConnect"/> interface.</param>
/// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
public UserController(
IUserManager userManager,
ISessionManager sessionManager,
@ -61,7 +64,8 @@ public class UserController : BaseJellyfinApiController
IAuthorizationContext authContext,
IServerConfigurationManager config,
ILogger<UserController> logger,
IQuickConnect quickConnectManager)
IQuickConnect quickConnectManager,
IPlaylistManager playlistManager)
{
_userManager = userManager;
_sessionManager = sessionManager;
@ -71,6 +75,7 @@ public class UserController : BaseJellyfinApiController
_config = config;
_logger = logger;
_quickConnectManager = quickConnectManager;
_playlistManager = playlistManager;
}
/// <summary>
@ -153,6 +158,7 @@ public class UserController : BaseJellyfinApiController
}
await _sessionManager.RevokeUserTokens(user.Id, null).ConfigureAwait(false);
await _playlistManager.RemovePlaylistsAsync(userId).ConfigureAwait(false);
await _userManager.DeleteUserAsync(userId).ConfigureAwait(false);
return NoContent();
}

@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@ -223,9 +224,17 @@ public class DynamicHlsHelper
sdrVideoUrl += "&AllowVideoStreamCopy=false";
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
var sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
var sdrOutputAudioBitrate = 0;
if (EncodingHelper.LosslessAudioCodecs.Contains(state.VideoRequest.AudioCodec, StringComparison.OrdinalIgnoreCase))
{
sdrOutputAudioBitrate = state.AudioStream.BitRate ?? 0;
}
else
{
sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0;
}
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
// Provide a workaround for the case issue between flac and fLaC.

@ -181,12 +181,18 @@ public static class StreamingHelpers
: GetOutputFileExtension(state, mediaSource);
}
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
state.OutputAudioCodec = streamingRequest.AudioCodec;
var outputAudioCodec = streamingRequest.AudioCodec;
if (EncodingHelper.LosslessAudioCodecs.Contains(outputAudioCodec))
{
state.OutputAudioBitrate = state.AudioStream.BitRate ?? 0;
}
else
{
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
}
state.OutputAudioCodec = outputAudioCodec;
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
if (state.VideoRequest is not null)

@ -0,0 +1,97 @@
namespace Jellyfin.Data.Enums;
/// <summary>
/// The person kind.
/// </summary>
public enum PersonKind
{
/// <summary>
/// An unknown person kind.
/// </summary>
Unknown,
/// <summary>
/// A person whose profession is acting on the stage, in films, or on television.
/// </summary>
Actor,
/// <summary>
/// A person who supervises the actors and other staff in a film, play, or similar production.
/// </summary>
Director,
/// <summary>
/// A person who writes music, especially as a professional occupation.
/// </summary>
Composer,
/// <summary>
/// A writer of a book, article, or document. Can also be used as a generic term for music writer if there is a lack of specificity.
/// </summary>
Writer,
/// <summary>
/// A well-known actor or other performer who appears in a work in which they do not have a regular role.
/// </summary>
GuestStar,
/// <summary>
/// A person responsible for the financial and managerial aspects of the making of a film or broadcast or for staging a play, opera, etc.
/// </summary>
Producer,
/// <summary>
/// A person who directs the performance of an orchestra or choir.
/// </summary>
Conductor,
/// <summary>
/// A person who writes the words to a song or musical.
/// </summary>
Lyricist,
/// <summary>
/// A person who adapts a musical composition for performance.
/// </summary>
Arranger,
/// <summary>
/// An audio engineer who performed a general engineering role.
/// </summary>
Engineer,
/// <summary>
/// An engineer responsible for using a mixing console to mix a recorded track into a single piece of music suitable for release.
/// </summary>
Mixer,
/// <summary>
/// A person who remixed a recording by taking one or more other tracks, substantially altering them and mixing them together with other material.
/// </summary>
Remixer,
/// <summary>
/// A person who created the material.
/// </summary>
Creator,
/// <summary>
/// A person who was the artist.
/// </summary>
Artist,
/// <summary>
/// A person who was the album artist.
/// </summary>
AlbumArtist,
/// <summary>
/// A person who was the author.
/// </summary>
Author,
/// <summary>
/// A person who was the illustrator.
/// </summary>
Illustrator,
}

@ -4,7 +4,6 @@ using System;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using EFCoreSecondLevelCacheInterceptor;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;

@ -22,7 +22,6 @@ using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Activity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

@ -40,7 +40,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.ReaddDefaultPluginRepository),
typeof(Routines.MigrateDisplayPreferencesDb),
typeof(Routines.RemoveDownloadImagesInAdvance),
typeof(Routines.MigrateAuthenticationDb)
typeof(Routines.MigrateAuthenticationDb),
typeof(Routines.FixPlaylistOwner)
};
/// <summary>

@ -44,9 +44,7 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine
return;
}
var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration"));
using var xmlReader = XmlReader.Create(path);
var oldPluginConfiguration = serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration;
var oldPluginConfiguration = ReadOld(path);
if (oldPluginConfiguration is not null)
{
@ -55,10 +53,25 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine
newPluginConfiguration.ReplaceArtistName = oldPluginConfiguration.ReplaceArtistName;
var newRateLimit = oldPluginConfiguration.RateLimit / 1000.0;
newPluginConfiguration.RateLimit = newRateLimit < 1.0 ? 1.0 : newRateLimit;
WriteNew(path, newPluginConfiguration);
}
}
var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration"));
var xmlWriterSettings = new XmlWriterSettings { Indent = true };
using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
private OldMusicBrainzConfiguration? ReadOld(string path)
{
using (var xmlReader = XmlReader.Create(path))
{
var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration"));
return serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration;
}
}
private void WriteNew(string path, PluginConfiguration newPluginConfiguration)
{
var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration"));
var xmlWriterSettings = new XmlWriterSettings { Indent = true };
using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings))
{
pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration);
}
}

@ -0,0 +1,67 @@
using System;
using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Properly set playlist owner.
/// </summary>
internal class FixPlaylistOwner : IMigrationRoutine
{
private readonly ILogger<RemoveDuplicateExtras> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IPlaylistManager _playlistManager;
public FixPlaylistOwner(
ILogger<RemoveDuplicateExtras> logger,
ILibraryManager libraryManager,
IPlaylistManager playlistManager)
{
_logger = logger;
_libraryManager = libraryManager;
_playlistManager = playlistManager;
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{615DFA9E-2497-4DBB-A472-61938B752C5B}");
/// <inheritdoc/>
public string Name => "FixPlaylistOwner";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/>
public void Perform()
{
var playlists = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.Playlist }
})
.Cast<Playlist>()
.Where(x => x.OwnerUserId.Equals(Guid.Empty))
.ToArray();
if (playlists.Length > 0)
{
foreach (var playlist in playlists)
{
var shares = playlist.Shares;
var firstEditShare = shares.First(x => x.CanEdit);
if (firstEditShare is not null && Guid.TryParse(firstEditShare.UserId, out var guid))
{
playlist.OwnerUserId = guid;
playlist.Shares = shares.Where(x => x != firstEditShare).ToArray();
_playlistManager.UpdatePlaylistAsync(playlist).GetAwaiter().GetResult();
}
}
}
}
}

@ -133,9 +133,7 @@ namespace Jellyfin.Server.Migrations.Routines
SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length) && int.TryParse(length, out var skipBackwardLength)
? skipBackwardLength
: 10000,
EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) && !string.IsNullOrEmpty(enabled)
? bool.Parse(enabled)
: true,
EnableNextVideoInfoOverlay = !dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled) || string.IsNullOrEmpty(enabled) || bool.Parse(enabled),
DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty,
TvHome = dto.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty
};

@ -4,7 +4,6 @@ using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Runtime.InteropServices;
using System.Text;
using Jellyfin.Api.Middleware;
using Jellyfin.MediaEncoding.Hls.Extensions;

@ -1,3 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Emby/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Jellyfin/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Playstate/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

@ -50,7 +50,7 @@ namespace MediaBrowser.Common.Plugins
if (Version is not null && !Directory.Exists(dataFolderPath))
{
// Try again with the version number appended to the folder name.
dataFolderPath += "_" + Version.ToString();
dataFolderPath += "_" + Version;
}
SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);

@ -801,16 +801,14 @@ namespace MediaBrowser.Controller.Entities
{
return allowed.Contains(ChannelId);
}
else
{
var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
foreach (var folder in collectionFolders)
var collectionFolders = LibraryManager.GetCollectionFolders(this, allCollectionFolders);
foreach (var folder in collectionFolders)
{
if (allowed.Contains(folder.Id))
{
if (allowed.Contains(folder.Id))
{
return true;
}
return true;
}
}
@ -893,16 +891,6 @@ namespace MediaBrowser.Controller.Entities
var sortable = Name.Trim().ToLowerInvariant();
foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
{
sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
}
foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
{
sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
}
foreach (var search in ConfigurationManager.Configuration.SortRemoveWords)
{
// Remove from beginning if a space follows
@ -921,12 +909,22 @@ namespace MediaBrowser.Controller.Entities
}
}
foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters)
{
sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal);
}
foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters)
{
sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal);
}
return ModifySortChunks(sortable);
}
internal static string ModifySortChunks(string name)
internal static string ModifySortChunks(ReadOnlySpan<char> name)
{
void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
static void AppendChunk(StringBuilder builder, bool isDigitChunk, ReadOnlySpan<char> chunk)
{
if (isDigitChunk && chunk.Length < 10)
{
@ -936,7 +934,7 @@ namespace MediaBrowser.Controller.Entities
builder.Append(chunk);
}
if (name.Length == 0)
if (name.IsEmpty)
{
return string.Empty;
}
@ -950,13 +948,13 @@ namespace MediaBrowser.Controller.Entities
var isDigit = char.IsDigit(name[i]);
if (isDigit != isDigitChunk)
{
AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart, i - chunkStart));
AppendChunk(builder, isDigitChunk, name.Slice(chunkStart, i - chunkStart));
chunkStart = i;
isDigitChunk = isDigit;
}
}
AppendChunk(builder, isDigitChunk, name.AsSpan(chunkStart));
AppendChunk(builder, isDigitChunk, name.Slice(chunkStart));
// logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString());
return builder.ToString().RemoveDiacritics();

@ -1,11 +0,0 @@
#nullable disable
#pragma warning disable CA1819, CS1591
namespace MediaBrowser.Controller.Entities
{
public interface IHasShares
{
Share[] Shares { get; set; }
}
}

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Entities
@ -17,38 +18,38 @@ namespace MediaBrowser.Controller.Entities
// Normalize
if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
{
person.Type = PersonType.GuestStar;
person.Type = PersonKind.GuestStar;
}
else if (string.Equals(person.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
{
person.Type = PersonType.Director;
person.Type = PersonKind.Director;
}
else if (string.Equals(person.Role, PersonType.Producer, StringComparison.OrdinalIgnoreCase))
{
person.Type = PersonType.Producer;
person.Type = PersonKind.Producer;
}
else if (string.Equals(person.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
{
person.Type = PersonType.Writer;
person.Type = PersonKind.Writer;
}
// If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes
if (string.Equals(person.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
if (person.Type == PersonKind.GuestStar)
{
var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase));
var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type == PersonKind.Actor);
if (existing is not null)
{
existing.Type = PersonType.GuestStar;
existing.Type = PersonKind.GuestStar;
MergeExisting(existing, person);
return;
}
}
if (string.Equals(person.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase))
if (person.Type == PersonKind.Actor)
{
// If the actor already exists without a role and we have one, fill it in
var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type.Equals(PersonType.Actor, StringComparison.OrdinalIgnoreCase) || p.Type.Equals(PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)));
var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type == PersonKind.Actor || p.Type == PersonKind.GuestStar));
if (existing is null)
{
// Wasn't there - add it
@ -68,8 +69,8 @@ namespace MediaBrowser.Controller.Entities
else
{
var existing = people.FirstOrDefault(p =>
string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase) &&
string.Equals(p.Type, person.Type, StringComparison.OrdinalIgnoreCase));
string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase)
&& p.Type == person.Type);
// Check for dupes based on the combination of Name and Type
if (existing is null)

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Entities
@ -36,7 +37,7 @@ namespace MediaBrowser.Controller.Entities
/// Gets or sets the type.
/// </summary>
/// <value>The type.</value>
public string Type { get; set; }
public PersonKind Type { get; set; }
/// <summary>
/// Gets or sets the ascending sort order.
@ -57,10 +58,6 @@ namespace MediaBrowser.Controller.Entities
return Name;
}
public bool IsType(string type)
{
return string.Equals(Type, type, StringComparison.OrdinalIgnoreCase)
|| string.Equals(Role, type, StringComparison.OrdinalIgnoreCase);
}
public bool IsType(PersonKind type) => Type == type || string.Equals(type.ToString(), Role, StringComparison.OrdinalIgnoreCase);
}
}

@ -1,13 +0,0 @@
#nullable disable
#pragma warning disable CS1591
namespace MediaBrowser.Controller.Entities
{
public class Share
{
public string UserId { get; set; }
public bool CanEdit { get; set; }
}
}

@ -197,10 +197,8 @@ namespace MediaBrowser.Controller.LiveTv
{
return 2.0 / 3;
}
else
{
return 16.0 / 9;
}
return 16.0 / 9;
}
public override string GetClientTypeName()

@ -1,5 +1,3 @@
using System;
namespace MediaBrowser.Controller.Lyrics;
/// <summary>

@ -89,6 +89,16 @@ namespace MediaBrowser.Controller.MediaEncoding
{ "truehd", 6 },
};
public static readonly string[] LosslessAudioCodecs = new string[]
{
"alac",
"ape",
"flac",
"mlp",
"truehd",
"wavpack"
};
public EncodingHelper(
IApplicationPaths appPaths,
IMediaEncoder mediaEncoder,
@ -128,14 +138,10 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(hwType)
&& encodingOptions.EnableHardwareEncoding
&& codecMap.ContainsKey(hwType))
&& codecMap.TryGetValue(hwType, out var preferredEncoder)
&& _mediaEncoder.SupportsEncoder(preferredEncoder))
{
var preferredEncoder = codecMap[hwType];
if (_mediaEncoder.SupportsEncoder(preferredEncoder))
{
return preferredEncoder;
}
return preferredEncoder;
}
}
@ -551,7 +557,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return Array.FindIndex(_videoProfilesH264, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
}
else if (string.Equals("hevc", videoCodec, StringComparison.OrdinalIgnoreCase))
if (string.Equals("hevc", videoCodec, StringComparison.OrdinalIgnoreCase))
{
return Array.FindIndex(_videoProfilesH265, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
}
@ -620,6 +627,11 @@ namespace MediaBrowser.Controller.MediaEncoding
return "flac";
}
if (string.Equals(codec, "dts", StringComparison.OrdinalIgnoreCase))
{
return "dca";
}
return codec.ToLowerInvariant();
}
@ -827,39 +839,38 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var filterDevArgs = GetFilterHwDeviceArgs(VaapiAlias);
var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported();
if (isHwTonemapAvailable && IsOpenclFullSupported())
if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
{
if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
if (doOclTonemap && !isVaapiDecoder)
{
if (!isVaapiDecoder)
{
args.Append(GetOpenclDeviceArgs(0, null, VaapiAlias, OpenclAlias));
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
}
args.Append(GetOpenclDeviceArgs(0, null, VaapiAlias, OpenclAlias));
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
}
else if (_mediaEncoder.IsVaapiDeviceAmd)
}
else if (_mediaEncoder.IsVaapiDeviceAmd)
{
if (IsVulkanFullSupported()
&& _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
{
if (!IsVulkanFullSupported()
|| !_mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
|| Environment.OSVersion.Version < _minKernelVersionAmdVkFmtModifier)
{
args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
}
else
{
// libplacebo wants an explicitly set vulkan filter device.
args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias));
filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
}
// libplacebo wants an explicitly set vulkan filter device.
args.Append(GetVulkanDeviceArgs(0, null, VaapiAlias, VulkanAlias));
filterDevArgs = GetFilterHwDeviceArgs(VulkanAlias);
}
else
else if (doOclTonemap)
{
args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias));
// ROCm/ROCr OpenCL runtime
args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias));
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
}
}
else if (doOclTonemap)
{
args.Append(GetOpenclDeviceArgs(0, null, null, OpenclAlias));
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
}
args.Append(filterDevArgs);
}
@ -1099,19 +1110,19 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return "-bsf:v h264_mp4toannexb";
}
else if (IsH265(stream))
if (IsH265(stream))
{
return "-bsf:v hevc_mp4toannexb";
}
else if (IsAAC(stream))
if (IsAAC(stream))
{
// Convert adts header(mpegts) to asc header(mp4).
return "-bsf:a aac_adtstoasc";
}
else
{
return null;
}
return null;
}
public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
@ -1189,10 +1200,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return FormattableString.Invariant($" -rc_mode CBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
else
{
return FormattableString.Invariant($" -rc_mode VBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
return FormattableString.Invariant($" -rc_mode VBR -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
}
return FormattableString.Invariant($" -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
@ -1406,7 +1415,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var param = string.Empty;
// Tutorials: Enable Intel GuC / HuC firmware loading for Low Power Encoding.
// https://01.org/linuxgraphics/downloads/firmware
// https://01.org/group/43/downloads/firmware
// https://wiki.archlinux.org/title/intel_graphics#Enable_GuC_/_HuC_firmware_loading
// Intel Low Power Encoding can save unnecessary CPU-GPU synchronization,
// which will reduce overhead in performance intensive tasks such as 4k transcoding and tonemapping.
@ -2050,9 +2059,9 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
// Video bitrate must fall within requested value
// Audio bitrate must fall within requested value
if (request.AudioBitRate.HasValue
&& audioStream.BitDepth.HasValue
&& audioStream.BitRate.HasValue
&& audioStream.BitRate.Value > request.AudioBitRate.Value)
{
return false;
@ -2139,7 +2148,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
// Don't scale the real bitrate lower than the requested bitrate
var scaleFactor = Math.Min(outputScaleFactor / inputScaleFactor, 1);
var scaleFactor = Math.Max(outputScaleFactor / inputScaleFactor, 1);
if (bitrate <= 500000)
{
@ -2161,56 +2170,96 @@ namespace MediaBrowser.Controller.MediaEncoding
return Convert.ToInt32(scaleFactor * bitrate);
}
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream, int? outputAudioChannels)
{
return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream, outputAudioChannels);
}
public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream, int? outputAudioChannels)
{
if (audioStream is null)
{
return null;
}
if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec))
var inputChannels = audioStream.Channels ?? 0;
var outputChannels = outputAudioChannels ?? 0;
var bitrate = audioBitRate ?? int.MaxValue;
if (string.IsNullOrEmpty(audioCodec)
|| string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{
return Math.Min(384000, audioBitRate.Value);
return (inputChannels, outputChannels) switch
{
(>= 6, >= 6 or 0) => Math.Min(640000, bitrate),
(> 0, > 0) => Math.Min(outputChannels * 128000, bitrate),
(> 0, _) => Math.Min(inputChannels * 128000, bitrate),
(_, _) => Math.Min(384000, bitrate)
};
}
if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec))
if (string.Equals(audioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "dca", StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "vorbis", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
return (inputChannels, outputChannels) switch
{
if ((audioStream.Channels ?? 0) >= 6)
{
return Math.Min(640000, audioBitRate.Value);
}
(>= 6, >= 6 or 0) => Math.Min(768000, bitrate),
(> 0, > 0) => Math.Min(outputChannels * 136000, bitrate),
(> 0, _) => Math.Min(inputChannels * 136000, bitrate),
(_, _) => Math.Min(672000, bitrate)
};
}
return Math.Min(384000, audioBitRate.Value);
}
// Empty bitrate area is not allow on iOS
// Default audio bitrate to 128K per channel if we don't have codec specific defaults
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
return 128000 * (outputAudioChannels ?? audioStream.Channels ?? 2);
}
if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
public string GetAudioVbrModeParam(string encoder, int bitratePerChannel)
{
if (string.Equals(encoder, "libfdk_aac", StringComparison.OrdinalIgnoreCase))
{
return " -vbr:a " + bitratePerChannel switch
{
if ((audioStream.Channels ?? 0) >= 6)
{
return Math.Min(3584000, audioBitRate.Value);
}
< 32000 => "1",
< 48000 => "2",
< 64000 => "3",
< 96000 => "4",
_ => "5"
};
}
return Math.Min(1536000, audioBitRate.Value);
}
if (string.Equals(encoder, "libmp3lame", StringComparison.OrdinalIgnoreCase))
{
return " -qscale:a " + bitratePerChannel switch
{
< 48000 => "8",
< 64000 => "6",
< 88000 => "4",
< 112000 => "2",
_ => "0"
};
}
// Empty bitrate area is not allow on iOS
// Default audio bitrate to 128K if it is not being requested
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
return 128000;
if (string.Equals(encoder, "libvorbis", StringComparison.OrdinalIgnoreCase))
{
return " -qscale:a " + bitratePerChannel switch
{
< 40000 => "0",
< 56000 => "2",
< 80000 => "4",
< 112000 => "6",
_ => "8"
};
}
return null;
}
public string GetAudioFilterParam(EncodingJobInfo state, EncodingOptions encodingOptions)
@ -2564,8 +2613,8 @@ namespace MediaBrowser.Controller.MediaEncoding
if (outputWidth > maximumWidth || outputHeight > maximumHeight)
{
var scaleW = (double)maximumWidth / (double)outputWidth;
var scaleH = (double)maximumHeight / (double)outputHeight;
var scaleW = (double)maximumWidth / outputWidth;
var scaleH = (double)maximumHeight / outputHeight;
var scale = Math.Min(scaleW, scaleH);
outputWidth = Math.Min(maximumWidth, (int)(outputWidth * scale));
outputHeight = Math.Min(maximumHeight, (int)(outputHeight * scale));
@ -2712,79 +2761,76 @@ namespace MediaBrowser.Controller.MediaEncoding
widthParam,
heightParam);
}
else
{
return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, requestedHeight.Value);
}
return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, requestedHeight.Value);
}
// If Max dimensions were supplied, for width selects lowest even number between input width and width req size and selects lowest even number from in width*display aspect and requested size
else if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue)
if (requestedMaxWidth.HasValue && requestedMaxHeight.HasValue)
{
var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture);
var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture);
return string.Format(
CultureInfo.InvariantCulture,
"scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2",
maxWidthParam,
maxHeightParam,
scaleVal);
CultureInfo.InvariantCulture,
"scale=trunc(min(max(iw\\,ih*a)\\,min({0}\\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\\,ih)\\,min({0}/a\\,{1}))/2)*2",
maxWidthParam,
maxHeightParam,
scaleVal);
}
// If a fixed width was requested
else if (requestedWidth.HasValue)
if (requestedWidth.HasValue)
{
if (threedFormat.HasValue)
{
// This method can handle 0 being passed in for the requested height
return GetFixedSwScaleFilter(threedFormat, requestedWidth.Value, 0);
}
else
{
var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture);
return string.Format(
CultureInfo.InvariantCulture,
"scale={0}:trunc(ow/a/2)*2",
widthParam);
}
var widthParam = requestedWidth.Value.ToString(CultureInfo.InvariantCulture);
return string.Format(
CultureInfo.InvariantCulture,
"scale={0}:trunc(ow/a/2)*2",
widthParam);
}
// If a fixed height was requested
else if (requestedHeight.HasValue)
if (requestedHeight.HasValue)
{
var heightParam = requestedHeight.Value.ToString(CultureInfo.InvariantCulture);
return string.Format(
CultureInfo.InvariantCulture,
"scale=trunc(oh*a/{1})*{1}:{0}",
heightParam,
scaleVal);
CultureInfo.InvariantCulture,
"scale=trunc(oh*a/{1})*{1}:{0}",
heightParam,
scaleVal);
}
// If a max width was requested
else if (requestedMaxWidth.HasValue)
if (requestedMaxWidth.HasValue)
{
var maxWidthParam = requestedMaxWidth.Value.ToString(CultureInfo.InvariantCulture);
return string.Format(
CultureInfo.InvariantCulture,
"scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2",
maxWidthParam,
scaleVal);
CultureInfo.InvariantCulture,
"scale=trunc(min(max(iw\\,ih*a)\\,{0})/{1})*{1}:trunc(ow/a/2)*2",
maxWidthParam,
scaleVal);
}
// If a max height was requested
else if (requestedMaxHeight.HasValue)
if (requestedMaxHeight.HasValue)
{
var maxHeightParam = requestedMaxHeight.Value.ToString(CultureInfo.InvariantCulture);
return string.Format(
CultureInfo.InvariantCulture,
"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})",
maxHeightParam,
scaleVal);
CultureInfo.InvariantCulture,
"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\\,ih)\\,{0})",
maxHeightParam,
scaleVal);
}
return string.Empty;
@ -2858,18 +2904,21 @@ namespace MediaBrowser.Controller.MediaEncoding
"yadif_cuda={0}:-1:0",
doubleRateDeint ? "1" : "0");
}
else if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
if (hwDeintSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase))
{
return string.Format(
CultureInfo.InvariantCulture,
"deinterlace_vaapi=rate={0}",
doubleRateDeint ? "field" : "frame");
}
else if (hwDeintSuffix.Contains("qsv", StringComparison.OrdinalIgnoreCase))
if (hwDeintSuffix.Contains("qsv", StringComparison.OrdinalIgnoreCase))
{
return "deinterlace_qsv=mode=2";
}
else if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
if (hwDeintSuffix.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase))
{
return string.Format(
CultureInfo.InvariantCulture,
@ -2900,7 +2949,8 @@ namespace MediaBrowser.Controller.MediaEncoding
options.VppTonemappingBrightness,
options.VppTonemappingContrast);
}
else if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase))
if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase))
{
args = "libplacebo=format={1}:tonemapping={2}:color_primaries=bt709:color_trc=bt709:colorspace=bt709:peak_detect=0:upscaler=none:downscaler=none";
@ -4214,7 +4264,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// sw => hw
if (doVkTonemap)
{
mainFilters.Add("hwupload_vaapi");
mainFilters.Add("hwupload=derive_device=vaapi");
mainFilters.Add("format=vaapi");
mainFilters.Add("hwmap=derive_device=vulkan");
mainFilters.Add("format=vulkan");
}
@ -4325,12 +4376,15 @@ namespace MediaBrowser.Controller.MediaEncoding
// prefer vaapi hwupload to vulkan hwupload,
// Mesa RADV does not support a dedicated transfer queue.
subFilters.Add("hwupload_vaapi");
subFilters.Add("hwupload=derive_device=vaapi");
subFilters.Add("format=vaapi");
subFilters.Add("hwmap=derive_device=vulkan");
subFilters.Add("format=vulkan");
overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0");
overlayFilters.Add("scale_vulkan=format=nv12");
// TODO: figure out why libplacebo can sync without vaSyncSurface VPP support in radeonsi.
overlayFilters.Add("libplacebo=format=nv12:apply_filmgrain=0:apply_dolbyvision=0:upscaler=none:downscaler=none:dithering=none");
// OUTPUT vaapi(nv12/bgra) surface(vram)
// reverse-mapping via vaapi-vulkan interop.
@ -4772,26 +4826,27 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return videoStream.BitDepth.Value;
}
else if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase))
if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase))
{
return 8;
}
else if (string.Equals(videoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase))
if (string.Equals(videoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase))
{
return 10;
}
else if (string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase))
if (string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase))
{
return 12;
}
else
{
return 8;
}
return 8;
}
return 0;
@ -5023,11 +5078,9 @@ namespace MediaBrowser.Controller.MediaEncoding
return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty)
+ (nvdecNoInternalCopy ? " -hwaccel_flags +unsafe_output" : string.Empty) + " -threads 1" + (isAv1 ? " -c:v av1" : string.Empty);
}
else
{
// cuvid decoder doesn't have threading issue.
return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty);
}
// cuvid decoder doesn't have threading issue.
return " -hwaccel cuda" + (outputHwSurface ? " -hwaccel_output_format cuda" : string.Empty);
}
}
@ -5385,7 +5438,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// Automatically set thread count
return mustSetThreadCount ? Math.Max(Environment.ProcessorCount - 1, 1) : 0;
}
else if (threads >= Environment.ProcessorCount)
if (threads >= Environment.ProcessorCount)
{
return Environment.ProcessorCount;
}
@ -5670,14 +5724,22 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var inputChannels = audioStream is null ? 6 : audioStream.Channels ?? 6;
var shiftAudioCodecs = new List<string>();
if (inputChannels >= 6)
{
return;
// DTS and TrueHD are not supported by HLS
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
shiftAudioCodecs.Add("dca");
shiftAudioCodecs.Add("truehd");
}
else
{
// Transcoding to 2ch ac3 or eac3 almost always causes a playback failure
// Keep them in the supported codecs list, but shift them to the end of the list so that if transcoding happens, another codec is used
shiftAudioCodecs.Add("ac3");
shiftAudioCodecs.Add("eac3");
}
// Transcoding to 2ch ac3 almost always causes a playback failure
// Keep it in the supported codecs list, but shift it to the end of the list so that if transcoding happens, another codec is used
var shiftAudioCodecs = new[] { "ac3", "eac3" };
if (audioCodecs.All(i => shiftAudioCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return;
@ -5919,10 +5981,17 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var bitrate = state.OutputAudioBitrate;
if (bitrate.HasValue)
if (bitrate.HasValue && !LosslessAudioCodecs.Contains(codec, StringComparison.OrdinalIgnoreCase))
{
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
var vbrParam = GetAudioVbrModeParam(codec, bitrate.Value / (channels ?? 2));
if (encodingOptions.EnableAudioVbr && vbrParam is not null)
{
args += vbrParam;
}
else
{
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
}
}
if (state.OutputAudioSampleRate.HasValue)
@ -5940,23 +6009,33 @@ namespace MediaBrowser.Controller.MediaEncoding
var audioTranscodeParams = new List<string>();
var bitrate = state.OutputAudioBitrate;
var channels = state.OutputAudioChannels;
var outputCodec = state.OutputAudioCodec;
if (bitrate.HasValue)
if (bitrate.HasValue && !LosslessAudioCodecs.Contains(outputCodec, StringComparison.OrdinalIgnoreCase))
{
audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
var vbrParam = GetAudioVbrModeParam(GetAudioEncoder(state), bitrate.Value / (channels ?? 2));
if (encodingOptions.EnableAudioVbr && vbrParam is not null)
{
audioTranscodeParams.Add(vbrParam);
}
else
{
audioTranscodeParams.Add("-ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture));
}
}
if (state.OutputAudioChannels.HasValue)
if (channels.HasValue)
{
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
}
if (!string.IsNullOrEmpty(state.OutputAudioCodec))
if (!string.IsNullOrEmpty(outputCodec))
{
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
}
if (!string.Equals(state.OutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase))
if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
{
// opus only supports specific sampling rates
var sampleRate = state.OutputAudioSampleRate;

@ -56,5 +56,20 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="newIndex">The new index.</param>
/// <returns>Task.</returns>
Task MoveItemAsync(string playlistId, string entryId, int newIndex);
/// <summary>
/// Removed all playlists of a user.
/// If the playlist is shared, ownership is transferred.
/// </summary>
/// <param name="userId">The user id.</param>
/// <returns>Task.</returns>
Task RemovePlaylistsAsync(Guid userId);
/// <summary>
/// Updates a playlist.
/// </summary>
/// <param name="playlist">The updated playlist.</param>
/// <returns>Task.</returns>
Task UpdatePlaylistAsync(Playlist playlist);
}
}

@ -15,6 +15,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Controller.Playlists
@ -232,7 +233,8 @@ namespace MediaBrowser.Controller.Playlists
return base.IsVisible(user);
}
if (user.Id.Equals(OwnerUserId))
var userId = user.Id;
if (userId.Equals(OwnerUserId))
{
return true;
}
@ -240,10 +242,9 @@ namespace MediaBrowser.Controller.Playlists
var shares = Shares;
if (shares.Length == 0)
{
return base.IsVisible(user);
return false;
}
var userId = user.Id;
return shares.Any(share => Guid.TryParse(share.UserId, out var id) && id.Equals(userId));
}

@ -1,6 +1,7 @@
#pragma warning disable CA1819, CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Model.Entities;
@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.Providers
public bool ReplaceAllImages { get; set; }
public ImageType[] ReplaceImages { get; set; }
public IReadOnlyList<ImageType> ReplaceImages { get; set; }
public bool IsAutomated { get; set; }

@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities.Security;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Session;

@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;

@ -533,11 +533,9 @@ namespace MediaBrowser.Controller.SyncPlay.GroupStates
_logger.LogWarning("Session {SessionId} is seeking to wrong position, correcting.", session.Id);
return;
}
else
{
// Session is ready.
context.SetBuffering(session, false);
}
// Session is ready.
context.SetBuffering(session, false);
if (!context.IsBuffering())
{

@ -313,17 +313,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
return true;
}
else
{
// Restoring playing item.
SetPlayingItemByPlaylistId(playingItem.PlaylistItemId);
return false;
}
}
else
{
// Restoring playing item.
SetPlayingItemByPlaylistId(playingItem.PlaylistItemId);
return false;
}
return false;
}
/// <summary>
@ -528,10 +524,8 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
return _shuffledPlaylist;
}
else
{
return _sortedPlaylist;
}
return _sortedPlaylist;
}
/// <summary>
@ -544,14 +538,13 @@ namespace MediaBrowser.Controller.SyncPlay.Queue
{
return null;
}
else if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
if (ShuffleMode.Equals(GroupShuffleMode.Shuffle))
{
return _shuffledPlaylist[PlayingItemIndex];
}
else
{
return _sortedPlaylist[PlayingItemIndex];
}
return _sortedPlaylist[PlayingItemIndex];
}
}
}

@ -6,8 +6,10 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Xml;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
@ -71,10 +73,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
foreach (var info in idInfos)
{
var id = info.Key + "Id";
if (!_validProviderIds.ContainsKey(id))
{
_validProviderIds.Add(id, info.Key);
}
_validProviderIds.TryAdd(id, info.Key);
}
// Additional Mappings
@ -370,7 +369,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "Director":
{
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Director }))
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@ -385,7 +384,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "Writer":
{
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Writer }))
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@ -412,7 +411,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
else
{
// Old-style piped string
foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.Actor }))
foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Actor }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@ -428,7 +427,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "GuestStars":
{
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonType.GuestStar }))
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
@ -636,6 +635,21 @@ namespace MediaBrowser.LocalMetadata.Parsers
break;
}
case "OwnerUserId":
{
var val = reader.ReadElementContentAsString();
if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty))
{
if (item is Playlist playlist)
{
playlist.OwnerUserId = guid;
}
}
break;
}
case "Format3D":
{
var val = reader.ReadElementContentAsString();
@ -1035,7 +1049,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader)
{
var name = string.Empty;
var type = PersonType.Actor; // If type is not specified assume actor
var type = PersonKind.Actor; // If type is not specified assume actor
var role = string.Empty;
int? sortOrder = null;
@ -1056,11 +1070,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "Type":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
type = val;
}
_ = Enum.TryParse(val, true, out type);
break;
}

@ -374,7 +374,7 @@ namespace MediaBrowser.LocalMetadata.Savers
{
await writer.WriteStartElementAsync(null, "Person", null).ConfigureAwait(false);
await writer.WriteElementStringAsync(null, "Name", null, person.Name).ConfigureAwait(false);
await writer.WriteElementStringAsync(null, "Type", null, person.Type).ConfigureAwait(false);
await writer.WriteElementStringAsync(null, "Type", null, person.Type.ToString()).ConfigureAwait(false);
await writer.WriteElementStringAsync(null, "Role", null, person.Role).ConfigureAwait(false);
if (person.SortOrder.HasValue)
@ -395,6 +395,7 @@ namespace MediaBrowser.LocalMetadata.Savers
if (item is Playlist playlist && !Playlist.IsPlaylistFile(playlist.Path))
{
await writer.WriteElementStringAsync(null, "OwnerUserId", null, playlist.OwnerUserId.ToString("N")).ConfigureAwait(false);
await AddLinkedChildren(playlist, writer, "PlaylistItems", "PlaylistItem").ConfigureAwait(false);
}
@ -418,16 +419,19 @@ namespace MediaBrowser.LocalMetadata.Savers
foreach (var share in item.Shares)
{
await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false);
if (share.UserId is not null)
{
await writer.WriteStartElementAsync(null, "Share", null).ConfigureAwait(false);
await writer.WriteElementStringAsync(null, "UserId", null, share.UserId).ConfigureAwait(false);
await writer.WriteElementStringAsync(
null,
"CanEdit",
null,
share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false);
await writer.WriteElementStringAsync(null, "UserId", null, share.UserId).ConfigureAwait(false);
await writer.WriteElementStringAsync(
null,
"CanEdit",
null,
share.CanEdit.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()).ConfigureAwait(false);
await writer.WriteEndElementAsync().ConfigureAwait(false);
await writer.WriteEndElementAsync().ConfigureAwait(false);
}
}
await writer.WriteEndElementAsync().ConfigureAwait(false);

@ -231,10 +231,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
throw new InvalidOperationException(
string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath));
}
else
{
_logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath);
}
_logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
}
private async Task<Stream> GetAttachmentStream(
@ -376,10 +374,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
throw new InvalidOperationException(
string.Format(CultureInfo.InvariantCulture, "ffmpeg attachment extraction failed for {0} to {1}", inputPath, outputPath));
}
else
{
_logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath);
}
_logger.LogInformation("ffmpeg attachment extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
}
private string GetAttachmentCachePath(string mediaPath, MediaSourceInfo mediaSource, int attachmentStreamIndex)

@ -1,4 +1,3 @@
using System;
using System.IO;
using System.Linq;
using BDInfo.IO;
@ -105,7 +104,7 @@ public class BdInfoDirectoryInfo : IDirectoryInfo
_impl.FullName,
new[] { searchPattern },
false,
(searchOption & SearchOption.AllDirectories) == SearchOption.AllDirectories)
searchOption == SearchOption.AllDirectories)
.Select(x => new BdInfoFileInfo(x))
.ToArray();
}

@ -25,11 +25,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
"mpeg2video",
"mpeg4",
"msmpeg4",
"dts",
"dca",
"ac3",
"aac",
"mp3",
"flac",
"truehd",
"h264_qsv",
"hevc_qsv",
"mpeg2_qsv",
@ -59,10 +60,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
"aac_at",
"libfdk_aac",
"ac3",
"dca",
"libmp3lame",
"libopus",
"libvorbis",
"flac",
"truehd",
"srt",
"h264_amf",
"hevc_amf",
@ -214,12 +217,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
return false;
}
else if (version < MinVersion) // Version is below what we recommend
if (version < MinVersion) // Version is below what we recommend
{
_logger.LogWarning("FFmpeg validation: The minimum recommended version is {MinVersion}", MinVersion);
return false;
}
else if (MaxVersion is not null && version > MaxVersion) // Version is above what we recommend
if (MaxVersion is not null && version > MaxVersion) // Version is above what we recommend
{
_logger.LogWarning("FFmpeg validation: The maximum recommended version is {MaxVersion}", MaxVersion);
return false;
@ -488,7 +493,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
var found = Regex
.Matches(output, @"^\s\S{6}\s(?<codec>[\w|-]+)\s+.+$", RegexOptions.Multiline)
.Cast<Match>()
.Select(x => x.Groups["codec"].Value)
.Where(x => required.Contains(x));
@ -517,7 +521,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
var found = Regex
.Matches(output, @"^\s\S{3}\s(?<filter>[\w|-]+)\s+.+$", RegexOptions.Multiline)
.Cast<Match>()
.Select(x => x.Groups["filter"].Value)
.Where(x => _requiredFilters.Contains(x));

@ -9,6 +9,7 @@ using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@ -67,6 +68,9 @@ namespace MediaBrowser.MediaEncoding.Probing
"諭吉佳作/men",
"//dARTH nULL",
"Phantom/Ghost",
"She/Her/Hers",
"5/8erl in Ehr'n",
"Smith/Kotzen",
};
public MediaInfo GetMediaInfo(InternalMediaInfoResult data, VideoType? videoType, bool isAudio, string path, MediaProtocol protocol)
@ -507,7 +511,7 @@ namespace MediaBrowser.MediaEncoding.Probing
peoples.Add(new BaseItemPerson
{
Name = pair.Value,
Type = PersonType.Writer
Type = PersonKind.Writer
});
}
}
@ -518,7 +522,7 @@ namespace MediaBrowser.MediaEncoding.Probing
peoples.Add(new BaseItemPerson
{
Name = pair.Value,
Type = PersonType.Producer
Type = PersonKind.Producer
});
}
}
@ -529,7 +533,7 @@ namespace MediaBrowser.MediaEncoding.Probing
peoples.Add(new BaseItemPerson
{
Name = pair.Value,
Type = PersonType.Director
Type = PersonKind.Director
});
}
}
@ -1163,7 +1167,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(composer, false))
{
people.Add(new BaseItemPerson { Name = person, Type = PersonType.Composer });
people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Composer });
}
}
@ -1171,7 +1175,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(conductor, false))
{
people.Add(new BaseItemPerson { Name = person, Type = PersonType.Conductor });
people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Conductor });
}
}
@ -1179,7 +1183,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(lyricist, false))
{
people.Add(new BaseItemPerson { Name = person, Type = PersonType.Lyricist });
people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Lyricist });
}
}
@ -1195,7 +1199,7 @@ namespace MediaBrowser.MediaEncoding.Probing
people.Add(new BaseItemPerson
{
Name = match.Groups["name"].Value,
Type = PersonType.Actor,
Type = PersonKind.Actor,
Role = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(match.Groups["instrument"].Value)
});
}
@ -1207,7 +1211,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(writer, false))
{
people.Add(new BaseItemPerson { Name = person, Type = PersonType.Writer });
people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Writer });
}
}
@ -1215,7 +1219,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(arranger, false))
{
people.Add(new BaseItemPerson { Name = person, Type = PersonType.Arranger });
people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Arranger });
}
}
@ -1223,7 +1227,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(engineer, false))
{
people.Add(new BaseItemPerson { Name = person, Type = PersonType.Engineer });
people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Engineer });
}
}
@ -1231,7 +1235,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(mixer, false))
{
people.Add(new BaseItemPerson { Name = person, Type = PersonType.Mixer });
people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Mixer });
}
}
@ -1239,7 +1243,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
foreach (var person in Split(remixer, false))
{
people.Add(new BaseItemPerson { Name = person, Type = PersonType.Remixer });
people.Add(new BaseItemPerson { Name = person, Type = PersonKind.Remixer });
}
}
@ -1491,7 +1495,7 @@ namespace MediaBrowser.MediaEncoding.Probing
{
video.People = people.Split(new[] { ';', '/' }, StringSplitOptions.RemoveEmptyEntries)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonType.Actor })
.Select(i => new BaseItemPerson { Name = i.Trim(), Type = PersonKind.Actor })
.ToArray();
}

@ -449,7 +449,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
{
try
{
_logger.LogInformation("Deleting converted subtitle due to failure: ", outputPath);
_logger.LogInformation("Deleting converted subtitle due to failure: {Path}", outputPath);
_fileSystem.DeleteFile(outputPath);
}
catch (IOException ex)
@ -624,10 +624,8 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw new FfmpegException(
string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0} to {1}", inputPath, outputPath));
}
else
{
_logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
}
_logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
if (string.Equals(outputCodec, "ass", StringComparison.OrdinalIgnoreCase))
{

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save