mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-05-24 02:02:29 -04:00
Merge branch 'master' into trickplay
This commit is contained in:
commit
cd662506a1
@ -168,6 +168,7 @@ jobs:
|
||||
- job: CollectArtifacts
|
||||
timeoutInMinutes: 20
|
||||
displayName: 'Collect Artifacts'
|
||||
condition: succeededOrFailed()
|
||||
continueOnError: true
|
||||
dependsOn:
|
||||
- BuildPackage
|
||||
|
12
.config/dotnet-tools.json
Normal file
12
.config/dotnet-tools.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "7.0.12",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@ -20,18 +20,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
|
||||
uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
|
||||
uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
|
||||
uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
|
||||
|
4
.github/workflows/commands.yml
vendored
4
.github/workflows/commands.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@ -51,7 +51,7 @@ jobs:
|
||||
reactions: eyes
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
8
.github/workflows/openapi.yml
vendored
8
.github/workflows/openapi.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@ -25,7 +25,7 @@ jobs:
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
@ -39,7 +39,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@ -59,7 +59,7 @@ jobs:
|
||||
- name: Generate openapi.json
|
||||
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
|
||||
- name: Upload openapi.json
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
|
82
.github/workflows/repo-bump-version.yaml
vendored
Normal file
82
.github/workflows/repo-bump-version.yaml
vendored
Normal file
@ -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 }}
|
13
.github/workflows/repo-stale.yaml
vendored
13
.github/workflows/repo-stale.yaml
vendored
@ -2,16 +2,17 @@ name: Stale Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
- cron: '30 */12 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
issues:
|
||||
name: Check issues
|
||||
name: Check for stale issues
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
@ -26,11 +27,11 @@ jobs:
|
||||
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
|
||||
stale-issue-label: stale
|
||||
stale-issue-message: |-
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
|
||||
close-issue-message: |-
|
||||
This issue was closed due to inactivity.
|
||||
|
||||
prs-conflicts:
|
||||
name: Check PRs with merge conflicts
|
||||
|
@ -168,6 +168,8 @@
|
||||
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||
- [ipitio](https://github.com/ipitio)
|
||||
- [TheTyrius](https://github.com/TheTyrius)
|
||||
- [tallbl0nde](https://github.com/tallbl0nde)
|
||||
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
@ -238,3 +240,4 @@
|
||||
- [Jakob Kukla](https://github.com/jakobkukla)
|
||||
- [Utku Özdemir](https://github.com/utkuozdemir)
|
||||
- [JPUC1143](https://github.com/Jpuc1143/)
|
||||
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
||||
|
@ -10,26 +10,30 @@
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.0" />
|
||||
<PackageVersion Include="BDInfo" Version="0.7.6.2" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.2.0" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.3.0" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.18" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.4" />
|
||||
<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="libse" Version="3.6.13" />
|
||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" />
|
||||
<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.EntityFrameworkCore.Design" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" />
|
||||
<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.Memory" 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.DependencyInjection.Abstractions" 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" Version="7.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" 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" Version="7.0.0" />
|
||||
<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="MimeTypes" Version="2.4.0" />
|
||||
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
||||
@ -53,28 +57,25 @@
|
||||
<PackageVersion Include="NEbml" Version="0.11.0" />
|
||||
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<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" Version="8.0.0" />
|
||||
<PackageVersion Include="prometheus-net" Version="8.0.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="7.0.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.Console" Version="4.1.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="SharpFuzz" Version="2.1.0" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||
<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.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="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="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="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
@ -85,8 +86,8 @@
|
||||
<PackageVersion Include="TMDbLib" Version="2.0.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<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" Version="2.4.2" />
|
||||
<PackageVersion Include="xunit" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -4,7 +4,7 @@
|
||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=7.0
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
|
@ -5,7 +5,7 @@
|
||||
ARG DOTNET_VERSION=7.0
|
||||
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
|
@ -5,7 +5,7 @@
|
||||
ARG DOTNET_VERSION=7.0
|
||||
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
|
@ -17,7 +17,7 @@ namespace Emby.Dlna.Configuration
|
||||
BlastAliveMessages = true;
|
||||
SendOnlyMatchedHost = true;
|
||||
ClientDiscoveryIntervalSeconds = 60;
|
||||
AliveMessageIntervalSeconds = 1800;
|
||||
AliveMessageIntervalSeconds = 180;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@ -45,8 +43,8 @@ namespace Emby.Dlna.Didl
|
||||
private readonly DeviceProfile _profile;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly string _serverAddress;
|
||||
private readonly string _accessToken;
|
||||
private readonly User _user;
|
||||
private readonly string? _accessToken;
|
||||
private readonly User? _user;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
@ -56,10 +54,10 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
public DidlBuilder(
|
||||
DeviceProfile profile,
|
||||
User user,
|
||||
User? user,
|
||||
IImageProcessor imageProcessor,
|
||||
string serverAddress,
|
||||
string accessToken,
|
||||
string? accessToken,
|
||||
IUserDataManager userDataManager,
|
||||
ILocalizationManager localization,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
@ -85,7 +83,7 @@ namespace Emby.Dlna.Didl
|
||||
return url + "&dlnaheaders=true";
|
||||
}
|
||||
|
||||
public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo)
|
||||
public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo)
|
||||
{
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
@ -140,12 +138,12 @@ namespace Emby.Dlna.Didl
|
||||
public void WriteItemElement(
|
||||
XmlWriter writer,
|
||||
BaseItem item,
|
||||
User user,
|
||||
BaseItem context,
|
||||
User? user,
|
||||
BaseItem? context,
|
||||
StubType? contextStubType,
|
||||
string deviceId,
|
||||
Filter filter,
|
||||
StreamInfo streamInfo = null)
|
||||
StreamInfo? streamInfo = null)
|
||||
{
|
||||
var clientId = GetClientId(item, null);
|
||||
|
||||
@ -190,7 +188,7 @@ namespace Emby.Dlna.Didl
|
||||
writer.WriteFullEndElement();
|
||||
}
|
||||
|
||||
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
||||
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null)
|
||||
{
|
||||
if (streamInfo is null)
|
||||
{
|
||||
@ -203,7 +201,7 @@ namespace Emby.Dlna.Didl
|
||||
Profile = _profile,
|
||||
DeviceId = deviceId,
|
||||
MaxBitrate = _profile.MaxStreamingBitrate
|
||||
});
|
||||
}) ?? throw new InvalidOperationException("No optimal video stream found");
|
||||
}
|
||||
|
||||
var targetWidth = streamInfo.TargetWidth;
|
||||
@ -315,7 +313,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
var mediaSource = streamInfo.MediaSource;
|
||||
|
||||
if (mediaSource.RunTimeTicks.HasValue)
|
||||
if (mediaSource?.RunTimeTicks.HasValue == true)
|
||||
{
|
||||
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
|
||||
}
|
||||
@ -410,7 +408,7 @@ namespace Emby.Dlna.Didl
|
||||
writer.WriteFullEndElement();
|
||||
}
|
||||
|
||||
private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context)
|
||||
private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context)
|
||||
{
|
||||
if (itemStubType.HasValue)
|
||||
{
|
||||
@ -452,7 +450,7 @@ namespace Emby.Dlna.Didl
|
||||
/// <param name="episode">The episode.</param>
|
||||
/// <param name="context">Current context.</param>
|
||||
/// <returns>Formatted name of the episode.</returns>
|
||||
private string GetEpisodeDisplayName(Episode episode, BaseItem context)
|
||||
private string GetEpisodeDisplayName(Episode episode, BaseItem? context)
|
||||
{
|
||||
string[] components;
|
||||
|
||||
@ -530,7 +528,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
|
||||
|
||||
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
||||
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null)
|
||||
{
|
||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
||||
|
||||
@ -544,14 +542,14 @@ namespace Emby.Dlna.Didl
|
||||
MediaSources = sources.ToArray(),
|
||||
Profile = _profile,
|
||||
DeviceId = deviceId
|
||||
});
|
||||
}) ?? throw new InvalidOperationException("No optimal audio stream found");
|
||||
}
|
||||
|
||||
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
|
||||
|
||||
var mediaSource = streamInfo.MediaSource;
|
||||
|
||||
if (mediaSource.RunTimeTicks.HasValue)
|
||||
if (mediaSource?.RunTimeTicks is not null)
|
||||
{
|
||||
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
|
||||
}
|
||||
@ -634,7 +632,7 @@ namespace Emby.Dlna.Didl
|
||||
// Samsung sometimes uses 1 as root
|
||||
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
|
||||
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string? requestedId = null)
|
||||
{
|
||||
writer.WriteStartElement(string.Empty, "container", NsDidl);
|
||||
|
||||
@ -678,14 +676,14 @@ namespace Emby.Dlna.Didl
|
||||
writer.WriteFullEndElement();
|
||||
}
|
||||
|
||||
private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo)
|
||||
private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo)
|
||||
{
|
||||
if (!item.SupportsPositionTicksResume || item is Folder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
XmlAttribute secAttribute = null;
|
||||
XmlAttribute? secAttribute = null;
|
||||
foreach (var attribute in _profile.XmlRootAttributes)
|
||||
{
|
||||
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
|
||||
@ -695,8 +693,8 @@ namespace Emby.Dlna.Didl
|
||||
}
|
||||
}
|
||||
|
||||
// Not a samsung device
|
||||
if (secAttribute is null)
|
||||
// Not a samsung device or no user data
|
||||
if (secAttribute is null || user is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -717,7 +715,7 @@ namespace Emby.Dlna.Didl
|
||||
/// <summary>
|
||||
/// Adds fields used by both items and folders.
|
||||
/// </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
|
||||
// MediaMonkey for example won't display content without a title
|
||||
@ -795,7 +793,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
if (item.IsDisplayedAsFolder || stubType.HasValue)
|
||||
{
|
||||
string classType = null;
|
||||
string? classType = null;
|
||||
|
||||
if (!_profile.RequiresPlainFolders)
|
||||
{
|
||||
@ -899,7 +897,7 @@ namespace Emby.Dlna.Didl
|
||||
}
|
||||
}
|
||||
|
||||
private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
|
||||
private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
|
||||
{
|
||||
AddCommonFields(item, itemStubType, context, writer, filter);
|
||||
|
||||
@ -975,7 +973,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer)
|
||||
{
|
||||
ImageDownloadInfo imageInfo = GetImageInfo(item);
|
||||
ImageDownloadInfo? imageInfo = GetImageInfo(item);
|
||||
|
||||
if (imageInfo is null)
|
||||
{
|
||||
@ -1073,7 +1071,7 @@ namespace Emby.Dlna.Didl
|
||||
writer.WriteFullEndElement();
|
||||
}
|
||||
|
||||
private ImageDownloadInfo GetImageInfo(BaseItem item)
|
||||
private ImageDownloadInfo? GetImageInfo(BaseItem item)
|
||||
{
|
||||
if (item.HasImage(ImageType.Primary))
|
||||
{
|
||||
@ -1118,7 +1116,7 @@ namespace Emby.Dlna.Didl
|
||||
return null;
|
||||
}
|
||||
|
||||
private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
|
||||
private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item)
|
||||
{
|
||||
if (item is null)
|
||||
{
|
||||
@ -1148,7 +1146,7 @@ namespace Emby.Dlna.Didl
|
||||
private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
|
||||
{
|
||||
var imageInfo = item.GetImageInfo(type, 0);
|
||||
string tag = null;
|
||||
string? tag = null;
|
||||
|
||||
try
|
||||
{
|
||||
@ -1250,7 +1248,7 @@ namespace Emby.Dlna.Didl
|
||||
{
|
||||
internal Guid ItemId { get; set; }
|
||||
|
||||
internal string ImageTag { get; set; }
|
||||
internal string? ImageTag { get; set; }
|
||||
|
||||
internal ImageType Type { get; set; }
|
||||
|
||||
@ -1260,9 +1258,9 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
internal bool IsDirectStream { get; set; }
|
||||
|
||||
internal string Format { get; set; }
|
||||
internal required string Format { get; set; }
|
||||
|
||||
internal ItemImageInfo ItemImageInfo { get; set; }
|
||||
internal required ItemImageInfo ItemImageInfo { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -228,7 +228,7 @@ namespace Emby.Dlna
|
||||
try
|
||||
{
|
||||
return _fileSystem.GetFilePaths(path)
|
||||
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(i => ParseProfileFile(i, type))
|
||||
.Where(i => i is not null)
|
||||
.ToList()!; // We just filtered out all the nulls
|
||||
|
69
Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
Normal file
69
Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
Normal file
@ -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.Ssdp;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using Jellyfin.Networking.Manager;
|
||||
using Jellyfin.Networking.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
@ -23,10 +23,8 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Rssdp;
|
||||
using Rssdp.Infrastructure;
|
||||
@ -49,14 +47,13 @@ namespace Emby.Dlna.Main
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly ISsdpCommunicationsServer _communicationsServer;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly object _syncLock = new object();
|
||||
private readonly object _syncLock = new();
|
||||
private readonly bool _disabled;
|
||||
|
||||
private PlayToManager _manager;
|
||||
private SsdpDevicePublisher _publisher;
|
||||
private ISsdpCommunicationsServer _communicationsServer;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
@ -75,10 +72,8 @@ namespace Emby.Dlna.Main
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IDeviceDiscovery deviceDiscovery,
|
||||
IMediaEncoder mediaEncoder,
|
||||
ISocketFactory socketFactory,
|
||||
INetworkManager networkManager,
|
||||
IUserViewManager userViewManager,
|
||||
ITVSeriesManager tvSeriesManager)
|
||||
ISsdpCommunicationsServer communicationsServer,
|
||||
INetworkManager networkManager)
|
||||
{
|
||||
_config = config;
|
||||
_appHost = appHost;
|
||||
@ -93,37 +88,10 @@ namespace Emby.Dlna.Main
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_socketFactory = socketFactory;
|
||||
_communicationsServer = communicationsServer;
|
||||
_networkManager = networkManager;
|
||||
_logger = loggerFactory.CreateLogger<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);
|
||||
_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()
|
||||
{
|
||||
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
||||
@ -172,9 +127,7 @@ namespace Emby.Dlna.Main
|
||||
private void ReloadComponents()
|
||||
{
|
||||
var options = _config.GetDlnaConfiguration();
|
||||
Enabled = options.EnableServer;
|
||||
|
||||
StartSsdpHandler();
|
||||
StartDeviceDiscovery();
|
||||
|
||||
if (options.EnableServer)
|
||||
{
|
||||
@ -195,37 +148,11 @@ namespace Emby.Dlna.Main
|
||||
}
|
||||
}
|
||||
|
||||
private void StartSsdpHandler()
|
||||
private void StartDeviceDiscovery()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_communicationsServer is null)
|
||||
{
|
||||
var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
|
||||
OperatingSystem.IsLinux();
|
||||
|
||||
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||
{
|
||||
IsShared = true
|
||||
};
|
||||
|
||||
StartDeviceDiscovery(_communicationsServer);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting ssdp handlers");
|
||||
}
|
||||
}
|
||||
|
||||
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (communicationsServer is not null)
|
||||
{
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||
}
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -233,26 +160,8 @@ namespace Emby.Dlna.Main
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeDeviceDiscovery()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Disposing DeviceDiscovery");
|
||||
((DeviceDiscovery)_deviceDiscovery).Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error stopping device discovery");
|
||||
}
|
||||
}
|
||||
|
||||
public void StartDevicePublisher(Configuration.DlnaOptions options)
|
||||
{
|
||||
if (!options.BlastAliveMessages)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_publisher is not null)
|
||||
{
|
||||
return;
|
||||
@ -263,7 +172,8 @@ namespace Emby.Dlna.Main
|
||||
_publisher = new SsdpDevicePublisher(
|
||||
_communicationsServer,
|
||||
Environment.OSVersion.Platform.ToString(),
|
||||
Environment.OSVersion.VersionString,
|
||||
// Can not use VersionString here since that includes OS and version
|
||||
Environment.OSVersion.Version.ToString(),
|
||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||
{
|
||||
LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
|
||||
@ -272,7 +182,10 @@ namespace Emby.Dlna.Main
|
||||
|
||||
RegisterServerEndpoints();
|
||||
|
||||
_publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
|
||||
if (options.BlastAliveMessages)
|
||||
{
|
||||
_publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -285,42 +198,33 @@ namespace Emby.Dlna.Main
|
||||
var udn = CreateUuid(_appHost.SystemId);
|
||||
var descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||
|
||||
var bindAddresses = NetworkManager.CreateCollection(
|
||||
_networkManager.GetInternalBindAddresses()
|
||||
.Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)));
|
||||
// Only get bind addresses in LAN
|
||||
// IPv6 is currently unsupported
|
||||
var validInterfaces = _networkManager.GetInternalBindAddresses()
|
||||
.Where(x => x.Address is not null)
|
||||
.Where(x => x.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
.ToList();
|
||||
|
||||
if (bindAddresses.Count == 0)
|
||||
if (validInterfaces.Count == 0)
|
||||
{
|
||||
// No interfaces returned, so use loopback.
|
||||
bindAddresses = _networkManager.GetLoopbacks();
|
||||
// No interfaces returned, fall back to loopback
|
||||
validInterfaces = _networkManager.GetLoopbacks().ToList();
|
||||
}
|
||||
|
||||
foreach (IPNetAddress address in bindAddresses)
|
||||
foreach (var intf in validInterfaces)
|
||||
{
|
||||
if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
// Not supporting IPv6 right now
|
||||
continue;
|
||||
}
|
||||
|
||||
// Limit to LAN addresses only
|
||||
if (!_networkManager.IsInLocalNetwork(address))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
|
||||
|
||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
|
||||
_logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
|
||||
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri);
|
||||
var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri);
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
{
|
||||
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
|
||||
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
|
||||
Address = address.Address,
|
||||
PrefixLength = address.PrefixLength,
|
||||
Address = intf.Address,
|
||||
PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix),
|
||||
FriendlyName = "Jellyfin",
|
||||
Manufacturer = "Jellyfin",
|
||||
ModelName = "Jellyfin Server",
|
||||
@ -328,7 +232,7 @@ namespace Emby.Dlna.Main
|
||||
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
|
||||
};
|
||||
|
||||
SetProperies(device, fullService);
|
||||
SetProperties(device, fullService);
|
||||
_publisher.AddDevice(device);
|
||||
|
||||
var embeddedDevices = new[]
|
||||
@ -349,13 +253,13 @@ namespace Emby.Dlna.Main
|
||||
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
|
||||
};
|
||||
|
||||
SetProperies(embeddedDevice, subDevice);
|
||||
SetProperties(embeddedDevice, subDevice);
|
||||
device.AddDevice(embeddedDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string CreateUuid(string text)
|
||||
private static string CreateUuid(string text)
|
||||
{
|
||||
if (!Guid.TryParse(text, out var guid))
|
||||
{
|
||||
@ -365,15 +269,14 @@ namespace Emby.Dlna.Main
|
||||
return guid.ToString("D", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private void SetProperies(SsdpDevice device, string fullDeviceType)
|
||||
private static void SetProperties(SsdpDevice device, string fullDeviceType)
|
||||
{
|
||||
var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
var serviceParts = fullDeviceType
|
||||
.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Split(':');
|
||||
|
||||
var serviceParts = service.Split(':');
|
||||
|
||||
var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
|
||||
|
||||
device.DeviceTypeNamespace = deviceTypeNamespace;
|
||||
device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
|
||||
device.DeviceClass = serviceParts[1];
|
||||
device.DeviceType = serviceParts[2];
|
||||
}
|
||||
@ -454,20 +357,6 @@ namespace Emby.Dlna.Main
|
||||
|
||||
DisposeDevicePublisher();
|
||||
DisposePlayToManager();
|
||||
DisposeDeviceDiscovery();
|
||||
|
||||
if (_communicationsServer is not null)
|
||||
{
|
||||
_logger.LogInformation("Disposing SsdpCommunicationsServer");
|
||||
_communicationsServer.Dispose();
|
||||
_communicationsServer = null;
|
||||
}
|
||||
|
||||
ContentDirectory = null;
|
||||
ConnectionManager = null;
|
||||
MediaReceiverRegistrar = null;
|
||||
Current = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@ -25,7 +23,7 @@ namespace Emby.Dlna.PlayTo
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly object _timerLock = new object();
|
||||
private Timer _timer;
|
||||
private Timer? _timer;
|
||||
private int _muteVol;
|
||||
private int _volume;
|
||||
private DateTime _lastVolumeRefresh;
|
||||
@ -40,13 +38,13 @@ namespace Emby.Dlna.PlayTo
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public event EventHandler<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; }
|
||||
|
||||
@ -75,13 +73,13 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
public bool IsStopped => TransportState == TransportState.STOPPED;
|
||||
|
||||
public Action OnDeviceUnavailable { get; set; }
|
||||
public Action? OnDeviceUnavailable { get; set; }
|
||||
|
||||
private TransportCommands AvCommands { get; set; }
|
||||
private TransportCommands? AvCommands { get; set; }
|
||||
|
||||
private TransportCommands RendererCommands { get; set; }
|
||||
private TransportCommands? RendererCommands { get; set; }
|
||||
|
||||
public UBaseObject CurrentMediaInfo { get; private set; }
|
||||
public UBaseObject? CurrentMediaInfo { get; private set; }
|
||||
|
||||
public void Start()
|
||||
{
|
||||
@ -131,7 +129,7 @@ namespace Emby.Dlna.PlayTo
|
||||
_volumeRefreshActive = true;
|
||||
|
||||
var time = immediate ? 100 : 10000;
|
||||
_timer.Change(time, Timeout.Infinite);
|
||||
_timer?.Change(time, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,7 +147,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
_volumeRefreshActive = false;
|
||||
|
||||
_timer.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
_timer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,7 +197,7 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
}
|
||||
|
||||
private DeviceService GetServiceRenderingControl()
|
||||
private DeviceService? GetServiceRenderingControl()
|
||||
{
|
||||
var services = Properties.Services;
|
||||
|
||||
@ -207,7 +205,7 @@ namespace Emby.Dlna.PlayTo
|
||||
services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private DeviceService GetAvTransportService()
|
||||
private DeviceService? GetAvTransportService()
|
||||
{
|
||||
var services = Properties.Services;
|
||||
|
||||
@ -240,7 +238,7 @@ namespace Emby.Dlna.PlayTo
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
command.Name,
|
||||
rendererCommands.BuildPost(command, service.ServiceType, value),
|
||||
rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
|
||||
cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@ -265,12 +263,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return;
|
||||
}
|
||||
|
||||
var service = GetServiceRenderingControl();
|
||||
|
||||
if (service is null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to find service");
|
||||
}
|
||||
var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service");
|
||||
|
||||
// Set it early and assume it will succeed
|
||||
// Remote control will perform better
|
||||
@ -281,7 +274,7 @@ namespace Emby.Dlna.PlayTo
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
command.Name,
|
||||
rendererCommands.BuildPost(command, service.ServiceType, value),
|
||||
rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
|
||||
cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
@ -296,26 +289,20 @@ namespace Emby.Dlna.PlayTo
|
||||
return;
|
||||
}
|
||||
|
||||
var service = GetAvTransportService();
|
||||
|
||||
if (service is null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to find service");
|
||||
}
|
||||
|
||||
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||
await new DlnaHttpClient(_logger, _httpClientFactory)
|
||||
.SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
command.Name,
|
||||
avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
|
||||
avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above
|
||||
cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
RestartTimer(true);
|
||||
}
|
||||
|
||||
public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken)
|
||||
public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken)
|
||||
{
|
||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@ -335,14 +322,8 @@ namespace Emby.Dlna.PlayTo
|
||||
{ "CurrentURIMetaData", CreateDidlMeta(metaData) }
|
||||
};
|
||||
|
||||
var service = GetAvTransportService();
|
||||
|
||||
if (service is null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to find service");
|
||||
}
|
||||
|
||||
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
||||
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||
var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
|
||||
await new DlnaHttpClient(_logger, _httpClientFactory)
|
||||
.SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
@ -372,7 +353,7 @@ namespace Emby.Dlna.PlayTo
|
||||
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
|
||||
* Without that information, the next track command on the device does not work.
|
||||
*/
|
||||
public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
|
||||
public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@ -380,7 +361,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
|
||||
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
|
||||
var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
|
||||
if (command is null)
|
||||
{
|
||||
return;
|
||||
@ -392,14 +373,8 @@ namespace Emby.Dlna.PlayTo
|
||||
{ "NextURIMetaData", CreateDidlMeta(metaData) }
|
||||
};
|
||||
|
||||
var service = GetAvTransportService();
|
||||
|
||||
if (service is null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to find service");
|
||||
}
|
||||
|
||||
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
||||
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||
var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
|
||||
await new DlnaHttpClient(_logger, _httpClientFactory)
|
||||
.SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
@ -423,12 +398,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var service = GetAvTransportService();
|
||||
if (service is null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to find service");
|
||||
}
|
||||
|
||||
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||
return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
@ -460,14 +430,13 @@ namespace Emby.Dlna.PlayTo
|
||||
return;
|
||||
}
|
||||
|
||||
var service = GetAvTransportService();
|
||||
|
||||
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||
await new DlnaHttpClient(_logger, _httpClientFactory)
|
||||
.SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
command.Name,
|
||||
avCommands.BuildPost(command, service.ServiceType, 1),
|
||||
avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
|
||||
cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@ -484,14 +453,13 @@ namespace Emby.Dlna.PlayTo
|
||||
return;
|
||||
}
|
||||
|
||||
var service = GetAvTransportService();
|
||||
|
||||
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||
await new DlnaHttpClient(_logger, _httpClientFactory)
|
||||
.SendCommandAsync(
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
command.Name,
|
||||
avCommands.BuildPost(command, service.ServiceType, 1),
|
||||
avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
|
||||
cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@ -500,7 +468,7 @@ namespace Emby.Dlna.PlayTo
|
||||
RestartTimer(true);
|
||||
}
|
||||
|
||||
private async void TimerCallback(object sender)
|
||||
private async void TimerCallback(object? sender)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
@ -623,7 +591,7 @@ namespace Emby.Dlna.PlayTo
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
command.Name,
|
||||
rendererCommands.BuildPost(command, service.ServiceType),
|
||||
rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result is null || result.Document is null)
|
||||
@ -673,7 +641,7 @@ namespace Emby.Dlna.PlayTo
|
||||
Properties.BaseUrl,
|
||||
service,
|
||||
command.Name,
|
||||
rendererCommands.BuildPost(command, service.ServiceType),
|
||||
rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (result is null || result.Document is null)
|
||||
@ -728,7 +696,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
|
||||
if (command is null)
|
||||
@ -798,7 +766,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
|
||||
if (command is null)
|
||||
@ -871,7 +839,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
XElement uPnpResponse = null;
|
||||
XElement? uPnpResponse = null;
|
||||
|
||||
try
|
||||
{
|
||||
@ -895,7 +863,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return (true, uTrack);
|
||||
}
|
||||
|
||||
private XElement ParseResponse(string xml)
|
||||
private XElement? ParseResponse(string xml)
|
||||
{
|
||||
// Handle different variations sent back by devices.
|
||||
try
|
||||
@ -929,7 +897,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return null;
|
||||
}
|
||||
|
||||
private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
|
||||
private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(container);
|
||||
|
||||
@ -959,20 +927,17 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var resElement = container.Element(UPnpNamespaces.Res);
|
||||
|
||||
if (resElement is not null)
|
||||
{
|
||||
var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
|
||||
var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo);
|
||||
|
||||
if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
|
||||
{
|
||||
return info.Value.Split(':');
|
||||
}
|
||||
if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
|
||||
{
|
||||
return info.Value.Split(':');
|
||||
}
|
||||
|
||||
return new string[4];
|
||||
}
|
||||
|
||||
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
|
||||
private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (AvCommands is not null)
|
||||
{
|
||||
@ -1004,7 +969,7 @@ namespace Emby.Dlna.PlayTo
|
||||
return AvCommands;
|
||||
}
|
||||
|
||||
private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken)
|
||||
private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (RendererCommands is not null)
|
||||
{
|
||||
@ -1054,7 +1019,7 @@ namespace Emby.Dlna.PlayTo
|
||||
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);
|
||||
|
||||
@ -1171,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
|
||||
return new Device(deviceProperties, httpClientFactory, logger);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private static DeviceIcon CreateIcon(XElement element)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(element);
|
||||
@ -1287,7 +1251,7 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
|
||||
_timer = null;
|
||||
Properties = null;
|
||||
Properties = null!;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
@ -31,6 +31,9 @@ namespace Emby.Dlna.PlayTo
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[GeneratedRegex("(&(?![a-z]*;))")]
|
||||
private static partial Regex EscapeAmpersandRegex();
|
||||
|
||||
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||
{
|
||||
// If it's already a complete url, don't stick anything onto the front of it
|
||||
@ -52,40 +55,42 @@ namespace Emby.Dlna.PlayTo
|
||||
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
|
||||
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using MemoryStream ms = new MemoryStream();
|
||||
await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
ms,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException)
|
||||
{
|
||||
// try correcting the Xml response with common errors
|
||||
ms.Position = 0;
|
||||
using StreamReader sr = new StreamReader(ms);
|
||||
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// find and replace unescaped ampersands (&)
|
||||
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
|
||||
|
||||
try
|
||||
{
|
||||
// retry reading Xml
|
||||
using var xmlReader = new StringReader(xmlString);
|
||||
return await XDocument.LoadAsync(
|
||||
xmlReader,
|
||||
stream,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
catch (XmlException)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse response");
|
||||
_logger.LogDebug("Malformed response: {Content}\n", xmlString);
|
||||
// try correcting the Xml response with common errors
|
||||
stream.Position = 0;
|
||||
using StreamReader sr = new StreamReader(stream);
|
||||
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return null;
|
||||
// find and replace unescaped ampersands (&)
|
||||
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
|
||||
|
||||
try
|
||||
{
|
||||
// retry reading Xml
|
||||
using var xmlReader = new StringReader(xmlString);
|
||||
return await XDocument.LoadAsync(
|
||||
xmlReader,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse response");
|
||||
_logger.LogDebug("Malformed response: {Content}\n", xmlString);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -128,12 +133,5 @@ namespace Emby.Dlna.PlayTo
|
||||
// Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
|
||||
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <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 string _serverAddress;
|
||||
private readonly string _accessToken;
|
||||
private readonly string? _accessToken;
|
||||
|
||||
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
|
||||
private Device _device;
|
||||
@ -59,7 +59,7 @@ namespace Emby.Dlna.PlayTo
|
||||
IUserManager userManager,
|
||||
IImageProcessor imageProcessor,
|
||||
string serverAddress,
|
||||
string accessToken,
|
||||
string? accessToken,
|
||||
IDeviceDiscovery deviceDiscovery,
|
||||
IUserDataManager userDataManager,
|
||||
ILocalizationManager localization,
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@ -41,9 +39,9 @@ namespace Emby.Dlna.PlayTo
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||
private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||
private bool _disposed;
|
||||
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
|
||||
{
|
||||
@ -67,7 +65,7 @@ namespace Emby.Dlna.PlayTo
|
||||
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
|
||||
}
|
||||
|
||||
private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
@ -76,12 +74,12 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
var info = e.Argument;
|
||||
|
||||
if (!info.Headers.TryGetValue("USN", out string usn))
|
||||
if (!info.Headers.TryGetValue("USN", out string? usn))
|
||||
{
|
||||
usn = string.Empty;
|
||||
}
|
||||
|
||||
if (!info.Headers.TryGetValue("NT", out string nt))
|
||||
if (!info.Headers.TryGetValue("NT", out string? nt))
|
||||
{
|
||||
nt = string.Empty;
|
||||
}
|
||||
@ -161,7 +159,7 @@ namespace Emby.Dlna.PlayTo
|
||||
var uri = info.Location;
|
||||
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
|
||||
|
||||
if (info.Headers.TryGetValue("USN", out string uuid))
|
||||
if (info.Headers.TryGetValue("USN", out string? uuid))
|
||||
{
|
||||
uuid = GetUuid(uuid);
|
||||
}
|
||||
@ -189,7 +187,7 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
|
||||
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress);
|
||||
|
||||
controller = new PlayToController(
|
||||
sessionInfo,
|
||||
|
@ -73,7 +73,11 @@ namespace Emby.Dlna.Ssdp
|
||||
{
|
||||
if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null)
|
||||
{
|
||||
_deviceLocator = new SsdpDeviceLocator(_commsServer);
|
||||
_deviceLocator = new SsdpDeviceLocator(
|
||||
_commsServer,
|
||||
Environment.OSVersion.Platform.ToString(),
|
||||
// Can not use VersionString here since that includes OS and version
|
||||
Environment.OSVersion.Version.ToString());
|
||||
|
||||
// (Optional) Set the filter so we only see notifications for devices we care about
|
||||
// (can be any search target value i.e device type, uuid value etc - any value that appears in the
|
||||
@ -106,7 +110,7 @@ namespace Emby.Dlna.Ssdp
|
||||
{
|
||||
Location = e.DiscoveredDevice.DescriptionLocation,
|
||||
Headers = headers,
|
||||
RemoteIpAddress = e.RemoteIpAddress
|
||||
RemoteIPAddress = e.RemoteIPAddress
|
||||
});
|
||||
|
||||
DeviceDiscoveredInternal?.Invoke(this, args);
|
||||
|
@ -10,7 +10,7 @@ namespace Emby.Naming.Audio
|
||||
/// <summary>
|
||||
/// Helper class to determine if Album is multipart.
|
||||
/// </summary>
|
||||
public class AlbumParser
|
||||
public partial class AlbumParser
|
||||
{
|
||||
private readonly NamingOptions _options;
|
||||
|
||||
@ -23,6 +23,9 @@ namespace Emby.Naming.Audio
|
||||
_options = options;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"[-\.\(\)\s]+")]
|
||||
private static partial Regex CleanRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Function that determines if album is multipart.
|
||||
/// </summary>
|
||||
@ -42,13 +45,9 @@ namespace Emby.Naming.Audio
|
||||
|
||||
// Normalize
|
||||
// Remove whitespace
|
||||
filename = filename.Replace('-', ' ');
|
||||
filename = filename.Replace('.', ' ');
|
||||
filename = filename.Replace('(', ' ');
|
||||
filename = filename.Replace(')', ' ');
|
||||
filename = Regex.Replace(filename, @"\s+", " ");
|
||||
filename = CleanRegex().Replace(filename, " ");
|
||||
|
||||
ReadOnlySpan<char> trimmedFilename = filename.TrimStart();
|
||||
ReadOnlySpan<char> trimmedFilename = filename.AsSpan().TrimStart();
|
||||
|
||||
foreach (var prefix in _options.AlbumStackingPrefixes)
|
||||
{
|
||||
|
@ -318,22 +318,24 @@ namespace Emby.Naming.Common
|
||||
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
|
||||
// <!-- foo.E01., foo.e01. -->
|
||||
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
|
||||
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
|
||||
new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
|
||||
{
|
||||
DateTimeFormats = new[]
|
||||
{
|
||||
"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[]
|
||||
{
|
||||
"dd.MM.yyyy",
|
||||
"dd-MM-yyyy",
|
||||
"dd_MM_yyyy"
|
||||
"dd_MM_yyyy",
|
||||
"dd MM yyyy"
|
||||
}
|
||||
},
|
||||
|
||||
@ -374,7 +376,7 @@ namespace Emby.Naming.Common
|
||||
IsNamed = true,
|
||||
SupportsAbsoluteEpisodeNumbers = false
|
||||
},
|
||||
new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$")
|
||||
new EpisodeExpression(@"[\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\/]*)$")
|
||||
{
|
||||
SupportsAbsoluteEpisodeNumbers = true
|
||||
},
|
||||
@ -415,7 +417,7 @@ namespace Emby.Naming.Common
|
||||
},
|
||||
|
||||
// "1-12 episode title"
|
||||
new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
|
||||
new EpisodeExpression("([0-9]+)-([0-9]+)"),
|
||||
|
||||
// "01 - blah.avi", "01-blah.avi"
|
||||
new EpisodeExpression(@".*(\\|\/)(?<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>[0-9]+)",
|
||||
// Part if often ending of filename
|
||||
@"(?<!ch(?:apter) )(?<part>[0-9]+)$",
|
||||
"(?<!ch(?:apter) )(?<part>[0-9]+)$",
|
||||
// Sometimes named as 0001_005 (chapter_part)
|
||||
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
|
||||
// Some audiobooks are ripped from cd's, and will be named by disk number.
|
||||
|
@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
|
||||
return null;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
|
@ -7,14 +7,15 @@ namespace Emby.Naming.TV
|
||||
/// <summary>
|
||||
/// Used to resolve information about series from path.
|
||||
/// </summary>
|
||||
public static class SeriesResolver
|
||||
public static partial class SeriesResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// Regex that matches strings of at least 2 characters separated by a dot or underscore.
|
||||
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
|
||||
/// preserving namings like "S.H.O.W".
|
||||
/// </summary>
|
||||
private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled);
|
||||
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
|
||||
private static partial Regex SeriesNameRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Resolve information about series from path.
|
||||
@ -37,7 +38,7 @@ namespace Emby.Naming.TV
|
||||
|
||||
if (!string.IsNullOrEmpty(seriesName))
|
||||
{
|
||||
seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
|
||||
seriesName = SeriesNameRegex().Replace(seriesName, "${a} ${b}").Trim();
|
||||
}
|
||||
|
||||
return new SeriesInfo(path)
|
||||
|
@ -26,19 +26,18 @@ namespace Emby.Naming.Video
|
||||
return false;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
|
||||
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
path = Path.GetFileNameWithoutExtension(path);
|
||||
var token = Path.GetExtension(path).TrimStart('.');
|
||||
var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
|
||||
|
||||
foreach (var rule in options.StubTypes)
|
||||
{
|
||||
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
|
||||
if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stubType = rule.StubType;
|
||||
return true;
|
||||
|
@ -12,9 +12,13 @@ namespace Emby.Naming.Video
|
||||
/// <summary>
|
||||
/// Resolves alternative versions and extras from list of video files.
|
||||
/// </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>
|
||||
/// Resolves alternative versions and extras from list of video files.
|
||||
@ -131,7 +135,7 @@ namespace Emby.Naming.Video
|
||||
|
||||
if (videos.Count > 1)
|
||||
{
|
||||
var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
|
||||
var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
|
||||
videos.Clear();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
@ -201,7 +205,7 @@ namespace Emby.Naming.Video
|
||||
// The CleanStringParser should have removed common keywords etc.
|
||||
return testFilename.IsEmpty
|
||||
|| testFilename[0] == '-'
|
||||
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
||||
|| CheckMultiVersionRegex().IsMatch(testFilename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ namespace Emby.Photos
|
||||
item.SetImagePath(ImageType.Primary, item.Path);
|
||||
|
||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
|
||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase
|
||||
/// </summary>
|
||||
public abstract class BaseApplicationPaths : IApplicationPaths
|
||||
{
|
||||
private string _dataPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
|
||||
/// </summary>
|
||||
@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
CachePath = cacheDirectoryPath;
|
||||
WebPath = webDirectoryPath;
|
||||
|
||||
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
/// Gets the folder path to the data directory.
|
||||
/// </summary>
|
||||
/// <value>The data directory.</value>
|
||||
public string DataPath => _dataPath;
|
||||
public string DataPath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string VirtualDataPath => "%AppDataPath%";
|
||||
|
@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
|
||||
/// </summary>
|
||||
public abstract class BaseConfigurationManager : IConfigurationManager
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// The _configuration sync lock.
|
||||
/// </summary>
|
||||
private readonly object _configurationSyncLock = new object();
|
||||
private readonly ConcurrentDictionary<string, object> _configurations = new();
|
||||
private readonly object _configurationSyncLock = new();
|
||||
|
||||
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
||||
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
||||
@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
|
||||
/// <param name="applicationPaths">The application paths.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="xmlSerializer">The XML serializer.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
|
||||
protected BaseConfigurationManager(
|
||||
IApplicationPaths applicationPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IXmlSerializer xmlSerializer)
|
||||
{
|
||||
CommonApplicationPaths = applicationPaths;
|
||||
XmlSerializer = xmlSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
|
||||
|
||||
UpdateCachePath();
|
||||
@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
{
|
||||
var file = Path.Combine(path, Guid.NewGuid().ToString());
|
||||
File.WriteAllText(file, string.Empty);
|
||||
_fileSystem.DeleteFile(file);
|
||||
File.Delete(file);
|
||||
}
|
||||
|
||||
private string GetConfigurationFile(string key)
|
||||
|
@ -12,11 +12,8 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna;
|
||||
using Emby.Dlna.Main;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Photos;
|
||||
using Emby.Server.Implementations.Channels;
|
||||
@ -59,7 +56,6 @@ using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.ClientEvent;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -83,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers;
|
||||
using MediaBrowser.MediaEncoding.BdInfo;
|
||||
using MediaBrowser.MediaEncoding.Subtitles;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
@ -112,7 +107,7 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// Class CompositionRoot.
|
||||
/// </summary>
|
||||
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
|
||||
public abstract class ApplicationHost : IServerApplicationHost, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The disposable parts.
|
||||
@ -120,14 +115,12 @@ namespace Emby.Server.Implementations
|
||||
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
|
||||
private readonly DeviceId _deviceId;
|
||||
|
||||
private readonly IFileSystem _fileSystemManager;
|
||||
private readonly IConfiguration _startupConfig;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IStartupOptions _startupOptions;
|
||||
private readonly IPluginManager _pluginManager;
|
||||
|
||||
private List<Type> _creatingInstances;
|
||||
private ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets all concrete types.
|
||||
@ -135,7 +128,7 @@ namespace Emby.Server.Implementations
|
||||
/// <value>All concrete types.</value>
|
||||
private Type[] _allConcreteTypes;
|
||||
|
||||
private bool _disposed = false;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
||||
@ -154,10 +147,8 @@ namespace Emby.Server.Implementations
|
||||
LoggerFactory = loggerFactory;
|
||||
_startupOptions = options;
|
||||
_startupConfig = startupConfig;
|
||||
_fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
|
||||
|
||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
|
||||
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
|
||||
|
||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||
@ -165,13 +156,15 @@ namespace Emby.Server.Implementations
|
||||
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
||||
|
||||
_xmlSerializer = new MyXmlSerializer();
|
||||
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
|
||||
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer);
|
||||
_pluginManager = new PluginManager(
|
||||
LoggerFactory.CreateLogger<PluginManager>(),
|
||||
this,
|
||||
ConfigurationManager.Configuration,
|
||||
ApplicationPaths.PluginsPath,
|
||||
ApplicationVersion);
|
||||
|
||||
_disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -186,23 +179,16 @@ namespace Emby.Server.Implementations
|
||||
|
||||
public bool CoreStartupHasCompleted { get; private set; }
|
||||
|
||||
public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
|
||||
&& !_startupOptions.IsService
|
||||
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
||||
/// </summary>
|
||||
public INetworkManager NetManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <inheritdoc />
|
||||
public bool HasPendingRestart { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsShuttingDown { get; private set; }
|
||||
public bool ShouldRestart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
@ -406,11 +392,9 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// Runs the startup tasks.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns><see cref="Task" />.</returns>
|
||||
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
|
||||
public async Task RunStartupTasksAsync()
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Logger.LogInformation("Running startup tasks");
|
||||
|
||||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||
@ -424,8 +408,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
var entryPoints = GetExports<IServerEntryPoint>();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
|
||||
@ -435,8 +417,6 @@ namespace Emby.Server.Implementations
|
||||
Logger.LogInformation("Core startup complete");
|
||||
CoreStartupHasCompleted = true;
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
stopWatch.Restart();
|
||||
|
||||
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
||||
@ -466,7 +446,7 @@ namespace Emby.Server.Implementations
|
||||
|
||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||
|
||||
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
||||
NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
|
||||
|
||||
// Initialize runtime stat collection
|
||||
if (ConfigurationManager.Configuration.EnableMetrics)
|
||||
@ -475,8 +455,8 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||
HttpPort = networkConfiguration.HttpServerPortNumber;
|
||||
HttpsPort = networkConfiguration.HttpsPortNumber;
|
||||
HttpPort = networkConfiguration.InternalHttpPort;
|
||||
HttpsPort = networkConfiguration.InternalHttpsPort;
|
||||
|
||||
// Safeguard against invalid configuration
|
||||
if (HttpPort == HttpsPort)
|
||||
@ -509,7 +489,11 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton(_pluginManager);
|
||||
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||
|
||||
serviceCollection.AddSingleton(_fileSystemManager);
|
||||
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
|
||||
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
|
||||
|
||||
serviceCollection.AddScoped<ISystemManager, SystemManager>();
|
||||
|
||||
serviceCollection.AddSingleton<TmdbClientManager>();
|
||||
|
||||
serviceCollection.AddSingleton(NetManager);
|
||||
@ -575,8 +559,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||
@ -588,8 +570,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
||||
|
||||
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||
@ -633,8 +613,6 @@ namespace Emby.Server.Implementations
|
||||
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
|
||||
await localizationManager.LoadAll().ConfigureAwait(false);
|
||||
|
||||
_sessionManager = Resolve<ISessionManager>();
|
||||
|
||||
SetStaticProperties();
|
||||
|
||||
FindParts();
|
||||
@ -685,7 +663,7 @@ namespace Emby.Server.Implementations
|
||||
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||
BaseItem.ItemRepository = Resolve<IItemRepository>();
|
||||
BaseItem.FileSystem = _fileSystemManager;
|
||||
BaseItem.FileSystem = Resolve<IFileSystem>();
|
||||
BaseItem.UserDataManager = Resolve<IUserDataManager>();
|
||||
BaseItem.ChannelManager = Resolve<IChannelManager>();
|
||||
Video.LiveTvManager = Resolve<ILiveTvManager>();
|
||||
@ -785,8 +763,8 @@ namespace Emby.Server.Implementations
|
||||
if (HttpPort != 0 && HttpsPort != 0)
|
||||
{
|
||||
// Need to restart if ports have changed
|
||||
if (networkConfiguration.HttpServerPortNumber != HttpPort
|
||||
|| networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||
if (networkConfiguration.InternalHttpPort != HttpPort
|
||||
|| networkConfiguration.InternalHttpsPort != HttpsPort)
|
||||
{
|
||||
if (ConfigurationManager.Configuration.IsPortAuthorized)
|
||||
{
|
||||
@ -855,38 +833,6 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Gets the composable part assemblies.
|
||||
/// </summary>
|
||||
@ -942,49 +888,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
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/>
|
||||
public string GetSmartApiUrl(IPAddress remoteAddr)
|
||||
{
|
||||
@ -995,18 +898,20 @@ namespace Emby.Server.Implementations
|
||||
return PublishedServerUrl.Trim('/');
|
||||
}
|
||||
|
||||
string smart = NetManager.GetBindInterface(remoteAddr, out var port);
|
||||
string smart = NetManager.GetBindAddress(remoteAddr, out var port);
|
||||
return GetLocalApiUrl(smart.Trim('/'), null, port);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetSmartApiUrl(HttpRequest request)
|
||||
{
|
||||
// Return the host in the HTTP request as the API url
|
||||
// Return the host in the HTTP request as the API URL if not configured otherwise
|
||||
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
|
||||
{
|
||||
int? requestPort = request.Host.Port;
|
||||
if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
||||
if (requestPort is null
|
||||
|| (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
|
||||
|| (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
requestPort = -1;
|
||||
}
|
||||
@ -1027,15 +932,15 @@ namespace Emby.Server.Implementations
|
||||
return PublishedServerUrl.Trim('/');
|
||||
}
|
||||
|
||||
string smart = NetManager.GetBindInterface(hostname, out var port);
|
||||
string smart = NetManager.GetBindAddress(hostname, out var port);
|
||||
return GetLocalApiUrl(smart.Trim('/'), null, port);
|
||||
}
|
||||
|
||||
/// <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
|
||||
var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _);
|
||||
var smart = NetManager.GetBindAddress(ipAddress, out _, false);
|
||||
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
|
||||
int? port = !allowHttps ? HttpPort : null;
|
||||
return GetLocalApiUrl(smart, scheme, port);
|
||||
@ -1063,30 +968,6 @@ namespace Emby.Server.Implementations
|
||||
}.ToString().TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
var assemblies = _allConcreteTypes
|
||||
@ -1150,52 +1031,5 @@ namespace Emby.Server.Implementations
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await DisposeAsyncCore().ConfigureAwait(false);
|
||||
Dispose(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <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));
|
||||
|
||||
await using FileStream createStream = File.Create(path);
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
|
||||
FileStream createStream = File.Create(path);
|
||||
await using (createStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -1156,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
if (info.People is not null && info.People.Count > 0)
|
||||
{
|
||||
_libraryManager.UpdatePeople(item, info.People);
|
||||
await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (forceUpdate)
|
||||
|
@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
|
||||
/// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
|
||||
/// </summary>
|
||||
/// <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="fileSystem">The file system.</param>
|
||||
public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
|
||||
: base(applicationPaths, loggerFactory, xmlSerializer, fileSystem)
|
||||
public ServerConfigurationManager(
|
||||
IApplicationPaths applicationPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IXmlSerializer xmlSerializer)
|
||||
: base(applicationPaths, loggerFactory, xmlSerializer)
|
||||
{
|
||||
UpdateMetadataPath();
|
||||
}
|
||||
|
@ -5,8 +5,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
@ -45,24 +45,6 @@ namespace Emby.Server.Implementations.Data
|
||||
/// <value>The logger.</value>
|
||||
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>
|
||||
/// Gets the cache size.
|
||||
/// </summary>
|
||||
@ -107,23 +89,8 @@ namespace Emby.Server.Implementations.Data
|
||||
/// <see cref="SynchronousMode"/>
|
||||
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()
|
||||
{
|
||||
WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection);
|
||||
ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection);
|
||||
|
||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
@ -131,57 +98,10 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
protected ManagedConnection GetConnection(bool readOnly = false)
|
||||
=> readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
|
||||
|
||||
protected SQLiteDatabaseConnection CreateWriteConnection()
|
||||
protected SqliteConnection GetConnection()
|
||||
{
|
||||
var writeConnection = SQLite3.Open(
|
||||
DbFilePath,
|
||||
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
|
||||
null);
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
||||
{
|
||||
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||
{
|
||||
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
}
|
||||
|
||||
if (JournalSizeLimit.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
||||
}
|
||||
|
||||
if (Synchronous.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
}
|
||||
|
||||
if (PageSize.HasValue)
|
||||
{
|
||||
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
}
|
||||
|
||||
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
return writeConnection;
|
||||
}
|
||||
|
||||
protected SQLiteDatabaseConnection CreateReadConnection()
|
||||
{
|
||||
var connection = SQLite3.Open(
|
||||
DbFilePath,
|
||||
DefaultConnectionFlags | ConnectionFlags.ReadOnly,
|
||||
null);
|
||||
var connection = new SqliteConnection($"Filename={DbFilePath}");
|
||||
connection.Open();
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
@ -208,39 +128,38 @@ namespace Emby.Server.Implementations.Data
|
||||
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
}
|
||||
|
||||
if (PageSize.HasValue)
|
||||
{
|
||||
connection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
}
|
||||
|
||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
public IStatement PrepareStatement(ManagedConnection connection, string sql)
|
||||
=> connection.PrepareStatement(sql);
|
||||
|
||||
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
|
||||
=> connection.PrepareStatement(sql);
|
||||
|
||||
protected bool TableExists(ManagedConnection connection, string name)
|
||||
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
|
||||
{
|
||||
return connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
|
||||
{
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
ReadTransactionMode);
|
||||
var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
return command;
|
||||
}
|
||||
|
||||
protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
|
||||
protected bool TableExists(SqliteConnection connection, string name)
|
||||
{
|
||||
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected List<string> GetColumnNames(SqliteConnection connection, string table)
|
||||
{
|
||||
var columnNames = new List<string>();
|
||||
|
||||
@ -255,7 +174,7 @@ namespace Emby.Server.Implementations.Data
|
||||
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))
|
||||
{
|
||||
@ -291,12 +210,6 @@ namespace Emby.Server.Implementations.Data
|
||||
return;
|
||||
}
|
||||
|
||||
if (dispose)
|
||||
{
|
||||
WriteConnections.Dispose();
|
||||
ReadConnections.Dispose();
|
||||
}
|
||||
|
||||
_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
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using SQLitePCL.pretty;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
@ -52,19 +51,29 @@ namespace Emby.Server.Implementations.Data
|
||||
"yy-MM-dd"
|
||||
};
|
||||
|
||||
public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries)
|
||||
public static IEnumerable<SqliteDataReader> Query(this SqliteConnection sqliteConnection, string commandText)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(queries);
|
||||
|
||||
connection.RunInTransaction(conn =>
|
||||
if (sqliteConnection.State != ConnectionState.Open)
|
||||
{
|
||||
conn.ExecuteAll(string.Join(';', queries));
|
||||
});
|
||||
sqliteConnection.Open();
|
||||
}
|
||||
|
||||
using var command = sqliteConnection.CreateCommand();
|
||||
command.CommandText = commandText;
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
yield return reader;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Guid ReadGuidFromBlob(this ResultSetValue result)
|
||||
public static void Execute(this SqliteConnection sqliteConnection, string commandText)
|
||||
{
|
||||
return new Guid(result.ToBlob());
|
||||
using var command = sqliteConnection.CreateCommand();
|
||||
command.CommandText = commandText;
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public static string ToDateTimeParamValue(this DateTime dateValue)
|
||||
@ -83,27 +92,15 @@ namespace Emby.Server.Implementations.Data
|
||||
private static string GetDateTimeKindFormat(DateTimeKind kind)
|
||||
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
|
||||
|
||||
public static DateTime ReadDateTime(this ResultSetValue result)
|
||||
public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result)
|
||||
{
|
||||
var dateText = result.ToString();
|
||||
|
||||
return DateTime.ParseExact(
|
||||
dateText,
|
||||
_datetimeFormats,
|
||||
DateTimeFormatInfo.InvariantInfo,
|
||||
DateTimeStyles.AdjustToUniversal);
|
||||
}
|
||||
|
||||
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
|
||||
{
|
||||
var item = reader[index];
|
||||
if (item.IsDbNull())
|
||||
if (reader.IsDBNull(index))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var dateText = item.ToString();
|
||||
var dateText = reader.GetString(index);
|
||||
|
||||
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
|
||||
{
|
||||
@ -115,335 +112,145 @@ namespace Emby.Server.Implementations.Data
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
|
||||
public static bool TryGetGuid(this SqliteDataReader reader, int index, out Guid result)
|
||||
{
|
||||
var item = reader[index];
|
||||
if (item.IsDbNull())
|
||||
if (reader.IsDBNull(index))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = item.ReadGuidFromBlob();
|
||||
result = reader.GetGuid(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool IsDbNull(this ResultSetValue result)
|
||||
public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
|
||||
{
|
||||
return result.SQLiteType == SQLiteType.Null;
|
||||
}
|
||||
result = string.Empty;
|
||||
|
||||
public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
|
||||
{
|
||||
return result[index].ToString();
|
||||
}
|
||||
|
||||
public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
|
||||
{
|
||||
result = null;
|
||||
var item = reader[index];
|
||||
if (item.IsDbNull())
|
||||
if (reader.IsDBNull(index))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = item.ToString();
|
||||
result = reader.GetString(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool GetBoolean(this IReadOnlyList<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 (item.IsDbNull())
|
||||
if (reader.IsDBNull(index))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = item.ToBool();
|
||||
result = reader.GetBoolean(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
|
||||
public static bool TryGetInt32(this SqliteDataReader reader, int index, out int result)
|
||||
{
|
||||
var item = reader[index];
|
||||
if (item.IsDbNull())
|
||||
if (reader.IsDBNull(index))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = item.ToInt();
|
||||
result = reader.GetInt32(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
|
||||
public static bool TryGetInt64(this SqliteDataReader reader, int index, out long result)
|
||||
{
|
||||
return result[index].ToInt64();
|
||||
}
|
||||
|
||||
public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
|
||||
{
|
||||
var item = reader[index];
|
||||
if (item.IsDbNull())
|
||||
if (reader.IsDBNull(index))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = item.ToInt64();
|
||||
result = reader.GetInt64(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
|
||||
public static bool TryGetSingle(this SqliteDataReader reader, int index, out float result)
|
||||
{
|
||||
var item = reader[index];
|
||||
if (item.IsDbNull())
|
||||
if (reader.IsDBNull(index))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = item.ToFloat();
|
||||
result = reader.GetFloat(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
|
||||
public static bool TryGetDouble(this SqliteDataReader reader, int index, out double result)
|
||||
{
|
||||
var item = reader[index];
|
||||
if (item.IsDbNull())
|
||||
if (reader.IsDBNull(index))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = item.ToDouble();
|
||||
result = reader.GetDouble(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Guid GetGuid(this IReadOnlyList<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")]
|
||||
private static void CheckName(string name)
|
||||
public static void TryBind(this SqliteCommand statement, string name, object? value, bool isBlob = false)
|
||||
{
|
||||
throw new ArgumentException("Invalid param name: " + name, nameof(name));
|
||||
}
|
||||
|
||||
public static void TryBind(this IStatement statement, string name, double value)
|
||||
{
|
||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
||||
var preparedValue = value ?? DBNull.Value;
|
||||
if (statement.Parameters.Contains(name))
|
||||
{
|
||||
bindParam.Bind(value);
|
||||
statement.Parameters[name].Value = preparedValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckName(name);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TryBind(this IStatement statement, string name, string value)
|
||||
{
|
||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
||||
{
|
||||
if (value is null)
|
||||
// Blobs aren't always detected automatically
|
||||
if (isBlob)
|
||||
{
|
||||
bindParam.BindNull();
|
||||
statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value });
|
||||
}
|
||||
else
|
||||
{
|
||||
bindParam.Bind(value);
|
||||
statement.Parameters.AddWithValue(name, preparedValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
public static void TryBindNull(this SqliteCommand statement, string name)
|
||||
{
|
||||
statement.TryBind(name, DBNull.Value);
|
||||
}
|
||||
|
||||
public static IEnumerable<SqliteDataReader> ExecuteQuery(this SqliteCommand command)
|
||||
{
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
CheckName(name);
|
||||
while (reader.Read())
|
||||
{
|
||||
yield return reader;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void TryBind(this IStatement statement, string name, bool value)
|
||||
public static int SelectScalarInt(this SqliteCommand command)
|
||||
{
|
||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
||||
{
|
||||
bindParam.Bind(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckName(name);
|
||||
}
|
||||
var result = command.ExecuteScalar();
|
||||
// Can't be null since the method is used to retrieve Count
|
||||
return Convert.ToInt32(result!, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static void TryBind(this IStatement statement, string name, float value)
|
||||
public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql)
|
||||
{
|
||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
||||
{
|
||||
bindParam.Bind(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckName(name);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TryBind(this IStatement statement, string name, int value)
|
||||
{
|
||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
||||
{
|
||||
bindParam.Bind(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
CheckName(name);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TryBind(this IStatement statement, string name, Guid value)
|
||||
{
|
||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
||||
{
|
||||
Span<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)
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
TryBind(statement, name, value.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
TryBindNull(statement, name);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TryBind(this IStatement statement, string name, float? value)
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
TryBind(statement, name, value.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
TryBindNull(statement, name);
|
||||
}
|
||||
}
|
||||
|
||||
public static void TryBind(this IStatement statement, string name, bool? value)
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
TryBind(statement, name, value.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
TryBindNull(statement, name);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
|
||||
{
|
||||
while (statement.MoveNext())
|
||||
{
|
||||
yield return statement.Current;
|
||||
}
|
||||
var command = sqliteConnection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
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.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
@ -44,48 +44,48 @@ namespace Emby.Server.Implementations.Data
|
||||
var userDataTableExists = TableExists(connection, "userdata");
|
||||
|
||||
var users = userDatasTableExists ? null : _userManager.Users;
|
||||
using var transaction = connection.BeginTransaction();
|
||||
connection.Execute(string.Join(
|
||||
';',
|
||||
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
||||
"drop index if exists idx_userdata",
|
||||
"drop index if exists idx_userdata1",
|
||||
"drop index if exists idx_userdata2",
|
||||
"drop index if exists userdataindex1",
|
||||
"drop index if exists userdataindex",
|
||||
"drop index if exists userdataindex3",
|
||||
"drop index if exists userdataindex4",
|
||||
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
|
||||
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
|
||||
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
|
||||
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"));
|
||||
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
db.ExecuteAll(string.Join(';', new[]
|
||||
{
|
||||
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
||||
if (!userDataTableExists)
|
||||
{
|
||||
transaction.Commit();
|
||||
return;
|
||||
}
|
||||
|
||||
"drop index if exists idx_userdata",
|
||||
"drop index if exists idx_userdata1",
|
||||
"drop index if exists idx_userdata2",
|
||||
"drop index if exists userdataindex1",
|
||||
"drop index if exists userdataindex",
|
||||
"drop index if exists userdataindex3",
|
||||
"drop index if exists userdataindex4",
|
||||
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
|
||||
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
|
||||
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
|
||||
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
|
||||
}));
|
||||
var existingColumnNames = GetColumnNames(connection, "userdata");
|
||||
|
||||
if (userDataTableExists)
|
||||
{
|
||||
var existingColumnNames = GetColumnNames(db, "userdata");
|
||||
AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
|
||||
AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||
AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||
|
||||
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||
if (userDatasTableExists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userDatasTableExists)
|
||||
{
|
||||
ImportUserIds(db, users);
|
||||
ImportUserIds(connection, users);
|
||||
|
||||
db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
|
||||
}
|
||||
}
|
||||
},
|
||||
TransactionMode);
|
||||
connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users)
|
||||
private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
|
||||
{
|
||||
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
||||
|
||||
@ -101,13 +101,12 @@ namespace Emby.Server.Implementations.Data
|
||||
statement.TryBind("@UserId", user.Id);
|
||||
statement.TryBind("@InternalUserId", user.InternalId);
|
||||
|
||||
statement.MoveNext();
|
||||
statement.Reset();
|
||||
statement.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db)
|
||||
private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
|
||||
{
|
||||
var list = new List<Guid>();
|
||||
|
||||
@ -117,7 +116,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
try
|
||||
{
|
||||
list.Add(row[0].ReadGuidFromBlob());
|
||||
list.Add(row.GetGuid(0));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -169,17 +168,14 @@ namespace Emby.Server.Implementations.Data
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
{
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
SaveUserData(db, internalUserId, key, userData);
|
||||
},
|
||||
TransactionMode);
|
||||
SaveUserData(connection, internalUserId, key, userData);
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData)
|
||||
private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
|
||||
{
|
||||
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
||||
{
|
||||
@ -227,7 +223,7 @@ namespace Emby.Server.Implementations.Data
|
||||
statement.TryBindNull("@SubtitleStreamIndex");
|
||||
}
|
||||
|
||||
statement.MoveNext();
|
||||
statement.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
@ -239,16 +235,14 @@ namespace Emby.Server.Implementations.Data
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
using (var transaction = connection.BeginTransaction())
|
||||
{
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
foreach (var userItemData in userDataList)
|
||||
{
|
||||
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
|
||||
}
|
||||
},
|
||||
TransactionMode);
|
||||
foreach (var userItemData in userDataList)
|
||||
{
|
||||
SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
||||
{
|
||||
@ -336,7 +330,7 @@ namespace Emby.Server.Implementations.Data
|
||||
/// </summary>
|
||||
/// <param name="reader">The list of result set values.</param>
|
||||
/// <returns>The user item data.</returns>
|
||||
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
|
||||
private UserItemData ReadRow(SqliteDataReader reader)
|
||||
{
|
||||
var userData = new UserItemData();
|
||||
|
||||
@ -348,10 +342,10 @@ namespace Emby.Server.Implementations.Data
|
||||
userData.Rating = rating;
|
||||
}
|
||||
|
||||
userData.Played = reader[3].ToBool();
|
||||
userData.PlayCount = reader[4].ToInt();
|
||||
userData.IsFavorite = reader[5].ToBool();
|
||||
userData.PlaybackPositionTicks = reader[6].ToInt64();
|
||||
userData.Played = reader.GetBoolean(3);
|
||||
userData.PlayCount = reader.GetInt32(4);
|
||||
userData.IsFavorite = reader.GetBoolean(5);
|
||||
userData.PlaybackPositionTicks = reader.GetInt64(6);
|
||||
|
||||
if (reader.TryReadDateTime(7, out var lastPlayedDate))
|
||||
{
|
||||
|
@ -907,10 +907,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
|
||||
}
|
||||
|
||||
dto.LUFS = item.LUFS;
|
||||
|
||||
// Add audio info
|
||||
if (item is Audio audio)
|
||||
{
|
||||
dto.LUFS = audio.LUFS;
|
||||
dto.Album = audio.Album;
|
||||
if (audio.ExtraType.HasValue)
|
||||
{
|
||||
|
@ -24,6 +24,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DiscUtils.Udf" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
@ -31,7 +32,6 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||
<PackageReference Include="Mono.Nat" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" />
|
||||
<PackageReference Include="DotNet.Glob" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -43,8 +43,6 @@
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
||||
<NoWarn>AD0001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
|
@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
return new StringBuilder(32)
|
||||
.Append(config.EnableUPnP).Append(Separator)
|
||||
.Append(config.PublicPort).Append(Separator)
|
||||
.Append(config.PublicHttpPort).Append(Separator)
|
||||
.Append(config.PublicHttpsPort).Append(Separator)
|
||||
.Append(_appHost.HttpPort).Append(Separator)
|
||||
.Append(_appHost.HttpsPort).Append(Separator)
|
||||
@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
private IEnumerable<Task> CreatePortMaps(INatDevice device)
|
||||
{
|
||||
var config = _config.GetNetworkConfiguration();
|
||||
yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort);
|
||||
yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
|
||||
|
||||
if (_appHost.ListenWithHttps)
|
||||
{
|
||||
|
@ -1,10 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Udp;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using Jellyfin.Networking.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@ -13,7 +18,7 @@ using Microsoft.Extensions.Logging;
|
||||
namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Class UdpServerEntryPoint.
|
||||
/// Class responsible for registering all UDP broadcast endpoints and their handlers.
|
||||
/// </summary>
|
||||
public sealed class UdpServerEntryPoint : IServerEntryPoint
|
||||
{
|
||||
@ -29,13 +34,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
private readonly INetworkManager _networkManager;
|
||||
|
||||
/// <summary>
|
||||
/// The UDP server.
|
||||
/// </summary>
|
||||
private UdpServer? _udpServer;
|
||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private bool _disposed = false;
|
||||
private readonly List<UdpServer> _udpServers;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 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="configuration">Instance of the <see cref="IConfiguration"/> 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(
|
||||
ILogger<UdpServerEntryPoint> logger,
|
||||
IServerApplicationHost appHost,
|
||||
IConfiguration configuration,
|
||||
IConfigurationManager configurationManager)
|
||||
IConfigurationManager configurationManager,
|
||||
INetworkManager networkManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_config = configuration;
|
||||
_configurationManager = configurationManager;
|
||||
_networkManager = networkManager;
|
||||
_udpServers = new List<UdpServer>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -68,8 +78,43 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
try
|
||||
{
|
||||
_udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
|
||||
_udpServer.Start(_cancellationTokenSource.Token);
|
||||
// Linux needs to bind to the broadcast addresses to get broadcast traffic
|
||||
// Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
// Add global broadcast listener
|
||||
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
|
||||
server.Start(_cancellationTokenSource.Token);
|
||||
_udpServers.Add(server);
|
||||
|
||||
// Add bind address specific broadcast listeners
|
||||
// IPv6 is currently unsupported
|
||||
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
|
||||
foreach (var intf in validInterfaces)
|
||||
{
|
||||
var broadcastAddress = NetworkExtensions.GetBroadcastAddress(intf.Subnet);
|
||||
_logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber);
|
||||
|
||||
server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber);
|
||||
server.Start(_cancellationTokenSource.Token);
|
||||
_udpServers.Add(server);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add bind address specific broadcast listeners
|
||||
// IPv6 is currently unsupported
|
||||
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
|
||||
foreach (var intf in validInterfaces)
|
||||
{
|
||||
var intfAddress = intf.Address;
|
||||
_logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
|
||||
|
||||
var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
|
||||
server.Start(_cancellationTokenSource.Token);
|
||||
_udpServers.Add(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
@ -83,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(this.GetType().Name);
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,9 +142,12 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource.Dispose();
|
||||
_udpServer?.Dispose();
|
||||
_udpServer = null;
|
||||
foreach (var server in _udpServers)
|
||||
{
|
||||
server.Dispose();
|
||||
}
|
||||
|
||||
_udpServers.Clear();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -42,14 +43,17 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="socket">The socket.</param>
|
||||
/// <param name="authorizationInfo">The authorization information.</param>
|
||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||
public WebSocketConnection(
|
||||
ILogger<WebSocketConnection> logger,
|
||||
WebSocket socket,
|
||||
AuthorizationInfo authorizationInfo,
|
||||
IPAddress? remoteEndPoint)
|
||||
{
|
||||
_logger = logger;
|
||||
_socket = socket;
|
||||
AuthorizationInfo = authorizationInfo;
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
|
||||
_jsonOptions = JsonDefaults.Options;
|
||||
@ -59,47 +63,40 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<EventArgs>? Closed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote end point.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public AuthorizationInfo AuthorizationInfo { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPAddress? RemoteEndPoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the receive action.
|
||||
/// </summary>
|
||||
/// <value>The receive action.</value>
|
||||
/// <inheritdoc />
|
||||
public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the last activity date.
|
||||
/// </summary>
|
||||
/// <value>The last activity date.</value>
|
||||
/// <inheritdoc />
|
||||
public DateTime LastActivityDate { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime LastKeepAliveDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the state.
|
||||
/// </summary>
|
||||
/// <value>The state.</value>
|
||||
/// <inheritdoc />
|
||||
public WebSocketState State => _socket.State;
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message asynchronously.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the message.</typeparam>
|
||||
/// <param name="message">The message.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||
/// <inheritdoc />
|
||||
public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
||||
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ProcessAsync(CancellationToken cancellationToken = default)
|
||||
public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
|
||||
{
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
|
||||
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ReceiveAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pipe = new Pipe();
|
||||
var writer = pipe.Writer;
|
||||
@ -171,7 +168,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return;
|
||||
}
|
||||
|
||||
WebSocketMessage<object>? stub;
|
||||
InboundWebSocketMessage<object>? stub;
|
||||
long bytesConsumed;
|
||||
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 ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions);
|
||||
var ret = JsonSerializer.Deserialize<InboundWebSocketMessage<object>>(ref jsonReader, _jsonOptions);
|
||||
bytesConsumed = jsonReader.BytesConsumed;
|
||||
return ret;
|
||||
}
|
||||
@ -224,11 +221,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
LastKeepAliveDate = DateTime.UtcNow;
|
||||
return SendAsync(
|
||||
new WebSocketMessage<string>
|
||||
{
|
||||
MessageId = Guid.NewGuid(),
|
||||
MessageType = SessionMessageType.KeepAlive
|
||||
},
|
||||
new OutboundKeepAliveMessage(),
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
using var connection = new WebSocketConnection(
|
||||
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||
webSocket,
|
||||
context.GetNormalizedRemoteIp())
|
||||
authorizationInfo,
|
||||
context.GetNormalizedRemoteIP())
|
||||
{
|
||||
OnReceive = ProcessWebSocketMessageReceived
|
||||
};
|
||||
@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
await connection.ProcessAsync().ConfigureAwait(false);
|
||||
await connection.ReceiveAsync().ConfigureAwait(false);
|
||||
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
|
||||
}
|
||||
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)
|
||||
{
|
||||
@ -148,13 +148,6 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
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)
|
||||
{
|
||||
_logger.LogError(ex, "Error refreshing {Name}", item.Name);
|
||||
@ -217,7 +210,6 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
DisposeTimer();
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@ -160,7 +158,7 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</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)
|
||||
{
|
||||
@ -173,7 +171,7 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</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)
|
||||
{
|
||||
@ -189,19 +187,28 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <param name="path">The path.</param>
|
||||
/// <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>
|
||||
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);
|
||||
|
||||
return lst.Any(str =>
|
||||
foreach (var str in lst)
|
||||
{
|
||||
// this should be a little quicker than examining each actual parent folder...
|
||||
var compare = str.TrimEnd(Path.DirectorySeparatorChar);
|
||||
var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
|
||||
});
|
||||
if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
|
||||
|| (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -349,21 +356,19 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
|
||||
var monitorPath = !IgnorePatterns.ShouldIgnore(path);
|
||||
if (IgnorePatterns.ShouldIgnore(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
|
||||
if (_tempIgnoredPaths.Keys.Any(i =>
|
||||
foreach (var i in _tempIgnoredPaths.Keys)
|
||||
{
|
||||
if (_fileSystem.AreEqual(i, path))
|
||||
if (_fileSystem.AreEqual(i, path)
|
||||
|| _fileSystem.ContainsSubPath(i, path))
|
||||
{
|
||||
_logger.LogDebug("Ignoring change to {Path}", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_fileSystem.ContainsSubPath(i, path))
|
||||
{
|
||||
_logger.LogDebug("Ignoring change to {Path}", path);
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Go up a level
|
||||
@ -371,20 +376,11 @@ namespace Emby.Server.Implementations.IO
|
||||
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
|
||||
{
|
||||
_logger.LogDebug("Ignoring change to {Path}", path);
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
return false;
|
||||
}))
|
||||
{
|
||||
monitorPath = false;
|
||||
}
|
||||
|
||||
if (monitorPath)
|
||||
{
|
||||
// Avoid implicitly captured closure
|
||||
CreateRefresher(path);
|
||||
}
|
||||
CreateRefresher(path);
|
||||
}
|
||||
|
||||
private void CreateRefresher(string path)
|
||||
@ -417,7 +413,8 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
|
||||
// They are siblings. Rebase the refresher to the parent folder.
|
||||
if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal))
|
||||
if (parentPath is not null
|
||||
&& Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
|
||||
{
|
||||
refresher.ResetPath(parentPath, path);
|
||||
return;
|
||||
@ -430,8 +427,13 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNewRefresherCompleted(object sender, EventArgs e)
|
||||
private void OnNewRefresherCompleted(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var refresher = (FileRefresher)sender;
|
||||
DisposeRefresher(refresher);
|
||||
}
|
||||
|
@ -15,29 +15,34 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
public class ManagedFileSystem : IFileSystem
|
||||
{
|
||||
private readonly ILogger<ManagedFileSystem> _logger;
|
||||
|
||||
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
|
||||
private readonly string _tempPath;
|
||||
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
|
||||
private static readonly char[] _invalidPathCharacters =
|
||||
{
|
||||
'\"', '<', '>', '|', '\0',
|
||||
(char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
|
||||
(char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
|
||||
(char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
|
||||
(char)31, ':', '*', '?', '\\', '/'
|
||||
};
|
||||
|
||||
private readonly ILogger<ManagedFileSystem> _logger;
|
||||
private readonly List<IShortcutHandler> _shortcutHandlers;
|
||||
private readonly string _tempPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
|
||||
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
|
||||
/// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param>
|
||||
public ManagedFileSystem(
|
||||
ILogger<ManagedFileSystem> logger,
|
||||
IApplicationPaths applicationPaths)
|
||||
IApplicationPaths applicationPaths,
|
||||
IEnumerable<IShortcutHandler> shortcutHandlers)
|
||||
{
|
||||
_logger = logger;
|
||||
_tempPath = applicationPaths.TempDirectory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void AddShortcutHandler(IShortcutHandler handler)
|
||||
{
|
||||
_shortcutHandlers.Add(handler);
|
||||
_shortcutHandlers = shortcutHandlers.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -86,7 +91,7 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
|
||||
// unc path
|
||||
if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
|
||||
if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
return filePath;
|
||||
}
|
||||
@ -98,15 +103,17 @@ namespace Emby.Server.Implementations.IO
|
||||
return filePath;
|
||||
}
|
||||
|
||||
var filePathSpan = filePath.AsSpan();
|
||||
|
||||
// relative path
|
||||
if (firstChar == '\\')
|
||||
{
|
||||
filePath = filePath.Substring(1);
|
||||
filePathSpan = filePathSpan.Slice(1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(folderPath, filePath));
|
||||
return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
@ -275,8 +282,7 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <exception cref="ArgumentNullException">The filename is null.</exception>
|
||||
public string GetValidFilename(string filename)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
var first = filename.IndexOfAny(invalid);
|
||||
var first = filename.IndexOfAny(_invalidPathCharacters);
|
||||
if (first == -1)
|
||||
{
|
||||
// Fast path for clean strings
|
||||
@ -285,7 +291,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
return string.Create(
|
||||
filename.Length,
|
||||
(filename, invalid, first),
|
||||
(filename, _invalidPathCharacters, first),
|
||||
(chars, state) =>
|
||||
{
|
||||
state.filename.AsSpan().CopyTo(chars);
|
||||
@ -293,7 +299,7 @@ namespace Emby.Server.Implementations.IO
|
||||
chars[state.first++] = ' ';
|
||||
|
||||
var len = chars.Length;
|
||||
foreach (var c in state.invalid)
|
||||
foreach (var c in state._invalidPathCharacters)
|
||||
{
|
||||
for (int i = state.first; i < len; i++)
|
||||
{
|
||||
@ -478,25 +484,11 @@ namespace Emby.Server.Implementations.IO
|
||||
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string NormalizePath(string path)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
|
||||
if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
return Path.TrimEndingDirectorySeparator(path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool AreEqual(string path1, string path2)
|
||||
{
|
||||
return string.Equals(
|
||||
NormalizePath(path1),
|
||||
NormalizePath(path2),
|
||||
return Path.TrimEndingDirectorySeparator(path1).Equals(
|
||||
Path.TrimEndingDirectorySeparator(path2),
|
||||
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
@ -8,24 +8,17 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
public class MbLinkShortcutHandler : IShortcutHandler
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public MbLinkShortcutHandler(IFileSystem fileSystem)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public string Extension => ".mblink";
|
||||
|
||||
public string? Resolve(string shortcutPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(shortcutPath);
|
||||
|
||||
if (string.Equals(Path.GetExtension(shortcutPath), ".mblink", StringComparison.OrdinalIgnoreCase))
|
||||
if (Path.GetExtension(shortcutPath.AsSpan()).Equals(".mblink", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var path = File.ReadAllText(shortcutPath);
|
||||
|
||||
return _fileSystem.NormalizePath(path);
|
||||
return Path.TrimEndingDirectorySeparator(path);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -31,6 +31,7 @@ namespace Emby.Server.Implementations.Images
|
||||
return _libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Parent = item,
|
||||
Recursive = true,
|
||||
DtoOptions = new DtoOptions(true),
|
||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||
OrderBy = new (string, SortOrder)[]
|
||||
|
@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
|
||||
// bts sync files
|
||||
"**/*.bts",
|
||||
"**/*.sync",
|
||||
|
||||
// zfs
|
||||
"**/.zfs/**",
|
||||
"**/.zfs"
|
||||
};
|
||||
|
||||
private static readonly GlobOptions _globOptions = new GlobOptions
|
||||
|
@ -3,6 +3,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@ -45,7 +46,6 @@ using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
|
||||
@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library
|
||||
private const string ShortcutFileExtension = ".mblink";
|
||||
|
||||
private readonly ILogger<LibraryManager> _logger;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataRepository;
|
||||
@ -111,7 +111,6 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <param name="mediaEncoder">The media encoder.</param>
|
||||
/// <param name="itemRepository">The item repository.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
/// <param name="namingOptions">The naming options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
public LibraryManager(
|
||||
@ -128,7 +127,6 @@ namespace Emby.Server.Implementations.Library
|
||||
IMediaEncoder mediaEncoder,
|
||||
IItemRepository itemRepository,
|
||||
IImageProcessor imageProcessor,
|
||||
IMemoryCache memoryCache,
|
||||
NamingOptions namingOptions,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
@ -145,7 +143,7 @@ namespace Emby.Server.Implementations.Library
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_itemRepository = itemRepository;
|
||||
_imageProcessor = imageProcessor;
|
||||
_memoryCache = memoryCache;
|
||||
_cache = new ConcurrentDictionary<Guid, BaseItem>();
|
||||
_namingOptions = namingOptions;
|
||||
|
||||
_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)
|
||||
@ -359,7 +357,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
var children = item.IsFolder
|
||||
? ((Folder)item).GetRecursiveChildren(false)
|
||||
: Enumerable.Empty<BaseItem>();
|
||||
: Array.Empty<BaseItem>();
|
||||
|
||||
foreach (var metadataPath in GetMetadataPaths(item, children))
|
||||
{
|
||||
@ -441,7 +439,7 @@ namespace Emby.Server.Implementations.Library
|
||||
_itemRepository.DeleteItem(child.Id);
|
||||
}
|
||||
|
||||
_memoryCache.Remove(item.Id);
|
||||
_cache.TryRemove(item.Id, out _);
|
||||
|
||||
ReportItemRemoved(item, parent);
|
||||
}
|
||||
@ -609,7 +607,7 @@ namespace Emby.Server.Implementations.Library
|
||||
var originalList = paths.ToList();
|
||||
|
||||
var list = originalList.Where(i => i.IsDirectory)
|
||||
.Select(i => _fileSystem.NormalizePath(i.FullName))
|
||||
.Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
@ -840,19 +838,12 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var path = Person.GetPath(name);
|
||||
var id = GetItemByNameId<Person>(path);
|
||||
if (GetItemById(id) is not Person item)
|
||||
if (GetItemById(id) is Person item)
|
||||
{
|
||||
item = new Person
|
||||
{
|
||||
Name = name,
|
||||
Id = id,
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
Path = path
|
||||
};
|
||||
return item;
|
||||
}
|
||||
|
||||
return item;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1163,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
|
||||
Name = Path.GetFileName(dir),
|
||||
|
||||
Locations = _fileSystem.GetFilePaths(dir, false)
|
||||
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
||||
.Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
@ -1233,7 +1224,7 @@ namespace Emby.Server.Implementations.Library
|
||||
throw new ArgumentException("Guid can't be empty", nameof(id));
|
||||
}
|
||||
|
||||
if (_memoryCache.TryGetValue(id, out BaseItem item))
|
||||
if (_cache.TryGetValue(id, out BaseItem item))
|
||||
{
|
||||
return item;
|
||||
}
|
||||
@ -2069,7 +2060,9 @@ namespace Emby.Server.Implementations.Library
|
||||
.Find(folder => folder is CollectionFolder) as CollectionFolder;
|
||||
}
|
||||
|
||||
return collectionFolder is null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
|
||||
return collectionFolder is null
|
||||
? new LibraryOptions()
|
||||
: collectionFolder.GetLibraryOptions();
|
||||
}
|
||||
|
||||
public string GetContentType(BaseItem item)
|
||||
@ -2857,7 +2850,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
|
||||
|
||||
File.WriteAllBytes(path, Array.Empty<byte>());
|
||||
await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
|
||||
@ -2899,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
|
||||
var saveEntity = false;
|
||||
var personEntity = GetPerson(person.Name);
|
||||
|
||||
// if PresentationUniqueKey is empty it's likely a new item.
|
||||
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
|
||||
if (personEntity is null)
|
||||
{
|
||||
var path = Person.GetPath(person.Name);
|
||||
personEntity = new Person()
|
||||
{
|
||||
Name = person.Name,
|
||||
Id = GetItemByNameId<Person>(path),
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
Path = path
|
||||
};
|
||||
|
||||
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||
saveEntity = true;
|
||||
}
|
||||
@ -3134,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
|
||||
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
||||
.Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(shortcut))
|
||||
|
@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
{
|
||||
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
|
||||
try
|
||||
{
|
||||
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
|
||||
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// _logger.LogDebug("Found cached media info");
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deserializing mediainfo cache");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await jsonStream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library
|
||||
if (cacheFilePath is not null)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
||||
await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
|
||||
await using (createStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
|
||||
_logger.LogDebug("Saved media info to {0}", cacheFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -625,17 +625,19 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
{
|
||||
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
|
||||
try
|
||||
{
|
||||
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
|
||||
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// _logger.LogDebug("Found cached media info");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await jsonStream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaInfo is null)
|
||||
@ -664,8 +666,11 @@ namespace Emby.Server.Implementations.Library
|
||||
if (cacheFilePath is not null)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
||||
await using FileStream createStream = File.Create(cacheFilePath);
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
FileStream createStream = File.Create(cacheFilePath);
|
||||
await using (createStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
|
||||
}
|
||||
|
@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
|
||||
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
|
||||
{
|
||||
var extension = Path.GetExtension(args.Path);
|
||||
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||
|
||||
if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase))
|
||||
if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// if audio file exists of same name, return null
|
||||
return null;
|
||||
@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
|
||||
if (item is not null)
|
||||
{
|
||||
item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
|
||||
item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
item.IsInMixedFolder = true;
|
||||
}
|
||||
|
@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
return false;
|
||||
}
|
||||
|
||||
return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
|
||||
return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
return GetBook(args);
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(args.Path);
|
||||
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||
|
||||
if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// It's a book
|
||||
return new Book
|
||||
@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
{
|
||||
var bookFiles = args.FileSystemChildren.Where(f =>
|
||||
{
|
||||
var fileExtension = Path.GetExtension(f.FullName)
|
||||
?? string.Empty;
|
||||
var fileExtension = Path.GetExtension(f.FullName.AsSpan());
|
||||
|
||||
return _validExtensions.Contains(
|
||||
fileExtension,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}).ToList();
|
||||
|
||||
// Don't return a Book if there is more (or less) than one document in the directory
|
||||
|
@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
/// <summary>
|
||||
/// Class MovieResolver.
|
||||
/// </summary>
|
||||
public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
|
||||
public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
@ -56,6 +56,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.Fourth;
|
||||
|
||||
[GeneratedRegex(@"\bsample\b", RegexOptions.IgnoreCase)]
|
||||
private static partial Regex IsIgnoredRegex();
|
||||
|
||||
/// <inheritdoc />
|
||||
public MultiItemResolverResult ResolveMultiple(
|
||||
Folder parent,
|
||||
@ -261,7 +264,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
{
|
||||
leftOver.Add(child);
|
||||
}
|
||||
else if (!IsIgnored(child.Name))
|
||||
else if (!IsIgnoredRegex().IsMatch(child.Name))
|
||||
{
|
||||
files.Add(child);
|
||||
}
|
||||
@ -314,9 +317,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
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)
|
||||
{
|
||||
for (var i = 0; i < result.Count; i++)
|
||||
|
@ -1,7 +1,4 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
@ -25,7 +22,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
private static readonly string[] _ignoreFiles = new[]
|
||||
{
|
||||
"folder",
|
||||
"thumb",
|
||||
@ -56,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Trailer.</returns>
|
||||
protected override Photo Resolve(ItemResolveArgs args)
|
||||
protected override Photo? Resolve(ItemResolveArgs args)
|
||||
{
|
||||
if (!args.IsDirectory)
|
||||
{
|
||||
@ -68,10 +65,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
if (IsImageFile(args.Path, _imageProcessor))
|
||||
{
|
||||
var filename = Path.GetFileNameWithoutExtension(args.Path);
|
||||
var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
|
||||
|
||||
// Make sure the image doesn't belong to a video file
|
||||
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
|
||||
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
|
||||
?? throw new InvalidOperationException("Path can't be a root directory."));
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
@ -92,32 +90,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
|
||||
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> 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);
|
||||
|
||||
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(path);
|
||||
|
||||
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
|
||||
if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var filename = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
if (_ignoreFiles.Contains(filename))
|
||||
if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string extension = Path.GetExtension(path).TrimStart('.');
|
||||
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
var resolver = new Naming.TV.EpisodeResolver(namingOptions);
|
||||
|
||||
var folderName = System.IO.Path.GetFileName(path);
|
||||
var testPath = "\\\\test\\" + folderName;
|
||||
var testPath = @"\\test\" + folderName;
|
||||
|
||||
var episodeInfo = resolver.Resolve(testPath, true);
|
||||
|
||||
|
@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
return;
|
||||
}
|
||||
|
||||
await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
Async = true
|
||||
};
|
||||
|
||||
await using (var writer = XmlWriter.Create(stream, settings))
|
||||
var writer = XmlWriter.Create(stream, settings);
|
||||
await using (writer.ConfigureAwait(false))
|
||||
{
|
||||
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
|
||||
await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
|
||||
@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
return;
|
||||
}
|
||||
|
||||
await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
|
||||
var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
var isSeriesEpisode = timer.IsProgramSeries;
|
||||
|
||||
await using (var writer = XmlWriter.Create(stream, settings))
|
||||
var writer = XmlWriter.Create(stream, settings);
|
||||
await using (writer.ConfigureAwait(false))
|
||||
{
|
||||
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
|
||||
|
||||
@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
else
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "movie", null);
|
||||
await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.Name))
|
||||
{
|
||||
|
@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
options.Content = JsonContent.Create(requestList, options: _jsonOptions);
|
||||
options.Headers.TryAddWithoutValidation("token", token);
|
||||
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (dailySchedules is null)
|
||||
{
|
||||
return Array.Empty<ProgramInfo>();
|
||||
@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
|
||||
|
||||
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (programDetails is null)
|
||||
{
|
||||
return Array.Empty<ProgramInfo>();
|
||||
@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
try
|
||||
{
|
||||
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
try
|
||||
{
|
||||
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (root is not null)
|
||||
{
|
||||
foreach (HeadendsDto headend in root)
|
||||
@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
|
||||
@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||
httpResponse.EnsureSuccessStatusCode();
|
||||
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var response = httpResponse.Content;
|
||||
var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
options.Headers.TryAddWithoutValidation("token", token);
|
||||
|
||||
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (root is null)
|
||||
{
|
||||
return new List<ChannelInfo>();
|
||||
|
@ -3,6 +3,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -16,21 +17,20 @@ using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public abstract class BaseTunerHost
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly ConcurrentDictionary<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;
|
||||
Logger = logger;
|
||||
_memoryCache = memoryCache;
|
||||
FileSystem = fileSystem;
|
||||
_cache = new ConcurrentDictionary<string, List<ChannelInfo>>();
|
||||
}
|
||||
|
||||
protected IServerConfigurationManager Config { get; }
|
||||
@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
var key = tuner.Id;
|
||||
|
||||
if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache))
|
||||
if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List<ChannelInfo> cache))
|
||||
{
|
||||
return cache;
|
||||
}
|
||||
@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
||||
if (!string.IsNullOrEmpty(key) && list.Count > 0)
|
||||
{
|
||||
_memoryCache.Set(key, list);
|
||||
_cache[key] = list;
|
||||
}
|
||||
|
||||
return list;
|
||||
|
@ -9,6 +9,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -27,7 +28,6 @@ using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
@ -50,9 +50,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IServerApplicationHost appHost,
|
||||
ISocketFactory socketFactory,
|
||||
IStreamHelper streamHelper,
|
||||
IMemoryCache memoryCache)
|
||||
: base(config, logger, fileSystem, memoryCache)
|
||||
IStreamHelper streamHelper)
|
||||
: base(config, logger, fileSystem)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_appHost = appHost;
|
||||
@ -77,13 +76,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false) ?? new List<Channels>();
|
||||
|
||||
var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>();
|
||||
if (info.ImportFavoritesOnly)
|
||||
{
|
||||
lineup = lineup.Where(i => i.Favorite).ToList();
|
||||
lineup = lineup.Where(i => i.Favorite);
|
||||
}
|
||||
|
||||
return lineup.Where(i => !i.DRM).ToList();
|
||||
@ -130,9 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
.GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
{
|
||||
@ -176,34 +170,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
|
||||
var tuners = new List<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);
|
||||
if (stripedLine.Contains("Channel", StringComparison.Ordinal))
|
||||
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
|
||||
await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
|
||||
{
|
||||
LiveTvTunerStatus status;
|
||||
var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
|
||||
var name = stripedLine.Substring(0, index - 1);
|
||||
var currentChannel = stripedLine.Substring(index + 7);
|
||||
if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
|
||||
string stripedLine = StripXML(line);
|
||||
if (stripedLine.Contains("Channel", StringComparison.Ordinal))
|
||||
{
|
||||
status = LiveTvTunerStatus.LiveTv;
|
||||
}
|
||||
else
|
||||
{
|
||||
status = LiveTvTunerStatus.Available;
|
||||
}
|
||||
LiveTvTunerStatus status;
|
||||
var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
|
||||
var name = stripedLine.Substring(0, index - 1);
|
||||
var currentChannel = stripedLine.Substring(index + 7);
|
||||
if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
|
||||
{
|
||||
status = LiveTvTunerStatus.LiveTv;
|
||||
}
|
||||
else
|
||||
{
|
||||
status = LiveTvTunerStatus.Available;
|
||||
}
|
||||
|
||||
tuners.Add(new LiveTvTunerInfo
|
||||
{
|
||||
Name = name,
|
||||
SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
|
||||
ProgramName = currentChannel,
|
||||
Status = status
|
||||
});
|
||||
tuners.Add(new LiveTvTunerInfo
|
||||
{
|
||||
Name = name,
|
||||
SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
|
||||
ProgramName = currentChannel,
|
||||
Status = status
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -661,18 +658,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
// Need a way to set the Receive timeout on the socket otherwise this might never timeout?
|
||||
try
|
||||
{
|
||||
await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false);
|
||||
await udpClient.SendToAsync(discBytes, new IPEndPoint(IPAddress.Broadcast, 65001), cancellationToken).ConfigureAwait(false);
|
||||
var receiveBuffer = new byte[8192];
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var response = await udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
var deviceIp = response.RemoteEndPoint.Address.ToString();
|
||||
var response = await udpClient.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, 0), cancellationToken).ConfigureAwait(false);
|
||||
var deviceIP = ((IPEndPoint)response.RemoteEndPoint).Address.ToString();
|
||||
|
||||
// check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
|
||||
if (response.ReceivedBytes > 13 && response.Buffer[1] == 3)
|
||||
// Check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
|
||||
if (response.ReceivedBytes > 13 && receiveBuffer[1] == 3)
|
||||
{
|
||||
var deviceAddress = "http://" + deviceIp;
|
||||
var deviceAddress = "http://" + deviceIP;
|
||||
|
||||
var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
@ -44,14 +44,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
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();
|
||||
await client.ConnectAsync(remoteIp, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
|
||||
await client.ConnectAsync(remoteIP, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var stream = client.GetStream();
|
||||
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
|
||||
@ -75,9 +73,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartStreaming(IPAddress remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
|
||||
public async Task StartStreaming(IPAddress remoteIP, IPAddress localIP, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
|
||||
{
|
||||
_remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort);
|
||||
_remoteEndPoint = new IPEndPoint(remoteIP, HdHomeRunPort);
|
||||
|
||||
_tcpClient = new TcpClient();
|
||||
await _tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false);
|
||||
@ -125,7 +123,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
}
|
||||
|
||||
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
|
||||
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIP, localPort);
|
||||
var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
|
||||
|
||||
await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
|
||||
|
@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
|
||||
public partial class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
|
||||
{
|
||||
private string? _channel;
|
||||
private string? _program;
|
||||
@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
public LegacyHdHomerunChannelCommands(string url)
|
||||
{
|
||||
// parse url for channel and program
|
||||
var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)");
|
||||
var match = ChannelAndProgramRegex().Match(url);
|
||||
if (match.Success)
|
||||
{
|
||||
_channel = match.Groups[1].Value;
|
||||
@ -21,6 +21,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\/ch([0-9]+)-?([0-9]*)")]
|
||||
private static partial Regex ChannelAndProgramRegex();
|
||||
|
||||
public IEnumerable<(string CommandName, string CommandValue)> GetCommands()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_channel))
|
||||
|
@ -5,7 +5,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
@ -54,9 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IServerApplicationHost appHost,
|
||||
INetworkManager networkManager,
|
||||
IStreamHelper streamHelper,
|
||||
IMemoryCache memoryCache)
|
||||
: base(config, logger, fileSystem, memoryCache)
|
||||
IStreamHelper streamHelper)
|
||||
: base(config, logger, fileSystem)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_appHost = appHost;
|
||||
|
@ -20,7 +20,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
public class M3uParser
|
||||
public partial class M3uParser
|
||||
{
|
||||
private const string ExtInfPrefix = "#EXTINF:";
|
||||
|
||||
@ -33,6 +33,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase, "en-US")]
|
||||
private static partial Regex KeyValueRegex();
|
||||
|
||||
public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
|
||||
{
|
||||
// Read the file and display it line by line.
|
||||
@ -91,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
|
||||
{
|
||||
var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
|
||||
if (string.IsNullOrWhiteSpace(channel.Id))
|
||||
{
|
||||
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
channel.Path = trimmedLine;
|
||||
channels.Add(channel);
|
||||
@ -311,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
{
|
||||
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
|
||||
var matches = KeyValueRegex().Matches(line);
|
||||
|
||||
remaining = line;
|
||||
|
||||
@ -320,7 +316,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
var key = match.Groups[1].Value;
|
||||
var value = match.Groups[2].Value;
|
||||
|
||||
dict[match.Groups[1].Value] = match.Groups[2].Value;
|
||||
dict[key] = value;
|
||||
remaining = remaining.Replace(key + "=\"" + value + "\"", string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
@ -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": "ইনষ্টলেশ্যন ব্যৰ্থতা"
|
||||
}
|
||||
|
52
Emby.Server.Implementations/Localization/Core/chr.json
Normal file
52
Emby.Server.Implementations/Localization/Core/chr.json
Normal file
@ -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",
|
||||
"HeaderFavoriteShows": "Oblíbené seriály",
|
||||
"HeaderFavoriteSongs": "Oblíbená hudba",
|
||||
"HeaderLiveTV": "Televize",
|
||||
"HeaderLiveTV": "Živý přenos",
|
||||
"HeaderNextUp": "Další díly",
|
||||
"HeaderRecordingGroups": "Skupiny nahrávek",
|
||||
"HomeVideos": "Domácí videa",
|
||||
|
@ -15,13 +15,13 @@
|
||||
"Favorites": "Favoritter",
|
||||
"Folders": "Mapper",
|
||||
"Genres": "Genrer",
|
||||
"HeaderAlbumArtists": "Albums kunstnere",
|
||||
"HeaderAlbumArtists": "Albumkunstnere",
|
||||
"HeaderContinueWatching": "Fortsæt afspilning",
|
||||
"HeaderFavoriteAlbums": "Favorit albummer",
|
||||
"HeaderFavoriteArtists": "Favorit kunstnere",
|
||||
"HeaderFavoriteEpisodes": "Favorit afsnit",
|
||||
"HeaderFavoriteShows": "Favorit serier",
|
||||
"HeaderFavoriteSongs": "Favorit sange",
|
||||
"HeaderFavoriteAlbums": "Favoritalbummer",
|
||||
"HeaderFavoriteArtists": "Favoritkunstnere",
|
||||
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
|
||||
"HeaderFavoriteShows": "Yndlingsserier",
|
||||
"HeaderFavoriteSongs": "Yndlingssange",
|
||||
"HeaderLiveTV": "Live-TV",
|
||||
"HeaderNextUp": "Næste",
|
||||
"HeaderRecordingGroups": "Optagelsesgrupper",
|
||||
@ -34,8 +34,8 @@
|
||||
"Latest": "Seneste",
|
||||
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
|
||||
"MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
|
||||
"MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
|
||||
"MixedContent": "Blandet indhold",
|
||||
"Movies": "Film",
|
||||
"Music": "Musik",
|
||||
@ -51,7 +51,7 @@
|
||||
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
|
||||
"NotificationOptionInstallationFailed": "Installationen mislykkedes",
|
||||
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
|
||||
"NotificationOptionPluginError": "Plugin fejl",
|
||||
"NotificationOptionPluginError": "Plugin-fejl",
|
||||
"NotificationOptionPluginInstalled": "Plugin blev installeret",
|
||||
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
|
||||
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
|
||||
@ -92,26 +92,26 @@
|
||||
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
||||
"ValueSpecialEpisodeName": "Special - {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
|
||||
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
|
||||
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
|
||||
"TaskUpdatePlugins": "Opdater Plugins",
|
||||
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
|
||||
"TaskCleanLogs": "Ryd Log mappe",
|
||||
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
|
||||
"TaskRefreshLibrary": "Scan Medie Bibliotek",
|
||||
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
|
||||
"TaskCleanCache": "Ryd Cache mappe",
|
||||
"TasksChannelsCategory": "Internet Kanaler",
|
||||
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
|
||||
"TaskCleanLogs": "Ryd Log-mappe",
|
||||
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
|
||||
"TaskRefreshLibrary": "Scan Mediebibliotek",
|
||||
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
|
||||
"TaskCleanCache": "Ryd Cache-mappe",
|
||||
"TasksChannelsCategory": "Internetkanaler",
|
||||
"TasksApplicationCategory": "Applikation",
|
||||
"TasksLibraryCategory": "Bibliotek",
|
||||
"TasksMaintenanceCategory": "Vedligeholdelse",
|
||||
"TaskRefreshChapterImages": "Udtræk kapitel billeder",
|
||||
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
|
||||
"TaskRefreshChannelsDescription": "Opdater internet kanal information.",
|
||||
"TaskRefreshChapterImages": "Udtræk kapitelbilleder",
|
||||
"TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
|
||||
"TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
|
||||
"TaskRefreshChannels": "Opdater Kanaler",
|
||||
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
|
||||
"TaskCleanTranscode": "Tøm Transcode mappen",
|
||||
"TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
|
||||
"TaskCleanTranscode": "Tøm Transcode-mappen",
|
||||
"TaskRefreshPeople": "Opdater Personer",
|
||||
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
|
||||
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
|
||||
@ -121,8 +121,8 @@
|
||||
"Default": "Standard",
|
||||
"TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
|
||||
"TaskOptimizeDatabase": "Optimér database",
|
||||
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
|
||||
"TaskKeyframeExtractor": "Nøglebillede udtræk",
|
||||
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
|
||||
"TaskKeyframeExtractor": "Udtræk af nøglebillede",
|
||||
"External": "Ekstern",
|
||||
"HearingImpaired": "Hørehæmmet"
|
||||
}
|
||||
|
@ -3,9 +3,9 @@
|
||||
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
|
||||
"Application": "Aplicación",
|
||||
"Artists": "Artistas",
|
||||
"AuthenticationSucceededWithUserName": "{0} identificado correctamente",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
|
||||
"Books": "Libros",
|
||||
"CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
|
||||
"CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
|
||||
"Channels": "Canales",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"Collections": "Colecciones",
|
||||
|
@ -74,16 +74,16 @@
|
||||
"Shows": "Sarjat",
|
||||
"ServerNameNeedsToBeRestarted": "\"{0}\" on käynnistettävä uudelleen",
|
||||
"ProviderValue": "Lähde: {0}",
|
||||
"Plugin": "Laajennus",
|
||||
"Plugin": "Lisäosa",
|
||||
"NotificationOptionVideoPlaybackStopped": "Videon toisto lopetettu",
|
||||
"NotificationOptionVideoPlayback": "Videon toisto aloitettu",
|
||||
"NotificationOptionUserLockedOut": "Käyttäjä on lukittu",
|
||||
"NotificationOptionTaskFailed": "Ajoitettu tehtävä epäonnistui",
|
||||
"NotificationOptionServerRestartRequired": "Tarvitaan palvelimen uudelleenkäynnistys",
|
||||
"NotificationOptionPluginUpdateInstalled": "Laajennus on päivitetty",
|
||||
"NotificationOptionPluginUninstalled": "Laajennus on poistettu",
|
||||
"NotificationOptionPluginInstalled": "Laajennus on asennettu",
|
||||
"NotificationOptionPluginError": "Laajennuksen virhe",
|
||||
"NotificationOptionPluginUpdateInstalled": "Lisäosa päivitettiin",
|
||||
"NotificationOptionPluginUninstalled": "Lisäosa poistettiin",
|
||||
"NotificationOptionPluginInstalled": "Lisäosa asennettiin",
|
||||
"NotificationOptionPluginError": "Lisäosan virhe",
|
||||
"NotificationOptionNewLibraryContent": "Sisältöä on lisätty",
|
||||
"NotificationOptionInstallationFailed": "Asennus epäonnistui",
|
||||
"NotificationOptionCameraImageUploaded": "Kameran kuva on tallennettu",
|
||||
@ -98,8 +98,8 @@
|
||||
"TaskRefreshChannels": "Päivitä kanavat",
|
||||
"TaskCleanTranscodeDescription": "Poistaa päivää vanhemmat transkoodaustiedostot.",
|
||||
"TaskCleanTranscode": "Puhdista transkoodauskansio",
|
||||
"TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset laajennuksille, jotka on määritetty päivittymään automaattisesti.",
|
||||
"TaskUpdatePlugins": "Päivitä laajennukset",
|
||||
"TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset lisäosille, jotka on määritetty päivittymään automaattisesti.",
|
||||
"TaskUpdatePlugins": "Päivitä lisäosat",
|
||||
"TaskRefreshPeopleDescription": "Päivittää mediakirjaston näyttelijöiden ja ohjaajien metatiedot.",
|
||||
"TaskRefreshPeople": "Päivitä henkilöt",
|
||||
"TaskCleanLogsDescription": "Poistaa {0} päivää vanhemmat lokitiedostot.",
|
||||
|
18
Emby.Server.Implementations/Localization/Core/fo.json
Normal file
18
Emby.Server.Implementations/Localization/Core/fo.json
Normal file
@ -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",
|
||||
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
|
||||
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
|
||||
"TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.",
|
||||
"TaskRefreshLibrary": "Scanner la médiathèque",
|
||||
"TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
|
||||
"TaskRefreshLibrary": "Analyser la médiathèque",
|
||||
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
|
||||
"TaskRefreshChapterImages": "Extraire les images de chapitre",
|
||||
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
|
||||
|
@ -5,18 +5,18 @@
|
||||
"Artists": "אומנים",
|
||||
"AuthenticationSucceededWithUserName": "{0} אומת בהצלחה",
|
||||
"Books": "ספרים",
|
||||
"CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מ {0}",
|
||||
"CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מתוך {0}",
|
||||
"Channels": "ערוצים",
|
||||
"ChapterNameValue": "פרק {0}",
|
||||
"Collections": "אוספים",
|
||||
"DeviceOfflineWithName": "{0} התנתק",
|
||||
"DeviceOnlineWithName": "{0} מחובר",
|
||||
"FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי מ{0}",
|
||||
"FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי דרך {0}",
|
||||
"Favorites": "מועדפים",
|
||||
"Folders": "תיקיות",
|
||||
"Genres": "ז'אנרים",
|
||||
"Genres": "ז׳אנרים",
|
||||
"HeaderAlbumArtists": "אמני האלבום",
|
||||
"HeaderContinueWatching": "המשך לצפות",
|
||||
"HeaderContinueWatching": "להמשיך לצפות",
|
||||
"HeaderFavoriteAlbums": "אלבומים מועדפים",
|
||||
"HeaderFavoriteArtists": "אמנים מועדפים",
|
||||
"HeaderFavoriteEpisodes": "פרקים מועדפים",
|
||||
@ -27,14 +27,14 @@
|
||||
"HeaderRecordingGroups": "קבוצות הקלטה",
|
||||
"HomeVideos": "סרטונים בייתים",
|
||||
"Inherit": "הורש",
|
||||
"ItemAddedWithName": "{0} הוסף לספרייה",
|
||||
"ItemAddedWithName": "{0} נוסף לספרייה",
|
||||
"ItemRemovedWithName": "{0} נמחק מהספרייה",
|
||||
"LabelIpAddressValue": "Ip כתובת: {0}",
|
||||
"LabelRunningTimeValue": "משך צפייה: {0}",
|
||||
"Latest": "אחרון",
|
||||
"MessageApplicationUpdated": "שרת הJellyfin עודכן",
|
||||
"MessageApplicationUpdatedTo": "שרת הJellyfin עודכן לגרסא {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "הגדרת השרת {0} שונתה",
|
||||
"MessageApplicationUpdatedTo": "שרת ה־Jellyfin עודכן לגרסה {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
|
||||
"MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
|
||||
"MixedContent": "תוכן מעורב",
|
||||
"Movies": "סרטים",
|
||||
@ -50,7 +50,7 @@
|
||||
"NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
|
||||
"NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
|
||||
"NotificationOptionInstallationFailed": "התקנה נכשלה",
|
||||
"NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
|
||||
"NotificationOptionNewLibraryContent": "תוכן חדש נוסף",
|
||||
"NotificationOptionPluginError": "כשלון בתוסף",
|
||||
"NotificationOptionPluginInstalled": "התוסף הותקן",
|
||||
"NotificationOptionPluginUninstalled": "התוסף הוסר",
|
||||
@ -61,41 +61,41 @@
|
||||
"NotificationOptionVideoPlayback": "ניגון וידאו החל",
|
||||
"NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
|
||||
"Photos": "תמונות",
|
||||
"Playlists": "רשימות הפעלה",
|
||||
"Plugin": "Plugin",
|
||||
"Playlists": "רשימות נגינה",
|
||||
"Plugin": "תוסף",
|
||||
"PluginInstalledWithName": "{0} הותקן",
|
||||
"PluginUninstalledWithName": "{0} הוסר",
|
||||
"PluginUpdatedWithName": "{0} עודכן",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ProviderValue": "ספק: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} נכשל",
|
||||
"ScheduledTaskStartedWithName": "{0} החל",
|
||||
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
|
||||
"Shows": "סדרות",
|
||||
"Songs": "שירים",
|
||||
"StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
|
||||
"StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. נא לנסות שנית בהקדם.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
|
||||
"Sync": "סנכרן",
|
||||
"System": "System",
|
||||
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
|
||||
"Sync": "סנכרון",
|
||||
"System": "מערכת",
|
||||
"TvShows": "סדרות טלוויזיה",
|
||||
"User": "User",
|
||||
"User": "משתמש",
|
||||
"UserCreatedWithName": "המשתמש {0} נוצר",
|
||||
"UserDeletedWithName": "המשתמש {0} הוסר",
|
||||
"UserDownloadingItemWithValues": "{0} מוריד את {1}",
|
||||
"UserLockedOutWithName": "המשתמש {0} ננעל",
|
||||
"UserOfflineFromDevice": "{0} התנתק מ-{1}",
|
||||
"UserOnlineFromDevice": "{0} מחובר מ-{1}",
|
||||
"UserOfflineFromDevice": "{0} התנתק מ־{1}",
|
||||
"UserOnlineFromDevice": "{0} מחובר מ־{1}",
|
||||
"UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
|
||||
"UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
|
||||
"UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
|
||||
"ValueSpecialEpisodeName": "מיוחד- {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"VersionNumber": "גרסה {0}",
|
||||
"TaskRefreshLibrary": "סרוק ספריית מדיה",
|
||||
"TaskRefreshChapterImages": "חלץ תמונות פרקים",
|
||||
"TaskCleanCacheDescription": "מחק קבצי מטמון שלא בשימוש המערכת.",
|
||||
"TaskCleanCache": "נקה תיקיית מטמון",
|
||||
"TaskCleanCache": "ניקוי תיקיית מטמון",
|
||||
"TasksApplicationCategory": "יישום",
|
||||
"TasksLibraryCategory": "ספרייה",
|
||||
"TasksMaintenanceCategory": "תחזוקה",
|
||||
@ -103,7 +103,7 @@
|
||||
"TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
|
||||
"TaskRefreshPeople": "רענן אנשים",
|
||||
"TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
|
||||
"TaskCleanLogs": "נקה תיקיית יומן",
|
||||
"TaskCleanLogs": "ניקוי תיקיית יומן",
|
||||
"TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
|
||||
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
|
||||
"TasksChannelsCategory": "ערוצי אינטרנט",
|
||||
|
@ -1,11 +1,11 @@
|
||||
{
|
||||
"Albums": "Albumok",
|
||||
"AppDeviceValues": "Program: {0}, Eszköz: {1}",
|
||||
"AppDeviceValues": "Program: {0}, eszköz: {1}",
|
||||
"Application": "Alkalmazás",
|
||||
"Artists": "Előadók",
|
||||
"AuthenticationSucceededWithUserName": "{0} sikeresen azonosítva",
|
||||
"AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
|
||||
"Books": "Könyvek",
|
||||
"CameraImageUploadedFrom": "Új kamerakép került feltöltésre innen: {0}",
|
||||
"CameraImageUploadedFrom": "Új kamerakép feltöltve innen: {0}",
|
||||
"Channels": "Csatornák",
|
||||
"ChapterNameValue": "{0}. jelenet",
|
||||
"Collections": "Gyűjtemények",
|
||||
@ -15,13 +15,13 @@
|
||||
"Favorites": "Kedvencek",
|
||||
"Folders": "Könyvtárak",
|
||||
"Genres": "Műfajok",
|
||||
"HeaderAlbumArtists": "Album előadó(k)",
|
||||
"HeaderAlbumArtists": "Albumelőadók",
|
||||
"HeaderContinueWatching": "Megtekintés folytatása",
|
||||
"HeaderFavoriteAlbums": "Kedvenc albumok",
|
||||
"HeaderFavoriteArtists": "Kedvenc előadók",
|
||||
"HeaderFavoriteEpisodes": "Kedvenc epizódok",
|
||||
"HeaderFavoriteShows": "Kedvenc sorozatok",
|
||||
"HeaderFavoriteSongs": "Kedvenc dalok",
|
||||
"HeaderFavoriteSongs": "Kedvenc számok",
|
||||
"HeaderLiveTV": "Élő TV",
|
||||
"HeaderNextUp": "Következik",
|
||||
"HeaderRecordingGroups": "Felvételi csoportok",
|
||||
@ -29,37 +29,37 @@
|
||||
"Inherit": "Örökölt",
|
||||
"ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
|
||||
"ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
|
||||
"LabelIpAddressValue": "IP cím: {0}",
|
||||
"LabelRunningTimeValue": "Futási idő: {0}",
|
||||
"LabelIpAddressValue": "IP-cím: {0}",
|
||||
"LabelRunningTimeValue": "Lejátszási idő: {0}",
|
||||
"Latest": "Legújabb",
|
||||
"MessageApplicationUpdated": "Jellyfin Szerver frissítve",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Szerver frissítve lett a következőre: {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigurációs rész frissítve: {0}",
|
||||
"MessageServerConfigurationUpdated": "Szerver konfiguráció frissítve",
|
||||
"MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve",
|
||||
"MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve: {0}",
|
||||
"MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve",
|
||||
"MixedContent": "Vegyes tartalom",
|
||||
"Movies": "Filmek",
|
||||
"Music": "Zene",
|
||||
"Music": "Zenék",
|
||||
"MusicVideos": "Zenei videóklippek",
|
||||
"NameInstallFailed": "{0} sikertelen telepítés",
|
||||
"NameSeasonNumber": "{0}. évad",
|
||||
"NameSeasonUnknown": "Ismeretlen évad",
|
||||
"NewVersionIsAvailable": "Letölthető a Jellyfin Szerver új verziója.",
|
||||
"NewVersionIsAvailable": "Letölthető a Jellyfin kiszolgáló új verziója.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve",
|
||||
"NotificationOptionAudioPlayback": "Audió lejátszás elkezdve",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva",
|
||||
"NotificationOptionCameraImageUploaded": "Kamera kép feltöltve",
|
||||
"NotificationOptionInstallationFailed": "Telepítés sikertelen",
|
||||
"NotificationOptionAudioPlayback": "Hanglejátszás elkezdve",
|
||||
"NotificationOptionAudioPlaybackStopped": "Hanglejátszás leállítva",
|
||||
"NotificationOptionCameraImageUploaded": "Kamerakép feltöltve",
|
||||
"NotificationOptionInstallationFailed": "Telepítési hiba",
|
||||
"NotificationOptionNewLibraryContent": "Új tartalom hozzáadva",
|
||||
"NotificationOptionPluginError": "Bővítmény hiba",
|
||||
"NotificationOptionPluginError": "Bővítményhiba",
|
||||
"NotificationOptionPluginInstalled": "Bővítmény telepítve",
|
||||
"NotificationOptionPluginUninstalled": "Bővítmény eltávolítva",
|
||||
"NotificationOptionPluginUpdateInstalled": "Bővítmény frissítés telepítve",
|
||||
"NotificationOptionServerRestartRequired": "Szerver újraindítás szükséges",
|
||||
"NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve",
|
||||
"NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges",
|
||||
"NotificationOptionTaskFailed": "Ütemezett feladat hiba",
|
||||
"NotificationOptionUserLockedOut": "Felhasználó tiltva",
|
||||
"NotificationOptionVideoPlayback": "Videó lejátszás elkezdve",
|
||||
"NotificationOptionVideoPlaybackStopped": "Videó lejátszás leállítva",
|
||||
"NotificationOptionVideoPlayback": "Videólejátszás elkezdve",
|
||||
"NotificationOptionVideoPlaybackStopped": "Videólejátszás leállítva",
|
||||
"Photos": "Fényképek",
|
||||
"Playlists": "Lejátszási listák",
|
||||
"Plugin": "Bővítmény",
|
||||
@ -69,47 +69,47 @@
|
||||
"ProviderValue": "Szolgáltató: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} sikertelen",
|
||||
"ScheduledTaskStartedWithName": "{0} elkezdve",
|
||||
"ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani",
|
||||
"ServerNameNeedsToBeRestarted": "A(z) {0} újraindítása szükséges",
|
||||
"Shows": "Sorozatok",
|
||||
"Songs": "Dalok",
|
||||
"StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.",
|
||||
"Songs": "Számok",
|
||||
"StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}",
|
||||
"Sync": "Szinkronizál",
|
||||
"SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}",
|
||||
"Sync": "Szinkronizálás",
|
||||
"System": "Rendszer",
|
||||
"TvShows": "TV műsorok",
|
||||
"User": "Felhasználó",
|
||||
"UserCreatedWithName": "{0} felhasználó létrehozva",
|
||||
"UserDeletedWithName": "{0} felhasználó törölve",
|
||||
"UserDownloadingItemWithValues": "{0} letölti {1}",
|
||||
"UserDownloadingItemWithValues": "{0} letölti: {1}",
|
||||
"UserLockedOutWithName": "{0} felhasználó zárolva van",
|
||||
"UserOfflineFromDevice": "{0} kijelentkezett innen: {1}",
|
||||
"UserOnlineFromDevice": "{0} online innen: {1}",
|
||||
"UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}",
|
||||
"UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}",
|
||||
"UserPasswordChangedWithName": "{0} jelszava megváltozott",
|
||||
"UserPolicyUpdatedWithName": "{0} felhasználói házirendje frissült",
|
||||
"UserStartedPlayingItemWithValues": "{0} elkezdte lejátszani a következőt: {1}, itt: {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} befejezte a következő lejátszását: {1}, itt: {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz",
|
||||
"ValueSpecialEpisodeName": "Special - {0}",
|
||||
"ValueSpecialEpisodeName": "Különkiadás – {0}",
|
||||
"VersionNumber": "Verzió: {0}",
|
||||
"TaskCleanTranscode": "Átkódolási könyvtár ürítése",
|
||||
"TaskUpdatePluginsDescription": "Letölti és telepíti a frissítéseket azokhoz a bővítményekhez, amelyeknél az automatikus frissítés engedélyezve van.",
|
||||
"TaskUpdatePlugins": "Bővítmények frissítése",
|
||||
"TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a könyvtáradban.",
|
||||
"TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a médiatárban.",
|
||||
"TaskRefreshPeople": "Személyek frissítése",
|
||||
"TaskCleanLogsDescription": "Törli azokat a naplófájlokat, amelyek {0} napnál régebbiek.",
|
||||
"TaskCleanLogs": "Naplózási könyvtár ürítése",
|
||||
"TaskRefreshLibraryDescription": "Átvizsgálja a könyvtáraidat új fájlokért és frissíti a metaadatokat.",
|
||||
"TaskRefreshLibrary": "Média könyvtár beolvasása",
|
||||
"TaskRefreshChapterImagesDescription": "Miniatűröket generál olyan videókhoz, amely tartalmaz fejezeteket.",
|
||||
"TaskRefreshChapterImages": "Fejezetek képeinek generálása",
|
||||
"TaskRefreshLibraryDescription": "Átvizsgálja a médiatárat új fájlokat keresve, és frissíti a metaadatokat.",
|
||||
"TaskRefreshLibrary": "Médiatár átvizsgálása",
|
||||
"TaskRefreshChapterImagesDescription": "Miniatűröket hoz létre az olyan videókhoz, amely tartalmaz fejezeteket.",
|
||||
"TaskRefreshChapterImages": "Fejezetképek kinyerése",
|
||||
"TaskCleanCacheDescription": "Törli azokat a gyorsítótárazott fájlokat, amikre a rendszernek már nincs szüksége.",
|
||||
"TaskCleanCache": "Gyorsítótár könyvtárának ürítése",
|
||||
"TasksChannelsCategory": "Internetes csatornák",
|
||||
"TasksApplicationCategory": "Alkalmazás",
|
||||
"TasksLibraryCategory": "Könyvtár",
|
||||
"TasksMaintenanceCategory": "Karbantartás",
|
||||
"TaskDownloadMissingSubtitlesDescription": "A metaadat konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "A metaadat-konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.",
|
||||
"TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
|
||||
"TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.",
|
||||
"TaskRefreshChannels": "Csatornák frissítése",
|
||||
@ -121,8 +121,8 @@
|
||||
"Default": "Alapértelmezett",
|
||||
"TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.",
|
||||
"TaskOptimizeDatabase": "Adatbázis optimalizálása",
|
||||
"TaskKeyframeExtractor": "Kulcskockák kibontása",
|
||||
"TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
|
||||
"TaskKeyframeExtractor": "Kulcsképkockák kibontása",
|
||||
"TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
|
||||
"External": "Külső",
|
||||
"HearingImpaired": "Hallássérült"
|
||||
}
|
||||
|
@ -13,8 +13,8 @@
|
||||
"HeaderFavoriteArtists": "Uppáhalds Listamenn",
|
||||
"HeaderFavoriteAlbums": "Uppáhalds Plötur",
|
||||
"HeaderContinueWatching": "Halda áfram að horfa",
|
||||
"HeaderAlbumArtists": "Höfundur plötu",
|
||||
"Genres": "Tegundir",
|
||||
"HeaderAlbumArtists": "Listamaður á umslagi",
|
||||
"Genres": "Stefnur",
|
||||
"Folders": "Möppur",
|
||||
"Favorites": "Uppáhalds",
|
||||
"FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig",
|
||||
@ -22,32 +22,32 @@
|
||||
"DeviceOfflineWithName": "{0} hefur aftengst",
|
||||
"Collections": "Söfn",
|
||||
"ChapterNameValue": "Kafli {0}",
|
||||
"Channels": "Stöðvar",
|
||||
"CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}",
|
||||
"Channels": "Rásir",
|
||||
"CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni",
|
||||
"Books": "Bækur",
|
||||
"AuthenticationSucceededWithUserName": "{0} auðkenning tókst",
|
||||
"Artists": "Listamaður",
|
||||
"AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst",
|
||||
"Artists": "Listamenn",
|
||||
"Application": "Forrit",
|
||||
"AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
|
||||
"Albums": "Plötur",
|
||||
"Plugin": "Viðbót",
|
||||
"Photos": "Myndir",
|
||||
"NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
|
||||
"NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
|
||||
"Plugin": "Viðbótarvirkni",
|
||||
"Photos": "Ljósmyndir",
|
||||
"NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð",
|
||||
"NotificationOptionVideoPlayback": "Myndbandsafspilun hafin",
|
||||
"NotificationOptionUserLockedOut": "Notandi læstur úti",
|
||||
"NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg",
|
||||
"NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett",
|
||||
"NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
|
||||
"NotificationOptionPluginInstalled": "Viðbót sett upp",
|
||||
"NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg",
|
||||
"NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið",
|
||||
"NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð",
|
||||
"NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp",
|
||||
"NotificationOptionPluginError": "Bilun í viðbót",
|
||||
"NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
|
||||
"NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
|
||||
"NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp",
|
||||
"NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð",
|
||||
"NotificationOptionAudioPlayback": "Hljóðafspilun hafin",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði",
|
||||
"NameSeasonUnknown": "Sería óþekkt",
|
||||
"NameSeasonNumber": "Sería {0}",
|
||||
"NameSeasonUnknown": "Þáttaröð óþekkt",
|
||||
"NameSeasonNumber": "Þáttaröð {0}",
|
||||
"MixedContent": "Blandað efni",
|
||||
"MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
|
||||
@ -57,24 +57,24 @@
|
||||
"User": "Notandi",
|
||||
"System": "Kerfi",
|
||||
"NotificationOptionNewLibraryContent": "Nýju efni bætt við",
|
||||
"NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.",
|
||||
"NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.",
|
||||
"NameInstallFailed": "{0} uppsetning mistókst",
|
||||
"MusicVideos": "Tónlistarmyndbönd",
|
||||
"Music": "Tónlist",
|
||||
"Movies": "Kvikmyndir",
|
||||
"UserDeletedWithName": "Notanda {0} hefur verið eytt",
|
||||
"UserCreatedWithName": "Notandi {0} hefur verið stofnaður",
|
||||
"TvShows": "Þættir",
|
||||
"TvShows": "Sjónvarpsþættir",
|
||||
"Sync": "Samstilla",
|
||||
"Songs": "Lög",
|
||||
"ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa",
|
||||
"ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur",
|
||||
"ScheduledTaskStartedWithName": "{0} hafin",
|
||||
"ScheduledTaskFailedWithName": "{0} mistókst",
|
||||
"PluginUpdatedWithName": "{0} var uppfært",
|
||||
"PluginUninstalledWithName": "{0} var fjarlægt",
|
||||
"PluginInstalledWithName": "{0} var sett upp",
|
||||
"NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.",
|
||||
"VersionNumber": "Útgáfa {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
|
||||
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
|
||||
@ -83,14 +83,14 @@
|
||||
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
|
||||
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
|
||||
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
|
||||
"UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur",
|
||||
"UserDownloadingItemWithValues": "{0} Hleður niður {1}",
|
||||
"UserLockedOutWithName": "Notandi {0} hefur verið læstur úti",
|
||||
"UserDownloadingItemWithValues": "{0} hleður niður {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
|
||||
"ProviderValue": "Veitandi: {0}",
|
||||
"ProviderValue": "Efnisveita: {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
|
||||
"ValueSpecialEpisodeName": "Sérstakt - {0}",
|
||||
"Shows": "Sýningar",
|
||||
"Playlists": "Spilunarlisti",
|
||||
"ValueSpecialEpisodeName": "Sérstaktur - {0}",
|
||||
"Shows": "Þættir",
|
||||
"Playlists": "Efnisskrár",
|
||||
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
|
||||
"TaskRefreshChannels": "Endurhlaða Rásir",
|
||||
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
|
||||
@ -116,5 +116,12 @@
|
||||
"TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
|
||||
"TaskCleanLogs": "Hreinsa færslu skrá",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
|
||||
"HearingImpaired": "Heyrnarskertur"
|
||||
"HearingImpaired": "Heyrnarskertur",
|
||||
"TaskOptimizeDatabaseDescription": "Þjappar gagnagrunni og bætir við lausu diskaplássi. Að keyra þessa aðgerð eftir skönnun safnsins, eða eftir einhverjar breytingar sem fela í sér gagnagrunnsbreytingar, gætu aukið hraðvirkni.",
|
||||
"TaskKeyframeExtractor": "Lykilrammaplokkari",
|
||||
"TaskKeyframeExtractorDescription": "Plokkar lykilramma úr myndbandsskrám til að búa til nákvæmari HLS uppskiptingarlista. Þetta verk getur tekið langan tíma.",
|
||||
"TaskRefreshChapterImages": "Plokka kafla-myndir",
|
||||
"TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
|
||||
"Forced": "Þvingað",
|
||||
"External": "Útvær"
|
||||
}
|
||||
|
@ -3,5 +3,125 @@
|
||||
"TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
|
||||
"TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
|
||||
"TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್ಟ್ರಾಕ್ಟರ್",
|
||||
"TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್ಗಳಿಂದ ಕೀಫ್ರೇಮ್ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು."
|
||||
"TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್ಗಳಿಂದ ಕೀಫ್ರೇಮ್ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
|
||||
"ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
|
||||
"TasksLibraryCategory": "ಸಮೊಹ",
|
||||
"TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
|
||||
"TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
|
||||
"TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ",
|
||||
"TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
|
||||
"TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
|
||||
"UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
|
||||
"Albums": "ಸಂಪುಟ",
|
||||
"Application": "ಅಪ್ಲಿಕೇಶನ್",
|
||||
"AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
|
||||
"Artists": "ಕಲಾವಿದರು",
|
||||
"AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
|
||||
"Books": "ಪುಸ್ತಕಗಳು",
|
||||
"ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
|
||||
"Collections": "ಸಂಗ್ರಹಣೆಗಳು",
|
||||
"Default": "ಪೂರ್ವನಿಯೋಜಿತ",
|
||||
"DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
|
||||
"DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
|
||||
"External": "ಹೊರಗಿನ",
|
||||
"FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
|
||||
"Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
|
||||
"Folders": "ಫೋಲ್ಡರ್ಗಳು",
|
||||
"Forced": "ಬಲವಂತವಾಗಿ",
|
||||
"Genres": "ಪ್ರಕಾರಗಳು",
|
||||
"HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
|
||||
"HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
|
||||
"HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
|
||||
"HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
|
||||
"HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
|
||||
"HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
|
||||
"HeaderNextUp": "ಮುಂದೆ",
|
||||
"HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
|
||||
"MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"Channels": "ಮೂಲಗಳು",
|
||||
"HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
|
||||
"HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
|
||||
"HearingImpaired": "ಮೂಗ",
|
||||
"ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
|
||||
"MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್ಲೋಡ್ಗೆ ಲಭ್ಯವಿದೆ.",
|
||||
"NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
|
||||
"NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್ಇನ್ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ",
|
||||
"NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
|
||||
"PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್ಇನ್ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
|
||||
"ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
|
||||
"ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
|
||||
"UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
|
||||
"UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್ಲೈನ್ನಲ್ಲಿದೆ",
|
||||
"UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
|
||||
"UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
|
||||
"VersionNumber": "ಆವೃತ್ತಿ {0}",
|
||||
"TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ",
|
||||
"TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್",
|
||||
"TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
|
||||
"TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ",
|
||||
"TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್ನೇಲ್ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.",
|
||||
"TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
|
||||
"TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
|
||||
"TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.",
|
||||
"TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್ಕೋಡ್ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
|
||||
"TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡಿ",
|
||||
"Shows": "ಧಾರವಾಹಿಗಳು",
|
||||
"Songs": "ಹಾಡುಗಳು",
|
||||
"StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
|
||||
"UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
|
||||
"UserDownloadingItemWithValues": "{0} ಡೌನ್ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
|
||||
"Sync": "ಹೊಂದಿಕೆ",
|
||||
"System": "ವ್ಯವಸ್ಥೆ",
|
||||
"TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
|
||||
"Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
|
||||
"User": "ಬಳಕೆದಾರ",
|
||||
"HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
|
||||
"Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
|
||||
"ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
|
||||
"LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
|
||||
"LabelRunningTimeValue": "ಅವಧಿ: {0}",
|
||||
"Latest": "ಹೊಸದಾದ",
|
||||
"MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
|
||||
"Movies": "ಚಲನಚಿತ್ರಗಳು",
|
||||
"Music": "ಸಂಗೀತ",
|
||||
"MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು",
|
||||
"NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ",
|
||||
"NameSeasonNumber": "ಸೀಸನ್ {0}",
|
||||
"NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ",
|
||||
"NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ",
|
||||
"NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
|
||||
"NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
|
||||
"NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
|
||||
"NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
|
||||
"NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
|
||||
"NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
|
||||
"Photos": "ಚಿತ್ರಗಳು",
|
||||
"Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
|
||||
"Plugin": "ಪ್ಲಗಿನ್",
|
||||
"PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
|
||||
"PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"ProviderValue": "ಒದಗಿಸುವವರು: {0}",
|
||||
"TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
|
||||
"TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
|
||||
"TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
|
||||
"TaskUpdatePlugins": "ಪ್ಲಗಿನ್ಗಳನ್ನು ನವೀಕರಿಸಿ",
|
||||
"TaskCleanTranscode": "ಟ್ರಾನ್ಸ್ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
|
||||
"TaskRefreshChannels": "ಚಾನಲ್ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
|
||||
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
|
||||
"NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
|
||||
"HeaderRecordingGroups": "Ierakstu Grupas",
|
||||
"HeaderRecordingGroups": "Ierakstu grupas",
|
||||
"UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
|
||||
@ -14,7 +14,7 @@
|
||||
"Photos": "Attēli",
|
||||
"NotificationOptionUserLockedOut": "Lietotājs bloķēts",
|
||||
"LabelRunningTimeValue": "Garums: {0}",
|
||||
"Inherit": "Mantot",
|
||||
"Inherit": "Pārmantot",
|
||||
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
|
||||
"VersionNumber": "Versija {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
|
||||
@ -28,7 +28,7 @@
|
||||
"UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
|
||||
"UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
|
||||
"User": "Lietotājs",
|
||||
"TvShows": "TV Raidījumi",
|
||||
"TvShows": "TV raidījumi",
|
||||
"Sync": "Sinhronizācija",
|
||||
"System": "Sistēma",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.",
|
||||
@ -38,11 +38,11 @@
|
||||
"PluginUninstalledWithName": "{0} tika noņemts",
|
||||
"PluginInstalledWithName": "{0} tika uzstādīts",
|
||||
"Plugin": "Paplašinājums",
|
||||
"Playlists": "Atskaņošanas Saraksti",
|
||||
"Playlists": "Atskaņošanas saraksti",
|
||||
"MixedContent": "Jaukts saturs",
|
||||
"HomeVideos": "Mājas Video",
|
||||
"HomeVideos": "Mājas video",
|
||||
"HeaderNextUp": "Nākamais",
|
||||
"ChapterNameValue": "Nodaļa {0}",
|
||||
"ChapterNameValue": "{0}. nodaļa",
|
||||
"Application": "Lietotne",
|
||||
"NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts",
|
||||
"NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
|
||||
@ -56,14 +56,14 @@
|
||||
"NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams",
|
||||
"NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.",
|
||||
"NameSeasonUnknown": "Nezināma Sezona",
|
||||
"NameSeasonNumber": "Sezona {0}",
|
||||
"NameSeasonUnknown": "Nezināma sezona",
|
||||
"NameSeasonNumber": "{0}. sezona",
|
||||
"NameInstallFailed": "{0} instalācija neizdevās",
|
||||
"MusicVideos": "Mūzikas video",
|
||||
"Music": "Mūzika",
|
||||
"Movies": "Filmas",
|
||||
"MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} tika atjaunota",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
|
||||
"Latest": "Jaunākais",
|
||||
@ -71,57 +71,57 @@
|
||||
"ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
|
||||
"ItemAddedWithName": "{0} tika pievienots bibliotēkai",
|
||||
"HeaderLiveTV": "Tiešraides TV",
|
||||
"HeaderContinueWatching": "Turpināt Skatīšanos",
|
||||
"HeaderAlbumArtists": "Albumu Izpildītāji",
|
||||
"HeaderContinueWatching": "Turpināt skatīšanos",
|
||||
"HeaderAlbumArtists": "Albumu izpildītāji",
|
||||
"Genres": "Žanri",
|
||||
"Folders": "Mapes",
|
||||
"Favorites": "Favorīti",
|
||||
"FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}",
|
||||
"DeviceOnlineWithName": "{0} ir pievienojies",
|
||||
"DeviceOfflineWithName": "{0} ir atvienojies",
|
||||
"Favorites": "Izlase",
|
||||
"FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
|
||||
"DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
|
||||
"DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
|
||||
"Collections": "Kolekcijas",
|
||||
"Channels": "Kanāli",
|
||||
"CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
|
||||
"CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}",
|
||||
"Books": "Grāmatas",
|
||||
"Artists": "Izpildītāji",
|
||||
"Albums": "Albumi",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"HeaderFavoriteSongs": "Dziesmu Favorīti",
|
||||
"HeaderFavoriteShows": "Raidījumu Favorīti",
|
||||
"HeaderFavoriteEpisodes": "Episožu Favorīti",
|
||||
"HeaderFavoriteArtists": "Izpildītāju Favorīti",
|
||||
"HeaderFavoriteAlbums": "Albumu Favorīti",
|
||||
"TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.",
|
||||
"TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
|
||||
"HeaderFavoriteSongs": "Dziesmu izlase",
|
||||
"HeaderFavoriteShows": "Raidījumu izlase",
|
||||
"HeaderFavoriteEpisodes": "Sēriju izlase",
|
||||
"HeaderFavoriteArtists": "Izpildītāju izlase",
|
||||
"HeaderFavoriteAlbums": "Albumu izlase",
|
||||
"TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.",
|
||||
"TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
|
||||
"TasksApplicationCategory": "Lietotne",
|
||||
"TasksLibraryCategory": "Bibliotēka",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
|
||||
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus",
|
||||
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
|
||||
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
|
||||
"TaskRefreshChannels": "Atjaunot Kanālus",
|
||||
"TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.",
|
||||
"TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
|
||||
"TaskRefreshChannels": "Atjaunot kanālus",
|
||||
"TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.",
|
||||
"TaskCleanTranscode": "Iztīrīt transkodēšanas mapi",
|
||||
"TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
|
||||
"TaskUpdatePlugins": "Atjaunot Paplašinājumus",
|
||||
"TaskUpdatePlugins": "Atjaunot paplašinājumus",
|
||||
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
|
||||
"TaskRefreshPeople": "Atjaunot Cilvēkus",
|
||||
"TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
|
||||
"TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
|
||||
"TaskRefreshPeople": "Atjaunot cilvēkus",
|
||||
"TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
|
||||
"TaskCleanLogs": "Iztīrīt logdatņu mapi",
|
||||
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
|
||||
"TaskRefreshLibrary": "Skenēt Multivides Bibliotēku",
|
||||
"TaskRefreshLibrary": "Skenēt multivides bibliotēku",
|
||||
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
|
||||
"TaskCleanCache": "Iztīrīt Kešošanas Mapi",
|
||||
"TasksChannelsCategory": "Interneta Kanāli",
|
||||
"TaskCleanCache": "Iztīrīt kešatmiņas mapi",
|
||||
"TasksChannelsCategory": "Interneta kanāli",
|
||||
"TasksMaintenanceCategory": "Apkope",
|
||||
"Forced": "Piespiests",
|
||||
"Forced": "Piespiedu",
|
||||
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
|
||||
"TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
|
||||
"TaskCleanActivityLog": "Notīrīt darbību žurnālu",
|
||||
"Undefined": "Nenoteikts",
|
||||
"Default": "Noklusējuma",
|
||||
"TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
|
||||
"TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Šī uzdevuma palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
|
||||
"TaskOptimizeDatabase": "Optimizēt datubāzi",
|
||||
"External": "Ārējais",
|
||||
"HearingImpaired": "Ar dzirdes traucējumiem",
|
||||
"TaskKeyframeExtractor": "Atslēgkadru Ekstraktors",
|
||||
"TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
|
||||
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
|
||||
}
|
||||
|
@ -121,5 +121,7 @@
|
||||
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
|
||||
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
|
||||
"HearingImpaired": "കേൾവി തകരാറുകൾ",
|
||||
"External": "പുറമേയുള്ള"
|
||||
"External": "പുറമേയുള്ള",
|
||||
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
|
||||
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"Albums": "Album-album",
|
||||
"Albums": "Album",
|
||||
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
|
||||
"Application": "Aplikasi",
|
||||
"Artists": "Artis-artis",
|
||||
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"Albums": "Albums",
|
||||
"AppDeviceValues": "App: {0}, Apparaat: {1}",
|
||||
"Application": "Toepassing",
|
||||
"Application": "Applicatie",
|
||||
"Artists": "Artiesten",
|
||||
"AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
|
||||
"AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd",
|
||||
"Books": "Boeken",
|
||||
"CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
|
||||
"Channels": "Kanalen",
|
||||
|
@ -24,5 +24,13 @@
|
||||
"TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.",
|
||||
"HeaderAlbumArtists": "Buccaneers o' the musical arts",
|
||||
"HeaderFavoriteAlbums": "Beloved booty o' musical adventures",
|
||||
"HeaderFavoriteArtists": "Treasured scallywags o' the creative seas"
|
||||
"HeaderFavoriteArtists": "Treasured scallywags o' the creative seas",
|
||||
"Channels": "Channels",
|
||||
"Forced": "Pressed",
|
||||
"External": "Outboard",
|
||||
"HeaderFavoriteEpisodes": "Treasured Tales",
|
||||
"HeaderFavoriteShows": "Treasured Tales",
|
||||
"ChapterNameValue": "Piece {0}",
|
||||
"HeaderFavoriteSongs": "Treasured Chimes",
|
||||
"HeaderNextUp": "Incoming"
|
||||
}
|
||||
|
@ -31,13 +31,13 @@
|
||||
"ItemRemovedWithName": "{0} - изъято из медиатеки",
|
||||
"LabelIpAddressValue": "IP-адрес: {0}",
|
||||
"LabelRunningTimeValue": "Длительность: {0}",
|
||||
"Latest": "Новое",
|
||||
"Latest": "Последние добавленные",
|
||||
"MessageApplicationUpdated": "Jellyfin Server был обновлён",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
|
||||
"MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
|
||||
"MixedContent": "Смешанное содержание",
|
||||
"Movies": "Кино",
|
||||
"Movies": "Фильмы",
|
||||
"Music": "Музыка",
|
||||
"MusicVideos": "Муз. видео",
|
||||
"NameInstallFailed": "Установка {0} неудачна",
|
||||
@ -77,7 +77,7 @@
|
||||
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
|
||||
"Sync": "Синхронизация",
|
||||
"System": "Система",
|
||||
"TvShows": "ТВ",
|
||||
"TvShows": "Телесериалы",
|
||||
"User": "Пользователь",
|
||||
"UserCreatedWithName": "Пользователь {0} был создан",
|
||||
"UserDeletedWithName": "Пользователь {0} был удалён",
|
||||
|
1
Emby.Server.Implementations/Localization/Core/si.json
Normal file
1
Emby.Server.Implementations/Localization/Core/si.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
@ -124,5 +124,5 @@
|
||||
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
|
||||
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
|
||||
"External": "Externé",
|
||||
"HearingImpaired": "Sluchovo Postihnutý"
|
||||
"HearingImpaired": "Sluchovo postihnutí"
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
"Collections": "Zbirke",
|
||||
"DeviceOfflineWithName": "{0} je prekinil povezavo",
|
||||
"DeviceOnlineWithName": "{0} je povezan",
|
||||
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}",
|
||||
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
|
||||
"Favorites": "Priljubljeno",
|
||||
"Folders": "Mape",
|
||||
"Genres": "Zvrsti",
|
||||
|
@ -122,5 +122,6 @@
|
||||
"TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்",
|
||||
"TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.",
|
||||
"TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்",
|
||||
"External": "வெளி"
|
||||
"External": "வெளி",
|
||||
"HearingImpaired": "செவித்திறன் குறைபாடுடையவர்"
|
||||
}
|
||||
|
@ -121,5 +121,7 @@
|
||||
"TaskOptimizeDatabase": "ปรับปรุงประสิทธิภาพฐานข้อมูล",
|
||||
"TaskOptimizeDatabaseDescription": "ลดขนาดการจัดเก็บฐานข้อมูล ใช้งานคำสั่งนี้หลังจากสแกนไลบรารีหรือหลังจากการเปลี่ยนแปลงฐานข้อมูล อาจจะทำให้ระบบทำงานเร็วขึ้น",
|
||||
"External": "ภายนอก",
|
||||
"HearingImpaired": "บกพร่องทางการได้ยิน"
|
||||
"HearingImpaired": "บกพร่องทางการได้ยิน",
|
||||
"TaskKeyframeExtractor": "ตัวแยกคีย์เฟรม",
|
||||
"TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน"
|
||||
}
|
||||
|
@ -3,19 +3,19 @@
|
||||
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
|
||||
"Application": "Uygulama",
|
||||
"Artists": "Sanatçılar",
|
||||
"AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı",
|
||||
"AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı",
|
||||
"Books": "Kitaplar",
|
||||
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
|
||||
"Channels": "Kanallar",
|
||||
"ChapterNameValue": "Bölüm {0}",
|
||||
"ChapterNameValue": "{0}. Bölüm",
|
||||
"Collections": "Koleksiyonlar",
|
||||
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
||||
"DeviceOnlineWithName": "{0} bağlı",
|
||||
"FailedLoginAttemptWithUserName": "{0} adresinden giriş denemesi başarısız oldu",
|
||||
"FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
|
||||
"Favorites": "Favoriler",
|
||||
"Folders": "Klasörler",
|
||||
"Genres": "Türler",
|
||||
"HeaderAlbumArtists": "Albüm Sanatçıları",
|
||||
"HeaderAlbumArtists": "Albüm sanatçıları",
|
||||
"HeaderContinueWatching": "İzlemeye Devam Et",
|
||||
"HeaderFavoriteAlbums": "Favori Albümler",
|
||||
"HeaderFavoriteArtists": "Favori Sanatçılar",
|
||||
@ -25,7 +25,7 @@
|
||||
"HeaderLiveTV": "Canlı TV",
|
||||
"HeaderNextUp": "Gelecek Hafta",
|
||||
"HeaderRecordingGroups": "Kayıt Grupları",
|
||||
"HomeVideos": "Ana sayfa videoları",
|
||||
"HomeVideos": "Ana Sayfa Videoları",
|
||||
"Inherit": "Devral",
|
||||
"ItemAddedWithName": "{0} kütüphaneye eklendi",
|
||||
"ItemRemovedWithName": "{0} kütüphaneden silindi",
|
||||
@ -34,14 +34,14 @@
|
||||
"Latest": "En son",
|
||||
"MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Sunucu ayar kısmı {0} güncellendi",
|
||||
"MessageServerConfigurationUpdated": "Sunucu ayarları güncellendi",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Sunucu yapılandırma bölümü {0} güncellendi",
|
||||
"MessageServerConfigurationUpdated": "Sunucu yapılandırması güncellendi",
|
||||
"MixedContent": "Karışık içerik",
|
||||
"Movies": "Filmler",
|
||||
"Music": "Müzik",
|
||||
"MusicVideos": "Müzik videoları",
|
||||
"MusicVideos": "Müzik Videoları",
|
||||
"NameInstallFailed": "{0} kurulumu başarısız",
|
||||
"NameSeasonNumber": "Sezon {0}",
|
||||
"NameSeasonNumber": "{0}. Sezon",
|
||||
"NameSeasonUnknown": "Bilinmeyen Sezon",
|
||||
"NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut",
|
||||
@ -55,9 +55,9 @@
|
||||
"NotificationOptionPluginInstalled": "Eklenti yüklendi",
|
||||
"NotificationOptionPluginUninstalled": "Eklenti kaldırıldı",
|
||||
"NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi",
|
||||
"NotificationOptionServerRestartRequired": "Sunucu yeniden başlatma gerekli",
|
||||
"NotificationOptionServerRestartRequired": "Sunucunun yeniden başlatılması gerekiyor",
|
||||
"NotificationOptionTaskFailed": "Zamanlanmış görev hatası",
|
||||
"NotificationOptionUserLockedOut": "Kullanıcı kitlendi",
|
||||
"NotificationOptionUserLockedOut": "Kullanıcı kilitlendi",
|
||||
"NotificationOptionVideoPlayback": "Video oynatma başladı",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu",
|
||||
"Photos": "Fotoğraflar",
|
||||
@ -74,36 +74,36 @@
|
||||
"Songs": "Şarkılar",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi",
|
||||
"SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} sağlayıcısından indirilemedi",
|
||||
"Sync": "Eşzamanlama",
|
||||
"System": "Sistem",
|
||||
"TvShows": "Diziler",
|
||||
"User": "Kullanıcı",
|
||||
"UserCreatedWithName": "{0} kullanıcısı oluşturuldu",
|
||||
"UserDeletedWithName": "Kullanıcı {0} silindi",
|
||||
"UserDownloadingItemWithValues": "{0} indiriliyor {1}",
|
||||
"UserLockedOutWithName": "Kullanıcı {0} kitlendi",
|
||||
"UserOfflineFromDevice": "{0}, {1} ile bağlantısı kesildi",
|
||||
"UserOnlineFromDevice": "{0}, {1} çevrimiçi",
|
||||
"UserPasswordChangedWithName": "{0} kullanıcısı için şifre değiştirildi",
|
||||
"UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için güncellendi",
|
||||
"UserDeletedWithName": "{0} kullanıcısı silindi",
|
||||
"UserDownloadingItemWithValues": "{0} {1} medyasını indiriyor",
|
||||
"UserLockedOutWithName": "{0} adlı kullanıcı kilitlendi",
|
||||
"UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi",
|
||||
"UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi",
|
||||
"UserPasswordChangedWithName": "{0} kullanıcısının parolası değiştirildi",
|
||||
"UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi",
|
||||
"UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
|
||||
"UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
|
||||
"ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi",
|
||||
"ValueSpecialEpisodeName": "Özel - {0}",
|
||||
"VersionNumber": "Sürüm {0}",
|
||||
"TaskCleanCache": "Geçici dosya klasörünü temizle",
|
||||
"TasksChannelsCategory": "İnternet kanalları",
|
||||
"TaskCleanCache": "Geçici Dosya Klasörünü Temizle",
|
||||
"TasksChannelsCategory": "İnternet Kanalları",
|
||||
"TasksApplicationCategory": "Uygulama",
|
||||
"TasksLibraryCategory": "Kütüphane",
|
||||
"TasksMaintenanceCategory": "Bakım",
|
||||
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.",
|
||||
"TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
|
||||
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
|
||||
"TaskRefreshChannels": "Kanalları Yenile",
|
||||
"TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.",
|
||||
"TaskCleanTranscode": "Dönüşüm Dizinini Temizle",
|
||||
"TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.",
|
||||
"TaskCleanTranscode": "Kod Dönüştürme Dizinini Temizle",
|
||||
"TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
|
||||
"TaskUpdatePlugins": "Eklentileri Güncelle",
|
||||
"TaskRefreshPeople": "Kullanıcıları Yenile",
|
||||
|
@ -25,5 +25,14 @@
|
||||
"Channels": "Amashaneli",
|
||||
"Books": "Izincwadi",
|
||||
"Artists": "Abadlali",
|
||||
"Albums": "Ama-albhamu"
|
||||
"Albums": "Ama-albhamu",
|
||||
"CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
|
||||
"HeaderFavoriteArtists": "Abasethi Abathandekayo",
|
||||
"HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
|
||||
"HeaderFavoriteShows": "Izisho Ezithandekayo",
|
||||
"External": "Kwezifungo",
|
||||
"FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
|
||||
"HeaderContinueWatching": "Buyela Ukubona",
|
||||
"HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
|
||||
"HeaderAlbumArtists": "Abasethi wenkulumo"
|
||||
}
|
||||
|
@ -71,25 +71,28 @@ namespace Emby.Server.Implementations.Localization
|
||||
string countryCode = resource.Substring(RatingsPath.Length, 2);
|
||||
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
await using var stream = _assembly.GetManifestResourceStream(resource);
|
||||
using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
|
||||
await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
|
||||
var stream = _assembly.GetManifestResourceStream(resource);
|
||||
await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
using var reader = new StreamReader(stream!);
|
||||
await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string[] parts = line.Split(',');
|
||||
if (parts.Length == 2
|
||||
&& int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
var name = parts[0];
|
||||
dict.Add(name, new ParentalRating(name, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
|
||||
string[] parts = line.Split(',');
|
||||
if (parts.Length == 2
|
||||
&& int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
var name = parts[0];
|
||||
dict.Add(name, new ParentalRating(name, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,10 +4,14 @@ G,0
|
||||
M,15
|
||||
MA,15
|
||||
MA15+,15
|
||||
MA 15+,15
|
||||
PG,16
|
||||
16+,16
|
||||
R,18
|
||||
R18+,18
|
||||
X18+,18
|
||||
R 18+,18
|
||||
18+,18
|
||||
X18+,1000
|
||||
X 18+,1000
|
||||
X,1000
|
||||
RC,1001
|
||||
|
|
@ -1,12 +1,17 @@
|
||||
Educational,0
|
||||
Infoprogramm,0
|
||||
FSK-0,0
|
||||
FSK 0,0
|
||||
0,0
|
||||
FSK-6,6
|
||||
FSK 6,6
|
||||
6,6
|
||||
FSK-12,12
|
||||
FSK 12,12
|
||||
12,12
|
||||
FSK-16,16
|
||||
FSK 16,16
|
||||
16,16
|
||||
FSK-18,18
|
||||
FSK 18,18
|
||||
18,18
|
||||
|
|
@ -3,6 +3,7 @@ A/fig,0
|
||||
A/i,0
|
||||
A/fig/i,0
|
||||
APTA,0
|
||||
ERI,0
|
||||
TP,0
|
||||
0+,0
|
||||
6+,6
|
||||
|
|
@ -1,5 +1,6 @@
|
||||
Public Averti,0
|
||||
Tous Publics,0
|
||||
TP,0
|
||||
U,0
|
||||
0+,0
|
||||
6+,6
|
||||
|
|
6
Emby.Server.Implementations/Localization/Ratings/sk.csv
Normal file
6
Emby.Server.Implementations/Localization/Ratings/sk.csv
Normal file
@ -0,0 +1,6 @@
|
||||
NR,0
|
||||
U,0
|
||||
7,7
|
||||
12,12
|
||||
15,15
|
||||
18,18
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user