Merge branch 'master' into mbaff-interlace-detection

This commit is contained in:
Orry Verducci 2021-12-01 22:13:52 +00:00 committed by GitHub
commit e446e9fde9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
292 changed files with 4089 additions and 3487 deletions

View File

@ -34,7 +34,6 @@ jobs:
inputs: inputs:
packageType: sdk packageType: sdk
version: ${{ parameters.DotNetSdkVersion }} version: ${{ parameters.DotNetSdkVersion }}
includePreviewVersions: true
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Install ABI CompatibilityChecker Tool' displayName: 'Install ABI CompatibilityChecker Tool'

View File

@ -54,7 +54,6 @@ jobs:
inputs: inputs:
packageType: sdk packageType: sdk
version: ${{ parameters.DotNetSdkVersion }} version: ${{ parameters.DotNetSdkVersion }}
includePreviewVersions: true
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Publish Server' displayName: 'Publish Server'

View File

@ -181,7 +181,7 @@ jobs:
inputs: inputs:
sshEndpoint: repository sshEndpoint: repository
runOptions: 'commands' runOptions: 'commands'
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) & commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
- job: PublishNuget - job: PublishNuget
displayName: 'Publish NuGet packages' displayName: 'Publish NuGet packages'
@ -199,7 +199,6 @@ jobs:
inputs: inputs:
packageType: 'sdk' packageType: 'sdk'
version: '6.0.x' version: '6.0.x'
includePreviewVersions: true
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Build Stable Nuget packages' displayName: 'Build Stable Nuget packages'

View File

@ -41,7 +41,6 @@ jobs:
inputs: inputs:
packageType: sdk packageType: sdk
version: ${{ parameters.DotNetSdkVersion }} version: ${{ parameters.DotNetSdkVersion }}
includePreviewVersions: true
- task: SonarCloudPrepare@1 - task: SonarCloudPrepare@1
displayName: 'Prepare analysis on SonarCloud' displayName: 'Prepare analysis on SonarCloud'

View File

@ -1,51 +0,0 @@
---
name: Bug report
about: Create a bug report
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**System (please complete the following information):**
- OS: [e.g. Debian, Windows]
- Virtualization: [e.g. Docker, KVM, LXC]
- Clients: [Browser, Android, Fire Stick, etc.]
- Browser: [e.g. Firefox 91, Chrome 93, Safari 13]
- Jellyfin Version: [e.g. 10.7.6, unstable 20191231]
- FFmpeg Version: [e.g. 4.3.2-Jellyfin]
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
- Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
- Base URL: [e.g. none, yes: /example]
- Networking: [e.g. Host, Bridge/NAT]
- Storage: [e.g. local, NFS, cloud]
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Server Logs**
<!-- Please paste any log errors. -->
**FFmpeg Logs**
<!-- Please paste any log errors. -->
**Browser Console Logs**
<!-- Please paste any log errors. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Additional context**
<!-- Add any other context about the problem here. -->

106
.github/ISSUE_TEMPLATE/issue report.yml vendored Normal file
View File

@ -0,0 +1,106 @@
name: Issue Report
description: File an issue report
title: "[Issue]: "
labels: [bug, triage]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
- type: textarea
id: what-happened
attributes:
label: Please describe your bug
description: Also tell us, what did you expect to happen?
placeholder: |
The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
This is my issue.
Steps to Reproduce
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: true
- type: dropdown
id: version
attributes:
label: Jellyfin Version
description: What version of Jellyfin are you running?
options:
- 10.7.7
- 10.7.z
- 10.6.4
- Other
validations:
required: true
- type: input
id: version-other
attributes:
label: "if other:"
placeholder: Other
- type: textarea
attributes:
label: Environment
description: |
Examples:
- **OS**: [e.g. Debian, Windows]
- **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]
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example]
- **Networking**: [e.g. Host, Bridge/NAT]
- **Storage**: [e.g. local, NFS, cloud]
value: |
- OS:
- Virtualization:
- Clients:
- Browser:
- FFmpeg Version:
- Playback Method:
- Hardware Acceleration:
- Plugins:
- Reverse Proxy:
- Base URL:
- Networking:
- Storage:
render: markdown
- type: textarea
id: logs
attributes:
label: Jellyfin logs
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
render: shell
- type: textarea
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.
render: shell
- type: textarea
id: browserlogs
attributes:
label: Please attach any browser or client logs here
placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
- type: textarea
id: screenshots
attributes:
label: Please attach any screenshots here
placeholder: Images can be pasted directly into the textbox and will be hosted by github.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@ -25,7 +25,6 @@ jobs:
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: '6.0.x' dotnet-version: '6.0.x'
include-prerelease: true
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1

124
.github/workflows/openapi.yml vendored Normal file
View File

@ -0,0 +1,124 @@
name: OpenAPI
on:
push:
branches:
- master
pull_request_target:
jobs:
openapi-head:
name: OpenAPI - HEAD
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.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@v2
with:
name: openapi-head
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
openapi-base:
name: OpenAPI - BASE
if: ${{ github.base_ref != '' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
ref: ${{ github.base_ref }}
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.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@v2
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
openapi-diff:
name: OpenAPI - Difference
if: ${{ github.event_name == 'pull_request_target' }}
runs-on: ubuntu-latest
needs:
- openapi-head
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@v2
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@v2
with:
name: openapi-base
path: openapi-base
- name: Workaround openapi-diff issue
run: |
sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
- name: Calculate OpenAPI difference
uses: docker://openapitools/openapi-diff
continue-on-error: true
with:
args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
- id: read-diff
name: Read openapi-diff output
run: |
body=$(cat openapi-changes.md)
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
uses: peter-evans/find-comment@v1
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@v1.4.5
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
<!--openapi-diff-workflow-comment-->
<details>
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
${{ steps.read-diff.outputs.body }}
</details>
- name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@v1.4.5
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
<!--openapi-diff-workflow-comment-->
No changes to OpenAPI specification found. See history of this comment for previous changes.

View File

@ -3,10 +3,13 @@
<PropertyGroup> <PropertyGroup>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<AnalysisMode>AllEnabledByDefault</AnalysisMode> <AnalysisMode>AllEnabledByDefault</AnalysisMode>
</PropertyGroup> </PropertyGroup>

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
namespace Emby.Dlna.ContentDirectory namespace Emby.Dlna.ContentDirectory
@ -13,24 +11,29 @@ namespace Emby.Dlna.ContentDirectory
/// Initializes a new instance of the <see cref="ServerItem"/> class. /// Initializes a new instance of the <see cref="ServerItem"/> class.
/// </summary> /// </summary>
/// <param name="item">The <see cref="BaseItem"/>.</param> /// <param name="item">The <see cref="BaseItem"/>.</param>
public ServerItem(BaseItem item) /// <param name="stubType">The stub type.</param>
public ServerItem(BaseItem item, StubType? stubType)
{ {
Item = item; Item = item;
if (item is IItemByName && item is not Folder) if (stubType.HasValue)
{
StubType = stubType;
}
else if (item is IItemByName and not Folder)
{ {
StubType = Dlna.ContentDirectory.StubType.Folder; StubType = Dlna.ContentDirectory.StubType.Folder;
} }
} }
/// <summary> /// <summary>
/// Gets or sets the underlying base item. /// Gets the underlying base item.
/// </summary> /// </summary>
public BaseItem Item { get; set; } public BaseItem Item { get; }
/// <summary> /// <summary>
/// Gets or sets the DLNA item type. /// Gets the DLNA item type.
/// </summary> /// </summary>
public StubType? StubType { get; set; } public StubType? StubType { get; }
} }
} }

View File

@ -729,7 +729,7 @@ namespace Emby.Dlna.Didl
{ {
if (item.PremiereDate.HasValue) if (item.PremiereDate.HasValue)
{ {
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc); AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc);
} }
} }

View File

@ -112,7 +112,7 @@ namespace Emby.Dlna
if (profile == null) if (profile == null)
{ {
LogUnmatchedProfile(deviceInfo); _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
} }
else else
{ {
@ -122,23 +122,6 @@ namespace Emby.Dlna
return profile; return profile;
} }
private void LogUnmatchedProfile(DeviceIdentification profile)
{
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
builder.Append("ModelName: ").AppendLine(profile.ModelName);
builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
_logger.LogInformation(builder.ToString());
}
/// <summary> /// <summary>
/// Attempts to match a device with a profile. /// Attempts to match a device with a profile.
/// Rules: /// Rules:
@ -367,7 +350,7 @@ namespace Emby.Dlna
Directory.CreateDirectory(systemProfilesPath); Directory.CreateDirectory(systemProfilesPath);
var fileOptions = AsyncFile.WriteOptions; var fileOptions = AsyncFile.WriteOptions;
fileOptions.Mode = FileMode.CreateNew; fileOptions.Mode = FileMode.Create;
fileOptions.PreallocationSize = length; fileOptions.PreallocationSize = length;
using (var fileStream = new FileStream(path, fileOptions)) using (var fileStream = new FileStream(path, fileOptions))
{ {
@ -416,7 +399,7 @@ namespace Emby.Dlna
} }
/// <inheritdoc /> /// <inheritdoc />
public void UpdateProfile(DeviceProfile profile) public void UpdateProfile(string profileId, DeviceProfile profile)
{ {
profile = ReserializeProfile(profile); profile = ReserializeProfile(profile);
@ -430,7 +413,7 @@ namespace Emby.Dlna
throw new ArgumentException("Profile is missing Name"); throw new ArgumentException("Profile is missing Name");
} }
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase)); var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml"; var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
var path = Path.Combine(UserProfilesPath, newFilename); var path = Path.Combine(UserProfilesPath, newFilename);

View File

@ -72,7 +72,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" /> <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -52,7 +52,6 @@ namespace Emby.Dlna.Main
private readonly ISocketFactory _socketFactory; private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object(); private readonly object _syncLock = new object();
private readonly NetworkConfiguration _netConfig;
private readonly bool _disabled; private readonly bool _disabled;
private PlayToManager _manager; private PlayToManager _manager;
@ -125,8 +124,8 @@ namespace Emby.Dlna.Main
config); config);
Current = this; Current = this;
_netConfig = config.GetConfiguration<NetworkConfiguration>("network"); var netConfig = config.GetConfiguration<NetworkConfiguration>("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps; _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
if (_disabled && _config.GetDlnaConfiguration().EnableServer) if (_disabled && _config.GetDlnaConfiguration().EnableServer)
{ {
@ -219,11 +218,6 @@ namespace Emby.Dlna.Main
} }
} }
private void LogMessage(string msg)
{
_logger.LogDebug(msg);
}
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer) private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
{ {
try try
@ -268,12 +262,11 @@ namespace Emby.Dlna.Main
{ {
_publisher = new SsdpDevicePublisher( _publisher = new SsdpDevicePublisher(
_communicationsServer, _communicationsServer,
_networkManager,
MediaBrowser.Common.System.OperatingSystem.Name, MediaBrowser.Common.System.OperatingSystem.Name,
Environment.OSVersion.VersionString, Environment.OSVersion.VersionString,
_config.GetDlnaConfiguration().SendOnlyMatchedHost) _config.GetDlnaConfiguration().SendOnlyMatchedHost)
{ {
LogFunction = LogMessage, LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
SupportPnpRootDevice = false SupportPnpRootDevice = false
}; };
@ -318,15 +311,9 @@ namespace Emby.Dlna.Main
var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri); var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri);
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
{
// DLNA will only work over http, so we must reset to http:// : {port}.
uri.Scheme = "http";
uri.Port = _netConfig.HttpServerPortNumber;
}
var device = new SsdpRootDevice var device = new SsdpRootDevice
{ {
@ -412,7 +399,6 @@ namespace Emby.Dlna.Main
_imageProcessor, _imageProcessor,
_deviceDiscovery, _deviceDiscovery,
_httpClientFactory, _httpClientFactory,
_config,
_userDataManager, _userDataManager,
_localization, _localization,
_mediaSourceManager, _mediaSourceManager,

View File

@ -11,7 +11,6 @@ using System.Threading.Tasks;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -35,7 +34,6 @@ namespace Emby.Dlna.PlayTo
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
private readonly IUserDataManager _userDataManager; private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
@ -47,7 +45,7 @@ namespace Emby.Dlna.PlayTo
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1); private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{ {
_logger = logger; _logger = logger;
_sessionManager = sessionManager; _sessionManager = sessionManager;
@ -58,7 +56,6 @@ namespace Emby.Dlna.PlayTo
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_deviceDiscovery = deviceDiscovery; _deviceDiscovery = deviceDiscovery;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_config = config;
_userDataManager = userDataManager; _userDataManager = userDataManager;
_localization = localization; _localization = localization;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;

View File

@ -64,7 +64,7 @@ namespace Emby.Dlna.Service
requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false); requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
} }
Logger.LogDebug("Received control request {0}", requestInfo.LocalName); Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
var settings = new XmlWriterSettings var settings = new XmlWriterSettings
{ {

View File

@ -26,7 +26,7 @@ namespace Emby.Drawing
public sealed class ImageProcessor : IImageProcessor, IDisposable public sealed class ImageProcessor : IImageProcessor, IDisposable
{ {
// Increment this when there's a change requiring caches to be invalidated // Increment this when there's a change requiring caches to be invalidated
private const string Version = "3"; private const char Version = '3';
private static readonly HashSet<string> _transparentImageTypes private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };

View File

@ -1,3 +1,5 @@
#pragma warning disable CA1819
using System; using System;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -253,6 +255,8 @@ namespace Emby.Naming.Common
}, },
// <!-- foo.ep01, foo.EP_01 --> // <!-- foo.ep01, foo.EP_01 -->
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
// <!-- foo.E01., foo.e01. -->
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true) new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
{ {
DateTimeFormats = new[] DateTimeFormats = new[]
@ -371,6 +375,20 @@ namespace Emby.Naming.Common
IsOptimistic = true, IsOptimistic = true,
IsNamed = true IsNamed = true
}, },
// Series and season only expression
// "the show/season 1", "the show/s01"
new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)\/[Ss](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
{
IsNamed = true
},
// Series and season only expression
// "the show S01", "the show season 1"
new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)[\. _\-]+[sS](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
{
IsNamed = true
},
}; };
EpisodeWithoutSeasonExpressions = new[] EpisodeWithoutSeasonExpressions = new[]

View File

@ -38,7 +38,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<!-- Code Analyzers--> <!-- Code Analyzers-->

View File

@ -0,0 +1,29 @@
namespace Emby.Naming.TV
{
/// <summary>
/// Holder object for Series information.
/// </summary>
public class SeriesInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="SeriesInfo"/> class.
/// </summary>
/// <param name="path">Path to the file.</param>
public SeriesInfo(string path)
{
Path = path;
}
/// <summary>
/// Gets or sets the path.
/// </summary>
/// <value>The path.</value>
public string Path { get; set; }
/// <summary>
/// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
public string? Name { get; set; }
}
}

View File

@ -0,0 +1,61 @@
using System.Globalization;
using Emby.Naming.Common;
namespace Emby.Naming.TV
{
/// <summary>
/// Used to parse information about series from paths containing more information that only the series name.
/// Uses the same regular expressions as the EpisodePathParser but have different success criteria.
/// </summary>
public static class SeriesPathParser
{
/// <summary>
/// Parses information about series from path.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
/// <param name="path">Path.</param>
/// <returns>Returns <see cref="SeriesPathParserResult"/> object.</returns>
public static SeriesPathParserResult Parse(NamingOptions options, string path)
{
SeriesPathParserResult? result = null;
foreach (var expression in options.EpisodeExpressions)
{
var currentResult = Parse(path, expression);
if (currentResult.Success)
{
result = currentResult;
break;
}
}
if (result != null)
{
if (!string.IsNullOrEmpty(result.SeriesName))
{
result.SeriesName = result.SeriesName.Trim(' ', '_', '.', '-');
}
}
return result ?? new SeriesPathParserResult();
}
private static SeriesPathParserResult Parse(string name, EpisodeExpression expression)
{
var result = new SeriesPathParserResult();
var match = expression.Regex.Match(name);
if (match.Success && match.Groups.Count >= 3)
{
if (expression.IsNamed)
{
result.SeriesName = match.Groups["seriesname"].Value;
result.Success = !string.IsNullOrEmpty(result.SeriesName) && !string.IsNullOrEmpty(match.Groups["seasonnumber"]?.Value);
}
}
return result;
}
}
}

View File

@ -0,0 +1,19 @@
namespace Emby.Naming.TV
{
/// <summary>
/// Holder object for <see cref="SeriesPathParser"/> result.
/// </summary>
public class SeriesPathParserResult
{
/// <summary>
/// Gets or sets the name of the series.
/// </summary>
/// <value>The name of the series.</value>
public string? SeriesName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether parsing was successful.
/// </summary>
public bool Success { get; set; }
}
}

View File

@ -0,0 +1,49 @@
using System.IO;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
namespace Emby.Naming.TV
{
/// <summary>
/// Used to resolve information about series from path.
/// </summary>
public static class SeriesResolver
{
/// <summary>
/// Regex that matches strings of at least 2 characters separated by a dot or underscore.
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
/// preserving namings like "S.H.O.W".
/// </summary>
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
/// <summary>
/// Resolve information about series from path.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object passed to <see cref="SeriesPathParser"/>.</param>
/// <param name="path">Path to series.</param>
/// <returns>SeriesInfo.</returns>
public static SeriesInfo Resolve(NamingOptions options, string path)
{
string seriesName = Path.GetFileName(path);
SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
if (result.Success)
{
if (!string.IsNullOrEmpty(result.SeriesName))
{
seriesName = result.SeriesName;
}
}
if (!string.IsNullOrEmpty(seriesName))
{
seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
}
return new SeriesInfo(path)
{
Name = seriesName
};
}
}
}

View File

@ -3,6 +3,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
@ -18,6 +19,7 @@ using Emby.Dlna;
using Emby.Dlna.Main; using Emby.Dlna.Main;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using Emby.Drawing; using Emby.Drawing;
using Emby.Naming.Common;
using Emby.Notifications; using Emby.Notifications;
using Emby.Photos; using Emby.Photos;
using Emby.Server.Implementations.Archiving; using Emby.Server.Implementations.Archiving;
@ -56,6 +58,7 @@ using MediaBrowser.Common.Updates;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
@ -117,7 +120,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// The disposable parts. /// The disposable parts.
/// </summary> /// </summary>
private readonly List<IDisposable> _disposableParts = new List<IDisposable>(); private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new ();
private readonly IFileSystem _fileSystemManager; private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig; private readonly IConfiguration _startupConfig;
@ -128,7 +131,6 @@ namespace Emby.Server.Implementations
private List<Type> _creatingInstances; private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder; private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager; private ISessionManager _sessionManager;
private string[] _urlPrefixes;
/// <summary> /// <summary>
/// Gets or sets all concrete types. /// Gets or sets all concrete types.
@ -147,25 +149,20 @@ namespace Emby.Server.Implementations
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param> /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param> /// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost( public ApplicationHost(
IServerApplicationPaths applicationPaths, IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IStartupOptions options, IStartupOptions options,
IConfiguration startupConfig, IConfiguration startupConfig)
IFileSystem fileSystem,
IServiceCollection serviceCollection)
{ {
ApplicationPaths = applicationPaths; ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
_startupOptions = options; _startupOptions = options;
_startupConfig = startupConfig; _startupConfig = startupConfig;
_fileSystemManager = fileSystem; _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
ServiceCollection = serviceCollection;
Logger = LoggerFactory.CreateLogger<ApplicationHost>(); Logger = LoggerFactory.CreateLogger<ApplicationHost>();
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3); ApplicationVersionString = ApplicationVersion.ToString(3);
@ -214,7 +211,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance. /// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary> /// </summary>
public INetworkManager NetManager { get; internal set; } public INetworkManager NetManager { get; private set; }
/// <summary> /// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart. /// Gets a value indicating whether this instance has changes that require the entire application to restart.
@ -230,24 +227,22 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
protected ILogger<ApplicationHost> Logger { get; } protected ILogger<ApplicationHost> Logger { get; }
protected IServiceCollection ServiceCollection { get; }
/// <summary> /// <summary>
/// Gets the logger factory. /// Gets the logger factory.
/// </summary> /// </summary>
protected ILoggerFactory LoggerFactory { get; } protected ILoggerFactory LoggerFactory { get; }
/// <summary> /// <summary>
/// Gets or sets the application paths. /// Gets the application paths.
/// </summary> /// </summary>
/// <value>The application paths.</value> /// <value>The application paths.</value>
protected IServerApplicationPaths ApplicationPaths { get; set; } protected IServerApplicationPaths ApplicationPaths { get; }
/// <summary> /// <summary>
/// Gets or sets the configuration manager. /// Gets the configuration manager.
/// </summary> /// </summary>
/// <value>The configuration manager.</value> /// <value>The configuration manager.</value>
public ServerConfigurationManager ConfigurationManager { get; set; } public ServerConfigurationManager ConfigurationManager { get; }
/// <summary> /// <summary>
/// Gets or sets the service provider. /// Gets or sets the service provider.
@ -350,22 +345,6 @@ namespace Emby.Server.Implementations
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase); .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
} }
/// <summary>
/// Creates an instance of type and resolves all constructor dependencies.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>System.Object.</returns>
public object CreateInstance(Type type)
=> ActivatorUtilities.CreateInstance(ServiceProvider, type);
/// <summary>
/// Creates an instance of type and resolves all constructor dependencies.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>T.</returns>
public T CreateInstance<T>()
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
/// <summary> /// <summary>
/// Creates the instance safe. /// Creates the instance safe.
/// </summary> /// </summary>
@ -375,7 +354,7 @@ namespace Emby.Server.Implementations
{ {
_creatingInstances ??= new List<Type>(); _creatingInstances ??= new List<Type>();
if (_creatingInstances.IndexOf(type) != -1) if (_creatingInstances.Contains(type))
{ {
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName); Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
foreach (var entry in _creatingInstances) foreach (var entry in _creatingInstances)
@ -385,7 +364,7 @@ namespace Emby.Server.Implementations
_pluginManager.FailPlugin(type.Assembly); _pluginManager.FailPlugin(type.Assembly);
throw new ExternalException("DI Loop detected."); throw new TypeLoadException("DI Loop detected");
} }
try try
@ -418,8 +397,15 @@ namespace Emby.Server.Implementations
public IEnumerable<Type> GetExportTypes<T>() public IEnumerable<Type> GetExportTypes<T>()
{ {
var currentType = typeof(T); var currentType = typeof(T);
var numberOfConcreteTypes = _allConcreteTypes.Length;
return _allConcreteTypes.Where(i => currentType.IsAssignableFrom(i)); for (var i = 0; i < numberOfConcreteTypes; i++)
{
var type = _allConcreteTypes[i];
if (currentType.IsAssignableFrom(type))
{
yield return type;
}
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -434,9 +420,9 @@ namespace Emby.Server.Implementations
if (manageLifetime) if (manageLifetime)
{ {
lock (_disposableParts) foreach (var part in parts.OfType<IDisposable>())
{ {
_disposableParts.AddRange(parts.OfType<IDisposable>()); _disposableParts.TryAdd(part, byte.MinValue);
} }
} }
@ -455,9 +441,9 @@ namespace Emby.Server.Implementations
if (manageLifetime) if (manageLifetime)
{ {
lock (_disposableParts) foreach (var part in parts.OfType<IDisposable>())
{ {
_disposableParts.AddRange(parts.OfType<IDisposable>()); _disposableParts.TryAdd(part, byte.MinValue);
} }
} }
@ -521,7 +507,7 @@ namespace Emby.Server.Implementations
} }
/// <inheritdoc/> /// <inheritdoc/>
public void Init() public void Init(IServiceCollection serviceCollection)
{ {
DiscoverTypes(); DiscoverTypes();
@ -551,128 +537,130 @@ namespace Emby.Server.Implementations
CertificatePath = networkConfiguration.CertificatePath; CertificatePath = networkConfiguration.CertificatePath;
Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword); Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
RegisterServices(); RegisterServices(serviceCollection);
_pluginManager.RegisterServices(ServiceCollection); _pluginManager.RegisterServices(serviceCollection);
} }
/// <summary> /// <summary>
/// Registers services/resources with the service collection that will be available via DI. /// Registers services/resources with the service collection that will be available via DI.
/// </summary> /// </summary>
protected virtual void RegisterServices() /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
protected virtual void RegisterServices(IServiceCollection serviceCollection)
{ {
ServiceCollection.AddSingleton(_startupOptions); serviceCollection.AddSingleton(_startupOptions);
ServiceCollection.AddMemoryCache(); serviceCollection.AddMemoryCache();
ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager); serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager); serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this); serviceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager); serviceCollection.AddSingleton(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton(_fileSystemManager); serviceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TmdbClientManager>(); serviceCollection.AddSingleton<TmdbClientManager>();
ServiceCollection.AddSingleton(NetManager); serviceCollection.AddSingleton(NetManager);
ServiceCollection.AddSingleton<ITaskManager, TaskManager>(); serviceCollection.AddSingleton<ITaskManager, TaskManager>();
ServiceCollection.AddSingleton(_xmlSerializer); serviceCollection.AddSingleton(_xmlSerializer);
ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>(); serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>(); serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>(); serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>(); serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
ServiceCollection.AddSingleton<IZipClient, ZipClient>(); serviceCollection.AddSingleton<IZipClient, ZipClient>();
ServiceCollection.AddSingleton<IServerApplicationHost>(this); serviceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); serviceCollection.AddSingleton(ApplicationPaths);
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>(); serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>(); serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>(); serviceCollection.AddSingleton<EncodingHelper>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>)); serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>)); serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>(); serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
serviceCollection.AddSingleton<NamingOptions>();
ServiceCollection.AddSingleton<IMusicManager, MusicManager>(); serviceCollection.AddSingleton<IMusicManager, MusicManager>();
ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>(); serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>(); serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>(); serviceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>(); serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>(); serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>(); serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
ServiceCollection.AddSingleton<IProviderManager, ProviderManager>(); serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>)); serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
ServiceCollection.AddSingleton<IDtoService, DtoService>(); serviceCollection.AddSingleton<IDtoService, DtoService>();
ServiceCollection.AddSingleton<IChannelManager, ChannelManager>(); serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
ServiceCollection.AddSingleton<ISessionManager, SessionManager>(); serviceCollection.AddSingleton<ISessionManager, SessionManager>();
ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>(); serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>(); serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>(); serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
ServiceCollection.AddSingleton<LiveTvDtoService>(); serviceCollection.AddSingleton<LiveTvDtoService>();
ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>(); serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>(); serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
ServiceCollection.AddSingleton<INotificationManager, NotificationManager>(); serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
ServiceCollection.AddSingleton<IChapterManager, ChapterManager>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
ServiceCollection.AddScoped<ISessionContext, SessionContext>(); serviceCollection.AddScoped<ISessionContext, SessionContext>();
ServiceCollection.AddSingleton<IAuthService, AuthService>(); serviceCollection.AddSingleton<IAuthService, AuthService>();
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
ServiceCollection.AddSingleton<TranscodingJobHelper>(); serviceCollection.AddSingleton<TranscodingJobHelper>();
ServiceCollection.AddScoped<MediaInfoHelper>(); serviceCollection.AddScoped<MediaInfoHelper>();
ServiceCollection.AddScoped<AudioHelper>(); serviceCollection.AddScoped<AudioHelper>();
ServiceCollection.AddScoped<DynamicHlsHelper>(); serviceCollection.AddScoped<DynamicHlsHelper>();
serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>();
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>(); serviceCollection.AddSingleton<IDirectoryService, DirectoryService>();
} }
/// <summary> /// <summary>
@ -795,8 +783,6 @@ namespace Emby.Server.Implementations
_pluginManager.CreatePlugins(); _pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray();
Resolve<ILibraryManager>().AddParts( Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(), GetExports<IResolverIgnoreRule>(),
GetExports<IItemResolver>(), GetExports<IItemResolver>(),
@ -864,32 +850,12 @@ namespace Emby.Server.Implementations
} }
} }
private IEnumerable<string> GetUrlPrefixes()
{
var hosts = new[] { "+" };
return hosts.SelectMany(i =>
{
var prefixes = new List<string>
{
"http://" + i + ":" + HttpPort + "/"
};
if (Certificate != null)
{
prefixes.Add("https://" + i + ":" + HttpsPort + "/");
}
return prefixes;
});
}
/// <summary> /// <summary>
/// Called when [configuration updated]. /// Called when [configuration updated].
/// </summary> /// </summary>
/// <param name="sender">The sender.</param> /// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param> /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
protected void OnConfigurationUpdated(object sender, EventArgs e) private void OnConfigurationUpdated(object sender, EventArgs e)
{ {
var requiresRestart = false; var requiresRestart = false;
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
@ -898,8 +864,8 @@ namespace Emby.Server.Implementations
if (HttpPort != 0 && HttpsPort != 0) if (HttpPort != 0 && HttpsPort != 0)
{ {
// Need to restart if ports have changed // Need to restart if ports have changed
if (networkConfiguration.HttpServerPortNumber != HttpPort || if (networkConfiguration.HttpServerPortNumber != HttpPort
networkConfiguration.HttpsPortNumber != HttpsPort) || networkConfiguration.HttpsPortNumber != HttpsPort)
{ {
if (ConfigurationManager.Configuration.IsPortAuthorized) if (ConfigurationManager.Configuration.IsPortAuthorized)
{ {
@ -911,11 +877,6 @@ namespace Emby.Server.Implementations
} }
} }
if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
{
requiresRestart = true;
}
if (ValidateSslCertificate(networkConfiguration)) if (ValidateSslCertificate(networkConfiguration))
{ {
requiresRestart = true; requiresRestart = true;
@ -957,7 +918,7 @@ namespace Emby.Server.Implementations
} }
/// <summary> /// <summary>
/// Notifies that the kernel that a change has been made that requires a restart. /// Notifies the kernel that a change has been made that requires a restart.
/// </summary> /// </summary>
public void NotifyPendingRestart() public void NotifyPendingRestart()
{ {
@ -1098,11 +1059,6 @@ namespace Emby.Server.Implementations
}; };
} }
public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
=> NetManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i))
.ToList();
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{ {
return new PublicSystemInfo return new PublicSystemInfo
@ -1118,7 +1074,7 @@ namespace Emby.Server.Implementations
} }
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null) public string GetSmartApiUrl(IPAddress remoteAddr)
{ {
// Published server ends with a / // Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl)) if (!string.IsNullOrEmpty(PublishedServerUrl))
@ -1127,18 +1083,12 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(remoteAddr, out port); string smart = NetManager.GetBindInterface(remoteAddr, out var port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return smart.Trim('/');
}
return GetLocalApiUrl(smart.Trim('/'), null, port); return GetLocalApiUrl(smart.Trim('/'), null, port);
} }
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(HttpRequest request, int? port = null) public string GetSmartApiUrl(HttpRequest request)
{ {
// Return the host in the HTTP request as the API url // Return the host in the HTTP request as the API url
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
@ -1159,18 +1109,12 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(request, out port); string smart = NetManager.GetBindInterface(request, out var port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return smart.Trim('/');
}
return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port); return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
} }
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(string hostname, int? port = null) public string GetSmartApiUrl(string hostname)
{ {
// Published server ends with a / // Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl)) if (!string.IsNullOrEmpty(PublishedServerUrl))
@ -1179,31 +1123,29 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(hostname, out port); string smart = NetManager.GetBindInterface(hostname, out var port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return smart.Trim('/');
}
return GetLocalApiUrl(smart.Trim('/'), null, port); return GetLocalApiUrl(smart.Trim('/'), null, port);
} }
/// <inheritdoc/> /// <inheritdoc/>
public string GetLoopbackHttpApiUrl() public string GetApiUrlForLocalAccess(bool allowHttps = true)
{ {
if (NetManager.IsIP6Enabled) // With an empty source, the port will be null
{ string smart = NetManager.GetBindInterface(string.Empty, out _);
return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort); var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
} int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart.Trim('/'), scheme, port);
return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
} }
/// <inheritdoc/> /// <inheritdoc/>
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null) public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
{ {
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (hostname.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return hostname.TrimEnd('/');
}
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
// not. For consistency, always trim the trailing slash. // not. For consistency, always trim the trailing slash.
return new UriBuilder return new UriBuilder
@ -1277,12 +1219,15 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Disposing {Type}", type.Name); Logger.LogInformation("Disposing {Type}", type.Name);
var parts = _disposableParts.Distinct().Where(i => i.GetType() != type).ToList(); foreach (var (part, _) in _disposableParts)
_disposableParts.Clear();
foreach (var part in parts)
{ {
Logger.LogInformation("Disposing {Type}", part.GetType().Name); var partType = part.GetType();
if (partType == type)
{
continue;
}
Logger.LogInformation("Disposing {Type}", partType.Name);
try try
{ {
@ -1290,9 +1235,11 @@ namespace Emby.Server.Implementations
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name); Logger.LogError(ex, "Error disposing {Type}", partType.Name);
} }
} }
_disposableParts.Clear();
} }
_disposed = true; _disposed = true;

View File

@ -1075,14 +1075,6 @@ namespace Emby.Server.Implementations.Channels
forceUpdate = true; forceUpdate = true;
} }
// was used for status
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
// {
// item.ExternalEtag = info.Etag;
// forceUpdate = true;
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
// }
if (!internalChannelId.Equals(item.ChannelId)) if (!internalChannelId.Equals(item.ChannelId))
{ {
forceUpdate = true; forceUpdate = true;

View File

@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
using static MediaBrowser.Common.Cryptography.Constants; using static MediaBrowser.Model.Cryptography.Constants;
namespace Emby.Server.Implementations.Cryptography namespace Emby.Server.Implementations.Cryptography
{ {
@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography
/// </summary> /// </summary>
public class CryptographyProvider : ICryptoProvider public class CryptographyProvider : ICryptoProvider
{ {
// FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto // TODO: remove when not needed for backwards compat
// Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
// there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
// Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>() private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
{ {
"MD5", "MD5",
@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography
}; };
/// <inheritdoc /> /// <inheritdoc />
public string DefaultHashMethod => "PBKDF2"; public string DefaultHashMethod => "PBKDF2-SHA512";
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<string> GetSupportedHashMethods() public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
=> _supportedHashMethods;
private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
{ {
// downgrading for now as we need this library to be dotnetstandard compliant byte[] salt = GenerateSalt();
// with this downgrade we'll add a check to make sure we're on the downgrade method at the moment return new PasswordHash(
if (method != DefaultHashMethod) DefaultHashMethod,
Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
DefaultIterations,
HashAlgorithmName.SHA512,
DefaultOutputLength),
salt,
new Dictionary<string, string>
{ {
throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}"); { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
} });
using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
return r.GetBytes(32);
} }
/// <inheritdoc /> /// <inheritdoc />
public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) public bool Verify(PasswordHash hash, ReadOnlySpan<char> password)
{ {
if (hashMethod == DefaultHashMethod) if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
{ {
return PBKDF2(hashMethod, bytes, salt, DefaultIterations); return hash.Hash.SequenceEqual(
Rfc2898DeriveBytes.Pbkdf2(
password,
hash.Salt,
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
HashAlgorithmName.SHA1,
32));
} }
if (!_supportedHashMethods.Contains(hashMethod)) if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
{ {
throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); return hash.Hash.SequenceEqual(
Rfc2898DeriveBytes.Pbkdf2(
password,
hash.Salt,
int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
HashAlgorithmName.SHA512,
DefaultOutputLength));
} }
using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}."); if (!_supportedHashMethods.Contains(hash.Id))
if (salt.Length == 0)
{ {
return h.ComputeHash(bytes); throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
} }
byte[] salted = new byte[bytes.Length + salt.Length]; using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}.");
var bytes = Encoding.UTF8.GetBytes(password.ToArray());
if (hash.Salt.Length == 0)
{
return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
}
byte[] salted = new byte[bytes.Length + hash.Salt.Length];
Array.Copy(bytes, salted, bytes.Length); Array.Copy(bytes, salted, bytes.Length);
Array.Copy(salt, 0, salted, bytes.Length, salt.Length); hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
return h.ComputeHash(salted); return hash.Hash.SequenceEqual(h.ComputeHash(salted));
} }
/// <inheritdoc />
public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
=> PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
/// <inheritdoc /> /// <inheritdoc />
public byte[] GenerateSalt() public byte[] GenerateSalt()
=> GenerateSalt(DefaultSaltLength); => GenerateSalt(DefaultSaltLength);
/// <inheritdoc /> /// <inheritdoc />
public byte[] GenerateSalt(int length) public byte[] GenerateSalt(int length)
=> RandomNumberGenerator.GetBytes(length); {
var salt = new byte[length];
using var rng = RandomNumberGenerator.Create();
rng.GetNonZeroBytes(salt);
return salt;
}
} }
} }

View File

@ -7,7 +7,7 @@ using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data namespace Emby.Server.Implementations.Data
{ {
public class ManagedConnection : IDisposable public sealed class ManagedConnection : IDisposable
{ {
private readonly SemaphoreSlim _writeLock; private readonly SemaphoreSlim _writeLock;

View File

@ -134,14 +134,11 @@ namespace Emby.Server.Implementations.Dto
var dto = GetBaseItemDtoInternal(item, options, user, owner); var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel) if (item is LiveTvChannel tvChannel)
{ {
var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) }; LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
LivetvManager.AddChannelInfo(list, options, user);
} }
else if (item is LiveTvProgram) else if (item is LiveTvProgram)
{ {
var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) }; LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user);
Task.WaitAll(task);
} }
if (item is IItemByName itemByName if (item is IItemByName itemByName
@ -373,6 +370,12 @@ namespace Emby.Server.Implementations.Dto
if (item is MusicAlbum || item is Season || item is Playlist) if (item is MusicAlbum || item is Season || item is Playlist)
{ {
dto.ChildCount = dto.RecursiveItemCount; dto.ChildCount = dto.RecursiveItemCount;
var folderChildCount = folder.LinkedChildren.Length;
// The default is an empty array, so we can't reliably use the count when it's empty
if (folderChildCount > 0)
{
dto.ChildCount ??= folderChildCount;
}
} }
if (options.ContainsField(ItemFields.ChildCount)) if (options.ContainsField(ItemFields.ChildCount))
@ -497,7 +500,7 @@ namespace Emby.Server.Implementations.Dto
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error getting {imageType} image info for {path}", image.Type, image.Path); _logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path);
return null; return null;
} }
} }
@ -755,15 +758,6 @@ namespace Emby.Server.Implementations.Dto
dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit); dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
} }
if (options.ContainsField(ItemFields.ScreenshotImageTags))
{
var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
if (screenshotLimit > 0)
{
dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit);
}
}
if (options.ContainsField(ItemFields.Genres)) if (options.ContainsField(ItemFields.Genres))
{ {
dto.Genres = item.Genres; dto.Genres = item.Genres;

View File

@ -25,14 +25,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" /> <PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0-rc.2*" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0-rc.2*" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0-rc.2*" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0-rc.2*" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
<PackageReference Include="Mono.Nat" Version="3.0.1" /> <PackageReference Include="Mono.Nat" Version="3.0.2" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
<PackageReference Include="sharpcompress" Version="0.30.0" /> <PackageReference Include="sharpcompress" Version="0.30.1" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" /> <PackageReference Include="DotNet.Glob" Version="3.1.3" />
</ItemGroup> </ItemGroup>

View File

@ -27,7 +27,6 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly ILogger<ExternalPortForwarding> _logger; private readonly ILogger<ExternalPortForwarding> _logger;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IDeviceDiscovery _deviceDiscovery;
private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>(); private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
@ -42,17 +41,14 @@ namespace Emby.Server.Implementations.EntryPoints
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="appHost">The application host.</param> /// <param name="appHost">The application host.</param>
/// <param name="config">The configuration manager.</param> /// <param name="config">The configuration manager.</param>
/// <param name="deviceDiscovery">The device discovery.</param>
public ExternalPortForwarding( public ExternalPortForwarding(
ILogger<ExternalPortForwarding> logger, ILogger<ExternalPortForwarding> logger,
IServerApplicationHost appHost, IServerApplicationHost appHost,
IServerConfigurationManager config, IServerConfigurationManager config)
IDeviceDiscovery deviceDiscovery)
{ {
_logger = logger; _logger = logger;
_appHost = appHost; _appHost = appHost;
_config = config; _config = config;
_deviceDiscovery = deviceDiscovery;
} }
private string GetConfigIdentifier() private string GetConfigIdentifier()

View File

@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (!auth.HasToken) if (!auth.HasToken)
{ {
throw new AuthenticationException("Request does not contain a token."); return auth;
} }
if (!auth.IsAuthenticated) if (!auth.IsAuthenticated)

View File

@ -35,7 +35,12 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc /> /// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context) public async Task WebSocketRequestHandler(HttpContext context)
{ {
_ = await _authService.Authenticate(context.Request).ConfigureAwait(false); var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false);
if (!authorizationInfo.IsAuthenticated)
{
throw new SecurityException("Token is required");
}
try try
{ {
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);

View File

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO namespace Emby.Server.Implementations.IO
{ {
public class FileRefresher : IDisposable public sealed class FileRefresher : IDisposable
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<string> _affectedPaths = new List<string>(); private readonly List<string> _affectedPaths = new List<string>();
private readonly object _timerLock = new object(); private readonly object _timerLock = new object();
private Timer _timer; private Timer? _timer;
private bool _disposed; private bool _disposed;
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger) public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO
AddPath(path); AddPath(path);
} }
public event EventHandler<EventArgs> Completed; public event EventHandler<EventArgs>? Completed;
public string Path { get; private set; } public string Path { get; private set; }
@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO
RestartTimer(); RestartTimer();
} }
private void OnTimerCallback(object state) private void OnTimerCallback(object? state)
{ {
List<string> paths; List<string> paths;
@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO
try try
{ {
ProcessPathChanges(paths.ToList()); ProcessPathChanges(paths);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO
private void ProcessPathChanges(List<string> paths) private void ProcessPathChanges(List<string> paths)
{ {
var itemsToRefresh = paths IEnumerable<BaseItem> itemsToRefresh = paths
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.Select(GetAffectedBaseItem) .Select(GetAffectedBaseItem)
.Where(item => item != null) .Where(item => item != null)
.GroupBy(x => x.Id) .GroupBy(x => x!.Id) // Removed null values in the previous .Where()
.Select(x => x.First()); .Select(x => x.First())!;
foreach (var item in itemsToRefresh) foreach (var item in itemsToRefresh)
{ {
@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <returns>BaseItem.</returns> /// <returns>BaseItem.</returns>
private BaseItem GetAffectedBaseItem(string path) private BaseItem? GetAffectedBaseItem(string path)
{ {
BaseItem item = null; BaseItem? item = null;
while (item == null && !string.IsNullOrEmpty(path)) while (item == null && !string.IsNullOrEmpty(path))
{ {
item = _libraryManager.FindByPath(path, null); item = _libraryManager.FindByPath(path, null);
path = System.IO.Path.GetDirectoryName(path); path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
} }
if (item != null) if (item != null)

View File

@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.IO
if (_fileSystemWatchers.TryAdd(path, newWatcher)) if (_fileSystemWatchers.TryAdd(path, newWatcher))
{ {
newWatcher.EnableRaisingEvents = true; newWatcher.EnableRaisingEvents = true;
_logger.LogInformation("Watching directory " + path); _logger.LogInformation("Watching directory {Path}", path);
} }
else else
{ {
@ -276,7 +276,7 @@ namespace Emby.Server.Implementations.IO
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error watching path: {path}", path); _logger.LogError(ex, "Error watching path: {Path}", path);
} }
}); });
} }
@ -449,12 +449,12 @@ namespace Emby.Server.Implementations.IO
} }
var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger); var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
newRefresher.Completed += NewRefresher_Completed; newRefresher.Completed += OnNewRefresherCompleted;
_activeRefreshers.Add(newRefresher); _activeRefreshers.Add(newRefresher);
} }
} }
private void NewRefresher_Completed(object sender, EventArgs e) private void OnNewRefresherCompleted(object sender, EventArgs e)
{ {
var refresher = (FileRefresher)sender; var refresher = (FileRefresher)sender;
DisposeRefresher(refresher); DisposeRefresher(refresher);
@ -481,6 +481,7 @@ namespace Emby.Server.Implementations.IO
{ {
lock (_activeRefreshers) lock (_activeRefreshers)
{ {
refresher.Completed -= OnNewRefresherCompleted;
refresher.Dispose(); refresher.Dispose();
_activeRefreshers.Remove(refresher); _activeRefreshers.Remove(refresher);
} }
@ -492,6 +493,7 @@ namespace Emby.Server.Implementations.IO
{ {
foreach (var refresher in _activeRefreshers.ToList()) foreach (var refresher in _activeRefreshers.ToList())
{ {
refresher.Completed -= OnNewRefresherCompleted;
refresher.Dispose(); refresher.Dispose();
} }

View File

@ -1,5 +1,3 @@
#pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
@ -23,6 +21,11 @@ namespace Emby.Server.Implementations.IO
private readonly string _tempPath; private readonly string _tempPath;
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows(); private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
/// <summary>
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
public ManagedFileSystem( public ManagedFileSystem(
ILogger<ManagedFileSystem> logger, ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths) IApplicationPaths applicationPaths)
@ -31,6 +34,7 @@ namespace Emby.Server.Implementations.IO
_tempPath = applicationPaths.TempDirectory; _tempPath = applicationPaths.TempDirectory;
} }
/// <inheritdoc />
public virtual void AddShortcutHandler(IShortcutHandler handler) public virtual void AddShortcutHandler(IShortcutHandler handler)
{ {
_shortcutHandlers.Add(handler); _shortcutHandlers.Add(handler);
@ -72,6 +76,7 @@ namespace Emby.Server.Implementations.IO
return handler?.Resolve(filename); return handler?.Resolve(filename);
} }
/// <inheritdoc />
public virtual string MakeAbsolutePath(string folderPath, string filePath) public virtual string MakeAbsolutePath(string folderPath, string filePath)
{ {
// path is actually a stream // path is actually a stream
@ -358,11 +363,13 @@ namespace Emby.Server.Implementations.IO
return GetCreationTimeUtc(GetFileSystemInfo(path)); return GetCreationTimeUtc(GetFileSystemInfo(path));
} }
/// <inheritdoc />
public virtual DateTime GetCreationTimeUtc(FileSystemMetadata info) public virtual DateTime GetCreationTimeUtc(FileSystemMetadata info)
{ {
return info.CreationTimeUtc; return info.CreationTimeUtc;
} }
/// <inheritdoc />
public virtual DateTime GetLastWriteTimeUtc(FileSystemMetadata info) public virtual DateTime GetLastWriteTimeUtc(FileSystemMetadata info)
{ {
return info.LastWriteTimeUtc; return info.LastWriteTimeUtc;
@ -397,6 +404,7 @@ namespace Emby.Server.Implementations.IO
return GetLastWriteTimeUtc(GetFileSystemInfo(path)); return GetLastWriteTimeUtc(GetFileSystemInfo(path));
} }
/// <inheritdoc />
public virtual void SetHidden(string path, bool isHidden) public virtual void SetHidden(string path, bool isHidden)
{ {
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows())
@ -421,6 +429,7 @@ namespace Emby.Server.Implementations.IO
} }
} }
/// <inheritdoc />
public virtual void SetAttributes(string path, bool isHidden, bool readOnly) public virtual void SetAttributes(string path, bool isHidden, bool readOnly)
{ {
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows())
@ -444,7 +453,7 @@ namespace Emby.Server.Implementations.IO
if (readOnly) if (readOnly)
{ {
attributes = attributes | FileAttributes.ReadOnly; attributes |= FileAttributes.ReadOnly;
} }
else else
{ {
@ -453,7 +462,7 @@ namespace Emby.Server.Implementations.IO
if (isHidden) if (isHidden)
{ {
attributes = attributes | FileAttributes.Hidden; attributes |= FileAttributes.Hidden;
} }
else else
{ {
@ -498,6 +507,7 @@ namespace Emby.Server.Implementations.IO
File.Copy(temp1, file2, true); File.Copy(temp1, file2, true);
} }
/// <inheritdoc />
public virtual bool ContainsSubPath(string parentPath, string path) public virtual bool ContainsSubPath(string parentPath, string path)
{ {
if (string.IsNullOrEmpty(parentPath)) if (string.IsNullOrEmpty(parentPath))
@ -515,6 +525,7 @@ namespace Emby.Server.Implementations.IO
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
} }
/// <inheritdoc />
public virtual string NormalizePath(string path) public virtual string NormalizePath(string path)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
@ -530,6 +541,7 @@ namespace Emby.Server.Implementations.IO
return Path.TrimEndingDirectorySeparator(path); return Path.TrimEndingDirectorySeparator(path);
} }
/// <inheritdoc />
public virtual bool AreEqual(string path1, string path2) public virtual bool AreEqual(string path1, string path2)
{ {
if (path1 == null && path2 == null) if (path1 == null && path2 == null)
@ -548,6 +560,7 @@ namespace Emby.Server.Implementations.IO
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
} }
/// <inheritdoc />
public virtual string GetFileNameWithoutExtension(FileSystemMetadata info) public virtual string GetFileNameWithoutExtension(FileSystemMetadata info)
{ {
if (info.IsDirectory) if (info.IsDirectory)
@ -558,11 +571,11 @@ namespace Emby.Server.Implementations.IO
return Path.GetFileNameWithoutExtension(info.FullName); return Path.GetFileNameWithoutExtension(info.FullName);
} }
/// <inheritdoc />
public virtual bool IsPathFile(string path) public virtual bool IsPathFile(string path)
{ {
// Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\ if (path.Contains("://", StringComparison.OrdinalIgnoreCase)
if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 && && !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
!path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{ {
return false; return false;
} }
@ -570,17 +583,23 @@ namespace Emby.Server.Implementations.IO
return true; return true;
} }
/// <inheritdoc />
public virtual void DeleteFile(string path) public virtual void DeleteFile(string path)
{ {
SetAttributes(path, false, false); SetAttributes(path, false, false);
File.Delete(path); File.Delete(path);
} }
/// <inheritdoc />
public virtual List<FileSystemMetadata> GetDrives() public virtual List<FileSystemMetadata> GetDrives()
{ {
// check for ready state to avoid waiting for drives to timeout // check for ready state to avoid waiting for drives to timeout
// some drives on linux have no actual size or are used for other purposes // some drives on linux have no actual size or are used for other purposes
return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram) return DriveInfo.GetDrives()
.Where(
d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
&& d.IsReady
&& d.TotalSize != 0)
.Select(d => new FileSystemMetadata .Select(d => new FileSystemMetadata
{ {
Name = d.Name, Name = d.Name,
@ -589,16 +608,19 @@ namespace Emby.Server.Implementations.IO
}).ToList(); }).ToList();
} }
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false) public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
{ {
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive))); return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
} }
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false) public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
{ {
return GetFiles(path, null, false, recursive); return GetFiles(path, null, false, recursive);
} }
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{ {
var enumerationOptions = GetEnumerationOptions(recursive); var enumerationOptions = GetEnumerationOptions(recursive);
@ -629,6 +651,7 @@ namespace Emby.Server.Implementations.IO
return ToMetadata(files); return ToMetadata(files);
} }
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false) public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{ {
var directoryInfo = new DirectoryInfo(path); var directoryInfo = new DirectoryInfo(path);
@ -642,16 +665,19 @@ namespace Emby.Server.Implementations.IO
return infos.Select(GetFileSystemMetadata); return infos.Select(GetFileSystemMetadata);
} }
/// <inheritdoc />
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false) public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
{ {
return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive)); return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
} }
/// <inheritdoc />
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false) public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
{ {
return GetFilePaths(path, null, false, recursive); return GetFilePaths(path, null, false, recursive);
} }
/// <inheritdoc />
public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{ {
var enumerationOptions = GetEnumerationOptions(recursive); var enumerationOptions = GetEnumerationOptions(recursive);
@ -682,6 +708,7 @@ namespace Emby.Server.Implementations.IO
return files; return files;
} }
/// <inheritdoc />
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false) public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
{ {
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive)); return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));

View File

@ -1,8 +1,9 @@
using System; using System;
using System.IO; using System.IO;
using Emby.Naming.Audio;
using Emby.Naming.Common;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -13,17 +14,17 @@ namespace Emby.Server.Implementations.Library
/// </summary> /// </summary>
public class CoreResolutionIgnoreRule : IResolverIgnoreRule public class CoreResolutionIgnoreRule : IResolverIgnoreRule
{ {
private readonly ILibraryManager _libraryManager; private readonly NamingOptions _namingOptions;
private readonly IServerApplicationPaths _serverApplicationPaths; private readonly IServerApplicationPaths _serverApplicationPaths;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class. /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
/// </summary> /// </summary>
/// <param name="libraryManager">The library manager.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="serverApplicationPaths">The server application paths.</param> /// <param name="serverApplicationPaths">The server application paths.</param>
public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths) public CoreResolutionIgnoreRule(NamingOptions namingOptions, IServerApplicationPaths serverApplicationPaths)
{ {
_libraryManager = libraryManager; _namingOptions = namingOptions;
_serverApplicationPaths = serverApplicationPaths; _serverApplicationPaths = serverApplicationPaths;
} }
@ -78,7 +79,7 @@ namespace Emby.Server.Implementations.Library
{ {
// Don't resolve these into audio files // Don't resolve these into audio files
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal) if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename)) && AudioFileParser.IsAudioFile(filename, _namingOptions))
{ {
return true; return true;
} }

View File

@ -79,6 +79,7 @@ namespace Emby.Server.Implementations.Library
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository; private readonly IItemRepository _itemRepository;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly NamingOptions _namingOptions;
/// <summary> /// <summary>
/// The _root folder sync lock. /// The _root folder sync lock.
@ -88,9 +89,6 @@ namespace Emby.Server.Implementations.Library
private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24); private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
private NamingOptions _namingOptions;
private string[] _videoFileExtensions;
/// <summary> /// <summary>
/// The _root folder. /// The _root folder.
/// </summary> /// </summary>
@ -116,6 +114,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="itemRepository">The item repository.</param> /// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
/// <param name="memoryCache">The memory cache.</param> /// <param name="memoryCache">The memory cache.</param>
/// <param name="namingOptions">The naming options.</param>
public LibraryManager( public LibraryManager(
IServerApplicationHost appHost, IServerApplicationHost appHost,
ILogger<LibraryManager> logger, ILogger<LibraryManager> logger,
@ -130,7 +129,8 @@ namespace Emby.Server.Implementations.Library
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IItemRepository itemRepository, IItemRepository itemRepository,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
IMemoryCache memoryCache) IMemoryCache memoryCache,
NamingOptions namingOptions)
{ {
_appHost = appHost; _appHost = appHost;
_logger = logger; _logger = logger;
@ -146,6 +146,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository = itemRepository; _itemRepository = itemRepository;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_memoryCache = memoryCache; _memoryCache = memoryCache;
_namingOptions = namingOptions;
_configurationManager.ConfigurationUpdated += ConfigurationUpdated; _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@ -333,8 +334,7 @@ namespace Emby.Server.Implementations.Library
{ {
try try
{ {
var task = BaseItem.ChannelManager.DeleteItem(item); BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
Task.WaitAll(task);
} }
catch (ArgumentException) catch (ArgumentException)
{ {
@ -492,7 +492,7 @@ namespace Emby.Server.Implementations.Library
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error in {resolver} resolving {path}", resolver.GetType().Name, args.Path); _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
return null; return null;
} }
} }
@ -799,7 +799,7 @@ namespace Emby.Server.Implementations.Library
{ {
var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
_logger.LogDebug("Creating userRootPath at {path}", userRootPath); _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
Directory.CreateDirectory(userRootPath); Directory.CreateDirectory(userRootPath);
var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder)); var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
@ -810,7 +810,7 @@ namespace Emby.Server.Implementations.Library
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error creating UserRootFolder {path}", newItemId); _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
} }
if (tmpItem == null) if (tmpItem == null)
@ -827,7 +827,7 @@ namespace Emby.Server.Implementations.Library
} }
_userRootFolder = tmpItem; _userRootFolder = tmpItem;
_logger.LogDebug("Setting userRootFolder: {folder}", _userRootFolder); _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
} }
} }
} }
@ -1213,7 +1213,7 @@ namespace Emby.Server.Implementations.Library
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error resolving shortcut file {file}", i); _logger.LogError(ex, "Error resolving shortcut file {File}", i);
return null; return null;
} }
}) })
@ -1698,7 +1698,7 @@ namespace Emby.Server.Implementations.Library
if (video == null) if (video == null)
{ {
_logger.LogError("Intro resolver returned null for {path}.", info.Path); _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
} }
else else
{ {
@ -1717,7 +1717,7 @@ namespace Emby.Server.Implementations.Library
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error resolving path {path}.", info.Path); _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
} }
} }
else else
@ -2501,16 +2501,6 @@ namespace Emby.Server.Implementations.Library
return RootFolder; return RootFolder;
} }
/// <inheritdoc />
public bool IsVideoFile(string path)
{
return VideoResolver.IsVideoFile(path, GetNamingOptions());
}
/// <inheritdoc />
public bool IsAudioFile(string path)
=> AudioFileParser.IsAudioFile(path, GetNamingOptions());
/// <inheritdoc /> /// <inheritdoc />
public int? GetSeasonNumberFromPath(string path) public int? GetSeasonNumberFromPath(string path)
=> SeasonPathParser.Parse(path, true, true).SeasonNumber; => SeasonPathParser.Parse(path, true, true).SeasonNumber;
@ -2526,7 +2516,7 @@ namespace Emby.Server.Implementations.Library
isAbsoluteNaming = null; isAbsoluteNaming = null;
} }
var resolver = new EpisodeResolver(GetNamingOptions()); var resolver = new EpisodeResolver(_namingOptions);
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd; var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
@ -2683,21 +2673,9 @@ namespace Emby.Server.Implementations.Library
return changed; return changed;
} }
/// <inheritdoc />
public NamingOptions GetNamingOptions()
{
if (_namingOptions == null)
{
_namingOptions = new NamingOptions();
_videoFileExtensions = _namingOptions.VideoFileExtensions;
}
return _namingOptions;
}
public ItemLookupInfo ParseName(string name) public ItemLookupInfo ParseName(string name)
{ {
var namingOptions = GetNamingOptions(); var namingOptions = _namingOptions;
var result = VideoResolver.CleanDateTime(name, namingOptions); var result = VideoResolver.CleanDateTime(name, namingOptions);
return new ItemLookupInfo return new ItemLookupInfo
@ -2709,11 +2687,11 @@ namespace Emby.Server.Implementations.Library
public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{ {
var namingOptions = GetNamingOptions(); var namingOptions = _namingOptions;
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
.Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)) .Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
.ToList(); .ToList();
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
@ -2727,7 +2705,7 @@ namespace Emby.Server.Implementations.Library
var resolvers = new IItemResolver[] var resolvers = new IItemResolver[]
{ {
new GenericVideoResolver<Trailer>(this) new GenericVideoResolver<Trailer>(_namingOptions)
}; };
return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers) return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
@ -2753,11 +2731,11 @@ namespace Emby.Server.Implementations.Library
public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
{ {
var namingOptions = GetNamingOptions(); var namingOptions = _namingOptions;
var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
.Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty)) .Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
.ToList(); .ToList();
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions); var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
@ -2841,7 +2819,7 @@ namespace Emby.Server.Implementations.Library
private void SetExtraTypeFromFilename(Video item) private void SetExtraTypeFromFilename(Video item)
{ {
var resolver = new ExtraResolver(GetNamingOptions()); var resolver = new ExtraResolver(_namingOptions);
var result = resolver.GetExtraInfo(item.Path); var result = resolver.GetExtraInfo(item.Path);

View File

@ -45,6 +45,7 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly ILocalizationManager _localizationManager; private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IDirectoryService _directoryService;
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
@ -61,7 +62,8 @@ namespace Emby.Server.Implementations.Library
ILogger<MediaSourceManager> logger, ILogger<MediaSourceManager> logger,
IFileSystem fileSystem, IFileSystem fileSystem,
IUserDataManager userDataManager, IUserDataManager userDataManager,
IMediaEncoder mediaEncoder) IMediaEncoder mediaEncoder,
IDirectoryService directoryService)
{ {
_itemRepo = itemRepo; _itemRepo = itemRepo;
_userManager = userManager; _userManager = userManager;
@ -72,6 +74,7 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_localizationManager = localizationManager; _localizationManager = localizationManager;
_appPaths = applicationPaths; _appPaths = applicationPaths;
_directoryService = directoryService;
} }
public void AddParts(IEnumerable<IMediaSourceProvider> providers) public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@ -106,16 +109,6 @@ namespace Emby.Server.Implementations.Library
return false; return false;
} }
public List<MediaStream> GetMediaStreams(string mediaSourceId)
{
var list = GetMediaStreams(new MediaStreamQuery
{
ItemId = new Guid(mediaSourceId)
});
return GetMediaStreamsForItem(list);
}
public List<MediaStream> GetMediaStreams(Guid itemId) public List<MediaStream> GetMediaStreams(Guid itemId)
{ {
var list = GetMediaStreams(new MediaStreamQuery var list = GetMediaStreams(new MediaStreamQuery
@ -161,7 +154,7 @@ namespace Emby.Server.Implementations.Library
if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video)) if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video))
{ {
await item.RefreshMetadata( await item.RefreshMetadata(
new MetadataRefreshOptions(new DirectoryService(_fileSystem)) new MetadataRefreshOptions(_directoryService)
{ {
EnableRemoteContentProbe = true, EnableRemoteContentProbe = true,
MetadataRefreshMode = MetadataRefreshMode.FullRefresh MetadataRefreshMode = MetadataRefreshMode.FullRefresh
@ -212,6 +205,7 @@ namespace Emby.Server.Implementations.Library
return SortMediaSources(list); return SortMediaSources(list);
} }
/// <inheritdoc />>
public MediaProtocol GetPathProtocol(string path) public MediaProtocol GetPathProtocol(string path)
{ {
if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase)) if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
@ -258,7 +252,7 @@ namespace Emby.Server.Implementations.Library
{ {
if (path != null) if (path != null)
{ {
if (path.IndexOf(".m3u", StringComparison.OrdinalIgnoreCase) != -1) if (path.Contains(".m3u", StringComparison.OrdinalIgnoreCase))
{ {
return false; return false;
} }
@ -297,7 +291,7 @@ namespace Emby.Server.Implementations.Library
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error getting media sources"); _logger.LogError(ex, "Error getting media sources");
return new List<MediaSourceInfo>(); return Enumerable.Empty<MediaSourceInfo>();
} }
} }
@ -494,14 +488,11 @@ namespace Emby.Server.Implementations.Library
_liveStreamSemaphore.Release(); _liveStreamSemaphore.Release();
} }
// TODO: Don't hardcode this
const bool isAudio = false;
try try
{ {
if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing) if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing)
{ {
AddMediaInfo(mediaSource, isAudio); AddMediaInfo(mediaSource);
} }
else else
{ {
@ -509,14 +500,14 @@ namespace Emby.Server.Implementations.Library
string cacheKey = request.OpenToken; string cacheKey = request.OpenToken;
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths) await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken) .AddMediaInfoWithProbe(mediaSource, false, cacheKey, true, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error probing live tv stream"); _logger.LogError(ex, "Error probing live tv stream");
AddMediaInfo(mediaSource, isAudio); AddMediaInfo(mediaSource);
} }
// TODO: @bond Fix // TODO: @bond Fix
@ -536,7 +527,7 @@ namespace Emby.Server.Implementations.Library
return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider); return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider);
} }
private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio) private static void AddMediaInfo(MediaSourceInfo mediaSource)
{ {
mediaSource.DefaultSubtitleStreamIndex = null; mediaSource.DefaultSubtitleStreamIndex = null;
@ -855,9 +846,7 @@ namespace Emby.Server.Implementations.Library
return (provider, keyId); return (provider, keyId);
} }
/// <summary> /// <inheritdoc />
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);

View File

@ -6,7 +6,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Emby.Naming.Audio;
using Emby.Naming.AudioBook; using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -21,11 +24,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// </summary> /// </summary>
public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver
{ {
private readonly ILibraryManager _libraryManager; private readonly NamingOptions _namingOptions;
public AudioResolver(ILibraryManager libraryManager) public AudioResolver(NamingOptions namingOptions)
{ {
_libraryManager = libraryManager; _namingOptions = namingOptions;
} }
/// <summary> /// <summary>
@ -40,7 +43,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
string collectionType, string collectionType,
IDirectoryService directoryService) IDirectoryService directoryService)
{ {
var result = ResolveMultipleInternal(parent, files, collectionType, directoryService); var result = ResolveMultipleInternal(parent, files, collectionType);
if (result != null) if (result != null)
{ {
@ -56,12 +59,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
private MultiItemResolverResult ResolveMultipleInternal( private MultiItemResolverResult ResolveMultipleInternal(
Folder parent, Folder parent,
List<FileSystemMetadata> files, List<FileSystemMetadata> files,
string collectionType, string collectionType)
IDirectoryService directoryService)
{ {
if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase)) if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
{ {
return ResolveMultipleAudio<AudioBook>(parent, files, directoryService, false, collectionType, true); return ResolveMultipleAudio(parent, files, true);
} }
return null; return null;
@ -87,14 +89,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return null; return null;
} }
var files = args.FileSystemChildren return FindAudioBook(args, false);
.Where(i => !_libraryManager.IgnoreFile(i, args.Parent))
.ToList();
return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
} }
if (_libraryManager.IsAudioFile(args.Path)) if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
{ {
var extension = Path.GetExtension(args.Path); var extension = Path.GetExtension(args.Path);
@ -107,7 +105,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var isMixedCollectionType = string.IsNullOrEmpty(collectionType); var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
// For conflicting extensions, give priority to videos // For conflicting extensions, give priority to videos
if (isMixedCollectionType && _libraryManager.IsVideoFile(args.Path)) if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
{ {
return null; return null;
} }
@ -141,29 +139,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return null; return null;
} }
private T FindAudio<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName) private AudioBook FindAudioBook(ItemResolveArgs args, bool parseName)
where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
{ {
// TODO: Allow GetMultiDiscMovie in here // TODO: Allow GetMultiDiscMovie in here
const bool supportsMultiVersion = false; var result = ResolveMultipleAudio(args.Parent, args.GetActualFileSystemChildren(), parseName);
var result = ResolveMultipleAudio<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ?? if (result == null || result.Items.Count != 1 || result.Items[0] is not AudioBook item)
new MultiItemResolverResult();
if (result.Items.Count == 1)
{ {
return null;
}
// If we were supporting this we'd be checking filesFromOtherItems // If we were supporting this we'd be checking filesFromOtherItems
var item = (T)result.Items[0];
item.IsInMixedFolder = false; item.IsInMixedFolder = false;
item.Name = Path.GetFileName(item.ContainingFolderPath); item.Name = Path.GetFileName(item.ContainingFolderPath);
return item; return item;
} }
return null; private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
}
private MultiItemResolverResult ResolveMultipleAudio<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions, string collectionType, bool parseName)
where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
{ {
var files = new List<FileSystemMetadata>(); var files = new List<FileSystemMetadata>();
var items = new List<BaseItem>(); var items = new List<BaseItem>();
@ -176,15 +168,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{ {
leftOver.Add(child); leftOver.Add(child);
} }
else if (!IsIgnored(child.Name)) else
{ {
files.Add(child); files.Add(child);
} }
} }
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); var resolver = new AudioBookListResolver(_namingOptions);
var resolver = new AudioBookListResolver(namingOptions);
var resolverResult = resolver.Resolve(files).ToList(); var resolverResult = resolver.Resolve(files).ToList();
var result = new MultiItemResolverResult var result = new MultiItemResolverResult
@ -210,7 +200,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var firstMedia = resolvedItem.Files[0]; var firstMedia = resolvedItem.Files[0];
var libraryItem = new T var libraryItem = new AudioBook
{ {
Path = firstMedia.Path, Path = firstMedia.Path,
IsInMixedFolder = isInMixedFolder, IsInMixedFolder = isInMixedFolder,
@ -230,12 +220,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return result; return result;
} }
private bool ContainsFile(List<AudioBookInfo> result, FileSystemMetadata file) private static bool ContainsFile(IEnumerable<AudioBookInfo> result, FileSystemMetadata file)
{ {
return result.Any(i => ContainsFile(i, file)); return result.Any(i => ContainsFile(i, file));
} }
private bool ContainsFile(AudioBookInfo result, FileSystemMetadata file) private static bool ContainsFile(AudioBookInfo result, FileSystemMetadata file)
{ {
return result.Files.Any(i => ContainsFile(i, file)) || return result.Files.Any(i => ContainsFile(i, file)) ||
result.AlternateVersions.Any(i => ContainsFile(i, file)) || result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
@ -246,10 +236,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
{ {
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase); return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
} }
private static bool IsIgnored(string filename)
{
return false;
}
} }
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Naming.Audio; using Emby.Naming.Audio;
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
@ -22,20 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public class MusicAlbumResolver : ItemResolver<MusicAlbum> public class MusicAlbumResolver : ItemResolver<MusicAlbum>
{ {
private readonly ILogger<MusicAlbumResolver> _logger; private readonly ILogger<MusicAlbumResolver> _logger;
private readonly IFileSystem _fileSystem; private readonly NamingOptions _namingOptions;
private readonly ILibraryManager _libraryManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class. /// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="fileSystem">The file system.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="libraryManager">The library manager.</param> public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
{ {
_logger = logger; _logger = logger;
_fileSystem = fileSystem; _namingOptions = namingOptions;
_libraryManager = libraryManager;
} }
/// <summary> /// <summary>
@ -87,7 +85,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns>
public bool IsMusicAlbum(string path, IDirectoryService directoryService) public bool IsMusicAlbum(string path, IDirectoryService directoryService)
{ {
return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager); return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService);
} }
/// <summary> /// <summary>
@ -101,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (args.IsDirectory) if (args.IsDirectory)
{ {
// if (args.Parent is MusicArtist) return true; // saves us from testing children twice // if (args.Parent is MusicArtist) return true; // saves us from testing children twice
if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager)) if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
{ {
return true; return true;
} }
@ -116,13 +114,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
private bool ContainsMusic( private bool ContainsMusic(
IEnumerable<FileSystemMetadata> list, IEnumerable<FileSystemMetadata> list,
bool allowSubfolders, bool allowSubfolders,
IDirectoryService directoryService, IDirectoryService directoryService)
ILogger<MusicAlbumResolver> logger,
IFileSystem fileSystem,
ILibraryManager libraryManager)
{ {
// check for audio files before digging down into directories // check for audio files before digging down into directories
var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName)); var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && AudioFileParser.IsAudioFile(fileSystemInfo.FullName, _namingOptions));
if (foundAudioFile) if (foundAudioFile)
{ {
// at least one audio file exists // at least one audio file exists
@ -137,21 +132,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var discSubfolderCount = 0; var discSubfolderCount = 0;
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); var parser = new AlbumParser(_namingOptions);
var parser = new AlbumParser(namingOptions);
var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory); var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
var result = Parallel.ForEach(directories, (fileSystemInfo, state) => var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
{ {
var path = fileSystemInfo.FullName; var path = fileSystemInfo.FullName;
var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager); var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService);
if (hasMusic) if (hasMusic)
{ {
if (parser.IsMultiPart(path)) if (parser.IsMultiPart(path))
{ {
logger.LogDebug("Found multi-disc folder: " + path); _logger.LogDebug("Found multi-disc folder: {Path}", path);
Interlocked.Increment(ref discSubfolderCount); Interlocked.Increment(ref discSubfolderCount);
} }
else else

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Naming.Common;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -19,27 +20,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public class MusicArtistResolver : ItemResolver<MusicArtist> public class MusicArtistResolver : ItemResolver<MusicArtist>
{ {
private readonly ILogger<MusicAlbumResolver> _logger; private readonly ILogger<MusicAlbumResolver> _logger;
private readonly IFileSystem _fileSystem; private NamingOptions _namingOptions;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class. /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
/// </summary> /// </summary>
/// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param> /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
/// <param name="fileSystem">The file system.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="config">The configuration manager.</param>
public MusicArtistResolver( public MusicArtistResolver(
ILogger<MusicAlbumResolver> logger, ILogger<MusicAlbumResolver> logger,
IFileSystem fileSystem, NamingOptions namingOptions)
ILibraryManager libraryManager,
IServerConfigurationManager config)
{ {
_logger = logger; _logger = logger;
_fileSystem = fileSystem; _namingOptions = namingOptions;
_libraryManager = libraryManager;
_config = config;
} }
/// <summary> /// <summary>
@ -89,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var directoryService = args.DirectoryService; var directoryService = args.DirectoryService;
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager); var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
// If we contain an album assume we are an artist folder // If we contain an album assume we are an artist folder
var directories = args.FileSystemChildren.Where(i => i.IsDirectory); var directories = args.FileSystemChildren.Where(i => i.IsDirectory);

View File

@ -6,6 +6,7 @@ using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using DiscUtils.Udf; using DiscUtils.Udf;
using Emby.Naming.Common;
using Emby.Naming.Video; using Emby.Naming.Video;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -21,12 +22,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T> public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
where T : Video, new() where T : Video, new()
{ {
protected BaseVideoResolver(ILibraryManager libraryManager) protected BaseVideoResolver(NamingOptions namingOptions)
{ {
LibraryManager = libraryManager; NamingOptions = namingOptions;
} }
protected ILibraryManager LibraryManager { get; } protected NamingOptions NamingOptions { get; }
/// <summary> /// <summary>
/// Resolves the specified args. /// Resolves the specified args.
@ -48,7 +49,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new() where TVideoType : Video, new()
{ {
var namingOptions = LibraryManager.GetNamingOptions(); var namingOptions = NamingOptions;
// If the path is a file check for a matching extensions // If the path is a file check for a matching extensions
if (args.IsDirectory) if (args.IsDirectory)
@ -138,7 +139,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null; return null;
} }
if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub) if (VideoResolver.IsVideoFile(args.Path, NamingOptions) || videoInfo.IsStub)
{ {
var path = args.Path; var path = args.Path;
@ -267,7 +268,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected void Set3DFormat(Video video) protected void Set3DFormat(Video video)
{ {
var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions()); var result = Format3DParser.Parse(video.Path, NamingOptions);
Set3DFormat(video, result.Is3D, result.Format3D); Set3DFormat(video, result.Is3D, result.Format3D);
} }

View File

@ -2,16 +2,16 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
namespace Emby.Server.Implementations.Library.Resolvers namespace Emby.Server.Implementations.Library.Resolvers
{ {
public class GenericVideoResolver<T> : BaseVideoResolver<T> public class GenericVideoResolver<T> : BaseVideoResolver<T>
where T : Video, new() where T : Video, new()
{ {
public GenericVideoResolver(ILibraryManager libraryManager) public GenericVideoResolver(NamingOptions namingOptions)
: base(libraryManager) : base(namingOptions)
{ {
} }
} }

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Emby.Naming.Common;
using Emby.Naming.Video; using Emby.Naming.Video;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
@ -25,6 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{ {
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly StackResolver _stackResolver;
private string[] _validCollectionTypes = new[] private string[] _validCollectionTypes = new[]
{ {
@ -38,12 +40,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MovieResolver"/> class. /// Initializes a new instance of the <see cref="MovieResolver"/> class.
/// </summary> /// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor) /// <param name="namingOptions">The naming options.</param>
: base(libraryManager) public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
: base(namingOptions)
{ {
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_stackResolver = new StackResolver(NamingOptions);
} }
/// <summary> /// <summary>
@ -89,9 +92,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null; return null;
} }
var files = args.FileSystemChildren var files = args.GetActualFileSystemChildren().ToList();
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
.ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase)) if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{ {
@ -258,9 +259,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
} }
} }
var namingOptions = LibraryManager.GetNamingOptions(); var resolverResult = VideoListResolver.Resolve(files, NamingOptions, suppportMultiEditions).ToList();
var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
var result = new MultiItemResolverResult var result = new MultiItemResolverResult
{ {
@ -438,7 +437,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (result.Items.Count == 1) if (result.Items.Count == 1)
{ {
var videoPath = result.Items[0].Path; var videoPath = result.Items[0].Path;
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, videoPath, i.Name)); var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
if (!hasPhotos) if (!hasPhotos)
{ {
@ -511,9 +510,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null; return null;
} }
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); var result = _stackResolver.ResolveDirectories(folderPaths).ToList();
var result = new StackResolver(namingOptions).ResolveDirectories(folderPaths).ToList();
if (result.Count != 1) if (result.Count != 1)
{ {

View File

@ -1,6 +1,7 @@
#nullable disable #nullable disable
using System; using System;
using Emby.Naming.Common;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -15,17 +16,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum> public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
{ {
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly ILibraryManager _libraryManager; private readonly NamingOptions _namingOptions;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class. /// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
/// </summary> /// </summary>
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
/// <param name="libraryManager">The library manager.</param> /// <param name="namingOptions">The naming options.</param>
public PhotoAlbumResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager) public PhotoAlbumResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
{ {
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_libraryManager = libraryManager; _namingOptions = namingOptions;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -73,7 +74,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
foreach (var siblingFile in files) foreach (var siblingFile in files)
{ {
if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename)) if (PhotoResolver.IsOwnedByMedia(_namingOptions, siblingFile.FullName, filename))
{ {
ownedByMedia = true; ownedByMedia = true;
break; break;

View File

@ -6,6 +6,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -16,7 +18,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
public class PhotoResolver : ItemResolver<Photo> public class PhotoResolver : ItemResolver<Photo>
{ {
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly ILibraryManager _libraryManager; private readonly NamingOptions _namingOptions;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ {
"folder", "folder",
@ -30,10 +33,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
"default" "default"
}; };
public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
{ {
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_libraryManager = libraryManager; _namingOptions = namingOptions;
} }
/// <summary> /// <summary>
@ -60,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
foreach (var file in files) foreach (var file in files)
{ {
if (IsOwnedByMedia(_libraryManager, file.FullName, filename)) if (IsOwnedByMedia(_namingOptions, file.FullName, filename))
{ {
return null; return null;
} }
@ -77,17 +81,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null; return null;
} }
internal static bool IsOwnedByMedia(ILibraryManager libraryManager, string file, string imageFilename) internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
{ {
if (libraryManager.IsVideoFile(file)) return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
{
return IsOwnedByResolvedMedia(libraryManager, file, imageFilename);
} }
return false; internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
}
internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, string file, string imageFilename)
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase); => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor) internal static bool IsImageFile(string path, IImageProcessor imageProcessor)

View File

@ -2,6 +2,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -17,9 +18,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class. /// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary> /// </summary>
/// <param name="libraryManager">The library manager.</param> /// <param name="namingOptions">The naming options.</param>
public EpisodeResolver(ILibraryManager libraryManager) public EpisodeResolver(NamingOptions namingOptions)
: base(libraryManager) : base(namingOptions)
{ {
} }

View File

@ -1,6 +1,7 @@
#nullable disable #nullable disable
using System.Globalization; using System.Globalization;
using Emby.Naming.Common;
using Emby.Naming.TV; using Emby.Naming.TV;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -14,22 +15,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary> /// </summary>
public class SeasonResolver : GenericFolderResolver<Season> public class SeasonResolver : GenericFolderResolver<Season>
{ {
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly ILogger<SeasonResolver> _logger; private readonly ILogger<SeasonResolver> _logger;
private readonly NamingOptions _namingOptions;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SeasonResolver"/> class. /// Initializes a new instance of the <see cref="SeasonResolver"/> class.
/// </summary> /// </summary>
/// <param name="libraryManager">The library manager.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="localization">The localization.</param> /// <param name="localization">The localization.</param>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
public SeasonResolver( public SeasonResolver(
ILibraryManager libraryManager, NamingOptions namingOptions,
ILocalizationManager localization, ILocalizationManager localization,
ILogger<SeasonResolver> logger) ILogger<SeasonResolver> logger)
{ {
_libraryManager = libraryManager; _namingOptions = namingOptions;
_localization = localization; _localization = localization;
_logger = logger; _logger = logger;
} }
@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{ {
if (args.Parent is Series series && args.IsDirectory) if (args.Parent is Series series && args.IsDirectory)
{ {
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); var namingOptions = _namingOptions;
var path = args.Path; var path = args.Path;
@ -65,9 +66,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var episodeInfo = resolver.Resolve(testPath, true); var episodeInfo = resolver.Resolve(testPath, true);
if (episodeInfo != null) if (episodeInfo?.EpisodeNumber != null && episodeInfo.SeasonNumber.HasValue)
{
if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue)
{ {
_logger.LogDebug( _logger.LogDebug(
"Found folder underneath series with episode number: {0}. Season {1}. Episode {2}", "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
@ -78,7 +77,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null; return null;
} }
} }
}
if (season.IndexNumber.HasValue) if (season.IndexNumber.HasValue)
{ {

View File

@ -5,7 +5,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.TV; using Emby.Naming.TV;
using Emby.Naming.Video;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
@ -21,17 +26,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
public class SeriesResolver : GenericFolderResolver<Series> public class SeriesResolver : GenericFolderResolver<Series>
{ {
private readonly ILogger<SeriesResolver> _logger; private readonly ILogger<SeriesResolver> _logger;
private readonly ILibraryManager _libraryManager; private readonly NamingOptions _namingOptions;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SeriesResolver"/> class. /// Initializes a new instance of the <see cref="SeriesResolver"/> class.
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="libraryManager">The library manager.</param> /// <param name="namingOptions">The naming options.</param>
public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager) public SeriesResolver(ILogger<SeriesResolver> logger, NamingOptions namingOptions)
{ {
_logger = logger; _logger = logger;
_libraryManager = libraryManager; _namingOptions = namingOptions;
} }
/// <summary> /// <summary>
@ -54,16 +59,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null; return null;
} }
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
var collectionType = args.GetCollectionType(); var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{ {
var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path); // TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
var configuredContentType = args.GetConfiguredContentType();
if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{ {
return new Series return new Series
{ {
Path = args.Path, Path = args.Path,
Name = Path.GetFileName(args.Path) Name = seriesInfo.Name
}; };
} }
} }
@ -80,7 +88,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return new Series return new Series
{ {
Path = args.Path, Path = args.Path,
Name = Path.GetFileName(args.Path) Name = seriesInfo.Name
}; };
} }
@ -89,12 +97,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null; return null;
} }
if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false)) if (IsSeriesFolder(args.Path, args.FileSystemChildren, false))
{ {
return new Series return new Series
{ {
Path = args.Path, Path = args.Path,
Name = Path.GetFileName(args.Path) Name = seriesInfo.Name
}; };
} }
} }
@ -103,11 +111,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null; return null;
} }
public static bool IsSeriesFolder( private bool IsSeriesFolder(
string path, string path,
IEnumerable<FileSystemMetadata> fileSystemChildren, IEnumerable<FileSystemMetadata> fileSystemChildren,
ILogger<SeriesResolver> logger,
ILibraryManager libraryManager,
bool isTvContentType) bool isTvContentType)
{ {
foreach (var child in fileSystemChildren) foreach (var child in fileSystemChildren)
@ -116,21 +122,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{ {
if (IsSeasonFolder(child.FullName, isTvContentType)) if (IsSeasonFolder(child.FullName, isTvContentType))
{ {
logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true; return true;
} }
} }
else else
{ {
string fullName = child.FullName; string fullName = child.FullName;
if (libraryManager.IsVideoFile(fullName)) if (VideoResolver.IsVideoFile(path, _namingOptions))
{ {
if (isTvContentType) if (isTvContentType)
{ {
return true; return true;
} }
var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(); var namingOptions = _namingOptions;
var episodeResolver = new Naming.TV.EpisodeResolver(namingOptions); var episodeResolver = new Naming.TV.EpisodeResolver(namingOptions);
@ -143,7 +149,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
} }
} }
logger.LogDebug("{Path} is not a series folder.", path); _logger.LogDebug("{Path} is not a series folder.", path);
return false; return false;
} }

View File

@ -0,0 +1,156 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Querying;
using Jellyfin.Data.Enums;
using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library.Validators
{
/// <summary>
/// Class CollectionPostScanTask.
/// </summary>
public class CollectionPostScanTask : ILibraryPostScanTask
{
private readonly ILibraryManager _libraryManager;
private readonly ICollectionManager _collectionManager;
private readonly ILogger<CollectionPostScanTask> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="collectionManager">The collection manager.</param>
/// <param name="logger">The logger.</param>
public CollectionPostScanTask(
ILibraryManager libraryManager,
ICollectionManager collectionManager,
ILogger<CollectionPostScanTask> logger)
{
_libraryManager = libraryManager;
_collectionManager = collectionManager;
_logger = logger;
}
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
foreach (var library in _libraryManager.RootFolder.Children)
{
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
{
continue;
}
var startIndex = 0;
var pagesize = 1000;
while (true)
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
MediaTypes = new string[] { MediaType.Video },
IncludeItemTypes = new[] { nameof(Movie) },
IsVirtualItem = false,
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
Parent = library,
StartIndex = startIndex,
Limit = pagesize,
Recursive = true
});
foreach (var m in movies)
{
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
{
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
{
movieList.Add(movie.Id);
}
else
{
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
}
}
}
if (movies.Count < pagesize)
{
break;
}
startIndex += pagesize;
}
}
var numComplete = 0;
var count = collectionNameMoviesMap.Count;
if (count == 0)
{
progress.Report(100);
return;
}
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { nameof(BoxSet) },
CollapseBoxSetItems = false,
Recursive = true
});
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
{
try
{
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
if (boxSet == null)
{
// won't automatically create collection if only one movie in it
if (movieIds.Count >= 2)
{
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
Name = collectionName,
IsLocked = true
});
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
}
}
else
{
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
}
}
progress.Report(100);
}
}
}

View File

@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Library.Validators
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error validating IBN entry {person}", person); _logger.LogError(ex, "Error validating IBN entry {Person}", person);
} }
// Update progress // Update progress

View File

@ -957,7 +957,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{ {
_logger.LogInformation("Streaming Channel " + channelId); _logger.LogInformation("Streaming Channel {Id}", channelId);
var result = string.IsNullOrEmpty(streamId) ? var result = string.IsNullOrEmpty(streamId) ?
null : null :
@ -1027,7 +1027,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
var stream = new MediaSourceInfo var stream = new MediaSourceInfo
{ {
EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream", EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
EncoderProtocol = MediaProtocol.Http, EncoderProtocol = MediaProtocol.Http,
Path = info.Path, Path = info.Path,
Protocol = MediaProtocol.File, Protocol = MediaProtocol.File,
@ -1308,16 +1308,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false); await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
recordingStatus = RecordingStatus.Completed; recordingStatus = RecordingStatus.Completed;
_logger.LogInformation("Recording completed: {recordPath}", recordPath); _logger.LogInformation("Recording completed: {RecordPath}", recordPath);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
_logger.LogInformation("Recording stopped: {recordPath}", recordPath); _logger.LogInformation("Recording stopped: {RecordPath}", recordPath);
recordingStatus = RecordingStatus.Completed; recordingStatus = RecordingStatus.Completed;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error recording to {recordPath}", recordPath); _logger.LogError(ex, "Error recording to {RecordPath}", recordPath);
recordingStatus = RecordingStatus.Error; recordingStatus = RecordingStatus.Error;
} }
@ -1404,7 +1404,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error deleting 0-byte failed recording file {path}", path); _logger.LogError(ex, "Error deleting 0-byte failed recording file {Path}", path);
} }
} }
} }

View File

@ -87,8 +87,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
ErrorDialog = false ErrorDialog = false
}; };
var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments; _logger.LogInformation("{Filename} {Arguments}", processStartInfo.FileName, processStartInfo.Arguments);
_logger.LogInformation(commandLineLogMessage);
var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt"); var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false); await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false); await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + processStartInfo.FileName + " " + processStartInfo.Arguments + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
_process = new Process _process = new Process
{ {
@ -188,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"", "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
inputTempFile, inputTempFile,
targetFile.Replace("\"", "\\\""), // Escape quotes in filename targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename
videoArgs, videoArgs,
GetAudioArgs(mediaSource), GetAudioArgs(mediaSource),
subtitleArgs, subtitleArgs,
@ -225,13 +224,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
try try
{ {
_logger.LogInformation("Stopping ffmpeg recording process for {path}", _targetPath); _logger.LogInformation("Stopping ffmpeg recording process for {Path}", _targetPath);
_process.StandardInput.WriteLine("q"); _process.StandardInput.WriteLine("q");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error stopping recording transcoding job for {path}", _targetPath); _logger.LogError(ex, "Error stopping recording transcoding job for {Path}", _targetPath);
} }
if (_hasExited) if (_hasExited)
@ -241,7 +240,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try try
{ {
_logger.LogInformation("Calling recording process.WaitForExit for {path}", _targetPath); _logger.LogInformation("Calling recording process.WaitForExit for {Path}", _targetPath);
if (_process.WaitForExit(10000)) if (_process.WaitForExit(10000))
{ {
@ -250,7 +249,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error waiting for recording process to exit for {path}", _targetPath); _logger.LogError(ex, "Error waiting for recording process to exit for {Path}", _targetPath);
} }
if (_hasExited) if (_hasExited)
@ -260,13 +259,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try try
{ {
_logger.LogInformation("Killing ffmpeg recording process for {path}", _targetPath); _logger.LogInformation("Killing ffmpeg recording process for {Path}", _targetPath);
_process.Kill(); _process.Kill();
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error killing recording transcoding job for {path}", _targetPath); _logger.LogError(ex, "Error killing recording transcoding job for {Path}", _targetPath);
} }
} }
} }

View File

@ -11,6 +11,7 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Mime; using System.Net.Mime;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
@ -20,7 +21,6 @@ using Jellyfin.Extensions;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
@ -35,7 +35,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly ILogger<SchedulesDirect> _logger; private readonly ILogger<SchedulesDirect> _logger;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly ICryptoProvider _cryptoProvider;
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>(); private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@ -43,12 +42,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
public SchedulesDirect( public SchedulesDirect(
ILogger<SchedulesDirect> logger, ILogger<SchedulesDirect> logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory)
ICryptoProvider cryptoProvider)
{ {
_logger = logger; _logger = logger;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_cryptoProvider = cryptoProvider;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -169,12 +166,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
const double DesiredAspect = 2.0 / 3; const double DesiredAspect = 2.0 / 3;
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ??
GetProgramImage(ApiUrl, allImages, true, DesiredAspect); GetProgramImage(ApiUrl, allImages, DesiredAspect);
const double WideAspect = 16.0 / 9; const double WideAspect = 16.0 / 9;
programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect);
// Don't supply the same image twice // Don't supply the same image twice
if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal)) if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
@ -182,7 +179,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programEntry.ThumbImage = null; programEntry.ThumbImage = null;
} }
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect);
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? // GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
@ -403,7 +400,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return info; return info;
} }
private string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, bool returnDefaultImage, double desiredAspect) private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect)
{ {
var match = images var match = images
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i))) .OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
@ -648,7 +645,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>()); var hashedPasswordBytes = SHA1.HashData(Encoding.ASCII.GetBytes(password));
// TODO: remove ToLower when Convert.ToHexString supports lowercase // TODO: remove ToLower when Convert.ToHexString supports lowercase
// Schedules Direct requires the hex to be lowercase // Schedules Direct requires the hex to be lowercase
string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant(); string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();

View File

@ -393,7 +393,7 @@ namespace Emby.Server.Implementations.LiveTv
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error getting image info for {name}", info.Name); _logger.LogError(ex, "Error getting image info for {Name}", info.Name);
} }
return null; return null;

View File

@ -1054,7 +1054,7 @@ namespace Emby.Server.Implementations.LiveTv
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
_logger.LogDebug("Refreshing guide from {name}", service.Name); _logger.LogDebug("Refreshing guide from {Name}", service.Name);
try try
{ {
@ -1135,7 +1135,7 @@ namespace Emby.Server.Implementations.LiveTv
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error getting channel information for {name}", channelInfo.Item2.Name); _logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name);
} }
numComplete++; numComplete++;
@ -1248,7 +1248,7 @@ namespace Emby.Server.Implementations.LiveTv
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error getting programs for channel {name}", currentChannel.Name); _logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name);
} }
numComplete++; numComplete++;

View File

@ -104,7 +104,7 @@ namespace Emby.Server.Implementations.LiveTv
// Dummy this up so that direct play checks can still run // Dummy this up so that direct play checks can still run
if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http) if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
{ {
source.Path = _appHost.GetSmartApiUrl(string.Empty); source.Path = _appHost.GetApiUrlForLocalAccess();
} }
} }

View File

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

View File

@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
Logger.LogInformation("Opening HDHR UDP Live stream from {host}", uri.Host); Logger.LogInformation("Opening HDHR UDP Live stream from {Host}", uri.Host);
var remoteAddress = IPAddress.Parse(uri.Host); var remoteAddress = IPAddress.Parse(uri.Host);
IPAddress localAddress = null; IPAddress localAddress = null;
@ -147,7 +147,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// OpenedMediaSource.Path = tempFile; // OpenedMediaSource.Path = tempFile;
// OpenedMediaSource.ReadAtNativeFramerate = true; // OpenedMediaSource.ReadAtNativeFramerate = true;
MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http; MediaSource.Protocol = MediaProtocol.Http;
// OpenedMediaSource.SupportsDirectPlay = false; // OpenedMediaSource.SupportsDirectPlay = false;
// OpenedMediaSource.SupportsDirectStream = true; // OpenedMediaSource.SupportsDirectStream = true;

View File

@ -283,7 +283,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
// #EXTINF:0,84.0 - VOX Schweiz // #EXTINF:0,84.0 - VOX Schweiz
if (!string.IsNullOrWhiteSpace(nameInExtInf)) if (!string.IsNullOrWhiteSpace(nameInExtInf))
{ {
var numberIndex = nameInExtInf.IndexOf(' '); var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal);
if (numberIndex > 0) if (numberIndex > 0)
{ {
var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });

View File

@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
// OpenedMediaSource.Path = tempFile; // OpenedMediaSource.Path = tempFile;
// OpenedMediaSource.ReadAtNativeFramerate = true; // OpenedMediaSource.ReadAtNativeFramerate = true;
MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts"; MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http; MediaSource.Protocol = MediaProtocol.Http;
// OpenedMediaSource.Path = TempFilePath; // OpenedMediaSource.Path = TempFilePath;

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,4 @@
{
"Sync": "Сінхранізацыя",
"Playlists": "Плэйліст"
}

View File

@ -25,7 +25,7 @@
"HeaderLiveTV": "TV en Directe", "HeaderLiveTV": "TV en Directe",
"HeaderNextUp": "A continuació", "HeaderNextUp": "A continuació",
"HeaderRecordingGroups": "Grups d'Enregistrament", "HeaderRecordingGroups": "Grups d'Enregistrament",
"HomeVideos": "Vídeos domèstics", "HomeVideos": "Vídeos Domèstics",
"Inherit": "Hereta", "Inherit": "Hereta",
"ItemAddedWithName": "{0} ha estat afegit a la biblioteca", "ItemAddedWithName": "{0} ha estat afegit a la biblioteca",
"ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca", "ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca",
@ -39,7 +39,7 @@
"MixedContent": "Contingut barrejat", "MixedContent": "Contingut barrejat",
"Movies": "Pel·lícules", "Movies": "Pel·lícules",
"Music": "Música", "Music": "Música",
"MusicVideos": "Vídeos musicals", "MusicVideos": "Vídeos Musicals",
"NameInstallFailed": "Instalació de {0} fallida", "NameInstallFailed": "Instalació de {0} fallida",
"NameSeasonNumber": "Temporada {0}", "NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconeguda", "NameSeasonUnknown": "Temporada Desconeguda",

View File

@ -15,7 +15,7 @@
"Favorites": "Oblíbené", "Favorites": "Oblíbené",
"Folders": "Složky", "Folders": "Složky",
"Genres": "Žánry", "Genres": "Žánry",
"HeaderAlbumArtists": "Album umělce", "HeaderAlbumArtists": "Umělci alba",
"HeaderContinueWatching": "Pokračovat ve sledování", "HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba", "HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení interpreti", "HeaderFavoriteArtists": "Oblíbení interpreti",

View File

@ -15,7 +15,7 @@
"Favorites": "Favourites", "Favorites": "Favourites",
"Folders": "Folders", "Folders": "Folders",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Artist's Album", "HeaderAlbumArtists": "Album artists",
"HeaderContinueWatching": "Continue Watching", "HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favourite Albums", "HeaderFavoriteAlbums": "Favourite Albums",
"HeaderFavoriteArtists": "Favourite Artists", "HeaderFavoriteArtists": "Favourite Artists",

View File

@ -12,12 +12,12 @@
"Default": "Default", "Default": "Default",
"DeviceOfflineWithName": "{0} has disconnected", "DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected", "DeviceOnlineWithName": "{0} is connected",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}", "FailedLoginAttemptWithUserName": "Failed login try from {0}",
"Favorites": "Favorites", "Favorites": "Favorites",
"Folders": "Folders", "Folders": "Folders",
"Forced": "Forced", "Forced": "Forced",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Artist's Album", "HeaderAlbumArtists": "Album artists",
"HeaderContinueWatching": "Continue Watching", "HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists", "HeaderFavoriteArtists": "Favorite Artists",

View File

@ -1,5 +1,5 @@
{ {
"NotificationOptionInstallationFailed": "Instalada fiasko", "NotificationOptionInstallationFailed": "Instalada malsukceso",
"NotificationOptionAudioPlaybackStopped": "Ludado de sono haltis", "NotificationOptionAudioPlaybackStopped": "Ludado de sono haltis",
"NotificationOptionAudioPlayback": "Ludado de sono lanĉis", "NotificationOptionAudioPlayback": "Ludado de sono lanĉis",
"NameSeasonUnknown": "Sezono Nekonata", "NameSeasonUnknown": "Sezono Nekonata",
@ -11,7 +11,7 @@
"ItemAddedWithName": "{0} aldonis al la plurmediteko", "ItemAddedWithName": "{0} aldonis al la plurmediteko",
"HeaderLiveTV": "TV-etero", "HeaderLiveTV": "TV-etero",
"HeaderContinueWatching": "Daŭrigi Spektadon", "HeaderContinueWatching": "Daŭrigi Spektadon",
"HeaderAlbumArtists": "Albumo de artisto", "HeaderAlbumArtists": "Artistoj de albumo",
"Folders": "Dosierujoj", "Folders": "Dosierujoj",
"DeviceOnlineWithName": "{0} estas konektita", "DeviceOnlineWithName": "{0} estas konektita",
"Default": "Defaŭlte", "Default": "Defaŭlte",
@ -48,17 +48,17 @@
"Shows": "Serioj", "Shows": "Serioj",
"HeaderFavoriteShows": "Favorataj Serioj", "HeaderFavoriteShows": "Favorataj Serioj",
"TvShows": "TV-serioj", "TvShows": "TV-serioj",
"Favorites": "Favoratoj", "Favorites": "Favorataj",
"TaskCleanLogs": "Purigi Ĵurnalan Katalogon", "TaskCleanLogs": "Purigi Ĵurnalan Katalogon",
"TaskRefreshLibrary": "Skanu Plurmeditekon", "TaskRefreshLibrary": "Skani Plurmeditekon",
"ValueSpecialEpisodeName": "Speciala - {0}", "ValueSpecialEpisodeName": "Speciala - {0}",
"TaskOptimizeDatabase": "Optimigi datumbazon", "TaskOptimizeDatabase": "Optimumigi datenbazon",
"TaskRefreshChannels": "Refreŝigi Kanalojn", "TaskRefreshChannels": "Refreŝigi Kanalojn",
"TaskUpdatePlugins": "Ĝisdatigi Kromprogramojn", "TaskUpdatePlugins": "Ĝisdatigi Kromprogramojn",
"TaskRefreshPeople": "Refreŝigi Homojn", "TaskRefreshPeople": "Refreŝigi Homojn",
"TasksChannelsCategory": "Interretaj Kanaloj", "TasksChannelsCategory": "Interretaj Kanaloj",
"ProviderValue": "Provizanto: {0}", "ProviderValue": "Provizanto: {0}",
"NotificationOptionPluginError": "Kromprograma malsukceso", "NotificationOptionPluginError": "Kromprogramo malsukcesis",
"MixedContent": "Miksita enhavo", "MixedContent": "Miksita enhavo",
"TasksApplicationCategory": "Aplikaĵo", "TasksApplicationCategory": "Aplikaĵo",
"TasksMaintenanceCategory": "Prizorgado", "TasksMaintenanceCategory": "Prizorgado",
@ -75,7 +75,7 @@
"ServerNameNeedsToBeRestarted": "{0} devas esti relanĉita", "ServerNameNeedsToBeRestarted": "{0} devas esti relanĉita",
"NotificationOptionVideoPlayback": "La videoludado lanĉis", "NotificationOptionVideoPlayback": "La videoludado lanĉis",
"NotificationOptionServerRestartRequired": "Servila relanĉigo bezonata", "NotificationOptionServerRestartRequired": "Servila relanĉigo bezonata",
"TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la teka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.", "TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la plurmediteka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.",
"TaskUpdatePluginsDescription": "Elŝutas kaj instalas ĝisdatigojn por kromprogramojn, kiuj estas agorditaj por ĝisdatigi aŭtomate.", "TaskUpdatePluginsDescription": "Elŝutas kaj instalas ĝisdatigojn por kromprogramojn, kiuj estas agorditaj por ĝisdatigi aŭtomate.",
"TaskDownloadMissingSubtitlesDescription": "Serĉas en interreto mankantajn subtekstojn surbaze de metadatena agordaro.", "TaskDownloadMissingSubtitlesDescription": "Serĉas en interreto mankantajn subtekstojn surbaze de metadatena agordaro.",
"TaskRefreshPeopleDescription": "Ĝisdatigas metadatenojn por aktoroj kaj reĵisoroj en via plurmediteko.", "TaskRefreshPeopleDescription": "Ĝisdatigas metadatenojn por aktoroj kaj reĵisoroj en via plurmediteko.",
@ -102,9 +102,9 @@
"MessageApplicationUpdatedTo": "Jellyfin Server estis ĝisdatigita al {0}", "MessageApplicationUpdatedTo": "Jellyfin Server estis ĝisdatigita al {0}",
"MessageApplicationUpdated": "Jellyfin Server estis ĝisdatigita", "MessageApplicationUpdated": "Jellyfin Server estis ĝisdatigita",
"TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.", "TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
"TaskDownloadMissingSubtitles": "Elŝutu mankantajn subtekstojn", "TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn",
"TaskCleanTranscode": "Malplenigi Transkodadan Katalogon", "TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
"TaskRefreshChapterImages": "Eltiru Ĉapitro-Bildojn", "TaskRefreshChapterImages": "Eltiri Ĉapitraj Bildojn",
"TaskCleanCache": "Malplenigi Staplan Katalogon", "TaskCleanCache": "Malplenigi Staplan Katalogon",
"TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon", "TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon",
"PluginUpdatedWithName": "{0} estis ĝisdatigita", "PluginUpdatedWithName": "{0} estis ĝisdatigita",

View File

@ -15,7 +15,7 @@
"HeaderFavoriteEpisodes": "Episodios favoritos", "HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos", "HeaderFavoriteShows": "Programas favoritos",
"HeaderContinueWatching": "Continuar viendo", "HeaderContinueWatching": "Continuar viendo",
"HeaderAlbumArtists": "Artistas del álbum", "HeaderAlbumArtists": "Artistas de álbum",
"Genres": "Géneros", "Genres": "Géneros",
"Folders": "Carpetas", "Folders": "Carpetas",
"Favorites": "Favoritos", "Favorites": "Favoritos",
@ -29,7 +29,7 @@
"TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.", "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
"TaskRefreshChannels": "Actualizar canales", "TaskRefreshChannels": "Actualizar canales",
"TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.", "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
"TaskCleanTranscode": "Limpiar directorio de transcodificado", "TaskCleanTranscode": "Limpiar el directorio de transcodificaciones",
"TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.", "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
"TaskUpdatePlugins": "Actualizar complementos", "TaskUpdatePlugins": "Actualizar complementos",
"TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.", "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
@ -105,7 +105,7 @@
"Inherit": "Heredar", "Inherit": "Heredar",
"HomeVideos": "Videos caseros", "HomeVideos": "Videos caseros",
"HeaderRecordingGroups": "Grupos de grabación", "HeaderRecordingGroups": "Grupos de grabación",
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}", "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
"DeviceOnlineWithName": "{0} está conectado", "DeviceOnlineWithName": "{0} está conectado",
"DeviceOfflineWithName": "{0} se ha desconectado", "DeviceOfflineWithName": "{0} se ha desconectado",
"ChapterNameValue": "Capítulo {0}", "ChapterNameValue": "Capítulo {0}",
@ -114,10 +114,10 @@
"Application": "Aplicación", "Application": "Aplicación",
"AppDeviceValues": "App: {0}, Dispositivo: {1}", "AppDeviceValues": "App: {0}, Dispositivo: {1}",
"TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.", "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
"TaskCleanActivityLog": "Limpiar Registro de Actividades", "TaskCleanActivityLog": "Limpiar registro de actividades",
"Undefined": "Sin definir", "Undefined": "Sin definir",
"Forced": "Forzado", "Forced": "Forzado",
"Default": "Por Defecto", "Default": "Por defecto",
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.", "TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
"TaskOptimizeDatabase": "Optimización de base de datos" "TaskOptimizeDatabase": "Optimizar base de datos"
} }

View File

@ -17,7 +17,7 @@
"TaskCleanLogsDescription": "Kustutab logifailid, mis on vanemad kui {0} päeva.", "TaskCleanLogsDescription": "Kustutab logifailid, mis on vanemad kui {0} päeva.",
"TaskCleanLogs": "Puhasta logikataloog", "TaskCleanLogs": "Puhasta logikataloog",
"TaskRefreshLibraryDescription": "Otsib meedikogust uusi faile ja värskendab metaandmeid.", "TaskRefreshLibraryDescription": "Otsib meedikogust uusi faile ja värskendab metaandmeid.",
"Collections": "Kollektsioonid", "Collections": "Kogumikud",
"TaskRefreshLibrary": "Skaneeri meediakogu", "TaskRefreshLibrary": "Skaneeri meediakogu",
"TaskRefreshChapterImagesDescription": "Loob peatükkidega videote jaoks pisipildid.", "TaskRefreshChapterImagesDescription": "Loob peatükkidega videote jaoks pisipildid.",
"TaskRefreshChapterImages": "Eralda peatükipildid", "TaskRefreshChapterImages": "Eralda peatükipildid",
@ -32,9 +32,9 @@
"ValueSpecialEpisodeName": "Eriepisood - {0}", "ValueSpecialEpisodeName": "Eriepisood - {0}",
"ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse", "ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse",
"UserStartedPlayingItemWithValues": "{0} taasesitab {1} serveris {2}", "UserStartedPlayingItemWithValues": "{0} taasesitab {1} serveris {2}",
"UserPasswordChangedWithName": "Kasutaja {0} parooli on muudetud", "UserPasswordChangedWithName": "Kasutaja {0} parool muudeti",
"UserLockedOutWithName": "Kasutaja {0} on lukustatud", "UserLockedOutWithName": "Kasutaja {0} lukustati",
"UserDeletedWithName": "Kasutaja {0} on kustutatud", "UserDeletedWithName": "Kasutaja {0} kustutati",
"UserCreatedWithName": "Kasutaja {0} on loodud", "UserCreatedWithName": "Kasutaja {0} on loodud",
"ScheduledTaskStartedWithName": "{0} käivitati", "ScheduledTaskStartedWithName": "{0} käivitati",
"ProviderValue": "Allikas: {0}", "ProviderValue": "Allikas: {0}",
@ -54,9 +54,9 @@
"Plugin": "Plugin", "Plugin": "Plugin",
"Playlists": "Pleilistid", "Playlists": "Pleilistid",
"Photos": "Fotod", "Photos": "Fotod",
"NotificationOptionVideoPlaybackStopped": "Video taasesitus on peatatud", "NotificationOptionVideoPlaybackStopped": "Video taasesitus lõppes",
"NotificationOptionVideoPlayback": "Video taasesitus algas", "NotificationOptionVideoPlayback": "Video taasesitus algas",
"NotificationOptionUserLockedOut": "Kasutaja on lukustatud", "NotificationOptionUserLockedOut": "Kasutaja lukustati",
"NotificationOptionTaskFailed": "Ajastatud ülesanne nurjus", "NotificationOptionTaskFailed": "Ajastatud ülesanne nurjus",
"NotificationOptionServerRestartRequired": "Vajalik on serveri taaskäivitamine", "NotificationOptionServerRestartRequired": "Vajalik on serveri taaskäivitamine",
"NotificationOptionPluginUpdateInstalled": "Paigaldati plugina uuendus", "NotificationOptionPluginUpdateInstalled": "Paigaldati plugina uuendus",
@ -66,7 +66,7 @@
"NotificationOptionNewLibraryContent": "Lisati uut sisu", "NotificationOptionNewLibraryContent": "Lisati uut sisu",
"NotificationOptionInstallationFailed": "Paigaldamine nurjus", "NotificationOptionInstallationFailed": "Paigaldamine nurjus",
"NotificationOptionCameraImageUploaded": "Kaamera pilt on üles laaditud", "NotificationOptionCameraImageUploaded": "Kaamera pilt on üles laaditud",
"NotificationOptionAudioPlaybackStopped": "Heli taasesitus peatati", "NotificationOptionAudioPlaybackStopped": "Heli taasesitus lõppes",
"NotificationOptionAudioPlayback": "Heli taasesitus algas", "NotificationOptionAudioPlayback": "Heli taasesitus algas",
"NotificationOptionApplicationUpdateInstalled": "Rakenduse uuendus paigaldati", "NotificationOptionApplicationUpdateInstalled": "Rakenduse uuendus paigaldati",
"NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval", "NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
@ -97,12 +97,12 @@
"HeaderFavoriteArtists": "Lemmikesitajad", "HeaderFavoriteArtists": "Lemmikesitajad",
"HeaderFavoriteAlbums": "Lemmikalbumid", "HeaderFavoriteAlbums": "Lemmikalbumid",
"HeaderContinueWatching": "Jätka vaatamist", "HeaderContinueWatching": "Jätka vaatamist",
"HeaderAlbumArtists": "Albumi esitaja", "HeaderAlbumArtists": "Albumi esitajad",
"Genres": "Žanrid", "Genres": "Žanrid",
"Forced": "Sunnitud", "Forced": "Sunnitud",
"Folders": "Kaustad", "Folders": "Kaustad",
"Favorites": "Lemmikud", "Favorites": "Lemmikud",
"FailedLoginAttemptWithUserName": "Ebaõnnestunud sisselogimiskatse kasutajalt {0}", "FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus",
"DeviceOnlineWithName": "{0} on ühendatud", "DeviceOnlineWithName": "{0} on ühendatud",
"DeviceOfflineWithName": "{0} katkestas ühenduse", "DeviceOfflineWithName": "{0} katkestas ühenduse",
"Default": "Vaikimisi", "Default": "Vaikimisi",
@ -114,5 +114,10 @@
"Artists": "Esitajad", "Artists": "Esitajad",
"Application": "Rakendus", "Application": "Rakendus",
"AppDeviceValues": "Rakendus: {0}, seade: {1}", "AppDeviceValues": "Rakendus: {0}, seade: {1}",
"Albums": "Albumid" "Albums": "Albumid",
"UserOfflineFromDevice": "{0} katkestas ühenduse seadmega {1}",
"SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus",
"UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
"UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
"UserOnlineFromDevice": "{0} on ühendatud seadmest {1}"
} }

View File

@ -6,7 +6,7 @@
"AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد", "AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد",
"Books": "کتاب‌ها", "Books": "کتاب‌ها",
"CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}", "CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}",
"Channels": "کانالها", "Channels": "کانالها",
"ChapterNameValue": "قسمت {0}", "ChapterNameValue": "قسمت {0}",
"Collections": "مجموعه‌ها", "Collections": "مجموعه‌ها",
"DeviceOfflineWithName": "ارتباط {0} قطع شد", "DeviceOfflineWithName": "ارتباط {0} قطع شد",
@ -37,7 +37,7 @@
"MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد", "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
"MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد", "MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد",
"MixedContent": "محتوای مخلوط", "MixedContent": "محتوای مخلوط",
"Movies": "فیلمها", "Movies": "فیلم ها",
"Music": "موسیقی", "Music": "موسیقی",
"MusicVideos": "موزیک ویدیوها", "MusicVideos": "موزیک ویدیوها",
"NameInstallFailed": "{0} نصب با مشکل مواجه شد", "NameInstallFailed": "{0} نصب با مشکل مواجه شد",

View File

@ -15,7 +15,7 @@
"Favorites": "Favoris", "Favorites": "Favoris",
"Folders": "Dossiers", "Folders": "Dossiers",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Artistes de l'album", "HeaderAlbumArtists": "Artistes d'album",
"HeaderContinueWatching": "Continuer à regarder", "HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés", "HeaderFavoriteArtists": "Artistes préférés",

View File

@ -11,11 +11,11 @@
"Collections": "Gyűjtemények", "Collections": "Gyűjtemények",
"DeviceOfflineWithName": "{0} kijelentkezett", "DeviceOfflineWithName": "{0} kijelentkezett",
"DeviceOnlineWithName": "{0} belépett", "DeviceOnlineWithName": "{0} belépett",
"FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet tőle: {0}", "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
"Favorites": "Kedvencek", "Favorites": "Kedvencek",
"Folders": "Könyvtárak", "Folders": "Könyvtárak",
"Genres": "Műfajok", "Genres": "Műfajok",
"HeaderAlbumArtists": "Előadó albumai", "HeaderAlbumArtists": "Album előadó(k)",
"HeaderContinueWatching": "Megtekintés folytatása", "HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok", "HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteArtists": "Kedvenc előadók", "HeaderFavoriteArtists": "Kedvenc előadók",

View File

@ -7,10 +7,10 @@
"MessageApplicationUpdated": "Jellyfin Server sudah diperbarui", "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
"Latest": "Terbaru", "Latest": "Terbaru",
"LabelIpAddressValue": "Alamat IP: {0}", "LabelIpAddressValue": "Alamat IP: {0}",
"ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka", "ItemRemovedWithName": "{0} sudah dihapus dari pustaka",
"ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka", "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
"Inherit": "Warisan", "Inherit": "Warisi",
"HomeVideos": "Video Rumah", "HomeVideos": "Video Rumahan",
"HeaderRecordingGroups": "Grup Rekaman", "HeaderRecordingGroups": "Grup Rekaman",
"HeaderNextUp": "Selanjutnya", "HeaderNextUp": "Selanjutnya",
"HeaderLiveTV": "TV Live", "HeaderLiveTV": "TV Live",
@ -73,7 +73,7 @@
"NotificationOptionCameraImageUploaded": "Gambar kamera terunggah", "NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
"NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang", "NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
"NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia", "NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
"NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.", "NewVersionIsAvailable": "Versi baru dari Jellyfin Server sudah tersedia untuk diunduh.",
"NameSeasonUnknown": "Musim tak diketahui", "NameSeasonUnknown": "Musim tak diketahui",
"NameSeasonNumber": "Musim {0}", "NameSeasonNumber": "Musim {0}",
"NameInstallFailed": "{0} penginstalan gagal", "NameInstallFailed": "{0} penginstalan gagal",
@ -117,5 +117,7 @@
"TaskCleanActivityLog": "Bersihkan Log Aktivitas", "TaskCleanActivityLog": "Bersihkan Log Aktivitas",
"Undefined": "Tidak terdefinisi", "Undefined": "Tidak terdefinisi",
"Forced": "Dipaksa", "Forced": "Dipaksa",
"Default": "Bawaan" "Default": "Bawaan",
"TaskOptimizeDatabaseDescription": "Rapihkan basis data dan membersihkan ruang kosong. Menjalankan tugas ini setelah memindai pustaka atau melakukan perubahan lain yang menyiratkan modifikasi basis data dapat meningkatkan kinerja.",
"TaskOptimizeDatabase": "Optimalkan basis data"
} }

View File

@ -15,7 +15,7 @@
"Favorites": "Preferiti", "Favorites": "Preferiti",
"Folders": "Cartelle", "Folders": "Cartelle",
"Genres": "Generi", "Genres": "Generi",
"HeaderAlbumArtists": "Artisti dell'Album", "HeaderAlbumArtists": "Artisti dell'album",
"HeaderContinueWatching": "Continua a guardare", "HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album Preferiti", "HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti", "HeaderFavoriteArtists": "Artisti Preferiti",

View File

@ -15,7 +15,7 @@
"Favorites": "Tañdaulylar", "Favorites": "Tañdaulylar",
"Folders": "Qaltalar", "Folders": "Qaltalar",
"Genres": "Janrlar", "Genres": "Janrlar",
"HeaderAlbumArtists": "Oryndauşynyñ älbomy", "HeaderAlbumArtists": "Älbom oryndauşylary",
"HeaderContinueWatching": "Qaraudy jalğastyru", "HeaderContinueWatching": "Qaraudy jalğastyru",
"HeaderFavoriteAlbums": "Tañdauly älbomdar", "HeaderFavoriteAlbums": "Tañdauly älbomdar",
"HeaderFavoriteArtists": "Tañdauly oryndauşylar", "HeaderFavoriteArtists": "Tañdauly oryndauşylar",

View File

@ -50,7 +50,7 @@
"HeaderFavoriteEpisodes": "Омилени Епизоди", "HeaderFavoriteEpisodes": "Омилени Епизоди",
"HeaderFavoriteArtists": "Омилени Изведувачи", "HeaderFavoriteArtists": "Омилени Изведувачи",
"HeaderFavoriteAlbums": "Омилени Албуми", "HeaderFavoriteAlbums": "Омилени Албуми",
"HeaderContinueWatching": "Продолжи со гледање", "HeaderContinueWatching": "Продолжи со Гледање",
"HeaderAlbumArtists": "Изведувачи од Албуми", "HeaderAlbumArtists": "Изведувачи од Албуми",
"Genres": "Жанрови", "Genres": "Жанрови",
"Folders": "Папки", "Folders": "Папки",

View File

@ -0,0 +1,14 @@
{
"Books": "Номууд",
"HeaderNextUp": "Дараах",
"HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
"Songs": "Дуунууд",
"Playlists": "Тоглуулах жагсаалт",
"Movies": "Кино",
"Latest": "Сүүлийн үеийн",
"Genres": "Төрөл зүйл",
"Favorites": "Дуртай",
"Collections": "Багц",
"Artists": "Зураачуд",
"Albums": "Цомгууд"
}

View File

@ -39,7 +39,7 @@
"MixedContent": "Kandungan campuran", "MixedContent": "Kandungan campuran",
"Movies": "Filem", "Movies": "Filem",
"Music": "Muzik", "Music": "Muzik",
"MusicVideos": "Muzik video", "MusicVideos": "",
"NameInstallFailed": "{0} pemasangan gagal", "NameInstallFailed": "{0} pemasangan gagal",
"NameSeasonNumber": "Musim {0}", "NameSeasonNumber": "Musim {0}",
"NameSeasonUnknown": "Musim Tidak Diketahui", "NameSeasonUnknown": "Musim Tidak Diketahui",

View File

@ -15,7 +15,7 @@
"Favorites": "Ulubione", "Favorites": "Ulubione",
"Folders": "Foldery", "Folders": "Foldery",
"Genres": "Gatunki", "Genres": "Gatunki",
"HeaderAlbumArtists": "Album artysty", "HeaderAlbumArtists": "Wykonawcy albumów",
"HeaderContinueWatching": "Kontynuuj odtwarzanie", "HeaderContinueWatching": "Kontynuuj odtwarzanie",
"HeaderFavoriteAlbums": "Ulubione albumy", "HeaderFavoriteAlbums": "Ulubione albumy",
"HeaderFavoriteArtists": "Ulubieni wykonawcy", "HeaderFavoriteArtists": "Ulubieni wykonawcy",
@ -47,7 +47,7 @@
"NotificationOptionApplicationUpdateAvailable": "Dostępna aktualizacja aplikacji", "NotificationOptionApplicationUpdateAvailable": "Dostępna aktualizacja aplikacji",
"NotificationOptionApplicationUpdateInstalled": "Zaktualizowano aplikację", "NotificationOptionApplicationUpdateInstalled": "Zaktualizowano aplikację",
"NotificationOptionAudioPlayback": "Rozpoczęto odtwarzanie muzyki", "NotificationOptionAudioPlayback": "Rozpoczęto odtwarzanie muzyki",
"NotificationOptionAudioPlaybackStopped": "Odtwarzane dźwięku zatrzymane", "NotificationOptionAudioPlaybackStopped": "Odtwarzanie dźwięku zatrzymane",
"NotificationOptionCameraImageUploaded": "Przekazano obraz z urządzenia przenośnego", "NotificationOptionCameraImageUploaded": "Przekazano obraz z urządzenia przenośnego",
"NotificationOptionInstallationFailed": "Nieudana instalacja", "NotificationOptionInstallationFailed": "Nieudana instalacja",
"NotificationOptionNewLibraryContent": "Dodano nową zawartość", "NotificationOptionNewLibraryContent": "Dodano nową zawartość",
@ -98,7 +98,7 @@
"TaskRefreshChannels": "Odśwież kanały", "TaskRefreshChannels": "Odśwież kanały",
"TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.", "TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
"TaskCleanTranscode": "Wyczyść folder transkodowania", "TaskCleanTranscode": "Wyczyść folder transkodowania",
"TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów które są skonfigurowane do automatycznej aktualizacji.", "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.",
"TaskUpdatePlugins": "Aktualizuj pluginy", "TaskUpdatePlugins": "Aktualizuj pluginy",
"TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.", "TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
"TaskRefreshPeople": "Odśwież obsadę", "TaskRefreshPeople": "Odśwież obsadę",

View File

@ -1,5 +1,7 @@
{ {
"Books": "Libros", "Books": "Libros",
"AuthenticationSucceededWithUserName": "{0} autentificado correctamente", "AuthenticationSucceededWithUserName": "{0} autentificado correctamente",
"Artists": "Artistas" "Artists": "Artistas",
"Songs": "Shantees",
"Albums": "Ships"
} }

View File

@ -15,7 +15,7 @@
"Favorites": "Favoritos", "Favorites": "Favoritos",
"Folders": "Pastas", "Folders": "Pastas",
"Genres": "Géneros", "Genres": "Géneros",
"HeaderAlbumArtists": "Artistas do Álbum", "HeaderAlbumArtists": "Álbum do Artista",
"HeaderContinueWatching": "Continuar a Ver", "HeaderContinueWatching": "Continuar a Ver",
"HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos", "HeaderFavoriteArtists": "Artistas Favoritos",
@ -39,7 +39,7 @@
"MixedContent": "Conteúdo Misto", "MixedContent": "Conteúdo Misto",
"Movies": "Filmes", "Movies": "Filmes",
"Music": "Música", "Music": "Música",
"MusicVideos": "Videoclips", "MusicVideos": "Videoclipes",
"NameInstallFailed": "{0} falha na instalação", "NameInstallFailed": "{0} falha na instalação",
"NameSeasonNumber": "Temporada {0}", "NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada Desconhecida", "NameSeasonUnknown": "Temporada Desconhecida",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Limpar registo de atividade", "TaskCleanActivityLog": "Limpar registo de atividade",
"Undefined": "Indefinido", "Undefined": "Indefinido",
"Forced": "Forçado", "Forced": "Forçado",
"Default": "Padrão" "Default": "Padrão",
"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.",
"TaskOptimizeDatabase": "Otimizar base de dados"
} }

View File

@ -31,7 +31,7 @@
"ItemRemovedWithName": "{0} - изъято из медиатеки", "ItemRemovedWithName": "{0} - изъято из медиатеки",
"LabelIpAddressValue": "IP-адрес: {0}", "LabelIpAddressValue": "IP-адрес: {0}",
"LabelRunningTimeValue": "Длительность: {0}", "LabelRunningTimeValue": "Длительность: {0}",
"Latest": "Последнее", "Latest": "Крайнее",
"MessageApplicationUpdated": "Jellyfin Server был обновлён", "MessageApplicationUpdated": "Jellyfin Server был обновлён",
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}", "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена", "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
@ -119,6 +119,6 @@
"Undefined": "Не определено", "Undefined": "Не определено",
"Forced": "Форсир-ые", "Forced": "Форсир-ые",
"Default": "По умолчанию", "Default": "По умолчанию",
"TaskOptimizeDatabaseDescription": "Сжимает базу данных и обрезает свободное место. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.", "TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.",
"TaskOptimizeDatabase": "Оптимизировать базу данных" "TaskOptimizeDatabase": "Оптимизировать базу данных"
} }

View File

@ -39,7 +39,7 @@
"MixedContent": "Zmiešaný obsah", "MixedContent": "Zmiešaný obsah",
"Movies": "Filmy", "Movies": "Filmy",
"Music": "Hudba", "Music": "Hudba",
"MusicVideos": "Hudobné videá", "MusicVideos": "Hudobné videoklipy",
"NameInstallFailed": "Inštalácia {0} zlyhala", "NameInstallFailed": "Inštalácia {0} zlyhala",
"NameSeasonNumber": "Séria {0}", "NameSeasonNumber": "Séria {0}",
"NameSeasonUnknown": "Neznáma séria", "NameSeasonUnknown": "Neznáma séria",

View File

@ -74,7 +74,7 @@
"NameSeasonUnknown": "Sezon i panjohur", "NameSeasonUnknown": "Sezon i panjohur",
"NameSeasonNumber": "Sezoni {0}", "NameSeasonNumber": "Sezoni {0}",
"NameInstallFailed": "Instalimi i {0} dështoi", "NameInstallFailed": "Instalimi i {0} dështoi",
"MusicVideos": "Video muzikore", "MusicVideos": "Video Muzikore",
"Music": "Muzikë", "Music": "Muzikë",
"Movies": "Filmat", "Movies": "Filmat",
"MixedContent": "Përmbajtje e përzier", "MixedContent": "Përmbajtje e përzier",
@ -96,7 +96,7 @@
"HeaderFavoriteArtists": "Artistët e preferuar", "HeaderFavoriteArtists": "Artistët e preferuar",
"HeaderFavoriteAlbums": "Albumet e preferuar", "HeaderFavoriteAlbums": "Albumet e preferuar",
"HeaderContinueWatching": "Vazhdo të shikosh", "HeaderContinueWatching": "Vazhdo të shikosh",
"HeaderAlbumArtists": "Artistët e Albumeve", "HeaderAlbumArtists": "Artistët e albumeve",
"Genres": "Zhanret", "Genres": "Zhanret",
"Folders": "Skedarët", "Folders": "Skedarët",
"Favorites": "Të preferuarat", "Favorites": "Të preferuarat",

View File

@ -50,7 +50,7 @@
"NameSeasonUnknown": "Непозната сезона", "NameSeasonUnknown": "Непозната сезона",
"NameSeasonNumber": "Сезона {0}", "NameSeasonNumber": "Сезона {0}",
"NameInstallFailed": "Инсталација {0} није успела", "NameInstallFailed": "Инсталација {0} није успела",
"MusicVideos": "Музички спотови", "MusicVideos": "Музички видео",
"Music": "Музика", "Music": "Музика",
"Movies": "Филмови", "Movies": "Филмови",
"MixedContent": "Мешовит садржај", "MixedContent": "Мешовит садржај",

View File

@ -15,7 +15,7 @@
"Favorites": "Favoriter", "Favorites": "Favoriter",
"Folders": "Mappar", "Folders": "Mappar",
"Genres": "Genrer", "Genres": "Genrer",
"HeaderAlbumArtists": "Artistens album", "HeaderAlbumArtists": "Albumsartister",
"HeaderContinueWatching": "Fortsätt kolla", "HeaderContinueWatching": "Fortsätt kolla",
"HeaderFavoriteAlbums": "Favoritalbum", "HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritartister", "HeaderFavoriteArtists": "Favoritartister",

View File

@ -85,7 +85,7 @@
"HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்", "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
"HeaderFavoriteAlbums": "பிடித்த ஆல்பங்கள்", "HeaderFavoriteAlbums": "பிடித்த ஆல்பங்கள்",
"HeaderContinueWatching": "தொடர்ந்து பார்", "HeaderContinueWatching": "தொடர்ந்து பார்",
"HeaderAlbumArtists": "இசைக் கலைஞர்கள்", "HeaderAlbumArtists": "கலைஞரின் ஆல்பம்",
"Genres": "வகைகள்", "Genres": "வகைகள்",
"Favorites": "பிடித்தவை", "Favorites": "பிடித்தவை",
"ChapterNameValue": "அத்தியாயம் {0}", "ChapterNameValue": "அத்தியாயம் {0}",

View File

@ -0,0 +1,23 @@
{
"ValueSpecialEpisodeName": "ప్రత్యేక - {0}",
"Sync": "సమకాలీకరించు",
"Songs": "పాటలు",
"Shows": "ప్రదర్శనలు",
"Playlists": "ప్లేజాబితాలు",
"Photos": "ఫోటోలు",
"MusicVideos": "మ్యూజిక్ వీడియోలు",
"Music": "సంగీతం",
"Movies": "సినిమాలు",
"HeaderContinueWatching": "చూడటం కొనసాగించండి",
"HeaderAlbumArtists": "ఆల్బమ్ కళాకారులు",
"Genres": "శైలులు",
"Forced": "బలవంతంగా",
"Folders": "ఫోల్డర్లు",
"Favorites": "ఇష్టమైనవి",
"Default": "డిఫాల్ట్",
"Collections": "సేకరణలు",
"Channels": "ఛానెల్‌లు",
"Books": "పుస్తకాలు",
"Artists": "కళాకారులు",
"Albums": "ఆల్బమ్‌లు"
}

View File

@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi", "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
"Channels": "Kanallar", "Channels": "Kanallar",
"ChapterNameValue": "Bölüm {0}", "ChapterNameValue": "Bölüm {0}",
"Collections": "Koleksiyon", "Collections": "Koleksiyonlar",
"DeviceOfflineWithName": "{0} bağlantısı kesildi", "DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı", "DeviceOnlineWithName": "{0} bağlı",
"FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu", "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",

View File

@ -16,7 +16,7 @@
"HeaderFavoriteArtists": "Улюблені виконавці", "HeaderFavoriteArtists": "Улюблені виконавці",
"HeaderFavoriteAlbums": "Улюблені альбоми", "HeaderFavoriteAlbums": "Улюблені альбоми",
"HeaderContinueWatching": "Продовжити перегляд", "HeaderContinueWatching": "Продовжити перегляд",
"HeaderAlbumArtists": "Виконавці альбомів", "HeaderAlbumArtists": "Виконавці альбому",
"Genres": "Жанри", "Genres": "Жанри",
"Folders": "Каталоги", "Folders": "Каталоги",
"Favorites": "Улюблені", "Favorites": "Улюблені",

View File

@ -3,7 +3,7 @@
"Favorites": "Yêu Thích", "Favorites": "Yêu Thích",
"Folders": "Thư Mục", "Folders": "Thư Mục",
"Genres": "Thể Loại", "Genres": "Thể Loại",
"HeaderAlbumArtists": "Album Nghệ sĩ", "HeaderAlbumArtists": "Album nghệ sĩ",
"HeaderContinueWatching": "Xem Tiếp", "HeaderContinueWatching": "Xem Tiếp",
"HeaderLiveTV": "TV Trực Tiếp", "HeaderLiveTV": "TV Trực Tiếp",
"Movies": "Phim", "Movies": "Phim",
@ -62,11 +62,11 @@
"PluginUninstalledWithName": "{0} đã được gỡ bỏ", "PluginUninstalledWithName": "{0} đã được gỡ bỏ",
"PluginInstalledWithName": "{0} đã được cài đặt", "PluginInstalledWithName": "{0} đã được cài đặt",
"Plugin": "Plugin", "Plugin": "Plugin",
"NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng", "NotificationOptionVideoPlaybackStopped": "Đã dừng phát lại video",
"NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video", "NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video",
"NotificationOptionUserLockedOut": "Người dùng bị khóa", "NotificationOptionUserLockedOut": "Người dùng bị khóa",
"NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch", "NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch",
"NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server", "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại máy chủ",
"NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt", "NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt",
"NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin", "NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin",
"NotificationOptionPluginInstalled": "Đã cài đặt Plugin", "NotificationOptionPluginInstalled": "Đã cài đặt Plugin",
@ -75,7 +75,7 @@
"NotificationOptionInstallationFailed": "Cài đặt thất bại", "NotificationOptionInstallationFailed": "Cài đặt thất bại",
"NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh", "NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh",
"NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng", "NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng",
"NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu", "NotificationOptionAudioPlayback": "Đã bắt đầu phát lại âm thanh",
"NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt", "NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt",
"NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có", "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có",
"NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.", "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.",
@ -95,7 +95,7 @@
"ItemRemovedWithName": "{0} đã xóa khỏi thư viện", "ItemRemovedWithName": "{0} đã xóa khỏi thư viện",
"ItemAddedWithName": "{0} được thêm vào thư viện", "ItemAddedWithName": "{0} được thêm vào thư viện",
"Inherit": "Thừa hưởng", "Inherit": "Thừa hưởng",
"HomeVideos": "Video nhà", "HomeVideos": "Video Nhà",
"HeaderRecordingGroups": "Nhóm Ghi Video", "HeaderRecordingGroups": "Nhóm Ghi Video",
"HeaderNextUp": "Tiếp Theo", "HeaderNextUp": "Tiếp Theo",
"HeaderFavoriteSongs": "Bài Hát Yêu Thích", "HeaderFavoriteSongs": "Bài Hát Yêu Thích",
@ -103,7 +103,7 @@
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích", "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích", "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
"HeaderFavoriteAlbums": "Album Ưa Thích", "HeaderFavoriteAlbums": "Album Ưa Thích",
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}", "FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}",
"DeviceOnlineWithName": "{0} đã kết nối", "DeviceOnlineWithName": "{0} đã kết nối",
"DeviceOfflineWithName": "{0} đã ngắt kết nối", "DeviceOfflineWithName": "{0} đã ngắt kết nối",
"ChapterNameValue": "Phân Cảnh {0}", "ChapterNameValue": "Phân Cảnh {0}",

View File

@ -11,7 +11,7 @@
"Collections": "合集", "Collections": "合集",
"DeviceOfflineWithName": "{0} 已断开", "DeviceOfflineWithName": "{0} 已断开",
"DeviceOnlineWithName": "{0} 已连接", "DeviceOnlineWithName": "{0} 已连接",
"FailedLoginAttemptWithUserName": "来自 {0} 的失败登入", "FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
"Favorites": "我的最爱", "Favorites": "我的最爱",
"Folders": "文件夹", "Folders": "文件夹",
"Genres": "风格", "Genres": "风格",

View File

@ -0,0 +1 @@
{}

View File

@ -372,43 +372,76 @@ namespace Emby.Server.Implementations.Localization
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<LocalizationOption> GetLocalizationOptions() public IEnumerable<LocalizationOption> GetLocalizationOptions()
{ {
yield return new LocalizationOption("Arabic", "ar"); yield return new LocalizationOption("Afrikaans", "af");
yield return new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG"); yield return new LocalizationOption("العربية", "ar");
yield return new LocalizationOption("Catalan", "ca"); yield return new LocalizationOption("Беларуская", "be");
yield return new LocalizationOption("Chinese Simplified", "zh-CN"); yield return new LocalizationOption("Български", "bg-BG");
yield return new LocalizationOption("Chinese Traditional", "zh-TW"); yield return new LocalizationOption("বাংলা (বাংলাদেশ)", "bn");
yield return new LocalizationOption("Croatian", "hr"); yield return new LocalizationOption("Català", "ca");
yield return new LocalizationOption("Czech", "cs"); yield return new LocalizationOption("Čeština", "cs");
yield return new LocalizationOption("Danish", "da"); yield return new LocalizationOption("Cymraeg", "cy");
yield return new LocalizationOption("Dutch", "nl"); yield return new LocalizationOption("Dansk", "da");
yield return new LocalizationOption("Deutsch", "de");
yield return new LocalizationOption("English (United Kingdom)", "en-GB"); yield return new LocalizationOption("English (United Kingdom)", "en-GB");
yield return new LocalizationOption("English (United States)", "en-US"); yield return new LocalizationOption("English", "en-US");
yield return new LocalizationOption("French", "fr"); yield return new LocalizationOption("Ελληνικά", "el");
yield return new LocalizationOption("French (Canada)", "fr-CA"); yield return new LocalizationOption("Esperanto", "eo");
yield return new LocalizationOption("German", "de"); yield return new LocalizationOption("Español", "es");
yield return new LocalizationOption("Greek", "el"); yield return new LocalizationOption("Español americano", "es_419");
yield return new LocalizationOption("Hebrew", "he"); yield return new LocalizationOption("Español (Argentina)", "es-AR");
yield return new LocalizationOption("Hungarian", "hu"); yield return new LocalizationOption("Español (Dominicana)", "es_DO");
yield return new LocalizationOption("Italian", "it"); yield return new LocalizationOption("Español (México)", "es-MX");
yield return new LocalizationOption("Kazakh", "kk"); yield return new LocalizationOption("Eesti", "et");
yield return new LocalizationOption("Korean", "ko"); yield return new LocalizationOption("فارسی", "fa");
yield return new LocalizationOption("Lithuanian", "lt-LT"); yield return new LocalizationOption("Suomi", "fi");
yield return new LocalizationOption("Malay", "ms"); yield return new LocalizationOption("Filipino", "fil");
yield return new LocalizationOption("Norwegian Bokmål", "nb"); yield return new LocalizationOption("Français", "fr");
yield return new LocalizationOption("Persian", "fa"); yield return new LocalizationOption("Français (Canada)", "fr-CA");
yield return new LocalizationOption("Polish", "pl"); yield return new LocalizationOption("Galego", "gl");
yield return new LocalizationOption("Portuguese (Brazil)", "pt-BR"); yield return new LocalizationOption("Schwiizerdütsch", "gsw");
yield return new LocalizationOption("Portuguese (Portugal)", "pt-PT"); yield return new LocalizationOption("עִבְרִית", "he");
yield return new LocalizationOption("Russian", "ru"); yield return new LocalizationOption("हिन्दी", "hi");
yield return new LocalizationOption("Slovak", "sk"); yield return new LocalizationOption("Hrvatski", "hr");
yield return new LocalizationOption("Slovenian (Slovenia)", "sl-SI"); yield return new LocalizationOption("Magyar", "hu");
yield return new LocalizationOption("Spanish", "es"); yield return new LocalizationOption("Bahasa Indonesia", "id");
yield return new LocalizationOption("Spanish (Argentina)", "es-AR"); yield return new LocalizationOption("Íslenska", "is");
yield return new LocalizationOption("Spanish (Mexico)", "es-MX"); yield return new LocalizationOption("Italiano", "it");
yield return new LocalizationOption("Swedish", "sv"); yield return new LocalizationOption("日本語", "ja");
yield return new LocalizationOption("Swiss German", "gsw"); yield return new LocalizationOption("Qazaqşa", "kk");
yield return new LocalizationOption("Turkish", "tr"); yield return new LocalizationOption("한국어", "ko");
yield return new LocalizationOption("Lietuvių", "lt");
yield return new LocalizationOption("Latviešu", "lv");
yield return new LocalizationOption("Македонски", "mk");
yield return new LocalizationOption("മലയാളം", "ml");
yield return new LocalizationOption("मराठी", "mr");
yield return new LocalizationOption("Bahasa Melayu", "ms");
yield return new LocalizationOption("Norsk bokmål", "nb");
yield return new LocalizationOption("नेपाली", "ne");
yield return new LocalizationOption("Nederlands", "nl");
yield return new LocalizationOption("Norsk nynorsk", "nn");
yield return new LocalizationOption("ਪੰਜਾਬੀ", "pa");
yield return new LocalizationOption("Polski", "pl");
yield return new LocalizationOption("Pirate", "pr");
yield return new LocalizationOption("Português", "pt");
yield return new LocalizationOption("Português (Brasil)", "pt-BR");
yield return new LocalizationOption("Português (Portugal)", "pt-PT");
yield return new LocalizationOption("Românește", "ro");
yield return new LocalizationOption("Русский", "ru");
yield return new LocalizationOption("Slovenčina", "sk");
yield return new LocalizationOption("Slovenščina", "sl-SI");
yield return new LocalizationOption("Shqip", "sq");
yield return new LocalizationOption("Српски", "sr");
yield return new LocalizationOption("Svenska", "sv");
yield return new LocalizationOption("தமிழ்", "ta");
yield return new LocalizationOption("తెలుగు", "te");
yield return new LocalizationOption("ภาษาไทย", "th");
yield return new LocalizationOption("Türkçe", "tr");
yield return new LocalizationOption("Українська", "uk");
yield return new LocalizationOption("اُردُو", "ur_PK");
yield return new LocalizationOption("Tiếng Việt", "vi"); yield return new LocalizationOption("Tiếng Việt", "vi");
yield return new LocalizationOption("汉语 (简化字)", "zh-CN");
yield return new LocalizationOption("漢語 (繁体字)", "zh-TW");
yield return new LocalizationOption("廣東話 (香港)", "zh-HK");
} }
} }
} }

View File

@ -630,7 +630,7 @@
"TwoLetterISORegionName": "MD" "TwoLetterISORegionName": "MD"
}, },
{ {
"DisplayName": "Réunion", "DisplayName": "Réunion",
"Name": "RE", "Name": "RE",
"ThreeLetterISORegionName": "REU", "ThreeLetterISORegionName": "REU",
"TwoLetterISORegionName": "RE" "TwoLetterISORegionName": "RE"

View File

@ -349,7 +349,8 @@ pli||pi|Pali|pali
pol||pl|Polish|polonais pol||pl|Polish|polonais
pon|||Pohnpeian|pohnpei pon|||Pohnpeian|pohnpei
por||pt|Portuguese|portugais por||pt|Portuguese|portugais
pob||pt-br|Portuguese (Brazil)|portugais pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
pob||pt-br|Portuguese (Brazil)|portugais (pt-br)
pra|||Prakrit languages|prâkrit, langues pra|||Prakrit languages|prâkrit, langues
pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500) pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500)
pus||ps|Pushto; Pashto|pachto pus||ps|Pushto; Pashto|pachto

View File

@ -527,7 +527,7 @@ namespace Emby.Server.Implementations.Playlists
var relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri); var relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri);
string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); string relativePath = Uri.UnescapeDataString(relativeUri.ToString());
if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.CurrentCultureIgnoreCase)) if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase))
{ {
relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
} }

View File

@ -126,7 +126,8 @@ namespace Emby.Server.Implementations.Plugins
{ {
assembly = Assembly.LoadFrom(file); assembly = Assembly.LoadFrom(file);
assembly.GetExportedTypes(); // Load all required types to verify that the plugin will load
assembly.GetTypes();
} }
catch (FileLoadException ex) catch (FileLoadException ex)
{ {
@ -134,7 +135,7 @@ namespace Emby.Server.Implementations.Plugins
ChangePluginState(plugin, PluginStatus.Malfunctioned); ChangePluginState(plugin, PluginStatus.Malfunctioned);
continue; continue;
} }
catch (TypeLoadException ex) // Undocumented exception catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception
{ {
_logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file); _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
ChangePluginState(plugin, PluginStatus.NotSupported); ChangePluginState(plugin, PluginStatus.NotSupported);

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