Merge branch 'master' into bug/authorization-header-issue

pull/4799/head
cvium 3 years ago
commit 048c478b0d

@ -7,7 +7,7 @@ parameters:
default: "ubuntu-latest"
- name: DotNetSdkVersion
type: string
default: 5.0.103
default: 5.0.302
jobs:
- job: CompatibilityCheck

@ -1,7 +1,7 @@
parameters:
LinuxImage: 'ubuntu-latest'
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
DotNetSdkVersion: 5.0.103
DotNetSdkVersion: 5.0.302
jobs:
- job: Build

@ -10,7 +10,7 @@ parameters:
default: "tests/**/*Tests.csproj"
- name: DotNetSdkVersion
type: string
default: 5.0.103
default: 5.0.302
jobs:
- job: Test

@ -6,7 +6,7 @@ variables:
- name: RestoreBuildProjects
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
- name: DotNetSdkVersion
value: 5.0.103
value: 5.0.302
pr:
autoCancel: true

@ -17,6 +17,7 @@ assignees: ''
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
- Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
- Base URL: [e.g. none, yes: /example]

@ -1,43 +0,0 @@
comment:
header: Hello!
footer: "\
---\n\n
> This is an automated comment created by the [peaceiris/actions-label-commenter]. \
Responding to the bot or mentioning it won't have any effect.\n\n
[peaceiris/actions-label-commenter]: https://github.com/peaceiris/actions-label-commenter
"
labels:
- name: stable backport
labeled:
pr:
body: |
This pull request has been tagged as a stable backport. It will be cherry-picked into the next stable point release.
Please observe the following:
* Any dependent PRs that this PR requires **must** be tagged for stable backporting as well.
* Any issue(s) this PR fixes or closes **should** target the current stable release or a previous stable release to which a fix has not yet entered the current stable release.
* This PR **must** be test cherry-picked against the current release branch (`release-X.Y.z` where X and Y are numbers). It must apply cleanly, or a diff of the expected change must be provided.
To do this, run the following commands from your local copy of the Jellyfin repository:
1. `git checkout master`
1. `git merge --no-ff <myPullRequestBranch>`
1. `git log` -> `commit xxxxxxxxx`, grab hash
1. `git checkout release-X.Y.z` replacing X and Y with the *current* stable version (e.g. `release-10.7.z`)
1. `git cherry-pick -sx -m1 <hash>`
Ensure the `cherry-pick` applies cleanly. If it does not, fix any merge conflicts *preserving as much of the original code as possible*, and make note of the resulting diff.
Test your changes with a build to ensure they are successful. If not, adjust the diff accordingly.
**Do not** push your merges to either branch. Use `git reset --hard HEAD~1` to revert both branches to their original state.
Reply to this PR with a comment beginning "Cherry-pick test completed." and including the merge-conflict-fixing diff(s) if applicable.

6
.github/stale.yml vendored

@ -17,9 +17,13 @@ staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
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.
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, 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).
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
# Disable automatic closing of pull requests
pulls:
daysUntilClose: false

@ -1,21 +1,33 @@
name: Automation
on:
push:
branches:
- master
pull_request_target:
issue_comment:
jobs:
main:
label:
name: Labeling
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Does PR has the stable backport label?
uses: Dreamcodeio/does-pr-has-label@v1.2
id: checkLabel
- name: Apply label
uses: eps1lon/actions-label-merge-conflict@v2.0.1
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
label: stable backport
dirtyLabel: 'merge conflict'
repoToken: ${{ secrets.JF_BOT_TOKEN }}
project:
name: Project board
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Remove from 'Current Release' project
uses: alex-page/github-project-automation-plus@v0.7.1
if: (github.event.pull_request || github.event.issue.pull_request) && !steps.checkLabel.outputs.hasLabel
uses: alex-page/github-project-automation-plus@v0.8.1
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
project: Current Release
@ -23,7 +35,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project
uses: alex-page/github-project-automation-plus@v0.7.1
uses: alex-page/github-project-automation-plus@v0.8.1
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true
with:
@ -32,8 +44,8 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project
uses: alex-page/github-project-automation-plus@v0.7.1
if: (github.event.pull_request || github.event.issue.pull_request) && steps.checkLabel.outputs.hasLabel
uses: alex-page/github-project-automation-plus@v0.8.1
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true
with:
project: Current Release
@ -46,7 +58,7 @@ jobs:
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
- name: Move issue to needs triage
uses: alex-page/github-project-automation-plus@v0.7.1
uses: alex-page/github-project-automation-plus@v0.8.1
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true
with:
@ -55,7 +67,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project
uses: alex-page/github-project-automation-plus@v0.7.1
uses: alex-page/github-project-automation-plus@v0.8.1
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true
with:

@ -1,4 +1,4 @@
name: Stable Backport Check
name: Commands
on:
issue_comment:
types:
@ -10,6 +10,29 @@ on:
- synchronize
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@v1.4.5
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.4
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
check-backport:
name: Check Backport
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}

@ -1,24 +0,0 @@
name: Label Commenter
on:
issues:
types:
- labeled
- unlabeled
pull_request_target:
types:
- labeled
- unlabeled
jobs:
comment:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
with:
ref: master
- name: Label Commenter
uses: peaceiris/actions-label-commenter@v1
with:
github_token: ${{ secrets.JF_BOT_TOKEN }}

@ -1,17 +0,0 @@
name: 'Merge Conflicts'
on:
push:
branches:
- master
pull_request_target:
types:
- synchronize
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: eps1lon/actions-label-merge-conflict@v2.0.1
with:
dirtyLabel: 'merge conflict'
repoToken: ${{ secrets.JF_BOT_TOKEN }}

@ -1,30 +0,0 @@
name: Automatic Rebase
on:
issue_comment:
types:
- created
- edited
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@v1.4.5
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.4
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}

1
.gitignore vendored

@ -268,6 +268,7 @@ doc/
# Deployment artifacts
dist
*.exe
*.dll
# BenchmarkDotNet artifacts
BenchmarkDotNet.Artifacts

@ -212,3 +212,5 @@
- [Tim Hobbs](https://github.com/timhobbs)
- [SvenVandenbrande](https://github.com/SvenVandenbrande)
- [olsh](https://github.com/olsh)
- [lbenini](https://github.com/lbenini)
- [gnuyent](https://github.com/gnuyent)

@ -0,0 +1,14 @@
<Project>
<!-- Sets defaults for all projects in the repo -->
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
</PropertyGroup>
</Project>

@ -8,15 +8,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
FROM debian:buster-slim
FROM debian:bullseye-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@ -25,9 +17,6 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
# https://github.com/intel/compute-runtime/releases
ARG GMMLIB_VERSION=20.3.2
ARG IGC_VERSION=1.0.5435
@ -73,6 +62,19 @@ ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# because of changes in docker and systemd we need to not build in parallel at the moment
# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:DebugSymbols=false;DebugType=none"
FROM app
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \

@ -13,19 +13,8 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-arm as qemu
FROM arm32v7/debian:buster-slim
FROM arm32v7/debian:bullseye-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@ -61,14 +50,25 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:DebugSymbols=false;DebugType=none"
FROM app
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \

@ -13,18 +13,8 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& npm ci --no-audit --unsafe-perm \
&& mv dist /dist
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM arm64v8/debian:buster-slim
FROM arm64v8/debian:bullseye-slim as app
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive"
@ -50,14 +40,25 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge
&& chmod 777 /cache /config /media \
&& sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
WORKDIR /repo
COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
# Discard objs - may cause failures if exists
RUN find . -type d -name obj | xargs -r rm -r
# Build
RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:DebugSymbols=false;DebugType=none"
FROM app
COPY --from=builder /jellyfin /jellyfin
COPY --from=web-builder /dist /jellyfin/jellyfin-web
EXPOSE 8096
VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \

@ -13,7 +13,8 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
<Nullable>disable</Nullable>
</PropertyGroup>
</Project>

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
namespace Emby.Dlna.Configuration
@ -74,7 +72,7 @@ namespace Emby.Dlna.Configuration
/// <summary>
/// Gets or sets the default user account that the dlna server uses.
/// </summary>
public string DefaultUserId { get; set; }
public string? DefaultUserId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether playTo device profiles should be created.

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -140,7 +138,7 @@ namespace Emby.Dlna.ContentDirectory
/// </summary>
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
/// <returns>The <see cref="User"/>.</returns>
private User GetUser(DeviceProfile profile)
private User? GetUser(DeviceProfile profile)
{
if (!string.IsNullOrEmpty(profile.UserId))
{

@ -288,21 +288,14 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The xml feature list.</returns>
private static string WriteFeatureListXml()
{
// TODO: clean this up
var builder = new StringBuilder();
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
builder.Append("<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">");
builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">");
builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>");
builder.Append("<container id=\"A\" type=\"object.item.audioItem\"/>");
builder.Append("<container id=\"V\" type=\"object.item.videoItem\"/>");
builder.Append("</Feature>");
builder.Append("</Features>");
return builder.ToString();
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ "<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">"
+ "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"
+ "<container id=\"I\" type=\"object.item.imageItem\"/>"
+ "<container id=\"A\" type=\"object.item.audioItem\"/>"
+ "<container id=\"V\" type=\"object.item.videoItem\"/>"
+ "</Feature>"
+ "</Features>";
}
/// <summary>

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;
@ -8,9 +6,11 @@ namespace Emby.Dlna
{
public class ControlResponse
{
public ControlResponse()
public ControlResponse(string xml, bool isSuccessful)
{
Headers = new Dictionary<string, string>();
Xml = xml;
IsSuccessful = isSuccessful;
}
public IDictionary<string, string> Headers { get; }

@ -1,7 +1,4 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
@ -14,9 +11,9 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Emby.Dlna.Profiles;
using Emby.Dlna.Server;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@ -96,12 +93,14 @@ namespace Emby.Dlna
}
}
/// <inheritdoc />
public DeviceProfile GetDefaultProfile()
{
return new DefaultProfile();
}
public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
/// <inheritdoc />
public DeviceProfile? GetProfile(DeviceIdentification deviceInfo)
{
if (deviceInfo == null)
{
@ -111,13 +110,13 @@ namespace Emby.Dlna
var profile = GetProfiles()
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
if (profile != null)
if (profile == null)
{
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
LogUnmatchedProfile(deviceInfo);
}
else
{
LogUnmatchedProfile(deviceInfo);
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
}
return profile;
@ -187,7 +186,8 @@ namespace Emby.Dlna
}
}
public DeviceProfile GetProfile(IHeaderDictionary headers)
/// <inheritdoc />
public DeviceProfile? GetProfile(IHeaderDictionary headers)
{
if (headers == null)
{
@ -195,15 +195,13 @@ namespace Emby.Dlna
}
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
if (profile != null)
if (profile == null)
{
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
_logger.LogDebug("No matching device profile found. {@Headers}", headers);
}
else
{
var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
_logger.LogDebug("No matching device profile found. {0}", headerString);
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
}
return profile;
@ -253,19 +251,19 @@ namespace Emby.Dlna
return xmlFies
.Select(i => ParseProfileFile(i, type))
.Where(i => i != null)
.ToList();
.ToList()!; // We just filtered out all the nulls
}
catch (IOException)
{
return new List<DeviceProfile>();
return Array.Empty<DeviceProfile>();
}
}
private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
private DeviceProfile? ParseProfileFile(string path, DeviceProfileType type)
{
lock (_profiles)
{
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile> profileTuple))
if (_profiles.TryGetValue(path, out Tuple<InternalProfileInfo, DeviceProfile>? profileTuple))
{
return profileTuple.Item2;
}
@ -293,7 +291,8 @@ namespace Emby.Dlna
}
}
public DeviceProfile GetProfile(string id)
/// <inheritdoc />
public DeviceProfile? GetProfile(string id)
{
if (string.IsNullOrEmpty(id))
{
@ -322,6 +321,7 @@ namespace Emby.Dlna
}
}
/// <inheritdoc />
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
{
return GetProfileInfosInternal().Select(i => i.Info);
@ -329,17 +329,14 @@ namespace Emby.Dlna
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
{
return new InternalProfileInfo
{
Path = file.FullName,
Info = new DeviceProfileInfo
return new InternalProfileInfo(
new DeviceProfileInfo
{
Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture),
Name = _fileSystem.GetFileNameWithoutExtension(file),
Type = type
}
};
},
file.FullName);
}
private async Task ExtractSystemProfilesAsync()
@ -359,7 +356,8 @@ namespace Emby.Dlna
systemProfilesPath,
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
using (var stream = _assembly.GetManifestResourceStream(name))
// The stream should exist as we just got its name from GetManifestResourceNames
using (var stream = _assembly.GetManifestResourceStream(name)!)
{
var fileInfo = _fileSystem.GetFileInfo(path);
@ -380,6 +378,7 @@ namespace Emby.Dlna
Directory.CreateDirectory(UserProfilesPath);
}
/// <inheritdoc />
public void DeleteProfile(string id)
{
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
@ -397,6 +396,7 @@ namespace Emby.Dlna
}
}
/// <inheritdoc />
public void CreateProfile(DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@ -412,6 +412,7 @@ namespace Emby.Dlna
SaveProfile(profile, path, DeviceProfileType.User);
}
/// <inheritdoc />
public void UpdateProfile(DeviceProfile profile)
{
profile = ReserializeProfile(profile);
@ -470,9 +471,11 @@ namespace Emby.Dlna
var json = JsonSerializer.Serialize(profile, _jsonOptions);
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions);
// Output can't be null if the input isn't null
return JsonSerializer.Deserialize<DeviceProfile>(json, _jsonOptions)!;
}
/// <inheritdoc />
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
{
var profile = GetDefaultProfile();
@ -482,6 +485,7 @@ namespace Emby.Dlna
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
}
/// <inheritdoc />
public ImageStream GetIcon(string filename)
{
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
@ -499,9 +503,15 @@ namespace Emby.Dlna
private class InternalProfileInfo
{
internal DeviceProfileInfo Info { get; set; }
internal InternalProfileInfo(DeviceProfileInfo info, string path)
{
Info = info;
Path = path;
}
internal DeviceProfileInfo Info { get; }
internal string Path { get; set; }
internal string Path { get; }
}
}

@ -20,8 +20,7 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<!-- Code Analyzers-->
@ -31,10 +30,6 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Images\logo120.jpg" />
<EmbeddedResource Include="Images\logo120.png" />

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;
@ -8,8 +6,10 @@ namespace Emby.Dlna
{
public class EventSubscriptionResponse
{
public EventSubscriptionResponse()
public EventSubscriptionResponse(string content, string contentType)
{
Content = content;
ContentType = contentType;
Headers = new Dictionary<string, string>();
}

@ -51,11 +51,7 @@ namespace Emby.Dlna.Eventing
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
}
return new EventSubscriptionResponse
{
Content = string.Empty,
ContentType = "text/plain"
};
return new EventSubscriptionResponse(string.Empty, "text/plain");
}
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
@ -103,20 +99,12 @@ namespace Emby.Dlna.Eventing
_subscriptions.TryRemove(subscriptionId, out _);
return new EventSubscriptionResponse
{
Content = string.Empty,
ContentType = "text/plain"
};
return new EventSubscriptionResponse(string.Empty, "text/plain");
}
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
{
var response = new EventSubscriptionResponse
{
Content = string.Empty,
ContentType = "text/plain"
};
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
response.Headers["SID"] = subscriptionId;
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;

@ -27,11 +27,9 @@ using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
using Rssdp;
using Rssdp.Infrastructure;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Dlna.Main
{
@ -204,8 +202,8 @@ namespace Emby.Dlna.Main
{
if (_communicationsServer == null)
{
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
OperatingSystem.Id == OperatingSystemId.Linux;
var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
OperatingSystem.IsLinux();
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{
@ -268,7 +266,12 @@ namespace Emby.Dlna.Main
try
{
_publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
_publisher = new SsdpDevicePublisher(
_communicationsServer,
_networkManager,
MediaBrowser.Common.System.OperatingSystem.Name,
Environment.OSVersion.VersionString,
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
{
LogFunction = LogMessage,
SupportPnpRootDevice = false

@ -1260,10 +1260,7 @@ namespace Emby.Dlna.PlayTo
return;
}
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
{
MediaInfo = mediaInfo
});
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
}
private void OnPlaybackProgress(UBaseObject mediaInfo)
@ -1273,27 +1270,17 @@ namespace Emby.Dlna.PlayTo
return;
}
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
{
MediaInfo = mediaInfo
});
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo));
}
private void OnPlaybackStop(UBaseObject mediaInfo)
{
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
{
MediaInfo = mediaInfo
});
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo));
}
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
{
MediaChanged?.Invoke(this, new MediaChangedEventArgs
{
OldMediaInfo = old,
NewMediaInfo = newMedia
});
MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia));
}
/// <inheritdoc />

@ -1,6 +1,4 @@
#nullable disable
#pragma warning disable CS1591
#pragma warning disable CS1591
using System;
@ -8,6 +6,12 @@ namespace Emby.Dlna.PlayTo
{
public class MediaChangedEventArgs : EventArgs
{
public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
{
OldMediaInfo = oldMediaInfo;
NewMediaInfo = newMediaInfo;
}
public UBaseObject OldMediaInfo { get; set; }
public UBaseObject NewMediaInfo { get; set; }

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackProgressEventArgs : EventArgs
{
public PlaybackProgressEventArgs(UBaseObject mediaInfo)
{
MediaInfo = mediaInfo;
}
public UBaseObject MediaInfo { get; set; }
}
}

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStartEventArgs : EventArgs
{
public PlaybackStartEventArgs(UBaseObject mediaInfo)
{
MediaInfo = mediaInfo;
}
public UBaseObject MediaInfo { get; set; }
}
}

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591
using System;
@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
{
public class PlaybackStoppedEventArgs : EventArgs
{
public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
{
MediaInfo = mediaInfo;
}
public UBaseObject MediaInfo { get; set; }
}
}

@ -6,9 +6,9 @@ using System.IO;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Diacritics.Extensions;
using Emby.Dlna.Didl;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions;
using Microsoft.Extensions.Logging;
namespace Emby.Dlna.Service
@ -95,11 +95,7 @@ namespace Emby.Dlna.Service
var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
var controlResponse = new ControlResponse
{
Xml = xml,
IsSuccessful = true
};
var controlResponse = new ControlResponse(xml, true);
controlResponse.Headers.Add("EXT", string.Empty);

@ -46,11 +46,7 @@ namespace Emby.Dlna.Service
writer.WriteEndDocument();
}
return new ControlResponse
{
Xml = builder.ToString(),
IsSuccessful = false
};
return new ControlResponse(builder.ToString(), false);
}
}
}

@ -9,8 +9,7 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<ItemGroup>
@ -30,8 +29,4 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

@ -1,7 +1,7 @@
using System;
using System.IO;
using Emby.Naming.Common;
using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions;
namespace Emby.Naming.Audio
{

@ -15,7 +15,7 @@ namespace Emby.Naming.AudioBook
/// <param name="files">List of files composing the actual audiobook.</param>
/// <param name="extras">List of extra files.</param>
/// <param name="alternateVersions">Alternative version of files.</param>
public AudioBookInfo(string name, int? year, List<AudioBookFileInfo> files, List<AudioBookFileInfo> extras, List<AudioBookFileInfo> alternateVersions)
public AudioBookInfo(string name, int? year, IReadOnlyList<AudioBookFileInfo> files, IReadOnlyList<AudioBookFileInfo> extras, IReadOnlyList<AudioBookFileInfo> alternateVersions)
{
Name = name;
Year = year;
@ -39,18 +39,18 @@ namespace Emby.Naming.AudioBook
/// Gets or sets the files.
/// </summary>
/// <value>The files.</value>
public List<AudioBookFileInfo> Files { get; set; }
public IReadOnlyList<AudioBookFileInfo> Files { get; set; }
/// <summary>
/// Gets or sets the extras.
/// </summary>
/// <value>The extras.</value>
public List<AudioBookFileInfo> Extras { get; set; }
public IReadOnlyList<AudioBookFileInfo> Extras { get; set; }
/// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public List<AudioBookFileInfo> AlternateVersions { get; set; }
public IReadOnlyList<AudioBookFileInfo> AlternateVersions { get; set; }
}
}

@ -87,7 +87,7 @@ namespace Emby.Naming.AudioBook
foreach (var audioFile in group)
{
var name = Path.GetFileNameWithoutExtension(audioFile.Path);
if (name.Equals("audiobook") ||
if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
{

@ -137,7 +137,7 @@ namespace Emby.Naming.Common
CleanStrings = new[]
{
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"(\[.*\])"
};
@ -277,14 +277,14 @@ namespace Emby.Naming.Common
IsNamed = true
},
new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$")
new EpisodeExpression(@"[\\\/\._ \[\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\.[1-9])(?![0-9]))?)([^\\\/]*)$")
{
SupportsAbsoluteEpisodeNumbers = true
},
// Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
// [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
new EpisodeExpression(@".*[\\\/]?.*?(\[.*?\])+.*?(?<seriesname>[-\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
{
IsNamed = true
},
@ -305,6 +305,12 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming
// "Episode 16", "Episode 16 - Title"
new EpisodeExpression(@"[Ee]pisode (?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))?[^\\\/]*$")
{
IsNamed = true
},
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{
IsNamed = true
@ -362,12 +368,6 @@ namespace Emby.Naming.Common
IsOptimistic = true,
IsNamed = true
},
// "Episode 16", "Episode 16 - Title"
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
{
IsOptimistic = true,
IsNamed = true
}
};
EpisodeWithoutSeasonExpressions = new[]

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
@ -9,12 +9,11 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Nullable>enable</Nullable>
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
@ -50,8 +49,4 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

@ -16,7 +16,7 @@ namespace Emby.Naming.TV
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
public EpisodeResolver(NamingOptions options)
{
_options = options;
@ -62,8 +62,7 @@ namespace Emby.Naming.TV
container = extension.TrimStart('.');
}
var flags = new FlagParser(_options).GetFlags(path);
var format3DResult = new Format3DParser(_options).Parse(flags);
var format3DResult = Format3DParser.Parse(path, _options);
var parsingResult = new EpisodePathParser(_options)
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);

@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Audio;
using Emby.Naming.Common;
@ -44,7 +43,7 @@ namespace Emby.Naming.Video
}
else if (rule.MediaType == MediaType.Video)
{
if (!new VideoResolver(_options).IsVideoFile(path))
if (!VideoResolver.IsVideoFile(path, _options))
{
continue;
}

@ -1,53 +0,0 @@
using System;
using System.IO;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Parses list of flags from filename based on delimiters.
/// </summary>
public class FlagParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="FlagParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param>
public FlagParser(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
/// </summary>
/// <param name="path">Path to file.</param>
/// <returns>List of found flags.</returns>
public string[] GetFlags(string path)
{
return GetFlags(path, _options.VideoFlagDelimiters);
}
/// <summary>
/// Parses flags from filename based on delimiters.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="delimiters">Delimiters used to extract flags.</param>
/// <returns>List of found flags.</returns>
public string[] GetFlags(string path, char[] delimiters)
{
if (string.IsNullOrEmpty(path))
{
return Array.Empty<string>();
}
// Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
var file = Path.GetFileName(path);
return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
}
}
}

@ -1,45 +1,37 @@
using System;
using System.Linq;
using Emby.Naming.Common;
namespace Emby.Naming.Video
{
/// <summary>
/// Parste 3D format related flags.
/// Parse 3D format related flags.
/// </summary>
public class Format3DParser
public static class Format3DParser
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="Format3DParser"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param>
public Format3DParser(NamingOptions options)
{
_options = options;
}
// Static default result to save on allocation costs.
private static readonly Format3DResult _defaultResult = new (false, null);
/// <summary>
/// Parse 3D format related flags.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
public Format3DResult Parse(string path)
public static Format3DResult Parse(ReadOnlySpan<char> path, NamingOptions namingOptions)
{
int oldLen = _options.VideoFlagDelimiters.Length;
var delimiters = new char[oldLen + 1];
_options.VideoFlagDelimiters.CopyTo(delimiters, 0);
int oldLen = namingOptions.VideoFlagDelimiters.Length;
Span<char> delimiters = stackalloc char[oldLen + 1];
namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
delimiters[oldLen] = ' ';
return Parse(new FlagParser(_options).GetFlags(path, delimiters));
return Parse(path, delimiters, namingOptions);
}
internal Format3DResult Parse(string[] videoFlags)
private static Format3DResult Parse(ReadOnlySpan<char> path, ReadOnlySpan<char> delimiters, NamingOptions namingOptions)
{
foreach (var rule in _options.Format3DRules)
foreach (var rule in namingOptions.Format3DRules)
{
var result = Parse(videoFlags, rule);
var result = Parse(path, rule, delimiters);
if (result.Is3D)
{
@ -47,51 +39,43 @@ namespace Emby.Naming.Video
}
}
return new Format3DResult();
return _defaultResult;
}
private static Format3DResult Parse(string[] videoFlags, Format3DRule rule)
private static Format3DResult Parse(ReadOnlySpan<char> path, Format3DRule rule, ReadOnlySpan<char> delimiters)
{
var result = new Format3DResult();
bool is3D = false;
string? format3D = null;
if (string.IsNullOrEmpty(rule.PrecedingToken))
// If there's no preceding token we just consider it found
var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken);
while (path.Length > 0)
{
result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
result.Is3D = !string.IsNullOrEmpty(result.Format3D);
if (result.Is3D)
var index = path.IndexOfAny(delimiters);
if (index == -1)
{
result.Tokens.Add(rule.Token);
index = path.Length - 1;
}
}
else
{
var foundPrefix = false;
string? format = null;
foreach (var flag in videoFlags)
{
if (foundPrefix)
{
result.Tokens.Add(rule.PrecedingToken);
var currentSlice = path[..index];
path = path[(index + 1)..];
if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
{
format = flag;
result.Tokens.Add(rule.Token);
}
if (!foundPrefix)
{
foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
continue;
}
break;
}
is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
if (is3D)
{
format3D = rule.Token;
break;
}
result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
result.Format3D = format;
}
return result;
return is3D ? new Format3DResult(true, format3D) : _defaultResult;
}
}
}

@ -1,5 +1,3 @@
using System.Collections.Generic;
namespace Emby.Naming.Video
{
/// <summary>
@ -10,27 +8,24 @@ namespace Emby.Naming.Video
/// <summary>
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
/// </summary>
public Format3DResult()
/// <param name="is3D">A value indicating whether the parsed string contains 3D tokens.</param>
/// <param name="format3D">The 3D format. Value might be null if [is3D] is <c>false</c>.</param>
public Format3DResult(bool is3D, string? format3D)
{
Tokens = new List<string>();
Is3D = is3D;
Format3D = format3D;
}
/// <summary>
/// Gets or sets a value indicating whether [is3 d].
/// Gets a value indicating whether [is3 d].
/// </summary>
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
public bool Is3D { get; set; }
public bool Is3D { get; }
/// <summary>
/// Gets or sets the format3 d.
/// Gets the format3 d.
/// </summary>
/// <value>The format3 d.</value>
public string? Format3D { get; set; }
/// <summary>
/// Gets or sets the tokens.
/// </summary>
/// <value>The tokens.</value>
public List<string> Tokens { get; set; }
public string? Format3D { get; }
}
}

@ -85,10 +85,8 @@ namespace Emby.Naming.Video
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
{
var resolver = new VideoResolver(_options);
var list = files
.Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
.OrderBy(i => i.FullName)
.ToList();

@ -1,3 +1,4 @@
using System;
using MediaBrowser.Model.Entities;
namespace Emby.Naming.Video
@ -106,9 +107,9 @@ namespace Emby.Naming.Video
/// Gets the file name without extension.
/// </summary>
/// <value>The file name without extension.</value>
public string FileNameWithoutExtension => !IsDirectory
? System.IO.Path.GetFileNameWithoutExtension(Path)
: System.IO.Path.GetFileName(Path);
public ReadOnlySpan<char> FileNameWithoutExtension => !IsDirectory
? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan())
: System.IO.Path.GetFileName(Path.AsSpan());
/// <inheritdoc />
public override string ToString()

@ -12,31 +12,19 @@ namespace Emby.Naming.Video
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
public class VideoListResolver
public static class VideoListResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="VideoListResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
public VideoListResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Resolves alternative versions and extras from list of video files.
/// </summary>
/// <param name="files">List of related video files.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
{
var videoResolver = new VideoResolver(_options);
var videoInfos = files
.Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
.OfType<VideoFileInfo>()
.ToList();
@ -46,7 +34,7 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
var stackResult = new StackResolver(_options)
var stackResult = new StackResolver(namingOptions)
.Resolve(nonExtras).ToList();
var remainingFiles = videoInfos
@ -59,23 +47,17 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
.OfType<VideoFileInfo>()
.ToList()
};
info.Year = info.Files[0].Year;
var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
var extras = GetExtras(remainingFiles, extraBaseNames);
var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
if (extras.Count > 0)
{
remainingFiles = remainingFiles
.Except(extras)
.ToList();
info.Extras = extras;
}
@ -88,15 +70,12 @@ namespace Emby.Naming.Video
foreach (var media in standaloneMedia)
{
var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } };
var info = new VideoInfo(media.Name) { Files = new[] { media } };
info.Year = info.Files[0].Year;
var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
remainingFiles = remainingFiles
.Except(extras.Concat(new[] { media }))
.ToList();
remainingFiles.Remove(media);
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
info.Extras = extras;
@ -105,8 +84,7 @@ namespace Emby.Naming.Video
if (supportMultiVersion)
{
list = GetVideosGroupedByVersion(list)
.ToList();
list = GetVideosGroupedByVersion(list, namingOptions);
}
// If there's only one resolved video, use the folder name as well to find extras
@ -114,19 +92,14 @@ namespace Emby.Naming.Video
{
var info = list[0];
var videoPath = list[0].Files[0].Path;
var parentPath = Path.GetDirectoryName(videoPath);
var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
if (!string.IsNullOrEmpty(parentPath))
if (!parentPath.IsEmpty)
{
var folderName = Path.GetFileName(parentPath);
if (!string.IsNullOrEmpty(folderName))
if (!folderName.IsEmpty)
{
var extras = GetExtras(remainingFiles, new List<string> { folderName });
remainingFiles = remainingFiles
.Except(extras)
.ToList();
var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
extras.AddRange(info.Extras);
info.Extras = extras;
}
@ -164,96 +137,168 @@ namespace Emby.Naming.Video
// Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{
Files = new List<VideoFileInfo> { i },
Files = new[] { i },
Year = i.Year
}));
return list;
}
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
{
if (videos.Count == 0)
{
return videos;
}
var list = new List<VideoInfo>();
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path));
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
if (!string.IsNullOrEmpty(folderName)
&& folderName.Length > 1
&& videos.All(i => i.Files.Count == 1
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path))
&& HaveSameYear(videos))
if (folderName.Length <= 1 || !HaveSameYear(videos))
{
var ordered = videos.OrderBy(i => i.Name).ToList();
list.Add(ordered[0]);
return videos;
}
var alternateVersionsLen = ordered.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
for (int i = 0; i < alternateVersionsLen; i++)
// Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{
alternateVersions[i] = ordered[i + 1].Files[0];
return videos;
}
}
// The list is created and overwritten in the caller, so we are allowed to do in-place sorting
videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName;
var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
extras.AddRange(list[0].Extras);
list[0].Extras = extras;
var list = new List<VideoInfo>
{
videos[0]
};
return list;
var alternateVersionsLen = videos.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
var extras = new List<VideoFileInfo>(list[0].Extras);
for (int i = 0; i < alternateVersionsLen; i++)
{
var video = videos[i + 1];
alternateVersions[i] = video.Files[0];
extras.AddRange(video.Extras);
}
return videos;
}
list[0].AlternateVersions = alternateVersions;
list[0].Name = folderName.ToString();
list[0].Extras = extras;
private bool HaveSameYear(List<VideoInfo> videos)
{
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
return list;
}
private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
{
string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
if (videos.Count == 1)
{
// Remove the folder name before cleaning as we don't care about cleaning that part
if (folderName.Length <= testFilename.Length)
{
testFilename = testFilename.Substring(folderName.Length).Trim();
}
return true;
}
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
var firstYear = videos[0].Year ?? -1;
for (var i = 1; i < videos.Count; i++)
{
if ((videos[i].Year ?? -1) != firstYear)
{
testFilename = cleanName.Trim().ToString();
return false;
}
}
// The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(testFilename)
|| testFilename[0] == '-'
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
return true;
}
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
{
var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
return false;
}
return false;
// Remove the folder name before cleaning as we don't care about cleaning that part
if (folderName.Length <= testFilename.Length)
{
testFilename = testFilename[folderName.Length..].Trim();
}
// There are no span overloads for regex unfortunately
var tmpTestFilename = testFilename.ToString();
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
{
tmpTestFilename = cleanName.Trim().ToString();
}
// The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(tmpTestFilename)
|| testFilename[0] == '-'
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
{
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
}
private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames)
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
{
foreach (var name in baseNames.ToList())
if (baseName.IsEmpty)
{
var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
baseNames.Add(trimmedName);
return false;
}
return remainingFiles
.Where(i => i.ExtraType != null)
.Where(i => baseNames.Any(b =>
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
.ToList();
return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|| (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
/// </summary>
/// <param name="remainingFiles">The list of remaining filenames.</param>
/// <param name="baseName">The base name to use for the comparison.</param>
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
/// <returns>A list of video extras for [baseName].</returns>
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
{
return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
}
/// <summary>
/// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
/// </summary>
/// <param name="remainingFiles">The list of remaining filenames.</param>
/// <param name="firstBaseName">The first base name to use for the comparison.</param>
/// <param name="secondBaseName">The second base name to use for the comparison.</param>
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
/// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
{
var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
var result = new List<VideoFileInfo>();
for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
{
var file = remainingFiles[pos];
if (file.ExtraType == null)
{
continue;
}
var filename = file.FileNameWithoutExtension;
if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
|| StartsWith(filename, secondBaseName, trimmedSecondBaseName))
{
result.Add(file);
remainingFiles.RemoveAt(pos);
}
}
return result;
}
}
}

@ -2,45 +2,35 @@ using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Emby.Naming.Common;
using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions;
namespace Emby.Naming.Video
{
/// <summary>
/// Resolves <see cref="VideoFileInfo"/> from file path.
/// </summary>
public class VideoResolver
public static class VideoResolver
{
private readonly NamingOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="VideoResolver"/> class.
/// </summary>
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
/// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param>
public VideoResolver(NamingOptions options)
{
_options = options;
}
/// <summary>
/// Resolves the directory.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>VideoFileInfo.</returns>
public VideoFileInfo? ResolveDirectory(string? path)
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
{
return Resolve(path, true);
return Resolve(path, true, namingOptions);
}
/// <summary>
/// Resolves the file.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>VideoFileInfo.</returns>
public VideoFileInfo? ResolveFile(string? path)
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
{
return Resolve(path, false);
return Resolve(path, false, namingOptions);
}
/// <summary>
@ -48,10 +38,11 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
/// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true)
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
{
if (string.IsNullOrEmpty(path))
{
@ -67,10 +58,10 @@ namespace Emby.Naming.Video
var extension = Path.GetExtension(path.AsSpan());
// Check supported extensions
if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
if (!namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
// It's not supported. Check stub extensions
if (!StubResolver.TryResolveFile(path, _options, out stubType))
if (!StubResolver.TryResolveFile(path, namingOptions, out stubType))
{
return null;
}
@ -81,10 +72,9 @@ namespace Emby.Naming.Video
container = extension.TrimStart('.');
}
var flags = new FlagParser(_options).GetFlags(path);
var format3DResult = new Format3DParser(_options).Parse(flags);
var format3DResult = Format3DParser.Parse(path, namingOptions);
var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
var name = Path.GetFileNameWithoutExtension(path);
@ -92,12 +82,12 @@ namespace Emby.Naming.Video
if (parseName)
{
var cleanDateTimeResult = CleanDateTime(name);
var cleanDateTimeResult = CleanDateTime(name, namingOptions);
name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year;
if (extraResult.ExtraType == null
&& TryCleanString(name, out ReadOnlySpan<char> newName))
&& TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
{
name = newName.ToString();
}
@ -121,43 +111,47 @@ namespace Emby.Naming.Video
/// Determines if path is video file based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>True if is video file.</returns>
public bool IsVideoFile(string path)
public static bool IsVideoFile(string path, NamingOptions namingOptions)
{
var extension = Path.GetExtension(path.AsSpan());
return _options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines if path is video file stub based on extension.
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>True if is video file stub.</returns>
public bool IsStubFile(string path)
public static bool IsStubFile(string path, NamingOptions namingOptions)
{
var extension = Path.GetExtension(path.AsSpan());
return _options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Tries to clean name of clutter.
/// </summary>
/// <param name="name">Raw name.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="newName">Clean name.</param>
/// <returns>True if cleaning of name was successful.</returns>
public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
{
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
}
/// <summary>
/// Tries to get name and year from raw name.
/// </summary>
/// <param name="name">Raw name.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
public CleanDateTimeResult CleanDateTime(string name)
public static CleanDateTimeResult CleanDateTime(string name, NamingOptions namingOptions)
{
return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
return CleanDateTimeParser.Clean(name, namingOptions.CleanDateTimeRegexes);
}
}
}

@ -9,10 +9,6 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>

@ -77,7 +77,6 @@ namespace Emby.Notifications
{
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
return Task.CompletedTask;
@ -132,25 +131,6 @@ namespace Emby.Notifications
return _config.GetConfiguration<NotificationOptions>("notifications");
}
private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
{
if (!_appHost.HasUpdateAvailable)
{
return;
}
var type = NotificationType.ApplicationUpdateAvailable.ToString();
var notification = new NotificationRequest
{
Description = "Please see jellyfin.org for details.",
NotificationType = type,
Name = _localization.GetLocalizedString("NewVersionIsAvailable")
};
await SendNotification(notification, null).ConfigureAwait(false);
}
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (!FilterItem(e.Item))
@ -325,7 +305,6 @@ namespace Emby.Notifications
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
_disposed = true;

@ -22,10 +22,6 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers-->

@ -299,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
/// <inheritdoc />
public object GetConfiguration(string key)
{
return _configurations.GetOrAdd(key, k =>
{
var file = GetConfigurationFile(key);
return _configurations.GetOrAdd(
key,
(k, configurationManager) =>
{
var file = configurationManager.GetConfigurationFile(k);
var configurationInfo = _configurationStores
.FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
var configurationInfo = Array.Find(
configurationManager._configurationStores,
i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
if (configurationInfo == null)
{
throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
}
if (configurationInfo == null)
{
throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
}
var configurationType = configurationInfo.ConfigurationType;
var configurationType = configurationInfo.ConfigurationType;
lock (_configurationSyncLock)
{
return LoadConfiguration(file, configurationType);
}
});
lock (configurationManager._configurationSyncLock)
{
return configurationManager.LoadConfiguration(file, configurationType);
}
},
this);
}
private object LoadConfiguration(string path, Type configurationType)

@ -103,7 +103,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
namespace Emby.Server.Implementations
@ -150,13 +149,7 @@ namespace Emby.Server.Implementations
return false;
}
if (OperatingSystem.Id == OperatingSystemId.Windows
|| OperatingSystem.Id == OperatingSystemId.Darwin)
{
return true;
}
return false;
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
}
}
@ -721,7 +714,7 @@ namespace Emby.Server.Implementations
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
logger.LogInformation("Arguments: {Args}", commandLineArgs);
logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name);
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
@ -1098,16 +1091,14 @@ namespace Emby.Server.Implementations
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
OperatingSystem = OperatingSystem.Id.ToString(),
OperatingSystemDisplayName = OperatingSystem.Name,
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
CanSelfRestart = CanSelfRestart,
CanLaunchWebBrowser = CanLaunchWebBrowser,
HasUpdateAvailable = HasUpdateAvailable,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(source),
SupportsLibraryMonitor = true,
EncoderLocation = _mediaEncoder.EncoderLocation,
SystemArchitecture = RuntimeInformation.OSArchitecture,
PackageName = _startupOptions.PackageName
};
@ -1118,16 +1109,16 @@ namespace Emby.Server.Implementations
.Select(i => new WakeOnLanInfo(i))
.ToList();
public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
public PublicSystemInfo GetPublicSystemInfo(IPAddress address)
{
return new PublicSystemInfo
{
Version = ApplicationVersionString,
ProductName = ApplicationProductName,
Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(),
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(source),
LocalAddress = GetSmartApiUrl(address),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
@ -1136,7 +1127,7 @@ namespace Emby.Server.Implementations
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
/// <inheritdoc/>
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
{
// Published server ends with a /
if (!string.IsNullOrEmpty(PublishedServerUrl))
@ -1145,7 +1136,7 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(ipAddress, out port);
string smart = NetManager.GetBindInterface(remoteAddr, out port);
// If the smartAPI doesn't start with http then treat it as a host or ip.
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
@ -1208,14 +1199,14 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
public string GetLocalApiUrl(string host, string scheme = null, int? port = null)
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
{
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
// not. For consistency, always trim the trailing slash.
return new UriBuilder
{
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
Host = host,
Host = hostname,
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
}.ToString().TrimEnd('/');
@ -1252,26 +1243,6 @@ namespace Emby.Server.Implementations
protected abstract void ShutdownInternal();
public event EventHandler HasUpdateAvailableChanged;
private bool _hasUpdateAvailable;
public bool HasUpdateAvailable
{
get => _hasUpdateAvailable;
set
{
var fireEvent = value && !_hasUpdateAvailable;
_hasUpdateAvailable = value;
if (fireEvent)
{
HasUpdateAvailableChanged?.Invoke(this, EventArgs.Empty);
}
}
}
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes

@ -11,7 +11,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@ -880,7 +880,7 @@ namespace Emby.Server.Implementations.Channels
}
}
private async Task CacheResponse(object result, string path)
private async Task CacheResponse(ChannelItemResult result, string path)
{
try
{

@ -1,5 +1,3 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
@ -63,13 +61,13 @@ namespace Emby.Server.Implementations.Collections
}
/// <inheritdoc />
public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
public event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
/// <inheritdoc />
public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
public event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
/// <inheritdoc />
public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
public event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
private IEnumerable<Folder> FindFolders(string path)
{
@ -80,14 +78,12 @@ namespace Emby.Server.Implementations.Collections
.Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
}
internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded)
internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
{
var existingFolders = FindFolders(path)
.ToList();
if (existingFolders.Count > 0)
var existingFolder = FindFolders(path).FirstOrDefault();
if (existingFolder != null)
{
return existingFolders[0];
return existingFolder;
}
if (!createIfNeeded)
@ -116,7 +112,7 @@ namespace Emby.Server.Implementations.Collections
return Path.Combine(_appPaths.DataPath, "collections");
}
private Task<Folder> GetCollectionsFolder(bool createIfNeeded)
private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
{
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
}
@ -164,7 +160,7 @@ namespace Emby.Server.Implementations.Collections
DateCreated = DateTime.UtcNow
};
parentFolder.AddChild(collection, CancellationToken.None);
parentFolder.AddChild(collection);
if (options.ItemIdList.Count > 0)
{
@ -205,8 +201,7 @@ namespace Emby.Server.Implementations.Collections
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
if (collection == null)
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
{
throw new ArgumentException("No collection exists with the supplied Id");
}
@ -258,9 +253,7 @@ namespace Emby.Server.Implementations.Collections
/// <inheritdoc />
public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
if (collection == null)
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
{
throw new ArgumentException("No collection exists with the supplied Id");
}
@ -314,11 +307,7 @@ namespace Emby.Server.Implementations.Collections
foreach (var item in items)
{
if (item is not ISupportsBoxSetGrouping)
{
results[item.Id] = item;
}
else
if (item is ISupportsBoxSetGrouping)
{
var itemId = item.Id;
@ -342,6 +331,7 @@ namespace Emby.Server.Implementations.Collections
}
var alreadyInResults = false;
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
if (item is Video video)
{
@ -357,11 +347,13 @@ namespace Emby.Server.Implementations.Collections
}
}
if (!alreadyInResults)
if (alreadyInResults)
{
results[itemId] = item;
continue;
}
}
results[item.Id] = item;
}
return results.Values;

@ -11,10 +11,12 @@ using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using Diacritics.Extensions;
using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@ -43,6 +45,7 @@ namespace Emby.Server.Implementations.Data
/// </summary>
public class SqliteItemRepository : BaseSqliteRepository, IItemRepository
{
private const string FromText = " from TypedBaseItems A";
private const string ChaptersTableName = "Chapters2";
private readonly IServerConfigurationManager _config;
@ -1045,18 +1048,34 @@ namespace Emby.Server.Implementations.Data
return Array.Empty<ItemImageInfo>();
}
var list = new List<ItemImageInfo>();
foreach (var part in value.SpanSplit('|'))
// TODO The following is an ugly performance optimization, but it's extremely unlikely that the data in the database would be malformed
var valueSpan = value.AsSpan();
var count = valueSpan.Count('|') + 1;
var position = 0;
var result = new ItemImageInfo[count];
foreach (var part in valueSpan.Split('|'))
{
var image = ItemImageInfoFromValueString(part);
if (image != null)
{
list.Add(image);
result[position++] = image;
}
}
return list.ToArray();
if (position == count)
{
return result;
}
if (position == 0)
{
return Array.Empty<ItemImageInfo>();
}
// Extremely unlikely, but somehow one or more of the image strings were malformed. Cut the array.
return result[..position];
}
private void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
@ -2250,10 +2269,8 @@ namespace Emby.Server.Implementations.Data
return query.IncludeItemTypes.Any(x => _seriesTypes.Contains(x));
}
private List<string> GetFinalColumnsToSelect(InternalItemsQuery query, IEnumerable<string> startColumns)
private void SetFinalColumnsToSelect(InternalItemsQuery query, List<string> columns)
{
var list = startColumns.ToList();
foreach (var field in _allFields)
{
if (!HasField(query, field))
@ -2261,28 +2278,28 @@ namespace Emby.Server.Implementations.Data
switch (field)
{
case ItemFields.Settings:
list.Remove("IsLocked");
list.Remove("PreferredMetadataCountryCode");
list.Remove("PreferredMetadataLanguage");
list.Remove("LockedFields");
columns.Remove("IsLocked");
columns.Remove("PreferredMetadataCountryCode");
columns.Remove("PreferredMetadataLanguage");
columns.Remove("LockedFields");
break;
case ItemFields.ServiceName:
list.Remove("ExternalServiceId");
columns.Remove("ExternalServiceId");
break;
case ItemFields.SortName:
list.Remove("ForcedSortName");
columns.Remove("ForcedSortName");
break;
case ItemFields.Taglines:
list.Remove("Tagline");
columns.Remove("Tagline");
break;
case ItemFields.Tags:
list.Remove("Tags");
columns.Remove("Tags");
break;
case ItemFields.IsHD:
// do nothing
break;
default:
list.Remove(field.ToString());
columns.Remove(field.ToString());
break;
}
}
@ -2290,60 +2307,60 @@ namespace Emby.Server.Implementations.Data
if (!HasProgramAttributes(query))
{
list.Remove("IsMovie");
list.Remove("IsSeries");
list.Remove("EpisodeTitle");
list.Remove("IsRepeat");
list.Remove("ShowId");
columns.Remove("IsMovie");
columns.Remove("IsSeries");
columns.Remove("EpisodeTitle");
columns.Remove("IsRepeat");
columns.Remove("ShowId");
}
if (!HasEpisodeAttributes(query))
{
list.Remove("SeasonName");
list.Remove("SeasonId");
columns.Remove("SeasonName");
columns.Remove("SeasonId");
}
if (!HasStartDate(query))
{
list.Remove("StartDate");
columns.Remove("StartDate");
}
if (!HasTrailerTypes(query))
{
list.Remove("TrailerTypes");
columns.Remove("TrailerTypes");
}
if (!HasArtistFields(query))
{
list.Remove("AlbumArtists");
list.Remove("Artists");
columns.Remove("AlbumArtists");
columns.Remove("Artists");
}
if (!HasSeriesFields(query))
{
list.Remove("SeriesId");
columns.Remove("SeriesId");
}
if (!HasEpisodeAttributes(query))
{
list.Remove("SeasonName");
list.Remove("SeasonId");
columns.Remove("SeasonName");
columns.Remove("SeasonId");
}
if (!query.DtoOptions.EnableImages)
{
list.Remove("Images");
columns.Remove("Images");
}
if (EnableJoinUserData(query))
{
list.Add("UserDatas.UserId");
list.Add("UserDatas.lastPlayedDate");
list.Add("UserDatas.playbackPositionTicks");
list.Add("UserDatas.playcount");
list.Add("UserDatas.isFavorite");
list.Add("UserDatas.played");
list.Add("UserDatas.rating");
columns.Add("UserDatas.UserId");
columns.Add("UserDatas.lastPlayedDate");
columns.Add("UserDatas.playbackPositionTicks");
columns.Add("UserDatas.playcount");
columns.Add("UserDatas.isFavorite");
columns.Add("UserDatas.played");
columns.Add("UserDatas.rating");
}
if (query.SimilarTo != null)
@ -2391,7 +2408,7 @@ namespace Emby.Server.Implementations.Data
builder.Append(") as SimilarityScore");
list.Add(builder.ToString());
columns.Add(builder.ToString());
var oldLen = query.ExcludeItemIds.Length;
var newLen = oldLen + item.ExtraIds.Length + 1;
@ -2418,10 +2435,8 @@ namespace Emby.Server.Implementations.Data
builder.Append(") as SearchScore");
list.Add(builder.ToString());
columns.Add(builder.ToString());
}
return list;
}
private void BindSearchParams(InternalItemsQuery query, IStatement statement)
@ -2487,31 +2502,25 @@ namespace Emby.Server.Implementations.Data
private string GetGroupBy(InternalItemsQuery query)
{
var groups = new List<string>();
if (EnableGroupByPresentationUniqueKey(query))
var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(query);
if (enableGroupByPresentationUniqueKey && query.GroupBySeriesPresentationUniqueKey)
{
groups.Add("PresentationUniqueKey");
return " Group by PresentationUniqueKey, SeriesPresentationUniqueKey";
}
if (query.GroupBySeriesPresentationUniqueKey)
if (enableGroupByPresentationUniqueKey)
{
groups.Add("SeriesPresentationUniqueKey");
return " Group by PresentationUniqueKey";
}
if (groups.Count > 0)
if (query.GroupBySeriesPresentationUniqueKey)
{
return " Group by " + string.Join(',', groups);
return " Group by SeriesPresentationUniqueKey";
}
return string.Empty;
}
private string GetFromText(string alias = "A")
{
return " from TypedBaseItems " + alias;
}
public int GetCount(InternalItemsQuery query)
{
if (query == null)
@ -2529,17 +2538,21 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
var commandText = "select "
+ string.Join(',', GetFinalColumnsToSelect(query, new[] { "count(distinct PresentationUniqueKey)" }))
+ GetFromText()
+ GetJoinUserDataText(query);
var columns = new List<string> { "count(distinct PresentationUniqueKey)" };
SetFinalColumnsToSelect(query, columns);
var commandTextBuilder = new StringBuilder("select ", 256)
.AppendJoin(',', columns)
.Append(FromText)
.Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
commandText += " where " + string.Join(" AND ", whereClauses);
commandTextBuilder.Append(" where ")
.AppendJoin(" AND ", whereClauses);
}
var commandText = commandTextBuilder.ToString();
int count;
using (var connection = GetConnection(true))
{
@ -2581,20 +2594,23 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
var commandText = "select "
+ string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
+ GetFromText()
+ GetJoinUserDataText(query);
var columns = _retriveItemColumns.ToList();
SetFinalColumnsToSelect(query, columns);
var commandTextBuilder = new StringBuilder("select ", 1024)
.AppendJoin(',', columns)
.Append(FromText)
.Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
commandText += " where " + string.Join(" AND ", whereClauses);
commandTextBuilder.Append(" where ")
.AppendJoin(" AND ", whereClauses);
}
commandText += GetGroupBy(query)
+ GetOrderByText(query);
commandTextBuilder.Append(GetGroupBy(query))
.Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@ -2602,15 +2618,18 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
commandTextBuilder.Append(" LIMIT ")
.Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
commandTextBuilder.Append(" OFFSET ")
.Append(offset);
}
}
var commandText = commandTextBuilder.ToString();
var items = new List<BaseItem>();
using (var connection = GetConnection(true))
{
@ -2766,20 +2785,27 @@ namespace Emby.Server.Implementations.Data
query.Limit = query.Limit.Value + 4;
}
var commandText = "select "
+ string.Join(',', GetFinalColumnsToSelect(query, _retriveItemColumns))
+ GetFromText()
+ GetJoinUserDataText(query);
var columns = _retriveItemColumns.ToList();
SetFinalColumnsToSelect(query, columns);
var commandTextBuilder = new StringBuilder("select ", 512)
.AppendJoin(',', columns)
.Append(FromText)
.Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
var whereText = whereClauses.Count == 0 ?
string.Empty :
" where " + string.Join(" AND ", whereClauses);
string.Join(" AND ", whereClauses);
commandText += whereText
+ GetGroupBy(query)
+ GetOrderByText(query);
if (!string.IsNullOrEmpty(whereText))
{
commandTextBuilder.Append(" where ")
.Append(whereText);
}
commandTextBuilder.Append(GetGroupBy(query))
.Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@ -2787,43 +2813,58 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
commandTextBuilder.Append(" LIMIT ")
.Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
commandTextBuilder.Append(" OFFSET ")
.Append(offset);
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
var statementTexts = new List<string>();
var itemQuery = string.Empty;
var totalRecordCountQuery = string.Empty;
if (!isReturningZeroItems)
{
statementTexts.Add(commandText);
itemQuery = commandTextBuilder.ToString();
}
if (query.EnableTotalRecordCount)
{
commandText = string.Empty;
commandTextBuilder.Clear();
commandTextBuilder.Append(" select ");
List<string> columnsToSelect;
if (EnableGroupByPresentationUniqueKey(query))
{
commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
}
else
{
commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
columnsToSelect = new List<string> { "count (guid)" };
}
commandText += GetJoinUserDataText(query)
+ whereText;
statementTexts.Add(commandText);
SetFinalColumnsToSelect(query, columnsToSelect);
commandTextBuilder.AppendJoin(',', columnsToSelect)
.Append(FromText)
.Append(GetJoinUserDataText(query));
if (!string.IsNullOrEmpty(whereText))
{
commandTextBuilder.Append(" where ")
.Append(whereText);
}
totalRecordCountQuery = commandTextBuilder.ToString();
}
var list = new List<BaseItem>();
@ -2833,11 +2874,12 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts);
var itemQueryStatement = PrepareStatement(db, itemQuery);
var totalRecordCountQueryStatement = PrepareStatement(db, totalRecordCountQuery);
if (!isReturningZeroItems)
{
using (var statement = statements[0])
using (var statement = itemQueryStatement)
{
if (EnableJoinUserData(query))
{
@ -2867,11 +2909,14 @@ namespace Emby.Server.Implementations.Data
}
}
}
LogQueryTime("GetItems.ItemQuery", itemQuery, now);
}
now = DateTime.UtcNow;
if (query.EnableTotalRecordCount)
{
using (var statement = statements[statements.Length - 1])
using (var statement = totalRecordCountQueryStatement)
{
if (EnableJoinUserData(query))
{
@ -2886,11 +2931,12 @@ namespace Emby.Server.Implementations.Data
result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
}
LogQueryTime("GetItems.TotalRecordCount", totalRecordCountQuery, now);
}
}, ReadTransactionMode);
}
LogQueryTime("GetItems", commandText, now);
result.Items = list;
return result;
}
@ -3023,19 +3069,22 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
var commandText = "select "
+ string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
+ GetFromText()
+ GetJoinUserDataText(query);
var columns = new List<string> { "guid" };
SetFinalColumnsToSelect(query, columns);
var commandTextBuilder = new StringBuilder("select ", 256)
.AppendJoin(',', columns)
.Append(FromText)
.Append(GetJoinUserDataText(query));
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
{
commandText += " where " + string.Join(" AND ", whereClauses);
commandTextBuilder.Append(" where ")
.AppendJoin(" AND ", whereClauses);
}
commandText += GetGroupBy(query)
+ GetOrderByText(query);
commandTextBuilder.Append(GetGroupBy(query))
.Append(GetOrderByText(query));
if (query.Limit.HasValue || query.StartIndex.HasValue)
{
@ -3043,15 +3092,18 @@ namespace Emby.Server.Implementations.Data
if (query.Limit.HasValue || offset > 0)
{
commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
commandTextBuilder.Append(" LIMIT ")
.Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
commandTextBuilder.Append(" OFFSET ")
.Append(offset);
}
}
var commandText = commandTextBuilder.ToString();
var list = new List<Guid>();
using (var connection = GetConnection(true))
{
@ -3090,7 +3142,9 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
var commandText = "select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText();
var columns = new List<string> { "guid", "path" };
SetFinalColumnsToSelect(query, columns);
var commandText = "select " + string.Join(',', columns) + FromText;
var whereClauses = GetWhereClauses(query, null);
if (whereClauses.Count != 0)
@ -3166,9 +3220,11 @@ namespace Emby.Server.Implementations.Data
var now = DateTime.UtcNow;
var columns = new List<string> { "guid" };
SetFinalColumnsToSelect(query, columns);
var commandText = "select "
+ string.Join(',', GetFinalColumnsToSelect(query, new[] { "guid" }))
+ GetFromText()
+ string.Join(',', columns)
+ FromText
+ GetJoinUserDataText(query);
var whereClauses = GetWhereClauses(query, null);
@ -3208,19 +3264,23 @@ namespace Emby.Server.Implementations.Data
{
commandText = string.Empty;
List<string> columnsToSelect;
if (EnableGroupByPresentationUniqueKey(query))
{
commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + GetFromText();
columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
}
else if (query.GroupBySeriesPresentationUniqueKey)
{
commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct SeriesPresentationUniqueKey)" })) + GetFromText();
columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" };
}
else
{
commandText += " select " + string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (guid)" })) + GetFromText();
columnsToSelect = new List<string> { "count (guid)" };
}
SetFinalColumnsToSelect(query, columnsToSelect);
commandText += " select " + string.Join(',', columnsToSelect) + FromText;
commandText += GetJoinUserDataText(query)
+ whereText;
statementTexts.Add(commandText);
@ -4415,56 +4475,50 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(GetProviderIdClause(query.HasTvdbId.Value, "tvdb"));
}
var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
var queryTopParentIds = query.TopParentIds;
if (queryTopParentIds.Length == 1)
if (queryTopParentIds.Length > 0)
{
if (enableItemsByName && includedItemByNameTypes.Count == 1)
var includedItemByNameTypes = GetItemByNameTypesInQuery(query);
var enableItemsByName = (query.IncludeItemsByName ?? false) && includedItemByNameTypes.Count > 0;
if (queryTopParentIds.Length == 1)
{
whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
if (statement != null)
if (enableItemsByName && includedItemByNameTypes.Count == 1)
{
statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
whereClauses.Add("(TopParentId=@TopParentId or Type=@IncludedItemByNameType)");
statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
}
else if (enableItemsByName && includedItemByNameTypes.Count > 1)
{
var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
}
else
{
whereClauses.Add("(TopParentId=@TopParentId)");
}
}
else if (enableItemsByName && includedItemByNameTypes.Count > 1)
{
var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
whereClauses.Add("(TopParentId=@TopParentId or Type in (" + itemByNameTypeVal + "))");
}
else
{
whereClauses.Add("(TopParentId=@TopParentId)");
}
if (statement != null)
{
statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
statement?.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture));
}
}
else if (queryTopParentIds.Length > 1)
{
var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
if (enableItemsByName && includedItemByNameTypes.Count == 1)
else if (queryTopParentIds.Length > 1)
{
whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
if (statement != null)
var val = string.Join(',', queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
if (enableItemsByName && includedItemByNameTypes.Count == 1)
{
statement.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
whereClauses.Add("(Type=@IncludedItemByNameType or TopParentId in (" + val + "))");
statement?.TryBind("@IncludedItemByNameType", includedItemByNameTypes[0]);
}
else if (enableItemsByName && includedItemByNameTypes.Count > 1)
{
var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
}
else
{
whereClauses.Add("TopParentId in (" + val + ")");
}
}
else if (enableItemsByName && includedItemByNameTypes.Count > 1)
{
var itemByNameTypeVal = string.Join(',', includedItemByNameTypes.Select(i => "'" + i + "'"));
whereClauses.Add("(Type in (" + itemByNameTypeVal + ") or TopParentId in (" + val + "))");
}
else
{
whereClauses.Add("TopParentId in (" + val + ")");
}
}
@ -4746,17 +4800,12 @@ namespace Emby.Server.Implementations.Data
return true;
}
var types = new[]
{
nameof(Episode),
nameof(Video),
nameof(Movie),
nameof(MusicVideo),
nameof(Series),
nameof(Season)
};
if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
if (query.IncludeItemTypes.Contains(nameof(Episode), StringComparer.OrdinalIgnoreCase)
|| query.IncludeItemTypes.Contains(nameof(Video), StringComparer.OrdinalIgnoreCase)
|| query.IncludeItemTypes.Contains(nameof(Movie), StringComparer.OrdinalIgnoreCase)
|| query.IncludeItemTypes.Contains(nameof(MusicVideo), StringComparer.OrdinalIgnoreCase)
|| query.IncludeItemTypes.Contains(nameof(Series), StringComparer.OrdinalIgnoreCase)
|| query.IncludeItemTypes.Contains(nameof(Season), StringComparer.OrdinalIgnoreCase))
{
return true;
}
@ -5200,37 +5249,45 @@ AND Type = @InternalPersonType)");
var now = DateTime.UtcNow;
var typeClause = itemValueTypes.Length == 1 ?
("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
var commandText = "Select Value From ItemValues where " + typeClause;
var stringBuilder = new StringBuilder("Select Value From ItemValues where Type", 128);
if (itemValueTypes.Length == 1)
{
stringBuilder.Append('=')
.Append(itemValueTypes[0]);
}
else
{
stringBuilder.Append(" in (")
.AppendJoin(',', itemValueTypes)
.Append(')');
}
if (withItemTypes.Count > 0)
{
var typeString = string.Join(',', withItemTypes.Select(i => "'" + i + "'"));
commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))";
stringBuilder.Append(" AND ItemId In (select guid from typedbaseitems where type in (")
.AppendJoinInSingleQuotes(',', withItemTypes)
.Append("))");
}
if (excludeItemTypes.Count > 0)
{
var typeString = string.Join(',', excludeItemTypes.Select(i => "'" + i + "'"));
commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))";
stringBuilder.Append(" AND ItemId not In (select guid from typedbaseitems where type in (")
.AppendJoinInSingleQuotes(',', excludeItemTypes)
.Append("))");
}
commandText += " Group By CleanValue";
stringBuilder.Append(" Group By CleanValue");
var commandText = stringBuilder.ToString();
var list = new List<string>();
using (var connection = GetConnection(true))
using (var statement = PrepareStatement(connection, commandText))
{
using (var statement = PrepareStatement(connection, commandText))
foreach (var row in statement.ExecuteQuery())
{
foreach (var row in statement.ExecuteQuery())
if (row.TryGetString(0, out var result))
{
if (row.TryGetString(0, out var result))
{
list.Add(result);
}
list.Add(result);
}
}
}
@ -5256,18 +5313,19 @@ AND Type = @InternalPersonType)");
var now = DateTime.UtcNow;
var typeClause = itemValueTypes.Length == 1 ?
("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) :
("Type in (" + string.Join(',', itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture))) + ")");
("Type=" + itemValueTypes[0]) :
("Type in (" + string.Join(',', itemValueTypes) + ")");
InternalItemsQuery typeSubQuery = null;
Dictionary<string, string> itemCountColumns = null;
string itemCountColumns = null;
var stringBuilder = new StringBuilder(1024);
var typesToCount = query.IncludeItemTypes;
if (typesToCount.Length > 0)
{
var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B");
stringBuilder.Append("(select group_concat(type, '|') from TypedBaseItems B");
typeSubQuery = new InternalItemsQuery(query.User)
{
@ -5283,20 +5341,22 @@ AND Type = @InternalPersonType)");
};
var whereClauses = GetWhereClauses(typeSubQuery, null);
whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")");
stringBuilder.Append(" where ")
.AppendJoin(" AND ", whereClauses)
.Append(" AND ")
.Append("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND ")
.Append(typeClause)
.Append(")) as itemTypes");
itemCountColumnQuery += " where " + string.Join(" AND ", whereClauses);
itemCountColumns = new Dictionary<string, string>()
{
{ "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
};
itemCountColumns = stringBuilder.ToString();
stringBuilder.Clear();
}
List<string> columns = _retriveItemColumns.ToList();
if (itemCountColumns != null)
// Unfortunately we need to add it to columns to ensure the order of the columns in the select
if (!string.IsNullOrEmpty(itemCountColumns))
{
columns.AddRange(itemCountColumns.Values);
columns.Add(itemCountColumns);
}
// do this first before calling GetFinalColumnsToSelect, otherwise ExcludeItemIds will be set by SimilarTo
@ -5317,20 +5377,20 @@ AND Type = @InternalPersonType)");
IsSeries = query.IsSeries
};
columns = GetFinalColumnsToSelect(query, columns);
var commandText = "select "
+ string.Join(',', columns)
+ GetFromText()
+ GetJoinUserDataText(query);
SetFinalColumnsToSelect(query, columns);
var innerWhereClauses = GetWhereClauses(innerQuery, null);
var innerWhereText = innerWhereClauses.Count == 0 ?
string.Empty :
" where " + string.Join(" AND ", innerWhereClauses);
stringBuilder.Append(" where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where ")
.Append(typeClause)
.Append(" AND ItemId in (select guid from TypedBaseItems");
if (innerWhereClauses.Count > 0)
{
stringBuilder.Append(" where ")
.AppendJoin(" AND ", innerWhereClauses);
}
var whereText = " where Type=@SelectType And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))";
stringBuilder.Append("))");
var outerQuery = new InternalItemsQuery(query.User)
{
@ -5355,23 +5415,31 @@ AND Type = @InternalPersonType)");
};
var outerWhereClauses = GetWhereClauses(outerQuery, null);
if (outerWhereClauses.Count != 0)
{
whereText += " AND " + string.Join(" AND ", outerWhereClauses);
stringBuilder.Append(" AND ")
.AppendJoin(" AND ", outerWhereClauses);
}
commandText += whereText + " group by PresentationUniqueKey";
var whereText = stringBuilder.ToString();
stringBuilder.Clear();
stringBuilder.Append("select ")
.AppendJoin(',', columns)
.Append(FromText)
.Append(GetJoinUserDataText(query))
.Append(whereText)
.Append(" group by PresentationUniqueKey");
if (query.OrderBy.Count != 0
|| query.SimilarTo != null
|| !string.IsNullOrEmpty(query.SearchTerm))
{
commandText += GetOrderByText(query);
stringBuilder.Append(GetOrderByText(query));
}
else
{
commandText += " order by SortName";
stringBuilder.Append(" order by SortName");
}
if (query.Limit.HasValue || query.StartIndex.HasValue)
@ -5380,32 +5448,39 @@ AND Type = @InternalPersonType)");
if (query.Limit.HasValue || offset > 0)
{
commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture);
stringBuilder.Append(" LIMIT ")
.Append(query.Limit ?? int.MaxValue);
}
if (offset > 0)
{
commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture);
stringBuilder.Append(" OFFSET ")
.Append(offset);
}
}
var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0;
var statementTexts = new List<string>();
string commandText = string.Empty;
if (!isReturningZeroItems)
{
statementTexts.Add(commandText);
commandText = stringBuilder.ToString();
}
string countText = string.Empty;
if (query.EnableTotalRecordCount)
{
var countText = "select "
+ string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+ GetFromText()
+ GetJoinUserDataText(query)
+ whereText;
stringBuilder.Clear();
var columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" };
SetFinalColumnsToSelect(query, columnsToSelect);
stringBuilder.Append("select ")
.AppendJoin(',', columnsToSelect)
.Append(FromText)
.Append(GetJoinUserDataText(query))
.Append(whereText);
statementTexts.Add(countText);
countText = stringBuilder.ToString();
}
var list = new List<(BaseItem, ItemCounts)>();
@ -5415,11 +5490,9 @@ AND Type = @InternalPersonType)");
connection.RunInTransaction(
db =>
{
var statements = PrepareAll(db, statementTexts);
if (!isReturningZeroItems)
{
using (var statement = statements[0])
using (var statement = PrepareStatement(db, commandText))
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))
@ -5460,13 +5533,7 @@ AND Type = @InternalPersonType)");
if (query.EnableTotalRecordCount)
{
commandText = "select "
+ string.Join(',', GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" }))
+ GetFromText()
+ GetJoinUserDataText(query)
+ whereText;
using (var statement = statements[statements.Length - 1])
using (var statement = PrepareStatement(db, countText))
{
statement.TryBind("@SelectType", returnType);
if (EnableJoinUserData(query))

@ -28,19 +28,9 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(typeName));
}
return _typeMap.GetOrAdd(typeName, LookupType);
}
/// <summary>
/// Lookups the type.
/// </summary>
/// <param name="typeName">Name of the type.</param>
/// <returns>Type.</returns>
private Type? LookupType(string typeName)
{
return AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType(typeName))
.FirstOrDefault(t => t != null);
return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType(k))
.FirstOrDefault(t => t != null));
}
}
}

@ -23,16 +23,17 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscUtils.Udf" Version="0.16.4" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.9" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
<PackageReference Include="sharpcompress" Version="0.28.2" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.0" />
<PackageReference Include="sharpcompress" Version="0.28.3" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup>
@ -44,12 +45,13 @@
<TargetFramework>net5.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
<AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->

@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Net;
using MediaBrowser.Common.Extensions;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
var allowTokenInfoUpdate = authInfo.Client == null || !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(authInfo.Device))
{

@ -7,7 +7,7 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Json;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session;

@ -6,11 +6,11 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Server.Implementations.IO
{
@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath;
private static readonly bool _isEnvironmentCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
public ManagedFileSystem(
ILogger<ManagedFileSystem> logger,
@ -243,8 +243,8 @@ namespace Emby.Server.Implementations.IO
{
result.Length = fileInfo.Length;
// Issue #2354 get the size of files behind symbolic links
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
{
try
{
@ -401,7 +401,7 @@ namespace Emby.Server.Implementations.IO
public virtual void SetHidden(string path, bool isHidden)
{
if (OperatingSystem.Id != OperatingSystemId.Windows)
if (!OperatingSystem.IsWindows())
{
return;
}
@ -425,7 +425,7 @@ namespace Emby.Server.Implementations.IO
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
{
if (OperatingSystem.Id != OperatingSystemId.Windows)
if (!OperatingSystem.IsWindows())
{
return;
}
@ -618,13 +618,13 @@ namespace Emby.Server.Implementations.IO
{
files = files.Where(i =>
{
var ext = i.Extension;
if (ext == null)
var ext = i.Extension.AsSpan();
if (ext.IsEmpty)
{
return false;
}
return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
});
}
@ -636,8 +636,7 @@ namespace Emby.Server.Implementations.IO
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);
return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", enumerationOptions));
}
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
@ -672,13 +671,13 @@ namespace Emby.Server.Implementations.IO
{
files = files.Where(i =>
{
var ext = Path.GetExtension(i);
if (ext == null)
var ext = Path.GetExtension(i.AsSpan());
if (ext.IsEmpty)
{
return false;
}
return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
});
}

@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null)
{
// Don't resolve these into audio files
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
&& _libraryManager.IsAudioFile(filename))
{
return true;

@ -21,6 +21,7 @@ using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
@ -696,25 +697,32 @@ namespace Emby.Server.Implementations.Library
}
private IEnumerable<BaseItem> ResolveFileList(
IEnumerable<FileSystemMetadata> fileList,
IReadOnlyList<FileSystemMetadata> fileList,
IDirectoryService directoryService,
Folder parent,
string collectionType,
IItemResolver[] resolvers,
LibraryOptions libraryOptions)
{
return fileList.Select(f =>
// Given that fileList is a list we can save enumerator allocations by indexing
for (var i = 0; i < fileList.Count; i++)
{
var file = fileList[i];
BaseItem result = null;
try
{
return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions);
result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error resolving path {path}", f.FullName);
return null;
_logger.LogError(ex, "Error resolving path {Path}", file.FullName);
}
}).Where(i => i != null);
if (result != null)
{
yield return result;
}
}
}
/// <summary>
@ -1065,17 +1073,17 @@ namespace Emby.Server.Implementations.Library
// Start by just validating the children of the root, but go no further
await RootFolder.ValidateChildren(
new SimpleProgress<double>(),
cancellationToken,
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
recursive: false).ConfigureAwait(false);
recursive: false,
cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
await GetUserRootFolder().ValidateChildren(
new SimpleProgress<double>(),
cancellationToken,
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
recursive: false).ConfigureAwait(false);
recursive: false,
cancellationToken).ConfigureAwait(false);
// Quickly scan CollectionFolders for changes
foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
@ -1095,7 +1103,7 @@ namespace Emby.Server.Implementations.Library
innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
// Validate the entire media library
await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);
await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
progress.Report(96);
@ -2076,7 +2084,7 @@ namespace Emby.Server.Implementations.Library
return new List<Folder>();
}
return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>().ToList());
return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>());
}
public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren)
@ -2101,10 +2109,10 @@ namespace Emby.Server.Implementations.Library
return GetCollectionFoldersInternal(item, allUserRootChildren);
}
private static List<Folder> GetCollectionFoldersInternal(BaseItem item, List<Folder> allUserRootChildren)
private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
{
return allUserRootChildren
.Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase))
.Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path.AsSpan(), StringComparison.OrdinalIgnoreCase))
.ToList();
}
@ -2112,9 +2120,9 @@ namespace Emby.Server.Implementations.Library
{
if (!(item is CollectionFolder collectionFolder))
{
// List.Find is more performant than FirstOrDefault due to enumerator allocation
collectionFolder = GetCollectionFolders(item)
.OfType<CollectionFolder>()
.FirstOrDefault();
.Find(folder => folder is CollectionFolder) as CollectionFolder;
}
return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
@ -2500,8 +2508,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public bool IsVideoFile(string path)
{
var resolver = new VideoResolver(GetNamingOptions());
return resolver.IsVideoFile(path);
return VideoResolver.IsVideoFile(path, GetNamingOptions());
}
/// <inheritdoc />
@ -2533,9 +2540,10 @@ namespace Emby.Server.Implementations.Library
{
episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
// Resolve from parent folder if it's not the Season folder
if (episodeInfo == null && episode.Parent.GetType() == typeof(Folder))
var parent = episode.GetParent();
if (episodeInfo == null && parent.GetType() == typeof(Folder))
{
episodeInfo = resolver.Resolve(episode.Parent.Path, true, null, null, isAbsoluteNaming);
episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
if (episodeInfo != null)
{
// add the container
@ -2679,6 +2687,7 @@ namespace Emby.Server.Implementations.Library
return changed;
}
/// <inheritdoc />
public NamingOptions GetNamingOptions()
{
if (_namingOptions == null)
@ -2692,13 +2701,12 @@ namespace Emby.Server.Implementations.Library
public ItemLookupInfo ParseName(string name)
{
var resolver = new VideoResolver(GetNamingOptions());
var result = resolver.CleanDateTime(name);
var namingOptions = GetNamingOptions();
var result = VideoResolver.CleanDateTime(name, namingOptions);
return new ItemLookupInfo
{
Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name,
Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name,
Year = result.Year
};
}
@ -2712,9 +2720,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
var videoListResolver = new VideoListResolver(namingOptions);
var videos = videoListResolver.Resolve(fileSystemChildren);
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
@ -2758,9 +2764,7 @@ namespace Emby.Server.Implementations.Library
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
.ToList();
var videoListResolver = new VideoListResolver(namingOptions);
var videos = videoListResolver.Resolve(fileSystemChildren);
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));

@ -12,7 +12,7 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;

@ -15,7 +15,7 @@ using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@ -352,7 +352,7 @@ namespace Emby.Server.Implementations.Library
private string[] NormalizeLanguage(string language)
{
if (language == null)
if (string.IsNullOrEmpty(language))
{
return Array.Empty<string>();
}
@ -381,8 +381,7 @@ namespace Emby.Server.Implementations.Library
}
}
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
var audioLangage = defaultAudioIndex == null
@ -411,9 +410,7 @@ namespace Emby.Server.Implementations.Library
}
}
var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
? Array.Empty<string>()
: NormalizeLanguage(user.AudioLanguagePreference);
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
}

@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
using DiscUtils.Udf;
using Emby.Naming.Video;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@ -47,11 +48,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new()
{
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
var namingOptions = LibraryManager.GetNamingOptions();
// If the path is a file check for a matching extensions
var parser = new VideoResolver(namingOptions);
if (args.IsDirectory)
{
TVideoType video = null;
@ -66,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
{
videoInfo = parser.ResolveDirectory(args.Path);
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@ -84,7 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
{
videoInfo = parser.ResolveDirectory(args.Path);
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@ -102,7 +101,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
else if (IsDvdFile(filename))
{
videoInfo = parser.ResolveDirectory(args.Path);
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null)
{
@ -132,7 +131,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
else
{
var videoInfo = parser.Resolve(args.Path, false, false);
var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
if (videoInfo == null)
{
@ -203,6 +202,22 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
video.IsoType = IsoType.BluRay;
}
else
{
// use disc-utils, both DVDs and BDs use UDF filesystem
using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
{
UdfReader udfReader = new UdfReader(videoFileStream);
if (udfReader.DirectoryExists("VIDEO_TS"))
{
video.IsoType = IsoType.Dvd;
}
else if (udfReader.DirectoryExists("BDMV"))
{
video.IsoType = IsoType.BluRay;
}
}
}
}
}
@ -252,10 +267,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected void Set3DFormat(Video video)
{
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
var resolver = new Format3DParser(namingOptions);
var result = resolver.Parse(video.Path);
var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
Set3DFormat(video, result.Is3D, result.Format3D);
}

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@ -257,10 +258,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
var namingOptions = LibraryManager.GetNamingOptions();
var resolver = new VideoListResolver(namingOptions);
var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList();
var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
var result = new MultiItemResolverResult
{
@ -537,7 +537,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return returnVideo;
}
private bool IsInvalid(Folder parent, string collectionType)
private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType)
{
if (parent != null)
{
@ -547,12 +547,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
if (string.IsNullOrEmpty(collectionType))
if (collectionType.IsEmpty)
{
return false;
}
return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase);
return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase);
}
}
}

@ -5,12 +5,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Diacritics.Extensions;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;

@ -11,9 +11,9 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;

@ -7,7 +7,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using MediaBrowser.Common.Json;
using Jellyfin.Extensions.Json;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV

@ -15,7 +15,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Json;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
@ -789,7 +789,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
var channelNumber = GetChannelNumber(channel);
var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase))
var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase))
?? new ScheduleDirect.Station
{
stationID = channel.stationID

@ -12,8 +12,9 @@ using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;

@ -10,9 +10,9 @@ using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
@ -43,22 +43,29 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
{
if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
if (info == null)
{
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
if (!string.IsNullOrEmpty(info.UserAgent))
{
requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
}
throw new ArgumentNullException(nameof(info));
}
if (!info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return File.OpenRead(info.Url);
}
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(requestMessage, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
if (!string.IsNullOrEmpty(info.UserAgent))
{
requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
}
return File.OpenRead(info.Url);
// Set HttpCompletionOption.ResponseHeadersRead to prevent timeouts on larger files
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStreamAsync(cancellationToken);
}
private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
@ -82,7 +89,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
{
extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim();
_logger.LogInformation("Found m3u channel: {0}", extInf);
}
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
@ -98,6 +104,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
channel.Path = trimmedLine;
channels.Add(channel);
_logger.LogInformation("Parsed channel: {ChannelName}", channel.Name);
extInf = string.Empty;
}
}

@ -2,24 +2,24 @@
"Artists": "Kunstenare",
"Channels": "Kanale",
"Folders": "Lêergidse",
"Favorites": "Gunstellinge",
"Favorites": "Gunstelinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album Kunstenaars",
"HeaderAlbumArtists": "Kunstenaars se Album",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
"Movies": "Flieks",
"Shows": "Televisie Reekse",
"HeaderContinueWatching": "Kyk Verder",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Fotos",
"Photos": "Foto's",
"Playlists": "Snitlyste",
"HeaderFavoriteArtists": "Gunsteling Kunstenaars",
"HeaderFavoriteAlbums": "Gunsteling Albums",
"Sync": "Sinkroniseer",
"HeaderFavoriteSongs": "Gunsteling Liedjies",
"Songs": "Liedjies",
"DeviceOnlineWithName": "{0} gekoppel is",
"DeviceOnlineWithName": "{0} is gekoppel",
"DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
@ -71,7 +71,7 @@
"NameSeasonUnknown": "Seisoen Onbekend",
"NameSeasonNumber": "Seisoen {0}",
"NameInstallFailed": "{0} installering het misluk",
"MusicVideos": "Musiek videos",
"MusicVideos": "Musiek Videos",
"Music": "Musiek",
"MixedContent": "Gemengde inhoud",
"MessageServerConfigurationUpdated": "Bediener konfigurasie is opgedateer",
@ -79,15 +79,15 @@
"MessageApplicationUpdatedTo": "Jellyfin Bediener is opgedateer na {0}",
"MessageApplicationUpdated": "Jellyfin Bediener is opgedateer",
"Latest": "Nuutste",
"LabelRunningTimeValue": "Lopende tyd: {0}",
"LabelRunningTimeValue": "Werktyd: {0}",
"LabelIpAddressValue": "IP adres: {0}",
"ItemRemovedWithName": "{0} is uit versameling verwyder",
"ItemAddedWithName": "{0} is in die versameling",
"HomeVideos": "Tuis opnames",
"ItemAddedWithName": "{0} is by die versameling gevoeg",
"HomeVideos": "Tuis Videos",
"HeaderRecordingGroups": "Groep Opnames",
"Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
"ChapterNameValue": "Hoofstuk",
"ChapterNameValue": "Hoofstuk {0}",
"CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
"Albums": "Albums",
@ -117,5 +117,7 @@
"Forced": "Geforseer",
"Default": "Oorspronklik",
"TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.",
"TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon"
"TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon",
"TaskOptimizeDatabaseDescription": "Komprimeer databasis en verkort vrye ruimte. As hierdie taak uitgevoer word nadat die media versameling geskandeer is of ander veranderings aangebring is wat databasisaanpassings impliseer, kan dit die prestasie verbeter.",
"TaskOptimizeDatabase": "Optimaliseer databasis"
}

@ -118,5 +118,7 @@
"TaskCleanActivityLog": "حذف سجل الأنشطة",
"Default": "الإعدادات الافتراضية",
"Undefined": "غير معرف",
"Forced": "ملحقة"
"Forced": "ملحقة",
"TaskOptimizeDatabaseDescription": "يضغط قاعدة البيانات ويقتطع المساحة الحرة. تشغيل هذه المهمة بعد فحص المكتبة أو إجراء تغييرات أخرى تشير ضمنًا إلى أن تعديلات قاعدة البيانات قد تؤدي إلى تحسين الأداء.",
"TaskOptimizeDatabase": "تحسين قاعدة البيانات"
}

@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}",
"Channels": "Канали",
"ChapterNameValue": "Глава {0}",
"Collections": "Поредици",
"Collections": "Колекции",
"DeviceOfflineWithName": "{0} се разкачи",
"DeviceOnlineWithName": "{0} е свързан",
"FailedLoginAttemptWithUserName": "Неуспешен опит за влизане от {0}",
@ -29,13 +29,13 @@
"Inherit": "Наследяване",
"ItemAddedWithName": "{0} е добавено към библиотеката",
"ItemRemovedWithName": "{0} е премахнато от библиотеката",
"LabelIpAddressValue": "ИП адрес: {0}",
"LabelRunningTimeValue": "Стартирано от: {0}",
"LabelIpAddressValue": "IP адрес: {0}",
"LabelRunningTimeValue": "Продължителност: {0}",
"Latest": "Последни",
"MessageApplicationUpdated": "Сървърът е обновен",
"MessageApplicationUpdatedTo": "Сървърът е обновен до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Секцията {0} от сървърната конфигурация се актуализира",
"MessageServerConfigurationUpdated": "Конфигурацията на сървъра се актуализира",
"MessageApplicationUpdated": "Сървърът беше обновен",
"MessageApplicationUpdatedTo": "Сървърът беше обновен до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Секцията {0} от сървърната конфигурация беше актуализирана",
"MessageServerConfigurationUpdated": "Конфигурацията на сървъра беше актуализирана",
"MixedContent": "Смесено съдържание",
"Movies": "Филми",
"Music": "Музика",
@ -118,5 +118,7 @@
"Forced": "Принудително",
"Default": "По подразбиране",
"TaskCleanActivityLogDescription": "Изтрива записите в дневника с активност по стари от конфигурираната възраст.",
"TaskCleanActivityLog": "Изчисти дневника с активност"
"TaskCleanActivityLog": "Изчисти дневника с активност",
"TaskOptimizeDatabaseDescription": "Прави базата данни по-компактна и освобождава място. Пускането на тази задача след сканиране на библиотеката или правене на други промени, свързани с модификации на базата данни, може да подобри производителността.",
"TaskOptimizeDatabase": "Оптимизирай базата данни"
}

@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
"Books": "Llibres",
"CameraImageUploadedFrom": "Una nova imatge de la càmera ha estat pujada des de {0}",
"CameraImageUploadedFrom": "S'ha pujat una nova imatge des de la camera desde {0}",
"Channels": "Canals",
"ChapterNameValue": "Capítol {0}",
"Collections": "Col·leccions",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Buidar Registre d'Activitat",
"Undefined": "Indefinit",
"Forced": "Forçat",
"Default": "Defecto"
"Default": "Defecto",
"TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després descanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
"TaskOptimizeDatabase": "Optimitzar la base de dades"
}

@ -15,7 +15,7 @@
"Favorites": "Oblíbené",
"Folders": "Složky",
"Genres": "Žánry",
"HeaderAlbumArtists": "Umělci alba",
"HeaderAlbumArtists": "Album umělce",
"HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení interpreti",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Smazat záznam aktivity",
"Undefined": "Nedefinované",
"Forced": "Vynucené",
"Default": "Výchozí"
"Default": "Výchozí",
"TaskOptimizeDatabaseDescription": "Zmenší databázi a odstraní prázdné místo. Spuštění této úlohy po skenování knihovny či jiných změnách databáze může zlepšit výkon.",
"TaskOptimizeDatabase": "Optimalizovat databázi"
}

@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Ryd Aktivitetslog",
"Undefined": "Udefineret",
"Forced": "Tvunget",
"Default": "Standard"
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Kompakter database og forkorter fri plads. Ved at køre denne proces efter at scanne biblioteket eller efter at ændre noget som kunne have indflydelse på databasen, kan forbedre ydeevne.",
"TaskOptimizeDatabase": "Optimér database"
}

@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung",
"Artists": "Interpreten",
"AuthenticationSucceededWithUserName": "{0} wurde angemeldet",
"AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
"Books": "Bücher",
"CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
"Channels": "Kanäle",
@ -16,7 +16,7 @@
"Folders": "Verzeichnisse",
"Genres": "Genres",
"HeaderAlbumArtists": "Album-Interpreten",
"HeaderContinueWatching": "Fortsetzen",
"HeaderContinueWatching": "Weiterschauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Interpreten",
"HeaderFavoriteEpisodes": "Lieblingsepisoden",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen",
"Undefined": "Undefiniert",
"Forced": "Erzwungen",
"Default": "Standard"
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimiert die Datenbank und trimmt den freien Speicherplatz. Die Ausführung dieser Aufgabe nach dem Scannen der Bibliothek oder nach anderen Änderungen, die Datenbankänderungen implizieren, kann die Leistung verbessern.",
"TaskOptimizeDatabase": "Datenbank optimieren"
}

@ -1,5 +1,5 @@
{
"Albums": "Άλμπουμς",
"Albums": "Άλμπουμ",
"AppDeviceValues": "Εφαρμογή: {0}, Συσκευή: {1}",
"Application": "Εφαρμογή",
"Artists": "Καλλιτέχνες",
@ -15,7 +15,7 @@
"Favorites": "Αγαπημένα",
"Folders": "Φάκελοι",
"Genres": "Είδη",
"HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
"HeaderAlbumArtists": "Άλμπουμ Καλλιτέχνη",
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
@ -39,7 +39,7 @@
"MixedContent": "Ανάμεικτο Περιεχόμενο",
"Movies": "Ταινίες",
"Music": "Μουσική",
"MusicVideos": "Μουσικά βίντεο",
"MusicVideos": "Μουσικά Βίντεο",
"NameInstallFailed": "{0} η εγκατάσταση απέτυχε",
"NameSeasonNumber": "Κύκλος {0}",
"NameSeasonUnknown": "Άγνωστος Κύκλος",
@ -62,7 +62,7 @@
"NotificationOptionVideoPlaybackStopped": "Η αναπαραγωγή βίντεο σταμάτησε",
"Photos": "Φωτογραφίες",
"Playlists": "Λίστες αναπαραγωγής",
"Plugin": "Plugin",
"Plugin": "Πρόσθετο",
"PluginInstalledWithName": "{0} εγκαταστήθηκε",
"PluginUninstalledWithName": "{0} έχει απεγκατασταθεί",
"PluginUpdatedWithName": "{0} έχει αναβαθμιστεί",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
"Undefined": "Απροσδιόριστο",
"Forced": "Εξαναγκασμένο",
"Default": "Προεπιλογή"
"Default": "Προεπιλογή",
"TaskOptimizeDatabaseDescription": "Συμπιέζει τη βάση δεδομένων και δημιουργεί ελεύθερο χώρο. Η εκτέλεση αυτής της εργασίας μετά τη σάρωση της βιβλιοθήκης ή την πραγματοποίηση άλλων αλλαγών που συνεπάγονται τροποποιήσεις της βάσης δεδομένων μπορεί να βελτιώσει την απόδοση.",
"TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων"
}

@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Clean Activity Log",
"Undefined": "Undefined",
"Forced": "Forced",
"Default": "Default"
"Default": "Default",
"TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.",
"TaskOptimizeDatabase": "Optimise database"
}

@ -17,7 +17,7 @@
"Folders": "Folders",
"Forced": "Forced",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
"HeaderAlbumArtists": "Artist's Album",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
@ -27,7 +27,7 @@
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
"HeaderRecordingGroups": "Recording Groups",
"HomeVideos": "Home videos",
"HomeVideos": "Home Videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
@ -41,7 +41,7 @@
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
"MusicVideos": "Music videos",
"MusicVideos": "Music Videos",
"NameInstallFailed": "{0} installation failed",
"NameSeasonNumber": "Season {0}",
"NameSeasonUnknown": "Season Unknown",

@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Borrar log de actividades",
"Undefined": "Indefinido",
"Forced": "Forzado",
"Default": "Por Defecto"
"Default": "Por Defecto",
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.",
"TaskOptimizeDatabase": "Optimización de base de datos"
}

@ -15,7 +15,7 @@
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
"HeaderAlbumArtists": "Artistas del Álbum",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
@ -25,7 +25,7 @@
"HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "A continuación",
"HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos caseros",
"HomeVideos": "Videos Caseros",
"Inherit": "Heredar",
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
"ItemRemovedWithName": "{0} fue removido de la biblioteca",
@ -39,7 +39,7 @@
"MixedContent": "Contenido mezclado",
"Movies": "Películas",
"Music": "Música",
"MusicVideos": "Videos musicales",
"MusicVideos": "Videos Musicales",
"NameInstallFailed": "Falló la instalación de {0}",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconocida",
@ -49,7 +49,7 @@
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
"NotificationOptionInstallationFailed": "Falla de instalación",
"NotificationOptionInstallationFailed": "Fallo en la instalación",
"NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
"NotificationOptionPluginError": "Falla de complemento",
"NotificationOptionPluginInstalled": "Complemento instalado",
@ -69,7 +69,7 @@
"ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciado",
"ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Programas",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
@ -94,9 +94,9 @@
"VersionNumber": "Versión {0}",
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
"TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
"TaskRefreshChannelsDescription": "Actualiza la información de los canales de Internet.",
"TaskRefreshChannels": "Actualizar canales",
"TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
"TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día de antigüedad.",
"TaskCleanTranscode": "Limpiar directorio de transcodificado",
"TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
"TaskUpdatePlugins": "Actualizar complementos",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Limpiar registro de actividades",
"Undefined": "Sin definir",
"Forced": "Forzado",
"Default": "Predeterminado"
"Default": "Predeterminado",
"TaskOptimizeDatabase": "Optimizar base de datos",
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos."
}

@ -15,7 +15,7 @@
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
"HeaderAlbumArtists": "Artista del álbum",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciada",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Series de Televisión",
"Shows": "Series",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}",

@ -117,5 +117,7 @@
"TaskCleanActivityLog": "Limpiar Registro de Actividades",
"Undefined": "Sin definir",
"Forced": "Forzado",
"Default": "Por Defecto"
"Default": "Por Defecto",
"TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.",
"TaskOptimizeDatabase": "Optimización de base de datos"
}

@ -117,5 +117,7 @@
"Default": "Oletus",
"TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.",
"TaskCleanActivityLog": "Tyhjennä toimintahistoria",
"Undefined": "Määrittelemätön"
"Undefined": "Määrittelemätön",
"TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastojen skannauksen tai muiden tietokantaan liittyvien muutoksien jälkeen voi parantaa suorituskykyä.",
"TaskOptimizeDatabase": "Optimoi tietokanta"
}

@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Nettoyer le journal d'activité",
"Undefined": "Non défini",
"Forced": "Forcé",
"Default": "Par défaut"
"Default": "Par défaut",
"TaskOptimizeDatabaseDescription": "Réduit les espaces vides/inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la bibliothèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
"TaskOptimizeDatabase": "Optimiser la base de données"
}

@ -88,5 +88,34 @@
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo parada",
"NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada",
"NotificationOptionUserLockedOut": "Usuario bloqueado",
"NotificationOptionTaskFailed": "Falla na tarefa axendada"
"NotificationOptionTaskFailed": "Falla na tarefa axendada",
"TaskCleanTranscodeDescription": "Borra os arquivos de transcode anteriores a un día.",
"TaskCleanTranscode": "Limpar Directorio de Transcode",
"UserStoppedPlayingItemWithValues": "{0} rematou de reproducir {1} en {2}",
"UserStartedPlayingItemWithValues": "{0} está reproducindo {1} en {2}",
"TaskDownloadMissingSubtitlesDescription": "Busca en internet por subtítulos que faltan baseado na configuración de metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos que faltan",
"TaskRefreshChannelsDescription": "Refresca a información do canle de internet.",
"TaskRefreshChannels": "Refrescar Canles",
"TaskUpdatePluginsDescription": "Descarga e instala actualizacións para plugins que están configurados para actualizarse automáticamente.",
"TaskRefreshPeopleDescription": "Actualiza os metadatos dos actores e directores na túa libraría multimedia.",
"TaskRefreshPeople": "Refrescar Persoas",
"TaskCleanLogsDescription": "Borra arquivos de rexistro que son mais antigos que {0} días.",
"TaskRefreshLibraryDescription": "Escanea a tua libraría multimedia buscando novos arquivos e refrescando os metadatos.",
"TaskRefreshLibrary": "Escanear Libraría Multimedia",
"TaskRefreshChapterImagesDescription": "Crea previsualizacións para videos que teñen capítulos.",
"TaskRefreshChapterImages": "Extraer Imaxes dos Capítulos",
"TaskCleanCacheDescription": "Borra ficheiros da caché que xa non son necesarios para o sistema.",
"TaskCleanCache": "Limpa Directorio de Caché",
"TaskCleanActivityLogDescription": "Borra as entradas no rexistro de actividade anteriores á data configurada.",
"TasksApplicationCategory": "Aplicación",
"ValueSpecialEpisodeName": "Especial - {0}",
"ValueHasBeenAddedToLibrary": "{0} foi engadido a túa libraría multimedia",
"TasksLibraryCategory": "Libraría",
"TasksMaintenanceCategory": "Mantemento",
"VersionNumber": "Versión {0}",
"UserPolicyUpdatedWithName": "A política de usuario foi actualizada para {0}",
"UserPasswordChangedWithName": "Cambiouse o contrasinal para o usuario {0}",
"UserOnlineFromDevice": "{0} está en liña desde {1}",
"UserOfflineFromDevice": "{0} desconectouse desde {1}"
}

@ -15,7 +15,7 @@
"Favorites": "Kedvencek",
"Folders": "Könyvtárak",
"Genres": "Műfajok",
"HeaderAlbumArtists": "Album előadók",
"HeaderAlbumArtists": "Előadó albumai",
"HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteArtists": "Kedvenc előadók",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Tevékenységnapló törlése",
"Undefined": "Meghatározatlan",
"Forced": "Kényszerített",
"Default": "Alapértelmezett"
"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"
}

@ -15,7 +15,7 @@
"Favorites": "Preferiti",
"Folders": "Cartelle",
"Genres": "Generi",
"HeaderAlbumArtists": "Artisti degli Album",
"HeaderAlbumArtists": "Artisti dell'Album",
"HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti",
@ -25,7 +25,7 @@
"HeaderLiveTV": "Diretta TV",
"HeaderNextUp": "Prossimo",
"HeaderRecordingGroups": "Gruppi di Registrazione",
"HomeVideos": "Video personali",
"HomeVideos": "Video Personali",
"Inherit": "Eredita",
"ItemAddedWithName": "{0} è stato aggiunto alla libreria",
"ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
@ -39,7 +39,7 @@
"MixedContent": "Contenuto misto",
"Movies": "Film",
"Music": "Musica",
"MusicVideos": "Video musicali",
"MusicVideos": "Video Musicali",
"NameInstallFailed": "{0} installazione fallita",
"NameSeasonNumber": "Stagione {0}",
"NameSeasonUnknown": "Stagione sconosciuta",
@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} fallito",
"ScheduledTaskStartedWithName": "{0} avviati",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Programmi",
"Shows": "Serie TV",
"Songs": "Canzoni",
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
"SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}",
@ -118,5 +118,7 @@
"TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie delletà configurata.",
"Undefined": "Non Definito",
"Forced": "Forzato",
"Default": "Predefinito"
"Default": "Predefinito",
"TaskOptimizeDatabaseDescription": "Compatta Database e tronca spazi liberi. Eseguire questa azione dopo la scansione o dopo aver fatto altri cambiamenti inerenti il database potrebbe aumentarne la performance.",
"TaskOptimizeDatabase": "Ottimizza Database"
}

@ -15,7 +15,7 @@
"Favorites": "お気に入り",
"Folders": "フォルダー",
"Genres": "ジャンル",
"HeaderAlbumArtists": "アルバムアーティスト",
"HeaderAlbumArtists": "アーティストのアルバム",
"HeaderContinueWatching": "視聴を続ける",
"HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト",
@ -117,5 +117,7 @@
"TaskCleanActivityLog": "アクティビティの履歴を消去",
"Undefined": "未定義",
"Forced": "強制",
"Default": "デフォルト"
"Default": "デフォルト",
"TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリのスキャン後でこのタスクを実行するとパフォーマンスが向上する可能性があります。",
"TaskOptimizeDatabase": "データベースの最適化"
}

@ -15,7 +15,7 @@
"Favorites": "Tañdaulylar",
"Folders": "Qaltalar",
"Genres": "Janrlar",
"HeaderAlbumArtists": "Älbom oryndauşylary",
"HeaderAlbumArtists": "Oryndauşynyñ älbomy",
"HeaderContinueWatching": "Qaraudy jalğastyru",
"HeaderFavoriteAlbums": "Tañdauly älbomdar",
"HeaderFavoriteArtists": "Tañdauly oryndauşylar",
@ -118,5 +118,7 @@
"TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaña faildardy skanerleidі jäne metaderekterdı jañğyrtady.",
"TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.",
"TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.",
"TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teñşelgen jasynan asqan jazbalary joiady."
"TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teñşelgen jasynan asqan jazbalary joiady.",
"TaskOptimizeDatabaseDescription": "Derekqordy qysyp, bos oryndy qysqartady. Būl tapsyrmany tasyğyşhanany skanerlegennen keiın nemese derekqorğa meñzeitın basqa özgertuler ıstelgennen keiın oryndau önımdılıktı damytuy mümkın.",
"TaskOptimizeDatabase": "Derekqordy oñtailandyru"
}

@ -15,7 +15,7 @@
"Favorites": "즐겨찾기",
"Folders": "폴더",
"Genres": "장르",
"HeaderAlbumArtists": "앨범 아티스트",
"HeaderAlbumArtists": "아티스트의 앨범",
"HeaderContinueWatching": "계속 시청하기",
"HeaderFavoriteAlbums": "즐겨찾는 앨범",
"HeaderFavoriteArtists": "즐겨찾는 아티스트",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "활동내역청소",
"Undefined": "일치하지 않음",
"Forced": "강제하기",
"Default": "기본 설정"
"Default": "기본 설정",
"TaskOptimizeDatabaseDescription": "데이터베이스를 압축하고 사용 가능한 공간을 늘립니다. 라이브러리를 검색한 후 이 작업을 실행하거나 데이터베이스 수정같은 비슷한 작업을 수행하면 성능이 향상될 수 있습니다.",
"TaskOptimizeDatabase": "데이터베이스 최적화"
}

@ -117,5 +117,7 @@
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
"TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
"Undefined": "Nenoteikts",
"Default": "Noklusējums"
"Default": "Noklusējums",
"TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
"TaskOptimizeDatabase": "Optimizēt datubāzi"
}

@ -103,7 +103,7 @@
"ValueSpecialEpisodeName": "പ്രത്യേക - {0}",
"Collections": "ശേഖരങ്ങൾ",
"Folders": "ഫോൾഡറുകൾ",
"HeaderAlbumArtists": "ആൽബം ആർട്ടിസ്റ്റുകൾ",
"HeaderAlbumArtists": "കലാകാരന്റെ ആൽബം",
"Sync": "സമന്വയിപ്പിക്കുക",
"Movies": "സിനിമകൾ",
"Photos": "ഫോട്ടോകൾ",
@ -117,5 +117,7 @@
"Favorites": "പ്രിയങ്കരങ്ങൾ",
"Books": "പുസ്തകങ്ങൾ",
"Genres": "വിഭാഗങ്ങൾ",
"Channels": "ചാനലുകൾ"
"Channels": "ചാനലുകൾ",
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക"
}

@ -5,7 +5,7 @@
"Artists": "Artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku-buku",
"CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}",
"CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}",
"Channels": "Saluran",
"ChapterNameValue": "Bab {0}",
"Collections": "Koleksi",
@ -101,5 +101,13 @@
"Forced": "Paksa",
"Default": "Asal",
"TaskCleanCache": "Bersihkan Direktori Cache",
"TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi."
"TaskCleanActivityLogDescription": "Padamkan entri log aktiviti yang lebih tua daripada usia yang dikonfigurasi.",
"TaskRefreshPeople": "Segarkan Orang",
"TaskCleanLogsDescription": "Padamkan fail log yang berumur lebih dari {0} hari.",
"TaskCleanLogs": "Bersihkan Direktotri Log",
"TaskRefreshLibraryDescription": "Imbas perpustakaan media untuk mencari fail-fail baru dan menyegarkan metadata.",
"TaskRefreshLibrary": "Imbas Perpustakaan Media",
"TaskRefreshChapterImagesDescription": "Membuat gambaran kecil untuk video yang mempunyai bab.",
"TaskRefreshChapterImages": "Ekstrak Gambar-gambar Bab",
"TaskCleanCacheDescription": "Menghapuskan fail cache yang tidak lagi diperlukan oleh sistem."
}

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

Loading…
Cancel
Save