Merge branch 'master' into trickplay

pull/9554/head
Nick 7 months ago
commit cd662506a1

@ -168,6 +168,7 @@ jobs:
- job: CollectArtifacts - job: CollectArtifacts
timeoutInMinutes: 20 timeoutInMinutes: 20
displayName: 'Collect Artifacts' displayName: 'Collect Artifacts'
condition: succeededOrFailed()
continueOnError: true continueOnError: true
dependsOn: dependsOn:
- BuildPackage - BuildPackage

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "7.0.12",
"commands": [
"dotnet-ef"
]
}
}
}

@ -20,18 +20,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0 uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with: with:
dotnet-version: '7.0.x' dotnet-version: '7.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1 uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3

@ -24,7 +24,7 @@ jobs:
reactions: '+1' reactions: '+1'
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@ -51,7 +51,7 @@ jobs:
reactions: eyes reactions: eyes
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0

@ -14,7 +14,7 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -25,7 +25,7 @@ jobs:
- name: Generate openapi.json - name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json - name: Upload openapi.json
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with: with:
name: openapi-head name: openapi-head
retention-days: 14 retention-days: 14
@ -39,7 +39,7 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }} repository: ${{ github.event.pull_request.head.repo.full_name }}
@ -59,7 +59,7 @@ jobs:
- name: Generate openapi.json - name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json - name: Upload openapi.json
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with: with:
name: openapi-base name: openapi-base
retention-days: 14 retention-days: 14

@ -0,0 +1,82 @@
name: '🆙 Auto bump_version'
on:
release:
types:
- published
workflow_dispatch:
inputs:
TAG_BRANCH:
required: true
description: release-x.y.z
NEXT_VERSION:
required: true
description: x.y.z
jobs:
auto_bump_version:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'release' && !contains(github.event.release.tag_name, 'rc') }}
env:
TAG_BRANCH: ${{ github.event.release.target_commitish }}
steps:
- name: Wait for deploy checks to finish
uses: jitterbit/await-check-suites@292a541bb7618078395b2ce711a0d89cfb8a568a # v1
with:
ref: ${{ env.TAG_BRANCH }}
intervalSeconds: 60
timeoutSeconds: 3600
- name: Setup YQ
uses: chrisdickinson/setup-yq@latest
with:
yq-version: v4.9.8
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ env.TAG_BRANCH }}
- name: Setup EnvVars
run: |-
CURRENT_VERSION=$(yq e '.version' build.yaml)
CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*}
CURRENT_PATCH=${CURRENT_VERSION##*.}
echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV
echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV
echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV
echo "NEXT_VERSION=${CURRENT_MAJOR_MINOR}.$(($CURRENT_PATCH + 1))" >> $GITHUB_ENV
- name: Run bump_version
run: ./bump_version ${{ env.NEXT_VERSION }}
- name: Commit Changes
run: |-
git config user.name "jellyfin-bot"
git config user.email "team@jellyfin.org"
git checkout ${{ env.TAG_BRANCH }}
git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
git push origin ${{ env.TAG_BRANCH }}
manual_bump_version:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
env:
TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }}
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ env.TAG_BRANCH }}
- name: Run bump_version
run: ./bump_version ${{ env.NEXT_VERSION }}
- name: Commit Changes
run: |-
git config user.name "jellyfin-bot"
git config user.email "team@jellyfin.org"
git checkout ${{ env.TAG_BRANCH }}
git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
git push origin ${{ env.TAG_BRANCH }}

@ -2,16 +2,17 @@ name: Stale Check
on: on:
schedule: schedule:
- cron: '30 1 * * *' - cron: '30 */12 * * *'
workflow_dispatch: workflow_dispatch:
permissions: permissions:
issues: write issues: write
pull-requests: write pull-requests: write
actions: write
jobs: jobs:
issues: issues:
name: Check issues name: Check for stale issues
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
@ -26,11 +27,11 @@ jobs:
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale stale-issue-label: stale
stale-issue-message: |- stale-issue-message: |-
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments. This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label. If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
close-issue-message: |-
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 issue was closed due to inactivity.
prs-conflicts: prs-conflicts:
name: Check PRs with merge conflicts name: Check PRs with merge conflicts

@ -168,6 +168,8 @@
- [RealGreenDragon](https://github.com/RealGreenDragon) - [RealGreenDragon](https://github.com/RealGreenDragon)
- [ipitio](https://github.com/ipitio) - [ipitio](https://github.com/ipitio)
- [TheTyrius](https://github.com/TheTyrius) - [TheTyrius](https://github.com/TheTyrius)
- [tallbl0nde](https://github.com/tallbl0nde)
- [sleepycatcoding](https://github.com/sleepycatcoding)
# Emby Contributors # Emby Contributors
@ -238,3 +240,4 @@
- [Jakob Kukla](https://github.com/jakobkukla) - [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir) - [Utku Özdemir](https://github.com/utkuozdemir)
- [JPUC1143](https://github.com/Jpuc1143/) - [JPUC1143](https://github.com/Jpuc1143/)
- [0x25CBFC4F](https://github.com/0x25CBFC4F)

@ -10,26 +10,30 @@
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" /> <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
<PackageVersion Include="AutoFixture" Version="4.18.0" /> <PackageVersion Include="AutoFixture" Version="4.18.0" />
<PackageVersion Include="BDInfo" Version="0.7.6.2" /> <PackageVersion Include="BDInfo" Version="0.7.6.2" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" /> <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" />
<PackageVersion Include="BlurHashSharp" Version="1.2.0" /> <PackageVersion Include="BlurHashSharp" Version="1.3.0" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" /> <PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="Diacritics" Version="3.3.18" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" /> <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.4" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="libse" Version="3.6.13" />
<PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.8" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.8" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.12" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.8" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.8" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.8" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.8" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
@ -38,14 +42,14 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.8" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.8" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" /> <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" /> <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" />
@ -53,28 +57,25 @@
<PackageVersion Include="NEbml" Version="0.11.0" /> <PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.0" /> <PackageVersion Include="PlaylistsNET" Version="1.4.0" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" /> <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="8.0.0" /> <PackageVersion Include="prometheus-net" Version="8.0.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" /> <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" /> <PackageVersion Include="SkiaSharp" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" />
<PackageVersion Include="SkiaSharp" Version="2.88.3" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" />
@ -85,8 +86,8 @@
<PackageVersion Include="TMDbLib" Version="2.0.0" /> <PackageVersion Include="TMDbLib" Version="2.0.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.4.2" /> <PackageVersion Include="xunit" Version="2.5.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

@ -4,7 +4,7 @@
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=7.0 ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0 ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0 ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

@ -17,7 +17,7 @@ namespace Emby.Dlna.Configuration
BlastAliveMessages = true; BlastAliveMessages = true;
SendOnlyMatchedHost = true; SendOnlyMatchedHost = true;
ClientDiscoveryIntervalSeconds = 60; ClientDiscoveryIntervalSeconds = 60;
AliveMessageIntervalSeconds = 1800; AliveMessageIntervalSeconds = 180;
} }
/// <summary> /// <summary>

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -45,8 +43,8 @@ namespace Emby.Dlna.Didl
private readonly DeviceProfile _profile; private readonly DeviceProfile _profile;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly string _serverAddress; private readonly string _serverAddress;
private readonly string _accessToken; private readonly string? _accessToken;
private readonly User _user; private readonly User? _user;
private readonly IUserDataManager _userDataManager; private readonly IUserDataManager _userDataManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
@ -56,10 +54,10 @@ namespace Emby.Dlna.Didl
public DidlBuilder( public DidlBuilder(
DeviceProfile profile, DeviceProfile profile,
User user, User? user,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
string serverAddress, string serverAddress,
string accessToken, string? accessToken,
IUserDataManager userDataManager, IUserDataManager userDataManager,
ILocalizationManager localization, ILocalizationManager localization,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
@ -85,7 +83,7 @@ namespace Emby.Dlna.Didl
return url + "&dlnaheaders=true"; return url + "&dlnaheaders=true";
} }
public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo) public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo)
{ {
var settings = new XmlWriterSettings var settings = new XmlWriterSettings
{ {
@ -140,12 +138,12 @@ namespace Emby.Dlna.Didl
public void WriteItemElement( public void WriteItemElement(
XmlWriter writer, XmlWriter writer,
BaseItem item, BaseItem item,
User user, User? user,
BaseItem context, BaseItem? context,
StubType? contextStubType, StubType? contextStubType,
string deviceId, string deviceId,
Filter filter, Filter filter,
StreamInfo streamInfo = null) StreamInfo? streamInfo = null)
{ {
var clientId = GetClientId(item, null); var clientId = GetClientId(item, null);
@ -190,7 +188,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement(); writer.WriteFullEndElement();
} }
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null) private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null)
{ {
if (streamInfo is null) if (streamInfo is null)
{ {
@ -203,7 +201,7 @@ namespace Emby.Dlna.Didl
Profile = _profile, Profile = _profile,
DeviceId = deviceId, DeviceId = deviceId,
MaxBitrate = _profile.MaxStreamingBitrate MaxBitrate = _profile.MaxStreamingBitrate
}); }) ?? throw new InvalidOperationException("No optimal video stream found");
} }
var targetWidth = streamInfo.TargetWidth; var targetWidth = streamInfo.TargetWidth;
@ -315,7 +313,7 @@ namespace Emby.Dlna.Didl
var mediaSource = streamInfo.MediaSource; var mediaSource = streamInfo.MediaSource;
if (mediaSource.RunTimeTicks.HasValue) if (mediaSource?.RunTimeTicks.HasValue == true)
{ {
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
} }
@ -410,7 +408,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement(); writer.WriteFullEndElement();
} }
private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context) private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context)
{ {
if (itemStubType.HasValue) if (itemStubType.HasValue)
{ {
@ -452,7 +450,7 @@ namespace Emby.Dlna.Didl
/// <param name="episode">The episode.</param> /// <param name="episode">The episode.</param>
/// <param name="context">Current context.</param> /// <param name="context">Current context.</param>
/// <returns>Formatted name of the episode.</returns> /// <returns>Formatted name of the episode.</returns>
private string GetEpisodeDisplayName(Episode episode, BaseItem context) private string GetEpisodeDisplayName(Episode episode, BaseItem? context)
{ {
string[] components; string[] components;
@ -530,7 +528,7 @@ namespace Emby.Dlna.Didl
private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s); private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null)
{ {
writer.WriteStartElement(string.Empty, "res", NsDidl); writer.WriteStartElement(string.Empty, "res", NsDidl);
@ -544,14 +542,14 @@ namespace Emby.Dlna.Didl
MediaSources = sources.ToArray(), MediaSources = sources.ToArray(),
Profile = _profile, Profile = _profile,
DeviceId = deviceId DeviceId = deviceId
}); }) ?? throw new InvalidOperationException("No optimal audio stream found");
} }
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken)); var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
var mediaSource = streamInfo.MediaSource; var mediaSource = streamInfo.MediaSource;
if (mediaSource.RunTimeTicks.HasValue) if (mediaSource?.RunTimeTicks is not null)
{ {
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture)); writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
} }
@ -634,7 +632,7 @@ namespace Emby.Dlna.Didl
// Samsung sometimes uses 1 as root // Samsung sometimes uses 1 as root
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase); || string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null) public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string? requestedId = null)
{ {
writer.WriteStartElement(string.Empty, "container", NsDidl); writer.WriteStartElement(string.Empty, "container", NsDidl);
@ -678,14 +676,14 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement(); writer.WriteFullEndElement();
} }
private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo) private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo)
{ {
if (!item.SupportsPositionTicksResume || item is Folder) if (!item.SupportsPositionTicksResume || item is Folder)
{ {
return; return;
} }
XmlAttribute secAttribute = null; XmlAttribute? secAttribute = null;
foreach (var attribute in _profile.XmlRootAttributes) foreach (var attribute in _profile.XmlRootAttributes)
{ {
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase)) if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@ -695,8 +693,8 @@ namespace Emby.Dlna.Didl
} }
} }
// Not a samsung device // Not a samsung device or no user data
if (secAttribute is null) if (secAttribute is null || user is null)
{ {
return; return;
} }
@ -717,7 +715,7 @@ namespace Emby.Dlna.Didl
/// <summary> /// <summary>
/// Adds fields used by both items and folders. /// Adds fields used by both items and folders.
/// </summary> /// </summary>
private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
{ {
// Don't filter on dc:title because not all devices will include it in the filter // Don't filter on dc:title because not all devices will include it in the filter
// MediaMonkey for example won't display content without a title // MediaMonkey for example won't display content without a title
@ -795,7 +793,7 @@ namespace Emby.Dlna.Didl
if (item.IsDisplayedAsFolder || stubType.HasValue) if (item.IsDisplayedAsFolder || stubType.HasValue)
{ {
string classType = null; string? classType = null;
if (!_profile.RequiresPlainFolders) if (!_profile.RequiresPlainFolders)
{ {
@ -899,7 +897,7 @@ namespace Emby.Dlna.Didl
} }
} }
private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
{ {
AddCommonFields(item, itemStubType, context, writer, filter); AddCommonFields(item, itemStubType, context, writer, filter);
@ -975,7 +973,7 @@ namespace Emby.Dlna.Didl
private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer) private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer)
{ {
ImageDownloadInfo imageInfo = GetImageInfo(item); ImageDownloadInfo? imageInfo = GetImageInfo(item);
if (imageInfo is null) if (imageInfo is null)
{ {
@ -1073,7 +1071,7 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement(); writer.WriteFullEndElement();
} }
private ImageDownloadInfo GetImageInfo(BaseItem item) private ImageDownloadInfo? GetImageInfo(BaseItem item)
{ {
if (item.HasImage(ImageType.Primary)) if (item.HasImage(ImageType.Primary))
{ {
@ -1118,7 +1116,7 @@ namespace Emby.Dlna.Didl
return null; return null;
} }
private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item) private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item)
{ {
if (item is null) if (item is null)
{ {
@ -1148,7 +1146,7 @@ namespace Emby.Dlna.Didl
private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type) private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
{ {
var imageInfo = item.GetImageInfo(type, 0); var imageInfo = item.GetImageInfo(type, 0);
string tag = null; string? tag = null;
try try
{ {
@ -1250,7 +1248,7 @@ namespace Emby.Dlna.Didl
{ {
internal Guid ItemId { get; set; } internal Guid ItemId { get; set; }
internal string ImageTag { get; set; } internal string? ImageTag { get; set; }
internal ImageType Type { get; set; } internal ImageType Type { get; set; }
@ -1260,9 +1258,9 @@ namespace Emby.Dlna.Didl
internal bool IsDirectStream { get; set; } internal bool IsDirectStream { get; set; }
internal string Format { get; set; } internal required string Format { get; set; }
internal ItemImageInfo ItemImageInfo { get; set; } internal required ItemImageInfo ItemImageInfo { get; set; }
} }
} }
} }

@ -228,7 +228,7 @@ namespace Emby.Dlna
try try
{ {
return _fileSystem.GetFilePaths(path) return _fileSystem.GetFilePaths(path)
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase)) .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
.Select(i => ParseProfileFile(i, type)) .Select(i => ParseProfileFile(i, type))
.Where(i => i is not null) .Where(i => i is not null)
.ToList()!; // We just filtered out all the nulls .ToList()!; // We just filtered out all the nulls

@ -0,0 +1,69 @@
using System;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Text;
using Emby.Dlna.ConnectionManager;
using Emby.Dlna.ContentDirectory;
using Emby.Dlna.MediaReceiverRegistrar;
using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Rssdp.Infrastructure;
namespace Emby.Dlna.Extensions;
/// <summary>
/// Extension methods for adding DLNA services.
/// </summary>
public static class DlnaServiceCollectionExtensions
{
/// <summary>
/// Adds DLNA services to the provided <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="applicationHost">The <see cref="IServerApplicationHost"/>.</param>
public static void AddDlnaServices(
this IServiceCollection services,
IServerApplicationHost applicationHost)
{
services.AddHttpClient(NamedClient.Dlna, c =>
{
c.DefaultRequestHeaders.UserAgent.ParseAdd(
string.Format(
CultureInfo.InvariantCulture,
"{0}/{1} UPnP/1.0 {2}/{3}",
Environment.OSVersion.Platform,
Environment.OSVersion,
applicationHost.Name,
applicationHost.ApplicationVersionString));
c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from?
})
.ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.All,
RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
});
services.AddSingleton<IDlnaManager, DlnaManager>();
services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
services.AddSingleton<IContentDirectory, ContentDirectoryService>();
services.AddSingleton<IConnectionManager, ConnectionManagerService>();
services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>();
services.AddSingleton<ISsdpCommunicationsServer>(provider => new SsdpCommunicationsServer(
provider.GetRequiredService<ISocketFactory>(),
provider.GetRequiredService<INetworkManager>(),
provider.GetRequiredService<ILogger<SsdpCommunicationsServer>>())
{
IsShared = true
});
}
}

@ -11,7 +11,7 @@ using System.Threading.Tasks;
using Emby.Dlna.PlayTo; using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp; using Emby.Dlna.Ssdp;
using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager; using Jellyfin.Networking.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
@ -23,10 +23,8 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
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 Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Rssdp; using Rssdp;
using Rssdp.Infrastructure; using Rssdp.Infrastructure;
@ -49,14 +47,13 @@ namespace Emby.Dlna.Main
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery; private readonly IDeviceDiscovery _deviceDiscovery;
private readonly ISocketFactory _socketFactory; private readonly ISsdpCommunicationsServer _communicationsServer;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object(); private readonly object _syncLock = new();
private readonly bool _disabled; private readonly bool _disabled;
private PlayToManager _manager; private PlayToManager _manager;
private SsdpDevicePublisher _publisher; private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer;
private bool _disposed; private bool _disposed;
@ -75,10 +72,8 @@ namespace Emby.Dlna.Main
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IDeviceDiscovery deviceDiscovery, IDeviceDiscovery deviceDiscovery,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
ISocketFactory socketFactory, ISsdpCommunicationsServer communicationsServer,
INetworkManager networkManager, INetworkManager networkManager)
IUserViewManager userViewManager,
ITVSeriesManager tvSeriesManager)
{ {
_config = config; _config = config;
_appHost = appHost; _appHost = appHost;
@ -93,37 +88,10 @@ namespace Emby.Dlna.Main
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_deviceDiscovery = deviceDiscovery; _deviceDiscovery = deviceDiscovery;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_socketFactory = socketFactory; _communicationsServer = communicationsServer;
_networkManager = networkManager; _networkManager = networkManager;
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
ContentDirectory = new ContentDirectory.ContentDirectoryService(
dlnaManager,
userDataManager,
imageProcessor,
libraryManager,
config,
userManager,
loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
httpClientFactory,
localizationManager,
mediaSourceManager,
userViewManager,
mediaEncoder,
tvSeriesManager);
ConnectionManager = new ConnectionManager.ConnectionManagerService(
dlnaManager,
config,
loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
httpClientFactory);
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
httpClientFactory,
config);
Current = this;
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps; _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
@ -133,19 +101,6 @@ namespace Emby.Dlna.Main
} }
} }
public static DlnaEntryPoint Current { get; private set; }
/// <summary>
/// Gets a value indicating whether the dlna server is enabled.
/// </summary>
public static bool Enabled { get; private set; }
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public async Task RunAsync() public async Task RunAsync()
{ {
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
@ -172,9 +127,7 @@ namespace Emby.Dlna.Main
private void ReloadComponents() private void ReloadComponents()
{ {
var options = _config.GetDlnaConfiguration(); var options = _config.GetDlnaConfiguration();
Enabled = options.EnableServer; StartDeviceDiscovery();
StartSsdpHandler();
if (options.EnableServer) if (options.EnableServer)
{ {
@ -195,37 +148,11 @@ namespace Emby.Dlna.Main
} }
} }
private void StartSsdpHandler() private void StartDeviceDiscovery()
{
try
{
if (_communicationsServer is null)
{
var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
OperatingSystem.IsLinux();
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{
IsShared = true
};
StartDeviceDiscovery(_communicationsServer);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ssdp handlers");
}
}
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
{ {
try try
{ {
if (communicationsServer is not null) ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
{
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -233,26 +160,8 @@ namespace Emby.Dlna.Main
} }
} }
private void DisposeDeviceDiscovery()
{
try
{
_logger.LogInformation("Disposing DeviceDiscovery");
((DeviceDiscovery)_deviceDiscovery).Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error stopping device discovery");
}
}
public void StartDevicePublisher(Configuration.DlnaOptions options) public void StartDevicePublisher(Configuration.DlnaOptions options)
{ {
if (!options.BlastAliveMessages)
{
return;
}
if (_publisher is not null) if (_publisher is not null)
{ {
return; return;
@ -263,7 +172,8 @@ namespace Emby.Dlna.Main
_publisher = new SsdpDevicePublisher( _publisher = new SsdpDevicePublisher(
_communicationsServer, _communicationsServer,
Environment.OSVersion.Platform.ToString(), Environment.OSVersion.Platform.ToString(),
Environment.OSVersion.VersionString, // Can not use VersionString here since that includes OS and version
Environment.OSVersion.Version.ToString(),
_config.GetDlnaConfiguration().SendOnlyMatchedHost) _config.GetDlnaConfiguration().SendOnlyMatchedHost)
{ {
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg), LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
@ -272,7 +182,10 @@ namespace Emby.Dlna.Main
RegisterServerEndpoints(); RegisterServerEndpoints();
_publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds)); if (options.BlastAliveMessages)
{
_publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -285,42 +198,33 @@ namespace Emby.Dlna.Main
var udn = CreateUuid(_appHost.SystemId); var udn = CreateUuid(_appHost.SystemId);
var descriptorUri = "/dlna/" + udn + "/description.xml"; var descriptorUri = "/dlna/" + udn + "/description.xml";
var bindAddresses = NetworkManager.CreateCollection( // Only get bind addresses in LAN
_networkManager.GetInternalBindAddresses() // IPv6 is currently unsupported
.Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0))); var validInterfaces = _networkManager.GetInternalBindAddresses()
.Where(x => x.Address is not null)
.Where(x => x.AddressFamily != AddressFamily.InterNetworkV6)
.ToList();
if (bindAddresses.Count == 0) if (validInterfaces.Count == 0)
{ {
// No interfaces returned, so use loopback. // No interfaces returned, fall back to loopback
bindAddresses = _networkManager.GetLoopbacks(); validInterfaces = _networkManager.GetLoopbacks().ToList();
} }
foreach (IPNetAddress address in bindAddresses) foreach (var intf in validInterfaces)
{ {
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
// Not supporting IPv6 right now
continue;
}
// Limit to LAN addresses only
if (!_networkManager.IsInLocalNetwork(address))
{
continue;
}
var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address); _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri); var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri);
var device = new SsdpRootDevice var device = new SsdpRootDevice
{ {
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info. CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document. Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
Address = address.Address, Address = intf.Address,
PrefixLength = address.PrefixLength, PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix),
FriendlyName = "Jellyfin", FriendlyName = "Jellyfin",
Manufacturer = "Jellyfin", Manufacturer = "Jellyfin",
ModelName = "Jellyfin Server", ModelName = "Jellyfin Server",
@ -328,7 +232,7 @@ namespace Emby.Dlna.Main
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
}; };
SetProperies(device, fullService); SetProperties(device, fullService);
_publisher.AddDevice(device); _publisher.AddDevice(device);
var embeddedDevices = new[] var embeddedDevices = new[]
@ -349,13 +253,13 @@ namespace Emby.Dlna.Main
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
}; };
SetProperies(embeddedDevice, subDevice); SetProperties(embeddedDevice, subDevice);
device.AddDevice(embeddedDevice); device.AddDevice(embeddedDevice);
} }
} }
} }
private string CreateUuid(string text) private static string CreateUuid(string text)
{ {
if (!Guid.TryParse(text, out var guid)) if (!Guid.TryParse(text, out var guid))
{ {
@ -365,15 +269,14 @@ namespace Emby.Dlna.Main
return guid.ToString("D", CultureInfo.InvariantCulture); return guid.ToString("D", CultureInfo.InvariantCulture);
} }
private void SetProperies(SsdpDevice device, string fullDeviceType) private static void SetProperties(SsdpDevice device, string fullDeviceType)
{ {
var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase); var serviceParts = fullDeviceType
.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
var serviceParts = service.Split(':'); .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
.Split(':');
var deviceTypeNamespace = serviceParts[0].Replace('.', '-'); device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
device.DeviceTypeNamespace = deviceTypeNamespace;
device.DeviceClass = serviceParts[1]; device.DeviceClass = serviceParts[1];
device.DeviceType = serviceParts[2]; device.DeviceType = serviceParts[2];
} }
@ -454,20 +357,6 @@ namespace Emby.Dlna.Main
DisposeDevicePublisher(); DisposeDevicePublisher();
DisposePlayToManager(); DisposePlayToManager();
DisposeDeviceDiscovery();
if (_communicationsServer is not null)
{
_logger.LogInformation("Disposing SsdpCommunicationsServer");
_communicationsServer.Dispose();
_communicationsServer = null;
}
ContentDirectory = null;
ConnectionManager = null;
MediaReceiverRegistrar = null;
Current = null;
_disposed = true; _disposed = true;
} }
} }

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -25,7 +23,7 @@ namespace Emby.Dlna.PlayTo
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly object _timerLock = new object(); private readonly object _timerLock = new object();
private Timer _timer; private Timer? _timer;
private int _muteVol; private int _muteVol;
private int _volume; private int _volume;
private DateTime _lastVolumeRefresh; private DateTime _lastVolumeRefresh;
@ -40,13 +38,13 @@ namespace Emby.Dlna.PlayTo
_logger = logger; _logger = logger;
} }
public event EventHandler<PlaybackStartEventArgs> PlaybackStart; public event EventHandler<PlaybackStartEventArgs>? PlaybackStart;
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress; public event EventHandler<PlaybackProgressEventArgs>? PlaybackProgress;
public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped; public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped;
public event EventHandler<MediaChangedEventArgs> MediaChanged; public event EventHandler<MediaChangedEventArgs>? MediaChanged;
public DeviceInfo Properties { get; set; } public DeviceInfo Properties { get; set; }
@ -75,13 +73,13 @@ namespace Emby.Dlna.PlayTo
public bool IsStopped => TransportState == TransportState.STOPPED; public bool IsStopped => TransportState == TransportState.STOPPED;
public Action OnDeviceUnavailable { get; set; } public Action? OnDeviceUnavailable { get; set; }
private TransportCommands AvCommands { get; set; } private TransportCommands? AvCommands { get; set; }
private TransportCommands RendererCommands { get; set; } private TransportCommands? RendererCommands { get; set; }
public UBaseObject CurrentMediaInfo { get; private set; } public UBaseObject? CurrentMediaInfo { get; private set; }
public void Start() public void Start()
{ {
@ -131,7 +129,7 @@ namespace Emby.Dlna.PlayTo
_volumeRefreshActive = true; _volumeRefreshActive = true;
var time = immediate ? 100 : 10000; var time = immediate ? 100 : 10000;
_timer.Change(time, Timeout.Infinite); _timer?.Change(time, Timeout.Infinite);
} }
} }
@ -149,7 +147,7 @@ namespace Emby.Dlna.PlayTo
_volumeRefreshActive = false; _volumeRefreshActive = false;
_timer.Change(Timeout.Infinite, Timeout.Infinite); _timer?.Change(Timeout.Infinite, Timeout.Infinite);
} }
} }
@ -199,7 +197,7 @@ namespace Emby.Dlna.PlayTo
} }
} }
private DeviceService GetServiceRenderingControl() private DeviceService? GetServiceRenderingControl()
{ {
var services = Properties.Services; var services = Properties.Services;
@ -207,7 +205,7 @@ namespace Emby.Dlna.PlayTo
services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase)); services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase));
} }
private DeviceService GetAvTransportService() private DeviceService? GetAvTransportService()
{ {
var services = Properties.Services; var services = Properties.Services;
@ -240,7 +238,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
rendererCommands.BuildPost(command, service.ServiceType, value), rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
cancellationToken: cancellationToken) cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -265,12 +263,7 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
var service = GetServiceRenderingControl(); var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service");
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
// Set it early and assume it will succeed // Set it early and assume it will succeed
// Remote control will perform better // Remote control will perform better
@ -281,7 +274,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
rendererCommands.BuildPost(command, service.ServiceType, value), rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
cancellationToken: cancellationToken) cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@ -296,26 +289,20 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
var service = GetAvTransportService(); var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
await new DlnaHttpClient(_logger, _httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync( .SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above
cancellationToken: cancellationToken) cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
RestartTimer(true); RestartTimer(true);
} }
public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken) public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken)
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
@ -335,14 +322,8 @@ namespace Emby.Dlna.PlayTo
{ "CurrentURIMetaData", CreateDidlMeta(metaData) } { "CurrentURIMetaData", CreateDidlMeta(metaData) }
}; };
var service = GetAvTransportService(); var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
await new DlnaHttpClient(_logger, _httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync( .SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
@ -372,7 +353,7 @@ namespace Emby.Dlna.PlayTo
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play. * SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
* Without that information, the next track command on the device does not work. * Without that information, the next track command on the device does not work.
*/ */
public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default) public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default)
{ {
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false); var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
@ -380,7 +361,7 @@ namespace Emby.Dlna.PlayTo
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header); _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase)); var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
if (command is null) if (command is null)
{ {
return; return;
@ -392,14 +373,8 @@ namespace Emby.Dlna.PlayTo
{ "NextURIMetaData", CreateDidlMeta(metaData) } { "NextURIMetaData", CreateDidlMeta(metaData) }
}; };
var service = GetAvTransportService(); var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
await new DlnaHttpClient(_logger, _httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken) .SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -423,12 +398,7 @@ namespace Emby.Dlna.PlayTo
return Task.CompletedTask; return Task.CompletedTask;
} }
var service = GetAvTransportService(); var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
if (service is null)
{
throw new InvalidOperationException("Unable to find service");
}
return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync( return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
@ -460,14 +430,13 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
var service = GetAvTransportService(); var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync( .SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
avCommands.BuildPost(command, service.ServiceType, 1), avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
cancellationToken: cancellationToken) cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -484,14 +453,13 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
var service = GetAvTransportService(); var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
await new DlnaHttpClient(_logger, _httpClientFactory) await new DlnaHttpClient(_logger, _httpClientFactory)
.SendCommandAsync( .SendCommandAsync(
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
avCommands.BuildPost(command, service.ServiceType, 1), avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
cancellationToken: cancellationToken) cancellationToken: cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -500,7 +468,7 @@ namespace Emby.Dlna.PlayTo
RestartTimer(true); RestartTimer(true);
} }
private async void TimerCallback(object sender) private async void TimerCallback(object? sender)
{ {
if (_disposed) if (_disposed)
{ {
@ -623,7 +591,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
rendererCommands.BuildPost(command, service.ServiceType), rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
cancellationToken: cancellationToken).ConfigureAwait(false); cancellationToken: cancellationToken).ConfigureAwait(false);
if (result is null || result.Document is null) if (result is null || result.Document is null)
@ -673,7 +641,7 @@ namespace Emby.Dlna.PlayTo
Properties.BaseUrl, Properties.BaseUrl,
service, service,
command.Name, command.Name,
rendererCommands.BuildPost(command, service.ServiceType), rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
cancellationToken: cancellationToken).ConfigureAwait(false); cancellationToken: cancellationToken).ConfigureAwait(false);
if (result is null || result.Document is null) if (result is null || result.Document is null)
@ -728,7 +696,7 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken) private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{ {
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
if (command is null) if (command is null)
@ -798,7 +766,7 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken) private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{ {
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo"); var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
if (command is null) if (command is null)
@ -871,7 +839,7 @@ namespace Emby.Dlna.PlayTo
return (true, null); return (true, null);
} }
XElement uPnpResponse = null; XElement? uPnpResponse = null;
try try
{ {
@ -895,7 +863,7 @@ namespace Emby.Dlna.PlayTo
return (true, uTrack); return (true, uTrack);
} }
private XElement ParseResponse(string xml) private XElement? ParseResponse(string xml)
{ {
// Handle different variations sent back by devices. // Handle different variations sent back by devices.
try try
@ -929,7 +897,7 @@ namespace Emby.Dlna.PlayTo
return null; return null;
} }
private static UBaseObject CreateUBaseObject(XElement container, string trackUri) private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri)
{ {
ArgumentNullException.ThrowIfNull(container); ArgumentNullException.ThrowIfNull(container);
@ -959,20 +927,17 @@ namespace Emby.Dlna.PlayTo
var resElement = container.Element(UPnpNamespaces.Res); var resElement = container.Element(UPnpNamespaces.Res);
if (resElement is not null) var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo);
{
var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
if (info is not null && !string.IsNullOrWhiteSpace(info.Value)) if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
{ {
return info.Value.Split(':'); return info.Value.Split(':');
}
} }
return new string[4]; return new string[4];
} }
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken) private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken)
{ {
if (AvCommands is not null) if (AvCommands is not null)
{ {
@ -1004,7 +969,7 @@ namespace Emby.Dlna.PlayTo
return AvCommands; return AvCommands;
} }
private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken) private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken)
{ {
if (RendererCommands is not null) if (RendererCommands is not null)
{ {
@ -1054,7 +1019,7 @@ namespace Emby.Dlna.PlayTo
return baseUrl + url; return baseUrl + url;
} }
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken) public static async Task<Device?> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
{ {
var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory); var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
@ -1171,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
return new Device(deviceProperties, httpClientFactory, logger); return new Device(deviceProperties, httpClientFactory, logger);
} }
#nullable enable
private static DeviceIcon CreateIcon(XElement element) private static DeviceIcon CreateIcon(XElement element)
{ {
ArgumentNullException.ThrowIfNull(element); ArgumentNullException.ThrowIfNull(element);
@ -1287,7 +1251,7 @@ namespace Emby.Dlna.PlayTo
} }
_timer = null; _timer = null;
Properties = null; Properties = null!;
_disposed = true; _disposed = true;
} }

@ -31,6 +31,9 @@ namespace Emby.Dlna.PlayTo
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
} }
[GeneratedRegex("(&(?![a-z]*;))")]
private static partial Regex EscapeAmpersandRegex();
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl) private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
{ {
// If it's already a complete url, don't stick anything onto the front of it // If it's already a complete url, don't stick anything onto the front of it
@ -52,40 +55,42 @@ namespace Emby.Dlna.PlayTo
var client = _httpClientFactory.CreateClient(NamedClient.Dlna); var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using MemoryStream ms = new MemoryStream(); Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false); await using (stream.ConfigureAwait(false))
try
{ {
return await XDocument.LoadAsync(
ms,
LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch (XmlException)
{
// try correcting the Xml response with common errors
ms.Position = 0;
using StreamReader sr = new StreamReader(ms);
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
// find and replace unescaped ampersands (&)
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
try try
{ {
// retry reading Xml
using var xmlReader = new StringReader(xmlString);
return await XDocument.LoadAsync( return await XDocument.LoadAsync(
xmlReader, stream,
LoadOptions.None, LoadOptions.None,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }
catch (XmlException ex) catch (XmlException)
{ {
_logger.LogError(ex, "Failed to parse response"); // try correcting the Xml response with common errors
_logger.LogDebug("Malformed response: {Content}\n", xmlString); stream.Position = 0;
using StreamReader sr = new StreamReader(stream);
return null; var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
// find and replace unescaped ampersands (&)
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
try
{
// retry reading Xml
using var xmlReader = new StringReader(xmlString);
return await XDocument.LoadAsync(
xmlReader,
LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch (XmlException ex)
{
_logger.LogError(ex, "Failed to parse response");
_logger.LogDebug("Malformed response: {Content}\n", xmlString);
return null;
}
} }
} }
} }
@ -128,12 +133,5 @@ namespace Emby.Dlna.PlayTo
// Have to await here instead of returning the Task directly, otherwise request would be disposed too soon // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false); return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
} }
/// <summary>
/// Compile-time generated regular expression for escaping ampersands.
/// </summary>
/// <returns>Compiled regular expression.</returns>
[GeneratedRegex("(&(?![a-z]*;))")]
private static partial Regex EscapeAmpersandRegex();
} }
} }

@ -42,7 +42,7 @@ namespace Emby.Dlna.PlayTo
private readonly IDeviceDiscovery _deviceDiscovery; private readonly IDeviceDiscovery _deviceDiscovery;
private readonly string _serverAddress; private readonly string _serverAddress;
private readonly string _accessToken; private readonly string? _accessToken;
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
private Device _device; private Device _device;
@ -59,7 +59,7 @@ namespace Emby.Dlna.PlayTo
IUserManager userManager, IUserManager userManager,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
string serverAddress, string serverAddress,
string accessToken, string? accessToken,
IDeviceDiscovery deviceDiscovery, IDeviceDiscovery deviceDiscovery,
IUserDataManager userDataManager, IUserDataManager userDataManager,
ILocalizationManager localization, ILocalizationManager localization,

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -41,9 +39,9 @@ namespace Emby.Dlna.PlayTo
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
private bool _disposed; private bool _disposed;
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{ {
@ -67,7 +65,7 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
} }
private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
{ {
if (_disposed) if (_disposed)
{ {
@ -76,12 +74,12 @@ namespace Emby.Dlna.PlayTo
var info = e.Argument; var info = e.Argument;
if (!info.Headers.TryGetValue("USN", out string usn)) if (!info.Headers.TryGetValue("USN", out string? usn))
{ {
usn = string.Empty; usn = string.Empty;
} }
if (!info.Headers.TryGetValue("NT", out string nt)) if (!info.Headers.TryGetValue("NT", out string? nt))
{ {
nt = string.Empty; nt = string.Empty;
} }
@ -161,7 +159,7 @@ namespace Emby.Dlna.PlayTo
var uri = info.Location; var uri = info.Location;
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri); _logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
if (info.Headers.TryGetValue("USN", out string uuid)) if (info.Headers.TryGetValue("USN", out string? uuid))
{ {
uuid = GetUuid(uuid); uuid = GetUuid(uuid);
} }
@ -189,7 +187,7 @@ namespace Emby.Dlna.PlayTo
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress); string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress);
controller = new PlayToController( controller = new PlayToController(
sessionInfo, sessionInfo,

@ -73,7 +73,11 @@ namespace Emby.Dlna.Ssdp
{ {
if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null) if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null)
{ {
_deviceLocator = new SsdpDeviceLocator(_commsServer); _deviceLocator = new SsdpDeviceLocator(
_commsServer,
Environment.OSVersion.Platform.ToString(),
// Can not use VersionString here since that includes OS and version
Environment.OSVersion.Version.ToString());
// (Optional) Set the filter so we only see notifications for devices we care about // (Optional) Set the filter so we only see notifications for devices we care about
// (can be any search target value i.e device type, uuid value etc - any value that appears in the // (can be any search target value i.e device type, uuid value etc - any value that appears in the
@ -106,7 +110,7 @@ namespace Emby.Dlna.Ssdp
{ {
Location = e.DiscoveredDevice.DescriptionLocation, Location = e.DiscoveredDevice.DescriptionLocation,
Headers = headers, Headers = headers,
RemoteIpAddress = e.RemoteIpAddress RemoteIPAddress = e.RemoteIPAddress
}); });
DeviceDiscoveredInternal?.Invoke(this, args); DeviceDiscoveredInternal?.Invoke(this, args);

@ -10,7 +10,7 @@ namespace Emby.Naming.Audio
/// <summary> /// <summary>
/// Helper class to determine if Album is multipart. /// Helper class to determine if Album is multipart.
/// </summary> /// </summary>
public class AlbumParser public partial class AlbumParser
{ {
private readonly NamingOptions _options; private readonly NamingOptions _options;
@ -23,6 +23,9 @@ namespace Emby.Naming.Audio
_options = options; _options = options;
} }
[GeneratedRegex(@"[-\.\(\)\s]+")]
private static partial Regex CleanRegex();
/// <summary> /// <summary>
/// Function that determines if album is multipart. /// Function that determines if album is multipart.
/// </summary> /// </summary>
@ -42,13 +45,9 @@ namespace Emby.Naming.Audio
// Normalize // Normalize
// Remove whitespace // Remove whitespace
filename = filename.Replace('-', ' '); filename = CleanRegex().Replace(filename, " ");
filename = filename.Replace('.', ' ');
filename = filename.Replace('(', ' ');
filename = filename.Replace(')', ' ');
filename = Regex.Replace(filename, @"\s+", " ");
ReadOnlySpan<char> trimmedFilename = filename.TrimStart(); ReadOnlySpan<char> trimmedFilename = filename.AsSpan().TrimStart();
foreach (var prefix in _options.AlbumStackingPrefixes) foreach (var prefix in _options.AlbumStackingPrefixes)
{ {

@ -318,22 +318,24 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
// <!-- foo.E01., foo.e01. --> // <!-- foo.E01., foo.e01. -->
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"), new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true) new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
{ {
DateTimeFormats = new[] DateTimeFormats = new[]
{ {
"yyyy.MM.dd", "yyyy.MM.dd",
"yyyy-MM-dd", "yyyy-MM-dd",
"yyyy_MM_dd" "yyyy_MM_dd",
"yyyy MM dd"
} }
}, },
new EpisodeExpression(@"(?<day>[0-9]{2})[.-](?<month>[0-9]{2})[.-](?<year>[0-9]{4})", true) new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
{ {
DateTimeFormats = new[] DateTimeFormats = new[]
{ {
"dd.MM.yyyy", "dd.MM.yyyy",
"dd-MM-yyyy", "dd-MM-yyyy",
"dd_MM_yyyy" "dd_MM_yyyy",
"dd MM yyyy"
} }
}, },
@ -374,7 +376,7 @@ namespace Emby.Naming.Common
IsNamed = true, IsNamed = true,
SupportsAbsoluteEpisodeNumbers = false SupportsAbsoluteEpisodeNumbers = false
}, },
new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$") new EpisodeExpression(@"[\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\/]*)$")
{ {
SupportsAbsoluteEpisodeNumbers = true SupportsAbsoluteEpisodeNumbers = true
}, },
@ -415,7 +417,7 @@ namespace Emby.Naming.Common
}, },
// "1-12 episode title" // "1-12 episode title"
new EpisodeExpression(@"([0-9]+)-([0-9]+)"), new EpisodeExpression("([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi" // "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
@ -710,7 +712,7 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename // Chapter is often beginning of filename
"^(?<chapter>[0-9]+)", "^(?<chapter>[0-9]+)",
// Part if often ending of filename // Part if often ending of filename
@"(?<!ch(?:apter) )(?<part>[0-9]+)$", "(?<!ch(?:apter) )(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part) // Sometimes named as 0001_005 (chapter_part)
"(?<chapter>[0-9]+)_(?<part>[0-9]+)", "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number. // Some audiobooks are ripped from cd's, and will be named by disk number.

@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
return null; return null;
} }
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path.AsSpan());
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))) && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
{ {

@ -7,14 +7,15 @@ namespace Emby.Naming.TV
/// <summary> /// <summary>
/// Used to resolve information about series from path. /// Used to resolve information about series from path.
/// </summary> /// </summary>
public static class SeriesResolver public static partial class SeriesResolver
{ {
/// <summary> /// <summary>
/// Regex that matches strings of at least 2 characters separated by a dot or underscore. /// Regex that matches strings of at least 2 characters separated by a dot or underscore.
/// Used for removing separators between words, i.e turns "The_show" into "The show" while /// Used for removing separators between words, i.e turns "The_show" into "The show" while
/// preserving namings like "S.H.O.W". /// preserving namings like "S.H.O.W".
/// </summary> /// </summary>
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled); [GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
private static partial Regex SeriesNameRegex();
/// <summary> /// <summary>
/// Resolve information about series from path. /// Resolve information about series from path.
@ -37,7 +38,7 @@ namespace Emby.Naming.TV
if (!string.IsNullOrEmpty(seriesName)) if (!string.IsNullOrEmpty(seriesName))
{ {
seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim(); seriesName = SeriesNameRegex().Replace(seriesName, "${a} ${b}").Trim();
} }
return new SeriesInfo(path) return new SeriesInfo(path)

@ -26,19 +26,18 @@ namespace Emby.Naming.Video
return false; return false;
} }
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path.AsSpan());
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{ {
return false; return false;
} }
path = Path.GetFileNameWithoutExtension(path); var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
var token = Path.GetExtension(path).TrimStart('.');
foreach (var rule in options.StubTypes) foreach (var rule in options.StubTypes)
{ {
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase)) if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{ {
stubType = rule.StubType; stubType = rule.StubType;
return true; return true;

@ -12,9 +12,13 @@ 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 static class VideoListResolver public static partial class VideoListResolver
{ {
private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled); [GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
private static partial Regex ResolutionRegex();
[GeneratedRegex(@"^\[([^]]*)\]")]
private static partial Regex CheckMultiVersionRegex();
/// <summary> /// <summary>
/// Resolves alternative versions and extras from list of video files. /// Resolves alternative versions and extras from list of video files.
@ -131,7 +135,7 @@ namespace Emby.Naming.Video
if (videos.Count > 1) if (videos.Count > 1)
{ {
var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList(); var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
videos.Clear(); videos.Clear();
foreach (var group in groups) foreach (var group in groups)
{ {
@ -201,7 +205,7 @@ namespace Emby.Naming.Video
// The CleanStringParser should have removed common keywords etc. // The CleanStringParser should have removed common keywords etc.
return testFilename.IsEmpty return testFilename.IsEmpty
|| testFilename[0] == '-' || testFilename[0] == '-'
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); || CheckMultiVersionRegex().IsMatch(testFilename);
} }
} }
} }

@ -61,7 +61,7 @@ namespace Emby.Photos
item.SetImagePath(ImageType.Primary, item.Path); item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase)) if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
{ {
try try
{ {

@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase
/// </summary> /// </summary>
public abstract class BaseApplicationPaths : IApplicationPaths public abstract class BaseApplicationPaths : IApplicationPaths
{ {
private string _dataPath;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class. /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
/// </summary> /// </summary>
@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath; CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath; WebPath = webDirectoryPath;
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
} }
/// <summary> /// <summary>
@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory. /// Gets the folder path to the data directory.
/// </summary> /// </summary>
/// <value>The data directory.</value> /// <value>The data directory.</value>
public string DataPath => _dataPath; public string DataPath { get; }
/// <inheritdoc /> /// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%"; public string VirtualDataPath => "%AppDataPath%";

@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events; using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
/// </summary> /// </summary>
public abstract class BaseConfigurationManager : IConfigurationManager public abstract class BaseConfigurationManager : IConfigurationManager
{ {
private readonly IFileSystem _fileSystem; private readonly ConcurrentDictionary<string, object> _configurations = new();
private readonly object _configurationSyncLock = new();
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
/// <summary>
/// The _configuration sync lock.
/// </summary>
private readonly object _configurationSyncLock = new object();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
/// <param name="applicationPaths">The application paths.</param> /// <param name="applicationPaths">The application paths.</param>
/// <param name="loggerFactory">The logger factory.</param> /// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param> /// <param name="xmlSerializer">The XML serializer.</param>
/// <param name="fileSystem">The file system.</param> protected BaseConfigurationManager(
protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) IApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IXmlSerializer xmlSerializer)
{ {
CommonApplicationPaths = applicationPaths; CommonApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer; XmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
Logger = loggerFactory.CreateLogger<BaseConfigurationManager>(); Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
UpdateCachePath(); UpdateCachePath();
@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
{ {
var file = Path.Combine(path, Guid.NewGuid().ToString()); var file = Path.Combine(path, Guid.NewGuid().ToString());
File.WriteAllText(file, string.Empty); File.WriteAllText(file, string.Empty);
_fileSystem.DeleteFile(file); File.Delete(file);
} }
private string GetConfigurationFile(string key) private string GetConfigurationFile(string key)

@ -12,11 +12,8 @@ using System.Linq;
using System.Net; using System.Net;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main; using Emby.Dlna.Main;
using Emby.Dlna.Ssdp;
using Emby.Naming.Common; using Emby.Naming.Common;
using Emby.Photos; using Emby.Photos;
using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Channels;
@ -59,7 +56,6 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -83,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
@ -112,7 +107,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Class CompositionRoot. /// Class CompositionRoot.
/// </summary> /// </summary>
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable public abstract class ApplicationHost : IServerApplicationHost, IDisposable
{ {
/// <summary> /// <summary>
/// The disposable parts. /// The disposable parts.
@ -120,14 +115,12 @@ namespace Emby.Server.Implementations
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new(); private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
private readonly DeviceId _deviceId; private readonly DeviceId _deviceId;
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig; private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions; private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager; private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances; private List<Type> _creatingInstances;
private ISessionManager _sessionManager;
/// <summary> /// <summary>
/// Gets or sets all concrete types. /// Gets or sets all concrete types.
@ -135,7 +128,7 @@ namespace Emby.Server.Implementations
/// <value>All concrete types.</value> /// <value>All concrete types.</value>
private Type[] _allConcreteTypes; private Type[] _allConcreteTypes;
private bool _disposed = false; private bool _disposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class. /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@ -154,10 +147,8 @@ namespace Emby.Server.Implementations
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
_startupOptions = options; _startupOptions = options;
_startupConfig = startupConfig; _startupConfig = startupConfig;
_fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
Logger = LoggerFactory.CreateLogger<ApplicationHost>(); Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory); _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
@ -165,13 +156,15 @@ namespace Emby.Server.Implementations
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer(); _xmlSerializer = new MyXmlSerializer();
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer);
_pluginManager = new PluginManager( _pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(), LoggerFactory.CreateLogger<PluginManager>(),
this, this,
ConfigurationManager.Configuration, ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath, ApplicationPaths.PluginsPath,
ApplicationVersion); ApplicationVersion);
_disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
} }
/// <summary> /// <summary>
@ -186,23 +179,16 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; } public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
&& !_startupOptions.IsService
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
/// <summary> /// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance. /// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary> /// </summary>
public INetworkManager NetManager { get; private set; } public INetworkManager NetManager { get; private set; }
/// <summary> /// <inheritdoc />
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary>
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
public bool HasPendingRestart { get; private set; } public bool HasPendingRestart { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsShuttingDown { get; private set; } public bool ShouldRestart { get; set; }
/// <summary> /// <summary>
/// Gets the logger. /// Gets the logger.
@ -406,11 +392,9 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Runs the startup tasks. /// Runs the startup tasks.
/// </summary> /// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns> /// <returns><see cref="Task" />.</returns>
public async Task RunStartupTasksAsync(CancellationToken cancellationToken) public async Task RunStartupTasksAsync()
{ {
cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks"); Logger.LogInformation("Running startup tasks");
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
@ -424,8 +408,6 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports<IServerEntryPoint>(); var entryPoints = GetExports<IServerEntryPoint>();
cancellationToken.ThrowIfCancellationRequested();
var stopWatch = new Stopwatch(); var stopWatch = new Stopwatch();
stopWatch.Start(); stopWatch.Start();
@ -435,8 +417,6 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Core startup complete"); Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true; CoreStartupHasCompleted = true;
cancellationToken.ThrowIfCancellationRequested();
stopWatch.Restart(); stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@ -466,7 +446,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>()); NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
// Initialize runtime stat collection // Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics) if (ConfigurationManager.Configuration.EnableMetrics)
@ -475,8 +455,8 @@ namespace Emby.Server.Implementations
} }
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration(); var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
HttpPort = networkConfiguration.HttpServerPortNumber; HttpPort = networkConfiguration.InternalHttpPort;
HttpsPort = networkConfiguration.HttpsPortNumber; HttpsPort = networkConfiguration.InternalHttpsPort;
// Safeguard against invalid configuration // Safeguard against invalid configuration
if (HttpPort == HttpsPort) if (HttpPort == HttpsPort)
@ -509,7 +489,11 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(_pluginManager); serviceCollection.AddSingleton(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton(_fileSystemManager); serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
serviceCollection.AddScoped<ISystemManager, SystemManager>();
serviceCollection.AddSingleton<TmdbClientManager>(); serviceCollection.AddSingleton<TmdbClientManager>();
serviceCollection.AddSingleton(NetManager); serviceCollection.AddSingleton(NetManager);
@ -575,8 +559,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISessionManager, SessionManager>(); serviceCollection.AddSingleton<ISessionManager, SessionManager>();
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
@ -588,8 +570,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
@ -633,8 +613,6 @@ namespace Emby.Server.Implementations
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false); await localizationManager.LoadAll().ConfigureAwait(false);
_sessionManager = Resolve<ISessionManager>();
SetStaticProperties(); SetStaticProperties();
FindParts(); FindParts();
@ -685,7 +663,7 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>(); BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>(); BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.FileSystem = _fileSystemManager; BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.UserDataManager = Resolve<IUserDataManager>(); BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>(); BaseItem.ChannelManager = Resolve<IChannelManager>();
Video.LiveTvManager = Resolve<ILiveTvManager>(); Video.LiveTvManager = Resolve<ILiveTvManager>();
@ -785,8 +763,8 @@ namespace Emby.Server.Implementations
if (HttpPort != 0 && HttpsPort != 0) if (HttpPort != 0 && HttpsPort != 0)
{ {
// Need to restart if ports have changed // Need to restart if ports have changed
if (networkConfiguration.HttpServerPortNumber != HttpPort if (networkConfiguration.InternalHttpPort != HttpPort
|| networkConfiguration.HttpsPortNumber != HttpsPort) || networkConfiguration.InternalHttpsPort != HttpsPort)
{ {
if (ConfigurationManager.Configuration.IsPortAuthorized) if (ConfigurationManager.Configuration.IsPortAuthorized)
{ {
@ -855,38 +833,6 @@ namespace Emby.Server.Implementations
} }
} }
/// <summary>
/// Restarts this instance.
/// </summary>
public void Restart()
{
if (IsShuttingDown)
{
return;
}
IsShuttingDown = true;
_pluginManager.UnloadAssemblies();
Task.Run(async () =>
{
try
{
await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error sending server restart notification");
}
Logger.LogInformation("Calling RestartInternal");
RestartInternal();
});
}
protected abstract void RestartInternal();
/// <summary> /// <summary>
/// Gets the composable part assemblies. /// Gets the composable part assemblies.
/// </summary> /// </summary>
@ -942,49 +888,6 @@ namespace Emby.Server.Implementations
protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal(); protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
/// <summary>
/// Gets the system status.
/// </summary>
/// <param name="request">Where this request originated.</param>
/// <returns>SystemInfo.</returns>
public SystemInfo GetSystemInfo(HttpRequest request)
{
return new SystemInfo
{
HasPendingRestart = HasPendingRestart,
IsShuttingDown = IsShuttingDown,
Version = ApplicationVersionString,
WebSocketPortNumber = HttpPort,
CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
Id = SystemId,
ProgramDataPath = ApplicationPaths.ProgramDataPath,
WebPath = ApplicationPaths.WebPath,
LogPath = ApplicationPaths.LogDirectoryPath,
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
PackageName = _startupOptions.PackageName
};
}
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{
return new PublicSystemInfo
{
Version = ApplicationVersionString,
ProductName = ApplicationProductName,
Id = SystemId,
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(IPAddress remoteAddr) public string GetSmartApiUrl(IPAddress remoteAddr)
{ {
@ -995,18 +898,20 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(remoteAddr, out var port); string smart = NetManager.GetBindAddress(remoteAddr, out var port);
return GetLocalApiUrl(smart.Trim('/'), null, port); return GetLocalApiUrl(smart.Trim('/'), null, port);
} }
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(HttpRequest request) public string GetSmartApiUrl(HttpRequest request)
{ {
// Return the host in the HTTP request as the API url // Return the host in the HTTP request as the API URL if not configured otherwise
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
{ {
int? requestPort = request.Host.Port; int? requestPort = request.Host.Port;
if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase))) if (requestPort is null
|| (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
|| (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
{ {
requestPort = -1; requestPort = -1;
} }
@ -1027,15 +932,15 @@ namespace Emby.Server.Implementations
return PublishedServerUrl.Trim('/'); return PublishedServerUrl.Trim('/');
} }
string smart = NetManager.GetBindInterface(hostname, out var port); string smart = NetManager.GetBindAddress(hostname, out var port);
return GetLocalApiUrl(smart.Trim('/'), null, port); return GetLocalApiUrl(smart.Trim('/'), null, port);
} }
/// <inheritdoc/> /// <inheritdoc/>
public string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true) public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
{ {
// With an empty source, the port will be null // With an empty source, the port will be null
var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _); var smart = NetManager.GetBindAddress(ipAddress, out _, false);
var scheme = !allowHttps ? Uri.UriSchemeHttp : null; var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
int? port = !allowHttps ? HttpPort : null; int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart, scheme, port); return GetLocalApiUrl(smart, scheme, port);
@ -1063,30 +968,6 @@ namespace Emby.Server.Implementations
}.ToString().TrimEnd('/'); }.ToString().TrimEnd('/');
} }
/// <inheritdoc />
public async Task Shutdown()
{
if (IsShuttingDown)
{
return;
}
IsShuttingDown = true;
try
{
await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error sending server shutdown notification");
}
ShutdownInternal();
}
protected abstract void ShutdownInternal();
public IEnumerable<Assembly> GetApiPluginAssemblies() public IEnumerable<Assembly> GetApiPluginAssemblies()
{ {
var assemblies = _allConcreteTypes var assemblies = _allConcreteTypes
@ -1150,52 +1031,5 @@ namespace Emby.Server.Implementations
_disposed = true; _disposed = true;
} }
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(false);
GC.SuppressFinalize(this);
}
/// <summary>
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
/// </summary>
/// <returns>A ValueTask.</returns>
protected virtual async ValueTask DisposeAsyncCore()
{
var type = GetType();
Logger.LogInformation("Disposing {Type}", type.Name);
foreach (var (part, _) in _disposableParts)
{
var partType = part.GetType();
if (partType == type)
{
continue;
}
Logger.LogInformation("Disposing {Type}", partType.Name);
try
{
part.Dispose();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error disposing {Type}", partType.Name);
}
}
if (_sessionManager != null)
{
// used for closing websockets
foreach (var session in _sessionManager.Sessions)
{
await session.DisposeAsync().ConfigureAwait(false);
}
}
}
} }
} }

@ -371,8 +371,11 @@ namespace Emby.Server.Implementations.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path));
await using FileStream createStream = File.Create(path); FileStream createStream = File.Create(path);
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false); await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -1156,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels
if (info.People is not null && info.People.Count > 0) if (info.People is not null && info.People.Count > 0)
{ {
_libraryManager.UpdatePeople(item, info.People); await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
} }
} }
else if (forceUpdate) else if (forceUpdate)

@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
/// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class. /// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
/// </summary> /// </summary>
/// <param name="applicationPaths">The application paths.</param> /// <param name="applicationPaths">The application paths.</param>
/// <param name="loggerFactory">The paramref name="loggerFactory" factory.</param> /// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param> /// <param name="xmlSerializer">The XML serializer.</param>
/// <param name="fileSystem">The file system.</param> public ServerConfigurationManager(
public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) IApplicationPaths applicationPaths,
: base(applicationPaths, loggerFactory, xmlSerializer, fileSystem) ILoggerFactory loggerFactory,
IXmlSerializer xmlSerializer)
: base(applicationPaths, loggerFactory, xmlSerializer)
{ {
UpdateMetadataPath(); UpdateMetadataPath();
} }

@ -5,8 +5,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data namespace Emby.Server.Implementations.Data
{ {
@ -45,24 +45,6 @@ namespace Emby.Server.Implementations.Data
/// <value>The logger.</value> /// <value>The logger.</value>
protected ILogger<BaseSqliteRepository> Logger { get; } protected ILogger<BaseSqliteRepository> Logger { get; }
/// <summary>
/// Gets the default connection flags.
/// </summary>
/// <value>The default connection flags.</value>
protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex;
/// <summary>
/// Gets the transaction mode.
/// </summary>
/// <value>The transaction mode.</value>>
protected TransactionMode TransactionMode => TransactionMode.Deferred;
/// <summary>
/// Gets the transaction mode for read-only operations.
/// </summary>
/// <value>The transaction mode.</value>
protected TransactionMode ReadTransactionMode => TransactionMode.Deferred;
/// <summary> /// <summary>
/// Gets the cache size. /// Gets the cache size.
/// </summary> /// </summary>
@ -107,23 +89,8 @@ namespace Emby.Server.Implementations.Data
/// <see cref="SynchronousMode"/> /// <see cref="SynchronousMode"/>
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal; protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
/// <summary>
/// Gets or sets the write lock.
/// </summary>
/// <value>The write lock.</value>
protected ConnectionPool WriteConnections { get; set; }
/// <summary>
/// Gets or sets the write connection.
/// </summary>
/// <value>The write connection.</value>
protected ConnectionPool ReadConnections { get; set; }
public virtual void Initialize() public virtual void Initialize()
{ {
WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection);
ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection);
// Configuration and pragmas can affect VACUUM so it needs to be last. // Configuration and pragmas can affect VACUUM so it needs to be last.
using (var connection = GetConnection()) using (var connection = GetConnection())
{ {
@ -131,57 +98,10 @@ namespace Emby.Server.Implementations.Data
} }
} }
protected ManagedConnection GetConnection(bool readOnly = false) protected SqliteConnection GetConnection()
=> readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
protected SQLiteDatabaseConnection CreateWriteConnection()
{
var writeConnection = SQLite3.Open(
DbFilePath,
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
null);
if (CacheSize.HasValue)
{
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(LockingMode))
{
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
}
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
return writeConnection;
}
protected SQLiteDatabaseConnection CreateReadConnection()
{ {
var connection = SQLite3.Open( var connection = new SqliteConnection($"Filename={DbFilePath}");
DbFilePath, connection.Open();
DefaultConnectionFlags | ConnectionFlags.ReadOnly,
null);
if (CacheSize.HasValue) if (CacheSize.HasValue)
{ {
@ -208,39 +128,38 @@ namespace Emby.Server.Implementations.Data
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value); connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
} }
if (PageSize.HasValue)
{
connection.Execute("PRAGMA page_size=" + PageSize.Value);
}
connection.Execute("PRAGMA temp_store=" + (int)TempStore); connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return connection; return connection;
} }
public IStatement PrepareStatement(ManagedConnection connection, string sql) public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
=> connection.PrepareStatement(sql); {
var command = connection.CreateCommand();
public IStatement PrepareStatement(IDatabaseConnection connection, string sql) command.CommandText = sql;
=> connection.PrepareStatement(sql); return command;
}
protected bool TableExists(ManagedConnection connection, string name) protected bool TableExists(SqliteConnection connection, string name)
{ {
return connection.RunInTransaction( using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
db => foreach (var row in statement.ExecuteQuery())
{
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
{ {
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master")) return true;
{ }
foreach (var row in statement.ExecuteQuery()) }
{
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase)) return false;
{
return true;
}
}
}
return false;
},
ReadTransactionMode);
} }
protected List<string> GetColumnNames(IDatabaseConnection connection, string table) protected List<string> GetColumnNames(SqliteConnection connection, string table)
{ {
var columnNames = new List<string>(); var columnNames = new List<string>();
@ -255,7 +174,7 @@ namespace Emby.Server.Implementations.Data
return columnNames; return columnNames;
} }
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames) protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{ {
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase)) if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{ {
@ -291,12 +210,6 @@ namespace Emby.Server.Implementations.Data
return; return;
} }
if (dispose)
{
WriteConnections.Dispose();
ReadConnections.Dispose();
}
_disposed = true; _disposed = true;
} }
} }

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

@ -1,81 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
public sealed class ManagedConnection : IDisposable
{
private readonly ConnectionPool _pool;
private SQLiteDatabaseConnection _db;
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
{
_db = db;
_pool = pool;
}
public IStatement PrepareStatement(string sql)
{
return _db.PrepareStatement(sql);
}
public IEnumerable<IStatement> PrepareAll(string sql)
{
return _db.PrepareAll(sql);
}
public void ExecuteAll(string sql)
{
_db.ExecuteAll(sql);
}
public void Execute(string sql, params object[] values)
{
_db.Execute(sql, values);
}
public void RunQueries(string[] sql)
{
_db.RunQueries(sql);
}
public void RunInTransaction(Action<IDatabaseConnection> action, TransactionMode mode)
{
_db.RunInTransaction(action, mode);
}
public T RunInTransaction<T>(Func<IDatabaseConnection, T> action, TransactionMode mode)
{
return _db.RunInTransaction(action, mode);
}
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
{
return _db.Query(sql);
}
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
{
return _db.Query(sql, values);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_pool.Return(_db);
_db = null!; // Don't dispose it
_disposed = true;
}
}
}

@ -1,11 +1,10 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Data;
using System.Globalization; using System.Globalization;
using SQLitePCL.pretty; using Microsoft.Data.Sqlite;
namespace Emby.Server.Implementations.Data namespace Emby.Server.Implementations.Data
{ {
@ -52,19 +51,29 @@ namespace Emby.Server.Implementations.Data
"yy-MM-dd" "yy-MM-dd"
}; };
public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries) public static IEnumerable<SqliteDataReader> Query(this SqliteConnection sqliteConnection, string commandText)
{ {
ArgumentNullException.ThrowIfNull(queries); if (sqliteConnection.State != ConnectionState.Open)
{
sqliteConnection.Open();
}
connection.RunInTransaction(conn => using var command = sqliteConnection.CreateCommand();
command.CommandText = commandText;
using (var reader = command.ExecuteReader())
{ {
conn.ExecuteAll(string.Join(';', queries)); while (reader.Read())
}); {
yield return reader;
}
}
} }
public static Guid ReadGuidFromBlob(this ResultSetValue result) public static void Execute(this SqliteConnection sqliteConnection, string commandText)
{ {
return new Guid(result.ToBlob()); using var command = sqliteConnection.CreateCommand();
command.CommandText = commandText;
command.ExecuteNonQuery();
} }
public static string ToDateTimeParamValue(this DateTime dateValue) public static string ToDateTimeParamValue(this DateTime dateValue)
@ -83,27 +92,15 @@ namespace Emby.Server.Implementations.Data
private static string GetDateTimeKindFormat(DateTimeKind kind) private static string GetDateTimeKindFormat(DateTimeKind kind)
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal; => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
public static DateTime ReadDateTime(this ResultSetValue result) public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result)
{
var dateText = result.ToString();
return DateTime.ParseExact(
dateText,
_datetimeFormats,
DateTimeFormatInfo.InvariantInfo,
DateTimeStyles.AdjustToUniversal);
}
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
{ {
var item = reader[index]; if (reader.IsDBNull(index))
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
var dateText = item.ToString(); var dateText = reader.GetString(index);
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult)) if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
{ {
@ -115,335 +112,145 @@ namespace Emby.Server.Implementations.Data
return false; return false;
} }
public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result) public static bool TryGetGuid(this SqliteDataReader reader, int index, out Guid result)
{ {
var item = reader[index]; if (reader.IsDBNull(index))
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ReadGuidFromBlob(); result = reader.GetGuid(index);
return true; return true;
} }
public static bool IsDbNull(this ResultSetValue result) public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
{ {
return result.SQLiteType == SQLiteType.Null; result = string.Empty;
}
public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToString();
}
public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result) if (reader.IsDBNull(index))
{
result = null;
var item = reader[index];
if (item.IsDbNull())
{ {
return false; return false;
} }
result = item.ToString(); result = reader.GetString(index);
return true; return true;
} }
public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index) public static bool TryGetBoolean(this SqliteDataReader reader, int index, out bool result)
{
return result[index].ToBool();
}
public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
{ {
var item = reader[index]; if (reader.IsDBNull(index))
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ToBool(); result = reader.GetBoolean(index);
return true; return true;
} }
public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result) public static bool TryGetInt32(this SqliteDataReader reader, int index, out int result)
{ {
var item = reader[index]; if (reader.IsDBNull(index))
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ToInt(); result = reader.GetInt32(index);
return true; return true;
} }
public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index) public static bool TryGetInt64(this SqliteDataReader reader, int index, out long result)
{ {
return result[index].ToInt64(); if (reader.IsDBNull(index))
}
public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
{
var item = reader[index];
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ToInt64(); result = reader.GetInt64(index);
return true; return true;
} }
public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result) public static bool TryGetSingle(this SqliteDataReader reader, int index, out float result)
{ {
var item = reader[index]; if (reader.IsDBNull(index))
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ToFloat(); result = reader.GetFloat(index);
return true; return true;
} }
public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result) public static bool TryGetDouble(this SqliteDataReader reader, int index, out double result)
{ {
var item = reader[index]; if (reader.IsDBNull(index))
if (item.IsDbNull())
{ {
result = default; result = default;
return false; return false;
} }
result = item.ToDouble(); result = reader.GetDouble(index);
return true; return true;
} }
public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index) public static void TryBind(this SqliteCommand statement, string name, Guid value)
{ {
return result[index].ReadGuidFromBlob(); statement.TryBind(name, value, true);
} }
[Conditional("DEBUG")] public static void TryBind(this SqliteCommand statement, string name, object? value, bool isBlob = false)
private static void CheckName(string name)
{ {
throw new ArgumentException("Invalid param name: " + name, nameof(name)); var preparedValue = value ?? DBNull.Value;
} if (statement.Parameters.Contains(name))
public static void TryBind(this IStatement statement, string name, double value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{ {
bindParam.Bind(value); statement.Parameters[name].Value = preparedValue;
} }
else else
{ {
CheckName(name); // Blobs aren't always detected automatically
} if (isBlob)
}
public static void TryBind(this IStatement statement, string name, string value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
if (value is null)
{ {
bindParam.BindNull(); statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value });
} }
else else
{ {
bindParam.Bind(value); statement.Parameters.AddWithValue(name, preparedValue);
} }
} }
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, bool value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, float value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, int value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, Guid value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
Span<byte> byteValue = stackalloc byte[16];
value.TryWriteBytes(byteValue);
bindParam.Bind(byteValue);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, DateTime value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value.ToDateTimeParamValue());
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, long value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.Bind(value);
}
else
{
CheckName(name);
}
}
public static void TryBindNull(this IStatement statement, string name)
{
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
{
bindParam.BindNull();
}
else
{
CheckName(name);
}
}
public static void TryBind(this IStatement statement, string name, DateTime? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, Guid? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
}
public static void TryBind(this IStatement statement, string name, double? value)
{
if (value.HasValue)
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
} }
public static void TryBind(this IStatement statement, string name, int? value) public static void TryBindNull(this SqliteCommand statement, string name)
{ {
if (value.HasValue) statement.TryBind(name, DBNull.Value);
{
TryBind(statement, name, value.Value);
}
else
{
TryBindNull(statement, name);
}
} }
public static void TryBind(this IStatement statement, string name, float? value) public static IEnumerable<SqliteDataReader> ExecuteQuery(this SqliteCommand command)
{ {
if (value.HasValue) using (var reader = command.ExecuteReader())
{ {
TryBind(statement, name, value.Value); while (reader.Read())
} {
else yield return reader;
{ }
TryBindNull(statement, name);
} }
} }
public static void TryBind(this IStatement statement, string name, bool? value) public static int SelectScalarInt(this SqliteCommand command)
{ {
if (value.HasValue) var result = command.ExecuteScalar();
{ // Can't be null since the method is used to retrieve Count
TryBind(statement, name, value.Value); return Convert.ToInt32(result!, CultureInfo.InvariantCulture);
}
else
{
TryBindNull(statement, name);
}
} }
public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement) public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql)
{ {
while (statement.MoveNext()) var command = sqliteConnection.CreateCommand();
{ command.CommandText = sql;
yield return statement.Current; return command;
}
} }
} }
} }

File diff suppressed because it is too large Load Diff

@ -11,8 +11,8 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data namespace Emby.Server.Implementations.Data
{ {
@ -44,48 +44,48 @@ namespace Emby.Server.Implementations.Data
var userDataTableExists = TableExists(connection, "userdata"); var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : _userManager.Users; var users = userDatasTableExists ? null : _userManager.Users;
using var transaction = connection.BeginTransaction();
connection.Execute(string.Join(
';',
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
"drop index if exists idx_userdata",
"drop index if exists idx_userdata1",
"drop index if exists idx_userdata2",
"drop index if exists userdataindex1",
"drop index if exists userdataindex",
"drop index if exists userdataindex3",
"drop index if exists userdataindex4",
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"));
if (!userDataTableExists)
{
transaction.Commit();
return;
}
connection.RunInTransaction( var existingColumnNames = GetColumnNames(connection, "userdata");
db =>
{ AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
db.ExecuteAll(string.Join(';', new[] AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
{ AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
if (userDatasTableExists)
"drop index if exists idx_userdata", {
"drop index if exists idx_userdata1", return;
"drop index if exists idx_userdata2", }
"drop index if exists userdataindex1",
"drop index if exists userdataindex", ImportUserIds(connection, users);
"drop index if exists userdataindex3",
"drop index if exists userdataindex4", connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)", transaction.Commit();
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
}));
if (userDataTableExists)
{
var existingColumnNames = GetColumnNames(db, "userdata");
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
if (!userDatasTableExists)
{
ImportUserIds(db, users);
db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
}
}
},
TransactionMode);
} }
} }
private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users) private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
{ {
var userIdsWithUserData = GetAllUserIdsWithUserData(db); var userIdsWithUserData = GetAllUserIdsWithUserData(db);
@ -101,13 +101,12 @@ namespace Emby.Server.Implementations.Data
statement.TryBind("@UserId", user.Id); statement.TryBind("@UserId", user.Id);
statement.TryBind("@InternalUserId", user.InternalId); statement.TryBind("@InternalUserId", user.InternalId);
statement.MoveNext(); statement.ExecuteNonQuery();
statement.Reset();
} }
} }
} }
private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db) private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
{ {
var list = new List<Guid>(); var list = new List<Guid>();
@ -117,7 +116,7 @@ namespace Emby.Server.Implementations.Data
{ {
try try
{ {
list.Add(row[0].ReadGuidFromBlob()); list.Add(row.GetGuid(0));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -169,17 +168,14 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection()) using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{ {
connection.RunInTransaction( SaveUserData(connection, internalUserId, key, userData);
db => transaction.Commit();
{
SaveUserData(db, internalUserId, key, userData);
},
TransactionMode);
} }
} }
private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData) private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
{ {
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)")) using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
{ {
@ -227,7 +223,7 @@ namespace Emby.Server.Implementations.Data
statement.TryBindNull("@SubtitleStreamIndex"); statement.TryBindNull("@SubtitleStreamIndex");
} }
statement.MoveNext(); statement.ExecuteNonQuery();
} }
} }
@ -239,16 +235,14 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection()) using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{ {
connection.RunInTransaction( foreach (var userItemData in userDataList)
db => {
{ SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
foreach (var userItemData in userDataList) }
{
SaveUserData(db, internalUserId, userItemData.Key, userItemData); transaction.Commit();
}
},
TransactionMode);
} }
} }
@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.Data
ArgumentException.ThrowIfNullOrEmpty(key); ArgumentException.ThrowIfNullOrEmpty(key);
using (var connection = GetConnection(true)) using (var connection = GetConnection())
{ {
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId")) using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{ {
@ -336,7 +330,7 @@ namespace Emby.Server.Implementations.Data
/// </summary> /// </summary>
/// <param name="reader">The list of result set values.</param> /// <param name="reader">The list of result set values.</param>
/// <returns>The user item data.</returns> /// <returns>The user item data.</returns>
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader) private UserItemData ReadRow(SqliteDataReader reader)
{ {
var userData = new UserItemData(); var userData = new UserItemData();
@ -348,10 +342,10 @@ namespace Emby.Server.Implementations.Data
userData.Rating = rating; userData.Rating = rating;
} }
userData.Played = reader[3].ToBool(); userData.Played = reader.GetBoolean(3);
userData.PlayCount = reader[4].ToInt(); userData.PlayCount = reader.GetInt32(4);
userData.IsFavorite = reader[5].ToBool(); userData.IsFavorite = reader.GetBoolean(5);
userData.PlaybackPositionTicks = reader[6].ToInt64(); userData.PlaybackPositionTicks = reader.GetInt64(6);
if (reader.TryReadDateTime(7, out var lastPlayedDate)) if (reader.TryReadDateTime(7, out var lastPlayedDate))
{ {

@ -907,10 +907,11 @@ namespace Emby.Server.Implementations.Dto
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder; dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
} }
dto.LUFS = item.LUFS;
// Add audio info // Add audio info
if (item is Audio audio) if (item is Audio audio)
{ {
dto.LUFS = audio.LUFS;
dto.Album = audio.Album; dto.Album = audio.Album;
if (audio.ExtraType.HasValue) if (audio.ExtraType.HasValue)
{ {

@ -24,6 +24,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="DiscUtils.Udf" /> <PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Jellyfin.XmlTv" /> <PackageReference Include="Jellyfin.XmlTv" />
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
@ -31,7 +32,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
<PackageReference Include="Mono.Nat" /> <PackageReference Include="Mono.Nat" />
<PackageReference Include="prometheus-net.DotNetRuntime" /> <PackageReference Include="prometheus-net.DotNetRuntime" />
<PackageReference Include="SQLitePCL.pretty.netstandard" />
<PackageReference Include="DotNet.Glob" /> <PackageReference Include="DotNet.Glob" />
</ItemGroup> </ItemGroup>
@ -43,8 +43,6 @@
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.EntryPoints
return new StringBuilder(32) return new StringBuilder(32)
.Append(config.EnableUPnP).Append(Separator) .Append(config.EnableUPnP).Append(Separator)
.Append(config.PublicPort).Append(Separator) .Append(config.PublicHttpPort).Append(Separator)
.Append(config.PublicHttpsPort).Append(Separator) .Append(config.PublicHttpsPort).Append(Separator)
.Append(_appHost.HttpPort).Append(Separator) .Append(_appHost.HttpPort).Append(Separator)
.Append(_appHost.HttpsPort).Append(Separator) .Append(_appHost.HttpsPort).Append(Separator)
@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.EntryPoints
private IEnumerable<Task> CreatePortMaps(INatDevice device) private IEnumerable<Task> CreatePortMaps(INatDevice device)
{ {
var config = _config.GetNetworkConfiguration(); var config = _config.GetNetworkConfiguration();
yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort); yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
if (_appHost.ListenWithHttps) if (_appHost.ListenWithHttps)
{ {

@ -1,10 +1,15 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Udp; using Emby.Server.Implementations.Udp;
using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -13,7 +18,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints namespace Emby.Server.Implementations.EntryPoints
{ {
/// <summary> /// <summary>
/// Class UdpServerEntryPoint. /// Class responsible for registering all UDP broadcast endpoints and their handlers.
/// </summary> /// </summary>
public sealed class UdpServerEntryPoint : IServerEntryPoint public sealed class UdpServerEntryPoint : IServerEntryPoint
{ {
@ -29,13 +34,14 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IConfiguration _config; private readonly IConfiguration _config;
private readonly IConfigurationManager _configurationManager; private readonly IConfigurationManager _configurationManager;
private readonly INetworkManager _networkManager;
/// <summary> /// <summary>
/// The UDP server. /// The UDP server.
/// </summary> /// </summary>
private UdpServer? _udpServer; private readonly List<UdpServer> _udpServers;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private bool _disposed = false; private bool _disposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class. /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
@ -44,16 +50,20 @@ namespace Emby.Server.Implementations.EntryPoints
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param> /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
public UdpServerEntryPoint( public UdpServerEntryPoint(
ILogger<UdpServerEntryPoint> logger, ILogger<UdpServerEntryPoint> logger,
IServerApplicationHost appHost, IServerApplicationHost appHost,
IConfiguration configuration, IConfiguration configuration,
IConfigurationManager configurationManager) IConfigurationManager configurationManager,
INetworkManager networkManager)
{ {
_logger = logger; _logger = logger;
_appHost = appHost; _appHost = appHost;
_config = configuration; _config = configuration;
_configurationManager = configurationManager; _configurationManager = configurationManager;
_networkManager = networkManager;
_udpServers = new List<UdpServer>();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -68,8 +78,43 @@ namespace Emby.Server.Implementations.EntryPoints
try try
{ {
_udpServer = new UdpServer(_logger, _appHost, _config, PortNumber); // Linux needs to bind to the broadcast addresses to get broadcast traffic
_udpServer.Start(_cancellationTokenSource.Token); // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
if (OperatingSystem.IsLinux())
{
// Add global broadcast listener
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server);
// Add bind address specific broadcast listeners
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
foreach (var intf in validInterfaces)
{
var broadcastAddress = NetworkExtensions.GetBroadcastAddress(intf.Subnet);
_logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber);
server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber);
server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server);
}
}
else
{
// Add bind address specific broadcast listeners
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
foreach (var intf in validInterfaces)
{
var intfAddress = intf.Address;
_logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server);
}
}
} }
catch (SocketException ex) catch (SocketException ex)
{ {
@ -83,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
{ {
if (_disposed) if (_disposed)
{ {
throw new ObjectDisposedException(this.GetType().Name); throw new ObjectDisposedException(GetType().Name);
} }
} }
@ -97,9 +142,12 @@ namespace Emby.Server.Implementations.EntryPoints
_cancellationTokenSource.Cancel(); _cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose(); _cancellationTokenSource.Dispose();
_udpServer?.Dispose(); foreach (var server in _udpServers)
_udpServer = null; {
server.Dispose();
}
_udpServers.Clear();
_disposed = true; _disposed = true;
} }
} }

@ -9,7 +9,8 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net; using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -42,14 +43,17 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="socket">The socket.</param> /// <param name="socket">The socket.</param>
/// <param name="authorizationInfo">The authorization information.</param>
/// <param name="remoteEndPoint">The remote end point.</param> /// <param name="remoteEndPoint">The remote end point.</param>
public WebSocketConnection( public WebSocketConnection(
ILogger<WebSocketConnection> logger, ILogger<WebSocketConnection> logger,
WebSocket socket, WebSocket socket,
AuthorizationInfo authorizationInfo,
IPAddress? remoteEndPoint) IPAddress? remoteEndPoint)
{ {
_logger = logger; _logger = logger;
_socket = socket; _socket = socket;
AuthorizationInfo = authorizationInfo;
RemoteEndPoint = remoteEndPoint; RemoteEndPoint = remoteEndPoint;
_jsonOptions = JsonDefaults.Options; _jsonOptions = JsonDefaults.Options;
@ -59,47 +63,40 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc /> /// <inheritdoc />
public event EventHandler<EventArgs>? Closed; public event EventHandler<EventArgs>? Closed;
/// <summary> /// <inheritdoc />
/// Gets the remote end point. public AuthorizationInfo AuthorizationInfo { get; }
/// </summary>
/// <inheritdoc />
public IPAddress? RemoteEndPoint { get; } public IPAddress? RemoteEndPoint { get; }
/// <summary> /// <inheritdoc />
/// Gets or sets the receive action.
/// </summary>
/// <value>The receive action.</value>
public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; } public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
/// <summary> /// <inheritdoc />
/// Gets the last activity date.
/// </summary>
/// <value>The last activity date.</value>
public DateTime LastActivityDate { get; private set; } public DateTime LastActivityDate { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public DateTime LastKeepAliveDate { get; set; } public DateTime LastKeepAliveDate { get; set; }
/// <summary> /// <inheritdoc />
/// Gets the state.
/// </summary>
/// <value>The state.</value>
public WebSocketState State => _socket.State; public WebSocketState State => _socket.State;
/// <summary> /// <inheritdoc />
/// Sends a message asynchronously. public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
/// </summary> {
/// <typeparam name="T">The type of the message.</typeparam> var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
/// <param name="message">The message.</param> return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
/// <param name="cancellationToken">The cancellation token.</param> }
/// <returns>Task.</returns>
public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken) /// <inheritdoc />
public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
{ {
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions); var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken); return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task ProcessAsync(CancellationToken cancellationToken = default) public async Task ReceiveAsync(CancellationToken cancellationToken = default)
{ {
var pipe = new Pipe(); var pipe = new Pipe();
var writer = pipe.Writer; var writer = pipe.Writer;
@ -171,7 +168,7 @@ namespace Emby.Server.Implementations.HttpServer
return; return;
} }
WebSocketMessage<object>? stub; InboundWebSocketMessage<object>? stub;
long bytesConsumed; long bytesConsumed;
try try
{ {
@ -212,10 +209,10 @@ namespace Emby.Server.Implementations.HttpServer
} }
} }
internal WebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed) internal InboundWebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed)
{ {
var jsonReader = new Utf8JsonReader(bytes); var jsonReader = new Utf8JsonReader(bytes);
var ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions); var ret = JsonSerializer.Deserialize<InboundWebSocketMessage<object>>(ref jsonReader, _jsonOptions);
bytesConsumed = jsonReader.BytesConsumed; bytesConsumed = jsonReader.BytesConsumed;
return ret; return ret;
} }
@ -224,11 +221,7 @@ namespace Emby.Server.Implementations.HttpServer
{ {
LastKeepAliveDate = DateTime.UtcNow; LastKeepAliveDate = DateTime.UtcNow;
return SendAsync( return SendAsync(
new WebSocketMessage<string> new OutboundKeepAliveMessage(),
{
MessageId = Guid.NewGuid(),
MessageType = SessionMessageType.KeepAlive
},
CancellationToken.None); CancellationToken.None);
} }

@ -51,7 +51,8 @@ namespace Emby.Server.Implementations.HttpServer
using var connection = new WebSocketConnection( using var connection = new WebSocketConnection(
_loggerFactory.CreateLogger<WebSocketConnection>(), _loggerFactory.CreateLogger<WebSocketConnection>(),
webSocket, webSocket,
context.GetNormalizedRemoteIp()) authorizationInfo,
context.GetNormalizedRemoteIP())
{ {
OnReceive = ProcessWebSocketMessageReceived OnReceive = ProcessWebSocketMessageReceived
}; };
@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
await Task.WhenAll(tasks).ConfigureAwait(false); await Task.WhenAll(tasks).ConfigureAwait(false);
await connection.ProcessAsync().ConfigureAwait(false); await connection.ReceiveAsync().ConfigureAwait(false);
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
} }
catch (Exception ex) // Otherwise ASP.Net will ignore the exception catch (Exception ex) // Otherwise ASP.Net will ignore the exception

@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.IO
} }
} }
public void ResetPath(string path, string affectedFile) public void ResetPath(string path, string? affectedFile)
{ {
lock (_timerLock) lock (_timerLock)
{ {
@ -148,13 +148,6 @@ namespace Emby.Server.Implementations.IO
{ {
item.ChangedExternally(); item.ChangedExternally();
} }
catch (IOException ex)
{
// For now swallow and log.
// Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
// Should we remove it from it's parent?
_logger.LogError(ex, "Error refreshing {Name}", item.Name);
}
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error refreshing {Name}", item.Name); _logger.LogError(ex, "Error refreshing {Name}", item.Name);
@ -217,7 +210,6 @@ namespace Emby.Server.Implementations.IO
DisposeTimer(); DisposeTimer();
_disposed = true; _disposed = true;
GC.SuppressFinalize(this);
} }
} }
} }

@ -1,5 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -160,7 +158,7 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="sender">The source of the event.</param> /// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e) private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
{ {
if (e.Parent is AggregateFolder) if (e.Parent is AggregateFolder)
{ {
@ -173,7 +171,7 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="sender">The source of the event.</param> /// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e) private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{ {
if (e.Parent is AggregateFolder) if (e.Parent is AggregateFolder)
{ {
@ -189,19 +187,28 @@ namespace Emby.Server.Implementations.IO
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns> /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception> /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
private static bool ContainsParentFolder(IEnumerable<string> lst, string path) private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path)
{ {
ArgumentException.ThrowIfNullOrEmpty(path); if (path.IsEmpty)
{
throw new ArgumentException("Path can't be empty", nameof(path));
}
path = path.TrimEnd(Path.DirectorySeparatorChar); path = path.TrimEnd(Path.DirectorySeparatorChar);
return lst.Any(str => foreach (var str in lst)
{ {
// this should be a little quicker than examining each actual parent folder... // this should be a little quicker than examining each actual parent folder...
var compare = str.TrimEnd(Path.DirectorySeparatorChar); var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar); if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
}); || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar))
{
return true;
}
}
return false;
} }
/// <summary> /// <summary>
@ -349,21 +356,19 @@ namespace Emby.Server.Implementations.IO
{ {
ArgumentException.ThrowIfNullOrEmpty(path); ArgumentException.ThrowIfNullOrEmpty(path);
var monitorPath = !IgnorePatterns.ShouldIgnore(path); if (IgnorePatterns.ShouldIgnore(path))
{
return;
}
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too // Ignore certain files, If the parent of an ignored path has a change event, ignore that too
if (_tempIgnoredPaths.Keys.Any(i => foreach (var i in _tempIgnoredPaths.Keys)
{ {
if (_fileSystem.AreEqual(i, path)) if (_fileSystem.AreEqual(i, path)
{ || _fileSystem.ContainsSubPath(i, path))
_logger.LogDebug("Ignoring change to {Path}", path);
return true;
}
if (_fileSystem.ContainsSubPath(i, path))
{ {
_logger.LogDebug("Ignoring change to {Path}", path); _logger.LogDebug("Ignoring change to {Path}", path);
return true; return;
} }
// Go up a level // Go up a level
@ -371,20 +376,11 @@ namespace Emby.Server.Implementations.IO
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path)) if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
{ {
_logger.LogDebug("Ignoring change to {Path}", path); _logger.LogDebug("Ignoring change to {Path}", path);
return true; return;
} }
return false;
}))
{
monitorPath = false;
} }
if (monitorPath) CreateRefresher(path);
{
// Avoid implicitly captured closure
CreateRefresher(path);
}
} }
private void CreateRefresher(string path) private void CreateRefresher(string path)
@ -417,7 +413,8 @@ namespace Emby.Server.Implementations.IO
} }
// They are siblings. Rebase the refresher to the parent folder. // They are siblings. Rebase the refresher to the parent folder.
if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal)) if (parentPath is not null
&& Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
{ {
refresher.ResetPath(parentPath, path); refresher.ResetPath(parentPath, path);
return; return;
@ -430,8 +427,13 @@ namespace Emby.Server.Implementations.IO
} }
} }
private void OnNewRefresherCompleted(object sender, EventArgs e) private void OnNewRefresherCompleted(object? sender, EventArgs e)
{ {
if (sender is null)
{
return;
}
var refresher = (FileRefresher)sender; var refresher = (FileRefresher)sender;
DisposeRefresher(refresher); DisposeRefresher(refresher);
} }

@ -15,29 +15,34 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
public class ManagedFileSystem : IFileSystem public class ManagedFileSystem : IFileSystem
{ {
private readonly ILogger<ManagedFileSystem> _logger; private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
private static readonly char[] _invalidPathCharacters =
{
'\"', '<', '>', '|', '\0',
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
(char)31, ':', '*', '?', '\\', '/'
};
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); private readonly ILogger<ManagedFileSystem> _logger;
private readonly List<IShortcutHandler> _shortcutHandlers;
private readonly string _tempPath; private readonly string _tempPath;
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
/// </summary> /// </summary>
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param> /// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param> /// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
/// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param>
public ManagedFileSystem( public ManagedFileSystem(
ILogger<ManagedFileSystem> logger, ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths) IApplicationPaths applicationPaths,
IEnumerable<IShortcutHandler> shortcutHandlers)
{ {
_logger = logger; _logger = logger;
_tempPath = applicationPaths.TempDirectory; _tempPath = applicationPaths.TempDirectory;
} _shortcutHandlers = shortcutHandlers.ToList();
/// <inheritdoc />
public virtual void AddShortcutHandler(IShortcutHandler handler)
{
_shortcutHandlers.Add(handler);
} }
/// <summary> /// <summary>
@ -86,7 +91,7 @@ namespace Emby.Server.Implementations.IO
} }
// unc path // unc path
if (filePath.StartsWith("\\\\", StringComparison.Ordinal)) if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
{ {
return filePath; return filePath;
} }
@ -98,15 +103,17 @@ namespace Emby.Server.Implementations.IO
return filePath; return filePath;
} }
var filePathSpan = filePath.AsSpan();
// relative path // relative path
if (firstChar == '\\') if (firstChar == '\\')
{ {
filePath = filePath.Substring(1); filePathSpan = filePathSpan.Slice(1);
} }
try try
{ {
return Path.GetFullPath(Path.Combine(folderPath, filePath)); return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
} }
catch (ArgumentException) catch (ArgumentException)
{ {
@ -275,8 +282,7 @@ namespace Emby.Server.Implementations.IO
/// <exception cref="ArgumentNullException">The filename is null.</exception> /// <exception cref="ArgumentNullException">The filename is null.</exception>
public string GetValidFilename(string filename) public string GetValidFilename(string filename)
{ {
var invalid = Path.GetInvalidFileNameChars(); var first = filename.IndexOfAny(_invalidPathCharacters);
var first = filename.IndexOfAny(invalid);
if (first == -1) if (first == -1)
{ {
// Fast path for clean strings // Fast path for clean strings
@ -285,7 +291,7 @@ namespace Emby.Server.Implementations.IO
return string.Create( return string.Create(
filename.Length, filename.Length,
(filename, invalid, first), (filename, _invalidPathCharacters, first),
(chars, state) => (chars, state) =>
{ {
state.filename.AsSpan().CopyTo(chars); state.filename.AsSpan().CopyTo(chars);
@ -293,7 +299,7 @@ namespace Emby.Server.Implementations.IO
chars[state.first++] = ' '; chars[state.first++] = ' ';
var len = chars.Length; var len = chars.Length;
foreach (var c in state.invalid) foreach (var c in state._invalidPathCharacters)
{ {
for (int i = state.first; i < len; i++) for (int i = state.first; i < len; i++)
{ {
@ -478,25 +484,11 @@ namespace Emby.Server.Implementations.IO
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
} }
/// <inheritdoc />
public virtual string NormalizePath(string path)
{
ArgumentException.ThrowIfNullOrEmpty(path);
if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
{
return path;
}
return Path.TrimEndingDirectorySeparator(path);
}
/// <inheritdoc /> /// <inheritdoc />
public virtual bool AreEqual(string path1, string path2) public virtual bool AreEqual(string path1, string path2)
{ {
return string.Equals( return Path.TrimEndingDirectorySeparator(path1).Equals(
NormalizePath(path1), Path.TrimEndingDirectorySeparator(path2),
NormalizePath(path2),
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
} }

@ -8,24 +8,17 @@ namespace Emby.Server.Implementations.IO
{ {
public class MbLinkShortcutHandler : IShortcutHandler public class MbLinkShortcutHandler : IShortcutHandler
{ {
private readonly IFileSystem _fileSystem;
public MbLinkShortcutHandler(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
public string Extension => ".mblink"; public string Extension => ".mblink";
public string? Resolve(string shortcutPath) public string? Resolve(string shortcutPath)
{ {
ArgumentException.ThrowIfNullOrEmpty(shortcutPath); ArgumentException.ThrowIfNullOrEmpty(shortcutPath);
if (string.Equals(Path.GetExtension(shortcutPath), ".mblink", StringComparison.OrdinalIgnoreCase)) if (Path.GetExtension(shortcutPath.AsSpan()).Equals(".mblink", StringComparison.OrdinalIgnoreCase))
{ {
var path = File.ReadAllText(shortcutPath); var path = File.ReadAllText(shortcutPath);
return _fileSystem.NormalizePath(path); return Path.TrimEndingDirectorySeparator(path);
} }
return null; return null;

@ -31,6 +31,7 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery return _libraryManager.GetItemList(new InternalItemsQuery
{ {
Parent = item, Parent = item,
Recursive = true,
DtoOptions = new DtoOptions(true), DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary }, ImageTypes = new ImageType[] { ImageType.Primary },
OrderBy = new (string, SortOrder)[] OrderBy = new (string, SortOrder)[]

@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
// bts sync files // bts sync files
"**/*.bts", "**/*.bts",
"**/*.sync", "**/*.sync",
// zfs
"**/.zfs/**",
"**/.zfs"
}; };
private static readonly GlobOptions _globOptions = new GlobOptions private static readonly GlobOptions _globOptions = new GlobOptions

@ -3,6 +3,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@ -45,7 +46,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library; using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library
private const string ShortcutFileExtension = ".mblink"; private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger; private readonly ILogger<LibraryManager> _logger;
private readonly IMemoryCache _memoryCache; private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository; private readonly IUserDataManager _userDataRepository;
@ -111,7 +111,6 @@ namespace Emby.Server.Implementations.Library
/// <param name="mediaEncoder">The media encoder.</param> /// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param> /// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
/// <param name="memoryCache">The memory cache.</param>
/// <param name="namingOptions">The naming options.</param> /// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param> /// <param name="directoryService">The directory service.</param>
public LibraryManager( public LibraryManager(
@ -128,7 +127,6 @@ namespace Emby.Server.Implementations.Library
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IItemRepository itemRepository, IItemRepository itemRepository,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
IMemoryCache memoryCache,
NamingOptions namingOptions, NamingOptions namingOptions,
IDirectoryService directoryService) IDirectoryService directoryService)
{ {
@ -145,7 +143,7 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_itemRepository = itemRepository; _itemRepository = itemRepository;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_memoryCache = memoryCache; _cache = new ConcurrentDictionary<Guid, BaseItem>();
_namingOptions = namingOptions; _namingOptions = namingOptions;
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService); _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
@ -300,7 +298,7 @@ namespace Emby.Server.Implementations.Library
} }
} }
_memoryCache.Set(item.Id, item); _cache[item.Id] = item;
} }
public void DeleteItem(BaseItem item, DeleteOptions options) public void DeleteItem(BaseItem item, DeleteOptions options)
@ -359,7 +357,7 @@ namespace Emby.Server.Implementations.Library
var children = item.IsFolder var children = item.IsFolder
? ((Folder)item).GetRecursiveChildren(false) ? ((Folder)item).GetRecursiveChildren(false)
: Enumerable.Empty<BaseItem>(); : Array.Empty<BaseItem>();
foreach (var metadataPath in GetMetadataPaths(item, children)) foreach (var metadataPath in GetMetadataPaths(item, children))
{ {
@ -441,7 +439,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.DeleteItem(child.Id); _itemRepository.DeleteItem(child.Id);
} }
_memoryCache.Remove(item.Id); _cache.TryRemove(item.Id, out _);
ReportItemRemoved(item, parent); ReportItemRemoved(item, parent);
} }
@ -609,7 +607,7 @@ namespace Emby.Server.Implementations.Library
var originalList = paths.ToList(); var originalList = paths.ToList();
var list = originalList.Where(i => i.IsDirectory) var list = originalList.Where(i => i.IsDirectory)
.Select(i => _fileSystem.NormalizePath(i.FullName)) .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
@ -840,19 +838,12 @@ namespace Emby.Server.Implementations.Library
{ {
var path = Person.GetPath(name); var path = Person.GetPath(name);
var id = GetItemByNameId<Person>(path); var id = GetItemByNameId<Person>(path);
if (GetItemById(id) is not Person item) if (GetItemById(id) is Person item)
{ {
item = new Person return item;
{
Name = name,
Id = id,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
Path = path
};
} }
return item; return null;
} }
/// <summary> /// <summary>
@ -1163,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
Name = Path.GetFileName(dir), Name = Path.GetFileName(dir),
Locations = _fileSystem.GetFilePaths(dir, false) Locations = _fileSystem.GetFilePaths(dir, false)
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.Select(i => .Select(i =>
{ {
try try
@ -1233,7 +1224,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id)); throw new ArgumentException("Guid can't be empty", nameof(id));
} }
if (_memoryCache.TryGetValue(id, out BaseItem item)) if (_cache.TryGetValue(id, out BaseItem item))
{ {
return item; return item;
} }
@ -2069,7 +2060,9 @@ namespace Emby.Server.Implementations.Library
.Find(folder => folder is CollectionFolder) as CollectionFolder; .Find(folder => folder is CollectionFolder) as CollectionFolder;
} }
return collectionFolder is null ? new LibraryOptions() : collectionFolder.GetLibraryOptions(); return collectionFolder is null
? new LibraryOptions()
: collectionFolder.GetLibraryOptions();
} }
public string GetContentType(BaseItem item) public string GetContentType(BaseItem item)
@ -2857,7 +2850,7 @@ namespace Emby.Server.Implementations.Library
{ {
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection"); var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
File.WriteAllBytes(path, Array.Empty<byte>()); await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
} }
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@ -2899,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
var saveEntity = false; var saveEntity = false;
var personEntity = GetPerson(person.Name); var personEntity = GetPerson(person.Name);
// if PresentationUniqueKey is empty it's likely a new item. if (personEntity is null)
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
{ {
var path = Person.GetPath(person.Name);
personEntity = new Person()
{
Name = person.Name,
Id = GetItemByNameId<Person>(path),
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
Path = path
};
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true; saveEntity = true;
} }
@ -3134,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
} }
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(shortcut)) if (!string.IsNullOrEmpty(shortcut))

@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey)) if (!string.IsNullOrEmpty(cacheKey))
{ {
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try try
{ {
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info"); // _logger.LogDebug("Found cached media info");
} }
catch catch (Exception ex)
{ {
_logger.LogError(ex, "Error deserializing mediainfo cache");
}
finally
{
await jsonStream.DisposeAsync().ConfigureAwait(false);
} }
} }
@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null) if (cacheFilePath is not null)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
// _logger.LogDebug("Saved media info to {0}", cacheFilePath); _logger.LogDebug("Saved media info to {0}", cacheFilePath);
} }
} }

@ -625,17 +625,19 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey)) if (!string.IsNullOrEmpty(cacheKey))
{ {
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try try
{ {
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception."); _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
} }
finally
{
await jsonStream.DisposeAsync().ConfigureAwait(false);
}
} }
if (mediaInfo is null) if (mediaInfo is null)
@ -664,8 +666,11 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null) if (cacheFilePath is not null)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
await using FileStream createStream = File.Create(cacheFilePath); FileStream createStream = File.Create(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
// _logger.LogDebug("Saved media info to {0}", cacheFilePath); // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
} }

@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions)) if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
{ {
var extension = Path.GetExtension(args.Path); var extension = Path.GetExtension(args.Path.AsSpan());
if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase)) if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
{ {
// if audio file exists of same name, return null // if audio file exists of same name, return null
return null; return null;
@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (item is not null) if (item is not null)
{ {
item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
item.IsInMixedFolder = true; item.IsInMixedFolder = true;
} }

@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return false; return false;
} }
return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase)); return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
} }
/// <summary> /// <summary>

@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
return GetBook(args); return GetBook(args);
} }
var extension = Path.GetExtension(args.Path); var extension = Path.GetExtension(args.Path.AsSpan());
if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{ {
// It's a book // It's a book
return new Book return new Book
@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{ {
var bookFiles = args.FileSystemChildren.Where(f => var bookFiles = args.FileSystemChildren.Where(f =>
{ {
var fileExtension = Path.GetExtension(f.FullName) var fileExtension = Path.GetExtension(f.FullName.AsSpan());
?? string.Empty;
return _validExtensions.Contains( return _validExtensions.Contains(
fileExtension, fileExtension,
StringComparer.OrdinalIgnoreCase); StringComparison.OrdinalIgnoreCase);
}).ToList(); }).ToList();
// Don't return a Book if there is more (or less) than one document in the directory // Don't return a Book if there is more (or less) than one document in the directory

@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <summary> /// <summary>
/// Class MovieResolver. /// Class MovieResolver.
/// </summary> /// </summary>
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
{ {
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
@ -56,6 +56,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <value>The priority.</value> /// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Fourth; public override ResolverPriority Priority => ResolverPriority.Fourth;
[GeneratedRegex(@"\bsample\b", RegexOptions.IgnoreCase)]
private static partial Regex IsIgnoredRegex();
/// <inheritdoc /> /// <inheritdoc />
public MultiItemResolverResult ResolveMultiple( public MultiItemResolverResult ResolveMultiple(
Folder parent, Folder parent,
@ -261,7 +264,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{ {
leftOver.Add(child); leftOver.Add(child);
} }
else if (!IsIgnored(child.Name)) else if (!IsIgnoredRegex().IsMatch(child.Name))
{ {
files.Add(child); files.Add(child);
} }
@ -314,9 +317,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result; return result;
} }
private static bool IsIgnored(ReadOnlySpan<char> filename)
=> Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file) private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
{ {
for (var i = 0; i < result.Count; i++) for (var i = 0; i < result.Count; i++)

@ -1,7 +1,4 @@
#nullable disable
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Emby.Naming.Common; using Emby.Naming.Common;
@ -25,7 +22,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
private readonly NamingOptions _namingOptions; private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) private static readonly string[] _ignoreFiles = new[]
{ {
"folder", "folder",
"thumb", "thumb",
@ -56,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary> /// </summary>
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <returns>Trailer.</returns> /// <returns>Trailer.</returns>
protected override Photo Resolve(ItemResolveArgs args) protected override Photo? Resolve(ItemResolveArgs args)
{ {
if (!args.IsDirectory) if (!args.IsDirectory)
{ {
@ -68,10 +65,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
{ {
if (IsImageFile(args.Path, _imageProcessor)) if (IsImageFile(args.Path, _imageProcessor))
{ {
var filename = Path.GetFileNameWithoutExtension(args.Path); var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
// Make sure the image doesn't belong to a video file // Make sure the image doesn't belong to a video file
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)); var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
?? throw new InvalidOperationException("Path can't be a root directory."));
foreach (var file in files) foreach (var file in files)
{ {
@ -92,32 +90,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null; return null;
} }
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename) internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename)
{ {
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename); return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
} }
internal static bool IsOwnedByResolvedMedia(string file, string imageFilename) internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename)
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase); => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor) internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{ {
ArgumentNullException.ThrowIfNull(path); ArgumentNullException.ThrowIfNull(path);
var filename = Path.GetFileNameWithoutExtension(path); var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
if (_ignoreFiles.Contains(filename))
{ {
return false; return false;
} }
if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1)) var filename = Path.GetFileNameWithoutExtension(path);
if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
{ {
return false; return false;
} }
string extension = Path.GetExtension(path).TrimStart('.'); return true;
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
} }
} }
} }

@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var resolver = new Naming.TV.EpisodeResolver(namingOptions); var resolver = new Naming.TV.EpisodeResolver(namingOptions);
var folderName = System.IO.Path.GetFileName(path); var folderName = System.IO.Path.GetFileName(path);
var testPath = "\\\\test\\" + folderName; var testPath = @"\\test\" + folderName;
var episodeInfo = resolver.Resolve(testPath, true); var episodeInfo = resolver.Resolve(testPath, true);

@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return; return;
} }
await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
await using (stream.ConfigureAwait(false))
{ {
var settings = new XmlWriterSettings var settings = new XmlWriterSettings
{ {
@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Async = true Async = true
}; };
await using (var writer = XmlWriter.Create(stream, settings)) var writer = XmlWriter.Create(stream, settings);
await using (writer.ConfigureAwait(false))
{ {
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false); await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return; return;
} }
await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
await using (stream.ConfigureAwait(false))
{ {
var settings = new XmlWriterSettings var settings = new XmlWriterSettings
{ {
@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var isSeriesEpisode = timer.IsProgramSeries; var isSeriesEpisode = timer.IsProgramSeries;
await using (var writer = XmlWriter.Create(stream, settings)) var writer = XmlWriter.Create(stream, settings);
await using (writer.ConfigureAwait(false))
{ {
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
} }
else else
{ {
await writer.WriteStartElementAsync(null, "movie", null); await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(item.Name)) if (!string.IsNullOrWhiteSpace(item.Name))
{ {

@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = JsonContent.Create(requestList, options: _jsonOptions); options.Content = JsonContent.Create(requestList, options: _jsonOptions);
options.Headers.TryAddWithoutValidation("token", token); options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null) if (dailySchedules is null)
{ {
return Array.Empty<ProgramInfo>(); return Array.Empty<ProgramInfo>();
@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions); programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (programDetails is null) if (programDetails is null)
{ {
return Array.Empty<ProgramInfo>(); return Array.Empty<ProgramInfo>();
@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try try
{ {
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false); using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try try
{ {
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false); using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is not null) if (root is not null)
{ {
foreach (HeadendsDto headend in root) foreach (HeadendsDto headend in root)
@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(root?.Message, "OK", StringComparison.Ordinal)) if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{ {
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token); _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{ {
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode(); httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false; return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
} }
catch (HttpRequestException ex) catch (HttpRequestException ex)
@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token); options.Headers.TryAddWithoutValidation("token", token);
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is null) if (root is null)
{ {
return new List<ChannelInfo>(); return new List<ChannelInfo>();

@ -3,6 +3,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -16,21 +17,20 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
public abstract class BaseTunerHost public abstract class BaseTunerHost
{ {
private readonly IMemoryCache _memoryCache; private readonly ConcurrentDictionary<string, List<ChannelInfo>> _cache;
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache) protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem)
{ {
Config = config; Config = config;
Logger = logger; Logger = logger;
_memoryCache = memoryCache;
FileSystem = fileSystem; FileSystem = fileSystem;
_cache = new ConcurrentDictionary<string, List<ChannelInfo>>();
} }
protected IServerConfigurationManager Config { get; } protected IServerConfigurationManager Config { get; }
@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
var key = tuner.Id; var key = tuner.Id;
if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache)) if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List<ChannelInfo> cache))
{ {
return cache; return cache;
} }
@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!string.IsNullOrEmpty(key) && list.Count > 0) if (!string.IsNullOrEmpty(key) && list.Count > 0)
{ {
_memoryCache.Set(key, list); _cache[key] = list;
} }
return list; return list;

@ -9,6 +9,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -27,7 +28,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@ -50,9 +50,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost, IServerApplicationHost appHost,
ISocketFactory socketFactory, ISocketFactory socketFactory,
IStreamHelper streamHelper, IStreamHelper streamHelper)
IMemoryCache memoryCache) : base(config, logger, fileSystem)
: base(config, logger, fileSystem, memoryCache)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_appHost = appHost; _appHost = appHost;
@ -77,13 +76,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>();
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
if (info.ImportFavoritesOnly) if (info.ImportFavoritesOnly)
{ {
lineup = lineup.Where(i => i.Favorite).ToList(); lineup = lineup.Where(i => i.Favorite);
} }
return lineup.Where(i => !i.DRM).ToList(); return lineup.Where(i => !i.DRM).ToList();
@ -130,9 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
.GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken) .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey)) if (!string.IsNullOrEmpty(cacheKey))
{ {
@ -176,34 +170,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List<LiveTvTunerInfo>(); var tuners = new List<LiveTvTunerInfo>();
await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{ {
string stripedLine = StripXML(line); using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
if (stripedLine.Contains("Channel", StringComparison.Ordinal)) await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
{ {
LiveTvTunerStatus status; string stripedLine = StripXML(line);
var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); if (stripedLine.Contains("Channel", StringComparison.Ordinal))
var name = stripedLine.Substring(0, index - 1);
var currentChannel = stripedLine.Substring(index + 7);
if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
{ {
status = LiveTvTunerStatus.LiveTv; LiveTvTunerStatus status;
} var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
else var name = stripedLine.Substring(0, index - 1);
{ var currentChannel = stripedLine.Substring(index + 7);
status = LiveTvTunerStatus.Available; if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
} {
status = LiveTvTunerStatus.LiveTv;
}
else
{
status = LiveTvTunerStatus.Available;
}
tuners.Add(new LiveTvTunerInfo tuners.Add(new LiveTvTunerInfo
{ {
Name = name, Name = name,
SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
ProgramName = currentChannel, ProgramName = currentChannel,
Status = status Status = status
}); });
}
} }
} }
@ -661,18 +658,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
// Need a way to set the Receive timeout on the socket otherwise this might never timeout? // Need a way to set the Receive timeout on the socket otherwise this might never timeout?
try try
{ {
await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false); await udpClient.SendToAsync(discBytes, new IPEndPoint(IPAddress.Broadcast, 65001), cancellationToken).ConfigureAwait(false);
var receiveBuffer = new byte[8192]; var receiveBuffer = new byte[8192];
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)
{ {
var response = await udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); var response = await udpClient.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, 0), cancellationToken).ConfigureAwait(false);
var deviceIp = response.RemoteEndPoint.Address.ToString(); var deviceIP = ((IPEndPoint)response.RemoteEndPoint).Address.ToString();
// check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte // Check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
if (response.ReceivedBytes > 13 && response.Buffer[1] == 3) if (response.ReceivedBytes > 13 && receiveBuffer[1] == 3)
{ {
var deviceAddress = "http://" + deviceIp; var deviceAddress = "http://" + deviceIP;
var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false); var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false);

@ -44,14 +44,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult(); StopStreaming(socket).GetAwaiter().GetResult();
} }
} }
GC.SuppressFinalize(this);
} }
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)
{ {
using var client = new TcpClient(); using var client = new TcpClient();
await client.ConnectAsync(remoteIp, HdHomeRunPort, cancellationToken).ConfigureAwait(false); await client.ConnectAsync(remoteIP, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
using var stream = client.GetStream(); using var stream = client.GetStream();
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
@ -75,9 +73,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
} }
public async Task StartStreaming(IPAddress remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken) public async Task StartStreaming(IPAddress remoteIP, IPAddress localIP, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
{ {
_remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort); _remoteEndPoint = new IPEndPoint(remoteIP, HdHomeRunPort);
_tcpClient = new TcpClient(); _tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false); await _tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false);
@ -125,7 +123,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
} }
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort); var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIP, localPort);
var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue); var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false); await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);

@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands public partial class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
{ {
private string? _channel; private string? _channel;
private string? _program; private string? _program;
@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public LegacyHdHomerunChannelCommands(string url) public LegacyHdHomerunChannelCommands(string url)
{ {
// parse url for channel and program // parse url for channel and program
var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)"); var match = ChannelAndProgramRegex().Match(url);
if (match.Success) if (match.Success)
{ {
_channel = match.Groups[1].Value; _channel = match.Groups[1].Value;
@ -21,6 +21,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
} }
} }
[GeneratedRegex(@"\/ch([0-9]+)-?([0-9]*)")]
private static partial Regex ChannelAndProgramRegex();
public IEnumerable<(string CommandName, string CommandValue)> GetCommands() public IEnumerable<(string CommandName, string CommandValue)> GetCommands()
{ {
if (!string.IsNullOrEmpty(_channel)) if (!string.IsNullOrEmpty(_channel))

@ -5,7 +5,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -54,9 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IServerApplicationHost appHost, IServerApplicationHost appHost,
INetworkManager networkManager, INetworkManager networkManager,
IStreamHelper streamHelper, IStreamHelper streamHelper)
IMemoryCache memoryCache) : base(config, logger, fileSystem)
: base(config, logger, fileSystem, memoryCache)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_appHost = appHost; _appHost = appHost;

@ -20,7 +20,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
public class M3uParser public partial class M3uParser
{ {
private const string ExtInfPrefix = "#EXTINF:"; private const string ExtInfPrefix = "#EXTINF:";
@ -33,6 +33,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
} }
[GeneratedRegex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex KeyValueRegex();
public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken) public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
{ {
// Read the file and display it line by line. // Read the file and display it line by line.
@ -91,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{ {
var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine); var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
if (string.IsNullOrWhiteSpace(channel.Id)) channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
{
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
else
{
channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
channel.Path = trimmedLine; channel.Path = trimmedLine;
channels.Add(channel); channels.Add(channel);
@ -311,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase); var matches = KeyValueRegex().Matches(line);
remaining = line; remaining = line;
@ -320,7 +316,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var key = match.Groups[1].Value; var key = match.Groups[1].Value;
var value = match.Groups[2].Value; var value = match.Groups[2].Value;
dict[match.Groups[1].Value] = match.Groups[2].Value; dict[key] = value;
remaining = remaining.Replace(key + "=\"" + value + "\"", string.Empty, StringComparison.OrdinalIgnoreCase); remaining = remaining.Replace(key + "=\"" + value + "\"", string.Empty, StringComparison.OrdinalIgnoreCase);
} }

@ -1 +1,43 @@
{} {
"Albums": "এলবাম",
"Application": "আবেদন",
"AppDeviceValues": "এপ্‌: {0}, ডিভাইচ: {1}",
"Artists": "শিল্পী",
"Channels": "চেনেলস",
"Default": "ডিফল্ট",
"AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত",
"Books": "পুস্তক",
"Movies": "চলচ্চিত্ৰ",
"CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}",
"Collections": "সংগ্রহ",
"HeaderFavoriteShows": "প্রিয় শোসমূহ",
"Latest": "শেহতীয়া",
"MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে",
"MixedContent": "মিশ্ৰিত সমগ্ৰতা",
"NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.",
"NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল",
"External": "বাহ্যিক",
"Favorites": "পছন্দসই",
"Folders": "ফোল্ডাৰ",
"Forced": "বলপূর্বক",
"Genres": "শ্রেণী",
"HeaderAlbumArtists": "অ্যালবাম শিল্পী",
"HeaderContinueWatching": "দেখা চালিয়ে যান",
"FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}",
"HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ",
"HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ",
"HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ",
"HeaderFavoriteSongs": "প্ৰিয় গীত",
"HeaderLiveTV": "প্ৰতিবেদন টিভি",
"HeaderNextUp": "পৰৱৰ্তী অংশ",
"HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ",
"HearingImpaired": "শ্ৰবণ অক্ষম",
"HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ",
"Inherit": "উত্তপ্ত কৰা",
"MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে",
"NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ",
"NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল",
"NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল",
"NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল",
"NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা"
}

@ -0,0 +1,52 @@
{
"ChapterNameValue": "Didanedi {0}",
"HeaderAlbumArtists": "Didanidanolisgisgi",
"HeaderFavoriteAlbums": "Dvganidi didanidisgisgi",
"HeaderLiveTV": "Anigadi didanidisgosgi",
"HeaderRecordingGroups": "Didanisquodiisgisgi",
"HomeVideos": "Diganadi dinagadisgisgi",
"Inherit": "Anigwe",
"MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}",
"MixedContent": "Ganinidi dininoladisgisgi",
"Movies": "Anidvnisgisgi",
"MusicVideos": "Danodisgisgi didanidisgosgi",
"NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi",
"NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi",
"NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi",
"Albums": "Anigawidaniyv",
"Application": "Didanvyi",
"Artists": "Dinidaniyi",
"AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani",
"Books": "Didanedi",
"CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}",
"Channels": "Diganadasgi",
"Collections": "Diganadisgi",
"Default": "Dinadi",
"DeviceOfflineWithName": "{0} Aniyvolehvi nasgi",
"External": "Amohdi",
"Favorites": "Nvdayelvdisgi",
"Folders": "Didanididisgi",
"Forced": "Ganedi",
"Genres": "Diganadisgi",
"HeaderContinueWatching": "Uwoditsu asdanidisgisgi",
"HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi",
"HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi",
"HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)",
"HeaderFavoriteSongs": "Dvganidi danodisgisgi",
"HeaderNextUp": "Anidvli uwodoli",
"HearingImpaired": "Anitsunidi talunidisgisgi",
"ItemAddedWithName": "{0} Dinigwe anididanidisgi",
"Latest": "Uwodoli",
"MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe",
"MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi",
"Music": "Danodisgisgi",
"NameSeasonUnknown": "Tsunita anidvdisgi",
"NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.",
"NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi",
"NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi",
"NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi",
"NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi",
"NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi",
"NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi",
"NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi"
}

@ -22,7 +22,7 @@
"HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteShows": "Oblíbené seriály",
"HeaderFavoriteSongs": "Oblíbená hudba", "HeaderFavoriteSongs": "Oblíbená hudba",
"HeaderLiveTV": "Televize", "HeaderLiveTV": "Živý přenos",
"HeaderNextUp": "Další díly", "HeaderNextUp": "Další díly",
"HeaderRecordingGroups": "Skupiny nahrávek", "HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domácí videa", "HomeVideos": "Domácí videa",

@ -15,13 +15,13 @@
"Favorites": "Favoritter", "Favorites": "Favoritter",
"Folders": "Mapper", "Folders": "Mapper",
"Genres": "Genrer", "Genres": "Genrer",
"HeaderAlbumArtists": "Albums kunstnere", "HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning", "HeaderContinueWatching": "Fortsæt afspilning",
"HeaderFavoriteAlbums": "Favorit albummer", "HeaderFavoriteAlbums": "Favoritalbummer",
"HeaderFavoriteArtists": "Favorit kunstnere", "HeaderFavoriteArtists": "Favoritkunstnere",
"HeaderFavoriteEpisodes": "Favorit afsnit", "HeaderFavoriteEpisodes": "Yndlingsafsnit",
"HeaderFavoriteShows": "Favorit serier", "HeaderFavoriteShows": "Yndlingsserier",
"HeaderFavoriteSongs": "Favorit sange", "HeaderFavoriteSongs": "Yndlingssange",
"HeaderLiveTV": "Live-TV", "HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste", "HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper", "HeaderRecordingGroups": "Optagelsesgrupper",
@ -34,8 +34,8 @@
"Latest": "Seneste", "Latest": "Seneste",
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret", "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}", "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret", "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
"MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret", "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
"MixedContent": "Blandet indhold", "MixedContent": "Blandet indhold",
"Movies": "Film", "Movies": "Film",
"Music": "Musik", "Music": "Musik",
@ -51,7 +51,7 @@
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet", "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
"NotificationOptionInstallationFailed": "Installationen mislykkedes", "NotificationOptionInstallationFailed": "Installationen mislykkedes",
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet", "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
"NotificationOptionPluginError": "Plugin fejl", "NotificationOptionPluginError": "Plugin-fejl",
"NotificationOptionPluginInstalled": "Plugin blev installeret", "NotificationOptionPluginInstalled": "Plugin blev installeret",
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret", "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret", "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
@ -92,26 +92,26 @@
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek", "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}", "ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}", "VersionNumber": "Version {0}",
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.", "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster", "TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.", "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins", "TaskUpdatePlugins": "Opdater Plugins",
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.", "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
"TaskCleanLogs": "Ryd Log mappe", "TaskCleanLogs": "Ryd Log-mappe",
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.", "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
"TaskRefreshLibrary": "Scan Medie Bibliotek", "TaskRefreshLibrary": "Scan Mediebibliotek",
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.", "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
"TaskCleanCache": "Ryd Cache mappe", "TaskCleanCache": "Ryd Cache-mappe",
"TasksChannelsCategory": "Internet Kanaler", "TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation", "TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek", "TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedligeholdelse", "TasksMaintenanceCategory": "Vedligeholdelse",
"TaskRefreshChapterImages": "Udtræk kapitel billeder", "TaskRefreshChapterImages": "Udtræk kapitelbilleder",
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.", "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
"TaskRefreshChannelsDescription": "Opdater internet kanal information.", "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
"TaskRefreshChannels": "Opdater Kanaler", "TaskRefreshChannels": "Opdater Kanaler",
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.", "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
"TaskCleanTranscode": "Tøm Transcode mappen", "TaskCleanTranscode": "Tøm Transcode-mappen",
"TaskRefreshPeople": "Opdater Personer", "TaskRefreshPeople": "Opdater Personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.", "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.", "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
@ -121,8 +121,8 @@
"Default": "Standard", "Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.", "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
"TaskOptimizeDatabase": "Optimér database", "TaskOptimizeDatabase": "Optimér database",
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.", "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
"TaskKeyframeExtractor": "Nøglebillede udtræk", "TaskKeyframeExtractor": "Udtræk af nøglebillede",
"External": "Ekstern", "External": "Ekstern",
"HearingImpaired": "Hørehæmmet" "HearingImpaired": "Hørehæmmet"
} }

@ -3,9 +3,9 @@
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}", "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación", "Application": "Aplicación",
"Artists": "Artistas", "Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} identificado correctamente", "AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros", "Books": "Libros",
"CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}", "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
"Channels": "Canales", "Channels": "Canales",
"ChapterNameValue": "Capítulo {0}", "ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones", "Collections": "Colecciones",

@ -74,16 +74,16 @@
"Shows": "Sarjat", "Shows": "Sarjat",
"ServerNameNeedsToBeRestarted": "\"{0}\" on käynnistettävä uudelleen", "ServerNameNeedsToBeRestarted": "\"{0}\" on käynnistettävä uudelleen",
"ProviderValue": "Lähde: {0}", "ProviderValue": "Lähde: {0}",
"Plugin": "Laajennus", "Plugin": "Lisäosa",
"NotificationOptionVideoPlaybackStopped": "Videon toisto lopetettu", "NotificationOptionVideoPlaybackStopped": "Videon toisto lopetettu",
"NotificationOptionVideoPlayback": "Videon toisto aloitettu", "NotificationOptionVideoPlayback": "Videon toisto aloitettu",
"NotificationOptionUserLockedOut": "Käyttäjä on lukittu", "NotificationOptionUserLockedOut": "Käyttäjä on lukittu",
"NotificationOptionTaskFailed": "Ajoitettu tehtävä epäonnistui", "NotificationOptionTaskFailed": "Ajoitettu tehtävä epäonnistui",
"NotificationOptionServerRestartRequired": "Tarvitaan palvelimen uudelleenkäynnistys", "NotificationOptionServerRestartRequired": "Tarvitaan palvelimen uudelleenkäynnistys",
"NotificationOptionPluginUpdateInstalled": "Laajennus on päivitetty", "NotificationOptionPluginUpdateInstalled": "Lisäosa päivitettiin",
"NotificationOptionPluginUninstalled": "Laajennus on poistettu", "NotificationOptionPluginUninstalled": "Lisäosa poistettiin",
"NotificationOptionPluginInstalled": "Laajennus on asennettu", "NotificationOptionPluginInstalled": "Lisäosa asennettiin",
"NotificationOptionPluginError": "Laajennuksen virhe", "NotificationOptionPluginError": "Lisäosan virhe",
"NotificationOptionNewLibraryContent": "Sisältöä on lisätty", "NotificationOptionNewLibraryContent": "Sisältöä on lisätty",
"NotificationOptionInstallationFailed": "Asennus epäonnistui", "NotificationOptionInstallationFailed": "Asennus epäonnistui",
"NotificationOptionCameraImageUploaded": "Kameran kuva on tallennettu", "NotificationOptionCameraImageUploaded": "Kameran kuva on tallennettu",
@ -98,8 +98,8 @@
"TaskRefreshChannels": "Päivitä kanavat", "TaskRefreshChannels": "Päivitä kanavat",
"TaskCleanTranscodeDescription": "Poistaa päivää vanhemmat transkoodaustiedostot.", "TaskCleanTranscodeDescription": "Poistaa päivää vanhemmat transkoodaustiedostot.",
"TaskCleanTranscode": "Puhdista transkoodauskansio", "TaskCleanTranscode": "Puhdista transkoodauskansio",
"TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset laajennuksille, jotka on määritetty päivittymään automaattisesti.", "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset lisäosille, jotka on määritetty päivittymään automaattisesti.",
"TaskUpdatePlugins": "Päivitä laajennukset", "TaskUpdatePlugins": "Päivitä lisäosat",
"TaskRefreshPeopleDescription": "Päivittää mediakirjaston näyttelijöiden ja ohjaajien metatiedot.", "TaskRefreshPeopleDescription": "Päivittää mediakirjaston näyttelijöiden ja ohjaajien metatiedot.",
"TaskRefreshPeople": "Päivitä henkilöt", "TaskRefreshPeople": "Päivitä henkilöt",
"TaskCleanLogsDescription": "Poistaa {0} päivää vanhemmat lokitiedostot.", "TaskCleanLogsDescription": "Poistaa {0} päivää vanhemmat lokitiedostot.",

@ -0,0 +1,18 @@
{
"Artists": "Listafólk",
"Collections": "Søvn",
"Default": "Sjálvgildi",
"DeviceOfflineWithName": "{0} hevur slitið sambandið",
"External": "Ytri",
"Genres": "Greinar",
"Albums": "Album",
"AppDeviceValues": "App: {0}, Eind: {1}",
"Application": "Nýtsluskipan",
"Books": "Bøkur",
"Channels": "Rásir",
"ChapterNameValue": "Kapittul {0}",
"DeviceOnlineWithName": "{0} er sambundið",
"Favorites": "Yndis",
"Folders": "Mappur",
"Forced": "Kravt"
}

@ -105,8 +105,8 @@
"TaskRefreshPeople": "Actualiser les acteurs", "TaskRefreshPeople": "Actualiser les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux", "TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.", "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
"TaskRefreshLibrary": "Scanner la médiathèque", "TaskRefreshLibrary": "Analyser la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.", "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",

@ -5,18 +5,18 @@
"Artists": "אומנים", "Artists": "אומנים",
"AuthenticationSucceededWithUserName": "{0} אומת בהצלחה", "AuthenticationSucceededWithUserName": "{0} אומת בהצלחה",
"Books": "ספרים", "Books": "ספרים",
"CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מ {0}", "CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מתוך {0}",
"Channels": "ערוצים", "Channels": "ערוצים",
"ChapterNameValue": "פרק {0}", "ChapterNameValue": "פרק {0}",
"Collections": "אוספים", "Collections": "אוספים",
"DeviceOfflineWithName": "{0} התנתק", "DeviceOfflineWithName": "{0} התנתק",
"DeviceOnlineWithName": "{0} מחובר", "DeviceOnlineWithName": "{0} מחובר",
"FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי מ{0}", "FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי דרך {0}",
"Favorites": "מועדפים", "Favorites": "מועדפים",
"Folders": "תיקיות", "Folders": "תיקיות",
"Genres": 'אנרים", "Genres": ׳אנרים",
"HeaderAlbumArtists": "אמני האלבום", "HeaderAlbumArtists": "אמני האלבום",
"HeaderContinueWatching": "המשך לצפות", "HeaderContinueWatching": "להמשיך לצפות",
"HeaderFavoriteAlbums": "אלבומים מועדפים", "HeaderFavoriteAlbums": "אלבומים מועדפים",
"HeaderFavoriteArtists": "אמנים מועדפים", "HeaderFavoriteArtists": "אמנים מועדפים",
"HeaderFavoriteEpisodes": "פרקים מועדפים", "HeaderFavoriteEpisodes": "פרקים מועדפים",
@ -27,14 +27,14 @@
"HeaderRecordingGroups": "קבוצות הקלטה", "HeaderRecordingGroups": "קבוצות הקלטה",
"HomeVideos": "סרטונים בייתים", "HomeVideos": "סרטונים בייתים",
"Inherit": "הורש", "Inherit": "הורש",
"ItemAddedWithName": "{0} הוסף לספרייה", "ItemAddedWithName": "{0} נוסף לספרייה",
"ItemRemovedWithName": "{0} נמחק מהספרייה", "ItemRemovedWithName": "{0} נמחק מהספרייה",
"LabelIpAddressValue": "Ip כתובת: {0}", "LabelIpAddressValue": "Ip כתובת: {0}",
"LabelRunningTimeValue": "משך צפייה: {0}", "LabelRunningTimeValue": "משך צפייה: {0}",
"Latest": "אחרון", "Latest": "אחרון",
"MessageApplicationUpdated": "שרת הJellyfin עודכן", "MessageApplicationUpdated": "שרת הJellyfin עודכן",
"MessageApplicationUpdatedTo": "שרת הJellyfin עודכן לגרסא {0}", "MessageApplicationUpdatedTo": "שרת ה־Jellyfin עודכן לגרסה {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "הגדרת השרת {0} שונתה", "MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
"MessageServerConfigurationUpdated": "תצורת השרת עודכנה", "MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
"MixedContent": "תוכן מעורב", "MixedContent": "תוכן מעורב",
"Movies": "סרטים", "Movies": "סרטים",
@ -50,7 +50,7 @@
"NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק", "NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
"NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה", "NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
"NotificationOptionInstallationFailed": "התקנה נכשלה", "NotificationOptionInstallationFailed": "התקנה נכשלה",
"NotificationOptionNewLibraryContent": "תוכן חדש הוסף", "NotificationOptionNewLibraryContent": "תוכן חדש נוסף",
"NotificationOptionPluginError": "כשלון בתוסף", "NotificationOptionPluginError": "כשלון בתוסף",
"NotificationOptionPluginInstalled": "התוסף הותקן", "NotificationOptionPluginInstalled": "התוסף הותקן",
"NotificationOptionPluginUninstalled": "התוסף הוסר", "NotificationOptionPluginUninstalled": "התוסף הוסר",
@ -61,41 +61,41 @@
"NotificationOptionVideoPlayback": "ניגון וידאו החל", "NotificationOptionVideoPlayback": "ניגון וידאו החל",
"NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק", "NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
"Photos": "תמונות", "Photos": "תמונות",
"Playlists": "רשימות הפעלה", "Playlists": "רשימות נגינה",
"Plugin": "Plugin", "Plugin": "תוסף",
"PluginInstalledWithName": "{0} הותקן", "PluginInstalledWithName": "{0} הותקן",
"PluginUninstalledWithName": "{0} הוסר", "PluginUninstalledWithName": "{0} הוסר",
"PluginUpdatedWithName": "{0} עודכן", "PluginUpdatedWithName": "{0} עודכן",
"ProviderValue": "Provider: {0}", "ProviderValue": "ספק: {0}",
"ScheduledTaskFailedWithName": "{0} נכשל", "ScheduledTaskFailedWithName": "{0} נכשל",
"ScheduledTaskStartedWithName": "{0} החל", "ScheduledTaskStartedWithName": "{0} החל",
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש", "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
"Shows": "סדרות", "Shows": "סדרות",
"Songs": "שירים", "Songs": "שירים",
"StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.", "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. נא לנסות שנית בהקדם.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}", "SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
"Sync": "סנכרן", "Sync": "סנכרון",
"System": "System", "System": "מערכת",
"TvShows": "סדרות טלוויזיה", "TvShows": "סדרות טלוויזיה",
"User": "User", "User": "משתמש",
"UserCreatedWithName": "המשתמש {0} נוצר", "UserCreatedWithName": "המשתמש {0} נוצר",
"UserDeletedWithName": "המשתמש {0} הוסר", "UserDeletedWithName": "המשתמש {0} הוסר",
"UserDownloadingItemWithValues": "{0} מוריד את {1}", "UserDownloadingItemWithValues": "{0} מוריד את {1}",
"UserLockedOutWithName": "המשתמש {0} ננעל", "UserLockedOutWithName": "המשתמש {0} ננעל",
"UserOfflineFromDevice": "{0} התנתק מ-{1}", "UserOfflineFromDevice": "{0} התנתק מ־{1}",
"UserOnlineFromDevice": "{0} מחובר מ-{1}", "UserOnlineFromDevice": "{0} מחובר מ־{1}",
"UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}", "UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
"UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה", "UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
"UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}", "UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}", "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
"ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך", "ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
"ValueSpecialEpisodeName": "מיוחד- {0}", "ValueSpecialEpisodeName": "מיוחד- {0}",
"VersionNumber": "Version {0}", "VersionNumber": "גרסה {0}",
"TaskRefreshLibrary": "סרוק ספריית מדיה", "TaskRefreshLibrary": "סרוק ספריית מדיה",
"TaskRefreshChapterImages": "חלץ תמונות פרקים", "TaskRefreshChapterImages": "חלץ תמונות פרקים",
"TaskCleanCacheDescription": "מחק קבצי מטמון שלא בשימוש המערכת.", "TaskCleanCacheDescription": "מחק קבצי מטמון שלא בשימוש המערכת.",
"TaskCleanCache": קה תיקיית מטמון", "TaskCleanCache": יקוי תיקיית מטמון",
"TasksApplicationCategory": "יישום", "TasksApplicationCategory": "יישום",
"TasksLibraryCategory": "ספרייה", "TasksLibraryCategory": "ספרייה",
"TasksMaintenanceCategory": "תחזוקה", "TasksMaintenanceCategory": "תחזוקה",
@ -103,7 +103,7 @@
"TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.", "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
"TaskRefreshPeople": "רענן אנשים", "TaskRefreshPeople": "רענן אנשים",
"TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.", "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
"TaskCleanLogs": קה תיקיית יומן", "TaskCleanLogs": יקוי תיקיית יומן",
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.", "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.", "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
"TasksChannelsCategory": "ערוצי אינטרנט", "TasksChannelsCategory": "ערוצי אינטרנט",

@ -1,11 +1,11 @@
{ {
"Albums": "Albumok", "Albums": "Albumok",
"AppDeviceValues": "Program: {0}, Eszköz: {1}", "AppDeviceValues": "Program: {0}, eszköz: {1}",
"Application": "Alkalmazás", "Application": "Alkalmazás",
"Artists": "Előadók", "Artists": "Előadók",
"AuthenticationSucceededWithUserName": "{0} sikeresen azonosítva", "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
"Books": "Könyvek", "Books": "Könyvek",
"CameraImageUploadedFrom": "Új kamerakép került feltöltésre innen: {0}", "CameraImageUploadedFrom": "Új kamerakép feltöltve innen: {0}",
"Channels": "Csatornák", "Channels": "Csatornák",
"ChapterNameValue": "{0}. jelenet", "ChapterNameValue": "{0}. jelenet",
"Collections": "Gyűjtemények", "Collections": "Gyűjtemények",
@ -15,13 +15,13 @@
"Favorites": "Kedvencek", "Favorites": "Kedvencek",
"Folders": "Könyvtárak", "Folders": "Könyvtárak",
"Genres": "Műfajok", "Genres": "Műfajok",
"HeaderAlbumArtists": "Album előadó(k)", "HeaderAlbumArtists": "Albumelőadók",
"HeaderContinueWatching": "Megtekintés folytatása", "HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok", "HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteArtists": "Kedvenc előadók", "HeaderFavoriteArtists": "Kedvenc előadók",
"HeaderFavoriteEpisodes": "Kedvenc epizódok", "HeaderFavoriteEpisodes": "Kedvenc epizódok",
"HeaderFavoriteShows": "Kedvenc sorozatok", "HeaderFavoriteShows": "Kedvenc sorozatok",
"HeaderFavoriteSongs": "Kedvenc dalok", "HeaderFavoriteSongs": "Kedvenc számok",
"HeaderLiveTV": "Élő TV", "HeaderLiveTV": "Élő TV",
"HeaderNextUp": "Következik", "HeaderNextUp": "Következik",
"HeaderRecordingGroups": "Felvételi csoportok", "HeaderRecordingGroups": "Felvételi csoportok",
@ -29,37 +29,37 @@
"Inherit": "Örökölt", "Inherit": "Örökölt",
"ItemAddedWithName": "{0} hozzáadva a könyvtárhoz", "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
"ItemRemovedWithName": "{0} eltávolítva a könyvtárból", "ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
"LabelIpAddressValue": "IP cím: {0}", "LabelIpAddressValue": "IP-cím: {0}",
"LabelRunningTimeValue": "Futási idő: {0}", "LabelRunningTimeValue": "Lejátszási idő: {0}",
"Latest": "Legújabb", "Latest": "Legújabb",
"MessageApplicationUpdated": "Jellyfin Szerver frissítve", "MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve",
"MessageApplicationUpdatedTo": "Jellyfin Szerver frissítve lett a következőre: {0}", "MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigurációs rész frissítve: {0}", "MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve: {0}",
"MessageServerConfigurationUpdated": "Szerver konfiguráció frissítve", "MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve",
"MixedContent": "Vegyes tartalom", "MixedContent": "Vegyes tartalom",
"Movies": "Filmek", "Movies": "Filmek",
"Music": "Zene", "Music": "Zenék",
"MusicVideos": "Zenei videóklippek", "MusicVideos": "Zenei videóklippek",
"NameInstallFailed": "{0} sikertelen telepítés", "NameInstallFailed": "{0} sikertelen telepítés",
"NameSeasonNumber": "{0}. évad", "NameSeasonNumber": "{0}. évad",
"NameSeasonUnknown": "Ismeretlen évad", "NameSeasonUnknown": "Ismeretlen évad",
"NewVersionIsAvailable": "Letölthető a Jellyfin Szerver új verziója.", "NewVersionIsAvailable": "Letölthető a Jellyfin kiszolgáló új verziója.",
"NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz", "NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz",
"NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve", "NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve",
"NotificationOptionAudioPlayback": "Audió lejátszás elkezdve", "NotificationOptionAudioPlayback": "Hanglejátszás elkezdve",
"NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva", "NotificationOptionAudioPlaybackStopped": "Hanglejátszás leállítva",
"NotificationOptionCameraImageUploaded": "Kamera kép feltöltve", "NotificationOptionCameraImageUploaded": "Kamerakép feltöltve",
"NotificationOptionInstallationFailed": "Telepítés sikertelen", "NotificationOptionInstallationFailed": "Telepítési hiba",
"NotificationOptionNewLibraryContent": "Új tartalom hozzáadva", "NotificationOptionNewLibraryContent": "Új tartalom hozzáadva",
"NotificationOptionPluginError": "Bővítmény hiba", "NotificationOptionPluginError": "Bővítményhiba",
"NotificationOptionPluginInstalled": "Bővítmény telepítve", "NotificationOptionPluginInstalled": "Bővítmény telepítve",
"NotificationOptionPluginUninstalled": "Bővítmény eltávolítva", "NotificationOptionPluginUninstalled": "Bővítmény eltávolítva",
"NotificationOptionPluginUpdateInstalled": "Bővítmény frissítés telepítve", "NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve",
"NotificationOptionServerRestartRequired": "Szerver újraindítás szükséges", "NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges",
"NotificationOptionTaskFailed": "Ütemezett feladat hiba", "NotificationOptionTaskFailed": "Ütemezett feladat hiba",
"NotificationOptionUserLockedOut": "Felhasználó tiltva", "NotificationOptionUserLockedOut": "Felhasználó tiltva",
"NotificationOptionVideoPlayback": "Videó lejátszás elkezdve", "NotificationOptionVideoPlayback": "Videólejátszás elkezdve",
"NotificationOptionVideoPlaybackStopped": "Videó lejátszás leállítva", "NotificationOptionVideoPlaybackStopped": "Videólejátszás leállítva",
"Photos": "Fényképek", "Photos": "Fényképek",
"Playlists": "Lejátszási listák", "Playlists": "Lejátszási listák",
"Plugin": "Bővítmény", "Plugin": "Bővítmény",
@ -69,47 +69,47 @@
"ProviderValue": "Szolgáltató: {0}", "ProviderValue": "Szolgáltató: {0}",
"ScheduledTaskFailedWithName": "{0} sikertelen", "ScheduledTaskFailedWithName": "{0} sikertelen",
"ScheduledTaskStartedWithName": "{0} elkezdve", "ScheduledTaskStartedWithName": "{0} elkezdve",
"ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani", "ServerNameNeedsToBeRestarted": "A(z) {0} újraindítása szükséges",
"Shows": "Sorozatok", "Shows": "Sorozatok",
"Songs": "Dalok", "Songs": "Számok",
"StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.", "StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}", "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}",
"Sync": "Szinkronizál", "Sync": "Szinkronizálás",
"System": "Rendszer", "System": "Rendszer",
"TvShows": "TV műsorok", "TvShows": "TV műsorok",
"User": "Felhasználó", "User": "Felhasználó",
"UserCreatedWithName": "{0} felhasználó létrehozva", "UserCreatedWithName": "{0} felhasználó létrehozva",
"UserDeletedWithName": "{0} felhasználó törölve", "UserDeletedWithName": "{0} felhasználó törölve",
"UserDownloadingItemWithValues": "{0} letölti {1}", "UserDownloadingItemWithValues": "{0} letölti: {1}",
"UserLockedOutWithName": "{0} felhasználó zárolva van", "UserLockedOutWithName": "{0} felhasználó zárolva van",
"UserOfflineFromDevice": "{0} kijelentkezett innen: {1}", "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}",
"UserOnlineFromDevice": "{0} online innen: {1}", "UserOnlineFromDevice": "{0} online innen: {1}",
"UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}", "UserPasswordChangedWithName": "{0} jelszava megváltozott",
"UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}", "UserPolicyUpdatedWithName": "{0} felhasználói házirendje frissült",
"UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}", "UserStartedPlayingItemWithValues": "{0} elkezdte lejátszani a következőt: {1}, itt: {2}",
"UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}", "UserStoppedPlayingItemWithValues": "{0} befejezte a következő lejátszását: {1}, itt: {2}",
"ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz", "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz",
"ValueSpecialEpisodeName": "Special - {0}", "ValueSpecialEpisodeName": "Különkiadás {0}",
"VersionNumber": "Verzió: {0}", "VersionNumber": "Verzió: {0}",
"TaskCleanTranscode": "Átkódolási könyvtár ürítése", "TaskCleanTranscode": "Átkódolási könyvtár ürítése",
"TaskUpdatePluginsDescription": "Letölti és telepíti a frissítéseket azokhoz a bővítményekhez, amelyeknél az automatikus frissítés engedélyezve van.", "TaskUpdatePluginsDescription": "Letölti és telepíti a frissítéseket azokhoz a bővítményekhez, amelyeknél az automatikus frissítés engedélyezve van.",
"TaskUpdatePlugins": "Bővítmények frissítése", "TaskUpdatePlugins": "Bővítmények frissítése",
"TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a könyvtáradban.", "TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a médiatárban.",
"TaskRefreshPeople": "Személyek frissítése", "TaskRefreshPeople": "Személyek frissítése",
"TaskCleanLogsDescription": "Törli azokat a naplófájlokat, amelyek {0} napnál régebbiek.", "TaskCleanLogsDescription": "Törli azokat a naplófájlokat, amelyek {0} napnál régebbiek.",
"TaskCleanLogs": "Naplózási könyvtár ürítése", "TaskCleanLogs": "Naplózási könyvtár ürítése",
"TaskRefreshLibraryDescription": "Átvizsgálja a könyvtáraidat új fájlokért és frissíti a metaadatokat.", "TaskRefreshLibraryDescription": "Átvizsgálja a médiatárat új fájlokat keresve, és frissíti a metaadatokat.",
"TaskRefreshLibrary": "Média könyvtár beolvasása", "TaskRefreshLibrary": "Médiatár átvizsgálása",
"TaskRefreshChapterImagesDescription": "Miniatűröket generál olyan videókhoz, amely tartalmaz fejezeteket.", "TaskRefreshChapterImagesDescription": "Miniatűröket hoz létre az olyan videókhoz, amely tartalmaz fejezeteket.",
"TaskRefreshChapterImages": "Fejezetek képeinek generálása", "TaskRefreshChapterImages": "Fejezetképek kinyerése",
"TaskCleanCacheDescription": "Törli azokat a gyorsítótárazott fájlokat, amikre a rendszernek már nincs szüksége.", "TaskCleanCacheDescription": "Törli azokat a gyorsítótárazott fájlokat, amikre a rendszernek már nincs szüksége.",
"TaskCleanCache": "Gyorsítótár könyvtárának ürítése", "TaskCleanCache": "Gyorsítótár könyvtárának ürítése",
"TasksChannelsCategory": "Internetes csatornák", "TasksChannelsCategory": "Internetes csatornák",
"TasksApplicationCategory": "Alkalmazás", "TasksApplicationCategory": "Alkalmazás",
"TasksLibraryCategory": "Könyvtár", "TasksLibraryCategory": "Könyvtár",
"TasksMaintenanceCategory": "Karbantartás", "TasksMaintenanceCategory": "Karbantartás",
"TaskDownloadMissingSubtitlesDescription": "A metaadat konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.", "TaskDownloadMissingSubtitlesDescription": "A metaadat-konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.",
"TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése", "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
"TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.", "TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.",
"TaskRefreshChannels": "Csatornák frissítése", "TaskRefreshChannels": "Csatornák frissítése",
@ -121,8 +121,8 @@
"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.", "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", "TaskOptimizeDatabase": "Adatbázis optimalizálása",
"TaskKeyframeExtractor": "Kulcskockák kibontása", "TaskKeyframeExtractor": "Kulcsképkockák kibontása",
"TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.", "TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
"External": "Külső", "External": "Külső",
"HearingImpaired": "Hallássérült" "HearingImpaired": "Hallássérült"
} }

@ -13,8 +13,8 @@
"HeaderFavoriteArtists": "Uppáhalds Listamenn", "HeaderFavoriteArtists": "Uppáhalds Listamenn",
"HeaderFavoriteAlbums": "Uppáhalds Plötur", "HeaderFavoriteAlbums": "Uppáhalds Plötur",
"HeaderContinueWatching": "Halda áfram að horfa", "HeaderContinueWatching": "Halda áfram að horfa",
"HeaderAlbumArtists": "Höfundur plötu", "HeaderAlbumArtists": "Listamaður á umslagi",
"Genres": "Tegundir", "Genres": "Stefnur",
"Folders": "Möppur", "Folders": "Möppur",
"Favorites": "Uppáhalds", "Favorites": "Uppáhalds",
"FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig", "FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig",
@ -22,32 +22,32 @@
"DeviceOfflineWithName": "{0} hefur aftengst", "DeviceOfflineWithName": "{0} hefur aftengst",
"Collections": "Söfn", "Collections": "Söfn",
"ChapterNameValue": "Kafli {0}", "ChapterNameValue": "Kafli {0}",
"Channels": "Stöðvar", "Channels": "Rásir",
"CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}", "CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni",
"Books": "Bækur", "Books": "Bækur",
"AuthenticationSucceededWithUserName": "{0} auðkenning tókst", "AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst",
"Artists": "Listamaður", "Artists": "Listamenn",
"Application": "Forrit", "Application": "Forrit",
"AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}", "AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
"Albums": "Plötur", "Albums": "Plötur",
"Plugin": "Viðbót", "Plugin": "Viðbótarvirkni",
"Photos": "Myndir", "Photos": "Ljósmyndir",
"NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð", "NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð",
"NotificationOptionVideoPlayback": "Myndbandafspilun hafin", "NotificationOptionVideoPlayback": "Myndbandsafspilun hafin",
"NotificationOptionUserLockedOut": "Notandi læstur úti", "NotificationOptionUserLockedOut": "Notandi læstur úti",
"NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg", "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg",
"NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett", "NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið",
"NotificationOptionPluginUninstalled": "Viðbót fjarlægð", "NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð",
"NotificationOptionPluginInstalled": "Viðbót sett upp", "NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp",
"NotificationOptionPluginError": "Bilun í viðbót", "NotificationOptionPluginError": "Bilun í viðbót",
"NotificationOptionInstallationFailed": "Uppsetning tókst ekki", "NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
"NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp", "NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp",
"NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð", "NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð",
"NotificationOptionAudioPlayback": "Hljóðafspilun hafin", "NotificationOptionAudioPlayback": "Hljóðafspilun hafin",
"NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett", "NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett",
"NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði", "NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði",
"NameSeasonUnknown": "Sería óþekkt", "NameSeasonUnknown": "Þáttaröð óþekkt",
"NameSeasonNumber": "Sería {0}", "NameSeasonNumber": "Þáttaröð {0}",
"MixedContent": "Blandað efni", "MixedContent": "Blandað efni",
"MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar", "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
"MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}", "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
@ -57,24 +57,24 @@
"User": "Notandi", "User": "Notandi",
"System": "Kerfi", "System": "Kerfi",
"NotificationOptionNewLibraryContent": "Nýju efni bætt við", "NotificationOptionNewLibraryContent": "Nýju efni bætt við",
"NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.", "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.",
"NameInstallFailed": "{0} uppsetning mistókst", "NameInstallFailed": "{0} uppsetning mistókst",
"MusicVideos": "Tónlistarmyndbönd", "MusicVideos": "Tónlistarmyndbönd",
"Music": "Tónlist", "Music": "Tónlist",
"Movies": "Kvikmyndir", "Movies": "Kvikmyndir",
"UserDeletedWithName": "Notanda {0} hefur verið eytt", "UserDeletedWithName": "Notanda {0} hefur verið eytt",
"UserCreatedWithName": "Notandi {0} hefur verið stofnaður", "UserCreatedWithName": "Notandi {0} hefur verið stofnaður",
"TvShows": "Þættir", "TvShows": "Sjónvarpsþættir",
"Sync": "Samstilla", "Sync": "Samstilla",
"Songs": "Lög", "Songs": "Lög",
"ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa", "ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur",
"ScheduledTaskStartedWithName": "{0} hafin", "ScheduledTaskStartedWithName": "{0} hafin",
"ScheduledTaskFailedWithName": "{0} mistókst", "ScheduledTaskFailedWithName": "{0} mistókst",
"PluginUpdatedWithName": "{0} var uppfært", "PluginUpdatedWithName": "{0} var uppfært",
"PluginUninstalledWithName": "{0} var fjarlægt", "PluginUninstalledWithName": "{0} var fjarlægt",
"PluginInstalledWithName": "{0} var sett upp", "PluginInstalledWithName": "{0} var sett upp",
"NotificationOptionTaskFailed": "Tímasett verkefni mistókst", "NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
"StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.", "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.",
"VersionNumber": "Útgáfa {0}", "VersionNumber": "Útgáfa {0}",
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt", "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}", "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
@ -83,14 +83,14 @@
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt", "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}", "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}", "UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
"UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur", "UserLockedOutWithName": "Notandi {0} hefur verið læstur úti",
"UserDownloadingItemWithValues": "{0} Hleður niður {1}", "UserDownloadingItemWithValues": "{0} hleður niður {1}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}", "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
"ProviderValue": "Veitandi: {0}", "ProviderValue": "Efnisveita: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón", "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
"ValueSpecialEpisodeName": "Sérstakt - {0}", "ValueSpecialEpisodeName": "Sérstaktur - {0}",
"Shows": "Sýningar", "Shows": "Þættir",
"Playlists": "Spilunarlisti", "Playlists": "Efnisskrár",
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.", "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
"TaskRefreshChannels": "Endurhlaða Rásir", "TaskRefreshChannels": "Endurhlaða Rásir",
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.", "TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
@ -116,5 +116,12 @@
"TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.", "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
"TaskCleanLogs": "Hreinsa færslu skrá", "TaskCleanLogs": "Hreinsa færslu skrá",
"TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.", "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
"HearingImpaired": "Heyrnarskertur" "HearingImpaired": "Heyrnarskertur",
"TaskOptimizeDatabaseDescription": "Þjappar gagnagrunni og bætir við lausu diskaplássi. Að keyra þessa aðgerð eftir skönnun safnsins, eða eftir einhverjar breytingar sem fela í sér gagnagrunnsbreytingar, gætu aukið hraðvirkni.",
"TaskKeyframeExtractor": "Lykilrammaplokkari",
"TaskKeyframeExtractorDescription": "Plokkar lykilramma úr myndbandsskrám til að búa til nákvæmari HLS uppskiptingarlista. Þetta verk getur tekið langan tíma.",
"TaskRefreshChapterImages": "Plokka kafla-myndir",
"TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
"Forced": "Þvingað",
"External": "Útvær"
} }

@ -3,5 +3,125 @@
"TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ", "TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
"TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.", "TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
"TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್", "TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್",
"TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು." "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
"ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
"ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
"TasksLibraryCategory": "ಸಮೊಹ",
"TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
"TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
"TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ",
"TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್‌ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
"TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
"UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
"Albums": "ಸಂಪುಟ",
"Application": "ಅಪ್ಲಿಕೇಶನ್",
"AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
"Artists": "ಕಲಾವಿದರು",
"AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
"Books": "ಪುಸ್ತಕಗಳು",
"ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
"Collections": "ಸಂಗ್ರಹಣೆಗಳು",
"Default": "ಪೂರ್ವನಿಯೋಜಿತ",
"DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
"DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
"External": "ಹೊರಗಿನ",
"FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
"Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
"Folders": "ಫೋಲ್ಡರ್‌ಗಳು",
"Forced": "ಬಲವಂತವಾಗಿ",
"Genres": "ಪ್ರಕಾರಗಳು",
"HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
"HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
"HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
"HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
"HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
"HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
"HeaderNextUp": "ಮುಂದೆ",
"HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
"MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
"CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
"Channels": "ಮೂಲಗಳು",
"HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
"HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
"HearingImpaired": "ಮೂಗ",
"ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
"MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
"MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
"NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್‌ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್‌ಲೋಡ್‌ಗೆ ಲಭ್ಯವಿದೆ.",
"NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
"NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
"NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
"NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ",
"NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
"PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
"ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
"ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
"ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
"UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
"UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
"UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್‌ಲೈನ್‌ನಲ್ಲಿದೆ",
"UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
"UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
"UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
"UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
"VersionNumber": "ಆವೃತ್ತಿ {0}",
"TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ",
"TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್",
"TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
"TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ",
"TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್‌ನೇಲ್‌ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.",
"TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್‌ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
"TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
"TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್‌ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.",
"TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
"TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ",
"Shows": "ಧಾರವಾಹಿಗಳು",
"Songs": "ಹಾಡುಗಳು",
"StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
"UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
"UserDownloadingItemWithValues": "{0} ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
"SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
"Sync": "ಹೊಂದಿಕೆ",
"System": "ವ್ಯವಸ್ಥೆ",
"TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
"Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
"User": "ಬಳಕೆದಾರ",
"HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
"Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
"ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
"LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
"LabelRunningTimeValue": "ಅವಧಿ: {0}",
"Latest": "ಹೊಸದಾದ",
"MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
"MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
"Movies": "ಚಲನಚಿತ್ರಗಳು",
"Music": "ಸಂಗೀತ",
"MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು",
"NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ",
"NameSeasonNumber": "ಸೀಸನ್ {0}",
"NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ",
"NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ",
"NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
"NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
"NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
"NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
"NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
"NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
"NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
"NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
"NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
"NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
"Photos": "ಚಿತ್ರಗಳು",
"Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
"Plugin": "ಪ್ಲಗಿನ್",
"PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
"PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
"ProviderValue": "ಒದಗಿಸುವವರು: {0}",
"TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
"TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
"TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
"TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ",
"TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
"TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
} }

@ -1,7 +1,7 @@
{ {
"ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts", "ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
"NotificationOptionTaskFailed": "Plānota uzdevuma kļūme", "NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
"HeaderRecordingGroups": "Ierakstu Grupas", "HeaderRecordingGroups": "Ierakstu grupas",
"UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}", "UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
"SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās", "SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
"NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta", "NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
@ -14,7 +14,7 @@
"Photos": "Attēli", "Photos": "Attēli",
"NotificationOptionUserLockedOut": "Lietotājs bloķēts", "NotificationOptionUserLockedOut": "Lietotājs bloķēts",
"LabelRunningTimeValue": "Garums: {0}", "LabelRunningTimeValue": "Garums: {0}",
"Inherit": "Mantot", "Inherit": "Pārmantot",
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}", "AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
"VersionNumber": "Versija {0}", "VersionNumber": "Versija {0}",
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai", "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
@ -28,7 +28,7 @@
"UserDeletedWithName": "Lietotājs {0} ir izdzēsts", "UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
"UserCreatedWithName": "Lietotājs {0} ir ticis izveidots", "UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
"User": "Lietotājs", "User": "Lietotājs",
"TvShows": "TV Raidījumi", "TvShows": "TV raidījumi",
"Sync": "Sinhronizācija", "Sync": "Sinhronizācija",
"System": "Sistēma", "System": "Sistēma",
"StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.", "StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.",
@ -38,11 +38,11 @@
"PluginUninstalledWithName": "{0} tika noņemts", "PluginUninstalledWithName": "{0} tika noņemts",
"PluginInstalledWithName": "{0} tika uzstādīts", "PluginInstalledWithName": "{0} tika uzstādīts",
"Plugin": "Paplašinājums", "Plugin": "Paplašinājums",
"Playlists": "Atskaņošanas Saraksti", "Playlists": "Atskaņošanas saraksti",
"MixedContent": "Jaukts saturs", "MixedContent": "Jaukts saturs",
"HomeVideos": "Mājas Video", "HomeVideos": "Mājas video",
"HeaderNextUp": "Nākamais", "HeaderNextUp": "Nākamais",
"ChapterNameValue": "Nodaļa {0}", "ChapterNameValue": "{0}. nodaļa",
"Application": "Lietotne", "Application": "Lietotne",
"NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts", "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts",
"NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts", "NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
@ -56,14 +56,14 @@
"NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts", "NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts",
"NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams", "NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams",
"NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.", "NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.",
"NameSeasonUnknown": "Nezināma Sezona", "NameSeasonUnknown": "Nezināma sezona",
"NameSeasonNumber": "Sezona {0}", "NameSeasonNumber": "{0}. sezona",
"NameInstallFailed": "{0} instalācija neizdevās", "NameInstallFailed": "{0} instalācija neizdevās",
"MusicVideos": "Mūzikas video", "MusicVideos": "Mūzikas video",
"Music": "Mūzika", "Music": "Mūzika",
"Movies": "Filmas", "Movies": "Filmas",
"MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota", "MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota",
"MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota", "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} tika atjaunota",
"MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}", "MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}",
"MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots", "MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
"Latest": "Jaunākais", "Latest": "Jaunākais",
@ -71,57 +71,57 @@
"ItemRemovedWithName": "{0} tika noņemts no bibliotēkas", "ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
"ItemAddedWithName": "{0} tika pievienots bibliotēkai", "ItemAddedWithName": "{0} tika pievienots bibliotēkai",
"HeaderLiveTV": "Tiešraides TV", "HeaderLiveTV": "Tiešraides TV",
"HeaderContinueWatching": "Turpināt Skatīšanos", "HeaderContinueWatching": "Turpināt skatīšanos",
"HeaderAlbumArtists": "Albumu Izpildītāji", "HeaderAlbumArtists": "Albumu izpildītāji",
"Genres": "Žanri", "Genres": "Žanri",
"Folders": "Mapes", "Folders": "Mapes",
"Favorites": "Favorīti", "Favorites": "Izlase",
"FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}", "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
"DeviceOnlineWithName": "{0} ir pievienojies", "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
"DeviceOfflineWithName": "{0} ir atvienojies", "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
"Collections": "Kolekcijas", "Collections": "Kolekcijas",
"Channels": "Kanāli", "Channels": "Kanāli",
"CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}", "CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}",
"Books": "Grāmatas", "Books": "Grāmatas",
"Artists": "Izpildītāji", "Artists": "Izpildītāji",
"Albums": "Albumi", "Albums": "Albumi",
"ProviderValue": "Provider: {0}", "ProviderValue": "Provider: {0}",
"HeaderFavoriteSongs": "Dziesmu Favorīti", "HeaderFavoriteSongs": "Dziesmu izlase",
"HeaderFavoriteShows": "Raidījumu Favorīti", "HeaderFavoriteShows": "Raidījumu izlase",
"HeaderFavoriteEpisodes": "Episožu Favorīti", "HeaderFavoriteEpisodes": "Sēriju izlase",
"HeaderFavoriteArtists": "Izpildītāju Favorīti", "HeaderFavoriteArtists": "Izpildītāju izlase",
"HeaderFavoriteAlbums": "Albumu Favorīti", "HeaderFavoriteAlbums": "Albumu izlase",
"TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.", "TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.",
"TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus", "TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
"TasksApplicationCategory": "Lietotne", "TasksApplicationCategory": "Lietotne",
"TasksLibraryCategory": "Bibliotēka", "TasksLibraryCategory": "Bibliotēka",
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.", "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus", "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.", "TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
"TaskRefreshChannels": "Atjaunot Kanālus", "TaskRefreshChannels": "Atjaunot kanālus",
"TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.", "TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.",
"TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi", "TaskCleanTranscode": "Iztīrīt transkodēšanas mapi",
"TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.", "TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
"TaskUpdatePlugins": "Atjaunot Paplašinājumus", "TaskUpdatePlugins": "Atjaunot paplašinājumus",
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.", "TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
"TaskRefreshPeople": "Atjaunot Cilvēkus", "TaskRefreshPeople": "Atjaunot cilvēkus",
"TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.", "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
"TaskCleanLogs": "Iztīrīt Logdatņu Mapi", "TaskCleanLogs": "Iztīrīt logdatņu mapi",
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.", "TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
"TaskRefreshLibrary": "Skenēt Multivides Bibliotēku", "TaskRefreshLibrary": "Skenēt multivides bibliotēku",
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.", "TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
"TaskCleanCache": "Iztīrīt Kešošanas Mapi", "TaskCleanCache": "Iztīrīt kešatmiņas mapi",
"TasksChannelsCategory": "Interneta Kanāli", "TasksChannelsCategory": "Interneta kanāli",
"TasksMaintenanceCategory": "Apkope", "TasksMaintenanceCategory": "Apkope",
"Forced": "Piespiests", "Forced": "Piespiedu",
"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ējuma", "Default": "Noklusējuma",
"TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.", "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Šī uzdevuma palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
"TaskOptimizeDatabase": "Optimizēt datubāzi", "TaskOptimizeDatabase": "Optimizēt datubāzi",
"External": "Ārējais", "External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem", "HearingImpaired": "Ar dzirdes traucējumiem",
"TaskKeyframeExtractor": "Atslēgkadru Ekstraktors", "TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs." "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
} }

@ -121,5 +121,7 @@
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.", "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക", "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
"HearingImpaired": "കേൾവി തകരാറുകൾ", "HearingImpaired": "കേൾവി തകരാറുകൾ",
"External": "പുറമേയുള്ള" "External": "പുറമേയുള്ള",
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
} }

@ -1,5 +1,5 @@
{ {
"Albums": "Album-album", "Albums": "Album",
"AppDeviceValues": "Apl: {0}, Peranti: {1}", "AppDeviceValues": "Apl: {0}, Peranti: {1}",
"Application": "Aplikasi", "Application": "Aplikasi",
"Artists": "Artis-artis", "Artists": "Artis-artis",

@ -1,9 +1,9 @@
{ {
"Albums": "Albums", "Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}", "AppDeviceValues": "App: {0}, Apparaat: {1}",
"Application": "Toepassing", "Application": "Applicatie",
"Artists": "Artiesten", "Artists": "Artiesten",
"AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd", "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd",
"Books": "Boeken", "Books": "Boeken",
"CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}", "CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
"Channels": "Kanalen", "Channels": "Kanalen",

@ -24,5 +24,13 @@
"TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.", "TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.",
"HeaderAlbumArtists": "Buccaneers o' the musical arts", "HeaderAlbumArtists": "Buccaneers o' the musical arts",
"HeaderFavoriteAlbums": "Beloved booty o' musical adventures", "HeaderFavoriteAlbums": "Beloved booty o' musical adventures",
"HeaderFavoriteArtists": "Treasured scallywags o' the creative seas" "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas",
"Channels": "Channels",
"Forced": "Pressed",
"External": "Outboard",
"HeaderFavoriteEpisodes": "Treasured Tales",
"HeaderFavoriteShows": "Treasured Tales",
"ChapterNameValue": "Piece {0}",
"HeaderFavoriteSongs": "Treasured Chimes",
"HeaderNextUp": "Incoming"
} }

@ -31,13 +31,13 @@
"ItemRemovedWithName": "{0} - изъято из медиатеки", "ItemRemovedWithName": "{0} - изъято из медиатеки",
"LabelIpAddressValue": "IP-адрес: {0}", "LabelIpAddressValue": "IP-адрес: {0}",
"LabelRunningTimeValue": "Длительность: {0}", "LabelRunningTimeValue": "Длительность: {0}",
"Latest": "Новое", "Latest": "Последние добавленные",
"MessageApplicationUpdated": "Jellyfin Server был обновлён", "MessageApplicationUpdated": "Jellyfin Server был обновлён",
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}", "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена", "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
"MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена", "MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
"MixedContent": "Смешанное содержание", "MixedContent": "Смешанное содержание",
"Movies": "Кино", "Movies": "Фильмы",
"Music": "Музыка", "Music": "Музыка",
"MusicVideos": "Муз. видео", "MusicVideos": "Муз. видео",
"NameInstallFailed": "Установка {0} неудачна", "NameInstallFailed": "Установка {0} неудачна",
@ -77,7 +77,7 @@
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}", "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
"Sync": "Синхронизация", "Sync": "Синхронизация",
"System": "Система", "System": "Система",
"TvShows": "ТВ", "TvShows": "Телесериалы",
"User": "Пользователь", "User": "Пользователь",
"UserCreatedWithName": "Пользователь {0} был создан", "UserCreatedWithName": "Пользователь {0} был создан",
"UserDeletedWithName": "Пользователь {0} был удалён", "UserDeletedWithName": "Пользователь {0} был удалён",

@ -124,5 +124,5 @@
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.", "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov", "TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
"External": "Externé", "External": "Externé",
"HearingImpaired": "Sluchovo Postihnutý" "HearingImpaired": "Sluchovo postihnutí"
} }

@ -11,7 +11,7 @@
"Collections": "Zbirke", "Collections": "Zbirke",
"DeviceOfflineWithName": "{0} je prekinil povezavo", "DeviceOfflineWithName": "{0} je prekinil povezavo",
"DeviceOnlineWithName": "{0} je povezan", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}", "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
"Favorites": "Priljubljeno", "Favorites": "Priljubljeno",
"Folders": "Mape", "Folders": "Mape",
"Genres": "Zvrsti", "Genres": "Zvrsti",

@ -122,5 +122,6 @@
"TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்", "TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்",
"TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.", "TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.",
"TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்", "TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்",
"External": "வெளி" "External": "வெளி",
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்"
} }

@ -121,5 +121,7 @@
"TaskOptimizeDatabase": "ปรับปรุงประสิทธิภาพฐานข้อมูล", "TaskOptimizeDatabase": "ปรับปรุงประสิทธิภาพฐานข้อมูล",
"TaskOptimizeDatabaseDescription": "ลดขนาดการจัดเก็บฐานข้อมูล ใช้งานคำสั่งนี้หลังจากสแกนไลบรารีหรือหลังจากการเปลี่ยนแปลงฐานข้อมูล อาจจะทำให้ระบบทำงานเร็วขึ้น", "TaskOptimizeDatabaseDescription": "ลดขนาดการจัดเก็บฐานข้อมูล ใช้งานคำสั่งนี้หลังจากสแกนไลบรารีหรือหลังจากการเปลี่ยนแปลงฐานข้อมูล อาจจะทำให้ระบบทำงานเร็วขึ้น",
"External": "ภายนอก", "External": "ภายนอก",
"HearingImpaired": "บกพร่องทางการได้ยิน" "HearingImpaired": "บกพร่องทางการได้ยิน",
"TaskKeyframeExtractor": "ตัวแยกคีย์เฟรม",
"TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน"
} }

@ -3,19 +3,19 @@
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}", "AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
"Application": "Uygulama", "Application": "Uygulama",
"Artists": "Sanatçılar", "Artists": "Sanatçılar",
"AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı", "AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı",
"Books": "Kitaplar", "Books": "Kitaplar",
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi", "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
"Channels": "Kanallar", "Channels": "Kanallar",
"ChapterNameValue": "Bölüm {0}", "ChapterNameValue": "{0}. Bölüm",
"Collections": "Koleksiyonlar", "Collections": "Koleksiyonlar",
"DeviceOfflineWithName": "{0} bağlantısı kesildi", "DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı", "DeviceOnlineWithName": "{0} bağlı",
"FailedLoginAttemptWithUserName": "{0} adresinden giriş denemesi başarısız oldu", "FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
"Favorites": "Favoriler", "Favorites": "Favoriler",
"Folders": "Klasörler", "Folders": "Klasörler",
"Genres": "Türler", "Genres": "Türler",
"HeaderAlbumArtists": "Albüm Sanatçıları", "HeaderAlbumArtists": "Albüm sanatçıları",
"HeaderContinueWatching": "İzlemeye Devam Et", "HeaderContinueWatching": "İzlemeye Devam Et",
"HeaderFavoriteAlbums": "Favori Albümler", "HeaderFavoriteAlbums": "Favori Albümler",
"HeaderFavoriteArtists": "Favori Sanatçılar", "HeaderFavoriteArtists": "Favori Sanatçılar",
@ -25,7 +25,7 @@
"HeaderLiveTV": "Canlı TV", "HeaderLiveTV": "Canlı TV",
"HeaderNextUp": "Gelecek Hafta", "HeaderNextUp": "Gelecek Hafta",
"HeaderRecordingGroups": "Kayıt Grupları", "HeaderRecordingGroups": "Kayıt Grupları",
"HomeVideos": "Ana sayfa videoları", "HomeVideos": "Ana Sayfa Videoları",
"Inherit": "Devral", "Inherit": "Devral",
"ItemAddedWithName": "{0} kütüphaneye eklendi", "ItemAddedWithName": "{0} kütüphaneye eklendi",
"ItemRemovedWithName": "{0} kütüphaneden silindi", "ItemRemovedWithName": "{0} kütüphaneden silindi",
@ -34,14 +34,14 @@
"Latest": "En son", "Latest": "En son",
"MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi", "MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi",
"MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi", "MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi",
"MessageNamedServerConfigurationUpdatedWithValue": "Sunucu ayar kısmı {0} güncellendi", "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu yapılandırma bölümü {0} güncellendi",
"MessageServerConfigurationUpdated": "Sunucu ayarları güncellendi", "MessageServerConfigurationUpdated": "Sunucu yapılandırması güncellendi",
"MixedContent": "Karışık içerik", "MixedContent": "Karışık içerik",
"Movies": "Filmler", "Movies": "Filmler",
"Music": "Müzik", "Music": "Müzik",
"MusicVideos": "Müzik videoları", "MusicVideos": "Müzik Videoları",
"NameInstallFailed": "{0} kurulumu başarısız", "NameInstallFailed": "{0} kurulumu başarısız",
"NameSeasonNumber": "Sezon {0}", "NameSeasonNumber": "{0}. Sezon",
"NameSeasonUnknown": "Bilinmeyen Sezon", "NameSeasonUnknown": "Bilinmeyen Sezon",
"NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.", "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.",
"NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut", "NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut",
@ -55,9 +55,9 @@
"NotificationOptionPluginInstalled": "Eklenti yüklendi", "NotificationOptionPluginInstalled": "Eklenti yüklendi",
"NotificationOptionPluginUninstalled": "Eklenti kaldırıldı", "NotificationOptionPluginUninstalled": "Eklenti kaldırıldı",
"NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi", "NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi",
"NotificationOptionServerRestartRequired": "Sunucu yeniden başlatma gerekli", "NotificationOptionServerRestartRequired": "Sunucunun yeniden başlatılması gerekiyor",
"NotificationOptionTaskFailed": "Zamanlanmış görev hatası", "NotificationOptionTaskFailed": "Zamanlanmış görev hatası",
"NotificationOptionUserLockedOut": "Kullanıcı kitlendi", "NotificationOptionUserLockedOut": "Kullanıcı kilitlendi",
"NotificationOptionVideoPlayback": "Video oynatma başladı", "NotificationOptionVideoPlayback": "Video oynatma başladı",
"NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu", "NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu",
"Photos": "Fotoğraflar", "Photos": "Fotoğraflar",
@ -74,36 +74,36 @@
"Songs": "Şarkılar", "Songs": "Şarkılar",
"StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.", "StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi", "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} sağlayıcısından indirilemedi",
"Sync": "Eşzamanlama", "Sync": "Eşzamanlama",
"System": "Sistem", "System": "Sistem",
"TvShows": "Diziler", "TvShows": "Diziler",
"User": "Kullanıcı", "User": "Kullanıcı",
"UserCreatedWithName": "{0} kullanıcısı oluşturuldu", "UserCreatedWithName": "{0} kullanıcısı oluşturuldu",
"UserDeletedWithName": "Kullanıcı {0} silindi", "UserDeletedWithName": "{0} kullanıcısı silindi",
"UserDownloadingItemWithValues": "{0} indiriliyor {1}", "UserDownloadingItemWithValues": "{0} {1} medyasını indiriyor",
"UserLockedOutWithName": "Kullanıcı {0} kitlendi", "UserLockedOutWithName": "{0} adlı kullanıcı kilitlendi",
"UserOfflineFromDevice": "{0}, {1} ile bağlantısı kesildi", "UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi",
"UserOnlineFromDevice": "{0}, {1} çevrimiçi", "UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi",
"UserPasswordChangedWithName": "{0} kullanıcısı için şifre değiştirildi", "UserPasswordChangedWithName": "{0} kullanıcısının parolası değiştirildi",
"UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için güncellendi", "UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi",
"UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor", "UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
"UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi", "UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
"ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi", "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi",
"ValueSpecialEpisodeName": "Özel - {0}", "ValueSpecialEpisodeName": "Özel - {0}",
"VersionNumber": "Sürüm {0}", "VersionNumber": "Sürüm {0}",
"TaskCleanCache": "Geçici dosya klasörünü temizle", "TaskCleanCache": "Geçici Dosya Klasörünü Temizle",
"TasksChannelsCategory": "İnternet kanalları", "TasksChannelsCategory": "İnternet Kanalları",
"TasksApplicationCategory": "Uygulama", "TasksApplicationCategory": "Uygulama",
"TasksLibraryCategory": "Kütüphane", "TasksLibraryCategory": "Kütüphane",
"TasksMaintenanceCategory": "Bakım", "TasksMaintenanceCategory": "Bakım",
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.", "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
"TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.", "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.",
"TaskDownloadMissingSubtitles": "Eksik altyazıları indir", "TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.", "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
"TaskRefreshChannels": "Kanalları Yenile", "TaskRefreshChannels": "Kanalları Yenile",
"TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.", "TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.",
"TaskCleanTranscode": "Dönüşüm Dizinini Temizle", "TaskCleanTranscode": "Kod Dönüştürme Dizinini Temizle",
"TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.", "TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
"TaskUpdatePlugins": "Eklentileri Güncelle", "TaskUpdatePlugins": "Eklentileri Güncelle",
"TaskRefreshPeople": "Kullanıcıları Yenile", "TaskRefreshPeople": "Kullanıcıları Yenile",

@ -25,5 +25,14 @@
"Channels": "Amashaneli", "Channels": "Amashaneli",
"Books": "Izincwadi", "Books": "Izincwadi",
"Artists": "Abadlali", "Artists": "Abadlali",
"Albums": "Ama-albhamu" "Albums": "Ama-albhamu",
"CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
"HeaderFavoriteArtists": "Abasethi Abathandekayo",
"HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
"HeaderFavoriteShows": "Izisho Ezithandekayo",
"External": "Kwezifungo",
"FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
"HeaderContinueWatching": "Buyela Ukubona",
"HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
"HeaderAlbumArtists": "Abasethi wenkulumo"
} }

@ -71,25 +71,28 @@ namespace Emby.Server.Implementations.Localization
string countryCode = resource.Substring(RatingsPath.Length, 2); string countryCode = resource.Substring(RatingsPath.Length, 2);
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase); var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
await using var stream = _assembly.GetManifestResourceStream(resource); var stream = _assembly.GetManifestResourceStream(resource);
using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{ {
if (string.IsNullOrWhiteSpace(line)) using var reader = new StreamReader(stream!);
await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{ {
continue; if (string.IsNullOrWhiteSpace(line))
} {
continue;
string[] parts = line.Split(','); }
if (parts.Length == 2
&& int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) string[] parts = line.Split(',');
{ if (parts.Length == 2
var name = parts[0]; && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
dict.Add(name, new ParentalRating(name, value)); {
} var name = parts[0];
else dict.Add(name, new ParentalRating(name, value));
{ }
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); else
{
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
}
} }
} }

@ -4,10 +4,14 @@ G,0
M,15 M,15
MA,15 MA,15
MA15+,15 MA15+,15
MA 15+,15
PG,16 PG,16
16+,16 16+,16
R,18 R,18
R18+,18 R18+,18
X18+,18 R 18+,18
18+,18 18+,18
X18+,1000
X 18+,1000
X,1000 X,1000
RC,1001

1 Exempt 0
4 M 15
5 MA 15
6 MA15+ 15
7 MA 15+ 15
8 PG 16
9 16+ 16
10 R 18
11 R18+ 18
12 X18+ R 18+ 18
13 18+ 18
14 X18+ 1000
15 X 18+ 1000
16 X 1000
17 RC 1001

@ -1,12 +1,17 @@
Educational,0 Educational,0
Infoprogramm,0 Infoprogramm,0
FSK-0,0 FSK-0,0
FSK 0,0
0,0 0,0
FSK-6,6 FSK-6,6
FSK 6,6
6,6 6,6
FSK-12,12 FSK-12,12
FSK 12,12
12,12 12,12
FSK-16,16 FSK-16,16
FSK 16,16
16,16 16,16
FSK-18,18 FSK-18,18
FSK 18,18
18,18 18,18

1 Educational 0
2 Infoprogramm 0
3 FSK-0 0
4 FSK 0 0
5 0 0
6 FSK-6 6
7 FSK 6 6
8 6 6
9 FSK-12 12
10 FSK 12 12
11 12 12
12 FSK-16 16
13 FSK 16 16
14 16 16
15 FSK-18 18
16 FSK 18 18
17 18 18

@ -3,6 +3,7 @@ A/fig,0
A/i,0 A/i,0
A/fig/i,0 A/fig/i,0
APTA,0 APTA,0
ERI,0
TP,0 TP,0
0+,0 0+,0
6+,6 6+,6

1 A 0
3 A/i 0
4 A/fig/i 0
5 APTA 0
6 ERI 0
7 TP 0
8 0+ 0
9 6+ 6

@ -1,5 +1,6 @@
Public Averti,0 Public Averti,0
Tous Publics,0 Tous Publics,0
TP,0
U,0 U,0
0+,0 0+,0
6+,6 6+,6

1 Public Averti 0
2 Tous Publics 0
3 TP 0
4 U 0
5 0+ 0
6 6+ 6

@ -0,0 +1,6 @@
NR,0
U,0
7,7
12,12
15,15
18,18
1 NR 0
2 U 0
3 7 7
4 12 12
5 15 15
6 18 18

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

Loading…
Cancel
Save