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

This commit is contained in:
cvium 2021-09-03 21:25:18 +02:00
commit 048c478b0d
511 changed files with 8268 additions and 6089 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

4
.github/stale.yml vendored
View File

@ -23,3 +23,7 @@ markComment: >
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). 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 # Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false closeComment: false
# Disable automatic closing of pull requests
pulls:
daysUntilClose: false

View File

@ -1,21 +1,33 @@
name: Automation name: Automation
on: on:
push:
branches:
- master
pull_request_target: pull_request_target:
issue_comment:
jobs: jobs:
main: label:
name: Labeling
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps: steps:
- name: Does PR has the stable backport label? - name: Apply label
uses: Dreamcodeio/does-pr-has-label@v1.2 uses: eps1lon/actions-label-merge-conflict@v2.0.1
id: checkLabel if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with: 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 - name: Remove from 'Current Release' 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) && !steps.checkLabel.outputs.hasLabel if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true continue-on-error: true
with: with:
project: Current Release project: Current Release
@ -23,7 +35,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project - 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' if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true continue-on-error: true
with: with:
@ -32,8 +44,8 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project - name: Add to 'Current Release' 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) && steps.checkLabel.outputs.hasLabel if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true continue-on-error: true
with: with:
project: Current Release 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)" 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 - 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 if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true continue-on-error: true
with: with:
@ -55,7 +67,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project - 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' if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true continue-on-error: true
with: with:

View File

@ -1,4 +1,4 @@
name: Stable Backport Check name: Commands
on: on:
issue_comment: issue_comment:
types: types:
@ -10,6 +10,29 @@ on:
- synchronize - synchronize
jobs: 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: check-backport:
name: 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' ) }} 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' ) }}

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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
View File

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

View File

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

14
Directory.Build.props Normal file
View File

@ -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>

View File

@ -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 \ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist && mv dist /dist
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder FROM debian:bullseye-slim as app
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
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" 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) # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" 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 # https://github.com/intel/compute-runtime/releases
ARG GMMLIB_VERSION=20.3.2 ARG GMMLIB_VERSION=20.3.2
ARG IGC_VERSION=1.0.5435 ARG IGC_VERSION=1.0.5435
@ -73,6 +62,19 @@ ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8 ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en 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 EXPOSE 8096
VOLUME /cache /config /media VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \ ENTRYPOINT ["./jellyfin/jellyfin", \

View File

@ -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 \ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist && 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 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 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" ARG DEBIAN_FRONTEND="noninteractive"
@ -61,14 +50,25 @@ RUN apt-get update \
&& chmod 777 /cache /config /media \ && 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 && 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 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8 ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8 ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en 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 EXPOSE 8096
VOLUME /cache /config /media VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \ ENTRYPOINT ["./jellyfin/jellyfin", \

View File

@ -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 \ && npm ci --no-audit --unsafe-perm \
&& mv dist /dist && 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 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 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND="noninteractive" 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 \ && 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 && 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 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
ENV LC_ALL en_US.UTF-8 ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8 ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en 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 EXPOSE 8096
VOLUME /cache /config /media VOLUME /cache /config /media
ENTRYPOINT ["./jellyfin/jellyfin", \ ENTRYPOINT ["./jellyfin/jellyfin", \

View File

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

View File

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

View File

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

View File

@ -288,21 +288,14 @@ namespace Emby.Dlna.ContentDirectory
/// <returns>The xml feature list.</returns> /// <returns>The xml feature list.</returns>
private static string WriteFeatureListXml() private static string WriteFeatureListXml()
{ {
// TODO: clean this up return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
var builder = new StringBuilder(); + "<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\">"
builder.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); + "<container id=\"I\" type=\"object.item.imageItem\"/>"
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\">"); + "<container id=\"A\" type=\"object.item.audioItem\"/>"
+ "<container id=\"V\" type=\"object.item.videoItem\"/>"
builder.Append("<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">"); + "</Feature>"
builder.Append("<container id=\"I\" type=\"object.item.imageItem\"/>"); + "</Features>";
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();
} }
/// <summary> /// <summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -137,7 +137,7 @@ namespace Emby.Naming.Common
CleanStrings = new[] 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 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 SupportsAbsoluteEpisodeNumbers = true
}, },
// Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names // 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 // [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 IsNamed = true
}, },
@ -305,6 +305,12 @@ namespace Emby.Naming.Common
// *** End Kodi Standard Naming // *** 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]+)[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
{ {
IsNamed = true IsNamed = true
@ -362,12 +368,6 @@ namespace Emby.Naming.Common
IsOptimistic = true, IsOptimistic = true,
IsNamed = 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[] EpisodeWithoutSeasonExpressions = new[]

View File

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

View File

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

View File

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

View File

@ -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);
}
}
}

View File

@ -1,45 +1,37 @@
using System; using System;
using System.Linq;
using Emby.Naming.Common; using Emby.Naming.Common;
namespace Emby.Naming.Video namespace Emby.Naming.Video
{ {
/// <summary> /// <summary>
/// Parste 3D format related flags. /// Parse 3D format related flags.
/// </summary> /// </summary>
public class Format3DParser public static class Format3DParser
{ {
private readonly NamingOptions _options; // Static default result to save on allocation costs.
private static readonly Format3DResult _defaultResult = new (false, null);
/// <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;
}
/// <summary> /// <summary>
/// Parse 3D format related flags. /// Parse 3D format related flags.
/// </summary> /// </summary>
/// <param name="path">Path to file.</param> /// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <returns>Returns <see cref="Format3DResult"/> object.</returns> /// <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; int oldLen = namingOptions.VideoFlagDelimiters.Length;
var delimiters = new char[oldLen + 1]; Span<char> delimiters = stackalloc char[oldLen + 1];
_options.VideoFlagDelimiters.CopyTo(delimiters, 0); namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
delimiters[oldLen] = ' '; 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) 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)); var index = path.IndexOfAny(delimiters);
result.Is3D = !string.IsNullOrEmpty(result.Format3D); if (index == -1)
if (result.Is3D)
{ {
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);
if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
{
format = flag;
result.Tokens.Add(rule.Token);
}
break;
}
foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
} }
result.Is3D = foundPrefix && !string.IsNullOrEmpty(format); var currentSlice = path[..index];
result.Format3D = format; path = path[(index + 1)..];
if (!foundPrefix)
{
foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
continue;
}
is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
if (is3D)
{
format3D = rule.Token;
break;
}
} }
return result; return is3D ? new Format3DResult(true, format3D) : _defaultResult;
} }
} }
} }

View File

@ -1,5 +1,3 @@
using System.Collections.Generic;
namespace Emby.Naming.Video namespace Emby.Naming.Video
{ {
/// <summary> /// <summary>
@ -10,27 +8,24 @@ namespace Emby.Naming.Video
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Format3DResult"/> class. /// Initializes a new instance of the <see cref="Format3DResult"/> class.
/// </summary> /// </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> /// <summary>
/// Gets or sets a value indicating whether [is3 d]. /// Gets a value indicating whether [is3 d].
/// </summary> /// </summary>
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value> /// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
public bool Is3D { get; set; } public bool Is3D { get; }
/// <summary> /// <summary>
/// Gets or sets the format3 d. /// Gets the format3 d.
/// </summary> /// </summary>
/// <value>The format3 d.</value> /// <value>The format3 d.</value>
public string? Format3D { get; set; } public string? Format3D { get; }
/// <summary>
/// Gets or sets the tokens.
/// </summary>
/// <value>The tokens.</value>
public List<string> Tokens { get; set; }
} }
} }

View File

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

View File

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

View File

@ -12,31 +12,19 @@ namespace Emby.Naming.Video
/// <summary> /// <summary>
/// Resolves alternative versions and extras from list of video files. /// Resolves alternative versions and extras from list of video files.
/// </summary> /// </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> /// <summary>
/// Resolves alternative versions and extras from list of video files. /// Resolves alternative versions and extras from list of video files.
/// </summary> /// </summary>
/// <param name="files">List of related video files.</param> /// <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> /// <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> /// <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 var videoInfos = files
.Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory)) .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
.OfType<VideoFileInfo>() .OfType<VideoFileInfo>()
.ToList(); .ToList();
@ -46,7 +34,7 @@ namespace Emby.Naming.Video
.Where(i => i.ExtraType == null) .Where(i => i.ExtraType == null)
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory }); .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
var stackResult = new StackResolver(_options) var stackResult = new StackResolver(namingOptions)
.Resolve(nonExtras).ToList(); .Resolve(nonExtras).ToList();
var remainingFiles = videoInfos var remainingFiles = videoInfos
@ -59,23 +47,17 @@ namespace Emby.Naming.Video
{ {
var info = new VideoInfo(stack.Name) 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>() .OfType<VideoFileInfo>()
.ToList() .ToList()
}; };
info.Year = info.Files[0].Year; info.Year = info.Files[0].Year;
var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) }; var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
var extras = GetExtras(remainingFiles, extraBaseNames);
if (extras.Count > 0) if (extras.Count > 0)
{ {
remainingFiles = remainingFiles
.Except(extras)
.ToList();
info.Extras = extras; info.Extras = extras;
} }
@ -88,15 +70,12 @@ namespace Emby.Naming.Video
foreach (var media in standaloneMedia) 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; info.Year = info.Files[0].Year;
var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension }); remainingFiles.Remove(media);
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
remainingFiles = remainingFiles
.Except(extras.Concat(new[] { media }))
.ToList();
info.Extras = extras; info.Extras = extras;
@ -105,8 +84,7 @@ namespace Emby.Naming.Video
if (supportMultiVersion) if (supportMultiVersion)
{ {
list = GetVideosGroupedByVersion(list) list = GetVideosGroupedByVersion(list, namingOptions);
.ToList();
} }
// If there's only one resolved video, use the folder name as well to find extras // 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 info = list[0];
var videoPath = list[0].Files[0].Path; 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); var folderName = Path.GetFileName(parentPath);
if (!string.IsNullOrEmpty(folderName)) if (!folderName.IsEmpty)
{ {
var extras = GetExtras(remainingFiles, new List<string> { folderName }); var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
remainingFiles = remainingFiles
.Except(extras)
.ToList();
extras.AddRange(info.Extras); extras.AddRange(info.Extras);
info.Extras = extras; info.Extras = extras;
} }
@ -164,96 +137,168 @@ namespace Emby.Naming.Video
// Whatever files are left, just add them // Whatever files are left, just add them
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name) list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
{ {
Files = new List<VideoFileInfo> { i }, Files = new[] { i },
Year = i.Year Year = i.Year
})); }));
return list; return list;
} }
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos) private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
{ {
if (videos.Count == 0) if (videos.Count == 0)
{ {
return videos; return videos;
} }
var list = new List<VideoInfo>(); var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path)); if (folderName.Length <= 1 || !HaveSameYear(videos))
if (!string.IsNullOrEmpty(folderName)
&& folderName.Length > 1
&& videos.All(i => i.Files.Count == 1
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path))
&& HaveSameYear(videos))
{ {
var ordered = videos.OrderBy(i => i.Name).ToList(); return videos;
list.Add(ordered[0]);
var alternateVersionsLen = ordered.Count - 1;
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
for (int i = 0; i < alternateVersionsLen; i++)
{
alternateVersions[i] = ordered[i + 1].Files[0];
}
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;
return list;
} }
return videos; // 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++)
private bool HaveSameYear(List<VideoInfo> videos)
{
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
}
private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
{
string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{ {
// Remove the folder name before cleaning as we don't care about cleaning that part var video = videos[i];
if (folderName.Length <= testFilename.Length) if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
{ {
testFilename = testFilename.Substring(folderName.Length).Trim(); return videos;
} }
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
{
testFilename = cleanName.Trim().ToString();
}
// The CleanStringParser should have removed common keywords etc.
return string.IsNullOrEmpty(testFilename)
|| testFilename[0] == '-'
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
} }
return false; // 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));
private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames) var list = new List<VideoInfo>
{
foreach (var name in baseNames.ToList())
{ {
var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd(); videos[0]
baseNames.Add(trimmedName); };
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 remainingFiles list[0].AlternateVersions = alternateVersions;
.Where(i => i.ExtraType != null) list[0].Name = folderName.ToString();
.Where(i => baseNames.Any(b => list[0].Extras = extras;
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
.ToList(); return list;
}
private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
{
if (videos.Count == 1)
{
return true;
}
var firstYear = videos[0].Year ?? -1;
for (var i = 1; i < videos.Count; i++)
{
if ((videos[i].Year ?? -1) != firstYear)
{
return false;
}
}
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;
}
// 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 static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
{
if (baseName.IsEmpty)
{
return false;
}
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;
} }
} }
} }

View File

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

View File

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

View File

@ -77,7 +77,6 @@ namespace Emby.Notifications
{ {
_libraryManager.ItemAdded += OnLibraryManagerItemAdded; _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged; _appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated += OnActivityManagerEntryCreated; _activityManager.EntryCreated += OnActivityManagerEntryCreated;
return Task.CompletedTask; return Task.CompletedTask;
@ -132,25 +131,6 @@ namespace Emby.Notifications
return _config.GetConfiguration<NotificationOptions>("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) private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{ {
if (!FilterItem(e.Item)) if (!FilterItem(e.Item))
@ -325,7 +305,6 @@ namespace Emby.Notifications
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded; _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged; _appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
_appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
_activityManager.EntryCreated -= OnActivityManagerEntryCreated; _activityManager.EntryCreated -= OnActivityManagerEntryCreated;
_disposed = true; _disposed = true;

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json; using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Progress; using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration; 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 try
{ {

View File

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

View File

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

View File

@ -28,19 +28,9 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(typeName)); throw new ArgumentNullException(nameof(typeName));
} }
return _typeMap.GetOrAdd(typeName, LookupType); return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
} .Select(a => a.GetType(k))
.FirstOrDefault(t => t != null));
/// <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);
} }
} }
} }

View File

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

View File

@ -3,7 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
using MediaBrowser.Common.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security; 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 // 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)) if (string.IsNullOrWhiteSpace(authInfo.Device))
{ {

View File

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

View File

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

View File

@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
if (parent != null) if (parent != null)
{ {
// Don't resolve these into audio files // 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)) && _libraryManager.IsAudioFile(filename))
{ {
return true; return true;

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using DiscUtils.Udf;
using Emby.Naming.Video; using Emby.Naming.Video;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -47,11 +48,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName) protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new() where TVideoType : Video, new()
{ {
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); var namingOptions = LibraryManager.GetNamingOptions();
// If the path is a file check for a matching extensions // If the path is a file check for a matching extensions
var parser = new VideoResolver(namingOptions);
if (args.IsDirectory) if (args.IsDirectory)
{ {
TVideoType video = null; TVideoType video = null;
@ -66,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{ {
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService)) if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
{ {
videoInfo = parser.ResolveDirectory(args.Path); videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null) if (videoInfo == null)
{ {
@ -84,7 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService)) if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
{ {
videoInfo = parser.ResolveDirectory(args.Path); videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null) if (videoInfo == null)
{ {
@ -102,7 +101,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
} }
else if (IsDvdFile(filename)) else if (IsDvdFile(filename))
{ {
videoInfo = parser.ResolveDirectory(args.Path); videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
if (videoInfo == null) if (videoInfo == null)
{ {
@ -132,7 +131,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
} }
else else
{ {
var videoInfo = parser.Resolve(args.Path, false, false); var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
if (videoInfo == null) if (videoInfo == null)
{ {
@ -203,6 +202,22 @@ namespace Emby.Server.Implementations.Library.Resolvers
{ {
video.IsoType = IsoType.BluRay; 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) protected void Set3DFormat(Video video)
{ {
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions(); var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
var resolver = new Format3DParser(namingOptions);
var result = resolver.Parse(video.Path);
Set3DFormat(video, result.Is3D, result.Format3D); Set3DFormat(video, result.Is3D, result.Format3D);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,24 +2,24 @@
"Artists": "Kunstenare", "Artists": "Kunstenare",
"Channels": "Kanale", "Channels": "Kanale",
"Folders": "Lêergidse", "Folders": "Lêergidse",
"Favorites": "Gunstellinge", "Favorites": "Gunstelinge",
"HeaderFavoriteShows": "Gunsteling Vertonings", "HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}", "ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album Kunstenaars", "HeaderAlbumArtists": "Kunstenaars se Album",
"Books": "Boeke", "Books": "Boeke",
"HeaderNextUp": "Volgende", "HeaderNextUp": "Volgende",
"Movies": "Flieks", "Movies": "Flieks",
"Shows": "Televisie Reekse", "Shows": "Televisie Reekse",
"HeaderContinueWatching": "Kyk Verder", "HeaderContinueWatching": "Kyk Verder",
"HeaderFavoriteEpisodes": "Gunsteling Episodes", "HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Fotos", "Photos": "Foto's",
"Playlists": "Snitlyste", "Playlists": "Snitlyste",
"HeaderFavoriteArtists": "Gunsteling Kunstenaars", "HeaderFavoriteArtists": "Gunsteling Kunstenaars",
"HeaderFavoriteAlbums": "Gunsteling Albums", "HeaderFavoriteAlbums": "Gunsteling Albums",
"Sync": "Sinkroniseer", "Sync": "Sinkroniseer",
"HeaderFavoriteSongs": "Gunsteling Liedjies", "HeaderFavoriteSongs": "Gunsteling Liedjies",
"Songs": "Liedjies", "Songs": "Liedjies",
"DeviceOnlineWithName": "{0} gekoppel is", "DeviceOnlineWithName": "{0} is gekoppel",
"DeviceOfflineWithName": "{0} is ontkoppel", "DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings", "Collections": "Versamelings",
"Inherit": "Ontvang", "Inherit": "Ontvang",
@ -71,7 +71,7 @@
"NameSeasonUnknown": "Seisoen Onbekend", "NameSeasonUnknown": "Seisoen Onbekend",
"NameSeasonNumber": "Seisoen {0}", "NameSeasonNumber": "Seisoen {0}",
"NameInstallFailed": "{0} installering het misluk", "NameInstallFailed": "{0} installering het misluk",
"MusicVideos": "Musiek videos", "MusicVideos": "Musiek Videos",
"Music": "Musiek", "Music": "Musiek",
"MixedContent": "Gemengde inhoud", "MixedContent": "Gemengde inhoud",
"MessageServerConfigurationUpdated": "Bediener konfigurasie is opgedateer", "MessageServerConfigurationUpdated": "Bediener konfigurasie is opgedateer",
@ -79,15 +79,15 @@
"MessageApplicationUpdatedTo": "Jellyfin Bediener is opgedateer na {0}", "MessageApplicationUpdatedTo": "Jellyfin Bediener is opgedateer na {0}",
"MessageApplicationUpdated": "Jellyfin Bediener is opgedateer", "MessageApplicationUpdated": "Jellyfin Bediener is opgedateer",
"Latest": "Nuutste", "Latest": "Nuutste",
"LabelRunningTimeValue": "Lopende tyd: {0}", "LabelRunningTimeValue": "Werktyd: {0}",
"LabelIpAddressValue": "IP adres: {0}", "LabelIpAddressValue": "IP adres: {0}",
"ItemRemovedWithName": "{0} is uit versameling verwyder", "ItemRemovedWithName": "{0} is uit versameling verwyder",
"ItemAddedWithName": "{0} is in die versameling", "ItemAddedWithName": "{0} is by die versameling gevoeg",
"HomeVideos": "Tuis opnames", "HomeVideos": "Tuis Videos",
"HeaderRecordingGroups": "Groep Opnames", "HeaderRecordingGroups": "Groep Opnames",
"Genres": "Genres", "Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}", "FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
"ChapterNameValue": "Hoofstuk", "ChapterNameValue": "Hoofstuk {0}",
"CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}", "CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer", "AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
"Albums": "Albums", "Albums": "Albums",
@ -117,5 +117,7 @@
"Forced": "Geforseer", "Forced": "Geforseer",
"Default": "Oorspronklik", "Default": "Oorspronklik",
"TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.", "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"
} }

View File

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

View File

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

View File

@ -5,7 +5,7 @@
"Artists": "Artistes", "Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament", "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
"Books": "Llibres", "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", "Channels": "Canals",
"ChapterNameValue": "Capítol {0}", "ChapterNameValue": "Capítol {0}",
"Collections": "Col·leccions", "Collections": "Col·leccions",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Buidar Registre d'Activitat", "TaskCleanActivityLog": "Buidar Registre d'Activitat",
"Undefined": "Indefinit", "Undefined": "Indefinit",
"Forced": "Forçat", "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"
} }

View File

@ -15,7 +15,7 @@
"Favorites": "Oblíbené", "Favorites": "Oblíbené",
"Folders": "Složky", "Folders": "Složky",
"Genres": "Žánry", "Genres": "Žánry",
"HeaderAlbumArtists": "Umělci alba", "HeaderAlbumArtists": "Album umělce",
"HeaderContinueWatching": "Pokračovat ve sledování", "HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba", "HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení interpreti", "HeaderFavoriteArtists": "Oblíbení interpreti",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Smazat záznam aktivity", "TaskCleanActivityLog": "Smazat záznam aktivity",
"Undefined": "Nedefinované", "Undefined": "Nedefinované",
"Forced": "Vynucené", "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"
} }

View File

@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Ryd Aktivitetslog", "TaskCleanActivityLog": "Ryd Aktivitetslog",
"Undefined": "Udefineret", "Undefined": "Udefineret",
"Forced": "Tvunget", "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"
} }

View File

@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Gerät: {1}", "AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung", "Application": "Anwendung",
"Artists": "Interpreten", "Artists": "Interpreten",
"AuthenticationSucceededWithUserName": "{0} wurde angemeldet", "AuthenticationSucceededWithUserName": "{0} erfolgreich authentifiziert",
"Books": "Bücher", "Books": "Bücher",
"CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen", "CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
"Channels": "Kanäle", "Channels": "Kanäle",
@ -16,7 +16,7 @@
"Folders": "Verzeichnisse", "Folders": "Verzeichnisse",
"Genres": "Genres", "Genres": "Genres",
"HeaderAlbumArtists": "Album-Interpreten", "HeaderAlbumArtists": "Album-Interpreten",
"HeaderContinueWatching": "Fortsetzen", "HeaderContinueWatching": "Weiterschauen",
"HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Interpreten", "HeaderFavoriteArtists": "Lieblings-Interpreten",
"HeaderFavoriteEpisodes": "Lieblingsepisoden", "HeaderFavoriteEpisodes": "Lieblingsepisoden",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen", "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen",
"Undefined": "Undefiniert", "Undefined": "Undefiniert",
"Forced": "Erzwungen", "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"
} }

View File

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

View File

@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Clean Activity Log", "TaskCleanActivityLog": "Clean Activity Log",
"Undefined": "Undefined", "Undefined": "Undefined",
"Forced": "Forced", "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"
} }

View File

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

View File

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

View File

@ -15,7 +15,7 @@
"Favorites": "Favoritos", "Favorites": "Favoritos",
"Folders": "Carpetas", "Folders": "Carpetas",
"Genres": "Géneros", "Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum", "HeaderAlbumArtists": "Artistas del Álbum",
"HeaderContinueWatching": "Continuar viendo", "HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteArtists": "Artistas favoritos",
@ -25,7 +25,7 @@
"HeaderLiveTV": "TV en vivo", "HeaderLiveTV": "TV en vivo",
"HeaderNextUp": "A continuación", "HeaderNextUp": "A continuación",
"HeaderRecordingGroups": "Grupos de grabación", "HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos caseros", "HomeVideos": "Videos Caseros",
"Inherit": "Heredar", "Inherit": "Heredar",
"ItemAddedWithName": "{0} fue agregado a la biblioteca", "ItemAddedWithName": "{0} fue agregado a la biblioteca",
"ItemRemovedWithName": "{0} fue removido de la biblioteca", "ItemRemovedWithName": "{0} fue removido de la biblioteca",
@ -39,7 +39,7 @@
"MixedContent": "Contenido mezclado", "MixedContent": "Contenido mezclado",
"Movies": "Películas", "Movies": "Películas",
"Music": "Música", "Music": "Música",
"MusicVideos": "Videos musicales", "MusicVideos": "Videos Musicales",
"NameInstallFailed": "Falló la instalación de {0}", "NameInstallFailed": "Falló la instalación de {0}",
"NameSeasonNumber": "Temporada {0}", "NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconocida", "NameSeasonUnknown": "Temporada desconocida",
@ -49,7 +49,7 @@
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada", "NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida", "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida", "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
"NotificationOptionInstallationFailed": "Falla de instalación", "NotificationOptionInstallationFailed": "Fallo en la instalación",
"NotificationOptionNewLibraryContent": "Nuevo contenido agregado", "NotificationOptionNewLibraryContent": "Nuevo contenido agregado",
"NotificationOptionPluginError": "Falla de complemento", "NotificationOptionPluginError": "Falla de complemento",
"NotificationOptionPluginInstalled": "Complemento instalado", "NotificationOptionPluginInstalled": "Complemento instalado",
@ -69,7 +69,7 @@
"ProviderValue": "Proveedor: {0}", "ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló", "ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciado", "ScheduledTaskStartedWithName": "{0} iniciado",
"ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
"Shows": "Programas", "Shows": "Programas",
"Songs": "Canciones", "Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.", "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
@ -94,9 +94,9 @@
"VersionNumber": "Versión {0}", "VersionNumber": "Versión {0}",
"TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.", "TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
"TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes", "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", "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", "TaskCleanTranscode": "Limpiar directorio de transcodificado",
"TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.", "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
"TaskUpdatePlugins": "Actualizar complementos", "TaskUpdatePlugins": "Actualizar complementos",
@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Limpiar registro de actividades", "TaskCleanActivityLog": "Limpiar registro de actividades",
"Undefined": "Sin definir", "Undefined": "Sin definir",
"Forced": "Forzado", "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."
} }

View File

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

View File

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

View File

@ -117,5 +117,7 @@
"Default": "Oletus", "Default": "Oletus",
"TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.", "TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.",
"TaskCleanActivityLog": "Tyhjennä toimintahistoria", "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"
} }

View File

@ -118,5 +118,7 @@
"TaskCleanActivityLog": "Nettoyer le journal d'activité", "TaskCleanActivityLog": "Nettoyer le journal d'activité",
"Undefined": "Non défini", "Undefined": "Non défini",
"Forced": "Forcé", "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"
} }

View File

@ -88,5 +88,34 @@
"NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo parada", "NotificationOptionVideoPlaybackStopped": "Reproducción de vídeo parada",
"NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada", "NotificationOptionVideoPlayback": "Reproducción de vídeo iniciada",
"NotificationOptionUserLockedOut": "Usuario bloqueado", "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}"
} }

View File

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

View File

@ -15,7 +15,7 @@
"Favorites": "Preferiti", "Favorites": "Preferiti",
"Folders": "Cartelle", "Folders": "Cartelle",
"Genres": "Generi", "Genres": "Generi",
"HeaderAlbumArtists": "Artisti degli Album", "HeaderAlbumArtists": "Artisti dell'Album",
"HeaderContinueWatching": "Continua a guardare", "HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album Preferiti", "HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti", "HeaderFavoriteArtists": "Artisti Preferiti",
@ -25,7 +25,7 @@
"HeaderLiveTV": "Diretta TV", "HeaderLiveTV": "Diretta TV",
"HeaderNextUp": "Prossimo", "HeaderNextUp": "Prossimo",
"HeaderRecordingGroups": "Gruppi di Registrazione", "HeaderRecordingGroups": "Gruppi di Registrazione",
"HomeVideos": "Video personali", "HomeVideos": "Video Personali",
"Inherit": "Eredita", "Inherit": "Eredita",
"ItemAddedWithName": "{0} è stato aggiunto alla libreria", "ItemAddedWithName": "{0} è stato aggiunto alla libreria",
"ItemRemovedWithName": "{0} è stato rimosso dalla libreria", "ItemRemovedWithName": "{0} è stato rimosso dalla libreria",
@ -39,7 +39,7 @@
"MixedContent": "Contenuto misto", "MixedContent": "Contenuto misto",
"Movies": "Film", "Movies": "Film",
"Music": "Musica", "Music": "Musica",
"MusicVideos": "Video musicali", "MusicVideos": "Video Musicali",
"NameInstallFailed": "{0} installazione fallita", "NameInstallFailed": "{0} installazione fallita",
"NameSeasonNumber": "Stagione {0}", "NameSeasonNumber": "Stagione {0}",
"NameSeasonUnknown": "Stagione sconosciuta", "NameSeasonUnknown": "Stagione sconosciuta",
@ -70,7 +70,7 @@
"ScheduledTaskFailedWithName": "{0} fallito", "ScheduledTaskFailedWithName": "{0} fallito",
"ScheduledTaskStartedWithName": "{0} avviati", "ScheduledTaskStartedWithName": "{0} avviati",
"ServerNameNeedsToBeRestarted": "{0} deve essere riavviato", "ServerNameNeedsToBeRestarted": "{0} deve essere riavviato",
"Shows": "Programmi", "Shows": "Serie TV",
"Songs": "Canzoni", "Songs": "Canzoni",
"StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.", "StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.",
"SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}", "SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}",
@ -118,5 +118,7 @@
"TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie delletà configurata.", "TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie delletà configurata.",
"Undefined": "Non Definito", "Undefined": "Non Definito",
"Forced": "Forzato", "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"
} }

View File

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

View File

@ -15,7 +15,7 @@
"Favorites": "Tañdaulylar", "Favorites": "Tañdaulylar",
"Folders": "Qaltalar", "Folders": "Qaltalar",
"Genres": "Janrlar", "Genres": "Janrlar",
"HeaderAlbumArtists": "Älbom oryndauşylary", "HeaderAlbumArtists": "Oryndauşynyñ älbomy",
"HeaderContinueWatching": "Qaraudy jalğastyru", "HeaderContinueWatching": "Qaraudy jalğastyru",
"HeaderFavoriteAlbums": "Tañdauly älbomdar", "HeaderFavoriteAlbums": "Tañdauly älbomdar",
"HeaderFavoriteArtists": "Tañdauly oryndauşylar", "HeaderFavoriteArtists": "Tañdauly oryndauşylar",
@ -118,5 +118,7 @@
"TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaña faildardy skanerleidі jäne metaderekterdı jañğyrtady.", "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaña faildardy skanerleidі jäne metaderekterdı jañğyrtady.",
"TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.", "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.",
"TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.", "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"
} }

View File

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

View File

@ -117,5 +117,7 @@
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.", "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
"TaskCleanActivityLog": "Notīrīt Darbību Žurnālu", "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
"Undefined": "Nenoteikts", "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"
} }

View File

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

View File

@ -5,7 +5,7 @@
"Artists": "Artis", "Artists": "Artis",
"AuthenticationSucceededWithUserName": "{0} berjaya disahkan", "AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
"Books": "Buku-buku", "Books": "Buku-buku",
"CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}", "CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}",
"Channels": "Saluran", "Channels": "Saluran",
"ChapterNameValue": "Bab {0}", "ChapterNameValue": "Bab {0}",
"Collections": "Koleksi", "Collections": "Koleksi",
@ -101,5 +101,13 @@
"Forced": "Paksa", "Forced": "Paksa",
"Default": "Asal", "Default": "Asal",
"TaskCleanCache": "Bersihkan Direktori Cache", "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