diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index c28b1bf7f..c91a084e5 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -168,6 +168,7 @@ jobs:
- job: CollectArtifacts
timeoutInMinutes: 20
displayName: 'Collect Artifacts'
+ condition: succeededOrFailed()
continueOnError: true
dependsOn:
- BuildPackage
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 000000000..2f789b031
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-ef": {
+ "version": "7.0.12",
+ "commands": [
+ "dotnet-ef"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index f83b38949..a5f36eab4 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
+ uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
+ uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
+ uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 178959afc..8055438b5 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index d3dfd0a6a..c267fdcc2 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -14,7 +14,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -25,7 +25,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: openapi-head
retention-days: 14
@@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -59,7 +59,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: openapi-base
retention-days: 14
diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/repo-bump-version.yaml
new file mode 100644
index 000000000..e0383afd2
--- /dev/null
+++ b/.github/workflows/repo-bump-version.yaml
@@ -0,0 +1,82 @@
+name: '🆙 Auto bump_version'
+
+on:
+ release:
+ types:
+ - published
+ workflow_dispatch:
+ inputs:
+ TAG_BRANCH:
+ required: true
+ description: release-x.y.z
+ NEXT_VERSION:
+ required: true
+ description: x.y.z
+
+jobs:
+ auto_bump_version:
+ runs-on: ubuntu-latest
+ if: ${{ github.event_name == 'release' && !contains(github.event.release.tag_name, 'rc') }}
+ env:
+ TAG_BRANCH: ${{ github.event.release.target_commitish }}
+ steps:
+ - name: Wait for deploy checks to finish
+ uses: jitterbit/await-check-suites@292a541bb7618078395b2ce711a0d89cfb8a568a # v1
+ with:
+ ref: ${{ env.TAG_BRANCH }}
+ intervalSeconds: 60
+ timeoutSeconds: 3600
+
+ - name: Setup YQ
+ uses: chrisdickinson/setup-yq@latest
+ with:
+ yq-version: v4.9.8
+
+ - name: Checkout Repository
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ ref: ${{ env.TAG_BRANCH }}
+
+ - name: Setup EnvVars
+ run: |-
+ CURRENT_VERSION=$(yq e '.version' build.yaml)
+ CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*}
+ CURRENT_PATCH=${CURRENT_VERSION##*.}
+ echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV
+ echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV
+ echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV
+ echo "NEXT_VERSION=${CURRENT_MAJOR_MINOR}.$(($CURRENT_PATCH + 1))" >> $GITHUB_ENV
+
+ - name: Run bump_version
+ run: ./bump_version ${{ env.NEXT_VERSION }}
+
+ - name: Commit Changes
+ run: |-
+ git config user.name "jellyfin-bot"
+ git config user.email "team@jellyfin.org"
+ git checkout ${{ env.TAG_BRANCH }}
+ git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
+ git push origin ${{ env.TAG_BRANCH }}
+
+ manual_bump_version:
+ runs-on: ubuntu-latest
+ if: ${{ github.event_name == 'workflow_dispatch' }}
+ env:
+ TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }}
+ NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ ref: ${{ env.TAG_BRANCH }}
+
+ - name: Run bump_version
+ run: ./bump_version ${{ env.NEXT_VERSION }}
+
+ - name: Commit Changes
+ run: |-
+ git config user.name "jellyfin-bot"
+ git config user.email "team@jellyfin.org"
+ git checkout ${{ env.TAG_BRANCH }}
+ git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
+ git push origin ${{ env.TAG_BRANCH }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index c753c1600..2b1164116 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -2,16 +2,17 @@ name: Stale Check
on:
schedule:
- - cron: '30 1 * * *'
+ - cron: '30 */12 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
+ actions: write
jobs:
issues:
- name: Check issues
+ name: Check for stale issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
@@ -26,11 +27,11 @@ jobs:
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
- This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
+ This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
- If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
-
- This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
+ If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
+ close-issue-message: |-
+ This issue was closed due to inactivity.
prs-conflicts:
name: Check PRs with merge conflicts
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 009610c41..dc5f99c0c 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -168,6 +168,8 @@
- [RealGreenDragon](https://github.com/RealGreenDragon)
- [ipitio](https://github.com/ipitio)
- [TheTyrius](https://github.com/TheTyrius)
+ - [tallbl0nde](https://github.com/tallbl0nde)
+ - [sleepycatcoding](https://github.com/sleepycatcoding)
# Emby Contributors
@@ -238,3 +240,4 @@
- [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir)
- [JPUC1143](https://github.com/Jpuc1143/)
+ - [0x25CBFC4F](https://github.com/0x25CBFC4F)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index c3532467a..d95cecdbf 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -10,26 +10,30 @@
-
-
+
+
-
-
+
+
+
+
-
-
+
+
+
-
-
-
-
+
+
+
+
+
@@ -38,14 +42,14 @@
-
-
+
+
-
+
@@ -53,28 +57,25 @@
-
+
-
+
-
+
-
+
-
-
+
+
+
+
-
-
-
-
-
-
+
@@ -85,8 +86,8 @@
-
+
-
+
diff --git a/Dockerfile b/Dockerfile
index e51d285e1..9be319311 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 46a3e9b99..e8ec6398e 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 4f9d5e1fd..83137ee89 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Emby.Dlna/Configuration/DlnaOptions.cs b/Emby.Dlna/Configuration/DlnaOptions.cs
index e95a878c6..f233468de 100644
--- a/Emby.Dlna/Configuration/DlnaOptions.cs
+++ b/Emby.Dlna/Configuration/DlnaOptions.cs
@@ -17,7 +17,7 @@ namespace Emby.Dlna.Configuration
BlastAliveMessages = true;
SendOnlyMatchedHost = true;
ClientDiscoveryIntervalSeconds = 60;
- AliveMessageIntervalSeconds = 1800;
+ AliveMessageIntervalSeconds = 180;
}
///
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index f668dc829..5ed982876 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -45,8 +43,8 @@ namespace Emby.Dlna.Didl
private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor;
private readonly string _serverAddress;
- private readonly string _accessToken;
- private readonly User _user;
+ private readonly string? _accessToken;
+ private readonly User? _user;
private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager;
@@ -56,10 +54,10 @@ namespace Emby.Dlna.Didl
public DidlBuilder(
DeviceProfile profile,
- User user,
+ User? user,
IImageProcessor imageProcessor,
string serverAddress,
- string accessToken,
+ string? accessToken,
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
@@ -85,7 +83,7 @@ namespace Emby.Dlna.Didl
return url + "&dlnaheaders=true";
}
- public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo)
+ public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo)
{
var settings = new XmlWriterSettings
{
@@ -140,12 +138,12 @@ namespace Emby.Dlna.Didl
public void WriteItemElement(
XmlWriter writer,
BaseItem item,
- User user,
- BaseItem context,
+ User? user,
+ BaseItem? context,
StubType? contextStubType,
string deviceId,
Filter filter,
- StreamInfo streamInfo = null)
+ StreamInfo? streamInfo = null)
{
var clientId = GetClientId(item, null);
@@ -190,7 +188,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
- private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
+ private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null)
{
if (streamInfo is null)
{
@@ -203,7 +201,7 @@ namespace Emby.Dlna.Didl
Profile = _profile,
DeviceId = deviceId,
MaxBitrate = _profile.MaxStreamingBitrate
- });
+ }) ?? throw new InvalidOperationException("No optimal video stream found");
}
var targetWidth = streamInfo.TargetWidth;
@@ -315,7 +313,7 @@ namespace Emby.Dlna.Didl
var mediaSource = streamInfo.MediaSource;
- if (mediaSource.RunTimeTicks.HasValue)
+ if (mediaSource?.RunTimeTicks.HasValue == true)
{
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
@@ -410,7 +408,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
- private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context)
+ private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context)
{
if (itemStubType.HasValue)
{
@@ -452,7 +450,7 @@ namespace Emby.Dlna.Didl
/// The episode.
/// Current context.
/// Formatted name of the episode.
- private string GetEpisodeDisplayName(Episode episode, BaseItem context)
+ private string GetEpisodeDisplayName(Episode episode, BaseItem? context)
{
string[] components;
@@ -530,7 +528,7 @@ namespace Emby.Dlna.Didl
private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
- private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
+ private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null)
{
writer.WriteStartElement(string.Empty, "res", NsDidl);
@@ -544,14 +542,14 @@ namespace Emby.Dlna.Didl
MediaSources = sources.ToArray(),
Profile = _profile,
DeviceId = deviceId
- });
+ }) ?? throw new InvalidOperationException("No optimal audio stream found");
}
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
var mediaSource = streamInfo.MediaSource;
- if (mediaSource.RunTimeTicks.HasValue)
+ if (mediaSource?.RunTimeTicks is not null)
{
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
}
@@ -634,7 +632,7 @@ namespace Emby.Dlna.Didl
// Samsung sometimes uses 1 as root
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
- public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
+ public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string? requestedId = null)
{
writer.WriteStartElement(string.Empty, "container", NsDidl);
@@ -678,14 +676,14 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
- private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo)
+ private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo)
{
if (!item.SupportsPositionTicksResume || item is Folder)
{
return;
}
- XmlAttribute secAttribute = null;
+ XmlAttribute? secAttribute = null;
foreach (var attribute in _profile.XmlRootAttributes)
{
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@@ -695,8 +693,8 @@ namespace Emby.Dlna.Didl
}
}
- // Not a samsung device
- if (secAttribute is null)
+ // Not a samsung device or no user data
+ if (secAttribute is null || user is null)
{
return;
}
@@ -717,7 +715,7 @@ namespace Emby.Dlna.Didl
///
/// Adds fields used by both items and folders.
///
- private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
+ private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
{
// Don't filter on dc:title because not all devices will include it in the filter
// MediaMonkey for example won't display content without a title
@@ -795,7 +793,7 @@ namespace Emby.Dlna.Didl
if (item.IsDisplayedAsFolder || stubType.HasValue)
{
- string classType = null;
+ string? classType = null;
if (!_profile.RequiresPlainFolders)
{
@@ -899,7 +897,7 @@ namespace Emby.Dlna.Didl
}
}
- private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
+ private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
{
AddCommonFields(item, itemStubType, context, writer, filter);
@@ -975,7 +973,7 @@ namespace Emby.Dlna.Didl
private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer)
{
- ImageDownloadInfo imageInfo = GetImageInfo(item);
+ ImageDownloadInfo? imageInfo = GetImageInfo(item);
if (imageInfo is null)
{
@@ -1073,7 +1071,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement();
}
- private ImageDownloadInfo GetImageInfo(BaseItem item)
+ private ImageDownloadInfo? GetImageInfo(BaseItem item)
{
if (item.HasImage(ImageType.Primary))
{
@@ -1118,7 +1116,7 @@ namespace Emby.Dlna.Didl
return null;
}
- private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
+ private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item)
{
if (item is null)
{
@@ -1148,7 +1146,7 @@ namespace Emby.Dlna.Didl
private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
{
var imageInfo = item.GetImageInfo(type, 0);
- string tag = null;
+ string? tag = null;
try
{
@@ -1250,7 +1248,7 @@ namespace Emby.Dlna.Didl
{
internal Guid ItemId { get; set; }
- internal string ImageTag { get; set; }
+ internal string? ImageTag { get; set; }
internal ImageType Type { get; set; }
@@ -1260,9 +1258,9 @@ namespace Emby.Dlna.Didl
internal bool IsDirectStream { get; set; }
- internal string Format { get; set; }
+ internal required string Format { get; set; }
- internal ItemImageInfo ItemImageInfo { get; set; }
+ internal required ItemImageInfo ItemImageInfo { get; set; }
}
}
}
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 99b3e6e7e..d67cb67b5 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -228,7 +228,7 @@ namespace Emby.Dlna
try
{
return _fileSystem.GetFilePaths(path)
- .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
+ .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
.Select(i => ParseProfileFile(i, type))
.Where(i => i is not null)
.ToList()!; // We just filtered out all the nulls
diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
new file mode 100644
index 000000000..87ec14d95
--- /dev/null
+++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using Emby.Dlna.ConnectionManager;
+using Emby.Dlna.ContentDirectory;
+using Emby.Dlna.MediaReceiverRegistrar;
+using Emby.Dlna.Ssdp;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Net;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Rssdp.Infrastructure;
+
+namespace Emby.Dlna.Extensions;
+
+///
+/// Extension methods for adding DLNA services.
+///
+public static class DlnaServiceCollectionExtensions
+{
+ ///
+ /// Adds DLNA services to the provided .
+ ///
+ /// The .
+ /// The .
+ public static void AddDlnaServices(
+ this IServiceCollection services,
+ IServerApplicationHost applicationHost)
+ {
+ services.AddHttpClient(NamedClient.Dlna, c =>
+ {
+ c.DefaultRequestHeaders.UserAgent.ParseAdd(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}/{1} UPnP/1.0 {2}/{3}",
+ Environment.OSVersion.Platform,
+ Environment.OSVersion,
+ applicationHost.Name,
+ applicationHost.ApplicationVersionString));
+
+ c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
+ c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from?
+ })
+ .ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler
+ {
+ AutomaticDecompression = DecompressionMethods.All,
+ RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
+ });
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddSingleton(provider => new SsdpCommunicationsServer(
+ provider.GetRequiredService(),
+ provider.GetRequiredService(),
+ provider.GetRequiredService>())
+ {
+ IsShared = true
+ });
+ }
+}
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index 39cfc2d1d..aa7012487 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -11,7 +11,7 @@ using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
using Jellyfin.Networking.Configuration;
-using Jellyfin.Networking.Manager;
+using Jellyfin.Networking.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
@@ -23,10 +23,8 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
using Rssdp;
using Rssdp.Infrastructure;
@@ -49,14 +47,13 @@ namespace Emby.Dlna.Main
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery;
- private readonly ISocketFactory _socketFactory;
+ private readonly ISsdpCommunicationsServer _communicationsServer;
private readonly INetworkManager _networkManager;
- private readonly object _syncLock = new object();
+ private readonly object _syncLock = new();
private readonly bool _disabled;
private PlayToManager _manager;
private SsdpDevicePublisher _publisher;
- private ISsdpCommunicationsServer _communicationsServer;
private bool _disposed;
@@ -75,10 +72,8 @@ namespace Emby.Dlna.Main
IMediaSourceManager mediaSourceManager,
IDeviceDiscovery deviceDiscovery,
IMediaEncoder mediaEncoder,
- ISocketFactory socketFactory,
- INetworkManager networkManager,
- IUserViewManager userViewManager,
- ITVSeriesManager tvSeriesManager)
+ ISsdpCommunicationsServer communicationsServer,
+ INetworkManager networkManager)
{
_config = config;
_appHost = appHost;
@@ -93,37 +88,10 @@ namespace Emby.Dlna.Main
_mediaSourceManager = mediaSourceManager;
_deviceDiscovery = deviceDiscovery;
_mediaEncoder = mediaEncoder;
- _socketFactory = socketFactory;
+ _communicationsServer = communicationsServer;
_networkManager = networkManager;
_logger = loggerFactory.CreateLogger();
- ContentDirectory = new ContentDirectory.ContentDirectoryService(
- dlnaManager,
- userDataManager,
- imageProcessor,
- libraryManager,
- config,
- userManager,
- loggerFactory.CreateLogger(),
- httpClientFactory,
- localizationManager,
- mediaSourceManager,
- userViewManager,
- mediaEncoder,
- tvSeriesManager);
-
- ConnectionManager = new ConnectionManager.ConnectionManagerService(
- dlnaManager,
- config,
- loggerFactory.CreateLogger(),
- httpClientFactory);
-
- MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
- loggerFactory.CreateLogger(),
- httpClientFactory,
- config);
- Current = this;
-
var netConfig = config.GetConfiguration(NetworkConfigurationStore.StoreKey);
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
@@ -133,19 +101,6 @@ namespace Emby.Dlna.Main
}
}
- public static DlnaEntryPoint Current { get; private set; }
-
- ///
- /// Gets a value indicating whether the dlna server is enabled.
- ///
- public static bool Enabled { get; private set; }
-
- public IContentDirectory ContentDirectory { get; private set; }
-
- public IConnectionManager ConnectionManager { get; private set; }
-
- public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
-
public async Task RunAsync()
{
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
@@ -172,9 +127,7 @@ namespace Emby.Dlna.Main
private void ReloadComponents()
{
var options = _config.GetDlnaConfiguration();
- Enabled = options.EnableServer;
-
- StartSsdpHandler();
+ StartDeviceDiscovery();
if (options.EnableServer)
{
@@ -195,37 +148,11 @@ namespace Emby.Dlna.Main
}
}
- private void StartSsdpHandler()
+ private void StartDeviceDiscovery()
{
try
{
- if (_communicationsServer is null)
- {
- var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
- OperatingSystem.IsLinux();
-
- _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
- {
- IsShared = true
- };
-
- StartDeviceDiscovery(_communicationsServer);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error starting ssdp handlers");
- }
- }
-
- private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
- {
- try
- {
- if (communicationsServer is not null)
- {
- ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
- }
+ ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
}
catch (Exception ex)
{
@@ -233,26 +160,8 @@ namespace Emby.Dlna.Main
}
}
- private void DisposeDeviceDiscovery()
- {
- try
- {
- _logger.LogInformation("Disposing DeviceDiscovery");
- ((DeviceDiscovery)_deviceDiscovery).Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error stopping device discovery");
- }
- }
-
public void StartDevicePublisher(Configuration.DlnaOptions options)
{
- if (!options.BlastAliveMessages)
- {
- return;
- }
-
if (_publisher is not null)
{
return;
@@ -263,7 +172,8 @@ namespace Emby.Dlna.Main
_publisher = new SsdpDevicePublisher(
_communicationsServer,
Environment.OSVersion.Platform.ToString(),
- Environment.OSVersion.VersionString,
+ // Can not use VersionString here since that includes OS and version
+ Environment.OSVersion.Version.ToString(),
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
@@ -272,7 +182,10 @@ namespace Emby.Dlna.Main
RegisterServerEndpoints();
- _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
+ if (options.BlastAliveMessages)
+ {
+ _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
+ }
}
catch (Exception ex)
{
@@ -285,42 +198,33 @@ namespace Emby.Dlna.Main
var udn = CreateUuid(_appHost.SystemId);
var descriptorUri = "/dlna/" + udn + "/description.xml";
- var bindAddresses = NetworkManager.CreateCollection(
- _networkManager.GetInternalBindAddresses()
- .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)));
+ // Only get bind addresses in LAN
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses()
+ .Where(x => x.Address is not null)
+ .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6)
+ .ToList();
- if (bindAddresses.Count == 0)
+ if (validInterfaces.Count == 0)
{
- // No interfaces returned, so use loopback.
- bindAddresses = _networkManager.GetLoopbacks();
+ // No interfaces returned, fall back to loopback
+ validInterfaces = _networkManager.GetLoopbacks().ToList();
}
- foreach (IPNetAddress address in bindAddresses)
+ foreach (var intf in validInterfaces)
{
- if (address.AddressFamily == AddressFamily.InterNetworkV6)
- {
- // Not supporting IPv6 right now
- continue;
- }
-
- // Limit to LAN addresses only
- if (!_networkManager.IsInLocalNetwork(address))
- {
- continue;
- }
-
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
- _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
+ _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
- var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri);
+ var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri);
var device = new SsdpRootDevice
{
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
- Address = address.Address,
- PrefixLength = address.PrefixLength,
+ Address = intf.Address,
+ PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix),
FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server",
@@ -328,7 +232,7 @@ namespace Emby.Dlna.Main
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
};
- SetProperies(device, fullService);
+ SetProperties(device, fullService);
_publisher.AddDevice(device);
var embeddedDevices = new[]
@@ -349,13 +253,13 @@ namespace Emby.Dlna.Main
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
};
- SetProperies(embeddedDevice, subDevice);
+ SetProperties(embeddedDevice, subDevice);
device.AddDevice(embeddedDevice);
}
}
}
- private string CreateUuid(string text)
+ private static string CreateUuid(string text)
{
if (!Guid.TryParse(text, out var guid))
{
@@ -365,15 +269,14 @@ namespace Emby.Dlna.Main
return guid.ToString("D", CultureInfo.InvariantCulture);
}
- private void SetProperies(SsdpDevice device, string fullDeviceType)
+ private static void SetProperties(SsdpDevice device, string fullDeviceType)
{
- var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
+ var serviceParts = fullDeviceType
+ .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Split(':');
- var serviceParts = service.Split(':');
-
- var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
-
- device.DeviceTypeNamespace = deviceTypeNamespace;
+ device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
device.DeviceClass = serviceParts[1];
device.DeviceType = serviceParts[2];
}
@@ -454,20 +357,6 @@ namespace Emby.Dlna.Main
DisposeDevicePublisher();
DisposePlayToManager();
- DisposeDeviceDiscovery();
-
- if (_communicationsServer is not null)
- {
- _logger.LogInformation("Disposing SsdpCommunicationsServer");
- _communicationsServer.Dispose();
- _communicationsServer = null;
- }
-
- ContentDirectory = null;
- ConnectionManager = null;
- MediaReceiverRegistrar = null;
- Current = null;
-
_disposed = true;
}
}
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 9c476119d..bb9b8b0fd 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -25,7 +23,7 @@ namespace Emby.Dlna.PlayTo
private readonly ILogger _logger;
private readonly object _timerLock = new object();
- private Timer _timer;
+ private Timer? _timer;
private int _muteVol;
private int _volume;
private DateTime _lastVolumeRefresh;
@@ -40,13 +38,13 @@ namespace Emby.Dlna.PlayTo
_logger = logger;
}
- public event EventHandler PlaybackStart;
+ public event EventHandler? PlaybackStart;
- public event EventHandler PlaybackProgress;
+ public event EventHandler? PlaybackProgress;
- public event EventHandler PlaybackStopped;
+ public event EventHandler? PlaybackStopped;
- public event EventHandler MediaChanged;
+ public event EventHandler? MediaChanged;
public DeviceInfo Properties { get; set; }
@@ -75,13 +73,13 @@ namespace Emby.Dlna.PlayTo
public bool IsStopped => TransportState == TransportState.STOPPED;
- public Action OnDeviceUnavailable { get; set; }
+ public Action? OnDeviceUnavailable { get; set; }
- private TransportCommands AvCommands { get; set; }
+ private TransportCommands? AvCommands { get; set; }
- private TransportCommands RendererCommands { get; set; }
+ private TransportCommands? RendererCommands { get; set; }
- public UBaseObject CurrentMediaInfo { get; private set; }
+ public UBaseObject? CurrentMediaInfo { get; private set; }
public void Start()
{
@@ -131,7 +129,7 @@ namespace Emby.Dlna.PlayTo
_volumeRefreshActive = true;
var time = immediate ? 100 : 10000;
- _timer.Change(time, Timeout.Infinite);
+ _timer?.Change(time, Timeout.Infinite);
}
}
@@ -149,7 +147,7 @@ namespace Emby.Dlna.PlayTo
_volumeRefreshActive = false;
- _timer.Change(Timeout.Infinite, Timeout.Infinite);
+ _timer?.Change(Timeout.Infinite, Timeout.Infinite);
}
}
@@ -199,7 +197,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private DeviceService GetServiceRenderingControl()
+ private DeviceService? GetServiceRenderingControl()
{
var services = Properties.Services;
@@ -207,7 +205,7 @@ namespace Emby.Dlna.PlayTo
services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase));
}
- private DeviceService GetAvTransportService()
+ private DeviceService? GetAvTransportService()
{
var services = Properties.Services;
@@ -240,7 +238,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
- rendererCommands.BuildPost(command, service.ServiceType, value),
+ rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@@ -265,12 +263,7 @@ namespace Emby.Dlna.PlayTo
return;
}
- var service = GetServiceRenderingControl();
-
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
+ var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service");
// Set it early and assume it will succeed
// Remote control will perform better
@@ -281,7 +274,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
- rendererCommands.BuildPost(command, service.ServiceType, value),
+ rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
}
@@ -296,26 +289,20 @@ namespace Emby.Dlna.PlayTo
return;
}
- var service = GetAvTransportService();
-
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
-
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
- avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
+ avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
RestartTimer(true);
}
- public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken)
+ public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
@@ -335,14 +322,8 @@ namespace Emby.Dlna.PlayTo
{ "CurrentURIMetaData", CreateDidlMeta(metaData) }
};
- var service = GetAvTransportService();
-
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
-
- var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
+ var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
@@ -372,7 +353,7 @@ namespace Emby.Dlna.PlayTo
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
* Without that information, the next track command on the device does not work.
*/
- public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
+ public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default)
{
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
@@ -380,7 +361,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
- var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
+ var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
if (command is null)
{
return;
@@ -392,14 +373,8 @@ namespace Emby.Dlna.PlayTo
{ "NextURIMetaData", CreateDidlMeta(metaData) }
};
- var service = GetAvTransportService();
-
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
-
- var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
+ var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
.ConfigureAwait(false);
@@ -423,12 +398,7 @@ namespace Emby.Dlna.PlayTo
return Task.CompletedTask;
}
- var service = GetAvTransportService();
- if (service is null)
- {
- throw new InvalidOperationException("Unable to find service");
- }
-
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl,
service,
@@ -460,14 +430,13 @@ namespace Emby.Dlna.PlayTo
return;
}
- var service = GetAvTransportService();
-
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
- avCommands.BuildPost(command, service.ServiceType, 1),
+ avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@@ -484,14 +453,13 @@ namespace Emby.Dlna.PlayTo
return;
}
- var service = GetAvTransportService();
-
+ var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(
Properties.BaseUrl,
service,
command.Name,
- avCommands.BuildPost(command, service.ServiceType, 1),
+ avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
cancellationToken: cancellationToken)
.ConfigureAwait(false);
@@ -500,7 +468,7 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true);
}
- private async void TimerCallback(object sender)
+ private async void TimerCallback(object? sender)
{
if (_disposed)
{
@@ -623,7 +591,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
- rendererCommands.BuildPost(command, service.ServiceType),
+ rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result is null || result.Document is null)
@@ -673,7 +641,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl,
service,
command.Name,
- rendererCommands.BuildPost(command, service.ServiceType),
+ rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
cancellationToken: cancellationToken).ConfigureAwait(false);
if (result is null || result.Document is null)
@@ -728,7 +696,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private async Task GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+ private async Task GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
if (command is null)
@@ -798,7 +766,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+ private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
if (command is null)
@@ -871,7 +839,7 @@ namespace Emby.Dlna.PlayTo
return (true, null);
}
- XElement uPnpResponse = null;
+ XElement? uPnpResponse = null;
try
{
@@ -895,7 +863,7 @@ namespace Emby.Dlna.PlayTo
return (true, uTrack);
}
- private XElement ParseResponse(string xml)
+ private XElement? ParseResponse(string xml)
{
// Handle different variations sent back by devices.
try
@@ -929,7 +897,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
+ private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri)
{
ArgumentNullException.ThrowIfNull(container);
@@ -959,20 +927,17 @@ namespace Emby.Dlna.PlayTo
var resElement = container.Element(UPnpNamespaces.Res);
- if (resElement is not null)
- {
- var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
+ var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo);
- if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
- {
- return info.Value.Split(':');
- }
+ if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
+ {
+ return info.Value.Split(':');
}
return new string[4];
}
- private async Task GetAVProtocolAsync(CancellationToken cancellationToken)
+ private async Task GetAVProtocolAsync(CancellationToken cancellationToken)
{
if (AvCommands is not null)
{
@@ -1004,7 +969,7 @@ namespace Emby.Dlna.PlayTo
return AvCommands;
}
- private async Task GetRenderingProtocolAsync(CancellationToken cancellationToken)
+ private async Task GetRenderingProtocolAsync(CancellationToken cancellationToken)
{
if (RendererCommands is not null)
{
@@ -1054,7 +1019,7 @@ namespace Emby.Dlna.PlayTo
return baseUrl + url;
}
- public static async Task CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
+ public static async Task CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
{
var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
@@ -1171,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
return new Device(deviceProperties, httpClientFactory, logger);
}
-#nullable enable
private static DeviceIcon CreateIcon(XElement element)
{
ArgumentNullException.ThrowIfNull(element);
@@ -1287,7 +1251,7 @@ namespace Emby.Dlna.PlayTo
}
_timer = null;
- Properties = null;
+ Properties = null!;
_disposed = true;
}
diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
index 8b983e9e3..255c51f19 100644
--- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs
+++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
@@ -31,6 +31,9 @@ namespace Emby.Dlna.PlayTo
_httpClientFactory = httpClientFactory;
}
+ [GeneratedRegex("(&(?![a-z]*;))")]
+ private static partial Regex EscapeAmpersandRegex();
+
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
{
// If it's already a complete url, don't stick anything onto the front of it
@@ -52,40 +55,42 @@ namespace Emby.Dlna.PlayTo
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using MemoryStream ms = new MemoryStream();
- await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
- try
+ Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
{
- return await XDocument.LoadAsync(
- ms,
- LoadOptions.None,
- cancellationToken).ConfigureAwait(false);
- }
- catch (XmlException)
- {
- // try correcting the Xml response with common errors
- ms.Position = 0;
- using StreamReader sr = new StreamReader(ms);
- var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
-
- // find and replace unescaped ampersands (&)
- xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
-
try
{
- // retry reading Xml
- using var xmlReader = new StringReader(xmlString);
return await XDocument.LoadAsync(
- xmlReader,
+ stream,
LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
- catch (XmlException ex)
+ catch (XmlException)
{
- _logger.LogError(ex, "Failed to parse response");
- _logger.LogDebug("Malformed response: {Content}\n", xmlString);
+ // try correcting the Xml response with common errors
+ stream.Position = 0;
+ using StreamReader sr = new StreamReader(stream);
+ var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
- return null;
+ // find and replace unescaped ampersands (&)
+ xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
+
+ try
+ {
+ // retry reading Xml
+ using var xmlReader = new StringReader(xmlString);
+ return await XDocument.LoadAsync(
+ xmlReader,
+ LoadOptions.None,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (XmlException ex)
+ {
+ _logger.LogError(ex, "Failed to parse response");
+ _logger.LogDebug("Malformed response: {Content}\n", xmlString);
+
+ return null;
+ }
}
}
}
@@ -128,12 +133,5 @@ namespace Emby.Dlna.PlayTo
// Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
}
-
- ///
- /// Compile-time generated regular expression for escaping ampersands.
- ///
- /// Compiled regular expression.
- [GeneratedRegex("(&(?![a-z]*;))")]
- private static partial Regex EscapeAmpersandRegex();
}
}
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 86db36337..b1ad15cdc 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -42,7 +42,7 @@ namespace Emby.Dlna.PlayTo
private readonly IDeviceDiscovery _deviceDiscovery;
private readonly string _serverAddress;
- private readonly string _accessToken;
+ private readonly string? _accessToken;
private readonly List _playlist = new List();
private Device _device;
@@ -59,7 +59,7 @@ namespace Emby.Dlna.PlayTo
IUserManager userManager,
IImageProcessor imageProcessor,
string serverAddress,
- string accessToken,
+ string? accessToken,
IDeviceDiscovery deviceDiscovery,
IUserDataManager userDataManager,
ILocalizationManager localization,
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index b469c9cb0..b05e0a095 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -41,9 +39,9 @@ namespace Emby.Dlna.PlayTo
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
+ private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
+ private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
private bool _disposed;
- private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
- 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, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{
@@ -67,7 +65,7 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
}
- private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs e)
+ private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs e)
{
if (_disposed)
{
@@ -76,12 +74,12 @@ namespace Emby.Dlna.PlayTo
var info = e.Argument;
- if (!info.Headers.TryGetValue("USN", out string usn))
+ if (!info.Headers.TryGetValue("USN", out string? usn))
{
usn = string.Empty;
}
- if (!info.Headers.TryGetValue("NT", out string nt))
+ if (!info.Headers.TryGetValue("NT", out string? nt))
{
nt = string.Empty;
}
@@ -161,7 +159,7 @@ namespace Emby.Dlna.PlayTo
var uri = info.Location;
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
- if (info.Headers.TryGetValue("USN", out string uuid))
+ if (info.Headers.TryGetValue("USN", out string? uuid))
{
uuid = GetUuid(uuid);
}
@@ -189,7 +187,7 @@ namespace Emby.Dlna.PlayTo
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
- string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
+ string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress);
controller = new PlayToController(
sessionInfo,
diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
index 8a4e5ff45..4fbbc3885 100644
--- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs
+++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs
@@ -73,7 +73,11 @@ namespace Emby.Dlna.Ssdp
{
if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null)
{
- _deviceLocator = new SsdpDeviceLocator(_commsServer);
+ _deviceLocator = new SsdpDeviceLocator(
+ _commsServer,
+ Environment.OSVersion.Platform.ToString(),
+ // Can not use VersionString here since that includes OS and version
+ Environment.OSVersion.Version.ToString());
// (Optional) Set the filter so we only see notifications for devices we care about
// (can be any search target value i.e device type, uuid value etc - any value that appears in the
@@ -106,7 +110,7 @@ namespace Emby.Dlna.Ssdp
{
Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers,
- RemoteIpAddress = e.RemoteIpAddress
+ RemoteIPAddress = e.RemoteIPAddress
});
DeviceDiscoveredInternal?.Invoke(this, args);
diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs
index 86a564153..97961778f 100644
--- a/Emby.Naming/Audio/AlbumParser.cs
+++ b/Emby.Naming/Audio/AlbumParser.cs
@@ -10,7 +10,7 @@ namespace Emby.Naming.Audio
///
/// Helper class to determine if Album is multipart.
///
- public class AlbumParser
+ public partial class AlbumParser
{
private readonly NamingOptions _options;
@@ -23,6 +23,9 @@ namespace Emby.Naming.Audio
_options = options;
}
+ [GeneratedRegex(@"[-\.\(\)\s]+")]
+ private static partial Regex CleanRegex();
+
///
/// Function that determines if album is multipart.
///
@@ -42,13 +45,9 @@ namespace Emby.Naming.Audio
// Normalize
// Remove whitespace
- filename = filename.Replace('-', ' ');
- filename = filename.Replace('.', ' ');
- filename = filename.Replace('(', ' ');
- filename = filename.Replace(')', ' ');
- filename = Regex.Replace(filename, @"\s+", " ");
+ filename = CleanRegex().Replace(filename, " ");
- ReadOnlySpan trimmedFilename = filename.TrimStart();
+ ReadOnlySpan trimmedFilename = filename.AsSpan().TrimStart();
foreach (var prefix in _options.AlbumStackingPrefixes)
{
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index a069da102..b63c8f10e 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -318,22 +318,24 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
//
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
- new EpisodeExpression("(?[0-9]{4})[\\.-](?[0-9]{2})[\\.-](?[0-9]{2})", true)
+ new EpisodeExpression("(?[0-9]{4})[._ -](?[0-9]{2})[._ -](?[0-9]{2})", true)
{
DateTimeFormats = new[]
{
"yyyy.MM.dd",
"yyyy-MM-dd",
- "yyyy_MM_dd"
+ "yyyy_MM_dd",
+ "yyyy MM dd"
}
},
- new EpisodeExpression(@"(?[0-9]{2})[.-](?[0-9]{2})[.-](?[0-9]{4})", true)
+ new EpisodeExpression("(?[0-9]{2})[._ -](?[0-9]{2})[._ -](?[0-9]{4})", true)
{
DateTimeFormats = new[]
{
"dd.MM.yyyy",
"dd-MM-yyyy",
- "dd_MM_yyyy"
+ "dd_MM_yyyy",
+ "dd MM yyyy"
}
},
@@ -374,7 +376,7 @@ namespace Emby.Naming.Common
IsNamed = true,
SupportsAbsoluteEpisodeNumbers = false
},
- new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$")
+ new EpisodeExpression(@"[\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\/]*)$")
{
SupportsAbsoluteEpisodeNumbers = true
},
@@ -415,7 +417,7 @@ namespace Emby.Naming.Common
},
// "1-12 episode title"
- new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
+ new EpisodeExpression("([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?[0-9]{1,3})(-(?[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
@@ -710,7 +712,7 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename
"^(?[0-9]+)",
// Part if often ending of filename
- @"(?[0-9]+)$",
+ "(?[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part)
"(?[0-9]+)_(?[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
index 953129671..4080ba10d 100644
--- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
return null;
}
- var extension = Path.GetExtension(path);
+ var extension = Path.GetExtension(path.AsSpan());
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
{
diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs
index 307a84096..d8fa41743 100644
--- a/Emby.Naming/TV/SeriesResolver.cs
+++ b/Emby.Naming/TV/SeriesResolver.cs
@@ -7,14 +7,15 @@ namespace Emby.Naming.TV
///
/// Used to resolve information about series from path.
///
- public static class SeriesResolver
+ public static partial class SeriesResolver
{
///
/// 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".
///
- private static readonly Regex _seriesNameRegex = new Regex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))", RegexOptions.Compiled);
+ [GeneratedRegex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))")]
+ private static partial Regex SeriesNameRegex();
///
/// Resolve information about series from path.
@@ -37,7 +38,7 @@ namespace Emby.Naming.TV
if (!string.IsNullOrEmpty(seriesName))
{
- seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
+ seriesName = SeriesNameRegex().Replace(seriesName, "${a} ${b}").Trim();
}
return new SeriesInfo(path)
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index f7ba606e3..4b9df19b0 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -26,19 +26,18 @@ namespace Emby.Naming.Video
return false;
}
- var extension = Path.GetExtension(path);
+ var extension = Path.GetExtension(path.AsSpan());
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
- path = Path.GetFileNameWithoutExtension(path);
- var token = Path.GetExtension(path).TrimStart('.');
+ var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
foreach (var rule in options.StubTypes)
{
- if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
+ if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
stubType = rule.StubType;
return true;
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 6209cd46f..51f29cf08 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -12,9 +12,13 @@ namespace Emby.Naming.Video
///
/// Resolves alternative versions and extras from list of video files.
///
- public static class VideoListResolver
+ public static partial class VideoListResolver
{
- private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+ [GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
+ private static partial Regex ResolutionRegex();
+
+ [GeneratedRegex(@"^\[([^]]*)\]")]
+ private static partial Regex CheckMultiVersionRegex();
///
/// Resolves alternative versions and extras from list of video files.
@@ -131,7 +135,7 @@ namespace Emby.Naming.Video
if (videos.Count > 1)
{
- var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
+ var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
videos.Clear();
foreach (var group in groups)
{
@@ -201,7 +205,7 @@ namespace Emby.Naming.Video
// The CleanStringParser should have removed common keywords etc.
return testFilename.IsEmpty
|| testFilename[0] == '-'
- || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+ || CheckMultiVersionRegex().IsMatch(testFilename);
}
}
}
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index f54066c57..27329a7f2 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -61,7 +61,7 @@ namespace Emby.Photos
item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
- if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
+ if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
{
try
{
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 6edfad575..39524be1d 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase
///
public abstract class BaseApplicationPaths : IApplicationPaths
{
- private string _dataPath;
-
///
/// Initializes a new instance of the class.
///
@@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
- _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
+ DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
///
@@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
///
/// The data directory.
- public string DataPath => _dataPath;
+ public string DataPath { get; }
///
public string VirtualDataPath => "%AppDataPath%";
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index a4deeddb7..a2f38c8c2 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
///
public abstract class BaseConfigurationManager : IConfigurationManager
{
- private readonly IFileSystem _fileSystem;
-
- private readonly ConcurrentDictionary _configurations = new ConcurrentDictionary();
-
- ///
- /// The _configuration sync lock.
- ///
- private readonly object _configurationSyncLock = new object();
+ private readonly ConcurrentDictionary _configurations = new();
+ private readonly object _configurationSyncLock = new();
private ConfigurationStore[] _configurationStores = Array.Empty();
private IConfigurationFactory[] _configurationFactories = Array.Empty();
@@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
/// The application paths.
/// The logger factory.
/// The XML serializer.
- /// The file system.
- protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
+ protected BaseConfigurationManager(
+ IApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IXmlSerializer xmlSerializer)
{
CommonApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
- _fileSystem = fileSystem;
Logger = loggerFactory.CreateLogger();
UpdateCachePath();
@@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
{
var file = Path.Combine(path, Guid.NewGuid().ToString());
File.WriteAllText(file, string.Empty);
- _fileSystem.DeleteFile(file);
+ File.Delete(file);
}
private string GetConfigurationFile(string key)
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 7969577bc..c9bf7f085 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -12,11 +12,8 @@ using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
-using System.Threading;
using System.Threading.Tasks;
-using Emby.Dlna;
using Emby.Dlna.Main;
-using Emby.Dlna.Ssdp;
using Emby.Naming.Common;
using Emby.Photos;
using Emby.Server.Implementations.Channels;
@@ -59,7 +56,6 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -83,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
@@ -112,7 +107,7 @@ namespace Emby.Server.Implementations
///
/// Class CompositionRoot.
///
- public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
+ public abstract class ApplicationHost : IServerApplicationHost, IDisposable
{
///
/// The disposable parts.
@@ -120,14 +115,12 @@ namespace Emby.Server.Implementations
private readonly ConcurrentDictionary _disposableParts = new();
private readonly DeviceId _deviceId;
- private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private List _creatingInstances;
- private ISessionManager _sessionManager;
///
/// Gets or sets all concrete types.
@@ -135,7 +128,7 @@ namespace Emby.Server.Implementations
/// All concrete types.
private Type[] _allConcreteTypes;
- private bool _disposed = false;
+ private bool _disposed;
///
/// Initializes a new instance of the class.
@@ -154,10 +147,8 @@ namespace Emby.Server.Implementations
LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
- _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger(), applicationPaths);
Logger = LoggerFactory.CreateLogger();
- _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
@@ -165,13 +156,15 @@ namespace Emby.Server.Implementations
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer();
- ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
+ ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger(),
this,
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
+
+ _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
}
///
@@ -186,23 +179,16 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; }
- public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
- && !_startupOptions.IsService
- && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
-
///
/// Gets the singleton instance.
///
public INetworkManager NetManager { get; private set; }
- ///
- /// Gets a value indicating whether this instance has changes that require the entire application to restart.
- ///
- /// true if this instance has pending application restart; otherwise, false.
+ ///
public bool HasPendingRestart { get; private set; }
///
- public bool IsShuttingDown { get; private set; }
+ public bool ShouldRestart { get; set; }
///
/// Gets the logger.
@@ -406,11 +392,9 @@ namespace Emby.Server.Implementations
///
/// Runs the startup tasks.
///
- /// The cancellation token.
/// .
- public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
+ public async Task RunStartupTasksAsync()
{
- cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks");
Resolve().AddTasks(GetExports(false));
@@ -424,8 +408,6 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports();
- cancellationToken.ThrowIfCancellationRequested();
-
var stopWatch = new Stopwatch();
stopWatch.Start();
@@ -435,8 +417,6 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true;
- cancellationToken.ThrowIfCancellationRequested();
-
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@@ -466,7 +446,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.AddParts(GetExports());
- NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger());
+ NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger());
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
@@ -475,8 +455,8 @@ namespace Emby.Server.Implementations
}
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
- HttpPort = networkConfiguration.HttpServerPortNumber;
- HttpsPort = networkConfiguration.HttpsPortNumber;
+ HttpPort = networkConfiguration.InternalHttpPort;
+ HttpsPort = networkConfiguration.InternalHttpsPort;
// Safeguard against invalid configuration
if (HttpPort == HttpsPort)
@@ -509,7 +489,11 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(_pluginManager);
serviceCollection.AddSingleton(ApplicationPaths);
- serviceCollection.AddSingleton(_fileSystemManager);
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+
+ serviceCollection.AddScoped();
+
serviceCollection.AddSingleton();
serviceCollection.AddSingleton(NetManager);
@@ -575,8 +559,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
-
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
@@ -588,8 +570,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
-
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
@@ -633,8 +613,6 @@ namespace Emby.Server.Implementations
var localizationManager = (LocalizationManager)Resolve();
await localizationManager.LoadAll().ConfigureAwait(false);
- _sessionManager = Resolve();
-
SetStaticProperties();
FindParts();
@@ -685,7 +663,7 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve();
BaseItem.LocalizationManager = Resolve();
BaseItem.ItemRepository = Resolve();
- BaseItem.FileSystem = _fileSystemManager;
+ BaseItem.FileSystem = Resolve();
BaseItem.UserDataManager = Resolve();
BaseItem.ChannelManager = Resolve();
Video.LiveTvManager = Resolve();
@@ -785,8 +763,8 @@ namespace Emby.Server.Implementations
if (HttpPort != 0 && HttpsPort != 0)
{
// Need to restart if ports have changed
- if (networkConfiguration.HttpServerPortNumber != HttpPort
- || networkConfiguration.HttpsPortNumber != HttpsPort)
+ if (networkConfiguration.InternalHttpPort != HttpPort
+ || networkConfiguration.InternalHttpsPort != HttpsPort)
{
if (ConfigurationManager.Configuration.IsPortAuthorized)
{
@@ -855,38 +833,6 @@ namespace Emby.Server.Implementations
}
}
- ///
- /// Restarts this instance.
- ///
- public void Restart()
- {
- if (IsShuttingDown)
- {
- return;
- }
-
- IsShuttingDown = true;
- _pluginManager.UnloadAssemblies();
-
- Task.Run(async () =>
- {
- try
- {
- await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error sending server restart notification");
- }
-
- Logger.LogInformation("Calling RestartInternal");
-
- RestartInternal();
- });
- }
-
- protected abstract void RestartInternal();
-
///
/// Gets the composable part assemblies.
///
@@ -942,49 +888,6 @@ namespace Emby.Server.Implementations
protected abstract IEnumerable GetAssembliesWithPartsInternal();
- ///
- /// Gets the system status.
- ///
- /// Where this request originated.
- /// SystemInfo.
- public SystemInfo GetSystemInfo(HttpRequest request)
- {
- return new SystemInfo
- {
- HasPendingRestart = HasPendingRestart,
- IsShuttingDown = IsShuttingDown,
- Version = ApplicationVersionString,
- WebSocketPortNumber = HttpPort,
- CompletedInstallations = Resolve().CompletedInstallations.ToArray(),
- Id = SystemId,
- ProgramDataPath = ApplicationPaths.ProgramDataPath,
- WebPath = ApplicationPaths.WebPath,
- LogPath = ApplicationPaths.LogDirectoryPath,
- ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
- InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
- CachePath = ApplicationPaths.CachePath,
- CanLaunchWebBrowser = CanLaunchWebBrowser,
- TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
- ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(request),
- SupportsLibraryMonitor = true,
- PackageName = _startupOptions.PackageName
- };
- }
-
- public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
- {
- return new PublicSystemInfo
- {
- Version = ApplicationVersionString,
- ProductName = ApplicationProductName,
- Id = SystemId,
- ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(request),
- StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
- };
- }
-
///
public string GetSmartApiUrl(IPAddress remoteAddr)
{
@@ -995,18 +898,20 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(remoteAddr, out var port);
+ string smart = NetManager.GetBindAddress(remoteAddr, out var port);
return GetLocalApiUrl(smart.Trim('/'), null, port);
}
///
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 not configured otherwise
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
{
int? requestPort = request.Host.Port;
- if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
+ if (requestPort is null
+ || (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
+ || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
{
requestPort = -1;
}
@@ -1027,15 +932,15 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
- string smart = NetManager.GetBindInterface(hostname, out var port);
+ string smart = NetManager.GetBindAddress(hostname, out var port);
return GetLocalApiUrl(smart.Trim('/'), null, port);
}
///
- public string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true)
+ public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
{
// With an empty source, the port will be null
- var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _);
+ var smart = NetManager.GetBindAddress(ipAddress, out _, false);
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart, scheme, port);
@@ -1063,30 +968,6 @@ namespace Emby.Server.Implementations
}.ToString().TrimEnd('/');
}
- ///
- public async Task Shutdown()
- {
- if (IsShuttingDown)
- {
- return;
- }
-
- IsShuttingDown = true;
-
- try
- {
- await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error sending server shutdown notification");
- }
-
- ShutdownInternal();
- }
-
- protected abstract void ShutdownInternal();
-
public IEnumerable GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes
@@ -1150,52 +1031,5 @@ namespace Emby.Server.Implementations
_disposed = true;
}
-
- public async ValueTask DisposeAsync()
- {
- await DisposeAsyncCore().ConfigureAwait(false);
- Dispose(false);
- GC.SuppressFinalize(this);
- }
-
- ///
- /// Used to perform asynchronous cleanup of managed resources or for cascading calls to .
- ///
- /// A ValueTask.
- protected virtual async ValueTask DisposeAsyncCore()
- {
- var type = GetType();
-
- Logger.LogInformation("Disposing {Type}", type.Name);
-
- foreach (var (part, _) in _disposableParts)
- {
- var partType = part.GetType();
- if (partType == type)
- {
- continue;
- }
-
- Logger.LogInformation("Disposing {Type}", partType.Name);
-
- try
- {
- part.Dispose();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error disposing {Type}", partType.Name);
- }
- }
-
- if (_sessionManager != null)
- {
- // used for closing websockets
- foreach (var session in _sessionManager.Sessions)
- {
- await session.DisposeAsync().ConfigureAwait(false);
- }
- }
- }
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 961e225e9..8279acb05 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -371,8 +371,11 @@ namespace Emby.Server.Implementations.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path));
- await using FileStream createStream = File.Create(path);
- await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
+ FileStream createStream = File.Create(path);
+ await using (createStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
+ }
}
///
@@ -1156,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels
if (info.People is not null && info.People.Count > 0)
{
- _libraryManager.UpdatePeople(item, info.People);
+ await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
}
}
else if (forceUpdate)
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index 6b8b1a620..0ee43ce0a 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
/// Initializes a new instance of the class.
///
/// The application paths.
- /// The paramref name="loggerFactory" factory.
+ /// The logger factory.
/// The XML serializer.
- /// The file system.
- public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
- : base(applicationPaths, loggerFactory, xmlSerializer, fileSystem)
+ public ServerConfigurationManager(
+ IApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IXmlSerializer xmlSerializer)
+ : base(applicationPaths, loggerFactory, xmlSerializer)
{
UpdateMetadataPath();
}
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index d05534ee7..bf079d90c 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -5,8 +5,8 @@
using System;
using System.Collections.Generic;
using Jellyfin.Extensions;
+using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
@@ -45,24 +45,6 @@ namespace Emby.Server.Implementations.Data
/// The logger.
protected ILogger Logger { get; }
- ///
- /// Gets the default connection flags.
- ///
- /// The default connection flags.
- protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex;
-
- ///
- /// Gets the transaction mode.
- ///
- /// The transaction mode.>
- protected TransactionMode TransactionMode => TransactionMode.Deferred;
-
- ///
- /// Gets the transaction mode for read-only operations.
- ///
- /// The transaction mode.
- protected TransactionMode ReadTransactionMode => TransactionMode.Deferred;
-
///
/// Gets the cache size.
///
@@ -107,23 +89,8 @@ namespace Emby.Server.Implementations.Data
///
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
- ///
- /// Gets or sets the write lock.
- ///
- /// The write lock.
- protected ConnectionPool WriteConnections { get; set; }
-
- ///
- /// Gets or sets the write connection.
- ///
- /// The write connection.
- protected ConnectionPool ReadConnections { get; set; }
-
public virtual void Initialize()
{
- WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection);
- ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection);
-
// Configuration and pragmas can affect VACUUM so it needs to be last.
using (var connection = GetConnection())
{
@@ -131,57 +98,10 @@ namespace Emby.Server.Implementations.Data
}
}
- protected ManagedConnection GetConnection(bool readOnly = false)
- => readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
-
- protected SQLiteDatabaseConnection CreateWriteConnection()
+ protected SqliteConnection GetConnection()
{
- var writeConnection = SQLite3.Open(
- DbFilePath,
- DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
- null);
-
- if (CacheSize.HasValue)
- {
- writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
- }
-
- if (!string.IsNullOrWhiteSpace(LockingMode))
- {
- writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
- }
-
- if (!string.IsNullOrWhiteSpace(JournalMode))
- {
- writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
- }
-
- if (JournalSizeLimit.HasValue)
- {
- writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
- }
-
- if (Synchronous.HasValue)
- {
- writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
- }
-
- if (PageSize.HasValue)
- {
- writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
- }
-
- writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
-
- return writeConnection;
- }
-
- protected SQLiteDatabaseConnection CreateReadConnection()
- {
- var connection = SQLite3.Open(
- DbFilePath,
- DefaultConnectionFlags | ConnectionFlags.ReadOnly,
- null);
+ var connection = new SqliteConnection($"Filename={DbFilePath}");
+ connection.Open();
if (CacheSize.HasValue)
{
@@ -208,39 +128,38 @@ namespace Emby.Server.Implementations.Data
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
+ if (PageSize.HasValue)
+ {
+ connection.Execute("PRAGMA page_size=" + PageSize.Value);
+ }
+
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return connection;
}
- public IStatement PrepareStatement(ManagedConnection connection, string sql)
- => connection.PrepareStatement(sql);
-
- public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
- => connection.PrepareStatement(sql);
-
- protected bool TableExists(ManagedConnection connection, string name)
+ public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
{
- return connection.RunInTransaction(
- db =>
- {
- using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
- {
- foreach (var row in statement.ExecuteQuery())
- {
- if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- }
-
- return false;
- },
- ReadTransactionMode);
+ var command = connection.CreateCommand();
+ command.CommandText = sql;
+ return command;
}
- protected List GetColumnNames(IDatabaseConnection connection, string table)
+ protected bool TableExists(SqliteConnection connection, string name)
+ {
+ using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
+ foreach (var row in statement.ExecuteQuery())
+ {
+ if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ protected List GetColumnNames(SqliteConnection connection, string table)
{
var columnNames = new List();
@@ -255,7 +174,7 @@ namespace Emby.Server.Implementations.Data
return columnNames;
}
- protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List existingColumnNames)
+ protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List existingColumnNames)
{
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{
@@ -291,12 +210,6 @@ namespace Emby.Server.Implementations.Data
return;
}
- if (dispose)
- {
- WriteConnections.Dispose();
- ReadConnections.Dispose();
- }
-
_disposed = true;
}
}
diff --git a/Emby.Server.Implementations/Data/ConnectionPool.cs b/Emby.Server.Implementations/Data/ConnectionPool.cs
deleted file mode 100644
index 5ea7e934f..000000000
--- a/Emby.Server.Implementations/Data/ConnectionPool.cs
+++ /dev/null
@@ -1,79 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data;
-
-///
-/// A pool of SQLite Database connections.
-///
-public sealed class ConnectionPool : IDisposable
-{
- private readonly BlockingCollection _connections = new();
- private bool _disposed;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The number of database connection to create.
- /// Factory function to create the database connections.
- public ConnectionPool(int count, Func factory)
- {
- for (int i = 0; i < count; i++)
- {
- _connections.Add(factory.Invoke());
- }
- }
-
- ///
- /// Gets a database connection from the pool if one is available, otherwise blocks.
- ///
- /// A database connection.
- public ManagedConnection GetConnection()
- {
- if (_disposed)
- {
- ThrowObjectDisposedException();
- }
-
- return new ManagedConnection(_connections.Take(), this);
-
- static void ThrowObjectDisposedException()
- {
- throw new ObjectDisposedException(nameof(ConnectionPool));
- }
- }
-
- ///
- /// Return a database connection to the pool.
- ///
- /// The database connection to return.
- public void Return(SQLiteDatabaseConnection connection)
- {
- if (_disposed)
- {
- connection.Dispose();
- return;
- }
-
- _connections.Add(connection);
- }
-
- ///
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- foreach (var connection in _connections)
- {
- connection.Dispose();
- }
-
- _connections.Dispose();
-
- _disposed = true;
- }
-}
diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs
deleted file mode 100644
index e84ed8f91..000000000
--- a/Emby.Server.Implementations/Data/ManagedConnection.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
- public sealed class ManagedConnection : IDisposable
- {
- private readonly ConnectionPool _pool;
-
- private SQLiteDatabaseConnection _db;
-
- private bool _disposed = false;
-
- public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
- {
- _db = db;
- _pool = pool;
- }
-
- public IStatement PrepareStatement(string sql)
- {
- return _db.PrepareStatement(sql);
- }
-
- public IEnumerable PrepareAll(string sql)
- {
- return _db.PrepareAll(sql);
- }
-
- public void ExecuteAll(string sql)
- {
- _db.ExecuteAll(sql);
- }
-
- public void Execute(string sql, params object[] values)
- {
- _db.Execute(sql, values);
- }
-
- public void RunQueries(string[] sql)
- {
- _db.RunQueries(sql);
- }
-
- public void RunInTransaction(Action action, TransactionMode mode)
- {
- _db.RunInTransaction(action, mode);
- }
-
- public T RunInTransaction(Func action, TransactionMode mode)
- {
- return _db.RunInTransaction(action, mode);
- }
-
- public IEnumerable> Query(string sql)
- {
- return _db.Query(sql);
- }
-
- public IEnumerable> Query(string sql, params object[] values)
- {
- return _db.Query(sql, values);
- }
-
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- _pool.Return(_db);
-
- _db = null!; // Don't dispose it
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index 4055b0ba1..01b5fdaee 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -1,11 +1,10 @@
-#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
-using System.Diagnostics;
+using System.Data;
using System.Globalization;
-using SQLitePCL.pretty;
+using Microsoft.Data.Sqlite;
namespace Emby.Server.Implementations.Data
{
@@ -52,19 +51,29 @@ namespace Emby.Server.Implementations.Data
"yy-MM-dd"
};
- public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries)
+ public static IEnumerable Query(this SqliteConnection sqliteConnection, string commandText)
{
- ArgumentNullException.ThrowIfNull(queries);
-
- connection.RunInTransaction(conn =>
+ if (sqliteConnection.State != ConnectionState.Open)
{
- conn.ExecuteAll(string.Join(';', queries));
- });
+ sqliteConnection.Open();
+ }
+
+ using var command = sqliteConnection.CreateCommand();
+ command.CommandText = commandText;
+ using (var reader = command.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ yield return reader;
+ }
+ }
}
- public static Guid ReadGuidFromBlob(this ResultSetValue result)
+ public static void Execute(this SqliteConnection sqliteConnection, string commandText)
{
- return new Guid(result.ToBlob());
+ using var command = sqliteConnection.CreateCommand();
+ command.CommandText = commandText;
+ command.ExecuteNonQuery();
}
public static string ToDateTimeParamValue(this DateTime dateValue)
@@ -83,27 +92,15 @@ namespace Emby.Server.Implementations.Data
private static string GetDateTimeKindFormat(DateTimeKind kind)
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
- public static DateTime ReadDateTime(this ResultSetValue result)
+ public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result)
{
- var dateText = result.ToString();
-
- return DateTime.ParseExact(
- dateText,
- _datetimeFormats,
- DateTimeFormatInfo.InvariantInfo,
- DateTimeStyles.AdjustToUniversal);
- }
-
- public static bool TryReadDateTime(this IReadOnlyList reader, int index, out DateTime result)
- {
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- var dateText = item.ToString();
+ var dateText = reader.GetString(index);
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{
@@ -115,335 +112,145 @@ namespace Emby.Server.Implementations.Data
return false;
}
- public static bool TryGetGuid(this IReadOnlyList reader, int index, out Guid result)
+ public static bool TryGetGuid(this SqliteDataReader reader, int index, out Guid result)
{
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ReadGuidFromBlob();
+ result = reader.GetGuid(index);
return true;
}
- public static bool IsDbNull(this ResultSetValue result)
+ public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
{
- return result.SQLiteType == SQLiteType.Null;
- }
+ result = string.Empty;
- public static string GetString(this IReadOnlyList result, int index)
- {
- return result[index].ToString();
- }
-
- public static bool TryGetString(this IReadOnlyList reader, int index, out string result)
- {
- result = null;
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
return false;
}
- result = item.ToString();
+ result = reader.GetString(index);
return true;
}
- public static bool GetBoolean(this IReadOnlyList result, int index)
+ public static bool TryGetBoolean(this SqliteDataReader reader, int index, out bool result)
{
- return result[index].ToBool();
- }
-
- public static bool TryGetBoolean(this IReadOnlyList reader, int index, out bool result)
- {
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ToBool();
+ result = reader.GetBoolean(index);
return true;
}
- public static bool TryGetInt32(this IReadOnlyList reader, int index, out int result)
+ public static bool TryGetInt32(this SqliteDataReader reader, int index, out int result)
{
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ToInt();
+ result = reader.GetInt32(index);
return true;
}
- public static long GetInt64(this IReadOnlyList result, int index)
+ public static bool TryGetInt64(this SqliteDataReader reader, int index, out long result)
{
- return result[index].ToInt64();
- }
-
- public static bool TryGetInt64(this IReadOnlyList reader, int index, out long result)
- {
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ToInt64();
+ result = reader.GetInt64(index);
return true;
}
- public static bool TryGetSingle(this IReadOnlyList reader, int index, out float result)
+ public static bool TryGetSingle(this SqliteDataReader reader, int index, out float result)
{
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ToFloat();
+ result = reader.GetFloat(index);
return true;
}
- public static bool TryGetDouble(this IReadOnlyList reader, int index, out double result)
+ public static bool TryGetDouble(this SqliteDataReader reader, int index, out double result)
{
- var item = reader[index];
- if (item.IsDbNull())
+ if (reader.IsDBNull(index))
{
result = default;
return false;
}
- result = item.ToDouble();
+ result = reader.GetDouble(index);
return true;
}
- public static Guid GetGuid(this IReadOnlyList result, int index)
+ public static void TryBind(this SqliteCommand statement, string name, Guid value)
{
- return result[index].ReadGuidFromBlob();
+ statement.TryBind(name, value, true);
}
- [Conditional("DEBUG")]
- private static void CheckName(string name)
+ public static void TryBind(this SqliteCommand statement, string name, object? value, bool isBlob = false)
{
- throw new ArgumentException("Invalid param name: " + name, nameof(name));
- }
-
- public static void TryBind(this IStatement statement, string name, double value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
+ var preparedValue = value ?? DBNull.Value;
+ if (statement.Parameters.Contains(name))
{
- bindParam.Bind(value);
+ statement.Parameters[name].Value = preparedValue;
}
else
{
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, string value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- if (value is null)
+ // Blobs aren't always detected automatically
+ if (isBlob)
{
- bindParam.BindNull();
+ statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value });
}
else
{
- bindParam.Bind(value);
+ statement.Parameters.AddWithValue(name, preparedValue);
}
}
- else
+ }
+
+ public static void TryBindNull(this SqliteCommand statement, string name)
+ {
+ statement.TryBind(name, DBNull.Value);
+ }
+
+ public static IEnumerable ExecuteQuery(this SqliteCommand command)
+ {
+ using (var reader = command.ExecuteReader())
{
- CheckName(name);
+ while (reader.Read())
+ {
+ yield return reader;
+ }
}
}
- public static void TryBind(this IStatement statement, string name, bool value)
+ public static int SelectScalarInt(this SqliteCommand command)
{
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value);
- }
- else
- {
- CheckName(name);
- }
+ var result = command.ExecuteScalar();
+ // Can't be null since the method is used to retrieve Count
+ return Convert.ToInt32(result!, CultureInfo.InvariantCulture);
}
- public static void TryBind(this IStatement statement, string name, float value)
+ public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql)
{
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, int value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, Guid value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- Span byteValue = stackalloc byte[16];
- value.TryWriteBytes(byteValue);
- bindParam.Bind(byteValue);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, DateTime value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value.ToDateTimeParamValue());
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, long value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, ReadOnlySpan value)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.Bind(value);
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBindNull(this IStatement statement, string name)
- {
- if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
- {
- bindParam.BindNull();
- }
- else
- {
- CheckName(name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, DateTime? value)
- {
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, Guid? value)
- {
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, double? value)
- {
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, int? value)
- {
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, float? value)
- {
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
- }
-
- public static void TryBind(this IStatement statement, string name, bool? value)
- {
- if (value.HasValue)
- {
- TryBind(statement, name, value.Value);
- }
- else
- {
- TryBindNull(statement, name);
- }
- }
-
- public static IEnumerable> ExecuteQuery(this IStatement statement)
- {
- while (statement.MoveNext())
- {
- yield return statement.Current;
- }
+ var command = sqliteConnection.CreateCommand();
+ command.CommandText = sql;
+ return command;
}
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index d1fbea95a..e519364c2 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -3,7 +3,6 @@
#pragma warning disable CS1591
using System;
-using System.Buffers.Text;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@@ -34,9 +33,9 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
+using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
@@ -436,128 +435,126 @@ namespace Emby.Server.Implementations.Data
};
using (var connection = GetConnection())
+ using (var transaction = connection.BeginTransaction())
{
- connection.RunQueries(queries);
+ connection.Execute(string.Join(';', queries));
- connection.RunInTransaction(
- db =>
- {
- var existingColumnNames = GetColumnNames(db, "AncestorIds");
- AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
+ var existingColumnNames = GetColumnNames(connection, "AncestorIds");
+ AddColumn(connection, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
- existingColumnNames = GetColumnNames(db, "TypedBaseItems");
+ existingColumnNames = GetColumnNames(connection, "TypedBaseItems");
- AddColumn(db, "TypedBaseItems", "Path", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ChannelId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "CustomRating", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Name", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "MediaType", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Overview", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ParentId", "GUID", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Genres", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SortName", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "LockedFields", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Studios", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Audio", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Tags", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "UnratedType", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "TopParentId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "CriticRating", "Float", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "CleanName", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SeasonName", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Tagline", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Images", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ExtraType", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Artists", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ExternalId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "ShowId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "OwnerId", "Text", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Width", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Height", "INT", existingColumnNames);
- AddColumn(db, "TypedBaseItems", "Size", "BIGINT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Path", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ChannelId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CustomRating", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Name", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "MediaType", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Overview", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ParentId", "GUID", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Genres", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SortName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "LockedFields", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Studios", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Audio", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Tags", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "UnratedType", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "TopParentId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CriticRating", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "CleanName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeasonName", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Tagline", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Images", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExtraIds", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExtraType", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Artists", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ExternalId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "ShowId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "OwnerId", "Text", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Width", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Height", "INT", existingColumnNames);
+ AddColumn(connection, "TypedBaseItems", "Size", "BIGINT", existingColumnNames);
- existingColumnNames = GetColumnNames(db, "ItemValues");
- AddColumn(db, "ItemValues", "CleanValue", "Text", existingColumnNames);
+ existingColumnNames = GetColumnNames(connection, "ItemValues");
+ AddColumn(connection, "ItemValues", "CleanValue", "Text", existingColumnNames);
- existingColumnNames = GetColumnNames(db, ChaptersTableName);
- AddColumn(db, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames);
+ existingColumnNames = GetColumnNames(connection, ChaptersTableName);
+ AddColumn(connection, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames);
- existingColumnNames = GetColumnNames(db, "MediaStreams");
- AddColumn(db, "MediaStreams", "IsAvc", "BIT", existingColumnNames);
- AddColumn(db, "MediaStreams", "TimeBase", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "Title", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "Comment", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "CodecTag", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "BitDepth", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "RefFrames", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames);
+ existingColumnNames = GetColumnNames(connection, "MediaStreams");
+ AddColumn(connection, "MediaStreams", "IsAvc", "BIT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "TimeBase", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "Title", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "Comment", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "CodecTag", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "BitDepth", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "RefFrames", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames);
- AddColumn(db, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "ColorPrimaries", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "ColorSpace", "TEXT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "ColorTransfer", "TEXT", existingColumnNames);
- AddColumn(db, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "DvProfile", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "DvLevel", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvVersionMajor", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvVersionMinor", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvProfile", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvLevel", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "RpuPresentFlag", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "ElPresentFlag", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "BlPresentFlag", "INT", existingColumnNames);
+ AddColumn(connection, "MediaStreams", "DvBlSignalCompatibilityId", "INT", existingColumnNames);
- AddColumn(db, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames);
- },
- TransactionMode);
+ AddColumn(connection, "MediaStreams", "IsHearingImpaired", "BIT", existingColumnNames);
- connection.RunQueries(postQueries);
+ connection.Execute(string.Join(';', postQueries));
+
+ transaction.Commit();
}
}
@@ -567,21 +564,15 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
- {
- saveImagesStatement.TryBind("@Id", item.Id);
- saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos));
+ var images = SerializeImages(item.ImageInfos);
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ using var saveImagesStatement = PrepareStatement(connection, "Update TypedBaseItems set Images=@Images where guid=@Id");
+ saveImagesStatement.TryBind("@Id", item.Id);
+ saveImagesStatement.TryBind("@Images", images);
- saveImagesStatement.MoveNext();
- }
- },
- TransactionMode);
- }
+ saveImagesStatement.ExecuteNonQuery();
+ transaction.Commit();
}
///
@@ -617,18 +608,13 @@ namespace Emby.Server.Implementations.Data
tuples[i] = (item, ancestorIds, topParent, userdataKey, inheritedTags);
}
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- SaveItemsInTransaction(db, tuples);
- },
- TransactionMode);
- }
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ SaveItemsInTransaction(connection, tuples);
+ transaction.Commit();
}
- private void SaveItemsInTransaction(IDatabaseConnection db, IEnumerable<(BaseItem Item, List AncestorIds, BaseItem TopParent, string UserDataKey, List InheritedTags)> tuples)
+ private void SaveItemsInTransaction(SqliteConnection db, IEnumerable<(BaseItem Item, List AncestorIds, BaseItem TopParent, string UserDataKey, List InheritedTags)> tuples)
{
using (var saveItemStatement = PrepareStatement(db, SaveItemCommandText))
using (var deleteAncestorsStatement = PrepareStatement(db, "delete from AncestorIds where ItemId=@ItemId"))
@@ -638,7 +624,8 @@ namespace Emby.Server.Implementations.Data
{
if (requiresReset)
{
- saveItemStatement.Reset();
+ saveItemStatement.Parameters.Clear();
+ deleteAncestorsStatement.Parameters.Clear();
}
var item = tuple.Item;
@@ -676,7 +663,7 @@ namespace Emby.Server.Implementations.Data
return _appHost.ExpandVirtualPath(path);
}
- private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, IStatement saveItemStatement)
+ private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, SqliteCommand saveItemStatement)
{
Type type = item.GetType();
@@ -685,7 +672,7 @@ namespace Emby.Server.Implementations.Data
if (TypeRequiresDeserialization(type))
{
- saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions));
+ saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, type, _jsonOptions), true);
}
else
{
@@ -1032,7 +1019,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@OwnerId", ownerId);
}
- saveItemStatement.MoveNext();
+ saveItemStatement.ExecuteNonQuery();
}
internal static string SerializeProviderIds(Dictionary providerIds)
@@ -1286,7 +1273,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
{
statement.TryBind("@guid", id);
@@ -1304,96 +1291,35 @@ namespace Emby.Server.Implementations.Data
{
if (_config.Configuration.SkipDeserializationForBasicTypes)
{
- if (type == typeof(Channel))
- {
- return false;
- }
-
- if (type == typeof(UserRootFolder))
+ if (type == typeof(Channel)
+ || type == typeof(UserRootFolder))
{
return false;
}
}
- if (type == typeof(Season))
- {
- return false;
- }
-
- if (type == typeof(MusicArtist))
- {
- return false;
- }
-
- if (type == typeof(Person))
- {
- return false;
- }
-
- if (type == typeof(MusicGenre))
- {
- return false;
- }
-
- if (type == typeof(Genre))
- {
- return false;
- }
-
- if (type == typeof(Studio))
- {
- return false;
- }
-
- if (type == typeof(PlaylistsFolder))
- {
- return false;
- }
-
- if (type == typeof(PhotoAlbum))
- {
- return false;
- }
-
- if (type == typeof(Year))
- {
- return false;
- }
-
- if (type == typeof(Book))
- {
- return false;
- }
-
- if (type == typeof(LiveTvProgram))
- {
- return false;
- }
-
- if (type == typeof(AudioBook))
- {
- return false;
- }
-
- if (type == typeof(Audio))
- {
- return false;
- }
-
- if (type == typeof(MusicAlbum))
- {
- return false;
- }
-
- return true;
+ return type != typeof(Season)
+ && type != typeof(MusicArtist)
+ && type != typeof(Person)
+ && type != typeof(MusicGenre)
+ && type != typeof(Genre)
+ && type != typeof(Studio)
+ && type != typeof(PlaylistsFolder)
+ && type != typeof(PhotoAlbum)
+ && type != typeof(Year)
+ && type != typeof(Book)
+ && type != typeof(LiveTvProgram)
+ && type != typeof(AudioBook)
+ && type != typeof(Audio)
+ && type != typeof(MusicAlbum);
}
- private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query)
+ private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query)
{
return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query));
}
- private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
+ private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
{
var typeString = reader.GetString(0);
@@ -1410,7 +1336,7 @@ namespace Emby.Server.Implementations.Data
{
try
{
- item = JsonSerializer.Deserialize(reader[1].ToBlob(), type, _jsonOptions) as BaseItem;
+ item = JsonSerializer.Deserialize(reader.GetStream(1), type, _jsonOptions) as BaseItem;
}
catch (JsonException ex)
{
@@ -1451,17 +1377,9 @@ namespace Emby.Server.Implementations.Data
item.EndDate = endDate;
}
- var channelId = reader[index];
- if (!channelId.IsDbNull())
+ if (reader.TryGetGuid(index, out var guid))
{
- if (!Utf8Parser.TryParse(channelId.ToBlob(), out Guid value, out _, standardFormat: 'N'))
- {
- var str = reader.GetString(index);
- Logger.LogWarning("{ChannelId} isn't in the expected format", str);
- value = new Guid(str);
- }
-
- item.ChannelId = value;
+ item.ChannelId = guid;
}
index++;
@@ -1977,7 +1895,7 @@ namespace Emby.Server.Implementations.Data
CheckDisposed();
var chapters = new List();
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"))
{
statement.TryBind("@ItemId", item.Id);
@@ -1996,7 +1914,7 @@ namespace Emby.Server.Implementations.Data
{
CheckDisposed();
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"))
{
statement.TryBind("@ItemId", item.Id);
@@ -2017,7 +1935,7 @@ namespace Emby.Server.Implementations.Data
/// The reader.
/// The item.
/// ChapterInfo.
- private ChapterInfo GetChapter(IReadOnlyList reader, BaseItem item)
+ private ChapterInfo GetChapter(SqliteDataReader reader, BaseItem item)
{
var chapter = new ChapterInfo
{
@@ -2032,18 +1950,7 @@ namespace Emby.Server.Implementations.Data
if (reader.TryGetString(2, out var imagePath))
{
chapter.ImagePath = imagePath;
-
- if (!string.IsNullOrEmpty(chapter.ImagePath))
- {
- try
- {
- chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Failed to create image cache tag.");
- }
- }
+ chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter);
}
if (reader.TryReadDateTime(3, out var imageDateModified))
@@ -2070,23 +1977,18 @@ namespace Emby.Server.Implementations.Data
ArgumentNullException.ThrowIfNull(chapters);
- var idBlob = id.ToByteArray();
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ // First delete chapters
+ using var command = connection.PrepareStatement($"delete from {ChaptersTableName} where ItemId=@ItemId");
+ command.TryBind("@ItemId", id);
+ command.ExecuteNonQuery();
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- // First delete chapters
- db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
-
- InsertChapters(idBlob, chapters, db);
- },
- TransactionMode);
- }
+ InsertChapters(id, chapters, connection);
+ transaction.Commit();
}
- private void InsertChapters(byte[] idBlob, IReadOnlyList chapters, IDatabaseConnection db)
+ private void InsertChapters(Guid idBlob, IReadOnlyList chapters, SqliteConnection db)
{
var startIndex = 0;
var limit = 100;
@@ -2104,7 +2006,7 @@ namespace Emby.Server.Implementations.Data
insertText.AppendFormat(CultureInfo.InvariantCulture, "(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture));
}
- insertText.Length -= 1; // Remove last ,
+ insertText.Length -= 1; // Remove trailing comma
using (var statement = PrepareStatement(db, insertText.ToString()))
{
@@ -2125,8 +2027,7 @@ namespace Emby.Server.Implementations.Data
chapterIndex++;
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
startIndex += limit;
@@ -2451,7 +2352,9 @@ namespace Emby.Server.Implementations.Data
if (query.SearchTerm.Length > 1)
{
builder.Append("+ ((CleanName like @SearchTermContains or (OriginalTitle not null and OriginalTitle like @SearchTermContains)) * 10)");
- builder.Append("+ ((Tags not null and Tags like @SearchTermContains) * 5)");
+ builder.Append("+ (SELECT COUNT(1) * 1 from ItemValues where ItemId=Guid and CleanValue like @SearchTermContains)");
+ builder.Append("+ (SELECT COUNT(1) * 2 from ItemValues where ItemId=Guid and CleanValue like @SearchTermStartsWith)");
+ builder.Append("+ (SELECT COUNT(1) * 10 from ItemValues where ItemId=Guid and CleanValue like @SearchTermEquals)");
}
builder.Append(") as SearchScore");
@@ -2460,7 +2363,7 @@ namespace Emby.Server.Implementations.Data
}
}
- private void BindSearchParams(InternalItemsQuery query, IStatement statement)
+ private void BindSearchParams(InternalItemsQuery query, SqliteCommand statement)
{
var searchTerm = query.SearchTerm;
@@ -2472,7 +2375,7 @@ namespace Emby.Server.Implementations.Data
searchTerm = FixUnicodeChars(searchTerm);
searchTerm = GetCleanValue(searchTerm);
- var commandText = statement.SQL;
+ var commandText = statement.CommandText;
if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase))
{
statement.TryBind("@SearchTermStartsWith", searchTerm + "%");
@@ -2482,9 +2385,14 @@ namespace Emby.Server.Implementations.Data
{
statement.TryBind("@SearchTermContains", "%" + searchTerm + "%");
}
+
+ if (commandText.Contains("@SearchTermEquals", StringComparison.OrdinalIgnoreCase))
+ {
+ statement.TryBind("@SearchTermEquals", searchTerm);
+ }
}
- private void BindSimilarParams(InternalItemsQuery query, IStatement statement)
+ private void BindSimilarParams(InternalItemsQuery query, SqliteCommand statement)
{
var item = query.SimilarTo;
@@ -2493,7 +2401,7 @@ namespace Emby.Server.Implementations.Data
return;
}
- var commandText = statement.SQL;
+ var commandText = statement.CommandText;
if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase))
{
@@ -2576,7 +2484,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2590,7 +2498,7 @@ namespace Emby.Server.Implementations.Data
// Running this again will bind the params
GetWhereClauses(query, statement);
- return statement.ExecuteQuery().SelectScalarInt().First();
+ return statement.SelectScalarInt();
}
}
@@ -2644,7 +2552,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var items = new List();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -2852,69 +2760,65 @@ namespace Emby.Server.Implementations.Data
var list = new List();
var result = new QueryResult();
- using (var connection = GetConnection(true))
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ if (!isReturningZeroItems)
{
- connection.RunInTransaction(
- db =>
+ using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
+ using (var statement = PrepareStatement(connection, itemQuery))
+ {
+ if (EnableJoinUserData(query))
{
- if (!isReturningZeroItems)
+ statement.TryBind("@UserId", query.User.InternalId);
+ }
+
+ BindSimilarParams(query, statement);
+ BindSearchParams(query, statement);
+
+ // Running this again will bind the params
+ GetWhereClauses(query, statement);
+
+ var hasEpisodeAttributes = HasEpisodeAttributes(query);
+ var hasServiceName = HasServiceName(query);
+ var hasProgramAttributes = HasProgramAttributes(query);
+ var hasStartDate = HasStartDate(query);
+ var hasTrailerTypes = HasTrailerTypes(query);
+ var hasArtistFields = HasArtistFields(query);
+ var hasSeriesFields = HasSeriesFields(query);
+
+ foreach (var row in statement.ExecuteQuery())
+ {
+ var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
+ if (item is not null)
{
- using (new QueryTimeLogger(Logger, itemQuery, "GetItems.ItemQuery"))
- using (var statement = PrepareStatement(db, itemQuery))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
- if (item is not null)
- {
- list.Add(item);
- }
- }
- }
+ list.Add(item);
}
-
- if (query.EnableTotalRecordCount)
- {
- using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
- using (var statement = PrepareStatement(db, totalRecordCountQuery))
- {
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
-
- // Running this again will bind the params
- GetWhereClauses(query, statement);
-
- result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
- }
- }
- },
- ReadTransactionMode);
+ }
+ }
}
+ if (query.EnableTotalRecordCount)
+ {
+ using (new QueryTimeLogger(Logger, totalRecordCountQuery, "GetItems.TotalRecordCount"))
+ using (var statement = PrepareStatement(connection, totalRecordCountQuery))
+ {
+ if (EnableJoinUserData(query))
+ {
+ statement.TryBind("@UserId", query.User.InternalId);
+ }
+
+ BindSimilarParams(query, statement);
+ BindSearchParams(query, statement);
+
+ // Running this again will bind the params
+ GetWhereClauses(query, statement);
+
+ result.TotalRecordCount = statement.SelectScalarInt();
+ }
+ }
+
+ transaction.Commit();
+
result.StartIndex = query.StartIndex ?? 0;
result.Items = list;
return result;
@@ -3164,7 +3068,7 @@ namespace Emby.Server.Implementations.Data
var commandText = commandTextBuilder.ToString();
var list = new List();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, commandText))
{
if (EnableJoinUserData(query))
@@ -3180,7 +3084,7 @@ namespace Emby.Server.Implementations.Data
foreach (var row in statement.ExecuteQuery())
{
- list.Add(row[0].ReadGuidFromBlob());
+ list.Add(row.GetGuid(0));
}
}
@@ -3216,7 +3120,7 @@ namespace Emby.Server.Implementations.Data
}
#nullable enable
- private List GetWhereClauses(InternalItemsQuery query, IStatement? statement)
+ private List GetWhereClauses(InternalItemsQuery query, SqliteCommand? statement)
{
if (query.IsResumable ?? false)
{
@@ -3596,7 +3500,6 @@ namespace Emby.Server.Implementations.Data
statement?.TryBind(paramName, "%" + trailerTypes[i] + "%");
}
- // Remove last " OR "
clauseBuilder.Length -= Or.Length;
clauseBuilder.Append(')');
@@ -3637,14 +3540,9 @@ namespace Emby.Server.Implementations.Data
.Append(paramName)
.Append("))) OR ");
- if (statement is not null)
- {
- query.PersonIds[i].TryWriteBytes(idBytes);
- statement.TryBind(paramName, idBytes);
- }
+ statement?.TryBind(paramName, query.PersonIds[i]);
}
- // Remove last " OR "
clauseBuilder.Length -= Or.Length;
clauseBuilder.Append(')');
@@ -3811,215 +3709,219 @@ namespace Emby.Server.Implementations.Data
if (query.ArtistIds.Length > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var artistId in query.ArtistIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.ArtistIds.Length; i++)
{
- var paramName = "@ArtistIds" + index;
- clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
- statement?.TryBind(paramName, artistId);
- index++;
+ clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds")
+ .Append(i)
+ .Append(") and Type<=1)) OR ");
+ statement?.TryBind("@ArtistIds" + i, query.ArtistIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.AlbumArtistIds.Length > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var artistId in query.AlbumArtistIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.AlbumArtistIds.Length; i++)
{
- var paramName = "@ArtistIds" + index;
- clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))");
- statement?.TryBind(paramName, artistId);
- index++;
+ clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ArtistIds")
+ .Append(i)
+ .Append(") and Type=1)) OR ");
+ statement?.TryBind("@ArtistIds" + i, query.AlbumArtistIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.ContributingArtistIds.Length > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var artistId in query.ContributingArtistIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.ContributingArtistIds.Length; i++)
{
- var paramName = "@ArtistIds" + index;
- clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1))");
- statement?.TryBind(paramName, artistId);
- index++;
+ clauseBuilder.Append("((select CleanName from TypedBaseItems where guid=@ArtistIds")
+ .Append(i)
+ .Append(") in (select CleanValue from ItemValues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=@ArtistIds")
+ .Append(i)
+ .Append(") not in (select CleanValue from ItemValues where ItemId=Guid and Type=1)) OR ");
+ statement?.TryBind("@ArtistIds" + i, query.ContributingArtistIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.AlbumIds.Length > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var albumId in query.AlbumIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.AlbumIds.Length; i++)
{
- var paramName = "@AlbumIds" + index;
- clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")");
- statement?.TryBind(paramName, albumId);
- index++;
+ clauseBuilder.Append("Album in (select Name from typedbaseitems where guid=@AlbumIds")
+ .Append(i)
+ .Append(") OR ");
+ statement?.TryBind("@AlbumIds" + i, query.AlbumIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.ExcludeArtistIds.Length > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var artistId in query.ExcludeArtistIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.ExcludeArtistIds.Length; i++)
{
- var paramName = "@ExcludeArtistId" + index;
- clauses.Add("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))");
- statement?.TryBind(paramName, artistId);
- index++;
+ clauseBuilder.Append("(guid not in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@ExcludeArtistId")
+ .Append(i)
+ .Append(") and Type<=1)) OR ");
+ statement?.TryBind("@ExcludeArtistId" + i, query.ExcludeArtistIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.GenreIds.Count > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var genreId in query.GenreIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.GenreIds.Count; i++)
{
- var paramName = "@GenreId" + index;
- clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))");
- statement?.TryBind(paramName, genreId);
- index++;
+ clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@GenreId")
+ .Append(i)
+ .Append(") and Type=2)) OR ");
+ statement?.TryBind("@GenreId" + i, query.GenreIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.Genres.Count > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var item in query.Genres)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.Genres.Count; i++)
{
- clauses.Add("@Genre" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=2)");
- statement?.TryBind("@Genre" + index, GetCleanValue(item));
- index++;
+ clauseBuilder.Append("@Genre")
+ .Append(i)
+ .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=2) OR ");
+ statement?.TryBind("@Genre" + i, GetCleanValue(query.Genres[i]));
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (tags.Count > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var item in tags)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < tags.Count; i++)
{
- clauses.Add("@Tag" + index + " in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
- statement?.TryBind("@Tag" + index, GetCleanValue(item));
- index++;
+ clauseBuilder.Append("@Tag")
+ .Append(i)
+ .Append(" in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR ");
+ statement?.TryBind("@Tag" + i, GetCleanValue(tags[i]));
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (excludeTags.Count > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var item in excludeTags)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < excludeTags.Count; i++)
{
- clauses.Add("@ExcludeTag" + index + " not in (select CleanValue from ItemValues where ItemId=Guid and Type=4)");
- statement?.TryBind("@ExcludeTag" + index, GetCleanValue(item));
- index++;
+ clauseBuilder.Append("@ExcludeTag")
+ .Append(i)
+ .Append(" not in (select CleanValue from ItemValues where ItemId=Guid and Type=4) OR ");
+ statement?.TryBind("@ExcludeTag" + i, GetCleanValue(excludeTags[i]));
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.StudioIds.Length > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var studioId in query.StudioIds)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.StudioIds.Length; i++)
{
- var paramName = "@StudioId" + index;
- clauses.Add("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=3))");
- statement?.TryBind(paramName, studioId);
- index++;
+ clauseBuilder.Append("(guid in (select itemid from ItemValues where CleanValue = (select CleanName from TypedBaseItems where guid=@StudioId")
+ .Append(i)
+ .Append(") and Type=3)) OR ");
+ statement?.TryBind("@StudioId" + i, query.StudioIds[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.OfficialRatings.Length > 0)
{
- var clauses = new List();
- var index = 0;
- foreach (var item in query.OfficialRatings)
+ clauseBuilder.Append('(');
+ for (var i = 0; i < query.OfficialRatings.Length; i++)
{
- clauses.Add("OfficialRating=@OfficialRating" + index);
- statement?.TryBind("@OfficialRating" + index, item);
- index++;
+ clauseBuilder.Append("OfficialRating=@OfficialRating").Append(i).Append(Or);
+ statement?.TryBind("@OfficialRating" + i, query.OfficialRatings[i]);
}
- var clause = "(" + string.Join(" OR ", clauses) + ")";
- whereClauses.Add(clause);
+ clauseBuilder.Length -= Or.Length;
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
- var ratingClauseBuilder = new StringBuilder("(");
+ clauseBuilder.Append('(');
if (query.HasParentalRating ?? false)
{
- ratingClauseBuilder.Append("InheritedParentalRatingValue not null");
+ clauseBuilder.Append("InheritedParentalRatingValue not null");
if (query.MinParentalRating.HasValue)
{
- ratingClauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating");
+ clauseBuilder.Append(" AND InheritedParentalRatingValue >= @MinParentalRating");
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
}
if (query.MaxParentalRating.HasValue)
{
- ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
+ clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
}
else if (query.BlockUnratedItems.Length > 0)
{
- var paramName = "@UnratedType";
- var index = 0;
- string blockedUnratedItems = string.Join(',', query.BlockUnratedItems.Select(_ => paramName + index++));
- ratingClauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (" + blockedUnratedItems + "))");
+ const string ParamName = "@UnratedType";
+ clauseBuilder.Append("(InheritedParentalRatingValue is null AND UnratedType not in (");
- if (statement is not null)
+ for (int i = 0; i < query.BlockUnratedItems.Length; i++)
{
- for (var ind = 0; ind < query.BlockUnratedItems.Length; ind++)
- {
- statement.TryBind(paramName + ind, query.BlockUnratedItems[ind].ToString());
- }
+ clauseBuilder.Append(ParamName).Append(i).Append(',');
+ statement?.TryBind(ParamName + i, query.BlockUnratedItems[i].ToString());
}
+ // Remove trailing comma
+ clauseBuilder.Length--;
+ clauseBuilder.Append("))");
+
if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
{
- ratingClauseBuilder.Append(" OR (");
+ clauseBuilder.Append(" OR (");
}
if (query.MinParentalRating.HasValue)
{
- ratingClauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating");
+ clauseBuilder.Append("InheritedParentalRatingValue >= @MinParentalRating");
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
}
@@ -4027,50 +3929,50 @@ namespace Emby.Server.Implementations.Data
{
if (query.MinParentalRating.HasValue)
{
- ratingClauseBuilder.Append(" AND ");
+ clauseBuilder.Append(" AND ");
}
- ratingClauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating");
+ clauseBuilder.Append("InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
if (query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue)
{
- ratingClauseBuilder.Append(")");
+ clauseBuilder.Append(')');
}
if (!(query.MinParentalRating.HasValue || query.MaxParentalRating.HasValue))
{
- ratingClauseBuilder.Append(" OR InheritedParentalRatingValue not null");
+ clauseBuilder.Append(" OR InheritedParentalRatingValue not null");
}
}
else if (query.MinParentalRating.HasValue)
{
- ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating");
+ clauseBuilder.Append("InheritedParentalRatingValue is null OR (InheritedParentalRatingValue >= @MinParentalRating");
statement?.TryBind("@MinParentalRating", query.MinParentalRating.Value);
if (query.MaxParentalRating.HasValue)
{
- ratingClauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
+ clauseBuilder.Append(" AND InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
- ratingClauseBuilder.Append(")");
+ clauseBuilder.Append(')');
}
else if (query.MaxParentalRating.HasValue)
{
- ratingClauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating");
+ clauseBuilder.Append("InheritedParentalRatingValue is null OR InheritedParentalRatingValue <= @MaxParentalRating");
statement?.TryBind("@MaxParentalRating", query.MaxParentalRating.Value);
}
else if (!query.HasParentalRating ?? false)
{
- ratingClauseBuilder.Append("InheritedParentalRatingValue is null");
+ clauseBuilder.Append("InheritedParentalRatingValue is null");
}
- var ratingClauseString = ratingClauseBuilder.ToString();
- if (!string.Equals(ratingClauseString, "(", StringComparison.OrdinalIgnoreCase))
+ if (clauseBuilder.Length > 1)
{
- whereClauses.Add(ratingClauseString + ")");
+ whereClauses.Add(clauseBuilder.Append(')').ToString());
+ clauseBuilder.Length = 0;
}
if (query.HasOfficialRating.HasValue)
@@ -4477,7 +4379,7 @@ namespace Emby.Server.Implementations.Data
foreach (var videoType in query.VideoTypes)
{
- videoTypes.Add("data like '%\"VideoType\":\"" + videoType.ToString() + "\"%'");
+ videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'");
}
whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");
@@ -4557,7 +4459,6 @@ namespace Emby.Server.Implementations.Data
return whereClauses;
}
-#nullable disable
///
/// Formats a where clause for the specified provider.
@@ -4574,6 +4475,7 @@ namespace Emby.Server.Implementations.Data
provider);
}
+#nullable disable
private List GetItemByNameTypesInQuery(InternalItemsQuery query)
{
var list = new List();
@@ -4653,44 +4555,28 @@ namespace Emby.Server.Implementations.Data
return true;
}
- if (query.IncludeItemTypes.Contains(BaseItemKind.Episode)
+ return query.IncludeItemTypes.Contains(BaseItemKind.Episode)
|| query.IncludeItemTypes.Contains(BaseItemKind.Video)
|| query.IncludeItemTypes.Contains(BaseItemKind.Movie)
|| query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
|| query.IncludeItemTypes.Contains(BaseItemKind.Series)
- || query.IncludeItemTypes.Contains(BaseItemKind.Season))
- {
- return true;
- }
-
- return false;
+ || query.IncludeItemTypes.Contains(BaseItemKind.Season);
}
public void UpdateInheritedValues()
{
- string sql = string.Join(
- ';',
- new string[]
- {
- "delete from ItemValues where type = 6",
-
- "insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4",
-
- @"insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
+ const string Statements = """
+delete from ItemValues where type = 6;
+insert into ItemValues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4;
+insert into ItemValues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue
FROM AncestorIds
LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId)
-where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 "
- });
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- connection.ExecuteAll(sql);
- },
- TransactionMode);
- }
+where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4;
+""";
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ connection.Execute(Statements);
+ transaction.Commit();
}
public void DeleteItem(Guid id)
@@ -4702,43 +4588,36 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- Span idBlob = stackalloc byte[16];
- id.TryWriteBytes(idBlob);
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ // Delete people
+ ExecuteWithSingleParam(connection, "delete from People where ItemId=@Id", id);
- // Delete people
- ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob);
+ // Delete chapters
+ ExecuteWithSingleParam(connection, "delete from " + ChaptersTableName + " where ItemId=@Id", id);
- // Delete chapters
- ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob);
+ // Delete media streams
+ ExecuteWithSingleParam(connection, "delete from mediastreams where ItemId=@Id", id);
- // Delete media streams
- ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob);
+ // Delete ancestors
+ ExecuteWithSingleParam(connection, "delete from AncestorIds where ItemId=@Id", id);
- // Delete ancestors
- ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob);
+ // Delete item values
+ ExecuteWithSingleParam(connection, "delete from ItemValues where ItemId=@Id", id);
- // Delete item values
- ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob);
+ // Delete the item
+ ExecuteWithSingleParam(connection, "delete from TypedBaseItems where guid=@Id", id);
- // Delete the item
- ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob);
- },
- TransactionMode);
- }
+ transaction.Commit();
}
- private void ExecuteWithSingleParam(IDatabaseConnection db, string query, ReadOnlySpan value)
+ private void ExecuteWithSingleParam(SqliteConnection db, string query, Guid value)
{
using (var statement = PrepareStatement(db, query))
{
statement.TryBind("@Id", value);
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
}
@@ -4765,7 +4644,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
}
var list = new List();
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
@@ -4786,25 +4665,25 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed();
- var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p";
+ StringBuilder commandText = new StringBuilder("select ItemId, Name, Role, PersonType, SortOrder from People p");
var whereClauses = GetPeopleWhereClauses(query, null);
if (whereClauses.Count != 0)
{
- commandText += " where " + string.Join(" AND ", whereClauses);
+ commandText.Append(" where ").AppendJoin(" AND ", whereClauses);
}
- commandText += " order by ListOrder";
+ commandText.Append(" order by ListOrder");
if (query.Limit > 0)
{
- commandText += " LIMIT " + query.Limit;
+ commandText.Append(" LIMIT ").Append(query.Limit);
}
var list = new List();
- using (var connection = GetConnection(true))
- using (var statement = PrepareStatement(connection, commandText))
+ using (var connection = GetConnection())
+ using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
GetPeopleWhereClauses(query, statement);
@@ -4818,7 +4697,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
return list;
}
- private List GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement)
+ private List GetPeopleWhereClauses(InternalPeopleQuery query, SqliteCommand statement)
{
var whereClauses = new List();
@@ -4888,7 +4767,7 @@ AND Type = @InternalPersonType)");
return whereClauses;
}
- private void UpdateAncestors(Guid itemId, List ancestorIds, IDatabaseConnection db, IStatement deleteAncestorsStatement)
+ private void UpdateAncestors(Guid itemId, List ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
{
if (itemId.Equals(default))
{
@@ -4899,13 +4778,9 @@ AND Type = @InternalPersonType)");
CheckDisposed();
- Span itemIdBlob = stackalloc byte[16];
- itemId.TryWriteBytes(itemIdBlob);
-
// First delete
- deleteAncestorsStatement.Reset();
- deleteAncestorsStatement.TryBind("@ItemId", itemIdBlob);
- deleteAncestorsStatement.MoveNext();
+ deleteAncestorsStatement.TryBind("@ItemId", itemId);
+ deleteAncestorsStatement.ExecuteNonQuery();
if (ancestorIds.Count == 0)
{
@@ -4922,26 +4797,24 @@ AND Type = @InternalPersonType)");
i.ToString(CultureInfo.InvariantCulture));
}
- // Remove last ,
+ // Remove trailing comma
insertText.Length--;
using (var statement = PrepareStatement(db, insertText.ToString()))
{
- statement.TryBind("@ItemId", itemIdBlob);
+ statement.TryBind("@ItemId", itemId);
for (var i = 0; i < ancestorIds.Count; i++)
{
var index = i.ToString(CultureInfo.InvariantCulture);
var ancestorId = ancestorIds[i];
- ancestorId.TryWriteBytes(itemIdBlob);
- statement.TryBind("@AncestorId" + index, itemIdBlob);
+ statement.TryBind("@AncestorId" + index, ancestorId);
statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture));
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
}
@@ -5049,7 +4922,7 @@ AND Type = @InternalPersonType)");
var list = new List();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, commandText))
{
foreach (var row in statement.ExecuteQuery())
@@ -5249,77 +5122,75 @@ AND Type = @InternalPersonType)");
var list = new List<(BaseItem, ItemCounts)>();
var result = new QueryResult<(BaseItem, ItemCounts)>();
using (new QueryTimeLogger(Logger, commandText))
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
+ using (var transaction = connection.BeginTransaction(deferred: true))
{
- connection.RunInTransaction(
- db =>
+ if (!isReturningZeroItems)
+ {
+ using (var statement = PrepareStatement(connection, commandText))
{
- if (!isReturningZeroItems)
+ statement.TryBind("@SelectType", returnType);
+ if (EnableJoinUserData(query))
{
- using (var statement = PrepareStatement(db, commandText))
- {
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
-
- if (typeSubQuery is not null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
-
- var hasEpisodeAttributes = HasEpisodeAttributes(query);
- var hasProgramAttributes = HasProgramAttributes(query);
- var hasServiceName = HasServiceName(query);
- var hasStartDate = HasStartDate(query);
- var hasTrailerTypes = HasTrailerTypes(query);
- var hasArtistFields = HasArtistFields(query);
- var hasSeriesFields = HasSeriesFields(query);
-
- foreach (var row in statement.ExecuteQuery())
- {
- var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
- if (item is not null)
- {
- var countStartColumn = columns.Count - 1;
-
- list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
- }
- }
- }
+ statement.TryBind("@UserId", query.User.InternalId);
}
- if (query.EnableTotalRecordCount)
+ if (typeSubQuery is not null)
{
- using (var statement = PrepareStatement(db, countText))
+ GetWhereClauses(typeSubQuery, null);
+ }
+
+ BindSimilarParams(query, statement);
+ BindSearchParams(query, statement);
+ GetWhereClauses(innerQuery, statement);
+ GetWhereClauses(outerQuery, statement);
+
+ var hasEpisodeAttributes = HasEpisodeAttributes(query);
+ var hasProgramAttributes = HasProgramAttributes(query);
+ var hasServiceName = HasServiceName(query);
+ var hasStartDate = HasStartDate(query);
+ var hasTrailerTypes = HasTrailerTypes(query);
+ var hasArtistFields = HasArtistFields(query);
+ var hasSeriesFields = HasSeriesFields(query);
+
+ foreach (var row in statement.ExecuteQuery())
+ {
+ var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
+ if (item is not null)
{
- statement.TryBind("@SelectType", returnType);
- if (EnableJoinUserData(query))
- {
- statement.TryBind("@UserId", query.User.InternalId);
- }
+ var countStartColumn = columns.Count - 1;
- if (typeSubQuery is not null)
- {
- GetWhereClauses(typeSubQuery, null);
- }
-
- BindSimilarParams(query, statement);
- BindSearchParams(query, statement);
- GetWhereClauses(innerQuery, statement);
- GetWhereClauses(outerQuery, statement);
-
- result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
+ list.Add((item, GetItemCounts(row, countStartColumn, typesToCount)));
}
}
- },
- ReadTransactionMode);
+ }
+ }
+
+ if (query.EnableTotalRecordCount)
+ {
+ using (var statement = PrepareStatement(connection, countText))
+ {
+ statement.TryBind("@SelectType", returnType);
+ if (EnableJoinUserData(query))
+ {
+ statement.TryBind("@UserId", query.User.InternalId);
+ }
+
+ if (typeSubQuery is not null)
+ {
+ GetWhereClauses(typeSubQuery, null);
+ }
+
+ BindSimilarParams(query, statement);
+ BindSearchParams(query, statement);
+ GetWhereClauses(innerQuery, statement);
+ GetWhereClauses(outerQuery, statement);
+
+ result.TotalRecordCount = statement.SelectScalarInt();
+ }
+ }
+
+ transaction.Commit();
}
if (result.TotalRecordCount == 0)
@@ -5333,7 +5204,7 @@ AND Type = @InternalPersonType)");
return result;
}
- private static ItemCounts GetItemCounts(IReadOnlyList reader, int countStartColumn, BaseItemKind[] typesToCount)
+ private static ItemCounts GetItemCounts(SqliteDataReader reader, int countStartColumn, BaseItemKind[] typesToCount)
{
var counts = new ItemCounts();
@@ -5412,7 +5283,7 @@ AND Type = @InternalPersonType)");
return list;
}
- private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, IDatabaseConnection db)
+ private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db)
{
if (itemId.Equals(default))
{
@@ -5423,15 +5294,15 @@ AND Type = @InternalPersonType)");
CheckDisposed();
- var guidBlob = itemId.ToByteArray();
-
// First delete
- db.Execute("delete from ItemValues where ItemId=@Id", guidBlob);
+ using var command = db.PrepareStatement("delete from ItemValues where ItemId=@Id");
+ command.TryBind("@Id", itemId);
+ command.ExecuteNonQuery();
- InsertItemValues(guidBlob, values, db);
+ InsertItemValues(itemId, values, db);
}
- private void InsertItemValues(byte[] idBlob, List<(int MagicNumber, string Value)> values, IDatabaseConnection db)
+ private void InsertItemValues(Guid id, List<(int MagicNumber, string Value)> values, SqliteConnection db)
{
const int Limit = 100;
var startIndex = 0;
@@ -5450,12 +5321,12 @@ AND Type = @InternalPersonType)");
i);
}
- // Remove last comma
+ // Remove trailing comma
insertText.Length--;
using (var statement = PrepareStatement(db, insertText.ToString()))
{
- statement.TryBind("@ItemId", idBlob);
+ statement.TryBind("@ItemId", id);
for (var i = startIndex; i < endIndex; i++)
{
@@ -5476,8 +5347,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@CleanValue" + index, GetCleanValue(itemValue));
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
startIndex += Limit;
@@ -5496,23 +5366,20 @@ AND Type = @InternalPersonType)");
CheckDisposed();
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- var itemIdBlob = itemId.ToByteArray();
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ // First delete chapters
+ using var command = connection.CreateCommand();
+ command.CommandText = "delete from People where ItemId=@ItemId";
+ command.TryBind("@ItemId", itemId);
+ command.ExecuteNonQuery();
- // First delete chapters
- db.Execute("delete from People where ItemId=@ItemId", itemIdBlob);
+ InsertPeople(itemId, people, connection);
- InsertPeople(itemIdBlob, people, db);
- },
- TransactionMode);
- }
+ transaction.Commit();
}
- private void InsertPeople(byte[] idBlob, List people, IDatabaseConnection db)
+ private void InsertPeople(Guid id, List people, SqliteConnection db)
{
const int Limit = 100;
var startIndex = 0;
@@ -5531,12 +5398,12 @@ AND Type = @InternalPersonType)");
i.ToString(CultureInfo.InvariantCulture));
}
- // Remove last comma
+ // Remove trailing comma
insertText.Length--;
using (var statement = PrepareStatement(db, insertText.ToString()))
{
- statement.TryBind("@ItemId", idBlob);
+ statement.TryBind("@ItemId", id);
for (var i = startIndex; i < endIndex; i++)
{
@@ -5553,8 +5420,7 @@ AND Type = @InternalPersonType)");
listIndex++;
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
startIndex += Limit;
@@ -5562,7 +5428,7 @@ AND Type = @InternalPersonType)");
}
}
- private PersonInfo GetPerson(IReadOnlyList reader)
+ private PersonInfo GetPerson(SqliteDataReader reader)
{
var item = new PersonInfo
{
@@ -5609,7 +5475,7 @@ AND Type = @InternalPersonType)");
cmdText += " order by StreamIndex ASC";
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
{
var list = new List();
@@ -5650,23 +5516,19 @@ AND Type = @InternalPersonType)");
cancellationToken.ThrowIfCancellationRequested();
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(
- db =>
- {
- var itemIdBlob = id.ToByteArray();
+ using var connection = GetConnection();
+ using var transaction = connection.BeginTransaction();
+ // Delete existing mediastreams
+ using var command = connection.PrepareStatement("delete from mediastreams where ItemId=@ItemId");
+ command.TryBind("@ItemId", id);
+ command.ExecuteNonQuery();
- // Delete existing mediastreams
- db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob);
+ InsertMediaStreams(id, streams, connection);
- InsertMediaStreams(itemIdBlob, streams, db);
- },
- TransactionMode);
- }
+ transaction.Commit();
}
- private void InsertMediaStreams(byte[] idBlob, IReadOnlyList streams, IDatabaseConnection db)
+ private void InsertMediaStreams(Guid id, IReadOnlyList streams, SqliteConnection db)
{
const int Limit = 10;
var startIndex = 0;
@@ -5698,7 +5560,7 @@ AND Type = @InternalPersonType)");
using (var statement = PrepareStatement(db, insertText.ToString()))
{
- statement.TryBind("@ItemId", idBlob);
+ statement.TryBind("@ItemId", id);
for (var i = startIndex; i < endIndex; i++)
{
@@ -5734,6 +5596,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@PixelFormat" + index, stream.PixelFormat);
statement.TryBind("@BitDepth" + index, stream.BitDepth);
+ statement.TryBind("@IsAnamorphic" + index, stream.IsAnamorphic);
statement.TryBind("@IsExternal" + index, stream.IsExternal);
statement.TryBind("@RefFrames" + index, stream.RefFrames);
@@ -5762,8 +5625,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@IsHearingImpaired" + index, stream.IsHearingImpaired);
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
startIndex += Limit;
@@ -5776,15 +5638,14 @@ AND Type = @InternalPersonType)");
///
/// The reader.
/// MediaStream.
- private MediaStream GetMediaStream(IReadOnlyList reader)
+ private MediaStream GetMediaStream(SqliteDataReader reader)
{
var item = new MediaStream
{
- Index = reader[1].ToInt()
+ Index = reader.GetInt32(1),
+ Type = Enum.Parse(reader.GetString(2), true)
};
- item.Type = Enum.Parse(reader[2].ToString(), true);
-
if (reader.TryGetString(3, out var codec))
{
item.Codec = codec;
@@ -5971,7 +5832,7 @@ AND Type = @InternalPersonType)");
item.DvBlSignalCompatibilityId = dvBlSignalCompatibilityId;
}
- item.IsHearingImpaired = reader.GetBoolean(43);
+ item.IsHearingImpaired = reader.TryGetBoolean(43, out var result) && result;
if (item.Type == MediaStreamType.Subtitle)
{
@@ -6001,10 +5862,10 @@ AND Type = @InternalPersonType)");
cmdText += " order by AttachmentIndex ASC";
var list = new List();
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
using (var statement = PrepareStatement(connection, cmdText))
{
- statement.TryBind("@ItemId", query.ItemId.ToByteArray());
+ statement.TryBind("@ItemId", query.ItemId);
if (query.Index.HasValue)
{
@@ -6036,24 +5897,22 @@ AND Type = @InternalPersonType)");
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
+ using (var transaction = connection.BeginTransaction())
+ using (var command = connection.PrepareStatement("delete from mediaattachments where ItemId=@ItemId"))
{
- connection.RunInTransaction(
- db =>
- {
- var itemIdBlob = id.ToByteArray();
+ command.TryBind("@ItemId", id);
+ command.ExecuteNonQuery();
- db.Execute("delete from mediaattachments where ItemId=@ItemId", itemIdBlob);
+ InsertMediaAttachments(id, attachments, connection, cancellationToken);
- InsertMediaAttachments(itemIdBlob, attachments, db, cancellationToken);
- },
- TransactionMode);
+ transaction.Commit();
}
}
private void InsertMediaAttachments(
- byte[] idBlob,
+ Guid id,
IReadOnlyList attachments,
- IDatabaseConnection db,
+ SqliteConnection db,
CancellationToken cancellationToken)
{
const int InsertAtOnce = 10;
@@ -6065,14 +5924,13 @@ AND Type = @InternalPersonType)");
for (var i = startIndex; i < endIndex; i++)
{
- var index = i.ToString(CultureInfo.InvariantCulture);
insertText.Append("(@ItemId, ");
foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
{
insertText.Append('@')
.Append(column)
- .Append(index)
+ .Append(i)
.Append(',');
}
@@ -6087,7 +5945,7 @@ AND Type = @InternalPersonType)");
using (var statement = PrepareStatement(db, insertText.ToString()))
{
- statement.TryBind("@ItemId", idBlob);
+ statement.TryBind("@ItemId", id);
for (var i = startIndex; i < endIndex; i++)
{
@@ -6103,8 +5961,7 @@ AND Type = @InternalPersonType)");
statement.TryBind("@MIMEType" + index, attachment.MimeType);
}
- statement.Reset();
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
insertText.Length = _mediaAttachmentInsertPrefix.Length;
@@ -6116,11 +5973,11 @@ AND Type = @InternalPersonType)");
///
/// The reader.
/// MediaAttachment.
- private MediaAttachment GetMediaAttachment(IReadOnlyList reader)
+ private MediaAttachment GetMediaAttachment(SqliteDataReader reader)
{
var item = new MediaAttachment
{
- Index = reader[1].ToInt()
+ Index = reader.GetInt32(1)
};
if (reader.TryGetString(2, out var codec))
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index a1e217ad1..a5edcc58c 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -11,8 +11,8 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
+using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
@@ -44,48 +44,48 @@ namespace Emby.Server.Implementations.Data
var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : _userManager.Users;
+ using var transaction = connection.BeginTransaction();
+ connection.Execute(string.Join(
+ ';',
+ "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
+ "drop index if exists idx_userdata",
+ "drop index if exists idx_userdata1",
+ "drop index if exists idx_userdata2",
+ "drop index if exists userdataindex1",
+ "drop index if exists userdataindex",
+ "drop index if exists userdataindex3",
+ "drop index if exists userdataindex4",
+ "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
+ "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
+ "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
+ "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"));
- connection.RunInTransaction(
- db =>
- {
- db.ExecuteAll(string.Join(';', new[]
- {
- "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
+ if (!userDataTableExists)
+ {
+ transaction.Commit();
+ return;
+ }
- "drop index if exists idx_userdata",
- "drop index if exists idx_userdata1",
- "drop index if exists idx_userdata2",
- "drop index if exists userdataindex1",
- "drop index if exists userdataindex",
- "drop index if exists userdataindex3",
- "drop index if exists userdataindex4",
- "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
- "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
- "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
- "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
- }));
+ var existingColumnNames = GetColumnNames(connection, "userdata");
- if (userDataTableExists)
- {
- var existingColumnNames = GetColumnNames(db, "userdata");
+ AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
+ AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
+ AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
- AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
- AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
- AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
+ if (userDatasTableExists)
+ {
+ return;
+ }
- if (!userDatasTableExists)
- {
- ImportUserIds(db, users);
+ ImportUserIds(connection, users);
- db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
- }
- }
- },
- TransactionMode);
+ connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
+
+ transaction.Commit();
}
}
- private void ImportUserIds(IDatabaseConnection db, IEnumerable users)
+ private void ImportUserIds(SqliteConnection db, IEnumerable users)
{
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
@@ -101,13 +101,12 @@ namespace Emby.Server.Implementations.Data
statement.TryBind("@UserId", user.Id);
statement.TryBind("@InternalUserId", user.InternalId);
- statement.MoveNext();
- statement.Reset();
+ statement.ExecuteNonQuery();
}
}
}
- private List GetAllUserIdsWithUserData(IDatabaseConnection db)
+ private List GetAllUserIdsWithUserData(SqliteConnection db)
{
var list = new List();
@@ -117,7 +116,7 @@ namespace Emby.Server.Implementations.Data
{
try
{
- list.Add(row[0].ReadGuidFromBlob());
+ list.Add(row.GetGuid(0));
}
catch (Exception ex)
{
@@ -169,17 +168,14 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
+ using (var transaction = connection.BeginTransaction())
{
- connection.RunInTransaction(
- db =>
- {
- SaveUserData(db, internalUserId, key, userData);
- },
- TransactionMode);
+ SaveUserData(connection, internalUserId, key, userData);
+ transaction.Commit();
}
}
- private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData)
+ private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
{
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
{
@@ -227,7 +223,7 @@ namespace Emby.Server.Implementations.Data
statement.TryBindNull("@SubtitleStreamIndex");
}
- statement.MoveNext();
+ statement.ExecuteNonQuery();
}
}
@@ -239,16 +235,14 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
+ using (var transaction = connection.BeginTransaction())
{
- connection.RunInTransaction(
- db =>
- {
- foreach (var userItemData in userDataList)
- {
- SaveUserData(db, internalUserId, userItemData.Key, userItemData);
- }
- },
- TransactionMode);
+ foreach (var userItemData in userDataList)
+ {
+ SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
+ }
+
+ transaction.Commit();
}
}
@@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.Data
ArgumentException.ThrowIfNullOrEmpty(key);
- using (var connection = GetConnection(true))
+ using (var connection = GetConnection())
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
@@ -336,7 +330,7 @@ namespace Emby.Server.Implementations.Data
///
/// The list of result set values.
/// The user item data.
- private UserItemData ReadRow(IReadOnlyList reader)
+ private UserItemData ReadRow(SqliteDataReader reader)
{
var userData = new UserItemData();
@@ -348,10 +342,10 @@ namespace Emby.Server.Implementations.Data
userData.Rating = rating;
}
- userData.Played = reader[3].ToBool();
- userData.PlayCount = reader[4].ToInt();
- userData.IsFavorite = reader[5].ToBool();
- userData.PlaybackPositionTicks = reader[6].ToInt64();
+ userData.Played = reader.GetBoolean(3);
+ userData.PlayCount = reader.GetInt32(4);
+ userData.IsFavorite = reader.GetBoolean(5);
+ userData.PlaybackPositionTicks = reader.GetInt64(6);
if (reader.TryReadDateTime(7, out var lastPlayedDate))
{
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index be361c4d1..44b97e8b8 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -907,10 +907,11 @@ namespace Emby.Server.Implementations.Dto
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
}
+ dto.LUFS = item.LUFS;
+
// Add audio info
if (item is Audio audio)
{
- dto.LUFS = audio.LUFS;
dto.Album = audio.Album;
if (audio.ExtraType.HasValue)
{
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index b8655c760..80263c139 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -24,6 +24,7 @@
+
@@ -31,7 +32,6 @@
-
@@ -43,8 +43,6 @@
net7.0
false
true
-
- AD0001
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index 06e57ad12..d6da597b8 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.EntryPoints
return new StringBuilder(32)
.Append(config.EnableUPnP).Append(Separator)
- .Append(config.PublicPort).Append(Separator)
+ .Append(config.PublicHttpPort).Append(Separator)
.Append(config.PublicHttpsPort).Append(Separator)
.Append(_appHost.HttpPort).Append(Separator)
.Append(_appHost.HttpsPort).Append(Separator)
@@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.EntryPoints
private IEnumerable CreatePortMaps(INatDevice device)
{
var config = _config.GetNetworkConfiguration();
- yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort);
+ yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
if (_appHost.ListenWithHttps)
{
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index e45baedd7..7e4994f1a 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -1,10 +1,15 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Udp;
using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Extensions;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.Configuration;
@@ -13,7 +18,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
{
///
- /// Class UdpServerEntryPoint.
+ /// Class responsible for registering all UDP broadcast endpoints and their handlers.
///
public sealed class UdpServerEntryPoint : IServerEntryPoint
{
@@ -29,13 +34,14 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _config;
private readonly IConfigurationManager _configurationManager;
+ private readonly INetworkManager _networkManager;
///
/// The UDP server.
///
- private UdpServer? _udpServer;
- private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
- private bool _disposed = false;
+ private readonly List _udpServers;
+ private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+ private bool _disposed;
///
/// Initializes a new instance of the class.
@@ -44,16 +50,20 @@ namespace Emby.Server.Implementations.EntryPoints
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
+ /// Instance of the interface.
public UdpServerEntryPoint(
ILogger logger,
IServerApplicationHost appHost,
IConfiguration configuration,
- IConfigurationManager configurationManager)
+ IConfigurationManager configurationManager,
+ INetworkManager networkManager)
{
_logger = logger;
_appHost = appHost;
_config = configuration;
_configurationManager = configurationManager;
+ _networkManager = networkManager;
+ _udpServers = new List();
}
///
@@ -68,8 +78,43 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
- _udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
- _udpServer.Start(_cancellationTokenSource.Token);
+ // Linux needs to bind to the broadcast addresses to get broadcast traffic
+ // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
+ if (OperatingSystem.IsLinux())
+ {
+ // Add global broadcast listener
+ var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
+ server.Start(_cancellationTokenSource.Token);
+ _udpServers.Add(server);
+
+ // Add bind address specific broadcast listeners
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+ foreach (var intf in validInterfaces)
+ {
+ var broadcastAddress = NetworkExtensions.GetBroadcastAddress(intf.Subnet);
+ _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber);
+
+ server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber);
+ server.Start(_cancellationTokenSource.Token);
+ _udpServers.Add(server);
+ }
+ }
+ else
+ {
+ // Add bind address specific broadcast listeners
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+ foreach (var intf in validInterfaces)
+ {
+ var intfAddress = intf.Address;
+ _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
+
+ var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
+ server.Start(_cancellationTokenSource.Token);
+ _udpServers.Add(server);
+ }
+ }
}
catch (SocketException ex)
{
@@ -83,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (_disposed)
{
- throw new ObjectDisposedException(this.GetType().Name);
+ throw new ObjectDisposedException(GetType().Name);
}
}
@@ -97,9 +142,12 @@ namespace Emby.Server.Implementations.EntryPoints
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
- _udpServer?.Dispose();
- _udpServer = null;
+ foreach (var server in _udpServers)
+ {
+ server.Dispose();
+ }
+ _udpServers.Clear();
_disposed = true;
}
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index b1a99853a..f83da566b 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -9,7 +9,8 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages;
+using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
@@ -42,14 +43,17 @@ namespace Emby.Server.Implementations.HttpServer
///
/// The logger.
/// The socket.
+ /// The authorization information.
/// The remote end point.
public WebSocketConnection(
ILogger logger,
WebSocket socket,
+ AuthorizationInfo authorizationInfo,
IPAddress? remoteEndPoint)
{
_logger = logger;
_socket = socket;
+ AuthorizationInfo = authorizationInfo;
RemoteEndPoint = remoteEndPoint;
_jsonOptions = JsonDefaults.Options;
@@ -59,47 +63,40 @@ namespace Emby.Server.Implementations.HttpServer
///
public event EventHandler? Closed;
- ///
- /// Gets the remote end point.
- ///
+ ///
+ public AuthorizationInfo AuthorizationInfo { get; }
+
+ ///
public IPAddress? RemoteEndPoint { get; }
- ///
- /// Gets or sets the receive action.
- ///
- /// The receive action.
+ ///
public Func? OnReceive { get; set; }
- ///
- /// Gets the last activity date.
- ///
- /// The last activity date.
+ ///
public DateTime LastActivityDate { get; private set; }
///
public DateTime LastKeepAliveDate { get; set; }
- ///
- /// Gets the state.
- ///
- /// The state.
+ ///
public WebSocketState State => _socket.State;
- ///
- /// Sends a message asynchronously.
- ///
- /// The type of the message.
- /// The message.
- /// The cancellation token.
- /// Task.
- public Task SendAsync(WebSocketMessage message, CancellationToken cancellationToken)
+ ///
+ public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
}
///
- public async Task ProcessAsync(CancellationToken cancellationToken = default)
+ public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
+ {
+ var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
+ return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
+ }
+
+ ///
+ public async Task ReceiveAsync(CancellationToken cancellationToken = default)
{
var pipe = new Pipe();
var writer = pipe.Writer;
@@ -171,7 +168,7 @@ namespace Emby.Server.Implementations.HttpServer
return;
}
- WebSocketMessage
/// The source of the event.
/// The instance containing the event data.
- private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e)
+ private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
@@ -173,7 +171,7 @@ namespace Emby.Server.Implementations.IO
///
/// The source of the event.
/// The instance containing the event data.
- private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
+ private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
@@ -189,19 +187,28 @@ namespace Emby.Server.Implementations.IO
/// The path.
/// true if [contains parent folder] [the specified LST]; otherwise, false.
/// is null.
- private static bool ContainsParentFolder(IEnumerable lst, string path)
+ private static bool ContainsParentFolder(IReadOnlyList lst, ReadOnlySpan path)
{
- ArgumentException.ThrowIfNullOrEmpty(path);
+ if (path.IsEmpty)
+ {
+ throw new ArgumentException("Path can't be empty", nameof(path));
+ }
path = path.TrimEnd(Path.DirectorySeparatorChar);
- return lst.Any(str =>
+ foreach (var str in lst)
{
// this should be a little quicker than examining each actual parent folder...
- var compare = str.TrimEnd(Path.DirectorySeparatorChar);
+ var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
- return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
- });
+ if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
+ || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar))
+ {
+ return true;
+ }
+ }
+
+ return false;
}
///
@@ -349,21 +356,19 @@ namespace Emby.Server.Implementations.IO
{
ArgumentException.ThrowIfNullOrEmpty(path);
- var monitorPath = !IgnorePatterns.ShouldIgnore(path);
+ if (IgnorePatterns.ShouldIgnore(path))
+ {
+ return;
+ }
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
- if (_tempIgnoredPaths.Keys.Any(i =>
+ foreach (var i in _tempIgnoredPaths.Keys)
{
- if (_fileSystem.AreEqual(i, path))
+ if (_fileSystem.AreEqual(i, path)
+ || _fileSystem.ContainsSubPath(i, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
- return true;
- }
-
- if (_fileSystem.ContainsSubPath(i, path))
- {
- _logger.LogDebug("Ignoring change to {Path}", path);
- return true;
+ return;
}
// Go up a level
@@ -371,20 +376,11 @@ namespace Emby.Server.Implementations.IO
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
- return true;
+ return;
}
-
- return false;
- }))
- {
- monitorPath = false;
}
- if (monitorPath)
- {
- // Avoid implicitly captured closure
- CreateRefresher(path);
- }
+ CreateRefresher(path);
}
private void CreateRefresher(string path)
@@ -417,7 +413,8 @@ namespace Emby.Server.Implementations.IO
}
// They are siblings. Rebase the refresher to the parent folder.
- if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal))
+ if (parentPath is not null
+ && Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
{
refresher.ResetPath(parentPath, path);
return;
@@ -430,8 +427,13 @@ namespace Emby.Server.Implementations.IO
}
}
- private void OnNewRefresherCompleted(object sender, EventArgs e)
+ private void OnNewRefresherCompleted(object? sender, EventArgs e)
{
+ if (sender is null)
+ {
+ return;
+ }
+
var refresher = (FileRefresher)sender;
DisposeRefresher(refresher);
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 1fffdfbfa..c380d67db 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -15,29 +15,34 @@ namespace Emby.Server.Implementations.IO
///
public class ManagedFileSystem : IFileSystem
{
- private readonly ILogger _logger;
-
- private readonly List _shortcutHandlers = new List();
- private readonly string _tempPath;
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
+ private static readonly char[] _invalidPathCharacters =
+ {
+ '\"', '<', '>', '|', '\0',
+ (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
+ (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
+ (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
+ (char)31, ':', '*', '?', '\\', '/'
+ };
+
+ private readonly ILogger _logger;
+ private readonly List _shortcutHandlers;
+ private readonly string _tempPath;
///
/// Initializes a new instance of the class.
///
/// The instance to use.
/// The instance to use.
+ /// the 's to use.
public ManagedFileSystem(
ILogger logger,
- IApplicationPaths applicationPaths)
+ IApplicationPaths applicationPaths,
+ IEnumerable shortcutHandlers)
{
_logger = logger;
_tempPath = applicationPaths.TempDirectory;
- }
-
- ///
- public virtual void AddShortcutHandler(IShortcutHandler handler)
- {
- _shortcutHandlers.Add(handler);
+ _shortcutHandlers = shortcutHandlers.ToList();
}
///
@@ -86,7 +91,7 @@ namespace Emby.Server.Implementations.IO
}
// unc path
- if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
+ if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
{
return filePath;
}
@@ -98,15 +103,17 @@ namespace Emby.Server.Implementations.IO
return filePath;
}
+ var filePathSpan = filePath.AsSpan();
+
// relative path
if (firstChar == '\\')
{
- filePath = filePath.Substring(1);
+ filePathSpan = filePathSpan.Slice(1);
}
try
{
- return Path.GetFullPath(Path.Combine(folderPath, filePath));
+ return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
}
catch (ArgumentException)
{
@@ -275,8 +282,7 @@ namespace Emby.Server.Implementations.IO
/// The filename is null.
public string GetValidFilename(string filename)
{
- var invalid = Path.GetInvalidFileNameChars();
- var first = filename.IndexOfAny(invalid);
+ var first = filename.IndexOfAny(_invalidPathCharacters);
if (first == -1)
{
// Fast path for clean strings
@@ -285,7 +291,7 @@ namespace Emby.Server.Implementations.IO
return string.Create(
filename.Length,
- (filename, invalid, first),
+ (filename, _invalidPathCharacters, first),
(chars, state) =>
{
state.filename.AsSpan().CopyTo(chars);
@@ -293,7 +299,7 @@ namespace Emby.Server.Implementations.IO
chars[state.first++] = ' ';
var len = chars.Length;
- foreach (var c in state.invalid)
+ foreach (var c in state._invalidPathCharacters)
{
for (int i = state.first; i < len; i++)
{
@@ -478,25 +484,11 @@ namespace Emby.Server.Implementations.IO
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
- ///
- public virtual string NormalizePath(string path)
- {
- ArgumentException.ThrowIfNullOrEmpty(path);
-
- if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
- {
- return path;
- }
-
- return Path.TrimEndingDirectorySeparator(path);
- }
-
///
public virtual bool AreEqual(string path1, string path2)
{
- return string.Equals(
- NormalizePath(path1),
- NormalizePath(path2),
+ return Path.TrimEndingDirectorySeparator(path1).Equals(
+ Path.TrimEndingDirectorySeparator(path2),
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
index c2aab3879..5776c7a7c 100644
--- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
+++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
@@ -8,24 +8,17 @@ namespace Emby.Server.Implementations.IO
{
public class MbLinkShortcutHandler : IShortcutHandler
{
- private readonly IFileSystem _fileSystem;
-
- public MbLinkShortcutHandler(IFileSystem fileSystem)
- {
- _fileSystem = fileSystem;
- }
-
public string Extension => ".mblink";
public string? Resolve(string shortcutPath)
{
ArgumentException.ThrowIfNullOrEmpty(shortcutPath);
- if (string.Equals(Path.GetExtension(shortcutPath), ".mblink", StringComparison.OrdinalIgnoreCase))
+ if (Path.GetExtension(shortcutPath.AsSpan()).Equals(".mblink", StringComparison.OrdinalIgnoreCase))
{
var path = File.ReadAllText(shortcutPath);
- return _fileSystem.NormalizePath(path);
+ return Path.TrimEndingDirectorySeparator(path);
}
return null;
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
index 84c21931c..539d4a63a 100644
--- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -31,6 +31,7 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery
{
Parent = item,
+ Recursive = true,
DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary },
OrderBy = new (string, SortOrder)[]
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index 5384c04b3..cf6fc1845 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
// bts sync files
"**/*.bts",
"**/*.sync",
+
+ // zfs
+ "**/.zfs/**",
+ "**/.zfs"
};
private static readonly GlobOptions _globOptions = new GlobOptions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index ea45bf0ba..4f0983564 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -45,7 +46,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger _logger;
- private readonly IMemoryCache _memoryCache;
+ private readonly ConcurrentDictionary _cache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
@@ -111,7 +111,6 @@ namespace Emby.Server.Implementations.Library
/// The media encoder.
/// The item repository.
/// The image processor.
- /// The memory cache.
/// The naming options.
/// The directory service.
public LibraryManager(
@@ -128,7 +127,6 @@ namespace Emby.Server.Implementations.Library
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IImageProcessor imageProcessor,
- IMemoryCache memoryCache,
NamingOptions namingOptions,
IDirectoryService directoryService)
{
@@ -145,7 +143,7 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
- _memoryCache = memoryCache;
+ _cache = new ConcurrentDictionary();
_namingOptions = namingOptions;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger(), namingOptions, directoryService);
@@ -300,7 +298,7 @@ namespace Emby.Server.Implementations.Library
}
}
- _memoryCache.Set(item.Id, item);
+ _cache[item.Id] = item;
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -359,7 +357,7 @@ namespace Emby.Server.Implementations.Library
var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false)
- : Enumerable.Empty();
+ : Array.Empty();
foreach (var metadataPath in GetMetadataPaths(item, children))
{
@@ -441,7 +439,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.DeleteItem(child.Id);
}
- _memoryCache.Remove(item.Id);
+ _cache.TryRemove(item.Id, out _);
ReportItemRemoved(item, parent);
}
@@ -609,7 +607,7 @@ namespace Emby.Server.Implementations.Library
var originalList = paths.ToList();
var list = originalList.Where(i => i.IsDirectory)
- .Select(i => _fileSystem.NormalizePath(i.FullName))
+ .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
@@ -840,19 +838,12 @@ namespace Emby.Server.Implementations.Library
{
var path = Person.GetPath(name);
var id = GetItemByNameId(path);
- if (GetItemById(id) is not Person item)
+ if (GetItemById(id) is Person item)
{
- item = new Person
- {
- Name = name,
- Id = id,
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow,
- Path = path
- };
+ return item;
}
- return item;
+ return null;
}
///
@@ -1163,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
Name = Path.GetFileName(dir),
Locations = _fileSystem.GetFilePaths(dir, false)
- .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.Select(i =>
{
try
@@ -1233,7 +1224,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (_memoryCache.TryGetValue(id, out BaseItem item))
+ if (_cache.TryGetValue(id, out BaseItem item))
{
return item;
}
@@ -2069,7 +2060,9 @@ namespace Emby.Server.Implementations.Library
.Find(folder => folder is CollectionFolder) as CollectionFolder;
}
- return collectionFolder is null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
+ return collectionFolder is null
+ ? new LibraryOptions()
+ : collectionFolder.GetLibraryOptions();
}
public string GetContentType(BaseItem item)
@@ -2857,7 +2850,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
- File.WriteAllBytes(path, Array.Empty());
+ await File.WriteAllBytesAsync(path, Array.Empty()).ConfigureAwait(false);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@@ -2899,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
var saveEntity = false;
var personEntity = GetPerson(person.Name);
- // if PresentationUniqueKey is empty it's likely a new item.
- if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
+ if (personEntity is null)
{
+ var path = Person.GetPath(person.Name);
+ personEntity = new Person()
+ {
+ Name = person.Name,
+ Id = GetItemByNameId(path),
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow,
+ Path = path
+ };
+
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
}
@@ -3134,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
}
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
- .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(shortcut))
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 936a08da8..59d705ace 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey))
{
+ FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try
{
- await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
- catch
+ catch (Exception ex)
{
+ _logger.LogError(ex, "Error deserializing mediainfo cache");
+ }
+ finally
+ {
+ await jsonStream.DisposeAsync().ConfigureAwait(false);
}
}
@@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
- await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
+ await using (createStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
- // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
+ _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index c9a26a30f..91469dba9 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -625,17 +625,19 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey))
{
+ FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try
{
- await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
- // _logger.LogDebug("Found cached media info");
}
catch (Exception ex)
{
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
}
+ finally
+ {
+ await jsonStream.DisposeAsync().ConfigureAwait(false);
+ }
}
if (mediaInfo is null)
@@ -664,8 +666,11 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- await using FileStream createStream = File.Create(cacheFilePath);
- await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ FileStream createStream = File.Create(cacheFilePath);
+ await using (createStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index a74f82475..862f144e6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
{
- var extension = Path.GetExtension(args.Path);
+ var extension = Path.GetExtension(args.Path.AsSpan());
- if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase))
+ if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
{
// if audio file exists of same name, return null
return null;
@@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (item is not null)
{
- item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+ item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
item.IsInMixedFolder = true;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 381796d0e..779cfd5be 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return false;
}
- return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
+ return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
}
///
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 042422c6f..73861ff59 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
return GetBook(args);
}
- var extension = Path.GetExtension(args.Path);
+ var extension = Path.GetExtension(args.Path.AsSpan());
- if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's a book
return new Book
@@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
var bookFiles = args.FileSystemChildren.Where(f =>
{
- var fileExtension = Path.GetExtension(f.FullName)
- ?? string.Empty;
+ var fileExtension = Path.GetExtension(f.FullName.AsSpan());
return _validExtensions.Contains(
fileExtension,
- StringComparer.OrdinalIgnoreCase);
+ StringComparison.OrdinalIgnoreCase);
}).ToList();
// Don't return a Book if there is more (or less) than one document in the directory
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index ea980b992..0b65bf921 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
///
/// Class MovieResolver.
///
- public class MovieResolver : BaseVideoResolver
/// The args.
/// Trailer.
- protected override Photo Resolve(ItemResolveArgs args)
+ protected override Photo? Resolve(ItemResolveArgs args)
{
if (!args.IsDirectory)
{
@@ -68,10 +65,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (IsImageFile(args.Path, _imageProcessor))
{
- var filename = Path.GetFileNameWithoutExtension(args.Path);
+ var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
// Make sure the image doesn't belong to a video file
- var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
+ var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
+ ?? throw new InvalidOperationException("Path can't be a root directory."));
foreach (var file in files)
{
@@ -92,32 +90,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
- internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
+ internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan imageFilename)
{
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
}
- internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
+ internal static bool IsOwnedByResolvedMedia(ReadOnlySpan file, ReadOnlySpan imageFilename)
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{
ArgumentNullException.ThrowIfNull(path);
+ var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
+ if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
var filename = Path.GetFileNameWithoutExtension(path);
- if (_ignoreFiles.Contains(filename))
+ if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
- if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
- {
- return false;
- }
-
- string extension = Path.GetExtension(path).TrimStart('.');
- return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
+ return true;
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index e9538a5c9..858c5b281 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var resolver = new Naming.TV.EpisodeResolver(namingOptions);
var folderName = System.IO.Path.GetFileName(path);
- var testPath = "\\\\test\\" + folderName;
+ var testPath = @"\\test\" + folderName;
var episodeInfo = resolver.Resolve(testPath, true);
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index b9d0f170a..74b62ca3f 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+ var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+ await using (stream.ConfigureAwait(false))
{
var settings = new XmlWriterSettings
{
@@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Async = true
};
- await using (var writer = XmlWriter.Create(stream, settings))
+ var writer = XmlWriter.Create(stream, settings);
+ await using (writer.ConfigureAwait(false))
{
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
@@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+ var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+ await using (stream.ConfigureAwait(false))
{
var settings = new XmlWriterSettings
{
@@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var isSeriesEpisode = timer.IsProgramSeries;
- await using (var writer = XmlWriter.Create(stream, settings))
+ var writer = XmlWriter.Create(stream, settings);
+ await using (writer.ConfigureAwait(false))
{
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
@@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
else
{
- await writer.WriteStartElementAsync(null, "movie", null);
+ await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(item.Name))
{
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 7645c6c52..6b0520ad0 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = JsonContent.Create(requestList, options: _jsonOptions);
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
- await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var dailySchedules = await JsonSerializer.DeserializeAsync>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var dailySchedules = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null)
{
return Array.Empty();
@@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
- await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var programDetails = await JsonSerializer.DeserializeAsync>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var programDetails = await innerResponse.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (programDetails is null)
{
return Array.Empty();
@@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
- await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ return await innerResponse2.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
- await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- var root = await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
+ var root = await httpResponse.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is not null)
{
foreach (HeadendsDto headend in root)
@@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
@@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
- await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var response = httpResponse.Content;
- var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
+ var root = await httpResponse.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
}
catch (HttpRequestException ex)
@@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token);
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
- await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await httpResponse.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is null)
{
return new List();
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 7b6c8b80a..ff25ee585 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -16,21 +17,20 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public abstract class BaseTunerHost
{
- private readonly IMemoryCache _memoryCache;
+ private readonly ConcurrentDictionary> _cache;
- protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IFileSystem fileSystem, IMemoryCache memoryCache)
+ protected BaseTunerHost(IServerConfigurationManager config, ILogger logger, IFileSystem fileSystem)
{
Config = config;
Logger = logger;
- _memoryCache = memoryCache;
FileSystem = fileSystem;
+ _cache = new ConcurrentDictionary>();
}
protected IServerConfigurationManager Config { get; }
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var key = tuner.Id;
- if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List cache))
+ if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List cache))
{
return cache;
}
@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!string.IsNullOrEmpty(key) && list.Count > 0)
{
- _memoryCache.Set(key, list);
+ _cache[key] = list;
}
return list;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 98bbc1540..8cd0c4ffb 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -27,7 +28,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -50,9 +50,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
ISocketFactory socketFactory,
- IStreamHelper streamHelper,
- IMemoryCache memoryCache)
- : base(config, logger, fileSystem, memoryCache)
+ IStreamHelper streamHelper)
+ : base(config, logger, fileSystem)
{
_httpClientFactory = httpClientFactory;
_appHost = appHost;
@@ -77,13 +76,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var lineup = await JsonSerializer.DeserializeAsync>(stream, _jsonOptions, cancellationToken)
- .ConfigureAwait(false) ?? new List();
-
+ var lineup = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty();
if (info.ImportFavoritesOnly)
{
- lineup = lineup.Where(i => i.Favorite).ToList();
+ lineup = lineup.Where(i => i.Favorite);
}
return lineup.Where(i => !i.DRM).ToList();
@@ -130,9 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
.GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var discoverResponse = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken)
- .ConfigureAwait(false);
+ var discoverResponse = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey))
{
@@ -176,34 +170,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List();
- await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
+ var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
{
- string stripedLine = StripXML(line);
- if (stripedLine.Contains("Channel", StringComparison.Ordinal))
+ using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
+ await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
{
- LiveTvTunerStatus status;
- var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
- var name = stripedLine.Substring(0, index - 1);
- var currentChannel = stripedLine.Substring(index + 7);
- if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
+ string stripedLine = StripXML(line);
+ if (stripedLine.Contains("Channel", StringComparison.Ordinal))
{
- status = LiveTvTunerStatus.LiveTv;
- }
- else
- {
- status = LiveTvTunerStatus.Available;
- }
+ LiveTvTunerStatus status;
+ var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+ var name = stripedLine.Substring(0, index - 1);
+ var currentChannel = stripedLine.Substring(index + 7);
+ if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
+ {
+ status = LiveTvTunerStatus.LiveTv;
+ }
+ else
+ {
+ status = LiveTvTunerStatus.Available;
+ }
- tuners.Add(new LiveTvTunerInfo
- {
- Name = name,
- SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
- ProgramName = currentChannel,
- Status = status
- });
+ tuners.Add(new LiveTvTunerInfo
+ {
+ Name = name,
+ SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
+ ProgramName = currentChannel,
+ Status = status
+ });
+ }
}
}
@@ -661,18 +658,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// Need a way to set the Receive timeout on the socket otherwise this might never timeout?
try
{
- await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false);
+ await udpClient.SendToAsync(discBytes, new IPEndPoint(IPAddress.Broadcast, 65001), cancellationToken).ConfigureAwait(false);
var receiveBuffer = new byte[8192];
while (!cancellationToken.IsCancellationRequested)
{
- var response = await udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
- var deviceIp = response.RemoteEndPoint.Address.ToString();
+ var response = await udpClient.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, 0), cancellationToken).ConfigureAwait(false);
+ var deviceIP = ((IPEndPoint)response.RemoteEndPoint).Address.ToString();
- // check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
- if (response.ReceivedBytes > 13 && response.Buffer[1] == 3)
+ // Check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
+ if (response.ReceivedBytes > 13 && receiveBuffer[1] == 3)
{
- var deviceAddress = "http://" + deviceIp;
+ var deviceAddress = "http://" + deviceIP;
var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index 7bc209d6b..68383a554 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -44,14 +44,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult();
}
}
-
- GC.SuppressFinalize(this);
}
- public async Task CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
+ public async Task CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)
{
using var client = new TcpClient();
- await client.ConnectAsync(remoteIp, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
+ await client.ConnectAsync(remoteIP, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
using var stream = client.GetStream();
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
@@ -75,9 +73,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- public async Task StartStreaming(IPAddress remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
+ public async Task StartStreaming(IPAddress remoteIP, IPAddress localIP, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
{
- _remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort);
+ _remoteEndPoint = new IPEndPoint(remoteIP, HdHomeRunPort);
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false);
@@ -125,7 +123,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
- var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
+ var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIP, localPort);
var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
index 3450f971f..654474e97 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
- public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
+ public partial class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
{
private string? _channel;
private string? _program;
@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public LegacyHdHomerunChannelCommands(string url)
{
// parse url for channel and program
- var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)");
+ var match = ChannelAndProgramRegex().Match(url);
if (match.Success)
{
_channel = match.Groups[1].Value;
@@ -21,6 +21,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
+ [GeneratedRegex(@"\/ch([0-9]+)-?([0-9]*)")]
+ private static partial Regex ChannelAndProgramRegex();
+
public IEnumerable<(string CommandName, string CommandValue)> GetCommands()
{
if (!string.IsNullOrEmpty(_channel))
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index acf3964c8..db5e81df5 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
@@ -54,9 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost,
INetworkManager networkManager,
- IStreamHelper streamHelper,
- IMemoryCache memoryCache)
- : base(config, logger, fileSystem, memoryCache)
+ IStreamHelper streamHelper)
+ : base(config, logger, fileSystem)
{
_httpClientFactory = httpClientFactory;
_appHost = appHost;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index b41816230..341782d9d 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -20,7 +20,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
- public class M3uParser
+ public partial class M3uParser
{
private const string ExtInfPrefix = "#EXTINF:";
@@ -33,6 +33,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_httpClientFactory = httpClientFactory;
}
+ [GeneratedRegex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase, "en-US")]
+ private static partial Regex KeyValueRegex();
+
public async Task> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
{
// Read the file and display it line by line.
@@ -91,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
- if (string.IsNullOrWhiteSpace(channel.Id))
- {
- channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
- else
- {
- channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
+ channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
channel.Path = trimmedLine;
channels.Add(channel);
@@ -311,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var dict = new Dictionary(StringComparer.OrdinalIgnoreCase);
- var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
+ var matches = KeyValueRegex().Matches(line);
remaining = line;
@@ -320,7 +316,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var key = match.Groups[1].Value;
var value = match.Groups[2].Value;
- dict[match.Groups[1].Value] = match.Groups[2].Value;
+ dict[key] = value;
remaining = remaining.Replace(key + "=\"" + value + "\"", string.Empty, StringComparison.OrdinalIgnoreCase);
}
diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json
index 0967ef424..7c7dd26e9 100644
--- a/Emby.Server.Implementations/Localization/Core/as.json
+++ b/Emby.Server.Implementations/Localization/Core/as.json
@@ -1 +1,43 @@
-{}
+{
+ "Albums": "এলবাম",
+ "Application": "আবেদন",
+ "AppDeviceValues": "এপ্: {0}, ডিভাইচ: {1}",
+ "Artists": "শিল্পী",
+ "Channels": "চেনেলস",
+ "Default": "ডিফল্ট",
+ "AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত",
+ "Books": "পুস্তক",
+ "Movies": "চলচ্চিত্ৰ",
+ "CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}",
+ "Collections": "সংগ্রহ",
+ "HeaderFavoriteShows": "প্রিয় শোসমূহ",
+ "Latest": "শেহতীয়া",
+ "MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে",
+ "MixedContent": "মিশ্ৰিত সমগ্ৰতা",
+ "NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.",
+ "NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল",
+ "External": "বাহ্যিক",
+ "Favorites": "পছন্দসই",
+ "Folders": "ফোল্ডাৰ",
+ "Forced": "বলপূর্বক",
+ "Genres": "শ্রেণী",
+ "HeaderAlbumArtists": "অ্যালবাম শিল্পী",
+ "HeaderContinueWatching": "দেখা চালিয়ে যান",
+ "FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}",
+ "HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ",
+ "HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ",
+ "HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ",
+ "HeaderFavoriteSongs": "প্ৰিয় গীত",
+ "HeaderLiveTV": "প্ৰতিবেদন টিভি",
+ "HeaderNextUp": "পৰৱৰ্তী অংশ",
+ "HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ",
+ "HearingImpaired": "শ্ৰবণ অক্ষম",
+ "HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ",
+ "Inherit": "উত্তপ্ত কৰা",
+ "MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে",
+ "NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ",
+ "NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল",
+ "NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল",
+ "NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল",
+ "NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/chr.json b/Emby.Server.Implementations/Localization/Core/chr.json
new file mode 100644
index 000000000..85d1f4c88
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/chr.json
@@ -0,0 +1,52 @@
+{
+ "ChapterNameValue": "Didanedi {0}",
+ "HeaderAlbumArtists": "Didanidanolisgisgi",
+ "HeaderFavoriteAlbums": "Dvganidi didanidisgisgi",
+ "HeaderLiveTV": "Anigadi didanidisgosgi",
+ "HeaderRecordingGroups": "Didanisquodiisgisgi",
+ "HomeVideos": "Diganadi dinagadisgisgi",
+ "Inherit": "Anigwe",
+ "MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}",
+ "MixedContent": "Ganinidi dininoladisgisgi",
+ "Movies": "Anidvnisgisgi",
+ "MusicVideos": "Danodisgisgi didanidisgosgi",
+ "NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi",
+ "NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi",
+ "NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi",
+ "Albums": "Anigawidaniyv",
+ "Application": "Didanvyi",
+ "Artists": "Dinidaniyi",
+ "AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani",
+ "Books": "Didanedi",
+ "CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}",
+ "Channels": "Diganadasgi",
+ "Collections": "Diganadisgi",
+ "Default": "Dinadi",
+ "DeviceOfflineWithName": "{0} Aniyvolehvi nasgi",
+ "External": "Amohdi",
+ "Favorites": "Nvdayelvdisgi",
+ "Folders": "Didanididisgi",
+ "Forced": "Ganedi",
+ "Genres": "Diganadisgi",
+ "HeaderContinueWatching": "Uwoditsu asdanidisgisgi",
+ "HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi",
+ "HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi",
+ "HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)",
+ "HeaderFavoriteSongs": "Dvganidi danodisgisgi",
+ "HeaderNextUp": "Anidvli uwodoli",
+ "HearingImpaired": "Anitsunidi talunidisgisgi",
+ "ItemAddedWithName": "{0} Dinigwe anididanidisgi",
+ "Latest": "Uwodoli",
+ "MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe",
+ "MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi",
+ "Music": "Danodisgisgi",
+ "NameSeasonUnknown": "Tsunita anidvdisgi",
+ "NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.",
+ "NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi",
+ "NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi",
+ "NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi",
+ "NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi",
+ "NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi",
+ "NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi",
+ "NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 08db5a30e..f33ea2fc9 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -22,7 +22,7 @@
"HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály",
"HeaderFavoriteSongs": "Oblíbená hudba",
- "HeaderLiveTV": "Televize",
+ "HeaderLiveTV": "Živý přenos",
"HeaderNextUp": "Další díly",
"HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domácí videa",
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 1b6eecdcf..837172a5b 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -15,13 +15,13 @@
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Genrer",
- "HeaderAlbumArtists": "Albums kunstnere",
+ "HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
- "HeaderFavoriteAlbums": "Favorit albummer",
- "HeaderFavoriteArtists": "Favorit kunstnere",
- "HeaderFavoriteEpisodes": "Favorit afsnit",
- "HeaderFavoriteShows": "Favorit serier",
- "HeaderFavoriteSongs": "Favorit sange",
+ "HeaderFavoriteAlbums": "Favoritalbummer",
+ "HeaderFavoriteArtists": "Favoritkunstnere",
+ "HeaderFavoriteEpisodes": "Yndlingsafsnit",
+ "HeaderFavoriteShows": "Yndlingsserier",
+ "HeaderFavoriteSongs": "Yndlingssange",
"HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper",
@@ -34,8 +34,8 @@
"Latest": "Seneste",
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
- "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
+ "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
"MixedContent": "Blandet indhold",
"Movies": "Film",
"Music": "Musik",
@@ -51,7 +51,7 @@
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
"NotificationOptionInstallationFailed": "Installationen mislykkedes",
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
- "NotificationOptionPluginError": "Plugin fejl",
+ "NotificationOptionPluginError": "Plugin-fejl",
"NotificationOptionPluginInstalled": "Plugin blev installeret",
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
@@ -92,26 +92,26 @@
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
- "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
+ "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins",
- "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
- "TaskCleanLogs": "Ryd Log mappe",
- "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
- "TaskRefreshLibrary": "Scan Medie Bibliotek",
- "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
- "TaskCleanCache": "Ryd Cache mappe",
- "TasksChannelsCategory": "Internet Kanaler",
+ "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
+ "TaskCleanLogs": "Ryd Log-mappe",
+ "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
+ "TaskRefreshLibrary": "Scan Mediebibliotek",
+ "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
+ "TaskCleanCache": "Ryd Cache-mappe",
+ "TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedligeholdelse",
- "TaskRefreshChapterImages": "Udtræk kapitel billeder",
- "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
- "TaskRefreshChannelsDescription": "Opdater internet kanal information.",
+ "TaskRefreshChapterImages": "Udtræk kapitelbilleder",
+ "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
+ "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
"TaskRefreshChannels": "Opdater Kanaler",
- "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
- "TaskCleanTranscode": "Tøm Transcode mappen",
+ "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
+ "TaskCleanTranscode": "Tøm Transcode-mappen",
"TaskRefreshPeople": "Opdater Personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
@@ -121,8 +121,8 @@
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
"TaskOptimizeDatabase": "Optimér database",
- "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
- "TaskKeyframeExtractor": "Nøglebillede udtræk",
+ "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
+ "TaskKeyframeExtractor": "Udtræk af nøglebillede",
"External": "Ekstern",
"HearingImpaired": "Hørehæmmet"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index f5636a0af..4c56f789d 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -3,9 +3,9 @@
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación",
"Artists": "Artistas",
- "AuthenticationSucceededWithUserName": "{0} identificado correctamente",
+ "AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros",
- "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
+ "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
"Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index 8672cfb9f..08344abeb 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -74,16 +74,16 @@
"Shows": "Sarjat",
"ServerNameNeedsToBeRestarted": "\"{0}\" on käynnistettävä uudelleen",
"ProviderValue": "Lähde: {0}",
- "Plugin": "Laajennus",
+ "Plugin": "Lisäosa",
"NotificationOptionVideoPlaybackStopped": "Videon toisto lopetettu",
"NotificationOptionVideoPlayback": "Videon toisto aloitettu",
"NotificationOptionUserLockedOut": "Käyttäjä on lukittu",
"NotificationOptionTaskFailed": "Ajoitettu tehtävä epäonnistui",
"NotificationOptionServerRestartRequired": "Tarvitaan palvelimen uudelleenkäynnistys",
- "NotificationOptionPluginUpdateInstalled": "Laajennus on päivitetty",
- "NotificationOptionPluginUninstalled": "Laajennus on poistettu",
- "NotificationOptionPluginInstalled": "Laajennus on asennettu",
- "NotificationOptionPluginError": "Laajennuksen virhe",
+ "NotificationOptionPluginUpdateInstalled": "Lisäosa päivitettiin",
+ "NotificationOptionPluginUninstalled": "Lisäosa poistettiin",
+ "NotificationOptionPluginInstalled": "Lisäosa asennettiin",
+ "NotificationOptionPluginError": "Lisäosan virhe",
"NotificationOptionNewLibraryContent": "Sisältöä on lisätty",
"NotificationOptionInstallationFailed": "Asennus epäonnistui",
"NotificationOptionCameraImageUploaded": "Kameran kuva on tallennettu",
@@ -98,8 +98,8 @@
"TaskRefreshChannels": "Päivitä kanavat",
"TaskCleanTranscodeDescription": "Poistaa päivää vanhemmat transkoodaustiedostot.",
"TaskCleanTranscode": "Puhdista transkoodauskansio",
- "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset laajennuksille, jotka on määritetty päivittymään automaattisesti.",
- "TaskUpdatePlugins": "Päivitä laajennukset",
+ "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset lisäosille, jotka on määritetty päivittymään automaattisesti.",
+ "TaskUpdatePlugins": "Päivitä lisäosat",
"TaskRefreshPeopleDescription": "Päivittää mediakirjaston näyttelijöiden ja ohjaajien metatiedot.",
"TaskRefreshPeople": "Päivitä henkilöt",
"TaskCleanLogsDescription": "Poistaa {0} päivää vanhemmat lokitiedostot.",
diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json
new file mode 100644
index 000000000..40aa5f71a
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/fo.json
@@ -0,0 +1,18 @@
+{
+ "Artists": "Listafólk",
+ "Collections": "Søvn",
+ "Default": "Sjálvgildi",
+ "DeviceOfflineWithName": "{0} hevur slitið sambandið",
+ "External": "Ytri",
+ "Genres": "Greinar",
+ "Albums": "Album",
+ "AppDeviceValues": "App: {0}, Eind: {1}",
+ "Application": "Nýtsluskipan",
+ "Books": "Bøkur",
+ "Channels": "Rásir",
+ "ChapterNameValue": "Kapittul {0}",
+ "DeviceOnlineWithName": "{0} er sambundið",
+ "Favorites": "Yndis",
+ "Folders": "Mappur",
+ "Forced": "Kravt"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 4877bcd7a..a2b429dcd 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -105,8 +105,8 @@
"TaskRefreshPeople": "Actualiser les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
- "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.",
- "TaskRefreshLibrary": "Scanner la médiathèque",
+ "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
+ "TaskRefreshLibrary": "Analyser la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 694a3d688..68e9fe833 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -5,18 +5,18 @@
"Artists": "אומנים",
"AuthenticationSucceededWithUserName": "{0} אומת בהצלחה",
"Books": "ספרים",
- "CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מ {0}",
+ "CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מתוך {0}",
"Channels": "ערוצים",
"ChapterNameValue": "פרק {0}",
"Collections": "אוספים",
"DeviceOfflineWithName": "{0} התנתק",
"DeviceOnlineWithName": "{0} מחובר",
- "FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי מ{0}",
+ "FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי דרך {0}",
"Favorites": "מועדפים",
"Folders": "תיקיות",
- "Genres": "ז'אנרים",
+ "Genres": "ז׳אנרים",
"HeaderAlbumArtists": "אמני האלבום",
- "HeaderContinueWatching": "המשך לצפות",
+ "HeaderContinueWatching": "להמשיך לצפות",
"HeaderFavoriteAlbums": "אלבומים מועדפים",
"HeaderFavoriteArtists": "אמנים מועדפים",
"HeaderFavoriteEpisodes": "פרקים מועדפים",
@@ -27,14 +27,14 @@
"HeaderRecordingGroups": "קבוצות הקלטה",
"HomeVideos": "סרטונים בייתים",
"Inherit": "הורש",
- "ItemAddedWithName": "{0} הוסף לספרייה",
+ "ItemAddedWithName": "{0} נוסף לספרייה",
"ItemRemovedWithName": "{0} נמחק מהספרייה",
"LabelIpAddressValue": "Ip כתובת: {0}",
"LabelRunningTimeValue": "משך צפייה: {0}",
"Latest": "אחרון",
"MessageApplicationUpdated": "שרת הJellyfin עודכן",
- "MessageApplicationUpdatedTo": "שרת הJellyfin עודכן לגרסא {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "הגדרת השרת {0} שונתה",
+ "MessageApplicationUpdatedTo": "שרת ה־Jellyfin עודכן לגרסה {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
"MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
"MixedContent": "תוכן מעורב",
"Movies": "סרטים",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
"NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
"NotificationOptionInstallationFailed": "התקנה נכשלה",
- "NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
+ "NotificationOptionNewLibraryContent": "תוכן חדש נוסף",
"NotificationOptionPluginError": "כשלון בתוסף",
"NotificationOptionPluginInstalled": "התוסף הותקן",
"NotificationOptionPluginUninstalled": "התוסף הוסר",
@@ -61,41 +61,41 @@
"NotificationOptionVideoPlayback": "ניגון וידאו החל",
"NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
"Photos": "תמונות",
- "Playlists": "רשימות הפעלה",
- "Plugin": "Plugin",
+ "Playlists": "רשימות נגינה",
+ "Plugin": "תוסף",
"PluginInstalledWithName": "{0} הותקן",
"PluginUninstalledWithName": "{0} הוסר",
"PluginUpdatedWithName": "{0} עודכן",
- "ProviderValue": "Provider: {0}",
+ "ProviderValue": "ספק: {0}",
"ScheduledTaskFailedWithName": "{0} נכשל",
"ScheduledTaskStartedWithName": "{0} החל",
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
"Shows": "סדרות",
"Songs": "שירים",
- "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
+ "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. נא לנסות שנית בהקדם.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
- "Sync": "סנכרן",
- "System": "System",
+ "SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
+ "Sync": "סנכרון",
+ "System": "מערכת",
"TvShows": "סדרות טלוויזיה",
- "User": "User",
+ "User": "משתמש",
"UserCreatedWithName": "המשתמש {0} נוצר",
"UserDeletedWithName": "המשתמש {0} הוסר",
"UserDownloadingItemWithValues": "{0} מוריד את {1}",
"UserLockedOutWithName": "המשתמש {0} ננעל",
- "UserOfflineFromDevice": "{0} התנתק מ-{1}",
- "UserOnlineFromDevice": "{0} מחובר מ-{1}",
+ "UserOfflineFromDevice": "{0} התנתק מ־{1}",
+ "UserOnlineFromDevice": "{0} מחובר מ־{1}",
"UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
"UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
"UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
"ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
"ValueSpecialEpisodeName": "מיוחד- {0}",
- "VersionNumber": "Version {0}",
+ "VersionNumber": "גרסה {0}",
"TaskRefreshLibrary": "סרוק ספריית מדיה",
"TaskRefreshChapterImages": "חלץ תמונות פרקים",
"TaskCleanCacheDescription": "מחק קבצי מטמון שלא בשימוש המערכת.",
- "TaskCleanCache": "נקה תיקיית מטמון",
+ "TaskCleanCache": "ניקוי תיקיית מטמון",
"TasksApplicationCategory": "יישום",
"TasksLibraryCategory": "ספרייה",
"TasksMaintenanceCategory": "תחזוקה",
@@ -103,7 +103,7 @@
"TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
"TaskRefreshPeople": "רענן אנשים",
"TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
- "TaskCleanLogs": "נקה תיקיית יומן",
+ "TaskCleanLogs": "ניקוי תיקיית יומן",
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
"TasksChannelsCategory": "ערוצי אינטרנט",
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index 62d48cebd..5a4a02d80 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -1,11 +1,11 @@
{
"Albums": "Albumok",
- "AppDeviceValues": "Program: {0}, Eszköz: {1}",
+ "AppDeviceValues": "Program: {0}, eszköz: {1}",
"Application": "Alkalmazás",
"Artists": "Előadók",
- "AuthenticationSucceededWithUserName": "{0} sikeresen azonosítva",
+ "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
"Books": "Könyvek",
- "CameraImageUploadedFrom": "Új kamerakép került feltöltésre innen: {0}",
+ "CameraImageUploadedFrom": "Új kamerakép feltöltve innen: {0}",
"Channels": "Csatornák",
"ChapterNameValue": "{0}. jelenet",
"Collections": "Gyűjtemények",
@@ -15,13 +15,13 @@
"Favorites": "Kedvencek",
"Folders": "Könyvtárak",
"Genres": "Műfajok",
- "HeaderAlbumArtists": "Album előadó(k)",
+ "HeaderAlbumArtists": "Albumelőadók",
"HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteArtists": "Kedvenc előadók",
"HeaderFavoriteEpisodes": "Kedvenc epizódok",
"HeaderFavoriteShows": "Kedvenc sorozatok",
- "HeaderFavoriteSongs": "Kedvenc dalok",
+ "HeaderFavoriteSongs": "Kedvenc számok",
"HeaderLiveTV": "Élő TV",
"HeaderNextUp": "Következik",
"HeaderRecordingGroups": "Felvételi csoportok",
@@ -29,37 +29,37 @@
"Inherit": "Örökölt",
"ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
"ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
- "LabelIpAddressValue": "IP cím: {0}",
- "LabelRunningTimeValue": "Futási idő: {0}",
+ "LabelIpAddressValue": "IP-cím: {0}",
+ "LabelRunningTimeValue": "Lejátszási idő: {0}",
"Latest": "Legújabb",
- "MessageApplicationUpdated": "Jellyfin Szerver frissítve",
- "MessageApplicationUpdatedTo": "Jellyfin Szerver frissítve lett a következőre: {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigurációs rész frissítve: {0}",
- "MessageServerConfigurationUpdated": "Szerver konfiguráció frissítve",
+ "MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve",
+ "MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}",
+ "MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve: {0}",
+ "MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve",
"MixedContent": "Vegyes tartalom",
"Movies": "Filmek",
- "Music": "Zene",
+ "Music": "Zenék",
"MusicVideos": "Zenei videóklippek",
"NameInstallFailed": "{0} sikertelen telepítés",
"NameSeasonNumber": "{0}. évad",
"NameSeasonUnknown": "Ismeretlen évad",
- "NewVersionIsAvailable": "Letölthető a Jellyfin Szerver új verziója.",
+ "NewVersionIsAvailable": "Letölthető a Jellyfin kiszolgáló új verziója.",
"NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz",
"NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve",
- "NotificationOptionAudioPlayback": "Audió lejátszás elkezdve",
- "NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva",
- "NotificationOptionCameraImageUploaded": "Kamera kép feltöltve",
- "NotificationOptionInstallationFailed": "Telepítés sikertelen",
+ "NotificationOptionAudioPlayback": "Hanglejátszás elkezdve",
+ "NotificationOptionAudioPlaybackStopped": "Hanglejátszás leállítva",
+ "NotificationOptionCameraImageUploaded": "Kamerakép feltöltve",
+ "NotificationOptionInstallationFailed": "Telepítési hiba",
"NotificationOptionNewLibraryContent": "Új tartalom hozzáadva",
- "NotificationOptionPluginError": "Bővítmény hiba",
+ "NotificationOptionPluginError": "Bővítményhiba",
"NotificationOptionPluginInstalled": "Bővítmény telepítve",
"NotificationOptionPluginUninstalled": "Bővítmény eltávolítva",
- "NotificationOptionPluginUpdateInstalled": "Bővítmény frissítés telepítve",
- "NotificationOptionServerRestartRequired": "Szerver újraindítás szükséges",
+ "NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve",
+ "NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges",
"NotificationOptionTaskFailed": "Ütemezett feladat hiba",
"NotificationOptionUserLockedOut": "Felhasználó tiltva",
- "NotificationOptionVideoPlayback": "Videó lejátszás elkezdve",
- "NotificationOptionVideoPlaybackStopped": "Videó lejátszás leállítva",
+ "NotificationOptionVideoPlayback": "Videólejátszás elkezdve",
+ "NotificationOptionVideoPlaybackStopped": "Videólejátszás leállítva",
"Photos": "Fényképek",
"Playlists": "Lejátszási listák",
"Plugin": "Bővítmény",
@@ -69,47 +69,47 @@
"ProviderValue": "Szolgáltató: {0}",
"ScheduledTaskFailedWithName": "{0} sikertelen",
"ScheduledTaskStartedWithName": "{0} elkezdve",
- "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani",
+ "ServerNameNeedsToBeRestarted": "A(z) {0} újraindítása szükséges",
"Shows": "Sorozatok",
- "Songs": "Dalok",
- "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.",
+ "Songs": "Számok",
+ "StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}",
- "Sync": "Szinkronizál",
+ "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}",
+ "Sync": "Szinkronizálás",
"System": "Rendszer",
"TvShows": "TV műsorok",
"User": "Felhasználó",
"UserCreatedWithName": "{0} felhasználó létrehozva",
"UserDeletedWithName": "{0} felhasználó törölve",
- "UserDownloadingItemWithValues": "{0} letölti {1}",
+ "UserDownloadingItemWithValues": "{0} letölti: {1}",
"UserLockedOutWithName": "{0} felhasználó zárolva van",
"UserOfflineFromDevice": "{0} kijelentkezett innen: {1}",
"UserOnlineFromDevice": "{0} online innen: {1}",
- "UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}",
- "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}",
- "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}",
- "UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}",
+ "UserPasswordChangedWithName": "{0} jelszava megváltozott",
+ "UserPolicyUpdatedWithName": "{0} felhasználói házirendje frissült",
+ "UserStartedPlayingItemWithValues": "{0} elkezdte lejátszani a következőt: {1}, itt: {2}",
+ "UserStoppedPlayingItemWithValues": "{0} befejezte a következő lejátszását: {1}, itt: {2}",
"ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz",
- "ValueSpecialEpisodeName": "Special - {0}",
+ "ValueSpecialEpisodeName": "Különkiadás – {0}",
"VersionNumber": "Verzió: {0}",
"TaskCleanTranscode": "Átkódolási könyvtár ürítése",
"TaskUpdatePluginsDescription": "Letölti és telepíti a frissítéseket azokhoz a bővítményekhez, amelyeknél az automatikus frissítés engedélyezve van.",
"TaskUpdatePlugins": "Bővítmények frissítése",
- "TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a könyvtáradban.",
+ "TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a médiatárban.",
"TaskRefreshPeople": "Személyek frissítése",
"TaskCleanLogsDescription": "Törli azokat a naplófájlokat, amelyek {0} napnál régebbiek.",
"TaskCleanLogs": "Naplózási könyvtár ürítése",
- "TaskRefreshLibraryDescription": "Átvizsgálja a könyvtáraidat új fájlokért és frissíti a metaadatokat.",
- "TaskRefreshLibrary": "Média könyvtár beolvasása",
- "TaskRefreshChapterImagesDescription": "Miniatűröket generál olyan videókhoz, amely tartalmaz fejezeteket.",
- "TaskRefreshChapterImages": "Fejezetek képeinek generálása",
+ "TaskRefreshLibraryDescription": "Átvizsgálja a médiatárat új fájlokat keresve, és frissíti a metaadatokat.",
+ "TaskRefreshLibrary": "Médiatár átvizsgálása",
+ "TaskRefreshChapterImagesDescription": "Miniatűröket hoz létre az olyan videókhoz, amely tartalmaz fejezeteket.",
+ "TaskRefreshChapterImages": "Fejezetképek kinyerése",
"TaskCleanCacheDescription": "Törli azokat a gyorsítótárazott fájlokat, amikre a rendszernek már nincs szüksége.",
"TaskCleanCache": "Gyorsítótár könyvtárának ürítése",
"TasksChannelsCategory": "Internetes csatornák",
"TasksApplicationCategory": "Alkalmazás",
"TasksLibraryCategory": "Könyvtár",
"TasksMaintenanceCategory": "Karbantartás",
- "TaskDownloadMissingSubtitlesDescription": "A metaadat konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.",
+ "TaskDownloadMissingSubtitlesDescription": "A metaadat-konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.",
"TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
"TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.",
"TaskRefreshChannels": "Csatornák frissítése",
@@ -121,8 +121,8 @@
"Default": "Alapértelmezett",
"TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.",
"TaskOptimizeDatabase": "Adatbázis optimalizálása",
- "TaskKeyframeExtractor": "Kulcskockák kibontása",
- "TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
+ "TaskKeyframeExtractor": "Kulcsképkockák kibontása",
+ "TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
"External": "Külső",
"HearingImpaired": "Hallássérült"
}
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index a40f49506..0f1f0b3d2 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -13,8 +13,8 @@
"HeaderFavoriteArtists": "Uppáhalds Listamenn",
"HeaderFavoriteAlbums": "Uppáhalds Plötur",
"HeaderContinueWatching": "Halda áfram að horfa",
- "HeaderAlbumArtists": "Höfundur plötu",
- "Genres": "Tegundir",
+ "HeaderAlbumArtists": "Listamaður á umslagi",
+ "Genres": "Stefnur",
"Folders": "Möppur",
"Favorites": "Uppáhalds",
"FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig",
@@ -22,32 +22,32 @@
"DeviceOfflineWithName": "{0} hefur aftengst",
"Collections": "Söfn",
"ChapterNameValue": "Kafli {0}",
- "Channels": "Stöðvar",
- "CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}",
+ "Channels": "Rásir",
+ "CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni",
"Books": "Bækur",
- "AuthenticationSucceededWithUserName": "{0} auðkenning tókst",
- "Artists": "Listamaður",
+ "AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst",
+ "Artists": "Listamenn",
"Application": "Forrit",
"AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
"Albums": "Plötur",
- "Plugin": "Viðbót",
- "Photos": "Myndir",
- "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
- "NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
+ "Plugin": "Viðbótarvirkni",
+ "Photos": "Ljósmyndir",
+ "NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð",
+ "NotificationOptionVideoPlayback": "Myndbandsafspilun hafin",
"NotificationOptionUserLockedOut": "Notandi læstur úti",
- "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg",
- "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett",
- "NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
- "NotificationOptionPluginInstalled": "Viðbót sett upp",
+ "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg",
+ "NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið",
+ "NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð",
+ "NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp",
"NotificationOptionPluginError": "Bilun í viðbót",
"NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
- "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
+ "NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp",
"NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð",
"NotificationOptionAudioPlayback": "Hljóðafspilun hafin",
"NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett",
"NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði",
- "NameSeasonUnknown": "Sería óþekkt",
- "NameSeasonNumber": "Sería {0}",
+ "NameSeasonUnknown": "Þáttaröð óþekkt",
+ "NameSeasonNumber": "Þáttaröð {0}",
"MixedContent": "Blandað efni",
"MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
"MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
@@ -57,24 +57,24 @@
"User": "Notandi",
"System": "Kerfi",
"NotificationOptionNewLibraryContent": "Nýju efni bætt við",
- "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.",
+ "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.",
"NameInstallFailed": "{0} uppsetning mistókst",
"MusicVideos": "Tónlistarmyndbönd",
"Music": "Tónlist",
"Movies": "Kvikmyndir",
"UserDeletedWithName": "Notanda {0} hefur verið eytt",
"UserCreatedWithName": "Notandi {0} hefur verið stofnaður",
- "TvShows": "Þættir",
+ "TvShows": "Sjónvarpsþættir",
"Sync": "Samstilla",
"Songs": "Lög",
- "ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa",
+ "ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur",
"ScheduledTaskStartedWithName": "{0} hafin",
"ScheduledTaskFailedWithName": "{0} mistókst",
"PluginUpdatedWithName": "{0} var uppfært",
"PluginUninstalledWithName": "{0} var fjarlægt",
"PluginInstalledWithName": "{0} var sett upp",
"NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
- "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.",
+ "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.",
"VersionNumber": "Útgáfa {0}",
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
@@ -83,14 +83,14 @@
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
- "UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur",
- "UserDownloadingItemWithValues": "{0} Hleður niður {1}",
+ "UserLockedOutWithName": "Notandi {0} hefur verið læstur úti",
+ "UserDownloadingItemWithValues": "{0} hleður niður {1}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
- "ProviderValue": "Veitandi: {0}",
+ "ProviderValue": "Efnisveita: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
- "ValueSpecialEpisodeName": "Sérstakt - {0}",
- "Shows": "Sýningar",
- "Playlists": "Spilunarlisti",
+ "ValueSpecialEpisodeName": "Sérstaktur - {0}",
+ "Shows": "Þættir",
+ "Playlists": "Efnisskrár",
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
"TaskRefreshChannels": "Endurhlaða Rásir",
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
@@ -116,5 +116,12 @@
"TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
"TaskCleanLogs": "Hreinsa færslu skrá",
"TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
- "HearingImpaired": "Heyrnarskertur"
+ "HearingImpaired": "Heyrnarskertur",
+ "TaskOptimizeDatabaseDescription": "Þjappar gagnagrunni og bætir við lausu diskaplássi. Að keyra þessa aðgerð eftir skönnun safnsins, eða eftir einhverjar breytingar sem fela í sér gagnagrunnsbreytingar, gætu aukið hraðvirkni.",
+ "TaskKeyframeExtractor": "Lykilrammaplokkari",
+ "TaskKeyframeExtractorDescription": "Plokkar lykilramma úr myndbandsskrám til að búa til nákvæmari HLS uppskiptingarlista. Þetta verk getur tekið langan tíma.",
+ "TaskRefreshChapterImages": "Plokka kafla-myndir",
+ "TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
+ "Forced": "Þvingað",
+ "External": "Útvær"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json
index 3c8c38ed4..5e2b3756b 100644
--- a/Emby.Server.Implementations/Localization/Core/kn.json
+++ b/Emby.Server.Implementations/Localization/Core/kn.json
@@ -3,5 +3,125 @@
"TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
"TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
"TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್ಟ್ರಾಕ್ಟರ್",
- "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್ಗಳಿಂದ ಕೀಫ್ರೇಮ್ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು."
+ "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್ಗಳಿಂದ ಕೀಫ್ರೇಮ್ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
+ "ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
+ "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
+ "TasksLibraryCategory": "ಸಮೊಹ",
+ "TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
+ "TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
+ "TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ",
+ "TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
+ "UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+ "Albums": "ಸಂಪುಟ",
+ "Application": "ಅಪ್ಲಿಕೇಶನ್",
+ "AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
+ "Artists": "ಕಲಾವಿದರು",
+ "AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
+ "Books": "ಪುಸ್ತಕಗಳು",
+ "ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
+ "Collections": "ಸಂಗ್ರಹಣೆಗಳು",
+ "Default": "ಪೂರ್ವನಿಯೋಜಿತ",
+ "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+ "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
+ "External": "ಹೊರಗಿನ",
+ "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
+ "Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
+ "Folders": "ಫೋಲ್ಡರ್ಗಳು",
+ "Forced": "ಬಲವಂತವಾಗಿ",
+ "Genres": "ಪ್ರಕಾರಗಳು",
+ "HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
+ "HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
+ "HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
+ "HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
+ "HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
+ "HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
+ "HeaderNextUp": "ಮುಂದೆ",
+ "HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
+ "MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+ "Channels": "ಮೂಲಗಳು",
+ "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
+ "HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
+ "HearingImpaired": "ಮೂಗ",
+ "ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
+ "MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+ "MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್ಲೋಡ್ಗೆ ಲಭ್ಯವಿದೆ.",
+ "NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+ "NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+ "NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್ಇನ್ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+ "NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ",
+ "NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+ "PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್ಇನ್ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+ "ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
+ "ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
+ "ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
+ "UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
+ "UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
+ "UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್ಲೈನ್ನಲ್ಲಿದೆ",
+ "UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
+ "UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+ "UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
+ "UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
+ "VersionNumber": "ಆವೃತ್ತಿ {0}",
+ "TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ",
+ "TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್",
+ "TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ",
+ "TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್ನೇಲ್ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.",
+ "TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
+ "TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.",
+ "TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್ಕೋಡ್ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡಿ",
+ "Shows": "ಧಾರವಾಹಿಗಳು",
+ "Songs": "ಹಾಡುಗಳು",
+ "StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
+ "UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
+ "UserDownloadingItemWithValues": "{0} ಡೌನ್ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
+ "SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
+ "Sync": "ಹೊಂದಿಕೆ",
+ "System": "ವ್ಯವಸ್ಥೆ",
+ "TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
+ "Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
+ "User": "ಬಳಕೆದಾರ",
+ "HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
+ "Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
+ "ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
+ "LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
+ "LabelRunningTimeValue": "ಅವಧಿ: {0}",
+ "Latest": "ಹೊಸದಾದ",
+ "MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
+ "Movies": "ಚಲನಚಿತ್ರಗಳು",
+ "Music": "ಸಂಗೀತ",
+ "MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು",
+ "NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ",
+ "NameSeasonNumber": "ಸೀಸನ್ {0}",
+ "NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ",
+ "NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ",
+ "NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+ "NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
+ "NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
+ "NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+ "NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+ "NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
+ "NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
+ "NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+ "Photos": "ಚಿತ್ರಗಳು",
+ "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
+ "Plugin": "ಪ್ಲಗಿನ್",
+ "PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "ProviderValue": "ಒದಗಿಸುವವರು: {0}",
+ "TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
+ "TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+ "TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
+ "TaskUpdatePlugins": "ಪ್ಲಗಿನ್ಗಳನ್ನು ನವೀಕರಿಸಿ",
+ "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
+ "TaskRefreshChannels": "ಚಾನಲ್ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+ "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index f7b24412a..83a000014 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -1,7 +1,7 @@
{
"ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
"NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
- "HeaderRecordingGroups": "Ierakstu Grupas",
+ "HeaderRecordingGroups": "Ierakstu grupas",
"UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
"SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
"NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
@@ -14,7 +14,7 @@
"Photos": "Attēli",
"NotificationOptionUserLockedOut": "Lietotājs bloķēts",
"LabelRunningTimeValue": "Garums: {0}",
- "Inherit": "Mantot",
+ "Inherit": "Pārmantot",
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
"VersionNumber": "Versija {0}",
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
@@ -28,7 +28,7 @@
"UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
"UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
"User": "Lietotājs",
- "TvShows": "TV Raidījumi",
+ "TvShows": "TV raidījumi",
"Sync": "Sinhronizācija",
"System": "Sistēma",
"StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.",
@@ -38,11 +38,11 @@
"PluginUninstalledWithName": "{0} tika noņemts",
"PluginInstalledWithName": "{0} tika uzstādīts",
"Plugin": "Paplašinājums",
- "Playlists": "Atskaņošanas Saraksti",
+ "Playlists": "Atskaņošanas saraksti",
"MixedContent": "Jaukts saturs",
- "HomeVideos": "Mājas Video",
+ "HomeVideos": "Mājas video",
"HeaderNextUp": "Nākamais",
- "ChapterNameValue": "Nodaļa {0}",
+ "ChapterNameValue": "{0}. nodaļa",
"Application": "Lietotne",
"NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts",
"NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
@@ -56,14 +56,14 @@
"NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts",
"NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams",
"NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.",
- "NameSeasonUnknown": "Nezināma Sezona",
- "NameSeasonNumber": "Sezona {0}",
+ "NameSeasonUnknown": "Nezināma sezona",
+ "NameSeasonNumber": "{0}. sezona",
"NameInstallFailed": "{0} instalācija neizdevās",
"MusicVideos": "Mūzikas video",
"Music": "Mūzika",
"Movies": "Filmas",
"MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota",
- "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} tika atjaunota",
"MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}",
"MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
"Latest": "Jaunākais",
@@ -71,57 +71,57 @@
"ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
"ItemAddedWithName": "{0} tika pievienots bibliotēkai",
"HeaderLiveTV": "Tiešraides TV",
- "HeaderContinueWatching": "Turpināt Skatīšanos",
- "HeaderAlbumArtists": "Albumu Izpildītāji",
+ "HeaderContinueWatching": "Turpināt skatīšanos",
+ "HeaderAlbumArtists": "Albumu izpildītāji",
"Genres": "Žanri",
"Folders": "Mapes",
- "Favorites": "Favorīti",
- "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}",
- "DeviceOnlineWithName": "{0} ir pievienojies",
- "DeviceOfflineWithName": "{0} ir atvienojies",
+ "Favorites": "Izlase",
+ "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
+ "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
+ "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
"Collections": "Kolekcijas",
"Channels": "Kanāli",
- "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
+ "CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}",
"Books": "Grāmatas",
"Artists": "Izpildītāji",
"Albums": "Albumi",
"ProviderValue": "Provider: {0}",
- "HeaderFavoriteSongs": "Dziesmu Favorīti",
- "HeaderFavoriteShows": "Raidījumu Favorīti",
- "HeaderFavoriteEpisodes": "Episožu Favorīti",
- "HeaderFavoriteArtists": "Izpildītāju Favorīti",
- "HeaderFavoriteAlbums": "Albumu Favorīti",
- "TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.",
- "TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
+ "HeaderFavoriteSongs": "Dziesmu izlase",
+ "HeaderFavoriteShows": "Raidījumu izlase",
+ "HeaderFavoriteEpisodes": "Sēriju izlase",
+ "HeaderFavoriteArtists": "Izpildītāju izlase",
+ "HeaderFavoriteAlbums": "Albumu izlase",
+ "TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.",
+ "TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
"TasksApplicationCategory": "Lietotne",
"TasksLibraryCategory": "Bibliotēka",
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
- "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus",
+ "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
- "TaskRefreshChannels": "Atjaunot Kanālus",
- "TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.",
- "TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
+ "TaskRefreshChannels": "Atjaunot kanālus",
+ "TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.",
+ "TaskCleanTranscode": "Iztīrīt transkodēšanas mapi",
"TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
- "TaskUpdatePlugins": "Atjaunot Paplašinājumus",
+ "TaskUpdatePlugins": "Atjaunot paplašinājumus",
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
- "TaskRefreshPeople": "Atjaunot Cilvēkus",
- "TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
- "TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
+ "TaskRefreshPeople": "Atjaunot cilvēkus",
+ "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
+ "TaskCleanLogs": "Iztīrīt logdatņu mapi",
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
- "TaskRefreshLibrary": "Skenēt Multivides Bibliotēku",
+ "TaskRefreshLibrary": "Skenēt multivides bibliotēku",
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
- "TaskCleanCache": "Iztīrīt Kešošanas Mapi",
- "TasksChannelsCategory": "Interneta Kanāli",
+ "TaskCleanCache": "Iztīrīt kešatmiņas mapi",
+ "TasksChannelsCategory": "Interneta kanāli",
"TasksMaintenanceCategory": "Apkope",
- "Forced": "Piespiests",
+ "Forced": "Piespiedu",
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
- "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
+ "TaskCleanActivityLog": "Notīrīt darbību žurnālu",
"Undefined": "Nenoteikts",
"Default": "Noklusējuma",
- "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
+ "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Šī uzdevuma palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
"TaskOptimizeDatabase": "Optimizēt datubāzi",
"External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem",
- "TaskKeyframeExtractor": "Atslēgkadru Ekstraktors",
+ "TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json
index 0620fbcdb..0b50fa529 100644
--- a/Emby.Server.Implementations/Localization/Core/ml.json
+++ b/Emby.Server.Implementations/Localization/Core/ml.json
@@ -121,5 +121,7 @@
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
"HearingImpaired": "കേൾവി തകരാറുകൾ",
- "External": "പുറമേയുള്ള"
+ "External": "പുറമേയുള്ള",
+ "TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
+ "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index b2293e4b6..a07222975 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -1,5 +1,5 @@
{
- "Albums": "Album-album",
+ "Albums": "Album",
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
"Application": "Aplikasi",
"Artists": "Artis-artis",
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 4eb00d289..ac7b92de6 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -1,9 +1,9 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}",
- "Application": "Toepassing",
+ "Application": "Applicatie",
"Artists": "Artiesten",
- "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
+ "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd",
"Books": "Boeken",
"CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
"Channels": "Kanalen",
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index 87800a2fe..26dc5ce82 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -24,5 +24,13 @@
"TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.",
"HeaderAlbumArtists": "Buccaneers o' the musical arts",
"HeaderFavoriteAlbums": "Beloved booty o' musical adventures",
- "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas"
+ "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas",
+ "Channels": "Channels",
+ "Forced": "Pressed",
+ "External": "Outboard",
+ "HeaderFavoriteEpisodes": "Treasured Tales",
+ "HeaderFavoriteShows": "Treasured Tales",
+ "ChapterNameValue": "Piece {0}",
+ "HeaderFavoriteSongs": "Treasured Chimes",
+ "HeaderNextUp": "Incoming"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 421513341..fa6c753b6 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -31,13 +31,13 @@
"ItemRemovedWithName": "{0} - изъято из медиатеки",
"LabelIpAddressValue": "IP-адрес: {0}",
"LabelRunningTimeValue": "Длительность: {0}",
- "Latest": "Новое",
+ "Latest": "Последние добавленные",
"MessageApplicationUpdated": "Jellyfin Server был обновлён",
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
"MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
"MixedContent": "Смешанное содержание",
- "Movies": "Кино",
+ "Movies": "Фильмы",
"Music": "Музыка",
"MusicVideos": "Муз. видео",
"NameInstallFailed": "Установка {0} неудачна",
@@ -77,7 +77,7 @@
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
"Sync": "Синхронизация",
"System": "Система",
- "TvShows": "ТВ",
+ "TvShows": "Телесериалы",
"User": "Пользователь",
"UserCreatedWithName": "Пользователь {0} был создан",
"UserDeletedWithName": "Пользователь {0} был удалён",
diff --git a/Emby.Server.Implementations/Localization/Core/si.json b/Emby.Server.Implementations/Localization/Core/si.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/si.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 858cc40dd..c231d76fe 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -124,5 +124,5 @@
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
"External": "Externé",
- "HearingImpaired": "Sluchovo Postihnutý"
+ "HearingImpaired": "Sluchovo postihnutí"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 4c23f71ef..1944e072c 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -11,7 +11,7 @@
"Collections": "Zbirke",
"DeviceOfflineWithName": "{0} je prekinil povezavo",
"DeviceOnlineWithName": "{0} je povezan",
- "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}",
+ "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
"Favorites": "Priljubljeno",
"Folders": "Mape",
"Genres": "Zvrsti",
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index dfce6bd25..770624a8d 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்",
"TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.",
"TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்",
- "External": "வெளி"
+ "External": "வெளி",
+ "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்"
}
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 1a4fef64e..3cdf743d5 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -121,5 +121,7 @@
"TaskOptimizeDatabase": "ปรับปรุงประสิทธิภาพฐานข้อมูล",
"TaskOptimizeDatabaseDescription": "ลดขนาดการจัดเก็บฐานข้อมูล ใช้งานคำสั่งนี้หลังจากสแกนไลบรารีหรือหลังจากการเปลี่ยนแปลงฐานข้อมูล อาจจะทำให้ระบบทำงานเร็วขึ้น",
"External": "ภายนอก",
- "HearingImpaired": "บกพร่องทางการได้ยิน"
+ "HearingImpaired": "บกพร่องทางการได้ยิน",
+ "TaskKeyframeExtractor": "ตัวแยกคีย์เฟรม",
+ "TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน"
}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 9a140f871..3ce928859 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -3,19 +3,19 @@
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
"Application": "Uygulama",
"Artists": "Sanatçılar",
- "AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı",
+ "AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı",
"Books": "Kitaplar",
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
"Channels": "Kanallar",
- "ChapterNameValue": "Bölüm {0}",
+ "ChapterNameValue": "{0}. Bölüm",
"Collections": "Koleksiyonlar",
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı",
- "FailedLoginAttemptWithUserName": "{0} adresinden giriş denemesi başarısız oldu",
+ "FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
"Favorites": "Favoriler",
"Folders": "Klasörler",
"Genres": "Türler",
- "HeaderAlbumArtists": "Albüm Sanatçıları",
+ "HeaderAlbumArtists": "Albüm sanatçıları",
"HeaderContinueWatching": "İzlemeye Devam Et",
"HeaderFavoriteAlbums": "Favori Albümler",
"HeaderFavoriteArtists": "Favori Sanatçılar",
@@ -25,7 +25,7 @@
"HeaderLiveTV": "Canlı TV",
"HeaderNextUp": "Gelecek Hafta",
"HeaderRecordingGroups": "Kayıt Grupları",
- "HomeVideos": "Ana sayfa videoları",
+ "HomeVideos": "Ana Sayfa Videoları",
"Inherit": "Devral",
"ItemAddedWithName": "{0} kütüphaneye eklendi",
"ItemRemovedWithName": "{0} kütüphaneden silindi",
@@ -34,14 +34,14 @@
"Latest": "En son",
"MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi",
"MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi",
- "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu ayar kısmı {0} güncellendi",
- "MessageServerConfigurationUpdated": "Sunucu ayarları güncellendi",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu yapılandırma bölümü {0} güncellendi",
+ "MessageServerConfigurationUpdated": "Sunucu yapılandırması güncellendi",
"MixedContent": "Karışık içerik",
"Movies": "Filmler",
"Music": "Müzik",
- "MusicVideos": "Müzik videoları",
+ "MusicVideos": "Müzik Videoları",
"NameInstallFailed": "{0} kurulumu başarısız",
- "NameSeasonNumber": "Sezon {0}",
+ "NameSeasonNumber": "{0}. Sezon",
"NameSeasonUnknown": "Bilinmeyen Sezon",
"NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.",
"NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut",
@@ -55,9 +55,9 @@
"NotificationOptionPluginInstalled": "Eklenti yüklendi",
"NotificationOptionPluginUninstalled": "Eklenti kaldırıldı",
"NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi",
- "NotificationOptionServerRestartRequired": "Sunucu yeniden başlatma gerekli",
+ "NotificationOptionServerRestartRequired": "Sunucunun yeniden başlatılması gerekiyor",
"NotificationOptionTaskFailed": "Zamanlanmış görev hatası",
- "NotificationOptionUserLockedOut": "Kullanıcı kitlendi",
+ "NotificationOptionUserLockedOut": "Kullanıcı kilitlendi",
"NotificationOptionVideoPlayback": "Video oynatma başladı",
"NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu",
"Photos": "Fotoğraflar",
@@ -74,36 +74,36 @@
"Songs": "Şarkılar",
"StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
- "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi",
+ "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} sağlayıcısından indirilemedi",
"Sync": "Eşzamanlama",
"System": "Sistem",
"TvShows": "Diziler",
"User": "Kullanıcı",
"UserCreatedWithName": "{0} kullanıcısı oluşturuldu",
- "UserDeletedWithName": "Kullanıcı {0} silindi",
- "UserDownloadingItemWithValues": "{0} indiriliyor {1}",
- "UserLockedOutWithName": "Kullanıcı {0} kitlendi",
- "UserOfflineFromDevice": "{0}, {1} ile bağlantısı kesildi",
- "UserOnlineFromDevice": "{0}, {1} çevrimiçi",
- "UserPasswordChangedWithName": "{0} kullanıcısı için şifre değiştirildi",
- "UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için güncellendi",
+ "UserDeletedWithName": "{0} kullanıcısı silindi",
+ "UserDownloadingItemWithValues": "{0} {1} medyasını indiriyor",
+ "UserLockedOutWithName": "{0} adlı kullanıcı kilitlendi",
+ "UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi",
+ "UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi",
+ "UserPasswordChangedWithName": "{0} kullanıcısının parolası değiştirildi",
+ "UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi",
"UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
"UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
"ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi",
"ValueSpecialEpisodeName": "Özel - {0}",
"VersionNumber": "Sürüm {0}",
- "TaskCleanCache": "Geçici dosya klasörünü temizle",
- "TasksChannelsCategory": "İnternet kanalları",
+ "TaskCleanCache": "Geçici Dosya Klasörünü Temizle",
+ "TasksChannelsCategory": "İnternet Kanalları",
"TasksApplicationCategory": "Uygulama",
"TasksLibraryCategory": "Kütüphane",
"TasksMaintenanceCategory": "Bakım",
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
- "TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.",
+ "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.",
"TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
"TaskRefreshChannels": "Kanalları Yenile",
- "TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.",
- "TaskCleanTranscode": "Dönüşüm Dizinini Temizle",
+ "TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.",
+ "TaskCleanTranscode": "Kod Dönüştürme Dizinini Temizle",
"TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
"TaskUpdatePlugins": "Eklentileri Güncelle",
"TaskRefreshPeople": "Kullanıcıları Yenile",
diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json
index b5f4b920f..aa056d449 100644
--- a/Emby.Server.Implementations/Localization/Core/zu.json
+++ b/Emby.Server.Implementations/Localization/Core/zu.json
@@ -25,5 +25,14 @@
"Channels": "Amashaneli",
"Books": "Izincwadi",
"Artists": "Abadlali",
- "Albums": "Ama-albhamu"
+ "Albums": "Ama-albhamu",
+ "CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
+ "HeaderFavoriteArtists": "Abasethi Abathandekayo",
+ "HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
+ "HeaderFavoriteShows": "Izisho Ezithandekayo",
+ "External": "Kwezifungo",
+ "FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
+ "HeaderContinueWatching": "Buyela Ukubona",
+ "HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
+ "HeaderAlbumArtists": "Abasethi wenkulumo"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 96f435399..16776b6bd 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -71,25 +71,28 @@ namespace Emby.Server.Implementations.Localization
string countryCode = resource.Substring(RatingsPath.Length, 2);
var dict = new Dictionary(StringComparer.OrdinalIgnoreCase);
- await using var stream = _assembly.GetManifestResourceStream(resource);
- using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ var stream = _assembly.GetManifestResourceStream(resource);
+ await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
{
- if (string.IsNullOrWhiteSpace(line))
+ using var reader = new StreamReader(stream!);
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- continue;
- }
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
- string[] parts = line.Split(',');
- if (parts.Length == 2
- && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- var name = parts[0];
- dict.Add(name, new ParentalRating(name, value));
- }
- else
- {
- _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
+ string[] parts = line.Split(',');
+ if (parts.Length == 2
+ && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ {
+ var name = parts[0];
+ dict.Add(name, new ParentalRating(name, value));
+ }
+ else
+ {
+ _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
+ }
}
}
diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv
index 4ab808ae9..688125917 100644
--- a/Emby.Server.Implementations/Localization/Ratings/au.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/au.csv
@@ -4,10 +4,14 @@ G,0
M,15
MA,15
MA15+,15
+MA 15+,15
PG,16
16+,16
R,18
R18+,18
-X18+,18
+R 18+,18
18+,18
+X18+,1000
+X 18+,1000
X,1000
+RC,1001
diff --git a/Emby.Server.Implementations/Localization/Ratings/de.csv b/Emby.Server.Implementations/Localization/Ratings/de.csv
index d633a5dab..f6181575e 100644
--- a/Emby.Server.Implementations/Localization/Ratings/de.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/de.csv
@@ -1,12 +1,17 @@
Educational,0
Infoprogramm,0
FSK-0,0
+FSK 0,0
0,0
FSK-6,6
+FSK 6,6
6,6
FSK-12,12
+FSK 12,12
12,12
FSK-16,16
+FSK 16,16
16,16
FSK-18,18
+FSK 18,18
18,18
diff --git a/Emby.Server.Implementations/Localization/Ratings/es.csv b/Emby.Server.Implementations/Localization/Ratings/es.csv
index 0bc1d3f7d..619e948d8 100644
--- a/Emby.Server.Implementations/Localization/Ratings/es.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/es.csv
@@ -3,6 +3,7 @@ A/fig,0
A/i,0
A/fig/i,0
APTA,0
+ERI,0
TP,0
0+,0
6+,6
diff --git a/Emby.Server.Implementations/Localization/Ratings/fr.csv b/Emby.Server.Implementations/Localization/Ratings/fr.csv
index 774a70589..139ea376b 100644
--- a/Emby.Server.Implementations/Localization/Ratings/fr.csv
+++ b/Emby.Server.Implementations/Localization/Ratings/fr.csv
@@ -1,5 +1,6 @@
Public Averti,0
Tous Publics,0
+TP,0
U,0
0+,0
6+,6
diff --git a/Emby.Server.Implementations/Localization/Ratings/sk.csv b/Emby.Server.Implementations/Localization/Ratings/sk.csv
new file mode 100644
index 000000000..dbafd8efa
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Ratings/sk.csv
@@ -0,0 +1,6 @@
+NR,0
+U,0
+7,7
+12,12
+15,15
+18,18
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 7732e32d0..896f47923 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder
{
var deadImages = images
.Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
- .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var image in deadImages)
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 303875df5..2bcd5eab2 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -1,69 +1,76 @@
-#pragma warning disable CS1591
-
using System;
+using System.Linq;
using System.Net;
+using System.Net.NetworkInformation;
using System.Net.Sockets;
using MediaBrowser.Model.Net;
namespace Emby.Server.Implementations.Net
{
+ ///
+ /// Factory class to create different kinds of sockets.
+ ///
public class SocketFactory : ISocketFactory
{
///
- public ISocket CreateUdpBroadcastSocket(int localPort)
+ public Socket CreateUdpBroadcastSocket(int localPort)
{
if (localPort < 0)
{
throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
}
- var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
try
{
- retVal.EnableBroadcast = true;
- retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
+ socket.EnableBroadcast = true;
+ socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+ socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, 1);
+ socket.Bind(new IPEndPoint(IPAddress.Any, localPort));
- return new UdpSocket(retVal, localPort, IPAddress.Any);
+ return socket;
}
catch
{
- retVal?.Dispose();
+ socket.Dispose();
throw;
}
}
///
- public ISocket CreateSsdpUdpSocket(IPAddress localIp, int localPort)
+ public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
{
+ var interfaceAddress = bindInterface.Address;
+ ArgumentNullException.ThrowIfNull(interfaceAddress);
+
if (localPort < 0)
{
throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
}
- var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
try
{
- retVal.EnableBroadcast = true;
- retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4);
+ socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+ socket.Bind(new IPEndPoint(interfaceAddress, localPort));
- retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIp));
- return new UdpSocket(retVal, localPort, localIp);
+ return socket;
}
catch
{
- retVal?.Dispose();
+ socket.Dispose();
throw;
}
}
///
- public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, int multicastTimeToLive, int localPort)
+ public Socket CreateUdpMulticastSocket(IPAddress multicastAddress, IPData bindInterface, int multicastTimeToLive, int localPort)
{
- ArgumentNullException.ThrowIfNull(ipAddress);
+ var bindIPAddress = bindInterface.Address;
+ ArgumentNullException.ThrowIfNull(multicastAddress);
+ ArgumentNullException.ThrowIfNull(bindIPAddress);
if (multicastTimeToLive <= 0)
{
@@ -75,36 +82,35 @@ namespace Emby.Server.Implementations.Net
throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort));
}
- var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
-
- retVal.ExclusiveAddressUse = false;
+ var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
try
{
- // seeing occasional exceptions thrown on qnap
- // System.Net.Sockets.SocketException (0x80004005): Protocol not available
- retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- }
- catch (SocketException)
- {
- }
+ socket.MulticastLoopback = false;
+ socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+ socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
+ socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
- try
- {
- retVal.EnableBroadcast = true;
- // retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true);
- retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
+ socket.Bind(new IPEndPoint(multicastAddress, localPort));
+ }
+ else
+ {
+ // Only create socket if interface supports multicast
+ var interfaceIndex = bindInterface.Index;
+ var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
- var localIp = IPAddress.Any;
+ socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
+ socket.Bind(new IPEndPoint(bindIPAddress, localPort));
+ }
- retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(ipAddress, localIp));
- retVal.MulticastLoopback = true;
-
- return new UdpSocket(retVal, localPort, localIp);
+ return socket;
}
catch
{
- retVal?.Dispose();
+ socket.Dispose();
throw;
}
diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs
deleted file mode 100644
index 577b79283..000000000
--- a/Emby.Server.Implementations/Net/UdpSocket.cs
+++ /dev/null
@@ -1,267 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Net;
-using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-
-namespace Emby.Server.Implementations.Net
-{
- // THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS
- // Be careful to check any changes compile and work for all platform projects it is shared in.
-
- public sealed class UdpSocket : ISocket, IDisposable
- {
- private readonly int _localPort;
-
- private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
- {
- SocketFlags = SocketFlags.None
- };
-
- private readonly SocketAsyncEventArgs _sendSocketAsyncEventArgs = new SocketAsyncEventArgs()
- {
- SocketFlags = SocketFlags.None
- };
-
- private Socket _socket;
- private bool _disposed = false;
- private TaskCompletionSource _currentReceiveTaskCompletionSource;
- private TaskCompletionSource _currentSendTaskCompletionSource;
-
- public UdpSocket(Socket socket, int localPort, IPAddress ip)
- {
- ArgumentNullException.ThrowIfNull(socket);
-
- _socket = socket;
- _localPort = localPort;
- LocalIPAddress = ip;
-
- _socket.Bind(new IPEndPoint(ip, _localPort));
-
- InitReceiveSocketAsyncEventArgs();
- }
-
- public UdpSocket(Socket socket, IPEndPoint endPoint)
- {
- ArgumentNullException.ThrowIfNull(socket);
-
- _socket = socket;
- _socket.Connect(endPoint);
-
- InitReceiveSocketAsyncEventArgs();
- }
-
- public Socket Socket => _socket;
-
- public IPAddress LocalIPAddress { get; }
-
- private void InitReceiveSocketAsyncEventArgs()
- {
- var receiveBuffer = new byte[8192];
- _receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
- _receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
-
- var sendBuffer = new byte[8192];
- _sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
- _sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted;
- }
-
- private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
- {
- var tcs = _currentReceiveTaskCompletionSource;
- if (tcs is not null)
- {
- _currentReceiveTaskCompletionSource = null;
-
- if (e.SocketError == SocketError.Success)
- {
- tcs.TrySetResult(new SocketReceiveResult
- {
- Buffer = e.Buffer,
- ReceivedBytes = e.BytesTransferred,
- RemoteEndPoint = e.RemoteEndPoint as IPEndPoint,
- LocalIPAddress = LocalIPAddress
- });
- }
- else
- {
- tcs.TrySetException(new SocketException((int)e.SocketError));
- }
- }
- }
-
- private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
- {
- var tcs = _currentSendTaskCompletionSource;
- if (tcs is not null)
- {
- _currentSendTaskCompletionSource = null;
-
- if (e.SocketError == SocketError.Success)
- {
- tcs.TrySetResult(e.BytesTransferred);
- }
- else
- {
- tcs.TrySetException(new SocketException((int)e.SocketError));
- }
- }
- }
-
- public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
- {
- ThrowIfDisposed();
-
- EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0);
-
- return _socket.BeginReceiveFrom(buffer, offset, count, SocketFlags.None, ref receivedFromEndPoint, callback, buffer);
- }
-
- public int Receive(byte[] buffer, int offset, int count)
- {
- ThrowIfDisposed();
-
- return _socket.Receive(buffer, 0, buffer.Length, SocketFlags.None);
- }
-
- public SocketReceiveResult EndReceive(IAsyncResult result)
- {
- ThrowIfDisposed();
-
- var sender = new IPEndPoint(IPAddress.Any, 0);
- var remoteEndPoint = (EndPoint)sender;
-
- var receivedBytes = _socket.EndReceiveFrom(result, ref remoteEndPoint);
-
- var buffer = (byte[])result.AsyncState;
-
- return new SocketReceiveResult
- {
- ReceivedBytes = receivedBytes,
- RemoteEndPoint = (IPEndPoint)remoteEndPoint,
- Buffer = buffer,
- LocalIPAddress = LocalIPAddress
- };
- }
-
- public Task ReceiveAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
- {
- ThrowIfDisposed();
-
- var taskCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- bool isResultSet = false;
-
- Action callback = callbackResult =>
- {
- try
- {
- if (!isResultSet)
- {
- isResultSet = true;
- taskCompletion.TrySetResult(EndReceive(callbackResult));
- }
- }
- catch (Exception ex)
- {
- taskCompletion.TrySetException(ex);
- }
- };
-
- var result = BeginReceive(buffer, offset, count, new AsyncCallback(callback));
-
- if (result.CompletedSynchronously)
- {
- callback(result);
- return taskCompletion.Task;
- }
-
- cancellationToken.Register(() => taskCompletion.TrySetCanceled());
-
- return taskCompletion.Task;
- }
-
- public Task SendToAsync(byte[] buffer, int offset, int bytes, IPEndPoint endPoint, CancellationToken cancellationToken)
- {
- ThrowIfDisposed();
-
- var taskCompletion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- bool isResultSet = false;
-
- Action callback = callbackResult =>
- {
- try
- {
- if (!isResultSet)
- {
- isResultSet = true;
- taskCompletion.TrySetResult(EndSendTo(callbackResult));
- }
- }
- catch (Exception ex)
- {
- taskCompletion.TrySetException(ex);
- }
- };
-
- var result = BeginSendTo(buffer, offset, bytes, endPoint, new AsyncCallback(callback), null);
-
- if (result.CompletedSynchronously)
- {
- callback(result);
- return taskCompletion.Task;
- }
-
- cancellationToken.Register(() => taskCompletion.TrySetCanceled());
-
- return taskCompletion.Task;
- }
-
- public IAsyncResult BeginSendTo(byte[] buffer, int offset, int size, IPEndPoint endPoint, AsyncCallback callback, object state)
- {
- ThrowIfDisposed();
-
- return _socket.BeginSendTo(buffer, offset, size, SocketFlags.None, endPoint, callback, state);
- }
-
- public int EndSendTo(IAsyncResult result)
- {
- ThrowIfDisposed();
-
- return _socket.EndSendTo(result);
- }
-
- private void ThrowIfDisposed()
- {
- if (_disposed)
- {
- throw new ObjectDisposedException(nameof(UdpSocket));
- }
- }
-
- ///
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- _socket?.Dispose();
- _receiveSocketAsyncEventArgs.Dispose();
- _sendSocketAsyncEventArgs.Dispose();
- _currentReceiveTaskCompletionSource?.TrySetCanceled();
- _currentSendTaskCompletionSource?.TrySetCanceled();
-
- _socket = null;
- _currentReceiveTaskCompletionSource = null;
- _currentSendTaskCompletionSource = null;
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 702f8d45b..649c49924 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists
// this is probably best done as a metadata provider
// saving a file over itself will require some work to prevent this from happening when not needed
var playlistPath = item.Path;
- var extension = Path.GetExtension(playlistPath);
+ var extension = Path.GetExtension(playlistPath.AsSpan());
- if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
+ if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase))
{
var playlist = new WplPlaylist();
foreach (var child in item.GetLinkedChildren())
@@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new WplContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
{
var playlist = new ZplPlaylist();
foreach (var child in item.GetLinkedChildren())
@@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new ZplContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
{
var playlist = new M3uPlaylist
{
@@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new M3uContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
var playlist = new M3uPlaylist();
playlist.IsExtended = true;
@@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new M3uContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
{
var playlist = new PlsPlaylist();
foreach (var child in item.GetLinkedChildren())
@@ -518,6 +514,11 @@ namespace Emby.Server.Implementations.Playlists
return relativePath;
}
+ public Folder GetPlaylistsFolder()
+ {
+ return GetPlaylistsFolder(Guid.Empty);
+ }
+
public Folder GetPlaylistsFolder(Guid userId)
{
const string TypeName = "PlaylistsFolder";
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 48584ae0c..20793ee39 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -11,7 +10,6 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
-using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
@@ -30,7 +28,7 @@ namespace Emby.Server.Implementations.Plugins
///
/// Defines the .
///
- public class PluginManager : IPluginManager
+ public sealed class PluginManager : IPluginManager, IDisposable
{
private const string MetafileName = "meta.json";
@@ -191,15 +189,6 @@ namespace Emby.Server.Implementations.Plugins
}
}
- ///
- public void UnloadAssemblies()
- {
- foreach (var assemblyLoadContext in _assemblyLoadContexts)
- {
- assemblyLoadContext.Unload();
- }
- }
-
///
/// Creates all the plugin instances.
///
@@ -397,11 +386,11 @@ namespace Emby.Server.Implementations.Plugins
var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]);
- await using var fileStream = AsyncFile.OpenWrite(imagePath);
-
+ var fileStream = AsyncFile.OpenWrite(imagePath);
+ Stream? downloadStream = null;
try
{
- await using var downloadStream = await HttpClientFactory
+ downloadStream = await HttpClientFactory
.CreateClient(NamedClient.Default)
.GetStreamAsync(url)
.ConfigureAwait(false);
@@ -413,6 +402,14 @@ namespace Emby.Server.Implementations.Plugins
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
imagePath = string.Empty;
}
+ finally
+ {
+ await fileStream.DisposeAsync().ConfigureAwait(false);
+ if (downloadStream is not null)
+ {
+ await downloadStream.DisposeAsync().ConfigureAwait(false);
+ }
+ }
}
var manifest = new PluginManifest
@@ -432,7 +429,7 @@ namespace Emby.Server.Implementations.Plugins
ImagePath = imagePath
};
- if (!await ReconcileManifest(manifest, path))
+ if (!await ReconcileManifest(manifest, path).ConfigureAwait(false))
{
// An error occurred during reconciliation and saving could be undesirable.
return false;
@@ -441,6 +438,15 @@ namespace Emby.Server.Implementations.Plugins
return SaveManifest(manifest, path);
}
+ ///
+ public void Dispose()
+ {
+ foreach (var assemblyLoadContext in _assemblyLoadContexts)
+ {
+ assemblyLoadContext.Unload();
+ }
+ }
+
///
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
/// If no file is found, no reconciliation occurs.
@@ -460,7 +466,7 @@ namespace Emby.Server.Implementations.Plugins
}
using var metaStream = File.OpenRead(metafile);
- var localManifest = await JsonSerializer.DeserializeAsync(metaStream, _jsonOptions);
+ var localManifest = await JsonSerializer.DeserializeAsync(metaStream, _jsonOptions).ConfigureAwait(false);
localManifest ??= new PluginManifest();
if (!Equals(localManifest.Id, manifest.Id))
@@ -677,7 +683,7 @@ namespace Emby.Server.Implementations.Plugins
}
catch (JsonException ex)
{
- _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data!));
+ _logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data));
}
if (manifest is not null)
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 6ad6c4cbd..5d15c3a21 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
try
{
- previouslyFailedImages = File.ReadAllText(failHistoryPath)
+ previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
.Split('|', StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
@@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
string text = string.Join('|', previouslyFailedImages);
- File.WriteAllText(failHistoryPath, text);
+ await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
}
numComplete++;
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
new file mode 100644
index 000000000..acd4bf905
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+///
+/// Deletes path references from collections and playlists that no longer exists.
+///
+public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
+{
+ private readonly ILocalizationManager _localization;
+ private readonly ICollectionManager _collectionManager;
+ private readonly IPlaylistManager _playlistManager;
+ private readonly ILogger _logger;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// The logger.
+ /// The provider manager.
+ /// The filesystem.
+ public CleanupCollectionAndPlaylistPathsTask(
+ ILocalizationManager localization,
+ ICollectionManager collectionManager,
+ IPlaylistManager playlistManager,
+ ILogger logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem)
+ {
+ _localization = localization;
+ _collectionManager = collectionManager;
+ _playlistManager = playlistManager;
+ _logger = logger;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ }
+
+ ///
+ public string Name => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylists");
+
+ ///
+ public string Key => "CleanCollectionsAndPlaylists";
+
+ ///
+ public string Description => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylistsDescription");
+
+ ///
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ ///
+ public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
+ {
+ var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false);
+ if (collectionsFolder is null)
+ {
+ _logger.LogDebug("There is no collections folder to be found");
+ }
+ else
+ {
+ var collections = collectionsFolder.Children.OfType().ToArray();
+ _logger.LogDebug("Found {CollectionLength} boxsets", collections.Length);
+
+ for (var index = 0; index < collections.Length; index++)
+ {
+ var collection = collections[index];
+ _logger.LogDebug("Checking boxset {CollectionName}", collection.Name);
+
+ CleanupLinkedChildren(collection, cancellationToken);
+ progress.Report(50D / collections.Length * (index + 1));
+ }
+ }
+
+ var playlistsFolder = _playlistManager.GetPlaylistsFolder();
+ if (playlistsFolder is null)
+ {
+ _logger.LogDebug("There is no playlists folder to be found");
+ return;
+ }
+
+ var playlists = playlistsFolder.Children.OfType().ToArray();
+ _logger.LogDebug("Found {PlaylistLength} playlists", playlists.Length);
+
+ for (var index = 0; index < playlists.Length; index++)
+ {
+ var playlist = playlists[index];
+ _logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name);
+
+ CleanupLinkedChildren(playlist, cancellationToken);
+ progress.Report(50D / playlists.Length * (index + 1));
+ }
+ }
+
+ private void CleanupLinkedChildren(T folder, CancellationToken cancellationToken)
+ where T : Folder
+ {
+ List? itemsToRemove = null;
+ foreach (var linkedChild in folder.LinkedChildren)
+ {
+ if (!File.Exists(folder.Path))
+ {
+ _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, linkedChild.Path);
+ (itemsToRemove ??= new List()).Add(linkedChild);
+ }
+ }
+
+ if (itemsToRemove is not null)
+ {
+ _logger.LogDebug("Updating {FolderName}", folder.Name);
+ folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
+ folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
+
+ _providerManager.QueueRefresh(
+ folder.Id,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ForceSave = true
+ },
+ RefreshPriority.High);
+ }
+ }
+
+ ///
+ public IEnumerable GetDefaultTriggers()
+ {
+ return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } };
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
deleted file mode 100644
index f78fc6f97..000000000
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Collections;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
-
-///
-/// Deletes Path references from collections that no longer exists.
-///
-public class CleanupCollectionPathsTask : IScheduledTask
-{
- private readonly ILocalizationManager _localization;
- private readonly ICollectionManager _collectionManager;
- private readonly ILogger _logger;
- private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of the interface.
- /// Instance of the interface.
- /// The logger.
- /// The provider manager.
- /// The filesystem.
- public CleanupCollectionPathsTask(
- ILocalizationManager localization,
- ICollectionManager collectionManager,
- ILogger logger,
- IProviderManager providerManager,
- IFileSystem fileSystem)
- {
- _localization = localization;
- _collectionManager = collectionManager;
- _logger = logger;
- _providerManager = providerManager;
- _fileSystem = fileSystem;
- }
-
- ///
- public string Name => _localization.GetLocalizedString("TaskCleanCollections");
-
- ///
- public string Key => "CleanCollections";
-
- ///
- public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription");
-
- ///
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
-
- ///
- public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken)
- {
- var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false);
- if (collectionsFolder is null)
- {
- _logger.LogDebug("There is no collection folder to be found");
- return;
- }
-
- var collections = collectionsFolder.Children.OfType().ToArray();
- _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length);
-
- var itemsToRemove = new List();
- for (var index = 0; index < collections.Length; index++)
- {
- var collection = collections[index];
- _logger.LogDebug("Check Boxset {CollectionName}", collection.Name);
-
- foreach (var collectionLinkedChild in collection.LinkedChildren)
- {
- if (!File.Exists(collectionLinkedChild.Path))
- {
- _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}", collection.Name, collectionLinkedChild.Path);
- itemsToRemove.Add(collectionLinkedChild);
- }
- }
-
- if (itemsToRemove.Count != 0)
- {
- _logger.LogDebug("Update Boxset {CollectionName}", collection.Name);
- collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray();
- await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken)
- .ConfigureAwait(false);
-
- _providerManager.QueueRefresh(
- collection.Id,
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- ForceSave = true
- },
- RefreshPriority.High);
-
- itemsToRemove.Clear();
- }
-
- progress.Report(100D / collections.Length * (index + 1));
- }
- }
-
- ///
- public IEnumerable GetDefaultTriggers()
- {
- return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } };
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 5f6dc93fb..e935f7e5e 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -24,6 +24,7 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Authentication;
using MediaBrowser.Controller.Events.Session;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
@@ -35,6 +36,7 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -43,7 +45,7 @@ namespace Emby.Server.Implementations.Session
///
/// Class SessionManager.
///
- public class SessionManager : ISessionManager, IDisposable
+ public sealed class SessionManager : ISessionManager, IAsyncDisposable
{
private readonly IUserDataManager _userDataManager;
private readonly ILogger _logger;
@@ -56,11 +58,9 @@ namespace Emby.Server.Implementations.Session
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerApplicationHost _appHost;
private readonly IDeviceManager _deviceManager;
-
- ///
- /// The active connections.
- ///
- private readonly ConcurrentDictionary _activeConnections = new(StringComparer.OrdinalIgnoreCase);
+ private readonly CancellationTokenRegistration _shutdownCallback;
+ private readonly ConcurrentDictionary _activeConnections
+ = new(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer;
@@ -78,7 +78,8 @@ namespace Emby.Server.Implementations.Session
IImageProcessor imageProcessor,
IServerApplicationHost appHost,
IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager)
+ IMediaSourceManager mediaSourceManager,
+ IHostApplicationLifetime hostApplicationLifetime)
{
_logger = logger;
_eventManager = eventManager;
@@ -91,6 +92,7 @@ namespace Emby.Server.Implementations.Session
_appHost = appHost;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
+ _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
}
@@ -150,36 +152,6 @@ namespace Emby.Server.Implementations.Session
}
}
- ///
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- ///
- /// Releases unmanaged and optionally managed resources.
- ///
- /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- _idleTimer?.Dispose();
- }
-
- _idleTimer = null;
-
- _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
-
- _disposed = true;
- }
-
private void CheckDisposed()
{
if (_disposed)
@@ -979,28 +951,28 @@ namespace Emby.Server.Implementations.Session
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
{
- bool playedToCompletion = false;
-
- if (!playbackFailed)
+ if (playbackFailed)
{
- var data = _userDataManager.GetUserData(user, item);
-
- if (positionTicks.HasValue)
- {
- playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
- }
- else
- {
- // If the client isn't able to report this, then we'll just have to make an assumption
- data.PlayCount++;
- data.Played = item.SupportsPlayedStatus;
- data.PlaybackPositionTicks = 0;
- playedToCompletion = true;
- }
-
- _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
+ return false;
}
+ var data = _userDataManager.GetUserData(user, item);
+ bool playedToCompletion;
+ if (positionTicks.HasValue)
+ {
+ playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
+ }
+ else
+ {
+ // If the client isn't able to report this, then we'll just have to make an assumption
+ data.PlayCount++;
+ data.Played = item.SupportsPlayedStatus;
+ data.PlaybackPositionTicks = 0;
+ playedToCompletion = true;
+ }
+
+ _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
+
return playedToCompletion;
}
@@ -1329,32 +1301,6 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
}
- ///
- /// Sends the server shutdown notification.
- ///
- /// The cancellation token.
- /// Task.
- public Task SendServerShutdownNotification(CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
- }
-
- ///
- /// Sends the server restart notification.
- ///
- /// The cancellation token.
- /// Task.
- public Task SendServerRestartNotification(CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- _logger.LogDebug("Beginning SendServerRestartNotification");
-
- return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
- }
-
///
/// Adds the additional user.
///
@@ -1462,7 +1408,7 @@ namespace Emby.Server.Implementations.Session
if (user is null)
{
- await _eventManager.PublishAsync(new GenericEventArgs(request)).ConfigureAwait(false);
+ await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
throw new AuthenticationException("Invalid username or password entered.");
}
@@ -1498,7 +1444,7 @@ namespace Emby.Server.Implementations.Session
ServerId = _appHost.SystemId
};
- await _eventManager.PublishAsync(new GenericEventArgs(returnResult)).ConfigureAwait(false);
+ await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
return returnResult;
}
@@ -1508,35 +1454,20 @@ namespace Emby.Server.Implementations.Session
new DeviceQuery
{
DeviceId = deviceId,
- UserId = user.Id,
- Limit = 1
- }).ConfigureAwait(false)).Items.FirstOrDefault();
-
- var allExistingForDevice = (await _deviceManager.GetDevices(
- new DeviceQuery
- {
- DeviceId = deviceId
+ UserId = user.Id
}).ConfigureAwait(false)).Items;
- foreach (var auth in allExistingForDevice)
+ foreach (var auth in existing)
{
- if (existing is null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
+ try
{
- try
- {
- await Logout(auth).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error while logging out.");
- }
+ // Logout any existing sessions for the user on this device
+ await Logout(auth).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while logging out existing session.");
}
- }
-
- if (existing is not null)
- {
- _logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken);
- return existing.AccessToken;
}
_logger.LogInformation("Creating new access token for user {0}", user.Id);
@@ -1847,5 +1778,53 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ foreach (var session in _activeConnections.Values)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
+
+ if (_idleTimer is not null)
+ {
+ await _idleTimer.DisposeAsync().ConfigureAwait(false);
+ _idleTimer = null;
+ }
+
+ await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
+
+ _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
+ _disposed = true;
+ }
+
+ private async void OnApplicationStopping()
+ {
+ _logger.LogInformation("Sending shutdown notifications");
+ try
+ {
+ var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.ServerShuttingDown;
+
+ await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error sending server shutdown notifications");
+ }
+
+ // Close open websockets to allow Kestrel to shut down cleanly
+ foreach (var session in _activeConnections.Values)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
+
+ _activeConnections.Clear();
+ }
}
}
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 4e427b1a4..b3c93a904 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -6,9 +6,8 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -308,11 +307,7 @@ namespace Emby.Server.Implementations.Session
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
{
return webSocket.SendAsync(
- new WebSocketMessage
- {
- MessageType = SessionMessageType.ForceKeepAlive,
- Data = WebSocketLostTimeout
- },
+ new ForceKeepAliveMessage(WebSocketLostTimeout),
CancellationToken.None);
}
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index cdc736950..cf8e0fb00 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -7,8 +7,8 @@ using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Session
}
return socket.SendAsync(
- new WebSocketMessage
+ new OutboundWebSocketMessage
{
Data = data,
MessageType = name,
diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs
new file mode 100644
index 000000000..2c477218f
--- /dev/null
+++ b/Emby.Server.Implementations/SystemManager.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Hosting;
+
+namespace Emby.Server.Implementations;
+
+///
+public class SystemManager : ISystemManager
+{
+ private readonly IHostApplicationLifetime _applicationLifetime;
+ private readonly IServerApplicationHost _applicationHost;
+ private readonly IServerApplicationPaths _applicationPaths;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IStartupOptions _startupOptions;
+ private readonly IInstallationManager _installationManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of .
+ /// Instance of .
+ /// Instance of .
+ /// Instance of .
+ /// Instance of .
+ /// Instance of .
+ public SystemManager(
+ IHostApplicationLifetime applicationLifetime,
+ IServerApplicationHost applicationHost,
+ IServerApplicationPaths applicationPaths,
+ IServerConfigurationManager configurationManager,
+ IStartupOptions startupOptions,
+ IInstallationManager installationManager)
+ {
+ _applicationLifetime = applicationLifetime;
+ _applicationHost = applicationHost;
+ _applicationPaths = applicationPaths;
+ _configurationManager = configurationManager;
+ _startupOptions = startupOptions;
+ _installationManager = installationManager;
+ }
+
+ ///
+ public SystemInfo GetSystemInfo(HttpRequest request)
+ {
+ return new SystemInfo
+ {
+ HasPendingRestart = _applicationHost.HasPendingRestart,
+ IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
+ Version = _applicationHost.ApplicationVersionString,
+ WebSocketPortNumber = _applicationHost.HttpPort,
+ CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
+ Id = _applicationHost.SystemId,
+ ProgramDataPath = _applicationPaths.ProgramDataPath,
+ WebPath = _applicationPaths.WebPath,
+ LogPath = _applicationPaths.LogDirectoryPath,
+ ItemsByNamePath = _applicationPaths.InternalMetadataPath,
+ InternalMetadataPath = _applicationPaths.InternalMetadataPath,
+ CachePath = _applicationPaths.CachePath,
+ TranscodingTempPath = _configurationManager.GetTranscodePath(),
+ ServerName = _applicationHost.FriendlyName,
+ LocalAddress = _applicationHost.GetSmartApiUrl(request),
+ SupportsLibraryMonitor = true,
+ PackageName = _startupOptions.PackageName,
+ CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications
+ };
+ }
+
+ ///
+ public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
+ {
+ return new PublicSystemInfo
+ {
+ Version = _applicationHost.ApplicationVersionString,
+ ProductName = _applicationHost.Name,
+ Id = _applicationHost.SystemId,
+ ServerName = _applicationHost.FriendlyName,
+ LocalAddress = _applicationHost.GetSmartApiUrl(request),
+ StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted
+ };
+ }
+
+ ///
+ public void Restart() => ShutdownInternal(true);
+
+ ///
+ public void Shutdown() => ShutdownInternal(false);
+
+ private void ShutdownInternal(bool restart)
+ {
+ Task.Run(async () =>
+ {
+ await Task.Delay(100).ConfigureAwait(false);
+ _applicationHost.ShouldRestart = restart;
+ _applicationLifetime.StopApplication();
+ });
+ }
+}
diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs
index f0e173f0b..ef890aeb4 100644
--- a/Emby.Server.Implementations/TV/TVSeriesManager.cs
+++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs
@@ -135,13 +135,13 @@ namespace Emby.Server.Implementations.TV
private IEnumerable GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList seriesKeys, DtoOptions dtoOptions)
{
- var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false));
+ var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, request.EnableResumable, false));
if (request.EnableRewatching)
{
- allNextUp = allNextUp.Concat(
- seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true)))
- .OrderByDescending(i => i.LastWatchedDate);
+ allNextUp = allNextUp
+ .Concat(seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false, true)))
+ .OrderByDescending(i => i.LastWatchedDate);
}
// If viewing all next up for all series, remove first episodes
@@ -183,7 +183,7 @@ namespace Emby.Server.Implementations.TV
/// Gets the next up.
///
/// Task{Episode}.
- private (DateTime LastWatchedDate, Func GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
+ private (DateTime LastWatchedDate, Func GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool includeResumable, bool includePlayed)
{
var lastQuery = new InternalItemsQuery(user)
{
@@ -200,8 +200,8 @@ namespace Emby.Server.Implementations.TV
}
};
- // If rewatching is enabled, sort first by date played and then by season and episode numbers
- lastQuery.OrderBy = rewatching
+ // If including played results, sort first by date played and then by season and episode numbers
+ lastQuery.OrderBy = includePlayed
? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
: new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
@@ -216,7 +216,7 @@ namespace Emby.Server.Implementations.TV
IncludeItemTypes = new[] { BaseItemKind.Episode },
OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
Limit = 1,
- IsPlayed = rewatching,
+ IsPlayed = includePlayed,
IsVirtualItem = false,
ParentIndexNumberNotEquals = 0,
DtoOptions = dtoOptions
@@ -240,7 +240,7 @@ namespace Emby.Server.Implementations.TV
SeriesPresentationUniqueKey = seriesKey,
ParentIndexNumber = 0,
IncludeItemTypes = new[] { BaseItemKind.Episode },
- IsPlayed = rewatching,
+ IsPlayed = includePlayed,
IsVirtualItem = false,
DtoOptions = dtoOptions
})
@@ -269,7 +269,7 @@ namespace Emby.Server.Implementations.TV
nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
}
- if (nextEpisode is not null)
+ if (nextEpisode is not null && !includeResumable)
{
var userData = _userDataManager.GetUserData(user, nextEpisode);
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 937e792f5..2d806c146 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -27,9 +27,9 @@ namespace Emby.Server.Implementations.Udp
private readonly byte[] _receiveBuffer = new byte[8192];
- private Socket _udpSocket;
- private IPEndPoint _endpoint;
- private bool _disposed = false;
+ private readonly Socket _udpSocket;
+ private readonly IPEndPoint _endpoint;
+ private bool _disposed;
///
/// Initializes a new instance of the class.
@@ -37,20 +37,25 @@ namespace Emby.Server.Implementations.Udp
/// The logger.
/// The application host.
/// The configuration manager.
+ /// The bind address.
/// The port.
public UdpServer(
ILogger logger,
IServerApplicationHost appHost,
IConfiguration configuration,
+ IPAddress bindAddress,
int port)
{
_logger = logger;
_appHost = appHost;
_config = configuration;
- _endpoint = new IPEndPoint(IPAddress.Any, port);
+ _endpoint = new IPEndPoint(bindAddress, port);
- _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
+ {
+ MulticastLoopback = false,
+ };
_udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
@@ -72,6 +77,7 @@ namespace Emby.Server.Implementations.Udp
try
{
+ _logger.LogDebug("Sending AutoDiscovery response");
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
}
catch (SocketException ex)
@@ -97,7 +103,8 @@ namespace Emby.Server.Implementations.Udp
{
try
{
- var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false);
+ var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
+ var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
{
@@ -110,7 +117,7 @@ namespace Emby.Server.Implementations.Udp
}
catch (OperationCanceledException)
{
- // Don't throw
+ _logger.LogDebug("Broadcast socket operation cancelled");
}
}
}
@@ -123,9 +130,8 @@ namespace Emby.Server.Implementations.Udp
return;
}
- _udpSocket?.Dispose();
-
- GC.SuppressFinalize(this);
+ _udpSocket.Dispose();
+ _disposed = true;
}
}
}
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 6c198b6f9..c717744b1 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
{
- var extension = Path.GetExtension(package.SourceUrl);
- if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
+ if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return;
@@ -521,10 +520,9 @@ namespace Emby.Server.Implementations.Updates
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
- using var md5 = MD5.Create();
cancellationToken.ThrowIfCancellationRequested();
- var hash = Convert.ToHexString(md5.ComputeHash(stream));
+ var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false));
if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
@@ -557,7 +555,7 @@ namespace Emby.Server.Implementations.Updates
reader.ExtractToDirectory(targetDir, true);
// Ensure we create one or populate existing ones with missing data.
- await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status);
+ await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
_pluginManager.ImportPluginFrom(targetDir);
}
diff --git a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
index 741b88ea9..3c1401ded 100644
--- a/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
+++ b/Jellyfin.Api/Auth/AnonymousLanAccessPolicy/AnonymousLanAccessHandler.cs
@@ -30,7 +30,7 @@ namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy
///
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement)
{
- var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
+ var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIP();
// Loopback will be on LAN, so we can accept null.
if (ip is null || _networkManager.IsInLocalNetwork(ip))
diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
index de271ab64..cf3cb6905 100644
--- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
+++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
@@ -54,7 +54,7 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
}
var isInLocalNetwork = _httpContextAccessor.HttpContext is not null
- && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp());
+ && _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIP());
var user = _userManager.GetUserById(userId);
if (user is null)
{
diff --git a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
index 6ed6fc90b..557b7d3aa 100644
--- a/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
+++ b/Jellyfin.Api/Auth/LocalAccessOrRequiresElevationPolicy/LocalAccessOrRequiresElevationHandler.cs
@@ -31,7 +31,7 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
///
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
{
- var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
+ var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIP();
// Loopback will be on LAN, so we can accept null.
if (ip is null || _networkManager.IsInLocalNetwork(ip))
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 95b296fae..42576934b 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -5,7 +5,6 @@ using System.IO;
using System.Net.Mime;
using System.Threading.Tasks;
using Emby.Dlna;
-using Emby.Dlna.Main;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Dlna;
@@ -33,12 +32,19 @@ public class DlnaServerController : BaseJellyfinApiController
/// Initializes a new instance of the class.
///
/// Instance of the interface.
- public DlnaServerController(IDlnaManager dlnaManager)
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public DlnaServerController(
+ IDlnaManager dlnaManager,
+ IContentDirectory contentDirectory,
+ IConnectionManager connectionManager,
+ IMediaReceiverRegistrar mediaReceiverRegistrar)
{
_dlnaManager = dlnaManager;
- _contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
- _connectionManager = DlnaEntryPoint.Current.ConnectionManager;
- _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
+ _contentDirectory = contentDirectory;
+ _connectionManager = connectionManager;
+ _mediaReceiverRegistrar = mediaReceiverRegistrar;
}
///
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 4b89738a1..38953dc21 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
private const string DefaultEventEncoderPreset = "superfast";
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
+ private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
+
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager;
@@ -1654,7 +1656,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer),
threads,
mapArgs,
- GetVideoArguments(state, startNumber, isEventPlaylist),
+ GetVideoArguments(state, startNumber, isEventPlaylist, segmentContainer),
GetAudioArguments(state),
maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
@@ -1706,19 +1708,33 @@ public class DynamicHlsController : BaseJellyfinApiController
}
var audioCodec = _encodingHelper.GetAudioEncoder(state);
+ var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+
+ // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
+ var strictArgs = string.Empty;
+ var actualOutputAudioCodec = state.ActualOutputAudioCodec;
+ if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
+ || (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
+ {
+ strictArgs = " -strict -2";
+ }
if (!state.IsOutputVideo)
{
- if (EncodingHelper.IsCopyCodec(audioCodec))
- {
- var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
-
- return "-acodec copy -strict -2" + bitStreamArgs;
- }
-
var audioTranscodeParams = string.Empty;
- audioTranscodeParams += "-acodec " + audioCodec;
+ // -vn to drop any video streams
+ audioTranscodeParams += "-vn";
+
+ if (EncodingHelper.IsCopyCodec(audioCodec))
+ {
+ return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs;
+ }
+
+ audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs;
var audioBitrate = state.OutputAudioBitrate;
var audioChannels = state.OutputAudioChannels;
@@ -1746,25 +1762,12 @@ public class DynamicHlsController : BaseJellyfinApiController
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- audioTranscodeParams += " -vn";
return audioTranscodeParams;
}
- // dts, flac, opus and truehd are experimental in mp4 muxer
- var strictArgs = string.Empty;
- var actualOutputAudioCodec = state.ActualOutputAudioCodec;
- if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
- || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
- || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
- {
- strictArgs = " -strict -2";
- }
-
if (EncodingHelper.IsCopyCodec(audioCodec))
{
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
- var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
@@ -1775,7 +1778,7 @@ public class DynamicHlsController : BaseJellyfinApiController
return copyArgs;
}
- var args = "-codec:a:0 " + audioCodec + strictArgs;
+ var args = "-codec:a:0 " + audioCodec + bitStreamArgs + strictArgs;
var channels = state.OutputAudioChannels;
@@ -1819,8 +1822,9 @@ public class DynamicHlsController : BaseJellyfinApiController
/// The .
/// The first number in the hls sequence.
/// Whether the playlist is EVENT or VOD.
+ /// The segment container.
/// The command line arguments for video transcoding.
- private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist)
+ private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist, string segmentContainer)
{
if (state.VideoStream is null)
{
@@ -1912,7 +1916,7 @@ public class DynamicHlsController : BaseJellyfinApiController
}
// TODO why was this not enabled for VOD?
- if (isEventPlaylist)
+ if (isEventPlaylist && string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
{
args += " -flags -global_header";
}
@@ -2045,9 +2049,9 @@ public class DynamicHlsController : BaseJellyfinApiController
return null;
}
- var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
+ var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
- var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+ var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index d7cec865e..6eedfd8c7 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{
// TODO: Deprecate with new iOS app
- var file = segmentId + Path.GetExtension(Request.Path);
+ var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
@@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
- var file = playlistId + Path.GetExtension(Request.Path);
+ var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
+ || Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Invalid segment.");
}
@@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
[FromRoute, Required] string segmentId,
[FromRoute, Required] string segmentContainer)
{
- var file = segmentId + Path.GetExtension(Request.Path);
+ var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 3c5f18af5..7b10ea170 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -7,6 +7,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
+using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
_appPaths = appPaths;
}
+ private static Stream GetFromBase64Stream(Stream inputStream)
+ => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
+
///
/// Sets the user image.
///
@@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .SaveImage(stream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .SaveImage(stream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
@@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
@@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
var brandingOptions = _serverConfigurationManager.GetConfiguration("branding");
@@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await using (fs.ConfigureAwait(false))
{
- await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
+ await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
}
return NoContent();
@@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
return NoContent();
}
- private static async Task GetMemoryStream(Stream inputStream)
- {
- using var reader = new StreamReader(inputStream);
- var text = await reader.ReadToEndAsync().ConfigureAwait(false);
-
- var bytes = Convert.FromBase64String(text);
- return new MemoryStream(bytes, 0, bytes.Length, false, true);
- }
-
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
{
int? width = null;
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 46c0a8d52..21941ff94 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -294,8 +294,8 @@ public class LibraryController : BaseJellyfinApiController
return new AllThemeMediaResult
{
- ThemeSongsResult = themeSongs?.Value,
- ThemeVideosResult = themeVideos?.Value,
+ ThemeSongsResult = themeSongs.Value,
+ ThemeVideosResult = themeVideos.Value,
SoundtrackSongsResult = new ThemeMediaResult()
};
}
@@ -490,7 +490,7 @@ public class LibraryController : BaseJellyfinApiController
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
- parent = parent?.GetParent();
+ parent = parent.GetParent();
}
return baseItemDtos;
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 267ba4afb..649397d68 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
@@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _configurationManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly ISessionManager _sessionManager;
///
/// Initializes a new instance of the class.
@@ -61,7 +59,6 @@ public class LiveTvController : BaseJellyfinApiController
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the class.
- /// Instance of the interface.
public LiveTvController(
ILiveTvManager liveTvManager,
IUserManager userManager,
@@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController
IDtoService dtoService,
IMediaSourceManager mediaSourceManager,
IConfigurationManager configurationManager,
- TranscodingJobHelper transcodingJobHelper,
- ISessionManager sessionManager)
+ TranscodingJobHelper transcodingJobHelper)
{
_liveTvManager = liveTvManager;
_userManager = userManager;
@@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController
_mediaSourceManager = mediaSourceManager;
_configurationManager = configurationManager;
_transcodingJobHelper = transcodingJobHelper;
- _sessionManager = sessionManager;
}
///
diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs
index da24616ff..bea545cfd 100644
--- a/Jellyfin.Api/Controllers/MediaInfoController.cs
+++ b/Jellyfin.Api/Controllers/MediaInfoController.cs
@@ -184,7 +184,7 @@ public class MediaInfoController : BaseJellyfinApiController
enableTranscoding.Value,
allowVideoStreamCopy.Value,
allowAudioStreamCopy.Value,
- Request.HttpContext.GetNormalizedRemoteIp());
+ Request.HttpContext.GetNormalizedRemoteIP());
}
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index b3e9d6297..fb89e9610 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -6,6 +6,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
+using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -90,7 +91,7 @@ public class SubtitleController : BaseJellyfinApiController
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
- public ActionResult DeleteSubtitle(
+ public async Task DeleteSubtitle(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int index)
{
@@ -101,7 +102,7 @@ public class SubtitleController : BaseJellyfinApiController
return NotFound();
}
- _subtitleManager.DeleteSubtitles(item, index);
+ await _subtitleManager.DeleteSubtitles(item, index).ConfigureAwait(false);
return NoContent();
}
@@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController
[FromBody, Required] UploadSubtitleDto body)
{
var video = (Video)_libraryManager.GetItemById(itemId);
- var data = Convert.FromBase64String(body.Data);
- var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
+ await using (stream.ConfigureAwait(false))
{
await _subtitleManager.UploadSubtitle(
video,
@@ -416,7 +416,8 @@ public class SubtitleController : BaseJellyfinApiController
Format = body.Format,
Language = body.Language,
IsForced = body.IsForced,
- Stream = memoryStream
+ IsHearingImpaired = body.IsHearingImpaired,
+ Stream = stream
}).ConfigureAwait(false);
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 9ed69f420..11095a97f 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -4,14 +4,12 @@ using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Mime;
-using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.System;
@@ -27,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
///
public class SystemController : BaseJellyfinApiController
{
+ private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost;
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
- private readonly INetworkManager _network;
- private readonly ILogger _logger;
+ private readonly INetworkManager _networkManager;
+ private readonly ISystemManager _systemManager;
///
/// Initializes a new instance of the class.
///
- /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
/// Instance of interface.
/// Instance of interface.
- /// Instance of interface.
- /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
public SystemController(
- IServerConfigurationManager serverConfigurationManager,
+ ILogger logger,
IServerApplicationHost appHost,
+ IServerApplicationPaths appPaths,
IFileSystem fileSystem,
- INetworkManager network,
- ILogger logger)
+ INetworkManager networkManager,
+ ISystemManager systemManager)
{
- _appPaths = serverConfigurationManager.ApplicationPaths;
- _appHost = appHost;
- _fileSystem = fileSystem;
- _network = network;
_logger = logger;
+ _appHost = appHost;
+ _appPaths = appPaths;
+ _fileSystem = fileSystem;
+ _networkManager = networkManager;
+ _systemManager = systemManager;
}
///
@@ -66,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult GetSystemInfo()
- {
- return _appHost.GetSystemInfo(Request);
- }
+ => _systemManager.GetSystemInfo(Request);
///
/// Gets public information about the server.
@@ -78,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
[HttpGet("Info/Public")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult GetPublicSystemInfo()
- {
- return _appHost.GetPublicSystemInfo(Request);
- }
+ => _systemManager.GetPublicSystemInfo(Request);
///
/// Pings the system.
@@ -91,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
[HttpPost("Ping", Name = "PostPingSystem")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult PingSystem()
- {
- return _appHost.Name;
- }
+ => _appHost.Name;
///
/// Restarts the application.
@@ -107,11 +103,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult RestartApplication()
{
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- _appHost.Restart();
- });
+ _systemManager.Restart();
return NoContent();
}
@@ -127,11 +119,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult ShutdownApplication()
{
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- await _appHost.Shutdown().ConfigureAwait(false);
- });
+ _systemManager.Shutdown();
return NoContent();
}
@@ -189,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
return new EndPointInfo
{
IsLocal = HttpContext.IsLocal(),
- IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp())
+ IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
};
}
@@ -227,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult> GetWakeOnLanInfo()
{
- var result = _network.GetMacAddresses()
+ var result = _networkManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i));
return Ok(result);
}
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index 7d23281f2..bdbbd1e0d 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -68,7 +68,8 @@ public class TvShowsController : BaseJellyfinApiController
/// Optional. Starting date of shows to show in Next Up section.
/// Whether to enable the total records count. Defaults to true.
/// Whether to disable sending the first episode in a series as next up.
- /// Whether to include watched episode in next up results.
+ /// Whether to include resumable episodes in next up results.
+ /// Whether to include watched episodes in next up results.
/// A with the next up episodes.
[HttpGet("NextUp")]
[ProducesResponseType(StatusCodes.Status200OK)]
@@ -86,6 +87,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] DateTime? nextUpDateCutoff,
[FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool disableFirstEpisode = false,
+ [FromQuery] bool enableResumable = true,
[FromQuery] bool enableRewatching = false)
{
userId = RequestHelpers.GetUserId(User, userId);
@@ -104,6 +106,7 @@ public class TvShowsController : BaseJellyfinApiController
EnableTotalRecordCount = enableTotalRecordCount,
DisableFirstEpisode = disableFirstEpisode,
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
+ EnableResumable = enableResumable,
EnableRewatching = enableRewatching
},
options);
diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs
index 2e9035d24..7177a0440 100644
--- a/Jellyfin.Api/Controllers/UniversalAudioController.cs
+++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs
@@ -138,7 +138,7 @@ public class UniversalAudioController : BaseJellyfinApiController
true,
true,
true,
- Request.HttpContext.GetNormalizedRemoteIp());
+ Request.HttpContext.GetNormalizedRemoteIP());
}
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs
index 530bd9603..1be40111d 100644
--- a/Jellyfin.Api/Controllers/UserController.cs
+++ b/Jellyfin.Api/Controllers/UserController.cs
@@ -134,7 +134,7 @@ public class UserController : BaseJellyfinApiController
return NotFound("User not found");
}
- var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIp().ToString());
+ var result = _userManager.GetUserDto(user, HttpContext.GetNormalizedRemoteIP().ToString());
return result;
}
@@ -217,7 +217,7 @@ public class UserController : BaseJellyfinApiController
DeviceId = auth.DeviceId,
DeviceName = auth.Device,
Password = request.Pw,
- RemoteEndPoint = HttpContext.GetNormalizedRemoteIp().ToString(),
+ RemoteEndPoint = HttpContext.GetNormalizedRemoteIP().ToString(),
Username = request.Username
}).ConfigureAwait(false);
@@ -226,7 +226,7 @@ public class UserController : BaseJellyfinApiController
catch (SecurityException e)
{
// rethrow adding IP address to message
- throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
+ throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIP()}] {e.Message}", e);
}
}
@@ -248,7 +248,7 @@ public class UserController : BaseJellyfinApiController
catch (SecurityException e)
{
// rethrow adding IP address to message
- throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIp()}] {e.Message}", e);
+ throw new SecurityException($"[{HttpContext.GetNormalizedRemoteIP()}] {e.Message}", e);
}
}
@@ -294,7 +294,7 @@ public class UserController : BaseJellyfinApiController
user.Username,
request.CurrentPw ?? string.Empty,
request.CurrentPw ?? string.Empty,
- HttpContext.GetNormalizedRemoteIp().ToString(),
+ HttpContext.GetNormalizedRemoteIP().ToString(),
false).ConfigureAwait(false);
if (success is null)
@@ -475,7 +475,7 @@ public class UserController : BaseJellyfinApiController
await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
}
- var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIp().ToString());
+ var result = _userManager.GetUserDto(newUser, HttpContext.GetNormalizedRemoteIP().ToString());
return result;
}
@@ -490,11 +490,11 @@ public class UserController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task> ForgotPassword([FromBody, Required] ForgotPasswordDto forgotPasswordRequest)
{
- var ip = HttpContext.GetNormalizedRemoteIp();
+ var ip = HttpContext.GetNormalizedRemoteIP();
var isLocal = HttpContext.IsLocal()
|| _networkManager.IsInLocalNetwork(ip);
- if (isLocal)
+ if (!isLocal)
{
_logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip);
}
@@ -571,7 +571,7 @@ public class UserController : BaseJellyfinApiController
if (filterByNetwork)
{
- if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIp()))
+ if (!_networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP()))
{
users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
}
@@ -579,7 +579,7 @@ public class UserController : BaseJellyfinApiController
var result = users
.OrderBy(u => u.Username)
- .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIp().ToString()));
+ .Select(i => _userManager.GetUserDto(i, HttpContext.GetNormalizedRemoteIP().ToString()));
return result;
}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index 9b0b65b10..24082fcff 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -206,13 +206,6 @@ public class DynamicHlsHelper
if (state.VideoStream is not null && state.VideoRequest is not null)
{
- // Provide a workaround for the case issue between flac and fLaC.
- var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
-
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
// Provide SDR HEVC entrance for backward compatibility.
@@ -242,14 +235,7 @@ public class DynamicHlsHelper
}
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
- var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
-
- // Provide a workaround for the case issue between flac and fLaC.
- flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
+ AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
// Restore the video codec
state.OutputVideoCodec = "copy";
@@ -280,17 +266,10 @@ public class DynamicHlsHelper
state.VideoStream.Level = originalLevel;
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
builder.Append(newPlaylist);
-
- // Provide a workaround for the case issue between flac and fLaC.
- flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
}
}
- if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
+ if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIP()))
{
var requestedVideoBitrate = state.VideoRequest is null ? 0 : state.VideoRequest.VideoBitRate ?? 0;
@@ -741,7 +720,7 @@ public class DynamicHlsHelper
// Currently we only transcode to 8 bits AV1
int bitDepth = 8;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && state.VideoStream != null
+ && state.VideoStream is not null
&& state.VideoStream.BitDepth.HasValue)
{
bitDepth = state.VideoStream.BitDepth.Value;
@@ -815,16 +794,4 @@ public class DynamicHlsHelper
newValue.ToString(),
StringComparison.Ordinal);
}
-
- private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
- {
- if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
- {
- return string.Empty;
- }
-
- var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
-
- return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
- }
}
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index 9a141a16d..5eec1b0ca 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -5,7 +5,9 @@ using System.Text;
namespace Jellyfin.Api.Helpers;
///
-/// Hls Codec string helpers.
+/// Helpers to generate HLS codec strings according to
+/// RFC 6381 section 3.3
+/// and the MP4 Registration Authority.
///
public static class HlsCodecStringHelpers
{
@@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
///
/// Codec name for FLAC.
///
- public const string FLAC = "flac";
+ public const string FLAC = "fLaC";
///
/// Codec name for ALAC.
@@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
///
/// Codec name for OPUS.
///
- public const string OPUS = "opus";
+ public const string OPUS = "Opus";
///
/// Gets a MP3 codec string.
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 5910d8073..a36028cfe 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -421,7 +421,7 @@ public class MediaInfoHelper
true,
true,
true,
- httpContext.GetNormalizedRemoteIp());
+ httpContext.GetNormalizedRemoteIP());
}
else
{
@@ -487,7 +487,7 @@ public class MediaInfoHelper
{
var isInLocalNetwork = _networkManager.IsInLocalNetwork(ipAddress);
- _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
+ _logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIP: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, ipAddress, isInLocalNetwork);
if (!isInLocalNetwork)
{
maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 57098edba..bc12ca388 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -125,7 +125,7 @@ public static class RequestHelpers
httpContext.User.GetVersion(),
httpContext.User.GetDeviceId(),
httpContext.User.GetDevice(),
- httpContext.GetNormalizedRemoteIp().ToString(),
+ httpContext.GetNormalizedRemoteIP().ToString(),
user).ConfigureAwait(false);
if (session is null)
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 782cd6568..11f6bcf6b 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -191,6 +191,11 @@ public static class StreamingHelpers
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
}
+ if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal))
+ {
+ containerInternal = ".pcm";
+ }
+
state.OutputAudioCodec = outputAudioCodec;
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
@@ -243,7 +248,7 @@ public static class StreamingHelpers
? GetOutputFileExtension(state, mediaSource)
: ("." + state.OutputContainer);
- state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
+ state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
return state;
}
@@ -416,10 +421,9 @@ public static class StreamingHelpers
/// The state.
/// The mediaSource.
/// System.String.
- private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
+ private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
{
var ext = Path.GetExtension(state.RequestedUrl);
-
if (!string.IsNullOrEmpty(ext))
{
return ext;
@@ -458,10 +462,9 @@ public static class StreamingHelpers
return ".asf";
}
}
-
- // Try to infer based on the desired audio codec
- if (!state.IsVideoRequest)
+ else
{
+ // Try to infer based on the desired audio codec
var audioCodec = state.Request.AudioCodec;
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
@@ -492,7 +495,7 @@ public static class StreamingHelpers
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
}
- return null;
+ throw new InvalidOperationException("Failed to find an appropriate file extension");
}
///
@@ -509,7 +512,7 @@ public static class StreamingHelpers
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- var ext = outputFileExtension?.ToLowerInvariant();
+ var ext = outputFileExtension.ToLowerInvariant();
var folder = serverConfigurationManager.GetTranscodePath();
return Path.Combine(folder, filename + ext);
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index cee8e0f9b..c16a586d6 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
}
- if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
+ if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
{
string subtitlePath = state.SubtitleStream.Path;
string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
@@ -620,7 +620,7 @@ public class TranscodingJobHelper : IDisposable
state.TranscodingJob = transcodingJob;
// Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
- _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
+ _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
// Wait for the file to exist before proceeding
var ffmpegTargetFile = state.WaitForPath ?? outputPath;
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 6a0a4706b..7ac231885 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -8,8 +8,6 @@
net7.0
true
-
- AD0001
diff --git a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
index 060c14f89..acbb4877d 100644
--- a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
@@ -122,17 +122,17 @@ public class ExceptionMiddleware
private static int GetStatusCode(Exception ex)
{
- switch (ex)
+ return ex switch
{
- case ArgumentException _: return StatusCodes.Status400BadRequest;
- case AuthenticationException _: return StatusCodes.Status401Unauthorized;
- case SecurityException _: return StatusCodes.Status403Forbidden;
- case DirectoryNotFoundException _:
- case FileNotFoundException _:
- case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
- case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
- default: return StatusCodes.Status500InternalServerError;
- }
+ ArgumentException => StatusCodes.Status400BadRequest,
+ AuthenticationException => StatusCodes.Status401Unauthorized,
+ SecurityException => StatusCodes.Status403Forbidden,
+ DirectoryNotFoundException => StatusCodes.Status404NotFound,
+ FileNotFoundException => StatusCodes.Status404NotFound,
+ ResourceNotFoundException => StatusCodes.Status404NotFound,
+ MethodNotAllowedException => StatusCodes.Status405MethodNotAllowed,
+ _ => StatusCodes.Status500InternalServerError
+ };
}
private string NormalizeExceptionMessage(string msg)
diff --git a/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs
index f45b6b5c0..27bcd5570 100644
--- a/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs
+++ b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs
@@ -9,15 +9,15 @@ namespace Jellyfin.Api.Middleware;
///
/// Validates the IP of requests coming from local networks wrt. remote access.
///
-public class IpBasedAccessValidationMiddleware
+public class IPBasedAccessValidationMiddleware
{
private readonly RequestDelegate _next;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The next delegate in the pipeline.
- public IpBasedAccessValidationMiddleware(RequestDelegate next)
+ public IPBasedAccessValidationMiddleware(RequestDelegate next)
{
_next = next;
}
@@ -37,9 +37,9 @@ public class IpBasedAccessValidationMiddleware
return;
}
- var remoteIp = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
+ var remoteIP = httpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback;
- if (!networkManager.HasRemoteAccess(remoteIp))
+ if (!networkManager.HasRemoteAccess(remoteIP))
{
return;
}
diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
index 9c2194faf..94de30d1b 100644
--- a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
+++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs
@@ -38,7 +38,7 @@ public class LanFilteringMiddleware
return;
}
- var host = httpContext.GetNormalizedRemoteIp();
+ var host = httpContext.GetNormalizedRemoteIP();
if (!networkManager.IsInLocalNetwork(host))
{
return;
diff --git a/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs
index db3917743..279ea70d8 100644
--- a/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs
+++ b/Jellyfin.Api/Middleware/ResponseTimeMiddleware.cs
@@ -51,9 +51,9 @@ public class ResponseTimeMiddleware
if (enableWarning && responseTimeMs > warningThreshold && _logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(
- "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}",
+ "Slow HTTP Response from {Url} to {RemoteIP} in {Elapsed:g} with Status Code {StatusCode}",
context.Request.GetDisplayUrl(),
- context.GetNormalizedRemoteIp(),
+ context.GetNormalizedRemoteIP(),
responseTime,
context.Response.StatusCode);
}
diff --git a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
index 8bf626035..acf3645fd 100644
--- a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
@@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware
/// The async task.
public async Task Invoke(HttpContext httpContext)
{
- var localPath = httpContext.Request.Path.ToString();
- if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
+ if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
httpContext.Response.Redirect("web/robots.txt");
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
index a34fd01d5..3e3604b2a 100644
--- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
@@ -77,7 +77,7 @@ public class CommaDelimitedArrayModelBinder : IModelBinder
var typedValueIndex = 0;
for (var i = 0; i < parsedValues.Length; i++)
{
- if (parsedValues[i] != null)
+ if (parsedValues[i] is not null)
{
typedValues.SetValue(parsedValues[i], typedValueIndex);
typedValueIndex++;
diff --git a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
index cb9a82955..ae9f0a8cd 100644
--- a/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
+++ b/Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
@@ -77,7 +77,7 @@ public class PipeDelimitedArrayModelBinder : IModelBinder
var typedValueIndex = 0;
for (var i = 0; i < parsedValues.Length; i++)
{
- if (parsedValues[i] != null)
+ if (parsedValues[i] is not null)
{
typedValues.SetValue(parsedValues[i], typedValueIndex);
typedValueIndex++;
diff --git a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
index 3c903ea6b..2c45e704b 100644
--- a/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
+++ b/Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
@@ -25,6 +25,12 @@ public class UploadSubtitleDto
[Required]
public bool IsForced { get; set; }
+ ///
+ /// Gets or sets a value indicating whether the subtitle is for hearing impaired.
+ ///
+ [Required]
+ public bool IsHearingImpaired { get; set; }
+
///
/// Gets or sets the subtitle data.
///
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 4a5e0ecd4..5b90d65d8 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -1,6 +1,8 @@
using System;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
+using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Session;
@@ -9,7 +11,7 @@ using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.WebSocketListeners;
///
-/// Class SessionInfoWebSocketListener.
+/// Class ActivityLogWebSocketListener.
///
public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener
{
@@ -56,6 +58,20 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener
+ /// Starts sending messages over an activity log web socket.
+ ///
+ /// The message.
+ protected override void Start(WebSocketMessageInfo message)
+ {
+ if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
+ {
+ throw new AuthenticationException("Only admin users can retrieve the activity log.");
+ }
+
+ base.Start(message);
+ }
+
private async void OnEntryCreated(object? sender, GenericEventArgs e)
{
await SendData(true).ConfigureAwait(false);
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index 0d8bf205c..b403ff46d 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
@@ -66,6 +68,20 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener
+ /// Starts sending messages over a session info web socket.
+ ///
+ /// The message.
+ protected override void Start(WebSocketMessageInfo message)
+ {
+ if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
+ {
+ throw new AuthenticationException("Only admin users can subscribe to session information.");
+ }
+
+ base.Start(message);
+ }
+
private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
{
await SendData(false).ConfigureAwait(false);
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 58ddaaf83..5c3e0338d 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -288,6 +288,12 @@ namespace Jellyfin.Data.Entities
///
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
+ ///
+ /// Gets or sets the cast receiver id.
+ ///
+ [StringLength(32)]
+ public string? CastReceiverId { get; set; }
+
///
[ConcurrencyCheck]
public uint RowVersion { get; private set; }
diff --git a/Jellyfin.Data/Enums/PersonKind.cs b/Jellyfin.Data/Enums/PersonKind.cs
index 10a805666..29308789a 100644
--- a/Jellyfin.Data/Enums/PersonKind.cs
+++ b/Jellyfin.Data/Enums/PersonKind.cs
@@ -94,4 +94,40 @@ public enum PersonKind
/// A person who was the illustrator.
///
Illustrator,
+
+ ///
+ /// A person responsible for drawing the art.
+ ///
+ Penciller,
+
+ ///
+ /// A person responsible for inking the pencil art.
+ ///
+ Inker,
+
+ ///
+ /// A person responsible for applying color to drawings.
+ ///
+ Colorist,
+
+ ///
+ /// A person responsible for drawing text and speech bubbles.
+ ///
+ Letterer,
+
+ ///
+ /// A person responsible for drawing the cover art.
+ ///
+ CoverArtist,
+
+ ///
+ /// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter.
+ /// An editor may also prepare a resource for production, publication, or distribution.
+ ///
+ Editor,
+
+ ///
+ /// A person who renders a text from one language into another.
+ ///
+ Translator
}
diff --git a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
index 361dbc814..90ebcd390 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfiguration.cs
@@ -10,32 +10,17 @@ namespace Jellyfin.Networking.Configuration
public class NetworkConfiguration
{
///
- /// The default value for .
+ /// The default value for .
///
public const int DefaultHttpPort = 8096;
///
- /// The default value for and .
+ /// The default value for and .
///
public const int DefaultHttpsPort = 8920;
private string _baseUrl = string.Empty;
- ///
- /// Gets or sets a value indicating whether the server should force connections over HTTPS.
- ///
- public bool RequireHttps { get; set; }
-
- ///
- /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
- ///
- public string CertificatePath { get; set; } = string.Empty;
-
- ///
- /// Gets or sets the password required to access the X.509 certificate data in the file specified by .
- ///
- public string CertificatePassword { get; set; } = string.Empty;
-
///
/// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
///
@@ -69,24 +54,6 @@ namespace Jellyfin.Networking.Configuration
}
}
- ///
- /// Gets or sets the public HTTPS port.
- ///
- /// The public HTTPS port.
- public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
-
- ///
- /// Gets or sets the HTTP server port number.
- ///
- /// The HTTP server port number.
- public int HttpServerPortNumber { get; set; } = DefaultHttpPort;
-
- ///
- /// Gets or sets the HTTPS server port number.
- ///
- /// The HTTPS server port number.
- public int HttpsPortNumber { get; set; } = DefaultHttpsPort;
-
///
/// Gets or sets a value indicating whether to use HTTPS.
///
@@ -97,118 +64,66 @@ namespace Jellyfin.Networking.Configuration
public bool EnableHttps { get; set; }
///
- /// Gets or sets the public mapped port.
+ /// Gets or sets a value indicating whether the server should force connections over HTTPS.
///
- /// The public mapped port.
- public int PublicPort { get; set; } = DefaultHttpPort;
+ public bool RequireHttps { get; set; }
///
- /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding.
+ /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
///
- public bool UPnPCreateHttpPortMap { get; set; }
+ public string CertificatePath { get; set; } = string.Empty;
///
- /// Gets or sets the UDPPortRange.
+ /// Gets or sets the password required to access the X.509 certificate data in the file specified by .
///
- public string UDPPortRange { get; set; } = string.Empty;
+ public string CertificatePassword { get; set; } = string.Empty;
///
- /// Gets or sets a value indicating whether gets or sets IPV6 capability.
+ /// Gets or sets the internal HTTP server port.
///
- public bool EnableIPV6 { get; set; }
+ /// The HTTP server port.
+ public int InternalHttpPort { get; set; } = DefaultHttpPort;
///
- /// Gets or sets a value indicating whether gets or sets IPV4 capability.
+ /// Gets or sets the internal HTTPS server port.
///
- public bool EnableIPV4 { get; set; } = true;
+ /// The HTTPS server port.
+ public int InternalHttpsPort { get; set; } = DefaultHttpsPort;
///
- /// Gets or sets a value indicating whether detailed SSDP logs are sent to the console/log.
- /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to have any effect.
+ /// Gets or sets the public HTTP port.
///
- public bool EnableSSDPTracing { get; set; }
+ /// The public HTTP port.
+ public int PublicHttpPort { get; set; } = DefaultHttpPort;
///
- /// Gets or sets the SSDPTracingFilter
- /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log.
- /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
+ /// Gets or sets the public HTTPS port.
///
- public string SSDPTracingFilter { get; set; } = string.Empty;
-
- ///
- /// Gets or sets the number of times SSDP UDP messages are sent.
- ///
- public int UDPSendCount { get; set; } = 2;
-
- ///
- /// Gets or sets the delay between each groups of SSDP messages (in ms).
- ///
- public int UDPSendDelay { get; set; } = 100;
-
- ///
- /// Gets or sets a value indicating whether address names that match should be Ignore for the purposes of binding.
- ///
- public bool IgnoreVirtualInterfaces { get; set; } = true;
-
- ///
- /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. .
- ///
- public string VirtualInterfaceNames { get; set; } = "vEthernet*";
-
- ///
- /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor.
- ///
- public int GatewayMonitorPeriod { get; set; } = 60;
-
- ///
- /// Gets a value indicating whether multi-socket binding is available.
- ///
- public bool EnableMultiSocketBinding { get; } = true;
-
- ///
- /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network.
- /// Depending on the address range implemented ULA ranges might not be used.
- ///
- public bool TrustAllIP6Interfaces { get; set; }
-
- ///
- /// Gets or sets the ports that HDHomerun uses.
- ///
- public string HDHomerunPortRange { get; set; } = string.Empty;
-
- ///
- /// Gets or sets the PublishedServerUriBySubnet
- /// Gets or sets PublishedServerUri to advertise for specific subnets.
- ///
- public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty();
-
- ///
- /// Gets or sets a value indicating whether Autodiscovery tracing is enabled.
- ///
- public bool AutoDiscoveryTracing { get; set; }
+ /// The public HTTPS port.
+ public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
///
/// Gets or sets a value indicating whether Autodiscovery is enabled.
///
public bool AutoDiscovery { get; set; } = true;
- ///
- /// Gets or sets the filter for remote IP connectivity. Used in conjunction with .
- ///
- public string[] RemoteIPFilter { get; set; } = Array.Empty();
-
- ///
- /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist.
- ///
- public bool IsRemoteIPFilterBlacklist { get; set; }
-
///
/// Gets or sets a value indicating whether to enable automatic port forwarding.
///
public bool EnableUPnP { get; set; }
///
- /// Gets or sets a value indicating whether access outside of the LAN is permitted.
+ /// Gets or sets a value indicating whether IPv6 is enabled.
+ ///
+ public bool EnableIPv4 { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether IPv6 is enabled.
+ ///
+ public bool EnableIPv6 { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether access from outside of the LAN is permitted.
///
public bool EnableRemoteAccess { get; set; } = true;
@@ -223,13 +138,39 @@ namespace Jellyfin.Networking.Configuration
public string[] LocalNetworkAddresses { get; set; } = Array.Empty();
///
- /// Gets or sets the known proxies. If the proxy is a network, it's added to the KnownNetworks.
+ /// Gets or sets the known proxies.
///
public string[] KnownProxies { get; set; } = Array.Empty();
+ ///
+ /// Gets or sets a value indicating whether address names that match should be ignored for the purposes of binding.
+ ///
+ public bool IgnoreVirtualInterfaces { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. .
+ ///
+ public string[] VirtualInterfaceNames { get; set; } = new string[] { "veth" };
+
///
/// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests.
///
public bool EnablePublishedServerUriByRequest { get; set; } = false;
+
+ ///
+ /// Gets or sets the PublishedServerUriBySubnet
+ /// Gets or sets PublishedServerUri to advertise for specific subnets.
+ ///
+ public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty();
+
+ ///
+ /// Gets or sets the filter for remote IP connectivity. Used in conjunction with .
+ ///
+ public string[] RemoteIPFilter { get; set; } = Array.Empty();
+
+ ///
+ /// Gets or sets a value indicating whether contains a blacklist or a whitelist. Default is a whitelist.
+ ///
+ public bool IsRemoteIPFilterBlacklist { get; set; }
}
}
diff --git a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs
index 8cbe398b0..3ba6bb8fc 100644
--- a/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs
+++ b/Jellyfin.Networking/Configuration/NetworkConfigurationExtensions.cs
@@ -14,7 +14,7 @@ namespace Jellyfin.Networking.Configuration
/// The .
public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config)
{
- return config.GetConfiguration("network");
+ return config.GetConfiguration(NetworkConfigurationStore.StoreKey);
}
}
}
diff --git a/Jellyfin.Networking/Constants/Network.cs b/Jellyfin.Networking/Constants/Network.cs
new file mode 100644
index 000000000..7fadc74bb
--- /dev/null
+++ b/Jellyfin.Networking/Constants/Network.cs
@@ -0,0 +1,75 @@
+using System.Net;
+using Microsoft.AspNetCore.HttpOverrides;
+
+namespace Jellyfin.Networking.Constants;
+
+///
+/// Networking constants.
+///
+public static class Network
+{
+ ///
+ /// IPv4 mask bytes.
+ ///
+ public const int IPv4MaskBytes = 4;
+
+ ///
+ /// IPv6 mask bytes.
+ ///
+ public const int IPv6MaskBytes = 16;
+
+ ///
+ /// Minimum IPv4 prefix size.
+ ///
+ public const int MinimumIPv4PrefixSize = 32;
+
+ ///
+ /// Minimum IPv6 prefix size.
+ ///
+ public const int MinimumIPv6PrefixSize = 128;
+
+ ///
+ /// Whole IPv4 address space.
+ ///
+ public static readonly IPNetwork IPv4Any = new IPNetwork(IPAddress.Any, 0);
+
+ ///
+ /// Whole IPv6 address space.
+ ///
+ public static readonly IPNetwork IPv6Any = new IPNetwork(IPAddress.IPv6Any, 0);
+
+ ///
+ /// IPv4 Loopback as defined in RFC 5735.
+ ///
+ public static readonly IPNetwork IPv4RFC5735Loopback = new IPNetwork(IPAddress.Loopback, 8);
+
+ ///
+ /// IPv4 private class A as defined in RFC 1918.
+ ///
+ public static readonly IPNetwork IPv4RFC1918PrivateClassA = new IPNetwork(IPAddress.Parse("10.0.0.0"), 8);
+
+ ///
+ /// IPv4 private class B as defined in RFC 1918.
+ ///
+ public static readonly IPNetwork IPv4RFC1918PrivateClassB = new IPNetwork(IPAddress.Parse("172.16.0.0"), 12);
+
+ ///
+ /// IPv4 private class C as defined in RFC 1918.
+ ///
+ public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16);
+
+ ///
+ /// IPv6 loopback as defined in RFC 4291.
+ ///
+ public static readonly IPNetwork IPv6RFC4291Loopback = new IPNetwork(IPAddress.IPv6Loopback, 128);
+
+ ///
+ /// IPv6 site local as defined in RFC 4291.
+ ///
+ public static readonly IPNetwork IPv6RFC4291SiteLocal = new IPNetwork(IPAddress.Parse("fe80::"), 10);
+
+ ///
+ /// IPv6 unique local as defined in RFC 4193.
+ ///
+ public static readonly IPNetwork IPv6RFC4193UniqueLocal = new IPNetwork(IPAddress.Parse("fc00::"), 7);
+}
diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs
new file mode 100644
index 000000000..a1e1140f1
--- /dev/null
+++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs
@@ -0,0 +1,346 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Net.Sockets;
+using System.Text.RegularExpressions;
+using Jellyfin.Extensions;
+using Jellyfin.Networking.Constants;
+using Microsoft.AspNetCore.HttpOverrides;
+
+namespace Jellyfin.Networking.Extensions;
+
+///
+/// Defines the .
+///
+public static partial class NetworkExtensions
+{
+ // Use regular expression as CheckHostName isn't RFC5892 compliant.
+ // Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
+ [GeneratedRegex(@"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)(:(\d){1,5}){0,1}$", RegexOptions.IgnoreCase, "en-US")]
+ private static partial Regex FqdnGeneratedRegex();
+
+ ///
+ /// Returns true if the IPAddress contains an IP6 Local link address.
+ ///
+ /// IPAddress object to check.
+ /// True if it is a local link address.
+ ///
+ /// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress
+ /// it appears that the IPAddress.IsIPv6LinkLocal is out of date.
+ ///
+ public static bool IsIPv6LinkLocal(IPAddress address)
+ {
+ ArgumentNullException.ThrowIfNull(address);
+
+ if (address.IsIPv4MappedToIPv6)
+ {
+ address = address.MapToIPv4();
+ }
+
+ if (address.AddressFamily != AddressFamily.InterNetworkV6)
+ {
+ return false;
+ }
+
+ // GetAddressBytes
+ Span octet = stackalloc byte[16];
+ address.TryWriteBytes(octet, out _);
+ uint word = (uint)(octet[0] << 8) + octet[1];
+
+ return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link.
+ }
+
+ ///
+ /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only.
+ ///
+ /// Subnet mask in CIDR notation.
+ /// IPv4 or IPv6 family.
+ /// String value of the subnet mask in dotted decimal notation.
+ public static IPAddress CidrToMask(byte cidr, AddressFamily family)
+ {
+ uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr);
+ addr = ((addr & 0xff000000) >> 24)
+ | ((addr & 0x00ff0000) >> 8)
+ | ((addr & 0x0000ff00) << 8)
+ | ((addr & 0x000000ff) << 24);
+ return new IPAddress(addr);
+ }
+
+ ///
+ /// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only.
+ ///
+ /// Subnet mask in CIDR notation.
+ /// IPv4 or IPv6 family.
+ /// String value of the subnet mask in dotted decimal notation.
+ public static IPAddress CidrToMask(int cidr, AddressFamily family)
+ {
+ uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize) - cidr);
+ addr = ((addr & 0xff000000) >> 24)
+ | ((addr & 0x00ff0000) >> 8)
+ | ((addr & 0x0000ff00) << 8)
+ | ((addr & 0x000000ff) << 24);
+ return new IPAddress(addr);
+ }
+
+ ///
+ /// Convert a subnet mask to a CIDR. IPv4 only.
+ /// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask.
+ ///
+ /// Subnet mask.
+ /// Byte CIDR representing the mask.
+ public static byte MaskToCidr(IPAddress mask)
+ {
+ ArgumentNullException.ThrowIfNull(mask);
+
+ byte cidrnet = 0;
+ if (mask.Equals(IPAddress.Any))
+ {
+ return cidrnet;
+ }
+
+ // GetAddressBytes
+ Span bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? Network.IPv4MaskBytes : Network.IPv6MaskBytes];
+ if (!mask.TryWriteBytes(bytes, out var bytesWritten))
+ {
+ Console.WriteLine("Unable to write address bytes, only ${bytesWritten} bytes written.");
+ }
+
+ var zeroed = false;
+ for (var i = 0; i < bytes.Length; i++)
+ {
+ for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1)
+ {
+ if (zeroed)
+ {
+ // Invalid netmask.
+ return (byte)~cidrnet;
+ }
+
+ if ((v & 0x80) == 0)
+ {
+ zeroed = true;
+ }
+ else
+ {
+ cidrnet++;
+ }
+ }
+ }
+
+ return cidrnet;
+ }
+
+ ///
+ /// Converts an IPAddress into a string.
+ /// IPv6 addresses are returned in [ ], with their scope removed.
+ ///
+ /// Address to convert.
+ /// URI safe conversion of the address.
+ public static string FormatIPString(IPAddress? address)
+ {
+ if (address is null)
+ {
+ return string.Empty;
+ }
+
+ var str = address.ToString();
+ if (address.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ int i = str.IndexOf('%', StringComparison.Ordinal);
+ if (i != -1)
+ {
+ str = str.Substring(0, i);
+ }
+
+ return $"[{str}]";
+ }
+
+ return str;
+ }
+
+ ///
+ /// Try parsing an array of strings into objects, respecting exclusions.
+ /// Elements without a subnet mask will be represented as with a single IP.
+ ///
+ /// Input string array to be parsed.
+ /// Collection of .
+ /// Boolean signaling if negated or not negated values should be parsed.
+ /// True if parsing was successful.
+ public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList? result, bool negated = false)
+ {
+ if (values is null || values.Length == 0)
+ {
+ result = null;
+ return false;
+ }
+
+ var tmpResult = new List();
+ for (int a = 0; a < values.Length; a++)
+ {
+ if (TryParseToSubnet(values[a], out var innerResult, negated))
+ {
+ tmpResult.Add(innerResult);
+ }
+ }
+
+ result = tmpResult;
+ return tmpResult.Count > 0;
+ }
+
+ ///
+ /// Try parsing a string into an , respecting exclusions.
+ /// Inputs without a subnet mask will be represented as with a single IP.
+ ///
+ /// Input string to be parsed.
+ /// An .
+ /// Boolean signaling if negated or not negated values should be parsed.
+ /// True if parsing was successful.
+ public static bool TryParseToSubnet(ReadOnlySpan value, [NotNullWhen(true)] out IPNetwork? result, bool negated = false)
+ {
+ var splitString = value.Trim().Split('/');
+ if (splitString.MoveNext())
+ {
+ var ipBlock = splitString.Current;
+ var address = IPAddress.None;
+ if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress))
+ {
+ address = tmpAddress;
+ }
+ else if (!negated && IPAddress.TryParse(ipBlock, out tmpAddress))
+ {
+ address = tmpAddress;
+ }
+
+ if (address != IPAddress.None)
+ {
+ if (splitString.MoveNext())
+ {
+ var subnetBlock = splitString.Current;
+ if (int.TryParse(subnetBlock, out var netmask))
+ {
+ result = new IPNetwork(address, netmask);
+ return true;
+ }
+ else if (IPAddress.TryParse(subnetBlock, out var netmaskAddress))
+ {
+ result = new IPNetwork(address, NetworkExtensions.MaskToCidr(netmaskAddress));
+ return true;
+ }
+ }
+ else if (address.AddressFamily == AddressFamily.InterNetwork)
+ {
+ result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize);
+ return true;
+ }
+ else if (address.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize);
+ return true;
+ }
+ }
+ }
+
+ result = null;
+ return false;
+ }
+
+ ///
+ /// Attempts to parse a host span.
+ ///
+ /// Host name to parse.
+ /// Object representing the span, if it has successfully been parsed.
+ /// true if IPv4 is enabled.
+ /// true if IPv6 is enabled.
+ /// true if the parsing is successful, false if not.
+ public static bool TryParseHost(ReadOnlySpan host, [NotNullWhen(true)] out IPAddress[]? addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false)
+ {
+ host = host.Trim();
+ if (host.IsEmpty)
+ {
+ addresses = null;
+ return false;
+ }
+
+ // See if it's an IPv6 with port address e.g. [::1] or [::1]:120.
+ if (host[0] == '[')
+ {
+ int i = host.IndexOf(']');
+ if (i != -1)
+ {
+ return TryParseHost(host[1..(i - 1)], out addresses);
+ }
+
+ addresses = Array.Empty();
+ return false;
+ }
+
+ var hosts = new List();
+ foreach (var splitSpan in host.Split(':'))
+ {
+ hosts.Add(splitSpan.ToString());
+ }
+
+ if (hosts.Count <= 2)
+ {
+ var firstPart = hosts[0];
+
+ // Is hostname or hostname:port
+ if (FqdnGeneratedRegex().IsMatch(firstPart))
+ {
+ try
+ {
+ // .NET automatically filters only supported returned addresses based on OS support.
+ addresses = Dns.GetHostAddresses(firstPart);
+ return true;
+ }
+ catch (SocketException)
+ {
+ // Ignore socket errors, as the result value will just be an empty array.
+ }
+ }
+
+ // Is an IPv4 or IPv4:port
+ if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address))
+ {
+ if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
+ || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))
+ {
+ addresses = Array.Empty();
+ return false;
+ }
+
+ addresses = new[] { address };
+
+ // Host name is an IPv4 address, so fake resolve.
+ return true;
+ }
+ }
+ else if (hosts.Count > 0 && hosts.Count <= 9) // 8 octets + port
+ {
+ if (IPAddress.TryParse(host.LeftPart('/'), out var address))
+ {
+ addresses = new[] { address };
+ return true;
+ }
+ }
+
+ addresses = Array.Empty();
+ return false;
+ }
+
+ ///
+ /// Gets the broadcast address for a .
+ ///
+ /// The .
+ /// The broadcast address.
+ public static IPAddress GetBroadcastAddress(IPNetwork network)
+ {
+ var addressBytes = network.Prefix.GetAddressBytes();
+ uint ipAddress = BitConverter.ToUInt32(addressBytes, 0);
+ uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0);
+ uint broadCastIPAddress = ipAddress | ~ipMaskV4;
+
+ return new IPAddress(BitConverter.GetBytes(broadCastIPAddress));
+ }
+}
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index afb053820..9c59500d7 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -1,57 +1,50 @@
using System;
using System.Collections.Generic;
-using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
-using System.Threading.Tasks;
+using System.Threading;
using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Constants;
+using Jellyfin.Networking.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
+using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Jellyfin.Networking.Manager
{
///
/// Class to take care of network interface management.
- /// Note: The normal collection methods and properties will not work with Collection{IPObject}. .
///
public class NetworkManager : INetworkManager, IDisposable
{
- ///
- /// Contains the description of the interface along with its index.
- ///
- private readonly Dictionary _interfaceNames;
-
///
/// Threading lock for network properties.
///
- private readonly object _intLock = new object();
-
- ///
- /// List of all interface addresses and masks.
- ///
- private readonly Collection _interfaceAddresses;
-
- ///
- /// List of all interface MAC addresses.
- ///
- private readonly List _macAddresses;
+ private readonly object _initLock;
private readonly ILogger _logger;
private readonly IConfigurationManager _configurationManager;
- private readonly object _eventFireLock;
+ private readonly IConfiguration _startupConfig;
+
+ private readonly object _networkEventLock;
///
- /// Holds the bind address overrides.
+ /// Holds the published server URLs and the IPs to use them on.
///
- private readonly Dictionary _publishedServerUrls;
+ private IReadOnlyList _publishedServerUrls;
+
+ private IReadOnlyList _remoteAddressFilter;
///
/// Used to stop "event-racing conditions".
@@ -59,35 +52,25 @@ namespace Jellyfin.Networking.Manager
private bool _eventfire;
///
- /// Unfiltered user defined LAN subnets. ()
+ /// List of all interface MAC addresses.
+ ///
+ private IReadOnlyList _macAddresses;
+
+ ///
+ /// Dictionary containing interface addresses and their subnets.
+ ///
+ private IReadOnlyList _interfaces;
+
+ ///
+ /// Unfiltered user defined LAN subnets ()
/// or internal interface network subnets if undefined by user.
///
- private Collection _lanSubnets;
+ private IReadOnlyList _lanSubnets;
///
/// User defined list of subnets to excluded from the LAN.
///
- private Collection _excludedSubnets;
-
- ///
- /// List of interface addresses to bind the WS.
- ///
- private Collection _bindAddresses;
-
- ///
- /// List of interface addresses to exclude from bind.
- ///
- private Collection _bindExclusions;
-
- ///
- /// Caches list of all internal filtered interface addresses and masks.
- ///
- private Collection _internalInterfaces;
-
- ///
- /// Flag set when no custom LAN has been defined in the configuration.
- ///
- private bool _usingPrivateAddresses;
+ private IReadOnlyList _excludedSubnets;
///
/// True if this object is disposed.
@@ -97,19 +80,24 @@ namespace Jellyfin.Networking.Manager
///
/// Initializes a new instance of the class.
///
- /// IServerConfigurationManager instance.
+ /// The instance.
+ /// The instance holding startup parameters.
/// Logger to use for messages.
#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
- public NetworkManager(IConfigurationManager configurationManager, ILogger logger)
+ public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger logger)
{
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _configurationManager = configurationManager ?? throw new ArgumentNullException(nameof(configurationManager));
+ ArgumentNullException.ThrowIfNull(logger);
+ ArgumentNullException.ThrowIfNull(configurationManager);
- _interfaceAddresses = new Collection();
+ _logger = logger;
+ _configurationManager = configurationManager;
+ _startupConfig = startupConfig;
+ _initLock = new();
+ _interfaces = new List();
_macAddresses = new List();
- _interfaceNames = new Dictionary();
- _publishedServerUrls = new Dictionary();
- _eventFireLock = new object();
+ _publishedServerUrls = new List();
+ _networkEventLock = new object();
+ _remoteAddressFilter = new List();
UpdateSettings(_configurationManager.GetNetworkConfiguration());
@@ -131,46 +119,24 @@ namespace Jellyfin.Networking.Manager
public static string MockNetworkSettings { get; set; } = string.Empty;
///
- /// Gets or sets a value indicating whether IP6 is enabled.
+ /// Gets a value indicating whether IP4 is enabled.
///
- public bool IsIP6Enabled { get; set; }
+ public bool IsIPv4Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv4;
///
- /// Gets or sets a value indicating whether IP4 is enabled.
+ /// Gets a value indicating whether IP6 is enabled.
///
- public bool IsIP4Enabled { get; set; }
-
- ///
- public Collection RemoteAddressFilter { get; private set; }
+ public bool IsIPv6Enabled => _configurationManager.GetNetworkConfiguration().EnableIPv6;
///
/// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
///
- public bool TrustAllIP6Interfaces { get; internal set; }
+ public bool TrustAllIPv6Interfaces { get; private set; }
///
/// Gets the Published server override list.
///
- public Dictionary PublishedServerUrls => _publishedServerUrls;
-
- ///
- /// Creates a new network collection.
- ///
- /// Items to assign the collection, or null.
- /// The collection created.
- public static Collection CreateCollection(IEnumerable? source = null)
- {
- var result = new Collection();
- if (source is not null)
- {
- foreach (var item in source)
- {
- result.AddItem(item, false);
- }
- }
-
- return result;
- }
+ public IReadOnlyList PublishedServerUrls => _publishedServerUrls;
///
public void Dispose()
@@ -179,452 +145,495 @@ namespace Jellyfin.Networking.Manager
GC.SuppressFinalize(this);
}
- ///
- public IReadOnlyCollection GetMacAddresses()
+ ///
+ /// Handler for network change events.
+ ///
+ /// Sender.
+ /// A containing network availability information.
+ private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e)
{
- // Populated in construction - so always has values.
- return _macAddresses;
- }
-
- ///
- public bool IsGatewayInterface(IPObject? addressObj)
- {
- var address = addressObj?.Address ?? IPAddress.None;
- return _internalInterfaces.Any(i => i.Address.Equals(address) && i.Tag < 0);
- }
-
- ///
- public bool IsGatewayInterface(IPAddress? addressObj)
- {
- return _internalInterfaces.Any(i => i.Address.Equals(addressObj ?? IPAddress.None) && i.Tag < 0);
- }
-
- ///
- public Collection GetLoopbacks()
- {
- Collection nc = new Collection();
- if (IsIP4Enabled)
- {
- nc.AddItem(IPAddress.Loopback);
- }
-
- if (IsIP6Enabled)
- {
- nc.AddItem(IPAddress.IPv6Loopback);
- }
-
- return nc;
- }
-
- ///
- public bool IsExcluded(IPAddress ip)
- {
- return _excludedSubnets.ContainsAddress(ip);
- }
-
- ///
- public bool IsExcluded(EndPoint ip)
- {
- return ip is not null && IsExcluded(((IPEndPoint)ip).Address);
- }
-
- ///
- public Collection CreateIPCollection(string[] values, bool negated = false)
- {
- Collection col = new Collection();
- if (values is null)
- {
- return col;
- }
-
- for (int a = 0; a < values.Length; a++)
- {
- string v = values[a].Trim();
-
- try
- {
- if (v.StartsWith('!'))
- {
- if (negated)
- {
- AddToCollection(col, v[1..]);
- }
- }
- else if (!negated)
- {
- AddToCollection(col, v);
- }
- }
- catch (ArgumentException e)
- {
- _logger.LogWarning(e, "Ignoring LAN value {Value}.", v);
- }
- }
-
- return col;
- }
-
- ///
- public Collection GetAllBindInterfaces(bool individualInterfaces = false)
- {
- int count = _bindAddresses.Count;
-
- if (count == 0)
- {
- if (_bindExclusions.Count > 0)
- {
- // Return all the interfaces except the ones specifically excluded.
- return _interfaceAddresses.Exclude(_bindExclusions, false);
- }
-
- if (individualInterfaces)
- {
- return new Collection(_interfaceAddresses);
- }
-
- // No bind address and no exclusions, so listen on all interfaces.
- Collection result = new Collection();
-
- if (IsIP6Enabled && IsIP4Enabled)
- {
- // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any
- result.AddItem(IPAddress.IPv6Any);
- }
- else if (IsIP4Enabled)
- {
- result.AddItem(IPAddress.Any);
- }
- else if (IsIP6Enabled)
- {
- // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses.
- foreach (var iface in _interfaceAddresses)
- {
- if (iface.AddressFamily == AddressFamily.InterNetworkV6)
- {
- result.AddItem(iface.Address);
- }
- }
- }
-
- return result;
- }
-
- // Remove any excluded bind interfaces.
- return _bindAddresses.Exclude(_bindExclusions, false);
- }
-
- ///
- public string GetBindInterface(string source, out int? port)
- {
- if (IPHost.TryParse(source, out IPHost host))
- {
- return GetBindInterface(host, out port);
- }
-
- return GetBindInterface(IPHost.None, out port);
- }
-
- ///
- public string GetBindInterface(IPAddress source, out int? port)
- {
- return GetBindInterface(new IPNetAddress(source), out port);
- }
-
- ///
- public string GetBindInterface(HttpRequest source, out int? port)
- {
- string result;
-
- if (source is not null && IPHost.TryParse(source.Host.Host, out IPHost host))
- {
- result = GetBindInterface(host, out port);
- port ??= source.Host.Port;
- }
- else
- {
- result = GetBindInterface(IPNetAddress.None, out port);
- port ??= source?.Host.Port;
- }
-
- return result;
- }
-
- ///
- public string GetBindInterface(IPObject source, out int? port)
- {
- port = null;
- ArgumentNullException.ThrowIfNull(source);
-
- // Do we have a source?
- bool haveSource = !source.Address.Equals(IPAddress.None);
- bool isExternal = false;
-
- if (haveSource)
- {
- if (!IsIP6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6)
- {
- _logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
- }
-
- if (!IsIP4Enabled && source.AddressFamily == AddressFamily.InterNetwork)
- {
- _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
- }
-
- isExternal = !IsInLocalNetwork(source);
-
- if (MatchesPublishedServerUrl(source, isExternal, out string res, out port))
- {
- _logger.LogDebug("{Source}: Using BindAddress {Address}:{Port}", source, res, port);
- return res;
- }
- }
-
- _logger.LogDebug("GetBindInterface: Source: {HaveSource}, External: {IsExternal}:", haveSource, isExternal);
-
- // No preference given, so move on to bind addresses.
- if (MatchesBindInterface(source, isExternal, out string result))
- {
- return result;
- }
-
- if (isExternal && MatchesExternalInterface(source, out result))
- {
- return result;
- }
-
- // Get the first LAN interface address that isn't a loopback.
- var interfaces = CreateCollection(
- _interfaceAddresses
- .Exclude(_bindExclusions, false)
- .Where(IsInLocalNetwork)
- .OrderBy(p => p.Tag));
-
- if (interfaces.Count > 0)
- {
- if (haveSource)
- {
- foreach (var intf in interfaces)
- {
- if (intf.Address.Equals(source.Address))
- {
- result = FormatIP6String(intf.Address);
- _logger.LogDebug("{Source}: GetBindInterface: Has found matching interface. {Result}", source, result);
- return result;
- }
- }
-
- // Does the request originate in one of the interface subnets?
- // (For systems with multiple internal network cards, and multiple subnets)
- foreach (var intf in interfaces)
- {
- if (intf.Contains(source))
- {
- result = FormatIP6String(intf.Address);
- _logger.LogDebug("{Source}: GetBindInterface: Has source, matched best internal interface on range. {Result}", source, result);
- return result;
- }
- }
- }
-
- result = FormatIP6String(interfaces.First().Address);
- _logger.LogDebug("{Source}: GetBindInterface: Matched first internal interface. {Result}", source, result);
- return result;
- }
-
- // There isn't any others, so we'll use the loopback.
- result = IsIP6Enabled ? "::1" : "127.0.0.1";
- _logger.LogWarning("{Source}: GetBindInterface: Loopback {Result} returned.", source, result);
- return result;
- }
-
- ///
- public Collection GetInternalBindAddresses()
- {
- int count = _bindAddresses.Count;
-
- if (count == 0)
- {
- if (_bindExclusions.Count > 0)
- {
- // Return all the internal interfaces except the ones excluded.
- return CreateCollection(_internalInterfaces.Where(p => !_bindExclusions.ContainsAddress(p)));
- }
-
- // No bind address, so return all internal interfaces.
- return CreateCollection(_internalInterfaces);
- }
-
- return new Collection(_bindAddresses.Where(a => IsInLocalNetwork(a)).ToArray());
- }
-
- ///
- public bool IsInLocalNetwork(IPObject address)
- {
- return IsInLocalNetwork(address.Address);
- }
-
- ///
- public bool IsInLocalNetwork(string address)
- {
- return IPHost.TryParse(address, out IPHost ipHost) && IsInLocalNetwork(ipHost);
- }
-
- ///
- public bool IsInLocalNetwork(IPAddress address)
- {
- ArgumentNullException.ThrowIfNull(address);
-
- if (address.Equals(IPAddress.None))
- {
- return false;
- }
-
- // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
- if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
- {
- return true;
- }
-
- // As private addresses can be redefined by Configuration.LocalNetworkAddresses
- return IPAddress.IsLoopback(address) || (_lanSubnets.ContainsAddress(address) && !_excludedSubnets.ContainsAddress(address));
- }
-
- ///
- public bool IsPrivateAddressRange(IPObject address)
- {
- ArgumentNullException.ThrowIfNull(address);
-
- // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
- if (TrustAllIP6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
- {
- return true;
- }
-
- return address.IsPrivateAddressRange();
- }
-
- ///
- public bool IsExcludedInterface(IPAddress address)
- {
- return _bindExclusions.ContainsAddress(address);
- }
-
- ///
- public Collection GetFilteredLANSubnets(Collection? filter = null)
- {
- if (filter is null)
- {
- return _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks();
- }
-
- return _lanSubnets.Exclude(filter, true);
- }
-
- ///
- public bool IsValidInterfaceAddress(IPAddress address)
- {
- return _interfaceAddresses.ContainsAddress(address);
- }
-
- ///
- public bool TryParseInterface(string token, out Collection? result)
- {
- result = null;
- if (string.IsNullOrEmpty(token))
- {
- return false;
- }
-
- if (_interfaceNames is not null && _interfaceNames.TryGetValue(token.ToLower(CultureInfo.InvariantCulture), out int index))
- {
- result = new Collection();
-
- _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token);
-
- // Replace interface tags with the interface IP's.
- foreach (IPNetAddress iface in _interfaceAddresses)
- {
- if (Math.Abs(iface.Tag) == index
- && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork)
- || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
- {
- result.AddItem(iface, false);
- }
- }
-
- return true;
- }
-
- return false;
- }
-
- ///
- public bool HasRemoteAccess(IPAddress remoteIp)
- {
- var config = _configurationManager.GetNetworkConfiguration();
- if (config.EnableRemoteAccess)
- {
- // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
- // If left blank, all remote addresses will be allowed.
- if (RemoteAddressFilter.Count > 0 && !IsInLocalNetwork(remoteIp))
- {
- // remoteAddressFilter is a whitelist or blacklist.
- return RemoteAddressFilter.ContainsAddress(remoteIp) == !config.IsRemoteIPFilterBlacklist;
- }
- }
- else if (!IsInLocalNetwork(remoteIp))
- {
- // Remote not enabled. So everyone should be LAN.
- return false;
- }
-
- return true;
+ _logger.LogDebug("Network availability changed.");
+ HandleNetworkChange();
}
///
- /// Reloads all settings and re-initialises the instance.
+ /// Handler for network change events.
+ ///
+ /// Sender.
+ /// An .
+ private void OnNetworkAddressChanged(object? sender, EventArgs e)
+ {
+ _logger.LogDebug("Network address change detected.");
+ HandleNetworkChange();
+ }
+
+ ///
+ /// Triggers our event, and re-loads interface information.
+ ///
+ private void HandleNetworkChange()
+ {
+ lock (_networkEventLock)
+ {
+ if (!_eventfire)
+ {
+ // As network events tend to fire one after the other only fire once every second.
+ _eventfire = true;
+ OnNetworkChange();
+ }
+ }
+ }
+
+ ///
+ /// Waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession.
+ ///
+ private void OnNetworkChange()
+ {
+ try
+ {
+ Thread.Sleep(2000);
+ var networkConfig = _configurationManager.GetNetworkConfiguration();
+ if (IsIPv6Enabled && !Socket.OSSupportsIPv6)
+ {
+ UpdateSettings(networkConfig);
+ }
+ else
+ {
+ InitializeInterfaces();
+ InitializeLan(networkConfig);
+ EnforceBindSettings(networkConfig);
+ }
+
+ PrintNetworkInformation(networkConfig);
+ NetworkChanged?.Invoke(this, EventArgs.Empty);
+ }
+ finally
+ {
+ _eventfire = false;
+ }
+ }
+
+ ///
+ /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
+ /// Generate a list of all active mac addresses that aren't loopback addresses.
+ ///
+ private void InitializeInterfaces()
+ {
+ lock (_initLock)
+ {
+ _logger.LogDebug("Refreshing interfaces.");
+
+ var interfaces = new List();
+ var macAddresses = new List();
+
+ try
+ {
+ var nics = NetworkInterface.GetAllNetworkInterfaces()
+ .Where(i => i.OperationalStatus == OperationalStatus.Up);
+
+ foreach (NetworkInterface adapter in nics)
+ {
+ try
+ {
+ var ipProperties = adapter.GetIPProperties();
+ var mac = adapter.GetPhysicalAddress();
+
+ // Populate MAC list
+ if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && PhysicalAddress.None.Equals(mac))
+ {
+ macAddresses.Add(mac);
+ }
+
+ // Populate interface list
+ foreach (var info in ipProperties.UnicastAddresses)
+ {
+ if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
+ {
+ var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+ {
+ Index = ipProperties.GetIPv4Properties().Index,
+ Name = adapter.Name,
+ SupportsMulticast = adapter.SupportsMulticast
+ };
+
+ interfaces.Add(interfaceObject);
+ }
+ else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+ {
+ Index = ipProperties.GetIPv6Properties().Index,
+ Name = adapter.Name,
+ SupportsMulticast = adapter.SupportsMulticast
+ };
+
+ interfaces.Add(interfaceObject);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // Ignore error, and attempt to continue.
+ _logger.LogError(ex, "Error encountered parsing interfaces.");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error obtaining interfaces.");
+ }
+
+ // If no interfaces are found, fallback to loopback interfaces.
+ if (interfaces.Count == 0)
+ {
+ _logger.LogWarning("No interface information available. Using loopback interface(s).");
+
+ if (IsIPv4Enabled)
+ {
+ interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
+ }
+
+ if (IsIPv6Enabled)
+ {
+ interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
+ }
+ }
+
+ _logger.LogDebug("Discovered {NumberOfInterfaces} interfaces.", interfaces.Count);
+ _logger.LogDebug("Interfaces addresses: {Addresses}", interfaces.OrderByDescending(s => s.AddressFamily == AddressFamily.InterNetwork).Select(s => s.Address.ToString()));
+
+ _macAddresses = macAddresses;
+ _interfaces = interfaces;
+ }
+ }
+
+ ///
+ /// Initializes internal LAN cache.
+ ///
+ private void InitializeLan(NetworkConfiguration config)
+ {
+ lock (_initLock)
+ {
+ _logger.LogDebug("Refreshing LAN information.");
+
+ // Get configuration options
+ var subnets = config.LocalNetworkSubnets;
+
+ // If no LAN addresses are specified, all private subnets and Loopback are deemed to be the LAN
+ if (!NetworkExtensions.TryParseToSubnets(subnets, out var lanSubnets, false) || lanSubnets.Count == 0)
+ {
+ _logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
+
+ var fallbackLanSubnets = new List();
+ if (IsIPv6Enabled)
+ {
+ fallbackLanSubnets.Add(Network.IPv6RFC4291Loopback); // RFC 4291 (Loopback)
+ fallbackLanSubnets.Add(Network.IPv6RFC4291SiteLocal); // RFC 4291 (Site local)
+ fallbackLanSubnets.Add(Network.IPv6RFC4193UniqueLocal); // RFC 4193 (Unique local)
+ }
+
+ if (IsIPv4Enabled)
+ {
+ fallbackLanSubnets.Add(Network.IPv4RFC5735Loopback); // RFC 5735 (Loopback)
+ fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassA); // RFC 1918 (private Class A)
+ fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassB); // RFC 1918 (private Class B)
+ fallbackLanSubnets.Add(Network.IPv4RFC1918PrivateClassC); // RFC 1918 (private Class C)
+ }
+
+ _lanSubnets = fallbackLanSubnets;
+ }
+ else
+ {
+ _lanSubnets = lanSubnets;
+ }
+
+ _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
+ ? excludedSubnets
+ : new List();
+ }
+ }
+
+ ///
+ /// Enforce bind addresses and exclusions on available interfaces.
+ ///
+ private void EnforceBindSettings(NetworkConfiguration config)
+ {
+ lock (_initLock)
+ {
+ // Respect explicit bind addresses
+ var interfaces = _interfaces.ToList();
+ var localNetworkAddresses = config.LocalNetworkAddresses;
+ if (localNetworkAddresses.Length > 0 && !string.IsNullOrWhiteSpace(localNetworkAddresses[0]))
+ {
+ var bindAddresses = localNetworkAddresses.Select(p => NetworkExtensions.TryParseToSubnet(p, out var network)
+ ? network.Prefix
+ : (interfaces.Where(x => x.Name.Equals(p, StringComparison.OrdinalIgnoreCase))
+ .Select(x => x.Address)
+ .FirstOrDefault() ?? IPAddress.None))
+ .Where(x => x != IPAddress.None)
+ .ToHashSet();
+ interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
+
+ if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
+ {
+ interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
+ }
+
+ if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
+ {
+ interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
+ }
+ }
+
+ // Remove all interfaces matching any virtual machine interface prefix
+ if (config.IgnoreVirtualInterfaces)
+ {
+ // Remove potentially existing * and split config string into prefixes
+ var virtualInterfacePrefixes = config.VirtualInterfaceNames
+ .Select(i => i.Replace("*", string.Empty, StringComparison.OrdinalIgnoreCase));
+
+ // Check all interfaces for matches against the prefixes and remove them
+ if (_interfaces.Count > 0)
+ {
+ foreach (var virtualInterfacePrefix in virtualInterfacePrefixes)
+ {
+ interfaces.RemoveAll(x => x.Name.StartsWith(virtualInterfacePrefix, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+ }
+
+ // Remove all IPv4 interfaces if IPv4 is disabled
+ if (!IsIPv4Enabled)
+ {
+ interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetwork);
+ }
+
+ // Remove all IPv6 interfaces if IPv6 is disabled
+ if (!IsIPv6Enabled)
+ {
+ interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
+ }
+
+ _interfaces = interfaces;
+ }
+ }
+
+ ///
+ /// Initializes the remote address values.
+ ///
+ private void InitializeRemote(NetworkConfiguration config)
+ {
+ lock (_initLock)
+ {
+ // Parse config values into filter collection
+ var remoteIPFilter = config.RemoteIPFilter;
+ if (remoteIPFilter.Any() && !string.IsNullOrWhiteSpace(remoteIPFilter.First()))
+ {
+ // Parse all IPs with netmask to a subnet
+ var remoteAddressFilter = new List();
+ var remoteFilteredSubnets = remoteIPFilter.Where(x => x.Contains('/', StringComparison.OrdinalIgnoreCase)).ToArray();
+ if (NetworkExtensions.TryParseToSubnets(remoteFilteredSubnets, out var remoteAddressFilterResult, false))
+ {
+ remoteAddressFilter = remoteAddressFilterResult.ToList();
+ }
+
+ // Parse everything else as an IP and construct subnet with a single IP
+ var remoteFilteredIPs = remoteIPFilter.Where(x => !x.Contains('/', StringComparison.OrdinalIgnoreCase));
+ foreach (var ip in remoteFilteredIPs)
+ {
+ if (IPAddress.TryParse(ip, out var ipp))
+ {
+ remoteAddressFilter.Add(new IPNetwork(ipp, ipp.AddressFamily == AddressFamily.InterNetwork ? Network.MinimumIPv4PrefixSize : Network.MinimumIPv6PrefixSize));
+ }
+ }
+
+ _remoteAddressFilter = remoteAddressFilter;
+ }
+ }
+ }
+
+ ///
+ /// Parses the user defined overrides into the dictionary object.
+ /// Overrides are the equivalent of localised publishedServerUrl, enabling
+ /// different addresses to be advertised over different subnets.
+ /// format is subnet=ipaddress|host|uri
+ /// when subnet = 0.0.0.0, any external address matches.
+ ///
+ private void InitializeOverrides(NetworkConfiguration config)
+ {
+ lock (_initLock)
+ {
+ var publishedServerUrls = new List();
+
+ // Prefer startup configuration.
+ var startupOverrideKey = _startupConfig[AddressOverrideKey];
+ if (!string.IsNullOrEmpty(startupOverrideKey))
+ {
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ startupOverrideKey,
+ true,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ startupOverrideKey,
+ true,
+ true));
+ _publishedServerUrls = publishedServerUrls;
+ return;
+ }
+
+ var overrides = config.PublishedServerUriBySubnet;
+ foreach (var entry in overrides)
+ {
+ var parts = entry.Split('=');
+ if (parts.Length != 2)
+ {
+ _logger.LogError("Unable to parse bind override: {Entry}", entry);
+ return;
+ }
+
+ var replacement = parts[1].Trim();
+ var identifier = parts[0];
+ if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
+ {
+ // Drop any other overrides in case an "all" override exists
+ publishedServerUrls.Clear();
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ replacement,
+ true,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ replacement,
+ true,
+ true));
+ break;
+ }
+ else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
+ {
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ replacement,
+ false,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ replacement,
+ false,
+ true));
+ }
+ else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
+ {
+ foreach (var lan in _lanSubnets)
+ {
+ var lanPrefix = lan.Prefix;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
+ replacement,
+ true,
+ false));
+ }
+ }
+ else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
+ {
+ var data = new IPData(result.Prefix, result);
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ data,
+ replacement,
+ true,
+ true));
+ }
+ else if (TryParseInterface(identifier, out var ifaces))
+ {
+ foreach (var iface in ifaces)
+ {
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ iface,
+ replacement,
+ true,
+ true));
+ }
+ }
+ else
+ {
+ _logger.LogError("Unable to parse bind override: {Entry}", entry);
+ }
+ }
+
+ _publishedServerUrls = publishedServerUrls;
+ }
+ }
+
+ private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
+ {
+ if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal))
+ {
+ UpdateSettings((NetworkConfiguration)evt.NewConfiguration);
+ }
+ }
+
+ ///
+ /// Reloads all settings and re-Initializes the instance.
///
/// The to use.
public void UpdateSettings(object configuration)
{
- NetworkConfiguration config = (NetworkConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration));
+ ArgumentNullException.ThrowIfNull(configuration);
- IsIP4Enabled = Socket.OSSupportsIPv4 && config.EnableIPV4;
- IsIP6Enabled = Socket.OSSupportsIPv6 && config.EnableIPV6;
- HappyEyeballs.HttpClientExtension.UseIPv6 = IsIP6Enabled;
+ var config = (NetworkConfiguration)configuration;
+ HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
- if (!IsIP6Enabled && !IsIP4Enabled)
- {
- _logger.LogError("IPv4 and IPv6 cannot both be disabled.");
- IsIP4Enabled = true;
- }
-
- TrustAllIP6Interfaces = config.TrustAllIP6Interfaces;
+ InitializeLan(config);
+ InitializeRemote(config);
if (string.IsNullOrEmpty(MockNetworkSettings))
{
- InitialiseInterfaces();
+ InitializeInterfaces();
}
else // Used in testing only.
{
// Format is ,,: . Set index to -ve to simulate a gateway.
var interfaceList = MockNetworkSettings.Split('|');
+ var interfaces = new List();
foreach (var details in interfaceList)
{
var parts = details.Split(',');
- var address = IPNetAddress.Parse(parts[0]);
- var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
- address.Tag = index;
- _interfaceAddresses.AddItem(address, false);
- _interfaceNames[parts[2]] = Math.Abs(index);
+ if (NetworkExtensions.TryParseToSubnet(parts[0], out var subnet))
+ {
+ var address = subnet.Prefix;
+ var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
+ if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ var data = new IPData(address, subnet, parts[2])
+ {
+ Index = index
+ };
+ interfaces.Add(data);
+ }
+ }
+ else
+ {
+ _logger.LogWarning("Could not parse mock interface settings: {Part}", details);
+ }
}
+
+ _interfaces = interfaces;
}
- InitialiseLAN(config);
- InitialiseBind(config);
- InitialiseRemote(config);
- InitialiseOverrides(config);
+ EnforceBindSettings(config);
+ InitializeOverrides(config);
+
+ PrintNetworkInformation(config, false);
}
///
@@ -646,562 +655,341 @@ namespace Jellyfin.Networking.Manager
}
}
- ///
- /// Tries to identify the string and return an object of that class.
- ///
- /// String to parse.
- /// IPObject to return.
- /// true if the value parsed successfully, false otherwise.
- private static bool TryParse(string addr, out IPObject result)
+ ///
+ public bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList? result)
{
- if (!string.IsNullOrEmpty(addr))
+ if (string.IsNullOrEmpty(intf)
+ || _interfaces is null
+ || _interfaces.Count == 0)
{
- // Is it an IP address
- if (IPNetAddress.TryParse(addr, out IPNetAddress nw))
+ result = null;
+ return false;
+ }
+
+ // Match all interfaces starting with names starting with token
+ result = _interfaces
+ .Where(i => i.Name.Equals(intf, StringComparison.OrdinalIgnoreCase)
+ && ((IsIPv4Enabled && i.Address.AddressFamily == AddressFamily.InterNetwork)
+ || (IsIPv6Enabled && i.Address.AddressFamily == AddressFamily.InterNetworkV6)))
+ .OrderBy(x => x.Index)
+ .ToArray();
+ return result.Count > 0;
+ }
+
+ ///
+ public bool HasRemoteAccess(IPAddress remoteIP)
+ {
+ var config = _configurationManager.GetNetworkConfiguration();
+ if (config.EnableRemoteAccess)
+ {
+ // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely.
+ // If left blank, all remote addresses will be allowed.
+ if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP)))
{
- result = nw;
- return true;
+ // remoteAddressFilter is a whitelist or blacklist.
+ var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP));
+ if ((!config.IsRemoteIPFilterBlacklist && matches > 0)
+ || (config.IsRemoteIPFilterBlacklist && matches == 0))
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+ else if (!_lanSubnets.Any(x => x.Contains(remoteIP)))
+ {
+ // Remote not enabled. So everyone should be LAN.
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ public IReadOnlyList GetMacAddresses()
+ {
+ // Populated in construction - so always has values.
+ return _macAddresses;
+ }
+
+ ///
+ public IReadOnlyList GetLoopbacks()
+ {
+ if (!IsIPv4Enabled && !IsIPv6Enabled)
+ {
+ return Array.Empty();
+ }
+
+ var loopbackNetworks = new List();
+ if (IsIPv4Enabled)
+ {
+ loopbackNetworks.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
+ }
+
+ if (IsIPv6Enabled)
+ {
+ loopbackNetworks.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
+ }
+
+ return loopbackNetworks;
+ }
+
+ ///
+ public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false)
+ {
+ if (_interfaces.Count > 0 || individualInterfaces)
+ {
+ return _interfaces;
+ }
+
+ // No bind address and no exclusions, so listen on all interfaces.
+ var result = new List();
+ if (IsIPv4Enabled && IsIPv6Enabled)
+ {
+ // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
+ result.Add(new IPData(IPAddress.IPv6Any, Network.IPv6Any));
+ }
+ else if (IsIPv4Enabled)
+ {
+ result.Add(new IPData(IPAddress.Any, Network.IPv4Any));
+ }
+ else if (IsIPv6Enabled)
+ {
+ // Cannot use IPv6Any as Kestrel will bind to IPv4 addresses too.
+ foreach (var iface in _interfaces)
+ {
+ if (iface.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ result.Add(iface);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ public string GetBindAddress(string source, out int? port)
+ {
+ if (!NetworkExtensions.TryParseHost(source, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
+ {
+ addresses = Array.Empty();
+ }
+
+ var result = GetBindAddress(addresses.FirstOrDefault(), out port);
+ return result;
+ }
+
+ ///
+ public string GetBindAddress(HttpRequest source, out int? port)
+ {
+ var result = GetBindAddress(source.Host.Host, out port);
+ port ??= source.Host.Port;
+
+ return result;
+ }
+
+ ///
+ public string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false)
+ {
+ port = null;
+
+ string result;
+
+ if (source is not null)
+ {
+ if (IsIPv4Enabled && !IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetworkV6)
+ {
+ _logger.LogWarning("IPv6 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
}
- if (IPHost.TryParse(addr, out IPHost h))
+ if (!IsIPv4Enabled && IsIPv6Enabled && source.AddressFamily == AddressFamily.InterNetwork)
{
- result = h;
+ _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected.");
+ }
+
+ bool isExternal = !_lanSubnets.Any(network => network.Contains(source));
+ _logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal);
+
+ if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result))
+ {
+ return result;
+ }
+
+ // No preference given, so move on to bind addresses.
+ if (MatchesBindInterface(source, isExternal, out result))
+ {
+ return result;
+ }
+
+ if (isExternal && MatchesExternalInterface(source, out result))
+ {
+ return result;
+ }
+ }
+
+ // Get the first LAN interface address that's not excluded and not a loopback address.
+ // Get all available interfaces, prefer local interfaces
+ var availableInterfaces = _interfaces.Where(x => !IPAddress.IsLoopback(x.Address))
+ .OrderByDescending(x => IsInLocalNetwork(x.Address))
+ .ThenBy(x => x.Index)
+ .ToList();
+
+ if (availableInterfaces.Count == 0)
+ {
+ // There isn't any others, so we'll use the loopback.
+ result = IsIPv4Enabled && !IsIPv6Enabled ? "127.0.0.1" : "::1";
+ _logger.LogWarning("{Source}: Only loopback {Result} returned, using that as bind address.", source, result);
+ return result;
+ }
+
+ // If no source address is given, use the preferred (first) interface
+ if (source is null)
+ {
+ result = NetworkExtensions.FormatIPString(availableInterfaces.First().Address);
+ _logger.LogDebug("{Source}: Using first internal interface as bind address: {Result}", source, result);
+ return result;
+ }
+
+ // Does the request originate in one of the interface subnets?
+ // (For systems with multiple internal network cards, and multiple subnets)
+ foreach (var intf in availableInterfaces)
+ {
+ if (intf.Subnet.Contains(source))
+ {
+ result = NetworkExtensions.FormatIPString(intf.Address);
+ _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result);
+ return result;
+ }
+ }
+
+ // Fallback to first available interface
+ result = NetworkExtensions.FormatIPString(availableInterfaces[0].Address);
+ _logger.LogDebug("{Source}: No matching interfaces found, using preferred interface as bind address: {Result}", source, result);
+ return result;
+ }
+
+ ///
+ public IReadOnlyList GetInternalBindAddresses()
+ {
+ // Select all local bind addresses
+ return _interfaces.Where(x => IsInLocalNetwork(x.Address))
+ .OrderBy(x => x.Index)
+ .ToList();
+ }
+
+ ///
+ public bool IsInLocalNetwork(string address)
+ {
+ if (NetworkExtensions.TryParseToSubnet(address, out var subnet))
+ {
+ return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix)));
+ }
+
+ if (NetworkExtensions.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled))
+ {
+ foreach (var ept in addresses)
+ {
+ if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept))))
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ public bool IsInLocalNetwork(IPAddress address)
+ {
+ ArgumentNullException.ThrowIfNull(address);
+
+ // See conversation at https://github.com/jellyfin/jellyfin/pull/3515.
+ if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6)
+ || address.Equals(IPAddress.Loopback)
+ || address.Equals(IPAddress.IPv6Loopback))
+ {
+ return true;
+ }
+
+ // As private addresses can be redefined by Configuration.LocalNetworkAddresses
+ return CheckIfLanAndNotExcluded(address);
+ }
+
+ private bool CheckIfLanAndNotExcluded(IPAddress address)
+ {
+ foreach (var lanSubnet in _lanSubnets)
+ {
+ if (lanSubnet.Contains(address))
+ {
+ foreach (var excludedSubnet in _excludedSubnets)
+ {
+ if (excludedSubnet.Contains(address))
+ {
+ return false;
+ }
+ }
+
return true;
}
}
- result = IPNetAddress.None;
return false;
}
///
- /// Converts an IPAddress into a string.
- /// Ipv6 addresses are returned in [ ], with their scope removed.
+ /// Attempts to match the source against the published server URL overrides.
///
- /// Address to convert.
- /// URI safe conversion of the address.
- private static string FormatIP6String(IPAddress address)
+ /// IP source address to use.
+ /// True if the source is in an external subnet.
+ /// The published server URL that matches the source address.
+ /// true if a match is found, false otherwise.
+ private bool MatchesPublishedServerUrl(IPAddress source, bool isInExternalSubnet, out string bindPreference)
{
- var str = address.ToString();
- if (address.AddressFamily == AddressFamily.InterNetworkV6)
+ bindPreference = string.Empty;
+ int? port = null;
+
+ // Only consider subnets including the source IP, prefering specific overrides
+ List validPublishedServerUrls;
+ if (!isInExternalSubnet)
{
- int i = str.IndexOf("%", StringComparison.OrdinalIgnoreCase);
- if (i != -1)
- {
- str = str.Substring(0, i);
- }
-
- return $"[{str}]";
- }
-
- return str;
- }
-
- private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
- {
- if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal))
- {
- UpdateSettings((NetworkConfiguration)evt.NewConfiguration);
- }
- }
-
- ///
- /// Checks the string to see if it matches any interface names.
- ///
- /// String to check.
- /// Interface index numbers that match.
- /// true if an interface name matches the token, False otherwise.
- private bool TryGetInterfaces(string token, [NotNullWhen(true)] out List? index)
- {
- index = null;
-
- // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
- // Null check required here for automated testing.
- if (_interfaceNames is not null && token.Length > 1)
- {
- bool partial = token[^1] == '*';
- if (partial)
- {
- token = token[..^1];
- }
-
- foreach ((string interfc, int interfcIndex) in _interfaceNames)
- {
- if ((!partial && string.Equals(interfc, token, StringComparison.OrdinalIgnoreCase))
- || (partial && interfc.StartsWith(token, true, CultureInfo.InvariantCulture)))
- {
- index ??= new List();
- index.Add(interfcIndex);
- }
- }
- }
-
- return index is not null;
- }
-
- ///
- /// Parses a string and adds it into the collection, replacing any interface references.
- ///
- /// Collection.
- /// String value to parse.
- private void AddToCollection(Collection col, string token)
- {
- // Is it the name of an interface (windows) eg, Wireless LAN adapter Wireless Network Connection 1.
- // Null check required here for automated testing.
- if (TryGetInterfaces(token, out var indices))
- {
- _logger.LogInformation("Interface {Token} used in settings. Using its interface addresses.", token);
-
- // Replace all the interface tags with the interface IP's.
- foreach (IPNetAddress iface in _interfaceAddresses)
- {
- if (indices.Contains(Math.Abs(iface.Tag))
- && ((IsIP4Enabled && iface.Address.AddressFamily == AddressFamily.InterNetwork)
- || (IsIP6Enabled && iface.Address.AddressFamily == AddressFamily.InterNetworkV6)))
- {
- col.AddItem(iface);
- }
- }
- }
- else if (TryParse(token, out IPObject obj))
- {
- // Expand if the ip address is "any".
- if ((obj.Address.Equals(IPAddress.Any) && IsIP4Enabled)
- || (obj.Address.Equals(IPAddress.IPv6Any) && IsIP6Enabled))
- {
- foreach (IPNetAddress iface in _interfaceAddresses)
- {
- if (obj.AddressFamily == iface.AddressFamily)
- {
- col.AddItem(iface);
- }
- }
- }
- else if (!IsIP6Enabled)
- {
- // Remove IP6 addresses from multi-homed IPHosts.
- obj.Remove(AddressFamily.InterNetworkV6);
- if (!obj.IsIP6())
- {
- col.AddItem(obj);
- }
- }
- else if (!IsIP4Enabled)
- {
- // Remove IP4 addresses from multi-homed IPHosts.
- obj.Remove(AddressFamily.InterNetwork);
- if (obj.IsIP6())
- {
- col.AddItem(obj);
- }
- }
- else
- {
- col.AddItem(obj);
- }
+ // Only use matching internal subnets
+ // Prefer more specific (bigger subnet prefix) overrides
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
+ .OrderByDescending(x => x.Data.Subnet.PrefixLength)
+ .ToList();
}
else
{
- _logger.LogDebug("Invalid or unknown object {Token}.", token);
+ // Only use matching external subnets
+ // Prefer more specific (bigger subnet prefix) overrides
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
+ .OrderByDescending(x => x.Data.Subnet.PrefixLength)
+ .ToList();
}
- }
- ///
- /// Handler for network change events.
- ///
- /// Sender.
- /// A containing network availability information.
- private void OnNetworkAvailabilityChanged(object? sender, NetworkAvailabilityEventArgs e)
- {
- _logger.LogDebug("Network availability changed.");
- OnNetworkChanged();
- }
-
- ///
- /// Handler for network change events.
- ///
- /// Sender.
- /// An .
- private void OnNetworkAddressChanged(object? sender, EventArgs e)
- {
- _logger.LogDebug("Network address change detected.");
- OnNetworkChanged();
- }
-
- ///
- /// Async task that waits for 2 seconds before re-initialising the settings, as typically these events fire multiple times in succession.
- ///
- /// A representing the asynchronous operation.
- private async Task OnNetworkChangeAsync()
- {
- try
+ foreach (var data in validPublishedServerUrls)
{
- await Task.Delay(2000).ConfigureAwait(false);
+ // Get interface matching override subnet
+ var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
- var config = _configurationManager.GetNetworkConfiguration();
- // Have we lost IPv6 capability?
- if (IsIP6Enabled && !Socket.OSSupportsIPv6)
+ if (intf?.Address is not null)
{
- UpdateSettings(config);
- }
- else
- {
- InitialiseInterfaces();
- // Recalculate LAN caches.
- InitialiseLAN(config);
- }
-
- NetworkChanged?.Invoke(this, EventArgs.Empty);
- }
- finally
- {
- _eventfire = false;
- }
- }
-
- ///
- /// Triggers our event, and re-loads interface information.
- ///
- private void OnNetworkChanged()
- {
- lock (_eventFireLock)
- {
- if (!_eventfire)
- {
- _logger.LogDebug("Network Address Change Event.");
- // As network events tend to fire one after the other only fire once every second.
- _eventfire = true;
- OnNetworkChangeAsync().GetAwaiter().GetResult();
- }
- }
- }
-
- ///
- /// Parses the user defined overrides into the dictionary object.
- /// Overrides are the equivalent of localised publishedServerUrl, enabling
- /// different addresses to be advertised over different subnets.
- /// format is subnet=ipaddress|host|uri
- /// when subnet = 0.0.0.0, any external address matches.
- ///
- private void InitialiseOverrides(NetworkConfiguration config)
- {
- lock (_intLock)
- {
- _publishedServerUrls.Clear();
- string[] overrides = config.PublishedServerUriBySubnet;
- if (overrides is null)
- {
- return;
- }
-
- foreach (var entry in overrides)
- {
- var parts = entry.Split('=');
- if (parts.Length != 2)
- {
- _logger.LogError("Unable to parse bind override: {Entry}", entry);
- }
- else
- {
- var replacement = parts[1].Trim();
- if (string.Equals(parts[0], "all", StringComparison.OrdinalIgnoreCase))
- {
- _publishedServerUrls[new IPNetAddress(IPAddress.Broadcast)] = replacement;
- }
- else if (string.Equals(parts[0], "external", StringComparison.OrdinalIgnoreCase))
- {
- _publishedServerUrls[new IPNetAddress(IPAddress.Any)] = replacement;
- }
- else if (TryParseInterface(parts[0], out Collection? addresses) && addresses is not null)
- {
- foreach (IPNetAddress na in addresses)
- {
- _publishedServerUrls[na] = replacement;
- }
- }
- else if (IPNetAddress.TryParse(parts[0], out IPNetAddress result))
- {
- _publishedServerUrls[result] = replacement;
- }
- else
- {
- _logger.LogError("Unable to parse bind ip address. {Parts}", parts[1]);
- }
- }
- }
- }
- }
-
- ///
- /// Initialises the network bind addresses.
- ///
- private void InitialiseBind(NetworkConfiguration config)
- {
- lock (_intLock)
- {
- string[] lanAddresses = config.LocalNetworkAddresses;
-
- // Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
- if (config.IgnoreVirtualInterfaces)
- {
- // each virtual interface name must be prepended with the exclusion symbol !
- var virtualInterfaceNames = config.VirtualInterfaceNames.Split(',').Select(p => "!" + p).ToArray();
- if (lanAddresses.Length > 0)
- {
- var newList = new string[lanAddresses.Length + virtualInterfaceNames.Length];
- Array.Copy(lanAddresses, newList, lanAddresses.Length);
- Array.Copy(virtualInterfaceNames, 0, newList, lanAddresses.Length, virtualInterfaceNames.Length);
- lanAddresses = newList;
- }
- else
- {
- lanAddresses = virtualInterfaceNames;
- }
- }
-
- // Read and parse bind addresses and exclusions, removing ones that don't exist.
- _bindAddresses = CreateIPCollection(lanAddresses).ThatAreContainedInNetworks(_interfaceAddresses);
- _bindExclusions = CreateIPCollection(lanAddresses, true).ThatAreContainedInNetworks(_interfaceAddresses);
- _logger.LogInformation("Using bind addresses: {0}", _bindAddresses.AsString());
- _logger.LogInformation("Using bind exclusions: {0}", _bindExclusions.AsString());
- }
- }
-
- ///
- /// Initialises the remote address values.
- ///
- private void InitialiseRemote(NetworkConfiguration config)
- {
- lock (_intLock)
- {
- RemoteAddressFilter = CreateIPCollection(config.RemoteIPFilter);
- }
- }
-
- ///
- /// Initialises internal LAN cache settings.
- ///
- private void InitialiseLAN(NetworkConfiguration config)
- {
- lock (_intLock)
- {
- _logger.LogDebug("Refreshing LAN information.");
-
- // Get configuration options.
- string[] subnets = config.LocalNetworkSubnets;
-
- // Create lists from user settings.
-
- _lanSubnets = CreateIPCollection(subnets);
- _excludedSubnets = CreateIPCollection(subnets, true).AsNetworks();
-
- // If no LAN addresses are specified - all private subnets are deemed to be the LAN
- _usingPrivateAddresses = _lanSubnets.Count == 0;
-
- // NOTE: The order of the commands generating the collection in this statement matters.
- // Altering the order will cause the collections to be created incorrectly.
- if (_usingPrivateAddresses)
- {
- _logger.LogDebug("Using LAN interface addresses as user provided no LAN details.");
- // Internal interfaces must be private and not excluded.
- _internalInterfaces = CreateCollection(_interfaceAddresses.Where(i => IsPrivateAddressRange(i) && !_excludedSubnets.ContainsAddress(i)));
-
- // Subnets are the same as the calculated internal interface.
- _lanSubnets = new Collection();
-
- if (IsIP6Enabled)
- {
- _lanSubnets.AddItem(IPNetAddress.Parse("fc00::/7")); // ULA
- _lanSubnets.AddItem(IPNetAddress.Parse("fe80::/10")); // Site local
- }
-
- if (IsIP4Enabled)
- {
- _lanSubnets.AddItem(IPNetAddress.Parse("10.0.0.0/8"));
- _lanSubnets.AddItem(IPNetAddress.Parse("172.16.0.0/12"));
- _lanSubnets.AddItem(IPNetAddress.Parse("192.168.0.0/16"));
- }
- }
- else
- {
- // Internal interfaces must be private, not excluded and part of the LocalNetworkSubnet.
- _internalInterfaces = CreateCollection(_interfaceAddresses.Where(IsInLocalNetwork));
- }
-
- _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.AsString());
- _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.AsString());
- _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Exclude(_excludedSubnets, true).AsNetworks().AsString());
- }
- }
-
- ///
- /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
- /// Generate a list of all active mac addresses that aren't loopback addresses.
- ///
- private void InitialiseInterfaces()
- {
- lock (_intLock)
- {
- _logger.LogDebug("Refreshing interfaces.");
-
- _interfaceNames.Clear();
- _interfaceAddresses.Clear();
- _macAddresses.Clear();
-
- try
- {
- IEnumerable nics = NetworkInterface.GetAllNetworkInterfaces()
- .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up);
-
- foreach (NetworkInterface adapter in nics)
- {
- try
- {
- IPInterfaceProperties ipProperties = adapter.GetIPProperties();
- PhysicalAddress mac = adapter.GetPhysicalAddress();
-
- // populate mac list
- if (adapter.NetworkInterfaceType != NetworkInterfaceType.Loopback && mac is not null && mac != PhysicalAddress.None)
- {
- _macAddresses.Add(mac);
- }
-
- // populate interface address list
- foreach (UnicastIPAddressInformation info in ipProperties.UnicastAddresses)
- {
- if (IsIP4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
- {
- IPNetAddress nw = new IPNetAddress(info.Address, IPObject.MaskToCidr(info.IPv4Mask))
- {
- // Keep the number of gateways on this interface, along with its index.
- Tag = ipProperties.GetIPv4Properties().Index
- };
-
- int tag = nw.Tag;
- if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback())
- {
- // -ve Tags signify the interface has a gateway.
- nw.Tag *= -1;
- }
-
- _interfaceAddresses.AddItem(nw, false);
-
- // Store interface name so we can use the name in Collections.
- _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
- _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag;
- }
- else if (IsIP6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
- {
- IPNetAddress nw = new IPNetAddress(info.Address, (byte)info.PrefixLength)
- {
- // Keep the number of gateways on this interface, along with its index.
- Tag = ipProperties.GetIPv6Properties().Index
- };
-
- int tag = nw.Tag;
- if (ipProperties.GatewayAddresses.Count > 0 && !nw.IsLoopback())
- {
- // -ve Tags signify the interface has a gateway.
- nw.Tag *= -1;
- }
-
- _interfaceAddresses.AddItem(nw, false);
-
- // Store interface name so we can use the name in Collections.
- _interfaceNames[adapter.Description.ToLower(CultureInfo.InvariantCulture)] = tag;
- _interfaceNames["eth" + tag.ToString(CultureInfo.InvariantCulture)] = tag;
- }
- }
- }
-#pragma warning disable CA1031 // Do not catch general exception types
- catch (Exception ex)
- {
- // Ignore error, and attempt to continue.
- _logger.LogError(ex, "Error encountered parsing interfaces.");
- }
-#pragma warning restore CA1031 // Do not catch general exception types
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error in InitialiseInterfaces.");
- }
-
- // If for some reason we don't have an interface info, resolve our DNS name.
- if (_interfaceAddresses.Count == 0)
- {
- _logger.LogError("No interfaces information available. Resolving DNS name.");
- IPHost host = new IPHost(Dns.GetHostName());
- foreach (var a in host.GetAddresses())
- {
- _interfaceAddresses.AddItem(a);
- }
-
- if (_interfaceAddresses.Count == 0)
- {
- _logger.LogWarning("No interfaces information available. Using loopback.");
- }
- }
-
- if (IsIP4Enabled)
- {
- _interfaceAddresses.AddItem(IPNetAddress.IP4Loopback);
- }
-
- if (IsIP6Enabled)
- {
- _interfaceAddresses.AddItem(IPNetAddress.IP6Loopback);
- }
-
- _logger.LogDebug("Discovered {0} interfaces.", _interfaceAddresses.Count);
- _logger.LogDebug("Interfaces addresses: {0}", _interfaceAddresses.AsString());
- }
- }
-
- ///
- /// Attempts to match the source against a user defined bind interface.
- ///
- /// IP source address to use.
- /// True if the source is in the external subnet.
- /// The published server url that matches the source address.
- /// The resultant port, if one exists.
- /// true if a match is found, false otherwise.
- private bool MatchesPublishedServerUrl(IPObject source, bool isInExternalSubnet, out string bindPreference, out int? port)
- {
- bindPreference = string.Empty;
- port = null;
-
- // Check for user override.
- foreach (var addr in _publishedServerUrls)
- {
- // Remaining. Match anything.
- if (addr.Key.Address.Equals(IPAddress.Broadcast))
- {
- bindPreference = addr.Value;
- break;
- }
-
- if ((addr.Key.Address.Equals(IPAddress.Any) || addr.Key.Address.Equals(IPAddress.IPv6Any)) && isInExternalSubnet)
- {
- // External.
- bindPreference = addr.Value;
- break;
- }
-
- if (addr.Key.Contains(source))
- {
- // Match ip address.
- bindPreference = addr.Value;
+ // If matching interface is found, use override
+ bindPreference = data.OverrideUri;
break;
}
}
if (string.IsNullOrEmpty(bindPreference))
{
+ _logger.LogDebug("{Source}: No matching bind address override found", source);
return false;
}
- // Has it got a port defined?
+ // Handle override specifying port
var parts = bindPreference.Split(':');
if (parts.Length > 1)
{
@@ -1209,132 +997,131 @@ namespace Jellyfin.Networking.Manager
{
bindPreference = parts[0];
port = p;
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
+ return true;
}
}
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
return true;
}
///
- /// Attempts to match the source against a user defined bind interface.
+ /// Attempts to match the source against the user defined bind interfaces.
///
/// IP source address to use.
/// True if the source is in the external subnet.
/// The result, if a match is found.
/// true if a match is found, false otherwise.
- private bool MatchesBindInterface(IPObject source, bool isInExternalSubnet, out string result)
+ private bool MatchesBindInterface(IPAddress source, bool isInExternalSubnet, out string result)
{
result = string.Empty;
- var addresses = _bindAddresses.Exclude(_bindExclusions, false);
- int count = addresses.Count;
- if (count == 1 && (_bindAddresses[0].Equals(IPAddress.Any) || _bindAddresses[0].Equals(IPAddress.IPv6Any)))
+ int count = _interfaces.Count;
+ if (count == 1 && (_interfaces[0].Equals(IPAddress.Any) || _interfaces[0].Equals(IPAddress.IPv6Any)))
{
// Ignore IPAny addresses.
count = 0;
}
- if (count != 0)
+ if (count == 0)
{
- // Check to see if any of the bind interfaces are in the same subnet.
+ return false;
+ }
- IPAddress? defaultGateway = null;
- IPAddress? bindAddress = null;
-
- if (isInExternalSubnet)
+ IPAddress? bindAddress = null;
+ if (isInExternalSubnet)
+ {
+ var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address))
+ .OrderBy(x => x.Index)
+ .ToList();
+ if (externalInterfaces.Count > 0)
{
- // Find all external bind addresses. Store the default gateway, but check to see if there is a better match first.
- foreach (var addr in addresses.OrderBy(p => p.Tag))
- {
- if (defaultGateway is null && !IsInLocalNetwork(addr))
- {
- defaultGateway = addr.Address;
- }
+ // Check to see if any of the external bind interfaces are in the same subnet as the source.
+ // If none exists, this will select the first external interface if there is one.
+ bindAddress = externalInterfaces
+ .OrderByDescending(x => x.Subnet.Contains(source))
+ .ThenBy(x => x.Index)
+ .Select(x => x.Address)
+ .First();
- if (bindAddress is null && addr.Contains(source))
- {
- bindAddress = addr.Address;
- }
+ result = NetworkExtensions.FormatIPString(bindAddress);
+ _logger.LogDebug("{Source}: External request received, matching external bind address found: {Result}", source, result);
+ return true;
+ }
- if (defaultGateway is not null && bindAddress is not null)
- {
- break;
- }
- }
- }
- else
- {
- // Look for the best internal address.
- bindAddress = addresses
- .Where(p => IsInLocalNetwork(p) && (p.Contains(source) || p.Equals(IPAddress.None)))
- .MinBy(p => p.Tag)?.Address;
- }
+ _logger.LogWarning("{Source}: External request received, no matching external bind address found, trying internal addresses.", source);
+ }
+ else
+ {
+ // Check to see if any of the internal bind interfaces are in the same subnet as the source.
+ // If none exists, this will select the first internal interface if there is one.
+ bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address))
+ .OrderByDescending(x => x.Subnet.Contains(source))
+ .ThenBy(x => x.Index)
+ .Select(x => x.Address)
+ .FirstOrDefault();
if (bindAddress is not null)
{
- result = FormatIP6String(bindAddress);
- _logger.LogDebug("{Source}: GetBindInterface: Has source, found a match bind interface subnets. {Result}", source, result);
+ result = NetworkExtensions.FormatIPString(bindAddress);
+ _logger.LogDebug("{Source}: Internal request received, matching internal bind address found: {Result}", source, result);
return true;
}
-
- if (isInExternalSubnet && defaultGateway is not null)
- {
- result = FormatIP6String(defaultGateway);
- _logger.LogDebug("{Source}: GetBindInterface: Using first user defined external interface. {Result}", source, result);
- return true;
- }
-
- result = FormatIP6String(addresses[0].Address);
- _logger.LogDebug("{Source}: GetBindInterface: Selected first user defined interface. {Result}", source, result);
-
- if (isInExternalSubnet)
- {
- _logger.LogWarning("{Source}: External request received, however, only an internal interface bind found.", source);
- }
-
- return true;
}
return false;
}
///
- /// Attempts to match the source against an external interface.
+ /// Attempts to match the source against external interfaces.
///
/// IP source address to use.
/// The result, if a match is found.
/// true if a match is found, false otherwise.
- private bool MatchesExternalInterface(IPObject source, out string result)
+ private bool MatchesExternalInterface(IPAddress source, out string result)
{
- result = string.Empty;
- // Get the first WAN interface address that isn't a loopback.
- var extResult = _interfaceAddresses
- .Exclude(_bindExclusions, false)
- .Where(p => !IsInLocalNetwork(p))
- .OrderBy(p => p.Tag)
- .ToList();
+ // Get the first external interface address that isn't a loopback.
+ var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index).ToArray();
- if (extResult.Any())
+ // No external interface found
+ if (extResult.Length == 0)
{
- // Does the request originate in one of the interface subnets?
- // (For systems with multiple internal network cards, and multiple subnets)
- foreach (var intf in extResult)
- {
- if (!IsInLocalNetwork(intf) && intf.Contains(source))
- {
- result = FormatIP6String(intf.Address);
- _logger.LogDebug("{Source}: GetBindInterface: Selected best external on interface on range. {Result}", source, result);
- return true;
- }
- }
-
- result = FormatIP6String(extResult.First().Address);
- _logger.LogDebug("{Source}: GetBindInterface: Selected first external interface. {Result}", source, result);
- return true;
+ result = string.Empty;
+ _logger.LogWarning("{Source}: External request received, but no external interface found. Need to route through internal network.", source);
+ return false;
}
- _logger.LogDebug("{Source}: External request received, but no WAN interface found. Need to route through internal network.", source);
- return false;
+ // Does the request originate in one of the interface subnets?
+ // (For systems with multiple network cards and/or multiple subnets)
+ foreach (var intf in extResult)
+ {
+ if (intf.Subnet.Contains(source))
+ {
+ result = NetworkExtensions.FormatIPString(intf.Address);
+ _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result);
+ return true;
+ }
+ }
+
+ // Fallback to first external interface.
+ result = NetworkExtensions.FormatIPString(extResult[0].Address);
+ _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
+ return true;
+ }
+
+ private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
+ {
+ var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
+ if (_logger.IsEnabled(logLevel))
+ {
+ _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
+ _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
+ _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
+ }
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs
index f899b4497..b5f18d983 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationFailedLogger.cs
@@ -2,9 +2,8 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
-using Jellyfin.Data.Events;
using MediaBrowser.Controller.Events;
-using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.Events.Authentication;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Globalization;
using Microsoft.Extensions.Logging;
@@ -14,7 +13,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
///
/// Creates an entry in the activity log when there is a failed login attempt.
///
- public class AuthenticationFailedLogger : IEventConsumer>
+ public class AuthenticationFailedLogger : IEventConsumer
{
private readonly ILocalizationManager _localizationManager;
private readonly IActivityManager _activityManager;
@@ -31,13 +30,13 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
}
///
- public async Task OnEvent(GenericEventArgs eventArgs)
+ public async Task OnEvent(AuthenticationRequestEventArgs eventArgs)
{
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localizationManager.GetLocalizedString("FailedLoginAttemptWithUserName"),
- eventArgs.Argument.Username),
+ eventArgs.Username),
"AuthenticationFailed",
Guid.Empty)
{
@@ -45,7 +44,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localizationManager.GetLocalizedString("LabelIpAddressValue"),
- eventArgs.Argument.RemoteEndPoint),
+ eventArgs.RemoteEndPoint),
}).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
index 8b0bd84c6..3f3a0dec5 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
@@ -1,9 +1,8 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
-using Jellyfin.Data.Events;
-using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Authentication;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Globalization;
@@ -12,7 +11,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
///
/// Creates an entry in the activity log when there is a successful login attempt.
///
- public class AuthenticationSucceededLogger : IEventConsumer>
+ public class AuthenticationSucceededLogger : IEventConsumer
{
private readonly ILocalizationManager _localizationManager;
private readonly IActivityManager _activityManager;
@@ -29,20 +28,20 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
}
///
- public async Task OnEvent(GenericEventArgs eventArgs)
+ public async Task OnEvent(AuthenticationResultEventArgs eventArgs)
{
await _activityManager.CreateAsync(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"),
- eventArgs.Argument.User.Name),
+ eventArgs.User.Name),
"AuthenticationSucceeded",
- eventArgs.Argument.User.Id)
+ eventArgs.User.Id)
{
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localizationManager.GetLocalizedString("LabelIpAddressValue"),
- eventArgs.Argument.SessionInfo.RemoteEndPoint),
+ eventArgs.SessionInfo?.RemoteEndPoint),
}).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
index aeb62e814..27726a57a 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
@@ -58,15 +58,18 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
var user = eventArgs.Users[0];
await _activityManager.CreateAsync(new ActivityLog(
- string.Format(
- CultureInfo.InvariantCulture,
- _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"),
- user.Username,
- GetItemName(eventArgs.MediaInfo),
- eventArgs.DeviceName),
- GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType),
- user.Id))
- .ConfigureAwait(false);
+ string.Format(
+ CultureInfo.InvariantCulture,
+ _localizationManager.GetLocalizedString("UserStartedPlayingItemWithValues"),
+ user.Username,
+ GetItemName(eventArgs.MediaInfo),
+ eventArgs.DeviceName),
+ GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType),
+ user.Id)
+ {
+ ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture),
+ })
+ .ConfigureAwait(false);
}
private static string GetItemName(BaseItemDto item)
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
index dd7290fb8..6b16477aa 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
@@ -73,7 +73,10 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
GetItemName(item),
eventArgs.DeviceName),
notificationType,
- user.Id))
+ user.Id)
+ {
+ ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture),
+ })
.ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
index 5d558189b..9626817e9 100644
--- a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
@@ -1,5 +1,4 @@
-using Jellyfin.Data.Events;
-using Jellyfin.Data.Events.System;
+using Jellyfin.Data.Events.System;
using Jellyfin.Data.Events.Users;
using Jellyfin.Server.Implementations.Events.Consumers.Library;
using Jellyfin.Server.Implementations.Events.Consumers.Security;
@@ -8,12 +7,11 @@ using Jellyfin.Server.Implementations.Events.Consumers.System;
using Jellyfin.Server.Implementations.Events.Consumers.Updates;
using Jellyfin.Server.Implementations.Events.Consumers.Users;
using MediaBrowser.Common.Updates;
-using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Events;
+using MediaBrowser.Controller.Events.Authentication;
using MediaBrowser.Controller.Events.Session;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.DependencyInjection;
@@ -35,8 +33,8 @@ namespace Jellyfin.Server.Implementations.Events
collection.AddScoped