diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000000..dbe78984a2
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-ef": {
+ "version": "7.0.13",
+ "commands": [
+ "dotnet-ef"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
similarity index 64%
rename from .github/workflows/codeql-analysis.yml
rename to .github/workflows/ci-codeql-analysis.yml
index 8e764b0194..f43d743f04 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ 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@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
+ uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
+ uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
+ uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
diff --git a/.github/workflows/openapi.yml b/.github/workflows/ci-openapi.yml
similarity index 93%
rename from .github/workflows/openapi.yml
rename to .github/workflows/ci-openapi.yml
index 693f98d160..8c463a8fcf 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -14,7 +14,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -39,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -112,7 +112,7 @@ jobs:
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -127,7 +127,7 @@ jobs:
- name: Edit difference comment (unchanged)
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
new file mode 100644
index 0000000000..36686e64ba
--- /dev/null
+++ b/.github/workflows/ci-tests.yml
@@ -0,0 +1,50 @@
+name: Tests
+on:
+ push:
+ branches:
+ - master
+ # Run tests against the forked branch, but
+ # do not allow access to secrets
+ # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflows-in-forked-repositories
+ pull_request:
+
+env:
+ SDK_VERSION: "7.0.x"
+
+jobs:
+ run-tests:
+ strategy:
+ matrix:
+ os: ["ubuntu-latest", "macos-latest", "windows-latest"]
+
+ runs-on: "${{ matrix.os }}"
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
+
+ - uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3
+ with:
+ dotnet-version: ${{ env.SDK_VERSION }}
+
+ - name: Run DotNet CLI Tests
+ run: >
+ dotnet test Jellyfin.sln
+ --configuration Release
+ --collect:"XPlat Code Coverage"
+ --settings tests/coverletArgs.runsettings
+ --verbosity minimal
+
+ - name: Merge code coverage results
+ uses: danielpalme/ReportGenerator-GitHub-Action@873ee34c88a6234bdab7fd264d3666fd1ab417f7 # 5
+ with:
+ reports: "**/coverage.cobertura.xml"
+ targetdir: "merged/"
+ reporttypes: "Cobertura"
+
+ # TODO - which action / tool to use to publish code coverage results?
+ # - name: Publish code coverage results
+
+ - name: Publish OpenAPI Artifact
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3
+ with:
+ name: "OpenAPI Spec"
+ path: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net*/openapi.json"
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index ba7883a734..75b6a73e56 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -51,14 +51,14 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -93,7 +93,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -108,7 +108,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
- uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+ uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/issue-stale.yml
similarity index 56%
rename from .github/workflows/repo-stale.yaml
rename to .github/workflows/issue-stale.yml
index 4eb0cf0996..926a7fbfb0 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/issue-stale.yml
@@ -1,8 +1,8 @@
-name: Stale Check
+name: Stale Issue Labeler
on:
schedule:
- - cron: '30 */12 * * *'
+ - cron: '30 1 * * *'
workflow_dispatch:
permissions:
@@ -16,37 +16,20 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8
+ - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
+ ascending: true
days-before-stale: 120
days-before-pr-stale: -1
days-before-close: 21
days-before-pr-close: -1
- operations-per-run: 75
+ operations-per-run: 500
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
stale-issue-message: |-
- 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.
+ 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 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
- runs-on: ubuntu-latest
- if: ${{ contains(github.repository, 'jellyfin/') }}
- steps:
- - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
- with:
- repo-token: ${{ secrets.JF_BOT_TOKEN }}
- operations-per-run: 75
- # The merge conflict action will remove the label when updated
- remove-stale-when-updated: false
- days-before-stale: -1
- days-before-close: 90
- days-before-issue-close: -1
- stale-pr-label: merge conflict
- close-pr-message: |-
- This PR has been closed due to having unresolved merge conflicts.
diff --git a/.github/workflows/automation.yml b/.github/workflows/project-automation.yml
similarity index 82%
rename from .github/workflows/automation.yml
rename to .github/workflows/project-automation.yml
index 47abce02a3..3637eb16ad 100644
--- a/.github/workflows/automation.yml
+++ b/.github/workflows/project-automation.yml
@@ -1,4 +1,4 @@
-name: Automation
+name: Project Automation
on:
push:
@@ -9,19 +9,6 @@ on:
permissions: {}
jobs:
- label:
- name: Labeling
- runs-on: ubuntu-latest
- if: ${{ github.repository == 'jellyfin/jellyfin' }}
- steps:
- - name: Apply label
- uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
- if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
- with:
- dirtyLabel: 'merge conflict'
- commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
- repoToken: ${{ secrets.JF_BOT_TOKEN }}
-
project:
name: Project board
runs-on: ubuntu-latest
diff --git a/.github/workflows/pull-request-conflict.yml b/.github/workflows/pull-request-conflict.yml
new file mode 100644
index 0000000000..05517bb030
--- /dev/null
+++ b/.github/workflows/pull-request-conflict.yml
@@ -0,0 +1,23 @@
+name: Merge Conflict Labeler
+
+on:
+ push:
+ branches:
+ - master
+ pull_request_target:
+ issue_comment:
+
+permissions: {}
+jobs:
+ label:
+ name: Labeling
+ runs-on: ubuntu-latest
+ if: ${{ github.repository == 'jellyfin/jellyfin' }}
+ steps:
+ - name: Apply label
+ uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
+ if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
+ with:
+ dirtyLabel: 'merge conflict'
+ commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
+ repoToken: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml
new file mode 100644
index 0000000000..de093a9887
--- /dev/null
+++ b/.github/workflows/pull-request-stale.yaml
@@ -0,0 +1,30 @@
+name: Stale PR Check
+
+on:
+ schedule:
+ - cron: '30 */12 * * *'
+ workflow_dispatch:
+
+permissions:
+ pull-requests: write
+ actions: write
+
+jobs:
+ prs-stale-conflicts:
+ name: Check PRs with merge conflicts
+ runs-on: ubuntu-latest
+ if: ${{ contains(github.repository, 'jellyfin/') }}
+ steps:
+ - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
+ with:
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+ ascending: true
+ operations-per-run: 150
+ # The merge conflict action will remove the label when updated
+ remove-stale-when-updated: false
+ days-before-stale: -1
+ days-before-close: 90
+ days-before-issue-close: -1
+ stale-pr-label: merge conflict
+ close-pr-message: |-
+ This PR has been closed due to having unresolved merge conflicts.
diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/release-bump-version.yaml
similarity index 93%
rename from .github/workflows/repo-bump-version.yaml
rename to .github/workflows/release-bump-version.yaml
index 0ba68dda36..e0383afd23 100644
--- a/.github/workflows/repo-bump-version.yaml
+++ b/.github/workflows/release-bump-version.yaml
@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
ref: ${{ env.TAG_BRANCH }}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index dfdfabd189..571da95bbf 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -57,6 +57,7 @@
- [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
+ - [iwalton3](https://github.com/iwalton3)
- [jftuga](https://github.com/jftuga)
- [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h)
@@ -88,6 +89,7 @@
- [neilsb](https://github.com/neilsb)
- [nevado](https://github.com/nevado)
- [Nickbert7](https://github.com/Nickbert7)
+ - [nicknsy](https://github.com/nicknsy)
- [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka)
- [OancaAndrei](https://github.com/OancaAndrei)
@@ -168,6 +170,8 @@
- [TheTyrius](https://github.com/TheTyrius)
- [tallbl0nde](https://github.com/tallbl0nde)
- [sleepycatcoding](https://github.com/sleepycatcoding)
+ - [scampower3](https://github.com/scampower3)
+ - [Chris-Codes-It] (https://github.com/Chris-Codes-It)
- [Pithaya](https://github.com/Pithaya)
# Emby Contributors
@@ -239,4 +243,4 @@
- [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir)
- [JPUC1143](https://github.com/Jpuc1143/)
- - [0x25CBFC4F](https://github.com/0x25CBFC4F)
+ - [0x25CBFC4F](https://github.com/0x25CBFC4F)
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8cf3eae2a5..9109a5a18a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,9 +2,7 @@
true
-
-
@@ -17,23 +15,23 @@
-
+
-
+
-
-
+
+
-
+
-
-
-
-
-
+
+
+
+
+
@@ -42,14 +40,14 @@
-
-
+
+
-
+
@@ -57,14 +55,14 @@
-
+
-
+
-
+
@@ -72,9 +70,9 @@
-
+
@@ -86,8 +84,8 @@
-
+
-
+
-
+
\ No newline at end of file
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index abd594a3a1..99068826d9 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -494,7 +494,7 @@ namespace Emby.Dlna.ContentDirectory
{
var folder = (Folder)item;
- string[] mediaTypes = Array.Empty();
+ MediaType[] mediaTypes = Array.Empty();
bool? isFolder = null;
switch (search.SearchType)
@@ -565,30 +565,18 @@ namespace Emby.Dlna.ContentDirectory
if (stubType != StubType.Folder && item is IHasCollectionType collectionFolder)
{
- var collectionType = collectionFolder.CollectionType;
- if (string.Equals(CollectionType.Music, collectionType, StringComparison.OrdinalIgnoreCase))
+ switch (collectionFolder.CollectionType)
{
- return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
- }
-
- if (string.Equals(CollectionType.Movies, collectionType, StringComparison.OrdinalIgnoreCase))
- {
- return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
- }
-
- if (string.Equals(CollectionType.TvShows, collectionType, StringComparison.OrdinalIgnoreCase))
- {
- return GetTvFolders(item, user, stubType, sort, startIndex, limit);
- }
-
- if (string.Equals(CollectionType.Folders, collectionType, StringComparison.OrdinalIgnoreCase))
- {
- return GetFolders(user, startIndex, limit);
- }
-
- if (string.Equals(CollectionType.LiveTv, collectionType, StringComparison.OrdinalIgnoreCase))
- {
- return GetLiveTvChannels(user, sort, startIndex, limit);
+ case CollectionType.Music:
+ return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
+ case CollectionType.Movies:
+ return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
+ case CollectionType.TvShows:
+ return GetTvFolders(item, user, stubType, sort, startIndex, limit);
+ case CollectionType.Folders:
+ return GetFolders(user, startIndex, limit);
+ case CollectionType.LiveTv:
+ return GetLiveTvChannels(user, sort, startIndex, limit);
}
}
@@ -917,7 +905,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult GetGenres(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var genresResult = _libraryManager.GetGenres(query);
@@ -933,7 +921,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult GetMusicGenres(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var genresResult = _libraryManager.GetMusicGenres(query);
@@ -949,7 +937,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var artists = _libraryManager.GetAlbumArtists(query);
@@ -965,7 +953,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult GetMusicArtists(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
var artists = _libraryManager.GetArtists(query);
return ToResult(query.StartIndex, artists);
@@ -980,7 +968,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult GetFavoriteArtists(BaseItem parent, InternalItemsQuery query)
{
// Don't sort
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
query.AncestorIds = new[] { parent.Id };
query.IsFavorite = true;
var artists = _libraryManager.GetArtists(query);
@@ -1011,7 +999,7 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private QueryResult GetNextUp(BaseItem parent, InternalItemsQuery query)
{
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
var result = _tvSeriesManager.GetNextUp(
new NextUpQuery
@@ -1036,7 +1024,7 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private QueryResult GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
{
- query.OrderBy = Array.Empty<(string, SortOrder)>();
+ query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
@@ -1203,9 +1191,9 @@ namespace Emby.Dlna.ContentDirectory
///
/// The .
/// True if pre-sorted.
- private static (string SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
+ private static (ItemSortBy SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
{
- return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
+ return isPreSorted ? Array.Empty<(ItemSortBy, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
}
///
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index 5ed982876d..9f152df132 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -174,13 +174,14 @@ namespace Emby.Dlna.Didl
if (item is IHasMediaSources)
{
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ switch (item.MediaType)
{
- AddAudioResource(writer, item, deviceId, filter, streamInfo);
- }
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
- {
- AddVideoResource(writer, item, deviceId, filter, streamInfo);
+ case MediaType.Audio:
+ AddAudioResource(writer, item, deviceId, filter, streamInfo);
+ break;
+ case MediaType.Video:
+ AddVideoResource(writer, item, deviceId, filter, streamInfo);
+ break;
}
}
@@ -821,15 +822,15 @@ namespace Emby.Dlna.Didl
writer.WriteString(classType ?? "object.container.storageFolder");
}
- else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Audio)
{
writer.WriteString("object.item.audioItem.musicTrack");
}
- else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Photo)
{
writer.WriteString("object.item.imageItem.photo");
}
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Video)
{
if (!_profile.RequiresPlainVideoItems && item is Movie)
{
@@ -1006,8 +1007,7 @@ namespace Emby.Dlna.Didl
if (!_profile.EnableAlbumArtInDidl)
{
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
- || string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Audio || item.MediaType == MediaType.Video)
{
if (!stubType.HasValue)
{
@@ -1016,7 +1016,7 @@ namespace Emby.Dlna.Didl
}
}
- if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
+ if (!_profile.EnableSingleAlbumArtLimit || item.MediaType == MediaType.Photo)
{
AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG");
AddImageResElement(item, writer, 1024, 768, "jpg", "JPEG_MED");
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 99b3e6e7ef..d67cb67b54 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -228,7 +228,7 @@ namespace Emby.Dlna
try
{
return _fileSystem.GetFilePaths(path)
- .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
+ .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
.Select(i => ParseProfileFile(i, type))
.Where(i => i is not null)
.ToList()!; // We just filtered out all the nulls
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index aca2399644..efbef05640 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -26,8 +26,12 @@
false
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..82c80070a5
--- /dev/null
+++ b/Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
@@ -0,0 +1,72 @@
+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.Main;
+using Emby.Dlna.MediaReceiverRegistrar;
+using Emby.Dlna.Ssdp;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Net;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Rssdp.Infrastructure;
+
+namespace Emby.Dlna.Extensions;
+
+///
+/// Extension methods for adding DLNA services.
+///
+public static class DlnaServiceCollectionExtensions
+{
+ ///
+ /// Adds DLNA services to the provided .
+ ///
+ /// The .
+ /// The .
+ public static void AddDlnaServices(
+ this IServiceCollection services,
+ IServerApplicationHost applicationHost)
+ {
+ services.AddHttpClient(NamedClient.Dlna, c =>
+ {
+ c.DefaultRequestHeaders.UserAgent.ParseAdd(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}/{1} UPnP/1.0 {2}/{3}",
+ Environment.OSVersion.Platform,
+ Environment.OSVersion,
+ applicationHost.Name,
+ applicationHost.ApplicationVersionString));
+
+ c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
+ c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from?
+ })
+ .ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler
+ {
+ AutomaticDecompression = DecompressionMethods.All,
+ RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
+ });
+
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddSingleton(provider => new SsdpCommunicationsServer(
+ provider.GetRequiredService(),
+ provider.GetRequiredService(),
+ provider.GetRequiredService>())
+ {
+ IsShared = true
+ });
+
+ services.AddHostedService();
+ }
+}
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
deleted file mode 100644
index 1a4bec398c..0000000000
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ /dev/null
@@ -1,463 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-using Emby.Dlna.PlayTo;
-using Emby.Dlna.Ssdp;
-using Jellyfin.Networking.Configuration;
-using Jellyfin.Networking.Extensions;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
-using MediaBrowser.Controller.Drawing;
-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;
-
-namespace Emby.Dlna.Main
-{
- public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
- {
- private readonly IServerConfigurationManager _config;
- private readonly ILogger _logger;
- private readonly IServerApplicationHost _appHost;
- private readonly ISessionManager _sessionManager;
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDlnaManager _dlnaManager;
- private readonly IImageProcessor _imageProcessor;
- private readonly IUserDataManager _userDataManager;
- private readonly ILocalizationManager _localization;
- private readonly IMediaSourceManager _mediaSourceManager;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IDeviceDiscovery _deviceDiscovery;
- private readonly ISocketFactory _socketFactory;
- private readonly INetworkManager _networkManager;
- private readonly object _syncLock = new object();
- private readonly bool _disabled;
-
- private PlayToManager _manager;
- private SsdpDevicePublisher _publisher;
- private ISsdpCommunicationsServer _communicationsServer;
-
- private bool _disposed;
-
- public DlnaEntryPoint(
- IServerConfigurationManager config,
- ILoggerFactory loggerFactory,
- IServerApplicationHost appHost,
- ISessionManager sessionManager,
- IHttpClientFactory httpClientFactory,
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDlnaManager dlnaManager,
- IImageProcessor imageProcessor,
- IUserDataManager userDataManager,
- ILocalizationManager localizationManager,
- IMediaSourceManager mediaSourceManager,
- IDeviceDiscovery deviceDiscovery,
- IMediaEncoder mediaEncoder,
- ISocketFactory socketFactory,
- INetworkManager networkManager,
- IUserViewManager userViewManager,
- ITVSeriesManager tvSeriesManager)
- {
- _config = config;
- _appHost = appHost;
- _sessionManager = sessionManager;
- _httpClientFactory = httpClientFactory;
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dlnaManager = dlnaManager;
- _imageProcessor = imageProcessor;
- _userDataManager = userDataManager;
- _localization = localizationManager;
- _mediaSourceManager = mediaSourceManager;
- _deviceDiscovery = deviceDiscovery;
- _mediaEncoder = mediaEncoder;
- _socketFactory = socketFactory;
- _networkManager = networkManager;
- _logger = loggerFactory.CreateLogger();
-
- ContentDirectory = new ContentDirectory.ContentDirectoryService(
- dlnaManager,
- userDataManager,
- imageProcessor,
- libraryManager,
- config,
- userManager,
- loggerFactory.CreateLogger(),
- httpClientFactory,
- localizationManager,
- mediaSourceManager,
- userViewManager,
- mediaEncoder,
- tvSeriesManager);
-
- ConnectionManager = new ConnectionManager.ConnectionManagerService(
- dlnaManager,
- config,
- loggerFactory.CreateLogger(),
- httpClientFactory);
-
- MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
- loggerFactory.CreateLogger(),
- httpClientFactory,
- config);
- Current = this;
-
- var netConfig = config.GetConfiguration(NetworkConfigurationStore.StoreKey);
- _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
-
- if (_disabled && _config.GetDlnaConfiguration().EnableServer)
- {
- _logger.LogError("The DLNA specification does not support HTTPS.");
- }
- }
-
- public static DlnaEntryPoint Current { get; private set; }
-
- ///
- /// Gets a value indicating whether the dlna server is enabled.
- ///
- public static bool Enabled { get; private set; }
-
- public IContentDirectory ContentDirectory { get; private set; }
-
- public IConnectionManager ConnectionManager { get; private set; }
-
- public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
-
- public async Task RunAsync()
- {
- await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
-
- if (_disabled)
- {
- // No use starting as dlna won't work, as we're running purely on HTTPS.
- return;
- }
-
- ReloadComponents();
-
- _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
- }
-
- private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e)
- {
- if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
- {
- ReloadComponents();
- }
- }
-
- private void ReloadComponents()
- {
- var options = _config.GetDlnaConfiguration();
- Enabled = options.EnableServer;
-
- StartSsdpHandler();
-
- if (options.EnableServer)
- {
- StartDevicePublisher(options);
- }
- else
- {
- DisposeDevicePublisher();
- }
-
- if (options.EnablePlayTo)
- {
- StartPlayToManager();
- }
- else
- {
- DisposePlayToManager();
- }
- }
-
- private void StartSsdpHandler()
- {
- 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);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error starting device discovery");
- }
- }
-
- 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 (_publisher is not null)
- {
- return;
- }
-
- try
- {
- _publisher = new SsdpDevicePublisher(
- _communicationsServer,
- Environment.OSVersion.Platform.ToString(),
- // Can not use VersionString here since that includes OS and version
- Environment.OSVersion.Version.ToString(),
- _config.GetDlnaConfiguration().SendOnlyMatchedHost)
- {
- LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
- SupportPnpRootDevice = false
- };
-
- RegisterServerEndpoints();
-
- if (options.BlastAliveMessages)
- {
- _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error registering endpoint");
- }
- }
-
- private void RegisterServerEndpoints()
- {
- var udn = CreateUuid(_appHost.SystemId);
- var descriptorUri = "/dlna/" + udn + "/description.xml";
-
- // 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 (validInterfaces.Count == 0)
- {
- // No interfaces returned, fall back to loopback
- validInterfaces = _networkManager.GetLoopbacks().ToList();
- }
-
- foreach (var intf in validInterfaces)
- {
- var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
-
- _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
-
- 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 = intf.Address,
- PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix),
- FriendlyName = "Jellyfin",
- Manufacturer = "Jellyfin",
- ModelName = "Jellyfin Server",
- Uuid = udn
- // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
- };
-
- SetProperies(device, fullService);
- _publisher.AddDevice(device);
-
- var embeddedDevices = new[]
- {
- "urn:schemas-upnp-org:service:ContentDirectory:1",
- "urn:schemas-upnp-org:service:ConnectionManager:1",
- // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
- };
-
- foreach (var subDevice in embeddedDevices)
- {
- var embeddedDevice = new SsdpEmbeddedDevice
- {
- FriendlyName = device.FriendlyName,
- Manufacturer = device.Manufacturer,
- ModelName = device.ModelName,
- Uuid = udn
- // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
- };
-
- SetProperies(embeddedDevice, subDevice);
- device.AddDevice(embeddedDevice);
- }
- }
- }
-
- private string CreateUuid(string text)
- {
- if (!Guid.TryParse(text, out var guid))
- {
- guid = text.GetMD5();
- }
-
- return guid.ToString("D", CultureInfo.InvariantCulture);
- }
-
- private void SetProperies(SsdpDevice device, string fullDeviceType)
- {
- var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
-
- var serviceParts = service.Split(':');
-
- var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
-
- device.DeviceTypeNamespace = deviceTypeNamespace;
- device.DeviceClass = serviceParts[1];
- device.DeviceType = serviceParts[2];
- }
-
- private void StartPlayToManager()
- {
- lock (_syncLock)
- {
- if (_manager is not null)
- {
- return;
- }
-
- try
- {
- _manager = new PlayToManager(
- _logger,
- _sessionManager,
- _libraryManager,
- _userManager,
- _dlnaManager,
- _appHost,
- _imageProcessor,
- _deviceDiscovery,
- _httpClientFactory,
- _userDataManager,
- _localization,
- _mediaSourceManager,
- _mediaEncoder);
-
- _manager.Start();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error starting PlayTo manager");
- }
- }
- }
-
- private void DisposePlayToManager()
- {
- lock (_syncLock)
- {
- if (_manager is not null)
- {
- try
- {
- _logger.LogInformation("Disposing PlayToManager");
- _manager.Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error disposing PlayTo manager");
- }
-
- _manager = null;
- }
- }
- }
-
- public void DisposeDevicePublisher()
- {
- if (_publisher is not null)
- {
- _logger.LogInformation("Disposing SsdpDevicePublisher");
- _publisher.Dispose();
- _publisher = null;
- }
- }
-
- ///
- public void Dispose()
- {
- if (_disposed)
- {
- return;
- }
-
- DisposeDevicePublisher();
- DisposePlayToManager();
- DisposeDeviceDiscovery();
-
- if (_communicationsServer is not null)
- {
- _logger.LogInformation("Disposing SsdpCommunicationsServer");
- _communicationsServer.Dispose();
- _communicationsServer = null;
- }
-
- ContentDirectory = null;
- ConnectionManager = null;
- MediaReceiverRegistrar = null;
- Current = null;
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Dlna/Main/DlnaHost.cs b/Emby.Dlna/Main/DlnaHost.cs
new file mode 100644
index 0000000000..3896b74a1b
--- /dev/null
+++ b/Emby.Dlna/Main/DlnaHost.cs
@@ -0,0 +1,389 @@
+#pragma warning disable CA1031 // Do not catch general exception types.
+
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Dlna.PlayTo;
+using Emby.Dlna.Ssdp;
+using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Extensions;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Globalization;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Rssdp;
+using Rssdp.Infrastructure;
+
+namespace Emby.Dlna.Main;
+
+///
+/// A that manages a DLNA server.
+///
+public sealed class DlnaHost : IHostedService, IDisposable
+{
+ private readonly ILogger _logger;
+ private readonly IServerConfigurationManager _config;
+ private readonly IServerApplicationHost _appHost;
+ private readonly ISessionManager _sessionManager;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDlnaManager _dlnaManager;
+ private readonly IImageProcessor _imageProcessor;
+ private readonly IUserDataManager _userDataManager;
+ private readonly ILocalizationManager _localization;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IDeviceDiscovery _deviceDiscovery;
+ private readonly ISsdpCommunicationsServer _communicationsServer;
+ private readonly INetworkManager _networkManager;
+ private readonly object _syncLock = new();
+
+ private SsdpDevicePublisher? _publisher;
+ private PlayToManager? _manager;
+ private bool _disposed;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ /// The .
+ public DlnaHost(
+ IServerConfigurationManager config,
+ ILoggerFactory loggerFactory,
+ IServerApplicationHost appHost,
+ ISessionManager sessionManager,
+ IHttpClientFactory httpClientFactory,
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDlnaManager dlnaManager,
+ IImageProcessor imageProcessor,
+ IUserDataManager userDataManager,
+ ILocalizationManager localizationManager,
+ IMediaSourceManager mediaSourceManager,
+ IDeviceDiscovery deviceDiscovery,
+ IMediaEncoder mediaEncoder,
+ ISsdpCommunicationsServer communicationsServer,
+ INetworkManager networkManager)
+ {
+ _config = config;
+ _appHost = appHost;
+ _sessionManager = sessionManager;
+ _httpClientFactory = httpClientFactory;
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dlnaManager = dlnaManager;
+ _imageProcessor = imageProcessor;
+ _userDataManager = userDataManager;
+ _localization = localizationManager;
+ _mediaSourceManager = mediaSourceManager;
+ _deviceDiscovery = deviceDiscovery;
+ _mediaEncoder = mediaEncoder;
+ _communicationsServer = communicationsServer;
+ _networkManager = networkManager;
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ ///
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ var netConfig = _config.GetConfiguration(NetworkConfigurationStore.StoreKey);
+ if (_appHost.ListenWithHttps && netConfig.RequireHttps)
+ {
+ if (_config.GetDlnaConfiguration().EnableServer)
+ {
+ _logger.LogError("The DLNA specification does not support HTTPS.");
+ }
+
+ // No use starting as dlna won't work, as we're running purely on HTTPS.
+ return;
+ }
+
+ await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
+ ReloadComponents();
+
+ _config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
+ }
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken)
+ {
+ Stop();
+
+ return Task.CompletedTask;
+ }
+
+ ///
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ Stop();
+ _disposed = true;
+ }
+ }
+
+ private void OnNamedConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs e)
+ {
+ if (string.Equals(e.Key, "dlna", StringComparison.OrdinalIgnoreCase))
+ {
+ ReloadComponents();
+ }
+ }
+
+ private void ReloadComponents()
+ {
+ var options = _config.GetDlnaConfiguration();
+ StartDeviceDiscovery();
+
+ if (options.EnableServer)
+ {
+ StartDevicePublisher(options);
+ }
+ else
+ {
+ DisposeDevicePublisher();
+ }
+
+ if (options.EnablePlayTo)
+ {
+ StartPlayToManager();
+ }
+ else
+ {
+ DisposePlayToManager();
+ }
+ }
+
+ private static string CreateUuid(string text)
+ {
+ if (!Guid.TryParse(text, out var guid))
+ {
+ guid = text.GetMD5();
+ }
+
+ return guid.ToString("D", CultureInfo.InvariantCulture);
+ }
+
+ private static void SetProperties(SsdpDevice device, string fullDeviceType)
+ {
+ var serviceParts = fullDeviceType
+ .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Split(':');
+
+ device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
+ device.DeviceClass = serviceParts[1];
+ device.DeviceType = serviceParts[2];
+ }
+
+ private void StartDeviceDiscovery()
+ {
+ try
+ {
+ ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error starting device discovery");
+ }
+ }
+
+ private void StartDevicePublisher(Configuration.DlnaOptions options)
+ {
+ if (_publisher is not null)
+ {
+ return;
+ }
+
+ try
+ {
+ _publisher = new SsdpDevicePublisher(
+ _communicationsServer,
+ Environment.OSVersion.Platform.ToString(),
+ // Can not use VersionString here since that includes OS and version
+ Environment.OSVersion.Version.ToString(),
+ _config.GetDlnaConfiguration().SendOnlyMatchedHost)
+ {
+ LogFunction = msg => _logger.LogDebug("{Msg}", msg),
+ SupportPnpRootDevice = false
+ };
+
+ RegisterServerEndpoints();
+
+ if (options.BlastAliveMessages)
+ {
+ _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error registering endpoint");
+ }
+ }
+
+ private void RegisterServerEndpoints()
+ {
+ var udn = CreateUuid(_appHost.SystemId);
+ var descriptorUri = "/dlna/" + udn + "/description.xml";
+
+ // Only get bind addresses in LAN
+ // IPv6 is currently unsupported
+ var validInterfaces = _networkManager.GetInternalBindAddresses()
+ .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6)
+ .ToList();
+
+ if (validInterfaces.Count == 0)
+ {
+ // No interfaces returned, fall back to loopback
+ validInterfaces = _networkManager.GetLoopbacks().ToList();
+ }
+
+ foreach (var intf in validInterfaces)
+ {
+ var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
+
+ _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
+
+ 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 = intf.Address,
+ PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix),
+ FriendlyName = "Jellyfin",
+ Manufacturer = "Jellyfin",
+ ModelName = "Jellyfin Server",
+ Uuid = udn
+ // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
+ };
+
+ SetProperties(device, fullService);
+ _publisher!.AddDevice(device);
+
+ var embeddedDevices = new[]
+ {
+ "urn:schemas-upnp-org:service:ContentDirectory:1",
+ "urn:schemas-upnp-org:service:ConnectionManager:1",
+ // "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1"
+ };
+
+ foreach (var subDevice in embeddedDevices)
+ {
+ var embeddedDevice = new SsdpEmbeddedDevice
+ {
+ FriendlyName = device.FriendlyName,
+ Manufacturer = device.Manufacturer,
+ ModelName = device.ModelName,
+ Uuid = udn
+ // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
+ };
+
+ SetProperties(embeddedDevice, subDevice);
+ device.AddDevice(embeddedDevice);
+ }
+ }
+ }
+
+ private void StartPlayToManager()
+ {
+ lock (_syncLock)
+ {
+ if (_manager is not null)
+ {
+ return;
+ }
+
+ try
+ {
+ _manager = new PlayToManager(
+ _logger,
+ _sessionManager,
+ _libraryManager,
+ _userManager,
+ _dlnaManager,
+ _appHost,
+ _imageProcessor,
+ _deviceDiscovery,
+ _httpClientFactory,
+ _userDataManager,
+ _localization,
+ _mediaSourceManager,
+ _mediaEncoder);
+
+ _manager.Start();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error starting PlayTo manager");
+ }
+ }
+ }
+
+ private void DisposePlayToManager()
+ {
+ lock (_syncLock)
+ {
+ if (_manager is not null)
+ {
+ try
+ {
+ _logger.LogInformation("Disposing PlayToManager");
+ _manager.Dispose();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error disposing PlayTo manager");
+ }
+
+ _manager = null;
+ }
+ }
+ }
+
+ private void DisposeDevicePublisher()
+ {
+ if (_publisher is not null)
+ {
+ _logger.LogInformation("Disposing SsdpDevicePublisher");
+ _publisher.Dispose();
+ _publisher = null;
+ }
+ }
+
+ private void Stop()
+ {
+ DisposeDevicePublisher();
+ DisposePlayToManager();
+ }
+}
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index d21cc69132..18fa196508 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -927,14 +927,11 @@ 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];
@@ -1139,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
return new Device(deviceProperties, httpClientFactory, logger);
}
-#nullable enable
private static DeviceIcon CreateIcon(XElement element)
{
ArgumentNullException.ThrowIfNull(element);
@@ -1252,11 +1248,10 @@ namespace Emby.Dlna.PlayTo
if (disposing)
{
_timer?.Dispose();
+ _timer = null;
+ Properties = null!;
}
- _timer = null;
- Properties = null!;
-
_disposed = true;
}
diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
index 220aa1a8dc..255c51f19a 100644
--- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs
+++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
@@ -55,41 +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);
- ms.Position = 0;
- 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);
-
- return null;
+ // 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);
+
+ // 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;
+ }
}
}
}
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index b1ad15cdc9..f70ebf3ebc 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.Didl;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@@ -577,7 +578,7 @@ namespace Emby.Dlna.PlayTo
private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
{
- if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Video)
{
return new PlaylistItem
{
@@ -597,7 +598,7 @@ namespace Emby.Dlna.PlayTo
};
}
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Audio)
{
return new PlaylistItem
{
@@ -615,7 +616,7 @@ namespace Emby.Dlna.PlayTo
};
}
- if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Photo)
{
return PlaylistItemFactory.Create((Photo)item, profile);
}
@@ -683,16 +684,15 @@ namespace Emby.Dlna.PlayTo
if (disposing)
{
+ _device.PlaybackStart -= OnDevicePlaybackStart;
+ _device.PlaybackProgress -= OnDevicePlaybackProgress;
+ _device.PlaybackStopped -= OnDevicePlaybackStopped;
+ _device.MediaChanged -= OnDeviceMediaChanged;
+ _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
+ _device.OnDeviceUnavailable = null;
_device.Dispose();
}
- _device.PlaybackStart -= OnDevicePlaybackStart;
- _device.PlaybackProgress -= OnDevicePlaybackProgress;
- _device.PlaybackStopped -= OnDevicePlaybackStopped;
- _device.MediaChanged -= OnDeviceMediaChanged;
- _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
- _device.OnDeviceUnavailable = null;
-
_disposed = true;
}
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index ef617422c4..b05e0a0957 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -39,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)
{
diff --git a/Emby.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs
index 2e0f2063be..a8f451405c 100644
--- a/Emby.Dlna/PlayTo/uBaseObject.cs
+++ b/Emby.Dlna/PlayTo/uBaseObject.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
+using Jellyfin.Data.Enums;
namespace Emby.Dlna.PlayTo
{
@@ -33,19 +34,19 @@ namespace Emby.Dlna.PlayTo
{
var classType = UpnpClass ?? string.Empty;
- if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Audio, StringComparison.Ordinal) != -1)
+ if (classType.Contains("Audio", StringComparison.Ordinal))
{
- return MediaBrowser.Model.Entities.MediaType.Audio;
+ return "Audio";
}
- if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1)
+ if (classType.Contains("Video", StringComparison.Ordinal))
{
- return MediaBrowser.Model.Entities.MediaType.Video;
+ return "Video";
}
- if (classType.IndexOf("image", StringComparison.Ordinal) != -1)
+ if (classType.Contains("image", StringComparison.Ordinal))
{
- return MediaBrowser.Model.Entities.MediaType.Photo;
+ return "Photo";
}
return null;
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 2bd089ed8f..b63c8f10e5 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -318,7 +318,7 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
//
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
- new EpisodeExpression(@"(?[0-9]{4})[._ -](?[0-9]{2})[._ -](?[0-9]{2})", true)
+ new EpisodeExpression("(?[0-9]{4})[._ -](?[0-9]{2})[._ -](?[0-9]{2})", true)
{
DateTimeFormats = new[]
{
@@ -328,7 +328,7 @@ namespace Emby.Naming.Common
"yyyy MM dd"
}
},
- new EpisodeExpression(@"(?[0-9]{2})[._ -](?[0-9]{2})[._ -](?[0-9]{4})", true)
+ new EpisodeExpression("(?[0-9]{2})[._ -](?[0-9]{2})[._ -](?[0-9]{4})", true)
{
DateTimeFormats = new[]
{
@@ -376,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
},
@@ -417,7 +417,7 @@ namespace Emby.Naming.Common
},
// "1-12 episode title"
- new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
+ new EpisodeExpression("([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?[0-9]{1,3})(-(?[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
@@ -712,7 +712,7 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename
"^(?[0-9]+)",
// Part if often ending of filename
- @"(?[0-9]+)$",
+ "(?[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part)
"(?[0-9]+)_(?[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index f3973dad95..bc7548189b 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -45,8 +45,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
index 9531296711..4080ba10d3 100644
--- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
return null;
}
- var extension = Path.GetExtension(path);
+ var extension = Path.GetExtension(path.AsSpan());
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
{
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index f7ba606e3e..4b9df19b08 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -26,19 +26,18 @@ namespace Emby.Naming.Video
return false;
}
- var extension = Path.GetExtension(path);
+ var extension = Path.GetExtension(path.AsSpan());
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
- path = Path.GetFileNameWithoutExtension(path);
- var token = Path.GetExtension(path).TrimStart('.');
+ var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
foreach (var rule in options.StubTypes)
{
- if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
+ if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
stubType = rule.StubType;
return true;
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index 0f97a06867..5a04bbe49b 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -24,14 +24,18 @@
true
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
-
+
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index f54066c57f..27329a7f2f 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -61,7 +61,7 @@ namespace Emby.Photos
item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
- if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
+ if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
{
try
{
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index 6edfad575a..39524be1d4 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase
///
public abstract class BaseApplicationPaths : IApplicationPaths
{
- private string _dataPath;
-
///
/// Initializes a new instance of the class.
///
@@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
- _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
+ DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
///
@@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
///
/// The data directory.
- public string DataPath => _dataPath;
+ public string DataPath { get; }
///
public string VirtualDataPath => "%AppDataPath%";
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 8518b13521..a1f1cd6490 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -13,9 +13,7 @@ using System.Net;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
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;
@@ -58,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;
@@ -82,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;
@@ -101,7 +97,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
@@ -133,7 +128,7 @@ namespace Emby.Server.Implementations
/// All concrete types.
private Type[] _allConcreteTypes;
- private bool _disposed = false;
+ private bool _disposed;
///
/// Initializes a new instance of the class.
@@ -184,26 +179,16 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; }
- public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
- && !_startupOptions.IsService
- && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
-
///
/// Gets the singleton instance.
///
public INetworkManager NetManager { get; private set; }
- ///
- /// Gets a value indicating whether this instance has changes that require the entire application to restart.
- ///
- /// true if this instance has pending application restart; otherwise, false.
- public bool HasPendingRestart { get; private set; }
-
///
- public bool IsShuttingDown { get; private set; }
+ public bool HasPendingRestart { get; private set; }
///
- public bool ShouldRestart { get; private set; }
+ public bool ShouldRestart { get; set; }
///
/// Gets the logger.
@@ -461,7 +446,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.AddParts(GetExports());
- NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger());
+ NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger());
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
@@ -507,6 +492,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
+ serviceCollection.AddScoped();
+
serviceCollection.AddSingleton();
serviceCollection.AddSingleton(NetManager);
@@ -572,8 +559,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
-
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
@@ -585,8 +570,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
- serviceCollection.AddSingleton();
-
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
@@ -850,24 +833,6 @@ namespace Emby.Server.Implementations
}
}
- ///
- public void Restart()
- {
- ShouldRestart = true;
- Shutdown();
- }
-
- ///
- public void Shutdown()
- {
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- IsShuttingDown = true;
- Resolve().StopApplication();
- });
- }
-
///
/// Gets the composable part assemblies.
///
@@ -901,7 +866,7 @@ namespace Emby.Server.Implementations
yield return typeof(MediaBrowser.MediaEncoding.Encoder.MediaEncoder).Assembly;
// Dlna
- yield return typeof(DlnaEntryPoint).Assembly;
+ yield return typeof(DlnaHost).Assembly;
// Local metadata
yield return typeof(BoxSetXmlSaver).Assembly;
@@ -923,49 +888,6 @@ namespace Emby.Server.Implementations
protected abstract IEnumerable GetAssembliesWithPartsInternal();
- ///
- /// Gets the system status.
- ///
- /// Where this request originated.
- /// SystemInfo.
- public SystemInfo GetSystemInfo(HttpRequest request)
- {
- return new SystemInfo
- {
- HasPendingRestart = HasPendingRestart,
- IsShuttingDown = IsShuttingDown,
- Version = ApplicationVersionString,
- WebSocketPortNumber = HttpPort,
- CompletedInstallations = Resolve().CompletedInstallations.ToArray(),
- Id = SystemId,
- ProgramDataPath = ApplicationPaths.ProgramDataPath,
- WebPath = ApplicationPaths.WebPath,
- LogPath = ApplicationPaths.LogDirectoryPath,
- ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
- InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
- CachePath = ApplicationPaths.CachePath,
- CanLaunchWebBrowser = CanLaunchWebBrowser,
- TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
- ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(request),
- SupportsLibraryMonitor = true,
- PackageName = _startupOptions.PackageName
- };
- }
-
- public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
- {
- return new PublicSystemInfo
- {
- Version = ApplicationVersionString,
- ProductName = ApplicationProductName,
- Id = SystemId,
- ServerName = FriendlyName,
- LocalAddress = GetSmartApiUrl(request),
- StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
- };
- }
-
///
public string GetSmartApiUrl(IPAddress remoteAddr)
{
@@ -983,7 +905,7 @@ namespace Emby.Server.Implementations
///
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;
@@ -1018,7 +940,7 @@ namespace Emby.Server.Implementations
public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
{
// With an empty source, the port will be null
- var smart = NetManager.GetBindAddress(ipAddress, out _, true);
+ var smart = NetManager.GetBindAddress(ipAddress, out _, false);
var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart, scheme, port);
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 961e225e9e..8279acb058 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -371,8 +371,11 @@ namespace Emby.Server.Implementations.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path));
- await using FileStream createStream = File.Create(path);
- await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
+ FileStream createStream = File.Create(path);
+ await using (createStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
+ }
}
///
@@ -1156,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels
if (info.People is not null && info.People.Count > 0)
{
- _libraryManager.UpdatePeople(item, info.People);
+ await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
}
}
else if (forceUpdate)
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 77cf4089be..d0772654ce 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -722,7 +722,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@IsLocked", item.IsLocked);
saveItemStatement.TryBind("@Name", item.Name);
saveItemStatement.TryBind("@OfficialRating", item.OfficialRating);
- saveItemStatement.TryBind("@MediaType", item.MediaType);
+ saveItemStatement.TryBind("@MediaType", item.MediaType.ToString());
saveItemStatement.TryBind("@Overview", item.Overview);
saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber);
saveItemStatement.TryBind("@PremiereDate", item.PremiereDate);
@@ -2042,7 +2042,7 @@ namespace Emby.Server.Implementations.Data
return false;
}
- var sortingFields = new HashSet(query.OrderBy.Select(i => i.OrderBy), StringComparer.OrdinalIgnoreCase);
+ var sortingFields = new HashSet(query.OrderBy.Select(i => i.OrderBy));
return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked)
|| sortingFields.Contains(ItemSortBy.IsPlayed)
@@ -2832,20 +2832,20 @@ namespace Emby.Server.Implementations.Data
if (hasSimilar || hasSearch)
{
- List<(string, SortOrder)> prepend = new List<(string, SortOrder)>(4);
+ List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4);
if (hasSearch)
{
- prepend.Add(("SearchScore", SortOrder.Descending));
+ prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending));
prepend.Add((ItemSortBy.SortName, SortOrder.Ascending));
}
if (hasSimilar)
{
- prepend.Add(("SimilarityScore", SortOrder.Descending));
+ prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending));
prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
}
- var arr = new (string, SortOrder)[prepend.Count + orderBy.Count];
+ var arr = new (ItemSortBy, SortOrder)[prepend.Count + orderBy.Count];
prepend.CopyTo(arr, 0);
orderBy.CopyTo(arr, prepend.Count);
orderBy = query.OrderBy = arr;
@@ -2863,166 +2863,43 @@ namespace Emby.Server.Implementations.Data
}));
}
- private string MapOrderByField(string name, InternalItemsQuery query)
+ private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
{
- if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase))
- {
- // TODO
- return "SortName";
- }
-
- if (string.Equals(name, ItemSortBy.Runtime, StringComparison.OrdinalIgnoreCase))
- {
- return "RuntimeTicks";
- }
-
- if (string.Equals(name, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
- {
- return "RANDOM()";
- }
-
- if (string.Equals(name, ItemSortBy.DatePlayed, StringComparison.OrdinalIgnoreCase))
- {
- if (query.GroupBySeriesPresentationUniqueKey)
- {
- return "MAX(LastPlayedDate)";
- }
-
- return "LastPlayedDate";
- }
-
- if (string.Equals(name, ItemSortBy.PlayCount, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.PlayCount;
- }
-
- if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase))
- {
- return "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )";
- }
-
- if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.IsFolder;
- }
-
- if (string.Equals(name, ItemSortBy.IsPlayed, StringComparison.OrdinalIgnoreCase))
- {
- return "played";
- }
-
- if (string.Equals(name, ItemSortBy.IsUnplayed, StringComparison.OrdinalIgnoreCase))
- {
- return "played";
- }
-
- if (string.Equals(name, ItemSortBy.DateLastContentAdded, StringComparison.OrdinalIgnoreCase))
- {
- return "DateLastMediaAdded";
- }
-
- if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase))
- {
- return "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)";
- }
-
- if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase))
- {
- return "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)";
- }
-
- if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase))
- {
- return "InheritedParentalRatingValue";
- }
-
- if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase))
- {
- return "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)";
- }
-
- if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase))
- {
- return "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)";
- }
-
- if (string.Equals(name, ItemSortBy.SeriesSortName, StringComparison.OrdinalIgnoreCase))
- {
- return "SeriesName";
- }
-
- if (string.Equals(name, ItemSortBy.AiredEpisodeOrder, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.AiredEpisodeOrder;
- }
-
- if (string.Equals(name, ItemSortBy.Album, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.Album;
- }
-
- if (string.Equals(name, ItemSortBy.DateCreated, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.DateCreated;
- }
-
- if (string.Equals(name, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.PremiereDate;
- }
-
- if (string.Equals(name, ItemSortBy.StartDate, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.StartDate;
- }
-
- if (string.Equals(name, ItemSortBy.Name, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.Name;
- }
-
- if (string.Equals(name, ItemSortBy.CommunityRating, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.CommunityRating;
- }
-
- if (string.Equals(name, ItemSortBy.ProductionYear, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.ProductionYear;
- }
-
- if (string.Equals(name, ItemSortBy.CriticRating, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.CriticRating;
- }
-
- if (string.Equals(name, ItemSortBy.VideoBitRate, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.VideoBitRate;
- }
-
- if (string.Equals(name, ItemSortBy.ParentIndexNumber, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.ParentIndexNumber;
- }
-
- if (string.Equals(name, ItemSortBy.IndexNumber, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.IndexNumber;
- }
-
- if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.SimilarityScore;
- }
-
- if (string.Equals(name, ItemSortBy.SearchScore, StringComparison.OrdinalIgnoreCase))
- {
- return ItemSortBy.SearchScore;
- }
-
- // Unknown SortBy, just sort by the SortName.
- return ItemSortBy.SortName;
+ return sortBy switch
+ {
+ ItemSortBy.AirTime => "SortName", // TODO
+ ItemSortBy.Runtime => "RuntimeTicks",
+ ItemSortBy.Random => "RANDOM()",
+ ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)",
+ ItemSortBy.DatePlayed => "LastPlayedDate",
+ ItemSortBy.PlayCount => "PlayCount",
+ ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )",
+ ItemSortBy.IsFolder => "IsFolder",
+ ItemSortBy.IsPlayed => "played",
+ ItemSortBy.IsUnplayed => "played",
+ ItemSortBy.DateLastContentAdded => "DateLastMediaAdded",
+ ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)",
+ ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)",
+ ItemSortBy.OfficialRating => "InheritedParentalRatingValue",
+ ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)",
+ ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
+ ItemSortBy.SeriesSortName => "SeriesName",
+ ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
+ ItemSortBy.Album => "Album",
+ ItemSortBy.DateCreated => "DateCreated",
+ ItemSortBy.PremiereDate => "PremiereDate",
+ ItemSortBy.StartDate => "StartDate",
+ ItemSortBy.Name => "Name",
+ ItemSortBy.CommunityRating => "CommunityRating",
+ ItemSortBy.ProductionYear => "ProductionYear",
+ ItemSortBy.CriticRating => "CriticRating",
+ ItemSortBy.VideoBitRate => "VideoBitRate",
+ ItemSortBy.ParentIndexNumber => "ParentIndexNumber",
+ ItemSortBy.IndexNumber => "IndexNumber",
+ ItemSortBy.SimilarityScore => "SimilarityScore",
+ ItemSortBy.SearchScore => "SearchScore",
+ _ => "SortName"
+ };
}
public List GetItemIdsList(InternalItemsQuery query)
@@ -3109,11 +2986,6 @@ namespace Emby.Server.Implementations.Data
return true;
}
- private bool IsValidMediaType(string value)
- {
- return IsAlphaNumeric(value);
- }
-
private bool IsValidPersonType(string value)
{
return IsAlphaNumeric(value);
@@ -3540,10 +3412,7 @@ namespace Emby.Server.Implementations.Data
.Append(paramName)
.Append("))) OR ");
- if (statement is not null)
- {
- statement.TryBind(paramName, query.PersonIds[i]);
- }
+ statement?.TryBind(paramName, query.PersonIds[i]);
}
clauseBuilder.Length -= Or.Length;
@@ -4124,15 +3993,14 @@ namespace Emby.Server.Implementations.Data
}
}
- var queryMediaTypes = query.MediaTypes.Where(IsValidMediaType).ToArray();
- if (queryMediaTypes.Length == 1)
+ if (query.MediaTypes.Length == 1)
{
whereClauses.Add("MediaType=@MediaTypes");
- statement?.TryBind("@MediaTypes", queryMediaTypes[0]);
+ statement?.TryBind("@MediaTypes", query.MediaTypes[0].ToString());
}
- else if (queryMediaTypes.Length > 1)
+ else if (query.MediaTypes.Length > 1)
{
- var val = string.Join(',', queryMediaTypes.Select(i => "'" + i + "'"));
+ var val = string.Join(',', query.MediaTypes.Select(i => $"'{i}'"));
whereClauses.Add("MediaType in (" + val + ")");
}
@@ -4382,7 +4250,7 @@ namespace Emby.Server.Implementations.Data
foreach (var videoType in query.VideoTypes)
{
- videoTypes.Add("data like '%\"VideoType\":\"" + videoType.ToString() + "\"%'");
+ videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'");
}
whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 6d27703bdc..44b97e8b83 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
@@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy _livetvManagerFactory;
private readonly ILyricManager _lyricManager;
+ private readonly ITrickplayManager _trickplayManager;
public DtoService(
ILogger logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy livetvManagerFactory,
- ILyricManager lyricManager)
+ ILyricManager lyricManager,
+ ITrickplayManager trickplayManager)
{
_logger = logger;
_libraryManager = libraryManager;
@@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_lyricManager = lyricManager;
+ _trickplayManager = trickplayManager;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -1059,6 +1063,11 @@ namespace Emby.Server.Implementations.Dto
dto.Chapters = _itemRepo.GetChapters(item);
}
+ if (options.ContainsField(ItemFields.Trickplay))
+ {
+ dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
+ }
+
if (video.ExtraType.HasValue)
{
dto.ExtraType = video.ExtraType.Value.ToString();
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 3aab0a5e9d..b48e389ace 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -43,16 +43,19 @@
net7.0
false
true
-
- AD0001
false
-
+
+
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 54191649da..7e4994f1af 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
{
///
- /// Class UdpServerEntryPoint.
+ /// Class responsible for registering all UDP broadcast endpoints and their handlers.
///
public sealed class UdpServerEntryPoint : IServerEntryPoint
{
@@ -35,14 +35,13 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IConfiguration _config;
private readonly IConfigurationManager _configurationManager;
private readonly INetworkManager _networkManager;
- private readonly bool _enableMultiSocketBinding;
///
/// The UDP server.
///
- private List _udpServers;
- private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
- private bool _disposed = false;
+ private readonly List _udpServers;
+ private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+ private bool _disposed;
///
/// Initializes a new instance of the class.
@@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints
_configurationManager = configurationManager;
_networkManager = networkManager;
_udpServers = new List();
- _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
}
///
@@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints
try
{
- if (_enableMultiSocketBinding)
+ // 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 socket
+ // 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 sockets
+ // 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)
@@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints
}
else
{
- var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, 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 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)
@@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (_disposed)
{
- throw new ObjectDisposedException(this.GetType().Name);
+ throw new ObjectDisposedException(GetType().Name);
}
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7f620d666d..f83da566b2 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -12,7 +12,6 @@ using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Net.WebSocketMessages;
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
using MediaBrowser.Model.Session;
-using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.HttpServer
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index 15b1836eba..e75cab64c9 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -210,7 +210,6 @@ namespace Emby.Server.Implementations.IO
DisposeTimer();
_disposed = true;
- GC.SuppressFinalize(this);
}
}
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 18b00ce0b2..c380d67db1 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.IO
}
// unc path
- if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
+ if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
{
return filePath;
}
@@ -103,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)
{
diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
index 539d4a63af..04d90af3c3 100644
--- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
@@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Images
Recursive = true,
DtoOptions = new DtoOptions(true),
ImageTypes = new ImageType[] { ImageType.Primary },
- OrderBy = new (string, SortOrder)[]
+ OrderBy = new (ItemSortBy, SortOrder)[]
{
(ItemSortBy.IsFolder, SortOrder.Ascending),
(ItemSortBy.SortName, SortOrder.Ascending)
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index 8a0e627b9c..6e8f77977e 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -30,47 +30,43 @@ namespace Emby.Server.Implementations.Images
BaseItemKind[] includeItemTypes;
- if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
+ switch (viewType)
{
- includeItemTypes = new[] { BaseItemKind.Movie };
- }
- else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.Series };
- }
- else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.MusicAlbum };
- }
- else if (string.Equals(viewType, CollectionType.MusicVideos, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.MusicVideo };
- }
- else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
- }
- else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.BoxSet };
- }
- else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal))
- {
- includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
- }
- else
- {
- includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
+ case CollectionType.Movies:
+ includeItemTypes = new[] { BaseItemKind.Movie };
+ break;
+ case CollectionType.TvShows:
+ includeItemTypes = new[] { BaseItemKind.Series };
+ break;
+ case CollectionType.Music:
+ includeItemTypes = new[] { BaseItemKind.MusicAlbum };
+ break;
+ case CollectionType.MusicVideos:
+ includeItemTypes = new[] { BaseItemKind.MusicVideo };
+ break;
+ case CollectionType.Books:
+ includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
+ break;
+ case CollectionType.BoxSets:
+ includeItemTypes = new[] { BaseItemKind.BoxSet };
+ break;
+ case CollectionType.HomeVideos:
+ case CollectionType.Photos:
+ includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
+ break;
+ default:
+ includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
+ break;
}
- var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase);
+ var recursive = viewType != CollectionType.Playlists;
return view.GetItemList(new InternalItemsQuery
{
CollapseBoxSetItems = false,
Recursive = recursive,
DtoOptions = new DtoOptions(false),
- ImageTypes = new ImageType[] { ImageType.Primary },
+ ImageTypes = new[] { ImageType.Primary },
Limit = 8,
OrderBy = new[]
{
diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
index 0bd5fdce0a..5de53df739 100644
--- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs
@@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Images
var view = (UserView)item;
var isUsingCollectionStrip = IsUsingCollectionStrip(view);
- var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+ var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.BoxSets && view.ViewType != CollectionType.Playlists;
var result = view.GetItemList(new InternalItemsQuery
{
@@ -112,14 +112,14 @@ namespace Emby.Server.Implementations.Images
private static bool IsUsingCollectionStrip(UserView view)
{
- string[] collectionStripViewTypes =
+ CollectionType[] collectionStripViewTypes =
{
CollectionType.Movies,
CollectionType.TvShows,
CollectionType.Playlists
};
- return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty);
+ return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value);
}
protected override string CreateImage(BaseItem item, IReadOnlyCollection itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index b0a4a4151a..f40177fa77 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -46,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;
@@ -526,14 +525,14 @@ namespace Emby.Server.Implementations.Library
IDirectoryService directoryService,
IItemResolver[] resolvers,
Folder parent = null,
- string collectionType = null,
+ CollectionType? collectionType = null,
LibraryOptions libraryOptions = null)
{
ArgumentNullException.ThrowIfNull(fileInfo);
var fullPath = fileInfo.FullName;
- if (string.IsNullOrEmpty(collectionType) && parent is not null)
+ if (collectionType is null && parent is not null)
{
collectionType = GetContentTypeOverride(fullPath, true);
}
@@ -636,7 +635,7 @@ namespace Emby.Server.Implementations.Library
return !args.ContainsFileSystemEntryByName(".ignore");
}
- public IEnumerable ResolvePaths(IEnumerable files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null)
+ public IEnumerable ResolvePaths(IEnumerable files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null)
{
return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
}
@@ -646,7 +645,7 @@ namespace Emby.Server.Implementations.Library
IDirectoryService directoryService,
Folder parent,
LibraryOptions libraryOptions,
- string collectionType,
+ CollectionType? collectionType,
IItemResolver[] resolvers)
{
var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
@@ -676,7 +675,7 @@ namespace Emby.Server.Implementations.Library
IReadOnlyList fileList,
IDirectoryService directoryService,
Folder parent,
- string collectionType,
+ CollectionType? collectionType,
IItemResolver[] resolvers,
LibraryOptions libraryOptions)
{
@@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library
{
var path = Person.GetPath(name);
var id = GetItemByNameId(path);
- if (GetItemById(id) is not Person item)
+ if (GetItemById(id) is Person item)
{
- item = new Person
- {
- Name = name,
- Id = id,
- DateCreated = DateTime.UtcNow,
- DateModified = DateTime.UtcNow,
- Path = path
- };
+ return item;
}
- return item;
+ return null;
}
///
@@ -1162,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
@@ -1522,7 +1514,7 @@ namespace Emby.Server.Implementations.Library
{
if (item is UserView view)
{
- if (string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.Ordinal))
+ if (view.ViewType == CollectionType.LiveTv)
{
return new[] { view.Id };
}
@@ -1551,13 +1543,13 @@ namespace Emby.Server.Implementations.Library
}
// Handle grouping
- if (user is not null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)
+ if (user is not null && view.ViewType != CollectionType.Unknown && UserView.IsEligibleForGrouping(view.ViewType)
&& user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
{
return GetUserRootFolder()
.GetChildren(user, true)
.OfType()
- .Where(i => string.IsNullOrEmpty(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase))
+ .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
.Where(i => user.IsFolderGrouped(i.Id))
.SelectMany(i => GetTopParentIdsForQuery(i, user));
}
@@ -1686,7 +1678,7 @@ namespace Emby.Server.Implementations.Library
/// The sort by.
/// The sort order.
/// IEnumerable{BaseItem}.
- public IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder)
+ public IEnumerable Sort(IEnumerable items, User user, IEnumerable sortBy, SortOrder sortOrder)
{
var isFirst = true;
@@ -1709,7 +1701,7 @@ namespace Emby.Server.Implementations.Library
return orderedItems ?? items;
}
- public IEnumerable Sort(IEnumerable items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy)
+ public IEnumerable Sort(IEnumerable items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
{
var isFirst = true;
@@ -1744,9 +1736,9 @@ namespace Emby.Server.Implementations.Library
/// The name.
/// The user.
/// IBaseItemComparer.
- private IBaseItemComparer GetComparer(string name, User user)
+ private IBaseItemComparer GetComparer(ItemSortBy name, User user)
{
- var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase));
+ var comparer = Comparers.FirstOrDefault(c => name == c.Type);
// If it requires a user, create a new one, and assign the user
if (comparer is IUserBaseItemComparer)
@@ -2073,16 +2065,16 @@ namespace Emby.Server.Implementations.Library
: collectionFolder.GetLibraryOptions();
}
- public string GetContentType(BaseItem item)
+ public CollectionType? GetContentType(BaseItem item)
{
- string configuredContentType = GetConfiguredContentType(item, false);
- if (!string.IsNullOrEmpty(configuredContentType))
+ var configuredContentType = GetConfiguredContentType(item, false);
+ if (configuredContentType is not null)
{
return configuredContentType;
}
configuredContentType = GetConfiguredContentType(item, true);
- if (!string.IsNullOrEmpty(configuredContentType))
+ if (configuredContentType is not null)
{
return configuredContentType;
}
@@ -2090,31 +2082,31 @@ namespace Emby.Server.Implementations.Library
return GetInheritedContentType(item);
}
- public string GetInheritedContentType(BaseItem item)
+ public CollectionType? GetInheritedContentType(BaseItem item)
{
var type = GetTopFolderContentType(item);
- if (!string.IsNullOrEmpty(type))
+ if (type is not null)
{
return type;
}
return item.GetParents()
.Select(GetConfiguredContentType)
- .LastOrDefault(i => !string.IsNullOrEmpty(i));
+ .LastOrDefault(i => i is not null);
}
- public string GetConfiguredContentType(BaseItem item)
+ public CollectionType? GetConfiguredContentType(BaseItem item)
{
return GetConfiguredContentType(item, false);
}
- public string GetConfiguredContentType(string path)
+ public CollectionType? GetConfiguredContentType(string path)
{
return GetContentTypeOverride(path, false);
}
- public string GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
+ public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
{
if (item is ICollectionFolder collectionFolder)
{
@@ -2124,16 +2116,21 @@ namespace Emby.Server.Implementations.Library
return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
}
- private string GetContentTypeOverride(string path, bool inherit)
+ private CollectionType? GetContentTypeOverride(string path, bool inherit)
{
var nameValuePair = _configurationManager.Configuration.ContentTypes
.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
|| (inherit && !string.IsNullOrEmpty(i.Name)
&& _fileSystem.ContainsSubPath(i.Name, path)));
- return nameValuePair?.Value;
+ if (Enum.TryParse(nameValuePair?.Value, out var collectionType))
+ {
+ return collectionType;
+ }
+
+ return null;
}
- private string GetTopFolderContentType(BaseItem item)
+ private CollectionType? GetTopFolderContentType(BaseItem item)
{
if (item is null)
{
@@ -2155,13 +2152,13 @@ namespace Emby.Server.Implementations.Library
.OfType()
.Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path))
.Select(i => i.CollectionType)
- .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ .FirstOrDefault(i => i is not null);
}
public UserView GetNamedView(
User user,
string name,
- string viewType,
+ CollectionType? viewType,
string sortName)
{
return GetNamedView(user, name, Guid.Empty, viewType, sortName);
@@ -2169,13 +2166,13 @@ namespace Emby.Server.Implementations.Library
public UserView GetNamedView(
string name,
- string viewType,
+ CollectionType viewType,
string sortName)
{
var path = Path.Combine(
_configurationManager.ApplicationPaths.InternalMetadataPath,
"views",
- _fileSystem.GetValidFilename(viewType));
+ _fileSystem.GetValidFilename(viewType.ToString()));
var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
@@ -2215,13 +2212,13 @@ namespace Emby.Server.Implementations.Library
User user,
string name,
Guid parentId,
- string viewType,
+ CollectionType? viewType,
string sortName)
{
var parentIdString = parentId.Equals(default)
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
- var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType ?? string.Empty);
+ var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
var id = GetNewItemId(idValues, typeof(UserView));
@@ -2277,7 +2274,7 @@ namespace Emby.Server.Implementations.Library
public UserView GetShadowView(
BaseItem parent,
- string viewType,
+ CollectionType? viewType,
string sortName)
{
ArgumentNullException.ThrowIfNull(parent);
@@ -2285,7 +2282,7 @@ namespace Emby.Server.Implementations.Library
var name = parent.Name;
var parentId = parent.Id;
- var idValues = "38_namedview_" + name + parentId + (viewType ?? string.Empty);
+ var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
var id = GetNewItemId(idValues, typeof(UserView));
@@ -2342,7 +2339,7 @@ namespace Emby.Server.Implementations.Library
public UserView GetNamedView(
string name,
Guid parentId,
- string viewType,
+ CollectionType? viewType,
string sortName,
string uniqueId)
{
@@ -2351,7 +2348,7 @@ namespace Emby.Server.Implementations.Library
var parentIdString = parentId.Equals(default)
? null
: parentId.ToString("N", CultureInfo.InvariantCulture);
- var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty);
+ var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
if (!string.IsNullOrEmpty(uniqueId))
{
idValues += uniqueId;
@@ -2386,7 +2383,7 @@ namespace Emby.Server.Implementations.Library
isNew = true;
}
- if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase))
+ if (viewType != item.ViewType)
{
item.ViewType = viewType;
item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
@@ -2858,7 +2855,7 @@ namespace Emby.Server.Implementations.Library
{
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
- File.WriteAllBytes(path, Array.Empty());
+ await File.WriteAllBytesAsync(path, Array.Empty()).ConfigureAwait(false);
}
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@@ -2900,9 +2897,18 @@ namespace Emby.Server.Implementations.Library
var saveEntity = false;
var personEntity = GetPerson(person.Name);
- // if PresentationUniqueKey is empty it's likely a new item.
- if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
+ if (personEntity is null)
{
+ var path = Person.GetPath(person.Name);
+ personEntity = new Person()
+ {
+ Name = person.Name,
+ Id = GetItemByNameId(path),
+ DateCreated = DateTime.UtcNow,
+ DateModified = DateTime.UtcNow,
+ Path = path
+ };
+
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
}
@@ -3135,7 +3141,7 @@ namespace Emby.Server.Implementations.Library
}
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
- .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(shortcut))
diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
index 936a08da81..59d705acef 100644
--- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs
+++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs
@@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey))
{
+ FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try
{
- await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
- catch
+ catch (Exception ex)
{
+ _logger.LogError(ex, "Error deserializing mediainfo cache");
+ }
+ finally
+ {
+ await jsonStream.DisposeAsync().ConfigureAwait(false);
}
}
@@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
- await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
+ await using (createStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
- // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
+ _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index c9a26a30f5..96fad9bca8 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -11,6 +11,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using EasyCaching.Core.Configurations;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json;
@@ -186,11 +187,11 @@ namespace Emby.Server.Implementations.Library
{
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Audio)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Video)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
@@ -334,11 +335,11 @@ namespace Emby.Server.Implementations.Library
{
SetDefaultAudioAndSubtitleStreamIndexes(item, source, user);
- if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (item.MediaType == MediaType.Audio)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
- else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ else if (item.MediaType == MediaType.Video)
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
@@ -417,9 +418,9 @@ namespace Emby.Server.Implementations.Library
public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
{
// Item would only be null if the app didn't supply ItemId as part of the live stream open request
- var mediaType = item is null ? MediaType.Video : item.MediaType;
+ var mediaType = item?.MediaType ?? MediaType.Video;
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Video)
{
var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item);
@@ -428,7 +429,7 @@ namespace Emby.Server.Implementations.Library
SetDefaultAudioStreamIndex(source, userData, user, allowRememberingSelection);
SetDefaultSubtitleStreamIndex(source, userData, user, allowRememberingSelection);
}
- else if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ else if (mediaType == MediaType.Audio)
{
var audio = source.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
@@ -625,17 +626,19 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey))
{
+ FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try
{
- await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
- // _logger.LogDebug("Found cached media info");
}
catch (Exception ex)
{
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
}
+ finally
+ {
+ await jsonStream.DisposeAsync().ConfigureAwait(false);
+ }
}
if (mediaInfo is null)
@@ -664,8 +667,11 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
- await using FileStream createStream = File.Create(cacheFilePath);
- await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ FileStream createStream = File.Create(cacheFilePath);
+ await using (createStream.ConfigureAwait(false))
+ {
+ await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ }
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index a74f824752..ac423ed091 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -10,11 +10,11 @@ using Emby.Naming.Audio;
using Emby.Naming.AudioBook;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library.Resolvers.Audio
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
public MultiItemResolverResult ResolveMultiple(
Folder parent,
List files,
- string collectionType,
+ CollectionType? collectionType,
IDirectoryService directoryService)
{
var result = ResolveMultipleInternal(parent, files, collectionType);
@@ -59,9 +59,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List files,
- string collectionType)
+ CollectionType? collectionType)
{
- if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.Books)
{
return ResolveMultipleAudio(parent, files, true);
}
@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var collectionType = args.GetCollectionType();
- var isBooksCollectionType = string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase);
+ var isBooksCollectionType = collectionType == CollectionType.Books;
if (args.IsDirectory)
{
@@ -94,15 +94,15 @@ 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;
}
- var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
+ var isMixedCollectionType = collectionType is null;
// For conflicting extensions, give priority to videos
if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
@@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
MediaBrowser.Controller.Entities.Audio.Audio item = null;
- var isMusicCollectionType = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+ var isMusicCollectionType = collectionType == CollectionType.Music;
// Use regular audio type for mixed libraries, owned items and music
if (isMixedCollectionType ||
@@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (item is not null)
{
- item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+ item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
item.IsInMixedFolder = true;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index bbc70701cb..06e292f4cf 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Naming.Audio;
using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -54,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
protected override MusicAlbum Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
- var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+ var isMusicMediaFolder = collectionType == CollectionType.Music;
// If there's a collection type and it's not music, don't allow it.
if (!isMusicMediaFolder)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index c858dc53d9..7d6f97b121 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -4,6 +4,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var collectionType = args.GetCollectionType();
- var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+ var isMusicMediaFolder = collectionType == CollectionType.Music;
// If there's a collection type and it's not music, it can't be a music artist
if (!isMusicMediaFolder)
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 381796d0e3..779cfd5be4 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return false;
}
- return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
+ return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
}
///
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 042422c6f4..b76bfe4274 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var collectionType = args.GetCollectionType();
// Only process items that are in a collection folder containing books
- if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
+ if (collectionType != CollectionType.Books)
{
return null;
}
@@ -32,9 +33,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 +52,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
var bookFiles = args.FileSystemChildren.Where(f =>
{
- var fileExtension = Path.GetExtension(f.FullName)
- ?? string.Empty;
+ var fileExtension = Path.GetExtension(f.FullName.AsSpan());
return _validExtensions.Contains(
fileExtension,
- StringComparer.OrdinalIgnoreCase);
+ StringComparison.OrdinalIgnoreCase);
}).ToList();
// Don't return a Book if there is more (or less) than one document in the directory
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 0b65bf921e..50fd8b8779 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@@ -28,13 +29,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
private readonly IImageProcessor _imageProcessor;
- private string[] _validCollectionTypes = new[]
+ private static readonly CollectionType[] _validCollectionTypes = new[]
{
- CollectionType.Movies,
- CollectionType.HomeVideos,
- CollectionType.MusicVideos,
- CollectionType.TvShows,
- CollectionType.Photos
+ CollectionType.Movies,
+ CollectionType.HomeVideos,
+ CollectionType.MusicVideos,
+ CollectionType.TvShows,
+ CollectionType.Photos
};
///
@@ -63,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
public MultiItemResolverResult ResolveMultiple(
Folder parent,
List files,
- string collectionType,
+ CollectionType? collectionType,
IDirectoryService directoryService)
{
var result = ResolveMultipleInternal(parent, files, collectionType);
@@ -99,17 +100,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Video movie = null;
var files = args.GetActualFileSystemChildren().ToList();
- if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.MusicVideos)
{
movie = FindMovie(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
}
- if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.HomeVideos)
{
movie = FindMovie
/// Movie.
- private T FindMovie(ItemResolveArgs args, string path, Folder parent, List fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName)
+ private T FindMovie(ItemResolveArgs args, string path, Folder parent, List fileSystemEntries, IDirectoryService directoryService, CollectionType? collectionType, bool parseName)
where T : Video, new()
{
var multiDiscFolders = new List();
var libraryOptions = args.LibraryOptions;
- var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos;
+ var supportPhotos = collectionType == CollectionType.HomeVideos && libraryOptions.EnablePhotos;
var photos = new List();
// Search for a folder rip
@@ -460,8 +459,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
var result = ResolveVideos(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult();
- var isPhotosCollection = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)
- || string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase);
+ var isPhotosCollection = collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos;
if (!isPhotosCollection && result.Items.Count == 1)
{
var videoPath = result.Items[0].Path;
@@ -562,7 +560,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return returnVideo;
}
- private bool IsInvalid(Folder parent, ReadOnlySpan collectionType)
+ private bool IsInvalid(Folder parent, CollectionType? collectionType)
{
if (parent is not null)
{
@@ -572,12 +570,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
}
- if (collectionType.IsEmpty)
+ if (collectionType is null)
{
return false;
}
- return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase);
+ return !_validCollectionTypes.Contains(collectionType.Value);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
index 7dd0ab1853..29d5407003 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
@@ -2,6 +2,7 @@
using System;
using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -45,8 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection
var collectionType = args.GetCollectionType();
- if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
- || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
+ if (collectionType == CollectionType.Photos
+ || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos))
{
if (HasPhotos(args))
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index b77c6b204b..d166ac37fb 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -1,9 +1,9 @@
using System;
-using System.Collections.Generic;
using System.IO;
using System.Linq;
using Emby.Naming.Common;
using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@@ -61,8 +61,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection
var collectionType = args.CollectionType;
- if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
- || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
+ if (collectionType == CollectionType.Photos
+ || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos))
{
if (IsImageFile(args.Path, _imageProcessor))
{
diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
index 5d569009d3..d4b3722c9a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
@@ -19,9 +20,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
///
public class PlaylistResolver : GenericFolderResolver
{
- private string[] _musicPlaylistCollectionTypes =
+ private CollectionType?[] _musicPlaylistCollectionTypes =
{
- string.Empty,
+ null,
CollectionType.Music
};
@@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Check if this is a music playlist file
// It should have the correct collection type and a supported file extension
- else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType))
{
var extension = Path.GetExtension(args.Path.AsSpan());
if (Playlist.SupportedExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
diff --git a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
index 6bb9996415..3d91ed242b 100644
--- a/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
@@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
- private string GetCollectionType(ItemResolveArgs args)
+ private CollectionType? GetCollectionType(ItemResolveArgs args)
{
return args.FileSystemChildren
.Where(i =>
@@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
})
.Select(i => _fileSystem.GetFileNameWithoutExtension(i))
- .FirstOrDefault();
+ .Select(i => Enum.TryParse(i, out var collectionType) ? collectionType : (CollectionType?)null)
+ .FirstOrDefault(i => i is not null);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
index 392ee4c771..8274881be1 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -48,9 +49,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders
- if (season is not null ||
- string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
- args.HasParent())
+ if (season is not null
+ || args.GetCollectionType() == CollectionType.TvShows
+ || args.HasParent())
{
var episode = ResolveVideo(args, false);
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
index e9538a5c97..858c5b2812 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var resolver = new Naming.TV.EpisodeResolver(namingOptions);
var folderName = System.IO.Path.GetFileName(path);
- var testPath = "\\\\test\\" + folderName;
+ var testPath = @"\\test\" + folderName;
var episodeInfo = resolver.Resolve(testPath, true);
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index d4f275bed4..2ae1138a53 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -8,6 +8,7 @@ using System.IO;
using Emby.Naming.Common;
using Emby.Naming.TV;
using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
@@ -59,11 +60,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
var collectionType = args.GetCollectionType();
- if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.TvShows)
{
// TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
var configuredContentType = args.GetConfiguredContentType();
- if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ if (configuredContentType != CollectionType.TvShows)
{
return new Series
{
@@ -72,7 +73,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
};
}
}
- else if (string.IsNullOrEmpty(collectionType))
+ else if (collectionType is null)
{
if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
{
diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs
index 2c3dc18574..113370fc37 100644
--- a/Emby.Server.Implementations/Library/UserViewManager.cs
+++ b/Emby.Server.Implementations/Library/UserViewManager.cs
@@ -8,7 +8,6 @@ using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@@ -64,8 +63,8 @@ namespace Emby.Server.Implementations.Library
var collectionFolder = folder as ICollectionFolder;
var folderViewType = collectionFolder?.CollectionType;
- // Playlist library requires special handling because the folder only refrences user playlists
- if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ // Playlist library requires special handling because the folder only references user playlists
+ if (folderViewType == CollectionType.Playlists)
{
var items = folder.GetItemList(new InternalItemsQuery(user)
{
@@ -90,7 +89,7 @@ namespace Emby.Server.Implementations.Library
continue;
}
- if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (query.PresetViews.Contains(folderViewType))
{
list.Add(GetUserView(folder, folderViewType, string.Empty));
}
@@ -102,14 +101,14 @@ namespace Emby.Server.Implementations.Library
foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows })
{
- var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(i.CollectionType))
+ var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null)
.ToList();
if (parents.Count > 0)
{
- var localizationKey = string.Equals(viewType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ?
- "TvShows" :
- "Movies";
+ var localizationKey = viewType == CollectionType.TvShows
+ ? "TvShows"
+ : "Movies";
list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews));
}
@@ -164,14 +163,14 @@ namespace Emby.Server.Implementations.Library
.ToArray();
}
- public UserView GetUserSubViewWithName(string name, Guid parentId, string type, string sortName)
+ public UserView GetUserSubViewWithName(string name, Guid parentId, CollectionType? type, string sortName)
{
var uniqueId = parentId + "subview" + type;
return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId);
}
- public UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName)
+ public UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName)
{
var name = _localizationManager.GetLocalizedString(localizationKey);
@@ -180,15 +179,15 @@ namespace Emby.Server.Implementations.Library
private Folder GetUserView(
List parents,
- string viewType,
+ CollectionType? viewType,
string localizationKey,
string sortName,
User user,
- string[] presetViews)
+ CollectionType?[] presetViews)
{
- if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
+ if (parents.Count == 1 && parents.All(i => i.CollectionType == viewType))
{
- if (!presetViews.Contains(viewType, StringComparison.OrdinalIgnoreCase))
+ if (!presetViews.Contains(viewType))
{
return (Folder)parents[0];
}
@@ -200,7 +199,7 @@ namespace Emby.Server.Implementations.Library
return _libraryManager.GetNamedView(user, name, viewType, sortName);
}
- public UserView GetUserView(Folder parent, string viewType, string sortName)
+ public UserView GetUserView(Folder parent, CollectionType? viewType, string sortName)
{
return _libraryManager.GetShadowView(parent, viewType, sortName);
}
@@ -280,7 +279,7 @@ namespace Emby.Server.Implementations.Library
var isPlayed = request.IsPlayed;
- if (parents.OfType().Any(i => string.Equals(i.CollectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase)))
+ if (parents.OfType().Any(i => i.CollectionType == CollectionType.Music))
{
isPlayed = null;
}
@@ -306,18 +305,18 @@ namespace Emby.Server.Implementations.Library
var hasCollectionType = parents.OfType().ToArray();
if (hasCollectionType.Length > 0)
{
- if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)))
+ if (hasCollectionType.All(i => i.CollectionType == CollectionType.Movies))
{
includeItemTypes = new[] { BaseItemKind.Movie };
}
- else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)))
+ else if (hasCollectionType.All(i => i.CollectionType == CollectionType.TvShows))
{
includeItemTypes = new[] { BaseItemKind.Episode };
}
}
}
- var mediaTypes = new List();
+ var mediaTypes = new List();
if (includeItemTypes.Length == 0)
{
diff --git a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
index df45793c3a..89f64ee4f0 100644
--- a/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
+++ b/Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
- MediaTypes = new string[] { MediaType.Video },
+ MediaTypes = new[] { MediaType.Video },
IncludeItemTypes = new[] { BaseItemKind.Movie },
IsVirtualItem = false,
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index b9d0f170ac..74b62ca3f2 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+ var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+ await using (stream.ConfigureAwait(false))
{
var settings = new XmlWriterSettings
{
@@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Async = true
};
- await using (var writer = XmlWriter.Create(stream, settings))
+ var writer = XmlWriter.Create(stream, settings);
+ await using (writer.ConfigureAwait(false))
{
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
@@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
- await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+ var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+ await using (stream.ConfigureAwait(false))
{
var settings = new XmlWriterSettings
{
@@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var isSeriesEpisode = timer.IsProgramSeries;
- await using (var writer = XmlWriter.Create(stream, settings))
+ var writer = XmlWriter.Create(stream, settings);
+ await using (writer.ConfigureAwait(false))
{
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
@@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
else
{
- await writer.WriteStartElementAsync(null, "movie", null);
+ await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(item.Name))
{
diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
index 7645c6c52d..6b0520ad0f 100644
--- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
+++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
@@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = JsonContent.Create(requestList, options: _jsonOptions);
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
- await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var dailySchedules = await JsonSerializer.DeserializeAsync>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var dailySchedules = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null)
{
return Array.Empty();
@@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
- await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var programDetails = await JsonSerializer.DeserializeAsync>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var programDetails = await innerResponse.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (programDetails is null)
{
return Array.Empty();
@@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
- await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- return await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ return await innerResponse2.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
- await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- var root = await JsonSerializer.DeserializeAsync>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
+ var root = await httpResponse.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is not null)
{
foreach (HeadendsDto headend in root)
@@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
@@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
- await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var response = httpResponse.Content;
- var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
+ var root = await httpResponse.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
}
catch (HttpRequestException ex)
@@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token);
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
- await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var root = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ var root = await httpResponse.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is null)
{
return new List();
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index ee039ff0f7..dd427c7368 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -207,7 +207,7 @@ namespace Emby.Server.Implementations.LiveTv
orderBy.Insert(0, (ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
}
- if (!internalQuery.OrderBy.Any(i => string.Equals(i.OrderBy, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)))
+ if (internalQuery.OrderBy.All(i => i.OrderBy != ItemSortBy.SortName))
{
orderBy.Add((ItemSortBy.SortName, SortOrder.Ascending));
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
index 1721be9e23..ff25ee5854 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
@@ -17,7 +17,6 @@ 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
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
index 7e588f6812..8cd0c4ffb7 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
@@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -27,7 +28,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -76,13 +76,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var lineup = await JsonSerializer.DeserializeAsync>(stream, _jsonOptions, cancellationToken)
- .ConfigureAwait(false) ?? new List();
-
+ var lineup = await response.Content.ReadFromJsonAsync>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty();
if (info.ImportFavoritesOnly)
{
- lineup = lineup.Where(i => i.Favorite).ToList();
+ lineup = lineup.Where(i => i.Favorite);
}
return lineup.Where(i => !i.DRM).ToList();
@@ -129,9 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
.GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var discoverResponse = await JsonSerializer.DeserializeAsync(stream, _jsonOptions, cancellationToken)
- .ConfigureAwait(false);
+ var discoverResponse = await response.Content.ReadFromJsonAsync(_jsonOptions, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey))
{
@@ -175,34 +170,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List();
- await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
+ var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using (stream.ConfigureAwait(false))
{
- string stripedLine = StripXML(line);
- if (stripedLine.Contains("Channel", StringComparison.Ordinal))
+ using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
+ await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
{
- LiveTvTunerStatus status;
- var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
- var name = stripedLine.Substring(0, index - 1);
- var currentChannel = stripedLine.Substring(index + 7);
- if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
+ string stripedLine = StripXML(line);
+ if (stripedLine.Contains("Channel", StringComparison.Ordinal))
{
- status = LiveTvTunerStatus.LiveTv;
- }
- else
- {
- status = LiveTvTunerStatus.Available;
- }
+ LiveTvTunerStatus status;
+ var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+ var name = stripedLine.Substring(0, index - 1);
+ var currentChannel = stripedLine.Substring(index + 7);
+ if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
+ {
+ status = LiveTvTunerStatus.LiveTv;
+ }
+ else
+ {
+ status = LiveTvTunerStatus.Available;
+ }
- tuners.Add(new LiveTvTunerInfo
- {
- Name = name,
- SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
- ProgramName = currentChannel,
- Status = status
- });
+ tuners.Add(new LiveTvTunerInfo
+ {
+ Name = name,
+ SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
+ ProgramName = currentChannel,
+ Status = status
+ });
+ }
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index a8b090635e..68383a5547 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -44,8 +44,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult();
}
}
-
- GC.SuppressFinalize(this);
}
public async Task CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index 613ea117f4..db5e81df5f 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 9fbf364efe..ecea8df6a8 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Haal keyframes vanuit video lêers om meer presiese HLS afspeellyste te maak. Dit kan lank duur.",
"TaskKeyframeExtractor": "Keyframe Ekstraktor",
"External": "Ekstern",
- "HearingImpaired": "gehoorgestremd"
+ "HearingImpaired": "gehoorgestremd",
+ "TaskRefreshTrickplayImages": "Genereer Fopspeel Beelde",
+ "TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling."
}
diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json
index 3af124678f..05af8d8a5a 100644
--- a/Emby.Server.Implementations/Localization/Core/be.json
+++ b/Emby.Server.Implementations/Localization/Core/be.json
@@ -123,5 +123,7 @@
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
"TaskRefreshChannels": "Абнавіць каналы",
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
- "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу."
+ "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.",
+ "TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках."
}
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 13b99cc997..e1cf1448be 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -124,5 +124,6 @@
"TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.",
"TaskKeyframeExtractor": "Извличане на ключови кадри",
"External": "Външен",
- "HearingImpaired": "Увреден слух"
+ "HearingImpaired": "Увреден слух",
+ "TaskRefreshTrickplayImages": "Генерирай изображение"
}
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index f33ea2fc95..5da33febe3 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.",
"TaskKeyframeExtractor": "Vytahovač klíčových snímků",
"External": "Externí",
- "HearingImpaired": "Sluchově postižení"
+ "HearingImpaired": "Sluchově postižení",
+ "TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno."
}
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index e1c3e9de12..f1dbf3c89d 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
"TaskKeyframeExtractor": "Keyframe Extraktor",
"External": "Extern",
- "HearingImpaired": "Hörgeschädigt"
+ "HearingImpaired": "Hörgeschädigt",
+ "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
+ "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken."
}
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index c6e2244cae..5ea6a22527 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.",
"TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
"External": "Εξωτερικό",
- "HearingImpaired": "Με προβλήματα ακοής"
+ "HearingImpaired": "Με προβλήματα ακοής",
+ "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 2436883883..32bf893100 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
"TaskKeyframeExtractor": "Keyframe Extractor",
"External": "External",
- "HearingImpaired": "Hearing Impaired"
+ "HearingImpaired": "Hearing Impaired",
+ "TaskRefreshTrickplayImages": "Generate Trickplay Images",
+ "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries."
}
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 15088384cc..496ecabd37 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -112,6 +112,8 @@
"TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.",
"TaskRefreshPeople": "Refresh People",
"TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.",
+ "TaskRefreshTrickplayImages": "Generate Trickplay Images",
+ "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
"TaskUpdatePlugins": "Update Plugins",
"TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.",
"TaskCleanTranscode": "Clean Transcode Directory",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index 4c56f789d3..fe10be3085 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
"External": "Externo",
- "HearingImpaired": "Discapacidad Auditiva"
+ "HearingImpaired": "Discapacidad Auditiva",
+ "TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
+ "TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index 08344abeb7..cba036ff47 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
"TaskKeyframeExtractor": "Avainkuvien purkain",
"External": "Ulkoinen",
- "HearingImpaired": "Kuulorajoitteinen"
+ "HearingImpaired": "Kuulorajoitteinen",
+ "TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
+ "TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index 01b3e95fc3..88a4a358e2 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -123,5 +123,6 @@
"HearingImpaired": "Bingi",
"TaskKeyframeExtractor": "Tagabunot ng Keyframe",
"TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.",
- "External": "External"
+ "External": "External",
+ "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe"
}
diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json
new file mode 100644
index 0000000000..40aa5f71a4
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/fo.json
@@ -0,0 +1,18 @@
+{
+ "Artists": "Listafólk",
+ "Collections": "Søvn",
+ "Default": "Sjálvgildi",
+ "DeviceOfflineWithName": "{0} hevur slitið sambandið",
+ "External": "Ytri",
+ "Genres": "Greinar",
+ "Albums": "Album",
+ "AppDeviceValues": "App: {0}, Eind: {1}",
+ "Application": "Nýtsluskipan",
+ "Books": "Bøkur",
+ "Channels": "Rásir",
+ "ChapterNameValue": "Kapittul {0}",
+ "DeviceOnlineWithName": "{0} er sambundið",
+ "Favorites": "Yndis",
+ "Folders": "Mappur",
+ "Forced": "Kravt"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 3ee045d89e..b816738c2c 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
"TaskKeyframeExtractor": "Extracteur d'image clé",
"External": "Externe",
- "HearingImpaired": "Malentendants"
+ "HearingImpaired": "Malentendants",
+ "TaskRefreshTrickplayImages": "Générer des images Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index a2b429dcdd..03002476ce 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -5,7 +5,7 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une photo a été téléversée depuis {0}",
+ "CameraImageUploadedFrom": "Une photo a été téléchargée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
@@ -16,14 +16,14 @@
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes de l'album",
- "HeaderContinueWatching": "Reprendre le visionnage",
+ "HeaderContinueWatching": "Continuer de regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
"HeaderFavoriteEpisodes": "Épisodes favoris",
"HeaderFavoriteShows": "Séries favorites",
"HeaderFavoriteSongs": "Chansons préférées",
"HeaderLiveTV": "TV en direct",
- "HeaderNextUp": "À suivre",
+ "HeaderNextUp": "Prochain à venir",
"HeaderRecordingGroups": "Groupes d'enregistrements",
"HomeVideos": "Vidéos personnelles",
"Inherit": "Hériter",
@@ -71,7 +71,7 @@
"ScheduledTaskStartedWithName": "{0} a démarré",
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
"Shows": "Séries",
- "Songs": "Titres",
+ "Songs": "Chansons",
"StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
"SubtitleDownloadFailureForItem": "Le téléchargement des sous-titres pour {0} a échoué.",
"SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
@@ -122,7 +122,9 @@
"TaskOptimizeDatabaseDescription": "Réduit les espaces vides ou inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la médiathèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
"TaskOptimizeDatabase": "Optimiser la base de données",
"TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
- "TaskKeyframeExtractor": "Extracteur d'image clé",
+ "TaskKeyframeExtractor": "Extracteur d'images clés",
"External": "Externe",
- "HearingImpaired": "Malentendants"
+ "HearingImpaired": "Malentendants",
+ "TaskRefreshTrickplayImages": "Générer des images Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 68e9fe8339..26eab392e7 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "חלץ תמונות מפתח מקבצי וידאו בכדי ליצור רשימות השמעה מדויקות יותר של HLS. משימה זו עלולה להימשך זמן רב.",
"TaskKeyframeExtractor": "מחלץ תמונות מפתח",
"External": "חיצוני",
- "HearingImpaired": "לקוי שמיעה"
+ "HearingImpaired": "לקוי שמיעה",
+ "TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
+ "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index d01295419e..5bb2b7d4db 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
"TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
"TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
- "HearingImpaired": "Oštećen sluh"
+ "HearingImpaired": "Oštećen sluh",
+ "TaskRefreshTrickplayImages": "Generiraj Trickplay Slike",
+ "TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index 5a4a02d80d..ba3d5872ad 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -124,5 +124,7 @@
"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"
+ "HearingImpaired": "Hallássérült",
+ "TaskRefreshTrickplayImages": "Trickplay képek generálása",
+ "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz."
}
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index a40f495061..0f1f0b3d24 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -13,8 +13,8 @@
"HeaderFavoriteArtists": "Uppáhalds Listamenn",
"HeaderFavoriteAlbums": "Uppáhalds Plötur",
"HeaderContinueWatching": "Halda áfram að horfa",
- "HeaderAlbumArtists": "Höfundur plötu",
- "Genres": "Tegundir",
+ "HeaderAlbumArtists": "Listamaður á umslagi",
+ "Genres": "Stefnur",
"Folders": "Möppur",
"Favorites": "Uppáhalds",
"FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig",
@@ -22,32 +22,32 @@
"DeviceOfflineWithName": "{0} hefur aftengst",
"Collections": "Söfn",
"ChapterNameValue": "Kafli {0}",
- "Channels": "Stöðvar",
- "CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}",
+ "Channels": "Rásir",
+ "CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni",
"Books": "Bækur",
- "AuthenticationSucceededWithUserName": "{0} auðkenning tókst",
- "Artists": "Listamaður",
+ "AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst",
+ "Artists": "Listamenn",
"Application": "Forrit",
"AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
"Albums": "Plötur",
- "Plugin": "Viðbót",
- "Photos": "Myndir",
- "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
- "NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
+ "Plugin": "Viðbótarvirkni",
+ "Photos": "Ljósmyndir",
+ "NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð",
+ "NotificationOptionVideoPlayback": "Myndbandsafspilun hafin",
"NotificationOptionUserLockedOut": "Notandi læstur úti",
- "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg",
- "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett",
- "NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
- "NotificationOptionPluginInstalled": "Viðbót sett upp",
+ "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg",
+ "NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið",
+ "NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð",
+ "NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp",
"NotificationOptionPluginError": "Bilun í viðbót",
"NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
- "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
+ "NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp",
"NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð",
"NotificationOptionAudioPlayback": "Hljóðafspilun hafin",
"NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett",
"NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði",
- "NameSeasonUnknown": "Sería óþekkt",
- "NameSeasonNumber": "Sería {0}",
+ "NameSeasonUnknown": "Þáttaröð óþekkt",
+ "NameSeasonNumber": "Þáttaröð {0}",
"MixedContent": "Blandað efni",
"MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
"MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
@@ -57,24 +57,24 @@
"User": "Notandi",
"System": "Kerfi",
"NotificationOptionNewLibraryContent": "Nýju efni bætt við",
- "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.",
+ "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.",
"NameInstallFailed": "{0} uppsetning mistókst",
"MusicVideos": "Tónlistarmyndbönd",
"Music": "Tónlist",
"Movies": "Kvikmyndir",
"UserDeletedWithName": "Notanda {0} hefur verið eytt",
"UserCreatedWithName": "Notandi {0} hefur verið stofnaður",
- "TvShows": "Þættir",
+ "TvShows": "Sjónvarpsþættir",
"Sync": "Samstilla",
"Songs": "Lög",
- "ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa",
+ "ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur",
"ScheduledTaskStartedWithName": "{0} hafin",
"ScheduledTaskFailedWithName": "{0} mistókst",
"PluginUpdatedWithName": "{0} var uppfært",
"PluginUninstalledWithName": "{0} var fjarlægt",
"PluginInstalledWithName": "{0} var sett upp",
"NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
- "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.",
+ "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.",
"VersionNumber": "Útgáfa {0}",
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
@@ -83,14 +83,14 @@
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
- "UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur",
- "UserDownloadingItemWithValues": "{0} Hleður niður {1}",
+ "UserLockedOutWithName": "Notandi {0} hefur verið læstur úti",
+ "UserDownloadingItemWithValues": "{0} hleður niður {1}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
- "ProviderValue": "Veitandi: {0}",
+ "ProviderValue": "Efnisveita: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
- "ValueSpecialEpisodeName": "Sérstakt - {0}",
- "Shows": "Sýningar",
- "Playlists": "Spilunarlisti",
+ "ValueSpecialEpisodeName": "Sérstaktur - {0}",
+ "Shows": "Þættir",
+ "Playlists": "Efnisskrár",
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
"TaskRefreshChannels": "Endurhlaða Rásir",
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
@@ -116,5 +116,12 @@
"TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
"TaskCleanLogs": "Hreinsa færslu skrá",
"TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
- "HearingImpaired": "Heyrnarskertur"
+ "HearingImpaired": "Heyrnarskertur",
+ "TaskOptimizeDatabaseDescription": "Þjappar gagnagrunni og bætir við lausu diskaplássi. Að keyra þessa aðgerð eftir skönnun safnsins, eða eftir einhverjar breytingar sem fela í sér gagnagrunnsbreytingar, gætu aukið hraðvirkni.",
+ "TaskKeyframeExtractor": "Lykilrammaplokkari",
+ "TaskKeyframeExtractorDescription": "Plokkar lykilramma úr myndbandsskrám til að búa til nákvæmari HLS uppskiptingarlista. Þetta verk getur tekið langan tíma.",
+ "TaskRefreshChapterImages": "Plokka kafla-myndir",
+ "TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
+ "Forced": "Þvingað",
+ "External": "Útvær"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 3710f03e07..a34bcc4907 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractor": "Estrattore di Keyframe",
"TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
"External": "Esterno",
- "HearingImpaired": "con problemi di udito"
+ "HearingImpaired": "con problemi di udito",
+ "TaskRefreshTrickplayImages": "Genera immagini Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 7b059c68ea..ab6988006a 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -4,19 +4,19 @@
"Application": "アプリケーション",
"Artists": "アーティスト",
"AuthenticationSucceededWithUserName": "{0} 認証に成功しました",
- "Books": "ブックス",
+ "Books": "ブック",
"CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました",
"Channels": "チャンネル",
"ChapterNameValue": "チャプター {0}",
"Collections": "コレクション",
- "DeviceOfflineWithName": "{0} が切断されました",
- "DeviceOnlineWithName": "{0} が接続されました",
- "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0} によって失敗しました",
+ "DeviceOfflineWithName": "{0} が切断しました",
+ "DeviceOnlineWithName": "{0} が接続しました",
+ "FailedLoginAttemptWithUserName": "{0} からのログインに失敗しました",
"Favorites": "お気に入り",
"Folders": "フォルダー",
"Genres": "ジャンル",
"HeaderAlbumArtists": "アルバムアーティスト",
- "HeaderContinueWatching": "続けて見る",
+ "HeaderContinueWatching": "再生を続ける",
"HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト",
"HeaderFavoriteEpisodes": "お気に入りのエピソード",
@@ -27,22 +27,22 @@
"HeaderRecordingGroups": "レコーディンググループ",
"HomeVideos": "ホームビデオ",
"Inherit": "継承",
- "ItemAddedWithName": "{0} をライブラリに追加しました",
- "ItemRemovedWithName": "{0} をライブラリから削除しました",
+ "ItemAddedWithName": "{0} をライブラリーに追加しました",
+ "ItemRemovedWithName": "{0} をライブラリーから削除しました",
"LabelIpAddressValue": "IPアドレス: {0}",
- "LabelRunningTimeValue": "稼働時間: {0}",
+ "LabelRunningTimeValue": "時間: {0}",
"Latest": "最新",
- "MessageApplicationUpdated": "Jellyfin Server が更新されました",
- "MessageApplicationUpdatedTo": "Jellyfin Server が {0}に更新されました",
- "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました",
- "MessageServerConfigurationUpdated": "サーバー設定が更新されました",
+ "MessageApplicationUpdated": "Jellyfin Server を更新しました",
+ "MessageApplicationUpdatedTo": "Jellyfin Server を {0}に更新しました",
+ "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} を更新しました",
+ "MessageServerConfigurationUpdated": "サーバー設定を更新しました",
"MixedContent": "ミックスコンテンツ",
"Movies": "映画",
"Music": "音楽",
"MusicVideos": "ミュージックビデオ",
"NameInstallFailed": "{0}のインストールに失敗しました",
"NameSeasonNumber": "シーズン {0}",
- "NameSeasonUnknown": "不明なシーズン",
+ "NameSeasonUnknown": "シーズン不明",
"NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。",
"NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります",
"NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です",
@@ -88,18 +88,18 @@
"UserPolicyUpdatedWithName": "ユーザーポリシーが{0}に更新されました",
"UserStartedPlayingItemWithValues": "{0} は {2}で{1} を再生しています",
"UserStoppedPlayingItemWithValues": "{0} は{2}で{1} の再生が終わりました",
- "ValueHasBeenAddedToLibrary": "{0}はあなたのメディアライブラリに追加されました",
+ "ValueHasBeenAddedToLibrary": "{0} をメディアライブラリーに追加しました",
"ValueSpecialEpisodeName": "スペシャル - {0}",
"VersionNumber": "バージョン {0}",
"TaskCleanLogsDescription": "{0} 日以上前のログを消去します。",
"TaskCleanLogs": "ログの掃除",
- "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータを更新します。",
- "TaskRefreshLibrary": "メディアライブラリのスキャン",
+ "TaskRefreshLibraryDescription": "メディアライブラリーをスキャンして、新しいファイルを探し、メタデータを更新します。",
+ "TaskRefreshLibrary": "メディアライブラリーをスキャン",
"TaskCleanCacheDescription": "不要なキャッシュを消去します。",
"TaskCleanCache": "キャッシュを消去",
"TasksChannelsCategory": "ネットチャンネル",
"TasksApplicationCategory": "アプリケーション",
- "TasksLibraryCategory": "ライブラリ",
+ "TasksLibraryCategory": "ライブラリー",
"TasksMaintenanceCategory": "メンテナンス",
"TaskRefreshChannelsDescription": "ネットチャンネルの情報を更新する。",
"TaskRefreshChannels": "チャンネルの更新",
@@ -107,7 +107,7 @@
"TaskCleanTranscode": "トランスコードディレクトリの削除",
"TaskUpdatePluginsDescription": "自動更新可能なプラグインのアップデートをダウンロードしてインストールします。",
"TaskUpdatePlugins": "プラグインの更新",
- "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータを更新します。",
+ "TaskRefreshPeopleDescription": "メディアライブラリー内の俳優や監督のメタデータを更新します。",
"TaskRefreshPeople": "俳優や監督のデータの更新",
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索する。",
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
@@ -118,10 +118,12 @@
"Undefined": "未定義",
"Forced": "強制",
"Default": "デフォルト",
- "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリのスキャン後でこのタスクを実行するとパフォーマンスが向上する可能性があります。",
+ "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリーのスキャンやその他のデータベースの更新を伴う変更の後でこのタスクを実行すると、パフォーマンスが向上します。",
"TaskOptimizeDatabase": "データベースの最適化",
"TaskKeyframeExtractorDescription": "より正確なHLSプレイリストを作成するため、動画ファイルからキーフレームを抽出する。この処理には時間がかかる場合があります。",
"TaskKeyframeExtractor": "キーフレーム抽出",
"External": "外部",
- "HearingImpaired": "聴覚障害の方"
+ "HearingImpaired": "聴覚障害の方",
+ "TaskRefreshTrickplayImages": "トリックプレー画像を生成",
+ "TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index c5a93cb963..e050196bc9 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -123,5 +123,8 @@
"TaskOptimizeDatabase": "Derekqordy oñtailandyru",
"TaskKeyframeExtractorDescription": "Naqtyraq HLS oynatu tızımderın jasau üşın beinefaildardan negızgı kadrlardy şyğarady. Būl tapsyrma ūzaq uaqytqa sozyluy mümkın.",
"TaskKeyframeExtractor": "Negızgı kadrlardy şyğaru",
- "External": "Syrtqy"
+ "External": "Syrtqy",
+ "TaskRefreshTrickplayImagesDescription": "Іске қосылған кітапханалардағы бейнелер үшін Trickplay алдын ала түрінде көрсетілімді жасайды.",
+ "TaskRefreshTrickplayImages": "Trickplay үшін суреттерді жасау",
+ "HearingImpaired": "Есту қабілеті нашарға"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index ce8d8fc322..e7279994bb 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractor": "Pagrindinių kadrų ištraukėjas",
"TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazė, gali pagerinti greitaveiką.",
"External": "Išorinis",
- "HearingImpaired": "Su klausos sutrikimais"
+ "HearingImpaired": "Su klausos sutrikimais",
+ "TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
+ "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index f7b24412af..82a071309e 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -1,7 +1,7 @@
{
"ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
"NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
- "HeaderRecordingGroups": "Ierakstu Grupas",
+ "HeaderRecordingGroups": "Ierakstu grupas",
"UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
"SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
"NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
@@ -14,13 +14,13 @@
"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",
"UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
"UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
- "UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}",
+ "UserPasswordChangedWithName": "Lietotāja {0} parole tika nomainīta",
"UserOnlineFromDevice": "{0} ir tiešsaistē no {1}",
"UserOfflineFromDevice": "{0} ir atvienojies no {1}",
"UserLockedOutWithName": "Lietotājs {0} ir ticis bloķēts",
@@ -28,23 +28,23 @@
"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.",
"Songs": "Dziesmas",
- "Shows": "Raidījumi",
+ "Shows": "Šovi",
"PluginUpdatedWithName": "{0} tika atjaunots",
"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",
+ "NotificationOptionServerRestartRequired": "Nepieciešams servera restarts",
"NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
"NotificationOptionPluginUninstalled": "Paplašinājums noņemts",
"NotificationOptionPluginInstalled": "Paplašinā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": "Turpini skatīties",
+ "HeaderAlbumArtists": "Albumu izpildītāji",
"Genres": "Žanri",
"Folders": "Mapes",
- "Favorites": "Favorīti",
- "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}",
- "DeviceOnlineWithName": "{0} ir pievienojies",
- "DeviceOfflineWithName": "{0} ir atvienojies",
+ "Favorites": "Izlase",
+ "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
+ "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
+ "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
"Collections": "Kolekcijas",
"Channels": "Kanāli",
- "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
+ "CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}",
"Books": "Grāmatas",
"Artists": "Izpildītāji",
"Albums": "Albumi",
"ProviderValue": "Provider: {0}",
- "HeaderFavoriteSongs": "Dziesmu Favorīti",
- "HeaderFavoriteShows": "Raidījumu Favorīti",
- "HeaderFavoriteEpisodes": "Episožu Favorīti",
- "HeaderFavoriteArtists": "Izpildītāju Favorīti",
- "HeaderFavoriteAlbums": "Albumu Favorīti",
- "TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.",
- "TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
+ "HeaderFavoriteSongs": "Dziesmu izlase",
+ "HeaderFavoriteShows": "Raidījumu izlase",
+ "HeaderFavoriteEpisodes": "Sēriju izlase",
+ "HeaderFavoriteArtists": "Izpildītāju izlase",
+ "HeaderFavoriteAlbums": "Albumu izlase",
+ "TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.",
+ "TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
"TasksApplicationCategory": "Lietotne",
"TasksLibraryCategory": "Bibliotēka",
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
- "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus",
+ "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
- "TaskRefreshChannels": "Atjaunot Kanālus",
- "TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.",
- "TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
+ "TaskRefreshChannels": "Atjaunot kanālus",
+ "TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.",
+ "TaskCleanTranscode": "Iztīrīt transkodēšanas mapi",
"TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
- "TaskUpdatePlugins": "Atjaunot Paplašinājumus",
+ "TaskUpdatePlugins": "Atjaunot paplašinājumus",
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
- "TaskRefreshPeople": "Atjaunot Cilvēkus",
- "TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
- "TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
+ "TaskRefreshPeople": "Atjaunot cilvēkus",
+ "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
+ "TaskCleanLogs": "Iztīrīt logdatņu mapi",
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
- "TaskRefreshLibrary": "Skenēt Multivides Bibliotēku",
+ "TaskRefreshLibrary": "Skenēt multivides bibliotēku",
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
- "TaskCleanCache": "Iztīrīt Kešošanas Mapi",
- "TasksChannelsCategory": "Interneta Kanāli",
+ "TaskCleanCache": "Iztīrīt kešatmiņas mapi",
+ "TasksChannelsCategory": "Interneta kanāli",
"TasksMaintenanceCategory": "Apkope",
- "Forced": "Piespiests",
+ "Forced": "Piespiedu",
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
- "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
+ "TaskCleanActivityLog": "Notīrīt darbību žurnālu",
"Undefined": "Nenoteikts",
"Default": "Noklusējuma",
- "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
+ "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Šī uzdevuma palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
"TaskOptimizeDatabase": "Optimizēt datubāzi",
"External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem",
- "TaskKeyframeExtractor": "Atslēgkadru Ekstraktors",
+ "TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json
index 0620fbcdb0..0b50fa5298 100644
--- a/Emby.Server.Implementations/Localization/Core/ml.json
+++ b/Emby.Server.Implementations/Localization/Core/ml.json
@@ -121,5 +121,7 @@
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
"HearingImpaired": "കേൾവി തകരാറുകൾ",
- "External": "പുറമേയുള്ള"
+ "External": "പുറമേയുള്ള",
+ "TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
+ "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index ac7b92de60..be397f1b89 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
"TaskKeyframeExtractor": "Keyframe-uitpakker",
"External": "Extern",
- "HearingImpaired": "Slechthorend"
+ "HearingImpaired": "Slechthorend",
+ "TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
+ "TaskRefreshTrickplayImagesDescription": "Genereert trickplay-afbeeldingen voor video's in bibliotheken waarvoor dit is ingeschakeld."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index d4c15ac876..bd572b744b 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -124,5 +124,7 @@
"External": "Zewnętrzny",
"TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.",
"TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
- "HearingImpaired": "Niedosłyszący"
+ "HearingImpaired": "Niedosłyszący",
+ "TaskRefreshTrickplayImages": "Generuj obrazy trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index a75182f220..92ac2681e4 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -104,8 +104,8 @@
"TaskRefreshPeople": "Atualizar Pessoas",
"TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.",
"TaskCleanLogs": "Limpar a Diretoria de Logs",
- "TaskRefreshLibraryDescription": "Scannear a biblioteca de música para novos ficheiros e atualizar os metadados.",
- "TaskRefreshLibrary": "Scannear Biblioteca de Música",
+ "TaskRefreshLibraryDescription": "Analisar a biblioteca de música para novos ficheiros e atualizar os metadados.",
+ "TaskRefreshLibrary": "Analisar Biblioteca de Música",
"TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.",
"TaskRefreshChapterImages": "Extrair Imagens dos Capítulos",
"TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.",
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.",
"TaskKeyframeExtractor": "Extrator de Quadros-chave",
"External": "Externo",
- "HearingImpaired": "Surdo"
+ "HearingImpaired": "Surdo",
+ "TaskRefreshTrickplayImages": "Gerar imagens de truques",
+ "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas."
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 2281e80c8a..103393a1e4 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -92,7 +92,7 @@
"Application": "Aplicação",
"AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
"TaskCleanCache": "Limpar Diretório de Cache",
- "TasksApplicationCategory": "Aplicativo",
+ "TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Manutenção",
"TaskRefreshChannels": "Atualizar Canais",
@@ -123,5 +123,7 @@
"External": "Externo",
"HearingImpaired": "Problemas auditivos",
"TaskKeyframeExtractor": "Extrator de quadro-chave",
- "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo."
+ "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
+ "TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
+ "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 2c10bb4779..537a6d3f2f 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Extrage cadrele cheie din fișierele video pentru a crea liste de redare HLS mai precise. Această sarcină poate rula o perioadă lungă de timp.",
"External": "Extern",
"TaskKeyframeExtractor": "Extractor de cadre cheie",
- "HearingImpaired": "Ascultare Impară"
+ "HearingImpaired": "Ascultare Impară",
+ "TaskRefreshTrickplayImages": "Generează imagini Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Generează previzualizările trickplay pentru videourile din librăriile selectate."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index fa6c753b60..26d678a0c3 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.",
"TaskKeyframeExtractor": "Извлечение ключевых кадров",
"External": "Внешние",
- "HearingImpaired": "Для слабослышащих"
+ "HearingImpaired": "Для слабослышащих",
+ "TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена."
}
diff --git a/Emby.Server.Implementations/Localization/Core/si.json b/Emby.Server.Implementations/Localization/Core/si.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/si.json
@@ -0,0 +1 @@
+{}
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 858cc40dd8..43594a42eb 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -124,5 +124,7 @@
"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í",
+ "TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index 785e6b2262..97062deece 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna rutin kan ta lång tid.",
"TaskKeyframeExtractor": "Extraktor för nyckelbildrutor",
"External": "Extern",
- "HearingImpaired": "Hörselskadad"
+ "HearingImpaired": "Hörselskadad",
+ "TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
+ "TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index 770624a8df..646d7d7a5f 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -102,7 +102,7 @@
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இல் இருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
"TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.",
- "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+ "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்கும்படி கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்கி நிறுவுகிறது.",
"TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
"TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
"TaskCleanLogs": "பதிவு அடைவை சுத்தம் செய்யுங்கள்",
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.",
"TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்",
"External": "வெளி",
- "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்"
+ "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
+ "TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
+ "TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்."
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index ff77fb8c56..bd5398f083 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.",
"TaskKeyframeExtractor": "Екстрактор ключових кадрів",
"External": "Зовнішній",
- "HearingImpaired": "З порушеннями слуху"
+ "HearingImpaired": "З порушеннями слуху",
+ "TaskRefreshTrickplayImagesDescription": "Створює trickplay-зображення для відео у ввімкнених медіатеках.",
+ "TaskRefreshTrickplayImages": "Створення Trickplay-зображень"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 03265d3fb0..b88d4eeaf5 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -121,8 +121,10 @@
"Default": "默认",
"TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。",
"TaskOptimizeDatabase": "优化数据库",
- "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。",
+ "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的 HLS 播放列表。这项任务可能需要很长时间。",
"TaskKeyframeExtractor": "关键帧提取器",
"External": "外部",
- "HearingImpaired": "听力障碍"
+ "HearingImpaired": "听力障碍",
+ "TaskRefreshTrickplayImages": "生成时间轴缩略图",
+ "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 36f4df93d1..d57a2811d1 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -123,5 +123,7 @@
"TaskKeyframeExtractorDescription": "將關鍵幀從影片檔案提取出來並建立更精準的HLS播放清單。這可能需要很長時間。",
"TaskKeyframeExtractor": "關鍵幀提取器",
"External": "外部",
- "HearingImpaired": "聽力障礙"
+ "HearingImpaired": "聽力障礙",
+ "TaskRefreshTrickplayImages": "生成快轉縮圖",
+ "TaskRefreshTrickplayImagesDescription": "為啟用此設定的媒體庫生成快轉縮圖。"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index 96f4353998..16776b6bd6 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -71,25 +71,28 @@ namespace Emby.Server.Implementations.Localization
string countryCode = resource.Substring(RatingsPath.Length, 2);
var dict = new Dictionary(StringComparer.OrdinalIgnoreCase);
- await using var stream = _assembly.GetManifestResourceStream(resource);
- using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
- await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+ var stream = _assembly.GetManifestResourceStream(resource);
+ await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
{
- if (string.IsNullOrWhiteSpace(line))
+ using var reader = new StreamReader(stream!);
+ await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
- continue;
- }
-
- 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);
+ 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);
+ }
}
}
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 7732e32d0a..896f47923f 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder
{
var deadImages = images
.Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
- .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+ .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var image in deadImages)
diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs
index 51e92953df..2bcd5eab29 100644
--- a/Emby.Server.Implementations/Net/SocketFactory.cs
+++ b/Emby.Server.Implementations/Net/SocketFactory.cs
@@ -1,12 +1,15 @@
-#pragma warning disable CS1591
-
using System;
+using System.Linq;
using System.Net;
+using System.Net.NetworkInformation;
using System.Net.Sockets;
using MediaBrowser.Model.Net;
namespace Emby.Server.Implementations.Net
{
+ ///
+ /// Factory class to create different kinds of sockets.
+ ///
public class SocketFactory : ISocketFactory
{
///
@@ -29,7 +32,7 @@ namespace Emby.Server.Implementations.Net
}
catch
{
- socket?.Dispose();
+ socket.Dispose();
throw;
}
@@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net
///
public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
{
- ArgumentNullException.ThrowIfNull(bindInterface.Address);
+ var interfaceAddress = bindInterface.Address;
+ ArgumentNullException.ThrowIfNull(interfaceAddress);
if (localPort < 0)
{
@@ -49,13 +53,13 @@ namespace Emby.Server.Implementations.Net
try
{
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
- socket.Bind(new IPEndPoint(bindInterface.Address, localPort));
+ socket.Bind(new IPEndPoint(interfaceAddress, localPort));
return socket;
}
catch
{
- socket?.Dispose();
+ socket.Dispose();
throw;
}
@@ -82,22 +86,31 @@ namespace Emby.Server.Implementations.Net
try
{
- var interfaceIndex = bindInterface.Index;
- var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex);
-
socket.MulticastLoopback = false;
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
- socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped);
- socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
- socket.Bind(new IPEndPoint(multicastAddress, localPort));
+
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+ {
+ socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
+ socket.Bind(new IPEndPoint(multicastAddress, localPort));
+ }
+ else
+ {
+ // Only create socket if interface supports multicast
+ var interfaceIndex = bindInterface.Index;
+ var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
+
+ socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
+ socket.Bind(new IPEndPoint(bindIPAddress, localPort));
+ }
return socket;
}
catch
{
- socket?.Dispose();
+ socket.Dispose();
throw;
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 0cb450cdf3..d2e2fd7d56 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -10,6 +10,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -74,7 +75,7 @@ namespace Emby.Server.Implementations.Playlists
throw new ArgumentException(nameof(parentFolder));
}
- if (string.IsNullOrEmpty(options.MediaType))
+ if (options.MediaType is null || options.MediaType == MediaType.Unknown)
{
foreach (var itemId in options.ItemIdList)
{
@@ -84,7 +85,7 @@ namespace Emby.Server.Implementations.Playlists
throw new ArgumentException("No item exists with the supplied Id");
}
- if (!string.IsNullOrEmpty(item.MediaType))
+ if (item.MediaType != MediaType.Unknown)
{
options.MediaType = item.MediaType;
}
@@ -102,20 +103,20 @@ namespace Emby.Server.Implementations.Playlists
{
options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist)
.Select(i => i.MediaType)
- .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+ .FirstOrDefault(i => i != MediaType.Unknown);
}
}
- if (!string.IsNullOrEmpty(options.MediaType))
+ if (options.MediaType is not null && options.MediaType != MediaType.Unknown)
{
break;
}
}
}
- if (string.IsNullOrEmpty(options.MediaType))
+ if (options.MediaType is null || options.MediaType == MediaType.Unknown)
{
- options.MediaType = "Audio";
+ options.MediaType = MediaType.Audio;
}
var user = _userManager.GetUserById(options.UserId);
@@ -168,7 +169,7 @@ namespace Emby.Server.Implementations.Playlists
return path;
}
- private List GetPlaylistItems(IEnumerable itemIds, string playlistMediaType, User user, DtoOptions options)
+ private List GetPlaylistItems(IEnumerable itemIds, MediaType playlistMediaType, User user, DtoOptions options)
{
var items = itemIds.Select(i => _libraryManager.GetItemById(i)).Where(i => i is not null);
@@ -327,9 +328,9 @@ namespace Emby.Server.Implementations.Playlists
// this is probably best done as a metadata provider
// saving a file over itself will require some work to prevent this from happening when not needed
var playlistPath = item.Path;
- var extension = Path.GetExtension(playlistPath);
+ var extension = Path.GetExtension(playlistPath.AsSpan());
- if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
+ if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase))
{
var playlist = new WplPlaylist();
foreach (var child in item.GetLinkedChildren())
@@ -362,8 +363,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new WplContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
{
var playlist = new ZplPlaylist();
foreach (var child in item.GetLinkedChildren())
@@ -396,8 +396,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new ZplContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
{
var playlist = new M3uPlaylist
{
@@ -428,8 +427,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new M3uContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
var playlist = new M3uPlaylist();
playlist.IsExtended = true;
@@ -458,8 +456,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new M3uContent().ToText(playlist);
File.WriteAllText(playlistPath, text);
}
-
- if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
+ else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
{
var playlist = new PlsPlaylist();
foreach (var child in item.GetLinkedChildren())
diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
index d67caa52dc..5c616d5349 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Playlists
public override bool SupportsInheritedParentImages => false;
[JsonIgnore]
- public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
+ public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.Playlists;
protected override IEnumerable GetEligibleChildrenForRecursiveChildren(User user)
{
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index d7189ef0ca..db82a2900a 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -12,10 +12,11 @@ using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
-using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Plugins;
@@ -37,7 +38,7 @@ namespace Emby.Server.Implementations.Plugins
private readonly List _assemblyLoadContexts;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger _logger;
- private readonly IApplicationHost _appHost;
+ private readonly IServerApplicationHost _appHost;
private readonly ServerConfiguration _config;
private readonly List _plugins;
private readonly Version _minimumVersion;
@@ -48,13 +49,13 @@ namespace Emby.Server.Implementations.Plugins
/// Initializes a new instance of the class.
///
/// The .
- /// The .
+ /// The .
/// The .
/// The plugin path.
/// The application version.
public PluginManager(
ILogger logger,
- IApplicationHost appHost,
+ IServerApplicationHost appHost,
ServerConfiguration config,
string pluginsPath,
Version appVersion)
@@ -222,7 +223,7 @@ namespace Emby.Server.Implementations.Plugins
try
{
var instance = (IPluginServiceRegistrator?)Activator.CreateInstance(pluginServiceRegistrator);
- instance?.RegisterServices(serviceCollection);
+ instance?.RegisterServices(serviceCollection, _appHost);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
@@ -386,11 +387,11 @@ namespace Emby.Server.Implementations.Plugins
var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]);
- await using var fileStream = AsyncFile.OpenWrite(imagePath);
-
+ var fileStream = AsyncFile.OpenWrite(imagePath);
+ Stream? downloadStream = null;
try
{
- await using var downloadStream = await HttpClientFactory
+ downloadStream = await HttpClientFactory
.CreateClient(NamedClient.Default)
.GetStreamAsync(url)
.ConfigureAwait(false);
@@ -402,6 +403,14 @@ namespace Emby.Server.Implementations.Plugins
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
imagePath = string.Empty;
}
+ finally
+ {
+ await fileStream.DisposeAsync().ConfigureAwait(false);
+ if (downloadStream is not null)
+ {
+ await downloadStream.DisposeAsync().ConfigureAwait(false);
+ }
+ }
}
var manifest = new PluginManifest
@@ -421,7 +430,7 @@ namespace Emby.Server.Implementations.Plugins
ImagePath = imagePath
};
- if (!await ReconcileManifest(manifest, path))
+ if (!await ReconcileManifest(manifest, path).ConfigureAwait(false))
{
// An error occurred during reconciliation and saving could be undesirable.
return false;
@@ -458,7 +467,7 @@ namespace Emby.Server.Implementations.Plugins
}
using var metaStream = File.OpenRead(metafile);
- var localManifest = await JsonSerializer.DeserializeAsync(metaStream, _jsonOptions);
+ var localManifest = await JsonSerializer.DeserializeAsync(metaStream, _jsonOptions).ConfigureAwait(false);
localManifest ??= new PluginManifest();
if (!Equals(localManifest.Id, manifest.Id))
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
index 6ad6c4cbd6..d03d408633 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dto;
@@ -115,7 +116,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
try
{
- previouslyFailedImages = File.ReadAllText(failHistoryPath)
+ previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
.Split('|', StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
@@ -156,7 +157,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
}
string text = string.Join('|', previouslyFailedImages);
- File.WriteAllText(failHistoryPath, text);
+ await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
}
numComplete++;
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index e935f7e5e5..e8e63d286d 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -19,6 +19,7 @@ using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -48,6 +49,7 @@ namespace Emby.Server.Implementations.Session
public sealed class SessionManager : ISessionManager, IAsyncDisposable
{
private readonly IUserDataManager _userDataManager;
+ private readonly IServerConfigurationManager _config;
private readonly ILogger _logger;
private readonly IEventManager _eventManager;
private readonly ILibraryManager _libraryManager;
@@ -63,6 +65,7 @@ namespace Emby.Server.Implementations.Session
= new(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer;
+ private Timer _inactiveTimer;
private DtoOptions _itemInfoDtoOptions;
private bool _disposed = false;
@@ -71,6 +74,7 @@ namespace Emby.Server.Implementations.Session
ILogger logger,
IEventManager eventManager,
IUserDataManager userDataManager,
+ IServerConfigurationManager config,
ILibraryManager libraryManager,
IUserManager userManager,
IMusicManager musicManager,
@@ -84,6 +88,7 @@ namespace Emby.Server.Implementations.Session
_logger = logger;
_eventManager = eventManager;
_userDataManager = userDataManager;
+ _config = config;
_libraryManager = libraryManager;
_userManager = userManager;
_musicManager = musicManager;
@@ -369,6 +374,15 @@ namespace Emby.Server.Implementations.Session
session.LastPlaybackCheckIn = DateTime.UtcNow;
}
+ if (info.IsPaused && session.LastPausedDate is null)
+ {
+ session.LastPausedDate = DateTime.UtcNow;
+ }
+ else if (!info.IsPaused)
+ {
+ session.LastPausedDate = null;
+ }
+
session.PlayState.IsPaused = info.IsPaused;
session.PlayState.PositionTicks = info.PositionTicks;
session.PlayState.MediaSourceId = info.MediaSourceId;
@@ -536,9 +550,18 @@ namespace Emby.Server.Implementations.Session
return users;
}
- private void StartIdleCheckTimer()
+ private void StartCheckTimers()
{
_idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
+
+ if (_config.Configuration.InactiveSessionThreshold > 0)
+ {
+ _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
+ }
+ else
+ {
+ StopInactiveCheckTimer();
+ }
}
private void StopIdleCheckTimer()
@@ -550,6 +573,15 @@ namespace Emby.Server.Implementations.Session
}
}
+ private void StopInactiveCheckTimer()
+ {
+ if (_inactiveTimer is not null)
+ {
+ _inactiveTimer.Dispose();
+ _inactiveTimer = null;
+ }
+ }
+
private async void CheckForIdlePlayback(object state)
{
var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
@@ -585,13 +617,50 @@ namespace Emby.Server.Implementations.Session
playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
.ToList();
}
-
- if (playingSessions.Count == 0)
+ else
{
StopIdleCheckTimer();
}
}
+ private async void CheckForInactiveSteams(object state)
+ {
+ var inactiveSessions = Sessions.Where(i =>
+ i.NowPlayingItem is not null
+ && i.PlayState.IsPaused
+ && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionThreshold);
+
+ foreach (var session in inactiveSessions)
+ {
+ _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session.Id, _config.Configuration.InactiveSessionThreshold);
+
+ try
+ {
+ await SendPlaystateCommand(
+ session.Id,
+ session.Id,
+ new PlaystateRequest()
+ {
+ Command = PlaystateCommand.Stop,
+ ControllingUserId = session.UserId.ToString(),
+ SeekPositionTicks = session.PlayState?.PositionTicks
+ },
+ CancellationToken.None).ConfigureAwait(true);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", session.Id);
+ }
+ }
+
+ bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
+
+ if (!playingSessions)
+ {
+ StopInactiveCheckTimer();
+ }
+ }
+
private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
{
var item = session.FullNowPlayingItem;
@@ -668,7 +737,7 @@ namespace Emby.Server.Implementations.Session
eventArgs,
_logger);
- StartIdleCheckTimer();
+ StartCheckTimers();
}
///
@@ -762,7 +831,7 @@ namespace Emby.Server.Implementations.Session
session.StartAutomaticProgress(info);
}
- StartIdleCheckTimer();
+ StartCheckTimers();
}
private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
@@ -1384,10 +1453,15 @@ namespace Emby.Server.Implementations.Session
return AuthenticateNewSessionInternal(request, false);
}
- private async Task AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
+ internal async Task AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
{
CheckDisposed();
+ ArgumentException.ThrowIfNullOrEmpty(request.App);
+ ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
+ ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
+ ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
+
User user = null;
if (!request.UserId.Equals(default))
{
@@ -1448,8 +1522,11 @@ namespace Emby.Server.Implementations.Session
return returnResult;
}
- private async Task GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
+ internal async Task GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
{
+ // This should be validated above, but if it isn't don't delete all tokens.
+ ArgumentException.ThrowIfNullOrEmpty(deviceId);
+
var existing = (await _deviceManager.GetDevices(
new DeviceQuery
{
@@ -1798,6 +1875,12 @@ namespace Emby.Server.Implementations.Session
_idleTimer = null;
}
+ if (_inactiveTimer is not null)
+ {
+ await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
+ _inactiveTimer = null;
+ }
+
await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
_deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
index 964004eccb..6d13c6d573 100644
--- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Sorting;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.AiredEpisodeOrder;
+ public ItemSortBy Type => ItemSortBy.AiredEpisodeOrder;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
index 67a9fbd3bc..65c8599e75 100644
--- a/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
@@ -16,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.AlbumArtist;
+ public ItemSortBy Type => ItemSortBy.AlbumArtist;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/AlbumComparer.cs b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
index 4bed0fca12..e071136551 100644
--- a/Emby.Server.Implementations/Sorting/AlbumComparer.cs
+++ b/Emby.Server.Implementations/Sorting/AlbumComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
@@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.Album;
+ public ItemSortBy Type => ItemSortBy.Album;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
index a8bb55e2bc..f99977e5c5 100644
--- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Sorting;
@@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting
public class ArtistComparer : IBaseItemComparer
{
///
- public string Name => ItemSortBy.Artist;
+ public ItemSortBy Type => ItemSortBy.Artist;
///
public int Compare(BaseItem? x, BaseItem? y)
diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
index 5cb11ab465..9e02ea2ae8 100644
--- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.CommunityRating;
+ public ItemSortBy Type => ItemSortBy.CommunityRating;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
index ba1835e4f2..d4a8d46898 100644
--- a/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.CriticRating;
+ public ItemSortBy Type => ItemSortBy.CriticRating;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
index 6133aaccc6..b86b4432f8 100644
--- a/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateCreatedComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.DateCreated;
+ public ItemSortBy Type => ItemSortBy.DateCreated;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index b1cb123ce1..e1c26d0121 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -3,6 +3,7 @@
using System;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -34,7 +35,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.DateLastContentAdded;
+ public ItemSortBy Type => ItemSortBy.DateLastContentAdded;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
index 453d817c76..d668c17bfc 100644
--- a/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DatePlayedComparer.cs
@@ -2,6 +2,7 @@
using System;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -36,7 +37,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.DatePlayed;
+ public ItemSortBy Type => ItemSortBy.DatePlayed;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
index 1bcaccd8a3..11cad62567 100644
--- a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.IndexNumber;
+ public ItemSortBy Type => ItemSortBy.IndexNumber;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
index 73e628cf75..622a341b6a 100644
--- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs
@@ -2,6 +2,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.IsFavoriteOrLiked;
+ public ItemSortBy Type => ItemSortBy.IsFavoriteOrLiked;
///
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
index 3c5ddeefaa..6f0ca59c53 100644
--- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs
@@ -1,5 +1,6 @@
#pragma warning disable CS1591
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.IsFolder;
+ public ItemSortBy Type => ItemSortBy.IsFolder;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
index 7d77a8bc5a..2a3e456c2d 100644
--- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.IsUnplayed;
+ public ItemSortBy Type => ItemSortBy.IsUnplayed;
///
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
index 926835f906..afd8ccf9f3 100644
--- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.IsUnplayed;
+ public ItemSortBy Type => ItemSortBy.IsUnplayed;
///
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/NameComparer.cs b/Emby.Server.Implementations/Sorting/NameComparer.cs
index 93bec4db99..72d9c79739 100644
--- a/Emby.Server.Implementations/Sorting/NameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/NameComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.Name;
+ public ItemSortBy Type => ItemSortBy.Name;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
index ce44f99a69..b4ee2c7234 100644
--- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
+++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Globalization;
@@ -21,7 +22,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.OfficialRating;
+ public ItemSortBy Type => ItemSortBy.OfficialRating;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
index c54750843e..5aeac29be4 100644
--- a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.ParentIndexNumber;
+ public ItemSortBy Type => ItemSortBy.ParentIndexNumber;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
index 16f1b79b3e..12f88bf4da 100644
--- a/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PlayCountComparer.cs
@@ -1,6 +1,7 @@
#nullable disable
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
@@ -23,7 +24,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.PlayCount;
+ public ItemSortBy Type => ItemSortBy.PlayCount;
///
/// Gets or sets the user data repository.
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
index db86b8002d..8c8b8824f3 100644
--- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.PremiereDate;
+ public ItemSortBy Type => ItemSortBy.PremiereDate;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
index 7fd1e024d0..9aec87f183 100644
--- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -1,3 +1,4 @@
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.ProductionYear;
+ public ItemSortBy Type => ItemSortBy.ProductionYear;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/RandomComparer.cs b/Emby.Server.Implementations/Sorting/RandomComparer.cs
index bf0168222d..6f8ea5b741 100644
--- a/Emby.Server.Implementations/Sorting/RandomComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RandomComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.Random;
+ public ItemSortBy Type => ItemSortBy.Random;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
index 753e58324c..3c096ab023 100644
--- a/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
+++ b/Emby.Server.Implementations/Sorting/RuntimeComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.Runtime;
+ public ItemSortBy Type => ItemSortBy.Runtime;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
index 5b6c64f63a..ed42fd6d55 100644
--- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.SeriesSortName;
+ public ItemSortBy Type => ItemSortBy.SeriesSortName;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/SortNameComparer.cs b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
index 19abafe192..314c25d128 100644
--- a/Emby.Server.Implementations/Sorting/SortNameComparer.cs
+++ b/Emby.Server.Implementations/Sorting/SortNameComparer.cs
@@ -1,4 +1,5 @@
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.SortName;
+ public ItemSortBy Type => ItemSortBy.SortName;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
index 2759d20de8..e0b438ef1a 100644
--- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Sorting;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.StartDate;
+ public ItemSortBy Type => ItemSortBy.StartDate;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs
index 89d10f3d23..0edffb783b 100644
--- a/Emby.Server.Implementations/Sorting/StudioComparer.cs
+++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Sorting;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
/// Gets the name.
///
/// The name.
- public string Name => ItemSortBy.Studio;
+ public ItemSortBy Type => ItemSortBy.Studio;
///
/// Compares the specified x.
diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs
new file mode 100644
index 0000000000..2c477218fe
--- /dev/null
+++ b/Emby.Server.Implementations/SystemManager.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Hosting;
+
+namespace Emby.Server.Implementations;
+
+///
+public class SystemManager : ISystemManager
+{
+ private readonly IHostApplicationLifetime _applicationLifetime;
+ private readonly IServerApplicationHost _applicationHost;
+ private readonly IServerApplicationPaths _applicationPaths;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IStartupOptions _startupOptions;
+ private readonly IInstallationManager _installationManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of .
+ /// Instance of .
+ /// Instance of .
+ /// Instance of .
+ /// Instance of .
+ /// Instance of .
+ public SystemManager(
+ IHostApplicationLifetime applicationLifetime,
+ IServerApplicationHost applicationHost,
+ IServerApplicationPaths applicationPaths,
+ IServerConfigurationManager configurationManager,
+ IStartupOptions startupOptions,
+ IInstallationManager installationManager)
+ {
+ _applicationLifetime = applicationLifetime;
+ _applicationHost = applicationHost;
+ _applicationPaths = applicationPaths;
+ _configurationManager = configurationManager;
+ _startupOptions = startupOptions;
+ _installationManager = installationManager;
+ }
+
+ ///
+ public SystemInfo GetSystemInfo(HttpRequest request)
+ {
+ return new SystemInfo
+ {
+ HasPendingRestart = _applicationHost.HasPendingRestart,
+ IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
+ Version = _applicationHost.ApplicationVersionString,
+ WebSocketPortNumber = _applicationHost.HttpPort,
+ CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
+ Id = _applicationHost.SystemId,
+ ProgramDataPath = _applicationPaths.ProgramDataPath,
+ WebPath = _applicationPaths.WebPath,
+ LogPath = _applicationPaths.LogDirectoryPath,
+ ItemsByNamePath = _applicationPaths.InternalMetadataPath,
+ InternalMetadataPath = _applicationPaths.InternalMetadataPath,
+ CachePath = _applicationPaths.CachePath,
+ TranscodingTempPath = _configurationManager.GetTranscodePath(),
+ ServerName = _applicationHost.FriendlyName,
+ LocalAddress = _applicationHost.GetSmartApiUrl(request),
+ SupportsLibraryMonitor = true,
+ PackageName = _startupOptions.PackageName,
+ CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications
+ };
+ }
+
+ ///
+ public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
+ {
+ return new PublicSystemInfo
+ {
+ Version = _applicationHost.ApplicationVersionString,
+ ProductName = _applicationHost.Name,
+ Id = _applicationHost.SystemId,
+ ServerName = _applicationHost.FriendlyName,
+ LocalAddress = _applicationHost.GetSmartApiUrl(request),
+ StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted
+ };
+ }
+
+ ///
+ public void Restart() => ShutdownInternal(true);
+
+ ///
+ public void Shutdown() => ShutdownInternal(false);
+
+ private void ShutdownInternal(bool restart)
+ {
+ Task.Run(async () =>
+ {
+ await Task.Delay(100).ConfigureAwait(false);
+ _applicationHost.ShouldRestart = restart;
+ _applicationLifetime.StopApplication();
+ });
+ }
+}
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index a3bbd6df0c..2d806c146b 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -27,9 +27,9 @@ namespace Emby.Server.Implementations.Udp
private readonly byte[] _receiveBuffer = new byte[8192];
- private Socket _udpSocket;
- private IPEndPoint _endpoint;
- private bool _disposed = false;
+ private readonly Socket _udpSocket;
+ private readonly IPEndPoint _endpoint;
+ private bool _disposed;
///
/// Initializes a new instance of the class.
@@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp
_endpoint = new IPEndPoint(bindAddress, port);
- _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+ _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
+ {
+ MulticastLoopback = false,
+ };
_udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
}
@@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp
try
{
+ _logger.LogDebug("Sending AutoDiscovery response");
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
}
catch (SocketException ex)
@@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp
{
try
{
- var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false);
+ var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
+ var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
{
@@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp
}
catch (OperationCanceledException)
{
- // Don't throw
+ _logger.LogDebug("Broadcast socket operation cancelled");
}
}
}
@@ -125,9 +130,8 @@ namespace Emby.Server.Implementations.Udp
return;
}
- _udpSocket?.Dispose();
-
- GC.SuppressFinalize(this);
+ _udpSocket.Dispose();
+ _disposed = true;
}
}
}
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 6c198b6f99..c717744b12 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
{
- var extension = Path.GetExtension(package.SourceUrl);
- if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
+ if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return;
@@ -521,10 +520,9 @@ namespace Emby.Server.Implementations.Updates
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
- using var md5 = MD5.Create();
cancellationToken.ThrowIfCancellationRequested();
- var hash = Convert.ToHexString(md5.ComputeHash(stream));
+ var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false));
if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
@@ -557,7 +555,7 @@ namespace Emby.Server.Implementations.Updates
reader.ExtractToDirectory(targetDir, true);
// Ensure we create one or populate existing ones with missing data.
- await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status);
+ await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
_pluginManager.ImportPluginFrom(targetDir);
}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index 53841b0c44..02fdef1507 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -84,4 +84,9 @@ public static class Policies
/// Policy name for managing LiveTV.
///
public const string LiveTvManagement = "LiveTvManagement";
+
+ ///
+ /// Policy name for accessing subtitles management.
+ ///
+ public const string SubtitleManagement = "SubtitleManagement";
}
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index c9d2f67f92..e7d3e694ab 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -95,7 +95,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -113,7 +113,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
@@ -299,7 +299,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -317,7 +317,7 @@ public class ArtistsController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index 11c4ac3768..fdc16ee23f 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -122,7 +122,7 @@ public class ChannelsController : BaseJellyfinApiController
[FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
userId = RequestHelpers.GetUserId(User, userId);
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 95b296fae9..42576934b3 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -5,7 +5,6 @@ using System.IO;
using System.Net.Mime;
using System.Threading.Tasks;
using Emby.Dlna;
-using Emby.Dlna.Main;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Dlna;
@@ -33,12 +32,19 @@ public class DlnaServerController : BaseJellyfinApiController
/// Initializes a new instance of the class.
///
/// Instance of the interface.
- public DlnaServerController(IDlnaManager dlnaManager)
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public DlnaServerController(
+ IDlnaManager dlnaManager,
+ IContentDirectory contentDirectory,
+ IConnectionManager connectionManager,
+ IMediaReceiverRegistrar mediaReceiverRegistrar)
{
_dlnaManager = dlnaManager;
- _contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
- _connectionManager = DlnaEntryPoint.Current.ConnectionManager;
- _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
+ _contentDirectory = contentDirectory;
+ _connectionManager = connectionManager;
+ _mediaReceiverRegistrar = mediaReceiverRegistrar;
}
///
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 065a4ce5c6..38953dc21f 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
private const string DefaultEventEncoderPreset = "superfast";
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
+ private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
+
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager;
@@ -408,6 +410,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// Optional. The .
/// Optional. The streaming options.
/// Enable adaptive bitrate streaming.
+ /// Enable trickplay image playlists being added to master playlist.
/// Video stream returned.
/// A containing the playlist file.
[HttpGet("Videos/{itemId}/master.m3u8")]
@@ -465,7 +468,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context,
[FromQuery] Dictionary streamOptions,
- [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+ [FromQuery] bool enableAdaptiveBitrateStreaming = true,
+ [FromQuery] bool enableTrickplay = true)
{
var streamingRequest = new HlsVideoRequestDto
{
@@ -519,7 +523,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions,
- EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+ EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
+ EnableTrickplay = enableTrickplay
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -1705,16 +1710,31 @@ public class DynamicHlsController : BaseJellyfinApiController
var audioCodec = _encodingHelper.GetAudioEncoder(state);
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
+ // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
+ var strictArgs = string.Empty;
+ var actualOutputAudioCodec = state.ActualOutputAudioCodec;
+ if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
+ || (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+ && _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
+ {
+ strictArgs = " -strict -2";
+ }
+
if (!state.IsOutputVideo)
{
+ var audioTranscodeParams = string.Empty;
+
+ // -vn to drop any video streams
+ audioTranscodeParams += "-vn";
+
if (EncodingHelper.IsCopyCodec(audioCodec))
{
- return "-acodec copy -strict -2" + bitStreamArgs;
+ return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs;
}
- var audioTranscodeParams = string.Empty;
-
- audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs;
+ audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs;
var audioBitrate = state.OutputAudioBitrate;
var audioChannels = state.OutputAudioChannels;
@@ -1742,21 +1762,9 @@ public class DynamicHlsController : BaseJellyfinApiController
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
- audioTranscodeParams += " -vn";
return audioTranscodeParams;
}
- // dts, flac, opus and truehd are experimental in mp4 muxer
- var strictArgs = string.Empty;
- var actualOutputAudioCodec = state.ActualOutputAudioCodec;
- if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
- || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
- || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
- || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
- {
- strictArgs = " -strict -2";
- }
-
if (EncodingHelper.IsCopyCodec(audioCodec))
{
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
@@ -2041,9 +2049,9 @@ public class DynamicHlsController : BaseJellyfinApiController
return null;
}
- var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
+ var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
- var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+ var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs
index d51a5325f5..baeb8b81a0 100644
--- a/Jellyfin.Api/Controllers/FilterController.cs
+++ b/Jellyfin.Api/Controllers/FilterController.cs
@@ -50,7 +50,7 @@ public class FilterController : BaseJellyfinApiController
[FromQuery] Guid? userId,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index da60f2c60b..062e1062d7 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -85,7 +85,7 @@ public class GenresController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
@@ -131,8 +131,8 @@ public class GenresController : BaseJellyfinApiController
QueryResult<(BaseItem, ItemCounts)> result;
if (parentItem is ICollectionFolder parentCollectionFolder
- && (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
- || string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
+ && (parentCollectionFolder.CollectionType == CollectionType.Music
+ || parentCollectionFolder.CollectionType == CollectionType.MusicVideos))
{
result = _libraryManager.GetMusicGenres(query);
}
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index d7cec865e1..6eedfd8c7f 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{
// TODO: Deprecate with new iOS app
- var file = segmentId + Path.GetExtension(Request.Path);
+ var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
@@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{
- var file = playlistId + Path.GetExtension(Request.Path);
+ var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file);
- if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
+ if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
+ || Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Invalid segment.");
}
@@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
[FromRoute, Required] string segmentId,
[FromRoute, Required] string segmentContainer)
{
- var file = segmentId + Path.GetExtension(Request.Path);
+ var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 3c5f18af55..7b10ea170f 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -7,6 +7,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
+using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
_appPaths = appPaths;
}
+ private static Stream GetFromBase64Stream(Stream inputStream)
+ => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
+
///
/// Sets the user image.
///
@@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .SaveImage(stream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager
- .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+ .SaveImage(stream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
@@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
- await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+ await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
@@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType.");
}
- var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = GetFromBase64Stream(Request.Body);
+ await using (stream.ConfigureAwait(false))
{
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
var brandingOptions = _serverConfigurationManager.GetConfiguration("branding");
@@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await using (fs.ConfigureAwait(false))
{
- await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
+ await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
}
return NoContent();
@@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
return NoContent();
}
- private static async Task GetMemoryStream(Stream inputStream)
- {
- using var reader = new StreamReader(inputStream);
- var text = await reader.ReadToEndAsync().ConfigureAwait(false);
-
- var bytes = Convert.FromBase64String(text);
- return new MemoryStream(bytes, 0, bytes.Length, false, true);
- }
-
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
{
int? width = null;
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index 504f2fa1d7..3be891b930 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -164,18 +165,16 @@ public class ItemUpdateController : BaseJellyfinApiController
var inheritedContentType = _libraryManager.GetInheritedContentType(item);
var configuredContentType = _libraryManager.GetConfiguredContentType(item);
- if (string.IsNullOrWhiteSpace(inheritedContentType) ||
- !string.IsNullOrWhiteSpace(configuredContentType))
+ if (inheritedContentType is null || configuredContentType is not null)
{
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
info.ContentType = configuredContentType;
- if (string.IsNullOrWhiteSpace(inheritedContentType)
- || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ if (inheritedContentType is null || inheritedContentType == CollectionType.TvShows)
{
info.ContentTypeOptions = info.ContentTypeOptions
.Where(i => string.IsNullOrWhiteSpace(i.Value)
- || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+ || string.Equals(i.Value, "TvShows", StringComparison.OrdinalIgnoreCase))
.ToArray();
}
}
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 80128536da..4e46e808ae 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -195,9 +195,9 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -269,13 +269,13 @@ public class ItemsController : BaseJellyfinApiController
folder = _libraryManager.GetUserRootFolder();
}
- string? collectionType = null;
+ CollectionType? collectionType = null;
if (folder is IHasCollectionType hasCollectionType)
{
collectionType = hasCollectionType.CollectionType;
}
- if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+ if (collectionType == CollectionType.Playlists)
{
recursive = true;
includeItemTypes = new[] { BaseItemKind.Playlist };
@@ -652,9 +652,9 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
@@ -812,7 +812,7 @@ public class ItemsController : BaseJellyfinApiController
[FromQuery] string? searchTerm,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs
index 46c0a8d527..3cd78b0863 100644
--- a/Jellyfin.Api/Controllers/LibraryController.cs
+++ b/Jellyfin.Api/Controllers/LibraryController.cs
@@ -294,8 +294,8 @@ public class LibraryController : BaseJellyfinApiController
return new AllThemeMediaResult
{
- ThemeSongsResult = themeSongs?.Value,
- ThemeVideosResult = themeVideos?.Value,
+ ThemeSongsResult = themeSongs.Value,
+ ThemeVideosResult = themeVideos.Value,
SoundtrackSongsResult = new ThemeMediaResult()
};
}
@@ -490,7 +490,7 @@ public class LibraryController : BaseJellyfinApiController
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
- parent = parent?.GetParent();
+ parent = parent.GetParent();
}
return baseItemDtos;
@@ -788,7 +788,7 @@ public class LibraryController : BaseJellyfinApiController
[Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult GetLibraryOptionsInfo(
- [FromQuery] string? libraryContentType,
+ [FromQuery] CollectionType? libraryContentType,
[FromQuery] bool isNewLibrary = false)
{
var result = new LibraryOptionsResultDto();
@@ -922,7 +922,7 @@ public class LibraryController : BaseJellyfinApiController
}
}
- private static string[] GetRepresentativeItemTypes(string? contentType)
+ private static string[] GetRepresentativeItemTypes(CollectionType? contentType)
{
return contentType switch
{
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 649397d68d..58159406a2 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -143,7 +143,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true)
@@ -547,7 +547,7 @@ public class LiveTvController : BaseJellyfinApiController
[FromQuery] bool? isSports,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 435457af67..94c8993575 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -85,7 +85,7 @@ public class MusicGenresController : BaseJellyfinApiController
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs
index 8d2a738d4a..c4c89ccde0 100644
--- a/Jellyfin.Api/Controllers/PlaylistsController.cs
+++ b/Jellyfin.Api/Controllers/PlaylistsController.cs
@@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.PlaylistDtos;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
@@ -75,7 +76,7 @@ public class PlaylistsController : BaseJellyfinApiController
[FromQuery, ParameterObsolete] string? name,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList ids,
[FromQuery, ParameterObsolete] Guid? userId,
- [FromQuery, ParameterObsolete] string? mediaType,
+ [FromQuery, ParameterObsolete] MediaType? mediaType,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
{
if (ids.Count == 0)
diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs
index 387b3ea5a6..5b45941657 100644
--- a/Jellyfin.Api/Controllers/SearchController.cs
+++ b/Jellyfin.Api/Controllers/SearchController.cs
@@ -86,7 +86,7 @@ public class SearchController : BaseJellyfinApiController
[FromQuery, Required] string searchTerm,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery] Guid? parentId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index e93456de66..e20cf034dc 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/Jellyfin.Api/Controllers/SessionController.cs
@@ -393,7 +393,7 @@ public class SessionController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task PostCapabilities(
[FromQuery] string? id,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 7d02550b68..c9e256af38 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -6,6 +6,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
+using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -114,7 +115,7 @@ public class SubtitleController : BaseJellyfinApiController
/// Subtitles retrieved.
/// An array of .
[HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
- [Authorize]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task>> SearchRemoteSubtitles(
[FromRoute, Required] Guid itemId,
@@ -134,7 +135,7 @@ public class SubtitleController : BaseJellyfinApiController
/// Subtitle downloaded.
/// A .
[HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
- [Authorize]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task DownloadRemoteSubtitles(
[FromRoute, Required] Guid itemId,
@@ -398,16 +399,15 @@ public class SubtitleController : BaseJellyfinApiController
/// Subtitle uploaded.
/// A .
[HttpPost("Videos/{itemId}/Subtitles")]
- [Authorize(Policy = Policies.RequiresElevation)]
+ [Authorize(Policy = Policies.SubtitleManagement)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task UploadSubtitle(
[FromRoute, Required] Guid itemId,
[FromBody, Required] UploadSubtitleDto body)
{
var video = (Video)_libraryManager.GetItemById(itemId);
- var data = Convert.FromBase64String(body.Data);
- var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
- await using (memoryStream.ConfigureAwait(false))
+ var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
+ await using (stream.ConfigureAwait(false))
{
await _subtitleManager.UploadSubtitle(
video,
@@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController
Language = body.Language,
IsForced = body.IsForced,
IsHearingImpaired = body.IsHearingImpaired,
- Stream = memoryStream
+ Stream = stream
}).ConfigureAwait(false);
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs
index 5b808f257c..675757fc51 100644
--- a/Jellyfin.Api/Controllers/SuggestionsController.cs
+++ b/Jellyfin.Api/Controllers/SuggestionsController.cs
@@ -56,7 +56,7 @@ public class SuggestionsController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult> GetSuggestions(
[FromRoute, Required] Guid userId,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaType,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index 42ac4a9b4b..11095a97f0 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -10,7 +10,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.System;
@@ -26,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
///
public class SystemController : BaseJellyfinApiController
{
+ private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost;
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
- private readonly INetworkManager _network;
- private readonly ILogger _logger;
+ private readonly INetworkManager _networkManager;
+ private readonly ISystemManager _systemManager;
///
/// Initializes a new instance of the class.
///
- /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
/// Instance of interface.
/// Instance of interface.
- /// Instance of interface.
- /// Instance of interface.
+ /// Instance of interface.
+ /// Instance of interface.
public SystemController(
- IServerConfigurationManager serverConfigurationManager,
+ ILogger logger,
IServerApplicationHost appHost,
+ IServerApplicationPaths appPaths,
IFileSystem fileSystem,
- INetworkManager network,
- ILogger logger)
+ INetworkManager networkManager,
+ ISystemManager systemManager)
{
- _appPaths = serverConfigurationManager.ApplicationPaths;
+ _logger = logger;
_appHost = appHost;
+ _appPaths = appPaths;
_fileSystem = fileSystem;
- _network = network;
- _logger = logger;
+ _networkManager = networkManager;
+ _systemManager = systemManager;
}
///
@@ -65,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult GetSystemInfo()
- {
- return _appHost.GetSystemInfo(Request);
- }
+ => _systemManager.GetSystemInfo(Request);
///
/// Gets public information about the server.
@@ -77,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
[HttpGet("Info/Public")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult GetPublicSystemInfo()
- {
- return _appHost.GetPublicSystemInfo(Request);
- }
+ => _systemManager.GetPublicSystemInfo(Request);
///
/// Pings the system.
@@ -90,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
[HttpPost("Ping", Name = "PostPingSystem")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult PingSystem()
- {
- return _appHost.Name;
- }
+ => _appHost.Name;
///
/// Restarts the application.
@@ -106,7 +103,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult RestartApplication()
{
- _appHost.Restart();
+ _systemManager.Restart();
return NoContent();
}
@@ -122,7 +119,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult ShutdownApplication()
{
- _appHost.Shutdown();
+ _systemManager.Shutdown();
return NoContent();
}
@@ -180,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
return new EndPointInfo
{
IsLocal = HttpContext.IsLocal(),
- IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
+ IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
};
}
@@ -218,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult> GetWakeOnLanInfo()
{
- var result = _network.GetMacAddresses()
+ var result = _networkManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i));
return Ok(result);
}
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index b5b6406206..4fbaafa2a0 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -160,9 +160,9 @@ public class TrailersController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? isPlayed,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
diff --git a/Jellyfin.Api/Controllers/TrickplayController.cs b/Jellyfin.Api/Controllers/TrickplayController.cs
new file mode 100644
index 0000000000..2dc960229a
--- /dev/null
+++ b/Jellyfin.Api/Controllers/TrickplayController.cs
@@ -0,0 +1,101 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers;
+
+///
+/// Trickplay controller.
+///
+[Route("")]
+[Authorize]
+public class TrickplayController : BaseJellyfinApiController
+{
+ private readonly ILibraryManager _libraryManager;
+ private readonly ITrickplayManager _trickplayManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of .
+ /// Instance of .
+ public TrickplayController(
+ ILibraryManager libraryManager,
+ ITrickplayManager trickplayManager)
+ {
+ _libraryManager = libraryManager;
+ _trickplayManager = trickplayManager;
+ }
+
+ ///
+ /// Gets an image tiles playlist for trickplay.
+ ///
+ /// The item id.
+ /// The width of a single tile.
+ /// The media version id, if using an alternate version.
+ /// Tiles playlist returned.
+ /// A containing the trickplay playlist file.
+ [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesPlaylistFile]
+ public async Task GetTrickplayHlsPlaylist(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int width,
+ [FromQuery] Guid? mediaSourceId)
+ {
+ string? playlist = await _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()).ConfigureAwait(false);
+
+ if (string.IsNullOrEmpty(playlist))
+ {
+ return NotFound();
+ }
+
+ return Content(playlist, MimeTypes.GetMimeType("playlist.m3u8"), Encoding.UTF8);
+ }
+
+ ///
+ /// Gets a trickplay tile image.
+ ///
+ /// The item id.
+ /// The width of a single tile.
+ /// The index of the desired tile.
+ /// The media version id, if using an alternate version.
+ /// Tile image returned.
+ /// Tile image not found at specified index.
+ /// A containing the trickplay tiles image.
+ [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesImageFile]
+ public ActionResult GetTrickplayTileImage(
+ [FromRoute, Required] Guid itemId,
+ [FromRoute, Required] int width,
+ [FromRoute, Required] int index,
+ [FromQuery] Guid? mediaSourceId)
+ {
+ var item = _libraryManager.GetItemById(mediaSourceId ?? itemId);
+ if (item is null)
+ {
+ return NotFound();
+ }
+
+ var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
+ if (System.IO.File.Exists(path))
+ {
+ return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
+ }
+
+ return NotFound();
+ }
+}
diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs
index bdbbd1e0db..55a30d4692 100644
--- a/Jellyfin.Api/Controllers/TvShowsController.cs
+++ b/Jellyfin.Api/Controllers/TvShowsController.cs
@@ -135,7 +135,7 @@ public class TvShowsController : BaseJellyfinApiController
/// Optional. The max number of images to return, per image type.
/// Optional. The image types to include in the output.
/// Optional. Include user data.
- /// A with the next up episodes.
+ /// A with the upcoming episodes.
[HttpGet("Upcoming")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult> GetUpcomingEpisodes(
@@ -219,7 +219,7 @@ public class TvShowsController : BaseJellyfinApiController
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
- [FromQuery] string? sortBy)
+ [FromQuery] ItemSortBy? sortBy)
{
userId = RequestHelpers.GetUserId(User, userId);
var user = userId.Value.Equals(default)
@@ -289,7 +289,7 @@ public class TvShowsController : BaseJellyfinApiController
episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
}
- if (string.Equals(sortBy, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
+ if (sortBy == ItemSortBy.Random)
{
episodes.Shuffle();
}
diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs
index 838b432340..0ffa3ab1ad 100644
--- a/Jellyfin.Api/Controllers/UserViewsController.cs
+++ b/Jellyfin.Api/Controllers/UserViewsController.cs
@@ -6,6 +6,7 @@ using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -63,7 +64,7 @@ public class UserViewsController : BaseJellyfinApiController
public QueryResult GetUserViews(
[FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] CollectionType?[] presetViews,
[FromQuery] bool includeHidden = false)
{
var query = new UserViewQuery
diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs
index 74370db50b..ca46c38c59 100644
--- a/Jellyfin.Api/Controllers/YearsController.cs
+++ b/Jellyfin.Api/Controllers/YearsController.cs
@@ -76,8 +76,8 @@ public class YearsController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemSortBy[] sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@@ -191,7 +191,7 @@ public class YearsController : BaseJellyfinApiController
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
- private bool FilterItem(BaseItem f, IReadOnlyCollection excludeItemTypes, IReadOnlyCollection includeItemTypes, IReadOnlyCollection mediaTypes)
+ private bool FilterItem(BaseItem f, IReadOnlyCollection excludeItemTypes, IReadOnlyCollection includeItemTypes, IReadOnlyCollection mediaTypes)
{
var baseItemKind = f.GetBaseItemKind();
// Exclude item types
@@ -207,7 +207,7 @@ public class YearsController : BaseJellyfinApiController
}
// Include MediaTypes
- if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+ if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType))
{
return false;
}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index fe602fba39..24082fcff1 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
@@ -19,6 +20,7 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Net;
@@ -46,6 +48,7 @@ public class DynamicHlsHelper
private readonly ILogger _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly EncodingHelper _encodingHelper;
+ private readonly ITrickplayManager _trickplayManager;
///
/// Initializes a new instance of the class.
@@ -62,6 +65,7 @@ public class DynamicHlsHelper
/// Instance of the interface.
/// Instance of the interface.
/// Instance of .
+ /// Instance of .
public DynamicHlsHelper(
ILibraryManager libraryManager,
IUserManager userManager,
@@ -74,7 +78,8 @@ public class DynamicHlsHelper
INetworkManager networkManager,
ILogger logger,
IHttpContextAccessor httpContextAccessor,
- EncodingHelper encodingHelper)
+ EncodingHelper encodingHelper,
+ ITrickplayManager trickplayManager)
{
_libraryManager = libraryManager;
_userManager = userManager;
@@ -88,6 +93,7 @@ public class DynamicHlsHelper
_logger = logger;
_httpContextAccessor = httpContextAccessor;
_encodingHelper = encodingHelper;
+ _trickplayManager = trickplayManager;
}
///
@@ -200,13 +206,6 @@ public class DynamicHlsHelper
if (state.VideoStream is not null && state.VideoRequest is not null)
{
- // Provide a workaround for the case issue between flac and fLaC.
- var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
-
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
// Provide SDR HEVC entrance for backward compatibility.
@@ -236,14 +235,7 @@ public class DynamicHlsHelper
}
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
- var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
-
- // Provide a workaround for the case issue between flac and fLaC.
- flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
+ AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
// Restore the video codec
state.OutputVideoCodec = "copy";
@@ -274,13 +266,6 @@ public class DynamicHlsHelper
state.VideoStream.Level = originalLevel;
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
builder.Append(newPlaylist);
-
- // Provide a workaround for the case issue between flac and fLaC.
- flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
- if (!string.IsNullOrEmpty(flacWaPlaylist))
- {
- builder.Append(flacWaPlaylist);
- }
}
}
@@ -301,6 +286,13 @@ public class DynamicHlsHelper
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
+ if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false))
+ {
+ var sourceId = Guid.Parse(state.Request.MediaSourceId);
+ var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
+ AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
+ }
+
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
}
@@ -529,6 +521,41 @@ public class DynamicHlsHelper
}
}
+ ///
+ /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
+ ///
+ /// StreamState of the current stream.
+ /// Dictionary of widths to corresponding tiles info.
+ /// StringBuilder to append the field to.
+ /// Http user context.
+ private void AddTrickplay(StreamState state, Dictionary trickplayResolutions, StringBuilder builder, ClaimsPrincipal user)
+ {
+ const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\"";
+
+ foreach (var resolution in trickplayResolutions)
+ {
+ var width = resolution.Key;
+ var trickplayInfo = resolution.Value;
+
+ var url = string.Format(
+ CultureInfo.InvariantCulture,
+ "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}",
+ width.ToString(CultureInfo.InvariantCulture),
+ state.Request.MediaSourceId,
+ user.GetToken());
+
+ builder.AppendFormat(
+ CultureInfo.InvariantCulture,
+ playlistFormat,
+ trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
+ trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
+ url);
+
+ builder.AppendLine();
+ }
+ }
+
///
/// Get the H.26X level of the output video stream.
///
@@ -767,16 +794,4 @@ public class DynamicHlsHelper
newValue.ToString(),
StringComparison.Ordinal);
}
-
- private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
- {
- if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
- {
- return string.Empty;
- }
-
- var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
-
- return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
- }
}
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index 9a141a16d9..5eec1b0ca6 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -5,7 +5,9 @@ using System.Text;
namespace Jellyfin.Api.Helpers;
///
-/// Hls Codec string helpers.
+/// Helpers to generate HLS codec strings according to
+/// RFC 6381 section 3.3
+/// and the MP4 Registration Authority.
///
public static class HlsCodecStringHelpers
{
@@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
///
/// Codec name for FLAC.
///
- public const string FLAC = "flac";
+ public const string FLAC = "fLaC";
///
/// Codec name for ALAC.
@@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
///
/// Codec name for OPUS.
///
- public const string OPUS = "opus";
+ public const string OPUS = "Opus";
///
/// Gets a MP3 codec string.
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index a36028cfeb..321987ca74 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
@@ -243,7 +243,7 @@ public class MediaInfoHelper
}
// Beginning of Playback Determination
- var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
+ var streamInfo = item.MediaType == MediaType.Audio
? streamBuilder.GetOptimalAudioStream(options)
: streamBuilder.GetOptimalVideoStream(options);
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index bc12ca3889..be3d4dfb67 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -30,14 +30,14 @@ public static class RequestHelpers
/// Sort By. Comma delimited string.
/// Sort Order. Comma delimited string.
/// Order By.
- public static (string, SortOrder)[] GetOrderBy(IReadOnlyList sortBy, IReadOnlyList requestedSortOrder)
+ public static (ItemSortBy, SortOrder)[] GetOrderBy(IReadOnlyList sortBy, IReadOnlyList requestedSortOrder)
{
if (sortBy.Count == 0)
{
- return Array.Empty<(string, SortOrder)>();
+ return Array.Empty<(ItemSortBy, SortOrder)>();
}
- var result = new (string, SortOrder)[sortBy.Count];
+ var result = new (ItemSortBy, SortOrder)[sortBy.Count];
var i = 0;
// Add elements which have a SortOrder specified
for (; i < requestedSortOrder.Count; i++)
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index e55420d116..6fbbceeabb 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -128,7 +129,7 @@ public static class StreamingHelpers
var item = libraryManager.GetItemById(streamingRequest.Id);
- state.IsInputVideo = string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase);
+ state.IsInputVideo = item.MediaType == MediaType.Video;
MediaSourceInfo? mediaSource = null;
if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
@@ -248,7 +249,7 @@ public static class StreamingHelpers
? GetOutputFileExtension(state, mediaSource)
: ("." + state.OutputContainer);
- state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
+ state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
return state;
}
@@ -421,10 +422,9 @@ public static class StreamingHelpers
/// The state.
/// The mediaSource.
/// System.String.
- private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
+ private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
{
var ext = Path.GetExtension(state.RequestedUrl);
-
if (!string.IsNullOrEmpty(ext))
{
return ext;
@@ -463,10 +463,9 @@ public static class StreamingHelpers
return ".asf";
}
}
-
- // Try to infer based on the desired audio codec
- if (!state.IsVideoRequest)
+ else
{
+ // Try to infer based on the desired audio codec
var audioCodec = state.Request.AudioCodec;
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
@@ -497,7 +496,7 @@ public static class StreamingHelpers
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
}
- return null;
+ throw new InvalidOperationException("Failed to find an appropriate file extension");
}
///
@@ -514,7 +513,7 @@ public static class StreamingHelpers
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- var ext = outputFileExtension?.ToLowerInvariant();
+ var ext = outputFileExtension.ToLowerInvariant();
var folder = serverConfigurationManager.GetTranscodePath();
return Path.Combine(folder, filename + ext);
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 73ebb396d0..c16a586d60 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
}
- if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
+ if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
{
string subtitlePath = state.SubtitleStream.Path;
string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 6a0a4706be..03dd97367f 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -8,8 +8,6 @@
net7.0
true
-
- AD0001
@@ -26,8 +24,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
index 060c14f89d..acbb4877d4 100644
--- a/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/ExceptionMiddleware.cs
@@ -122,17 +122,17 @@ public class ExceptionMiddleware
private static int GetStatusCode(Exception ex)
{
- switch (ex)
+ return ex switch
{
- case ArgumentException _: return StatusCodes.Status400BadRequest;
- case AuthenticationException _: return StatusCodes.Status401Unauthorized;
- case SecurityException _: return StatusCodes.Status403Forbidden;
- case DirectoryNotFoundException _:
- case FileNotFoundException _:
- case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
- case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
- default: return StatusCodes.Status500InternalServerError;
- }
+ ArgumentException => StatusCodes.Status400BadRequest,
+ AuthenticationException => StatusCodes.Status401Unauthorized,
+ SecurityException => StatusCodes.Status403Forbidden,
+ DirectoryNotFoundException => StatusCodes.Status404NotFound,
+ FileNotFoundException => StatusCodes.Status404NotFound,
+ ResourceNotFoundException => StatusCodes.Status404NotFound,
+ MethodNotAllowedException => StatusCodes.Status405MethodNotAllowed,
+ _ => StatusCodes.Status500InternalServerError
+ };
}
private string NormalizeExceptionMessage(string msg)
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
index 5e7dd689e8..6a30de5e6c 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -107,7 +107,7 @@ public class GetProgramsDto
/// Optional.
///
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList SortBy { get; set; } = Array.Empty();
+ public IReadOnlyList SortBy { get; set; } = Array.Empty();
///
/// Gets or sets sort Order - Ascending,Descending.
diff --git a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
index 1fba32c5b8..bdc4888719 100644
--- a/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
+++ b/Jellyfin.Api/Models/PlaylistDtos/CreatePlaylistDto.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
namespace Jellyfin.Api.Models.PlaylistDtos;
@@ -29,5 +30,5 @@ public class CreatePlaylistDto
///
/// Gets or sets the media type.
///
- public string? MediaType { get; set; }
+ public MediaType? MediaType { get; set; }
}
diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
index b88be33b22..b021771a0f 100644
--- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
+++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
+using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Session;
@@ -16,7 +17,7 @@ public class ClientCapabilitiesDto
/// Gets or sets the list of playable media types.
///
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList PlayableMediaTypes { get; set; } = Array.Empty();
+ public IReadOnlyList PlayableMediaTypes { get; set; } = Array.Empty();
///
/// Gets or sets the list of supported commands.
diff --git a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
index 60c529d4ab..8548fec1a1 100644
--- a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
+++ b/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
@@ -1,4 +1,4 @@
-namespace Jellyfin.Api.Models.StreamingDtos;
+namespace Jellyfin.Api.Models.StreamingDtos;
///
/// The video request dto.
@@ -15,4 +15,9 @@ public class VideoRequestDto : StreamingRequestDto
/// Gets or sets a value indicating whether to enable subtitles in the manifest.
///
public bool EnableSubtitlesInManifest { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to enable trickplay images.
+ ///
+ public bool EnableTrickplay { get; set; }
}
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 5b90d65d84..ba228cb002 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
@@ -53,7 +53,10 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener
protected override void Dispose(bool dispose)
{
- _activityManager.EntryCreated -= OnEntryCreated;
+ if (dispose)
+ {
+ _activityManager.EntryCreated -= OnEntryCreated;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index a9df2d6712..37c108d5a6 100644
--- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
@@ -58,8 +58,11 @@ public class ScheduledTasksWebSocketListener : BasePeriodicWebSocketListener
protected override void Dispose(bool dispose)
{
- _taskManager.TaskExecuting -= OnTaskExecuting;
- _taskManager.TaskCompleted -= OnTaskCompleted;
+ if (dispose)
+ {
+ _taskManager.TaskExecuting -= OnTaskExecuting;
+ _taskManager.TaskCompleted -= OnTaskCompleted;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index b403ff46d0..3c2b86142e 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
@@ -57,13 +57,16 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener
protected override void Dispose(bool dispose)
{
- _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
- _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
- _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
- _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
- _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
- _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
- _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+ if (dispose)
+ {
+ _sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
+ _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStart -= OnSessionManagerPlaybackStart;
+ _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+ _sessionManager.PlaybackProgress -= OnSessionManagerPlaybackProgress;
+ _sessionManager.CapabilitiesChanged -= OnSessionManagerCapabilitiesChanged;
+ _sessionManager.SessionActivity -= OnSessionManagerSessionActivity;
+ }
base.Dispose(dispose);
}
diff --git a/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs b/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs
new file mode 100644
index 0000000000..ff613d9f8d
--- /dev/null
+++ b/Jellyfin.Data/Attributes/OpenApiIgnoreEnumAttribute.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Jellyfin.Data.Attributes;
+
+///
+/// Attribute to specify that the enum value is to be ignored when generating the openapi spec.
+///
+[AttributeUsage(AttributeTargets.Field)]
+public sealed class OpenApiIgnoreEnumAttribute : Attribute
+{
+}
diff --git a/Jellyfin.Data/Entities/TrickplayInfo.cs b/Jellyfin.Data/Entities/TrickplayInfo.cs
new file mode 100644
index 0000000000..64e7da1b5d
--- /dev/null
+++ b/Jellyfin.Data/Entities/TrickplayInfo.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Data.Entities;
+
+///
+/// An entity representing the metadata for a group of trickplay tiles.
+///
+public class TrickplayInfo
+{
+ ///
+ /// Gets or sets the id of the associated item.
+ ///
+ ///
+ /// Required.
+ ///
+ [JsonIgnore]
+ public Guid ItemId { get; set; }
+
+ ///
+ /// Gets or sets width of an individual thumbnail.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Width { get; set; }
+
+ ///
+ /// Gets or sets height of an individual thumbnail.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Height { get; set; }
+
+ ///
+ /// Gets or sets amount of thumbnails per row.
+ ///
+ ///
+ /// Required.
+ ///
+ public int TileWidth { get; set; }
+
+ ///
+ /// Gets or sets amount of thumbnails per column.
+ ///
+ ///
+ /// Required.
+ ///
+ public int TileHeight { get; set; }
+
+ ///
+ /// Gets or sets total amount of non-black thumbnails.
+ ///
+ ///
+ /// Required.
+ ///
+ public int ThumbnailCount { get; set; }
+
+ ///
+ /// Gets or sets interval in milliseconds between each trickplay thumbnail.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Interval { get; set; }
+
+ ///
+ /// Gets or sets peak bandwith usage in bits per second.
+ ///
+ ///
+ /// Required.
+ ///
+ public int Bandwidth { get; set; }
+}
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index 58ddaaf83a..ea0de30164 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -288,6 +288,12 @@ namespace Jellyfin.Data.Entities
///
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
+ ///
+ /// Gets or sets the cast receiver id.
+ ///
+ [StringLength(32)]
+ public string? CastReceiverId { get; set; }
+
///
[ConcurrencyCheck]
public uint RowVersion { get; private set; }
@@ -499,6 +505,7 @@ namespace Jellyfin.Data.Entities
Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false));
+ Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false));
}
///
diff --git a/Jellyfin.Data/Enums/CollectionType.cs b/Jellyfin.Data/Enums/CollectionType.cs
new file mode 100644
index 0000000000..e2044a0bc8
--- /dev/null
+++ b/Jellyfin.Data/Enums/CollectionType.cs
@@ -0,0 +1,164 @@
+using Jellyfin.Data.Attributes;
+
+namespace Jellyfin.Data.Enums;
+
+///
+/// Collection type.
+///
+public enum CollectionType
+{
+ ///
+ /// Unknown collection.
+ ///
+ Unknown = 0,
+
+ ///
+ /// Movies collection.
+ ///
+ Movies = 1,
+
+ ///
+ /// Tv shows collection.
+ ///
+ TvShows = 2,
+
+ ///
+ /// Music collection.
+ ///
+ Music = 3,
+
+ ///
+ /// Music videos collection.
+ ///
+ MusicVideos = 4,
+
+ ///
+ /// Trailers collection.
+ ///
+ Trailers = 5,
+
+ ///
+ /// Home videos collection.
+ ///
+ HomeVideos = 6,
+
+ ///
+ /// Box sets collection.
+ ///
+ BoxSets = 7,
+
+ ///
+ /// Books collection.
+ ///
+ Books = 8,
+
+ ///
+ /// Photos collection.
+ ///
+ Photos = 9,
+
+ ///
+ /// Live tv collection.
+ ///
+ LiveTv = 10,
+
+ ///
+ /// Playlists collection.
+ ///
+ Playlists = 11,
+
+ ///
+ /// Folders collection.
+ ///
+ Folders = 12,
+
+ ///
+ /// Tv show series collection.
+ ///
+ [OpenApiIgnoreEnum]
+ TvShowSeries = 101,
+
+ ///
+ /// Tv genres collection.
+ ///
+ [OpenApiIgnoreEnum]
+ TvGenres = 102,
+
+ ///
+ /// Tv genre collection.
+ ///
+ [OpenApiIgnoreEnum]
+ TvGenre = 103,
+
+ ///
+ /// Tv latest collection.
+ ///
+ [OpenApiIgnoreEnum]
+ TvLatest = 104,
+
+ ///
+ /// Tv next up collection.
+ ///
+ [OpenApiIgnoreEnum]
+ TvNextUp = 105,
+
+ ///
+ /// Tv resume collection.
+ ///
+ [OpenApiIgnoreEnum]
+ TvResume = 106,
+
+ ///
+ /// Tv favorite series collection.
+ ///
+ [OpenApiIgnoreEnum]
+ TvFavoriteSeries = 107,
+
+ ///
+ /// Tv favorite episodes collection.
+ ///
+ [OpenApiIgnoreEnum]
+ TvFavoriteEpisodes = 108,
+
+ ///
+ /// Latest movies collection.
+ ///
+ [OpenApiIgnoreEnum]
+ MovieLatest = 109,
+
+ ///
+ /// Movies to resume collection.
+ ///
+ [OpenApiIgnoreEnum]
+ MovieResume = 110,
+
+ ///
+ /// Movie movie collection.
+ ///
+ [OpenApiIgnoreEnum]
+ MovieMovies = 111,
+
+ ///
+ /// Movie collections collection.
+ ///
+ [OpenApiIgnoreEnum]
+ MovieCollections = 112,
+
+ ///
+ /// Movie favorites collection.
+ ///
+ [OpenApiIgnoreEnum]
+ MovieFavorites = 113,
+
+ ///
+ /// Movie genres collection.
+ ///
+ [OpenApiIgnoreEnum]
+ MovieGenres = 114,
+
+ ///
+ /// Movie genre collection.
+ ///
+ [OpenApiIgnoreEnum]
+ MovieGenre = 115
+}
diff --git a/Jellyfin.Data/Enums/ItemSortBy.cs b/Jellyfin.Data/Enums/ItemSortBy.cs
new file mode 100644
index 0000000000..17bf1166de
--- /dev/null
+++ b/Jellyfin.Data/Enums/ItemSortBy.cs
@@ -0,0 +1,167 @@
+namespace Jellyfin.Data.Enums;
+
+///
+/// These represent sort orders.
+///
+public enum ItemSortBy
+{
+ ///
+ /// Default sort order.
+ ///
+ Default = 0,
+
+ ///
+ /// The aired episode order.
+ ///
+ AiredEpisodeOrder = 1,
+
+ ///
+ /// The album.
+ ///
+ Album = 2,
+
+ ///
+ /// The album artist.
+ ///
+ AlbumArtist = 3,
+
+ ///
+ /// The artist.
+ ///
+ Artist = 4,
+
+ ///
+ /// The date created.
+ ///
+ DateCreated = 5,
+
+ ///
+ /// The official rating.
+ ///
+ OfficialRating = 6,
+
+ ///
+ /// The date played.
+ ///
+ DatePlayed = 7,
+
+ ///
+ /// The premiere date.
+ ///
+ PremiereDate = 8,
+
+ ///
+ /// The start date.
+ ///
+ StartDate = 9,
+
+ ///
+ /// The sort name.
+ ///
+ SortName = 10,
+
+ ///
+ /// The name.
+ ///
+ Name = 11,
+
+ ///
+ /// The random.
+ ///
+ Random = 12,
+
+ ///
+ /// The runtime.
+ ///
+ Runtime = 13,
+
+ ///
+ /// The community rating.
+ ///
+ CommunityRating = 14,
+
+ ///
+ /// The production year.
+ ///
+ ProductionYear = 15,
+
+ ///
+ /// The play count.
+ ///
+ PlayCount = 16,
+
+ ///
+ /// The critic rating.
+ ///
+ CriticRating = 17,
+
+ ///
+ /// The IsFolder boolean.
+ ///
+ IsFolder = 18,
+
+ ///
+ /// The IsUnplayed boolean.
+ ///
+ IsUnplayed = 19,
+
+ ///
+ /// The IsPlayed boolean.
+ ///
+ IsPlayed = 20,
+
+ ///
+ /// The series sort.
+ ///
+ SeriesSortName = 21,
+
+ ///
+ /// The video bitrate.
+ ///
+ VideoBitRate = 22,
+
+ ///
+ /// The air time.
+ ///
+ AirTime = 23,
+
+ ///
+ /// The studio.
+ ///
+ Studio = 24,
+
+ ///
+ /// The IsFavouriteOrLiked boolean.
+ ///
+ IsFavoriteOrLiked = 25,
+
+ ///
+ /// The last content added date.
+ ///
+ DateLastContentAdded = 26,
+
+ ///
+ /// The series last played date.
+ ///
+ SeriesDatePlayed = 27,
+
+ ///
+ /// The parent index number.
+ ///
+ ParentIndexNumber = 28,
+
+ ///
+ /// The index number.
+ ///
+ IndexNumber = 29,
+
+ ///
+ /// The similarity score.
+ ///
+ SimilarityScore = 30,
+
+ ///
+ /// The search score.
+ ///
+ SearchScore = 31,
+}
diff --git a/Jellyfin.Data/Enums/MediaType.cs b/Jellyfin.Data/Enums/MediaType.cs
new file mode 100644
index 0000000000..b014ff3664
--- /dev/null
+++ b/Jellyfin.Data/Enums/MediaType.cs
@@ -0,0 +1,32 @@
+namespace Jellyfin.Data.Enums;
+
+///
+/// Media types.
+///
+public enum MediaType
+{
+ ///
+ /// Unknown media type.
+ ///
+ Unknown = 0,
+
+ ///
+ /// Video media.
+ ///
+ Video = 1,
+
+ ///
+ /// Audio media.
+ ///
+ Audio = 2,
+
+ ///
+ /// Photo media.
+ ///
+ Photo = 3,
+
+ ///
+ /// Book media.
+ ///
+ Book = 4
+}
diff --git a/Jellyfin.Data/Enums/PermissionKind.cs b/Jellyfin.Data/Enums/PermissionKind.cs
index 40280b95ef..6644f01515 100644
--- a/Jellyfin.Data/Enums/PermissionKind.cs
+++ b/Jellyfin.Data/Enums/PermissionKind.cs
@@ -113,6 +113,11 @@ namespace Jellyfin.Data.Enums
///
/// Whether the user can create, modify and delete collections.
///
- EnableCollectionManagement = 21
+ EnableCollectionManagement = 21,
+
+ ///
+ /// Whether the user can edit subtitles.
+ ///
+ EnableSubtitleManagement = 22
}
}
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 1bc5d8bf91..847853ca00 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -27,8 +27,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Jellyfin.Networking/Extensions/NetworkExtensions.cs b/Jellyfin.Networking/Extensions/NetworkExtensions.cs
index e45fa3bcb7..a1e1140f18 100644
--- a/Jellyfin.Networking/Extensions/NetworkExtensions.cs
+++ b/Jellyfin.Networking/Extensions/NetworkExtensions.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
@@ -204,7 +203,7 @@ public static partial class NetworkExtensions
{
var ipBlock = splitString.Current;
var address = IPAddress.None;
- if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress))
+ if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress))
{
address = tmpAddress;
}
@@ -231,12 +230,12 @@ public static partial class NetworkExtensions
}
else if (address.AddressFamily == AddressFamily.InterNetwork)
{
- result = new IPNetwork(address, Network.MinimumIPv4PrefixSize);
+ result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize);
return true;
}
else if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
- result = new IPNetwork(address, Network.MinimumIPv6PrefixSize);
+ result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize);
return true;
}
}
@@ -284,12 +283,15 @@ public static partial class NetworkExtensions
if (hosts.Count <= 2)
{
+ var firstPart = hosts[0];
+
// Is hostname or hostname:port
- if (FqdnGeneratedRegex().IsMatch(hosts[0]))
+ if (FqdnGeneratedRegex().IsMatch(firstPart))
{
try
{
- addresses = Dns.GetHostAddresses(hosts[0]);
+ // .NET automatically filters only supported returned addresses based on OS support.
+ addresses = Dns.GetHostAddresses(firstPart);
return true;
}
catch (SocketException)
@@ -299,7 +301,7 @@ public static partial class NetworkExtensions
}
// Is an IPv4 or IPv4:port
- if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address))
+ if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address))
{
if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
|| ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))
diff --git a/Jellyfin.Networking/Jellyfin.Networking.csproj b/Jellyfin.Networking/Jellyfin.Networking.csproj
index 4cff5927fd..43d08c37a1 100644
--- a/Jellyfin.Networking/Jellyfin.Networking.csproj
+++ b/Jellyfin.Networking/Jellyfin.Networking.csproj
@@ -9,8 +9,12 @@
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Jellyfin.Networking/Manager/NetworkManager.cs b/Jellyfin.Networking/Manager/NetworkManager.cs
index f20e285263..9c59500d77 100644
--- a/Jellyfin.Networking/Manager/NetworkManager.cs
+++ b/Jellyfin.Networking/Manager/NetworkManager.cs
@@ -15,7 +15,9 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Jellyfin.Networking.Manager
{
@@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager
private readonly IConfigurationManager _configurationManager;
+ private readonly IConfiguration _startupConfig;
+
private readonly object _networkEventLock;
///
/// Holds the published server URLs and the IPs to use them on.
///
- private IReadOnlyDictionary _publishedServerUrls;
+ private IReadOnlyList _publishedServerUrls;
private IReadOnlyList _remoteAddressFilter;
@@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager
///
/// Initializes a new instance of the class.
///
- /// IServerConfigurationManager instance.
+ /// The instance.
+ /// The instance holding startup parameters.
/// Logger to use for messages.
#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
- public NetworkManager(IConfigurationManager configurationManager, ILogger logger)
+ public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger logger)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(configurationManager);
_logger = logger;
_configurationManager = configurationManager;
+ _startupConfig = startupConfig;
_initLock = new();
_interfaces = new List();
_macAddresses = new List();
- _publishedServerUrls = new Dictionary();
+ _publishedServerUrls = new List();
_networkEventLock = new object();
_remoteAddressFilter = new List();
@@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager
///
/// Gets the Published server override list.
///
- public IReadOnlyDictionary PublishedServerUrls => _publishedServerUrls;
+ public IReadOnlyList PublishedServerUrls => _publishedServerUrls;
///
public void Dispose()
@@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager
{
if (!_eventfire)
{
- _logger.LogDebug("Network Address Change Event.");
// As network events tend to fire one after the other only fire once every second.
_eventfire = true;
OnNetworkChange();
@@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager
}
else
{
- InitialiseInterfaces();
- InitialiseLan(networkConfig);
+ InitializeInterfaces();
+ InitializeLan(networkConfig);
EnforceBindSettings(networkConfig);
}
+ PrintNetworkInformation(networkConfig);
NetworkChanged?.Invoke(this, EventArgs.Empty);
}
finally
@@ -210,7 +216,7 @@ namespace Jellyfin.Networking.Manager
/// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
/// Generate a list of all active mac addresses that aren't loopback addresses.
///
- private void InitialiseInterfaces()
+ private void InitializeInterfaces()
{
lock (_initLock)
{
@@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager
try
{
var nics = NetworkInterface.GetAllNetworkInterfaces()
- .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up);
+ .Where(i => i.OperationalStatus == OperationalStatus.Up);
foreach (NetworkInterface adapter in nics)
{
@@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager
{
if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
{
- var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
- interfaceObject.Index = ipProperties.GetIPv4Properties().Index;
- interfaceObject.Name = adapter.Name;
+ var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+ {
+ Index = ipProperties.GetIPv4Properties().Index,
+ Name = adapter.Name,
+ SupportsMulticast = adapter.SupportsMulticast
+ };
interfaces.Add(interfaceObject);
}
else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
{
- var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
- interfaceObject.Index = ipProperties.GetIPv6Properties().Index;
- interfaceObject.Name = adapter.Name;
+ var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+ {
+ Index = ipProperties.GetIPv6Properties().Index,
+ Name = adapter.Name,
+ SupportsMulticast = adapter.SupportsMulticast
+ };
interfaces.Add(interfaceObject);
}
}
}
-#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
-#pragma warning restore CA1031 // Do not catch general exception types
{
// Ignore error, and attempt to continue.
_logger.LogError(ex, "Error encountered parsing interfaces.");
}
}
}
-#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
-#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Error obtaining interfaces.");
}
@@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager
{
_logger.LogWarning("No interface information available. Using loopback interface(s).");
- if (IsIPv4Enabled && !IsIPv6Enabled)
+ if (IsIPv4Enabled)
{
- interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo"));
+ interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
}
- if (!IsIPv4Enabled && IsIPv6Enabled)
+ if (IsIPv6Enabled)
{
- interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo"));
+ interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
}
}
@@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager
}
///
- /// Initialises internal LAN cache.
+ /// Initializes internal LAN cache.
///
- private void InitialiseLan(NetworkConfiguration config)
+ private void InitializeLan(NetworkConfiguration config)
{
lock (_initLock)
{
@@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager
_excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
? excludedSubnets
: new List();
-
- _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
- _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
- _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
}
}
@@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager
.ToHashSet();
interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
- if (bindAddresses.Contains(IPAddress.Loopback))
+ if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
{
interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
}
- if (bindAddresses.Contains(IPAddress.IPv6Loopback))
+ if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
{
interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
}
@@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager
interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
}
- _logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
_interfaces = interfaces;
}
}
///
- /// Initialises the remote address values.
+ /// Initializes the remote address values.
///
- private void InitialiseRemote(NetworkConfiguration config)
+ private void InitializeRemote(NetworkConfiguration config)
{
lock (_initLock)
{
@@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager
/// format is subnet=ipaddress|host|uri
/// when subnet = 0.0.0.0, any external address matches.
///
- private void InitialiseOverrides(NetworkConfiguration config)
+ private void InitializeOverrides(NetworkConfiguration config)
{
lock (_initLock)
{
- var publishedServerUrls = new Dictionary();
- var overrides = config.PublishedServerUriBySubnet;
+ var publishedServerUrls = new List();
+
+ // Prefer startup configuration.
+ var startupOverrideKey = _startupConfig[AddressOverrideKey];
+ if (!string.IsNullOrEmpty(startupOverrideKey))
+ {
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ startupOverrideKey,
+ true,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ startupOverrideKey,
+ true,
+ true));
+ _publishedServerUrls = publishedServerUrls;
+ return;
+ }
+ var overrides = config.PublishedServerUriBySubnet;
foreach (var entry in overrides)
{
var parts = entry.Split('=');
@@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager
var identifier = parts[0];
if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
{
- publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement;
+ // Drop any other overrides in case an "all" override exists
+ publishedServerUrls.Clear();
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ replacement,
+ true,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ replacement,
+ true,
+ true));
+ break;
}
else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
{
- publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement;
- publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.Any, Network.IPv4Any),
+ replacement,
+ false,
+ true));
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+ replacement,
+ false,
+ true));
}
else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
{
foreach (var lan in _lanSubnets)
{
var lanPrefix = lan.Prefix;
- publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
+ replacement,
+ true,
+ false));
}
}
else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
{
var data = new IPData(result.Prefix, result);
- publishedServerUrls[data] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ data,
+ replacement,
+ true,
+ true));
}
else if (TryParseInterface(identifier, out var ifaces))
{
foreach (var iface in ifaces)
{
- publishedServerUrls[iface] = replacement;
+ publishedServerUrls.Add(
+ new PublishedServerUriOverride(
+ iface,
+ replacement,
+ true,
+ true));
}
}
else
@@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager
}
///
- /// Reloads all settings and re-initialises the instance.
+ /// Reloads all settings and re-Initializes the instance.
///
/// The to use.
public void UpdateSettings(object configuration)
@@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager
var config = (NetworkConfiguration)configuration;
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
- InitialiseLan(config);
- InitialiseRemote(config);
+ InitializeLan(config);
+ InitializeRemote(config);
if (string.IsNullOrEmpty(MockNetworkSettings))
{
- InitialiseInterfaces();
+ InitializeInterfaces();
}
else // Used in testing only.
{
@@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager
var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
{
- var data = new IPData(address, subnet, parts[2]);
- data.Index = index;
+ var data = new IPData(address, subnet, parts[2])
+ {
+ Index = index
+ };
interfaces.Add(data);
}
}
@@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager
}
EnforceBindSettings(config);
- InitialiseOverrides(config);
+ InitializeOverrides(config);
+
+ PrintNetworkInformation(config, false);
}
///
@@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager
///
public IReadOnlyList GetAllBindInterfaces(bool individualInterfaces = false)
{
- if (_interfaces.Count != 0)
+ if (_interfaces.Count > 0 || individualInterfaces)
{
return _interfaces;
}
// No bind address and no exclusions, so listen on all interfaces.
var result = new List();
-
- if (individualInterfaces)
- {
- result.AddRange(_interfaces);
- return result;
- }
-
if (IsIPv4Enabled && IsIPv6Enabled)
{
// Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
@@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager
bindPreference = string.Empty;
int? port = null;
- var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any)
- || x.Key.Address.Equals(IPAddress.IPv6Any)
- || x.Key.Subnet.Contains(source))
- .DistinctBy(x => x.Key)
- .OrderBy(x => x.Key.Address.Equals(IPAddress.Any)
- || x.Key.Address.Equals(IPAddress.IPv6Any))
+ // Only consider subnets including the source IP, prefering specific overrides
+ List validPublishedServerUrls;
+ if (!isInExternalSubnet)
+ {
+ // Only use matching internal subnets
+ // Prefer more specific (bigger subnet prefix) overrides
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
+ .OrderByDescending(x => x.Data.Subnet.PrefixLength)
+ .ToList();
+ }
+ else
+ {
+ // Only use matching external subnets
+ // Prefer more specific (bigger subnet prefix) overrides
+ validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
+ .OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList();
+ }
- // Check for user override.
foreach (var data in validPublishedServerUrls)
{
- if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any)))
- {
- // External.
- bindPreference = data.Value;
- break;
- }
-
- // Get address interface.
- var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address));
+ // Get interface matching override subnet
+ var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
if (intf?.Address is not null)
{
- // Match IP address.
- bindPreference = data.Value;
+ // If matching interface is found, use override
+ bindPreference = data.OverrideUri;
break;
}
}
@@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager
return false;
}
- // Has it got a port defined?
+ // Handle override specifying port
var parts = bindPreference.Split(':');
if (parts.Length > 1)
{
@@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager
{
bindPreference = parts[0];
port = p;
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
+ return true;
}
}
- if (port is not null)
- {
- _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
- }
- else
- {
- _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
- }
-
+ _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
return true;
}
@@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager
_logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
return true;
}
+
+ private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
+ {
+ var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
+ if (_logger.IsEnabled(logLevel))
+ {
+ _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
+ _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
+ _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
+ _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
+ }
+ }
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
index 2ee5b4e88d..3f3a0dec5e 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
@@ -1,7 +1,6 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
-using Jellyfin.Data.Events;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Authentication;
using MediaBrowser.Model.Activity;
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
index 27726a57a6..8a33383e3a 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStartLogger.cs
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
@@ -89,14 +90,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
return name;
}
- private static string GetPlaybackNotificationType(string mediaType)
+ private static string GetPlaybackNotificationType(MediaType mediaType)
{
- if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Audio)
{
return NotificationType.AudioPlayback.ToString();
}
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Video)
{
return NotificationType.VideoPlayback.ToString();
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
index 6b16477aa7..4c2effc2e3 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
+++ b/Jellyfin.Server.Implementations/Events/Consumers/Session/PlaybackStopLogger.cs
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
@@ -97,14 +98,14 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
return name;
}
- private static string? GetPlaybackStoppedNotificationType(string mediaType)
+ private static string? GetPlaybackStoppedNotificationType(MediaType mediaType)
{
- if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Audio)
{
return NotificationType.AudioPlaybackStopped.ToString();
}
- if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
+ if (mediaType == MediaType.Video)
{
return NotificationType.VideoPlaybackStopped.ToString();
}
diff --git a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
index 9a473de52d..9626817e90 100644
--- a/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
@@ -1,5 +1,4 @@
-using Jellyfin.Data.Events;
-using Jellyfin.Data.Events.System;
+using Jellyfin.Data.Events.System;
using Jellyfin.Data.Events.Users;
using Jellyfin.Server.Implementations.Events.Consumers.Library;
using Jellyfin.Server.Implementations.Events.Consumers.Security;
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 390ed58b3b..df1d5a3e18 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -6,8 +6,12 @@
true
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
all
runtime; build; native; contentfiles; analyzers
diff --git a/Jellyfin.Server.Implementations/JellyfinDbContext.cs b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
index 0d91707e3e..ea99af0047 100644
--- a/Jellyfin.Server.Implementations/JellyfinDbContext.cs
+++ b/Jellyfin.Server.Implementations/JellyfinDbContext.cs
@@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext
///
public DbSet Users => Set();
+ ///
+ /// Gets the containing the trickplay metadata.
+ ///
+ public DbSet TrickplayInfos => Set();
+
/*public DbSet Artwork => Set();
public DbSet Books => Set();
diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
new file mode 100644
index 0000000000..28baf19925
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
@@ -0,0 +1,681 @@
+//
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20230626233818_AddTrickplayInfos")]
+ partial class AddTrickplayInfos
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+ {
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Width")
+ .HasColumnType("INTEGER");
+
+ b.Property("Bandwidth")
+ .HasColumnType("INTEGER");
+
+ b.Property("Height")
+ .HasColumnType("INTEGER");
+
+ b.Property("Interval")
+ .HasColumnType("INTEGER");
+
+ b.Property("ThumbnailCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileHeight")
+ .HasColumnType("INTEGER");
+
+ b.Property("TileWidth")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("ItemId", "Width");
+
+ b.ToTable("TrickplayInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SubtitleLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("Username")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT")
+ .UseCollation("NOCASE");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Username")
+ .IsUnique();
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("AccessSchedules")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("DisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+ .WithMany("HomeSections")
+ .HasForeignKey("DisplayPreferencesId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithOne("ProfileImage")
+ .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("ItemDisplayPreferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Permissions")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", null)
+ .WithMany("Preferences")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.HasOne("Jellyfin.Data.Entities.User", "User")
+ .WithMany()
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Navigation("HomeSections");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Navigation("AccessSchedules");
+
+ b.Navigation("DisplayPreferences");
+
+ b.Navigation("ItemDisplayPreferences");
+
+ b.Navigation("Permissions");
+
+ b.Navigation("Preferences");
+
+ b.Navigation("ProfileImage");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
new file mode 100644
index 0000000000..76b12de083
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
@@ -0,0 +1,40 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ ///
+ public partial class AddTrickplayInfos : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "TrickplayInfos",
+ columns: table => new
+ {
+ ItemId = table.Column(type: "TEXT", nullable: false),
+ Width = table.Column(type: "INTEGER", nullable: false),
+ Height = table.Column(type: "INTEGER", nullable: false),
+ TileWidth = table.Column(type: "INTEGER", nullable: false),
+ TileHeight = table.Column(type: "INTEGER", nullable: false),
+ ThumbnailCount = table.Column(type: "INTEGER", nullable: false),
+ Interval = table.Column(type: "INTEGER", nullable: false),
+ Bandwidth = table.Column(type: "INTEGER", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width });
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "TrickplayInfos");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
new file mode 100644
index 0000000000..2884d4256c
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
@@ -0,0 +1,654 @@
+//
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+ [DbContext(typeof(JellyfinDbContext))]
+ [Migration("20230923170422_UserCastReceiver")]
+ partial class UserCastReceiver
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Overview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ShortOverview")
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DateCreated");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("Key")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client", "Key")
+ .IsUnique();
+
+ b.ToTable("CustomItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DashboardTheme")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("TvHome")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "ItemId", "Client")
+ .IsUnique();
+
+ b.ToTable("DisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("TEXT");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortBy")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId", "Kind")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("Preferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AccessToken")
+ .IsUnique();
+
+ b.ToTable("ApiKeys");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("AccessToken")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.Property("AppName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("AppVersion")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("DateLastActivity")
+ .HasColumnType("TEXT");
+
+ b.Property("DateModified")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceName")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("TEXT");
+
+ b.Property("IsActive")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId");
+
+ b.HasIndex("AccessToken", "DateLastActivity");
+
+ b.HasIndex("DeviceId", "DateLastActivity");
+
+ b.HasIndex("UserId", "DeviceId");
+
+ b.ToTable("Devices");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("CustomName")
+ .HasColumnType("TEXT");
+
+ b.Property("DeviceId")
+ .IsRequired()
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DeviceId")
+ .IsUnique();
+
+ b.ToTable("DeviceOptions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AudioLanguagePreference")
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("AuthenticationProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("CastReceiverId")
+ .HasMaxLength(32)
+ .HasColumnType("TEXT");
+
+ b.Property("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("Password")
+ .HasMaxLength(65535)
+ .HasColumnType("TEXT");
+
+ b.Property("PasswordResetProviderId")
+ .IsRequired()
+ .HasMaxLength(255)
+ .HasColumnType("TEXT");
+
+ b.Property("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property