Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 730b01fb14 | |||
| 45e8872cc0 | |||
| bcf884ccfa | |||
| 2eece01acc | |||
| ef985896e2 | |||
| 5e7514243c | |||
| 4a344bebc0 | |||
| b2d54b82fa | |||
| e7b1162cb3 | |||
| d89e5a0074 | |||
| 4a54e5ddeb | |||
| d9232e05f1 | |||
| 52be8be28f | |||
| ab6c2424db | |||
| eb437e7163 | |||
| 2ddf2a7866 | |||
| 60232ce9be | |||
| 952995f796 | |||
| 933a285bf5 | |||
| f8da69f8e5 | |||
| d5e29bfce3 | |||
| ab36c4c011 | |||
| c6e29647fc | |||
| fa2bff30f6 | |||
| 447f73caf4 | |||
| 86129589ef | |||
| 06a5ddda5e | |||
| 6777f47e0e | |||
| cf04e1d8e5 | |||
| d608f1e3cc | |||
| 86f5c93434 | |||
| 4fcbeef5e6 | |||
| 77abafca8e | |||
| 2cebd5e05f | |||
| b19b346670 | |||
| 53de8c0805 |
@@ -20,18 +20,18 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '8.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -14,7 +16,7 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@@ -39,7 +41,7 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@@ -99,11 +101,24 @@ jobs:
|
|||||||
- id: read-diff
|
- id: read-diff
|
||||||
name: Read openapi-diff output
|
name: Read openapi-diff output
|
||||||
run: |
|
run: |
|
||||||
|
# Read and fix markdown
|
||||||
body=$(cat openapi-changes.md)
|
body=$(cat openapi-changes.md)
|
||||||
body="${body//'%'/'%25'}"
|
# Write to workflow summary
|
||||||
body="${body//$'\n'/'%0A'}"
|
echo "$body" >> $GITHUB_STEP_SUMMARY
|
||||||
body="${body//$'\r'/'%0D'}"
|
# Set ApiChanged var
|
||||||
echo ::set-output name=body::$body
|
if [ "$body" != '' ]; then
|
||||||
|
echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
# Add header/footer for diff comment
|
||||||
|
echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md
|
||||||
|
echo "<details>" >> openapi-changes-reply.md
|
||||||
|
echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md
|
||||||
|
echo "" >> openapi-changes-reply.md
|
||||||
|
echo "$body" >> openapi-changes-reply.md
|
||||||
|
echo "" >> openapi-changes-reply.md
|
||||||
|
echo "</details>" >> openapi-changes-reply.md
|
||||||
- name: Find difference comment
|
- name: Find difference comment
|
||||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||||
id: find-comment
|
id: find-comment
|
||||||
@@ -113,22 +128,15 @@ jobs:
|
|||||||
body-includes: openapi-diff-workflow-comment
|
body-includes: openapi-diff-workflow-comment
|
||||||
- name: Reply or edit difference comment (changed)
|
- name: Reply or edit difference comment (changed)
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||||
if: ${{ steps.read-diff.outputs.body != '' }}
|
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||||
edit-mode: replace
|
edit-mode: replace
|
||||||
body: |
|
body-path: openapi-changes-reply.md
|
||||||
<!--openapi-diff-workflow-comment-->
|
|
||||||
<details>
|
|
||||||
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
|
|
||||||
|
|
||||||
${{ steps.read-diff.outputs.body }}
|
|
||||||
|
|
||||||
</details>
|
|
||||||
- name: Edit difference comment (unchanged)
|
- name: Edit difference comment (unchanged)
|
||||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||||
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||||
@@ -138,11 +146,9 @@ jobs:
|
|||||||
|
|
||||||
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
No changes to OpenAPI specification found. See history of this comment for previous changes.
|
||||||
|
|
||||||
publish:
|
publish-unstable:
|
||||||
name: OpenAPI - Publish Unstable Spec
|
name: OpenAPI - Publish Unstable Spec
|
||||||
if: |
|
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||||
github.event_name != 'pull_request_target' &&
|
|
||||||
contains(github.repository_owner, 'jellyfin')
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- openapi-head
|
- openapi-head
|
||||||
@@ -201,3 +207,65 @@ jobs:
|
|||||||
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||||
fi
|
fi
|
||||||
) 200>/run/workflows/openapi-unstable.lock
|
) 200>/run/workflows/openapi-unstable.lock
|
||||||
|
|
||||||
|
publish-stable:
|
||||||
|
name: OpenAPI - Publish Stable Spec
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- openapi-head
|
||||||
|
steps:
|
||||||
|
- name: Set version number
|
||||||
|
id: version
|
||||||
|
run: |-
|
||||||
|
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
- name: Download openapi-head
|
||||||
|
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||||
|
with:
|
||||||
|
name: openapi-head
|
||||||
|
path: openapi-head
|
||||||
|
- name: Upload openapi.json (stable) to repository server
|
||||||
|
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
|
||||||
|
with:
|
||||||
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
|
username: "${{ secrets.REPO_USER }}"
|
||||||
|
key: "${{ secrets.REPO_KEY }}"
|
||||||
|
source: openapi-head/openapi.json
|
||||||
|
strip_components: 1
|
||||||
|
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
|
||||||
|
- name: Move openapi.json (stable) into place
|
||||||
|
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
|
||||||
|
with:
|
||||||
|
host: "${{ secrets.REPO_HOST }}"
|
||||||
|
username: "${{ secrets.REPO_USER }}"
|
||||||
|
key: "${{ secrets.REPO_KEY }}"
|
||||||
|
debug: false
|
||||||
|
script_stop: false
|
||||||
|
script: |
|
||||||
|
if ! test -d /run/workflows; then
|
||||||
|
sudo mkdir -p /run/workflows
|
||||||
|
sudo chown ${{ secrets.REPO_USER }} /run/workflows
|
||||||
|
fi
|
||||||
|
(
|
||||||
|
flock -x -w 300 200 || exit 1
|
||||||
|
TGT_DIR="/srv/repository/main/openapi"
|
||||||
|
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
|
||||||
|
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
|
||||||
|
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
|
||||||
|
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Move new spec into place
|
||||||
|
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
|
||||||
|
# Delete previous jellyfin-openapi-stable_previous.json
|
||||||
|
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
|
||||||
|
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
# Create new jellyfin-openapi-stable.json symlink
|
||||||
|
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
|
||||||
|
# Check that the previous openapi stable spec link is correct
|
||||||
|
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
|
||||||
|
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
|
||||||
|
fi
|
||||||
|
) 200>/run/workflows/openapi-stable.lock
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
runs-on: "${{ matrix.os }}"
|
runs-on: "${{ matrix.os }}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
|
|
||||||
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||||
with:
|
with:
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
--verbosity minimal
|
--verbosity minimal
|
||||||
|
|
||||||
- name: Merge code coverage results
|
- name: Merge code coverage results
|
||||||
uses: danielpalme/ReportGenerator-GitHub-Action@2a2d60ea1c7e811f54684179af6ac1ae8c1ce69a # 5.2.5
|
uses: danielpalme/ReportGenerator-GitHub-Action@6b06171d1a131e7fd85121120a1c00c1ed03e033 # 5.3.0
|
||||||
with:
|
with:
|
||||||
reports: "**/coverage.cobertura.xml"
|
reports: "**/coverage.cobertura.xml"
|
||||||
targetdir: "merged/"
|
targetdir: "merged/"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
reactions: '+1'
|
reactions: '+1'
|
||||||
|
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
reactions: eyes
|
reactions: eyes
|
||||||
|
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -128,7 +128,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: pull in script
|
- name: pull in script
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
repository: jellyfin/jellyfin-triage-script
|
repository: jellyfin/jellyfin-triage-script
|
||||||
- name: install python
|
- name: install python
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: pull in script
|
- name: pull in script
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
repository: jellyfin/jellyfin-triage-script
|
repository: jellyfin/jellyfin-triage-script
|
||||||
- name: install python
|
- name: install python
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Apply label
|
- name: Apply label
|
||||||
uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0
|
uses: eps1lon/actions-label-merge-conflict@6d74047dcef155976a15e4a124dde2c7fe0c5522 # v3.0.1
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||||
with:
|
with:
|
||||||
dirtyLabel: 'merge conflict'
|
dirtyLabel: 'merge conflict'
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
yq-version: v4.9.8
|
yq-version: v4.9.8
|
||||||
|
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.TAG_BRANCH }}
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||||
with:
|
with:
|
||||||
ref: ${{ env.TAG_BRANCH }}
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,7 @@
|
|||||||
- [btopherjohnson](https://github.com/btopherjohnson)
|
- [btopherjohnson](https://github.com/btopherjohnson)
|
||||||
- [GeorgeH005](https://github.com/GeorgeH005)
|
- [GeorgeH005](https://github.com/GeorgeH005)
|
||||||
- [Vedant](https://github.com/viktory36/)
|
- [Vedant](https://github.com/viktory36/)
|
||||||
|
- [NotSaifA](https://github.com/NotSaifA)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.9.2</VersionPrefix>
|
<VersionPrefix>10.9.3</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace Emby.Naming.TV;
|
|||||||
public static class TvParserHelpers
|
public static class TvParserHelpers
|
||||||
{
|
{
|
||||||
private static readonly string[] _continuingState = ["Pilot", "Returning Series", "Returning"];
|
private static readonly string[] _continuingState = ["Pilot", "Returning Series", "Returning"];
|
||||||
private static readonly string[] _endedState = ["Cancelled"];
|
private static readonly string[] _endedState = ["Cancelled", "Canceled"];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to parse a string into <see cref="SeriesStatus"/>.
|
/// Tries to parse a string into <see cref="SeriesStatus"/>.
|
||||||
|
|||||||
@@ -385,6 +385,19 @@ public class MediaInfoHelper
|
|||||||
/// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
|
/// <returns>A <see cref="Task"/> containing the <see cref="LiveStreamResponse"/>.</returns>
|
||||||
public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
|
public async Task<LiveStreamResponse> OpenMediaSource(HttpContext httpContext, LiveStreamRequest request)
|
||||||
{
|
{
|
||||||
|
// Enforce more restrictive transcoding profile for LiveTV due to compatability reasons
|
||||||
|
// Cap the MaxStreamingBitrate to 20Mbps, because we are unable to reliably probe source bitrate,
|
||||||
|
// which will cause the client to request extremely high bitrate that may fail the player/encoder
|
||||||
|
request.MaxStreamingBitrate = request.MaxStreamingBitrate > 20000000 ? 20000000 : request.MaxStreamingBitrate;
|
||||||
|
|
||||||
|
if (request.DeviceProfile is not null)
|
||||||
|
{
|
||||||
|
// Remove all fmp4 transcoding profiles, because it causes playback error and/or A/V sync issues
|
||||||
|
// Notably: Some channels won't play on FireFox and LG webOs
|
||||||
|
// Some channels from HDHomerun will experience A/V sync issues
|
||||||
|
request.DeviceProfile.TranscodingProfiles = request.DeviceProfile.TranscodingProfiles.Where(p => !string.Equals(p.Container, "mp4", StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
|
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
var profile = request.DeviceProfile;
|
var profile = request.DeviceProfile;
|
||||||
|
|||||||
@@ -120,7 +120,12 @@ public static class RequestHelpers
|
|||||||
internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
|
internal static async Task<SessionInfo> GetSession(ISessionManager sessionManager, IUserManager userManager, HttpContext httpContext, Guid? userId = null)
|
||||||
{
|
{
|
||||||
userId ??= httpContext.User.GetUserId();
|
userId ??= httpContext.User.GetUserId();
|
||||||
var user = userManager.GetUserById(userId.Value);
|
User? user = null;
|
||||||
|
if (!userId.IsNullOrEmpty())
|
||||||
|
{
|
||||||
|
user = userManager.GetUserById(userId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
var session = await sessionManager.LogSessionActivity(
|
var session = await sessionManager.LogSessionActivity(
|
||||||
httpContext.User.GetClient(),
|
httpContext.User.GetClient(),
|
||||||
httpContext.User.GetVersion(),
|
httpContext.User.GetVersion(),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Data</PackageId>
|
<PackageId>Jellyfin.Data</PackageId>
|
||||||
<VersionPrefix>10.9.2</VersionPrefix>
|
<VersionPrefix>10.9.3</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
|
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
|
||||||
|
|
||||||
var options = _config.Configuration.TrickplayOptions;
|
var options = _config.Configuration.TrickplayOptions;
|
||||||
|
if (options.Interval < 1000)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Trickplay image interval {Interval} is too small, reset to the minimum valid value of 1000", options.Interval);
|
||||||
|
options.Interval = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var width in options.WidthResolutions)
|
foreach (var width in options.WidthResolutions)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
@@ -267,7 +273,7 @@ public class TrickplayManager : ITrickplayManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update bitrate
|
// Update bitrate
|
||||||
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
|
var bitrate = (int)Math.Ceiling(new FileInfo(tilePath).Length * 8m / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000m));
|
||||||
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
|
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Common</PackageId>
|
<PackageId>Jellyfin.Common</PackageId>
|
||||||
<VersionPrefix>10.9.2</VersionPrefix>
|
<VersionPrefix>10.9.3</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ public static class NetworkConstants
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16);
|
public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IPv4 Link-Local as defined in RFC 3927.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly IPNetwork IPv4RFC3927LinkLocal = new IPNetwork(IPAddress.Parse("169.254.0.0"), 16);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// IPv6 loopback as defined in RFC 4291.
|
/// IPv6 loopback as defined in RFC 4291.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Controller</PackageId>
|
<PackageId>Jellyfin.Controller</PackageId>
|
||||||
<VersionPrefix>10.9.2</VersionPrefix>
|
<VersionPrefix>10.9.3</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
private readonly Version _minKerneli915Hang = new Version(5, 18);
|
private readonly Version _minKerneli915Hang = new Version(5, 18);
|
||||||
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
|
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
|
||||||
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
|
private readonly Version _minFixedKernel60i915Hang = new Version(6, 0, 18);
|
||||||
|
private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
|
||||||
|
|
||||||
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
|
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
|
||||||
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
||||||
@@ -680,16 +681,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetInputPathArgument(EncodingJobInfo state)
|
|
||||||
{
|
|
||||||
return state.MediaSource.VideoType switch
|
|
||||||
{
|
|
||||||
VideoType.Dvd => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistVobFiles(state.MediaPath, null).ToList(), state.MediaSource),
|
|
||||||
VideoType.BluRay => _mediaEncoder.GetInputArgument(_mediaEncoder.GetPrimaryPlaylistM2tsFiles(state.MediaPath).ToList(), state.MediaSource),
|
|
||||||
_ => _mediaEncoder.GetInputArgument(state.MediaPath, state.MediaSource)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the audio encoder.
|
/// Gets the audio encoder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1005,7 +996,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc");
|
Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc");
|
||||||
|
|
||||||
if (IsVulkanFullSupported()
|
if (IsVulkanFullSupported()
|
||||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
|
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
|
||||||
|
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
|
||||||
{
|
{
|
||||||
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
|
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
|
||||||
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
|
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
|
||||||
@@ -1203,7 +1195,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
arg.Append(" -i ")
|
arg.Append(" -i ")
|
||||||
.Append(GetInputPathArgument(state));
|
.Append(_mediaEncoder.GetInputPathArgument(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
// sub2video for external graphical subtitles
|
// sub2video for external graphical subtitles
|
||||||
@@ -2083,6 +2075,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
profile = "constrained_high";
|
profile = "constrained_high";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& profile.Contains("constrainedbaseline", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
profile = "constrained_baseline";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(videoEncoder, "h264_videotoolbox", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& profile.Contains("constrainedhigh", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
profile = "constrained_high";
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profile))
|
if (!string.IsNullOrEmpty(profile))
|
||||||
{
|
{
|
||||||
// Currently there's no profile option in av1_nvenc encoder
|
// Currently there's no profile option in av1_nvenc encoder
|
||||||
@@ -2629,10 +2633,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
&& state.AudioStream.Channels.HasValue
|
&& state.AudioStream.Channels.HasValue
|
||||||
&& state.AudioStream.Channels.Value == 6)
|
&& state.AudioStream.Channels.Value == 6)
|
||||||
{
|
{
|
||||||
|
if (!encodingOptions.DownMixAudioBoost.Equals(1))
|
||||||
|
{
|
||||||
|
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
switch (encodingOptions.DownMixStereoAlgorithm)
|
switch (encodingOptions.DownMixStereoAlgorithm)
|
||||||
{
|
{
|
||||||
case DownMixStereoAlgorithms.Dave750:
|
case DownMixStereoAlgorithms.Dave750:
|
||||||
filters.Add("volume=4.25");
|
|
||||||
filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3");
|
filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3");
|
||||||
break;
|
break;
|
||||||
case DownMixStereoAlgorithms.NightmodeDialogue:
|
case DownMixStereoAlgorithms.NightmodeDialogue:
|
||||||
@@ -2640,11 +2648,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
break;
|
break;
|
||||||
case DownMixStereoAlgorithms.None:
|
case DownMixStereoAlgorithms.None:
|
||||||
default:
|
default:
|
||||||
if (!encodingOptions.DownMixAudioBoost.Equals(1))
|
|
||||||
{
|
|
||||||
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3161,7 +3164,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
int? requestedMaxHeight)
|
int? requestedMaxHeight)
|
||||||
{
|
{
|
||||||
var isV4l2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase);
|
var isV4l2 = string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isMjpeg = videoEncoder is not null && videoEncoder.Contains("mjpeg", StringComparison.OrdinalIgnoreCase);
|
||||||
var scaleVal = isV4l2 ? 64 : 2;
|
var scaleVal = isV4l2 ? 64 : 2;
|
||||||
|
var targetAr = isMjpeg ? "(a*sar)" : "a"; // manually calculate AR when using mjpeg encoder
|
||||||
|
|
||||||
// If fixed dimensions were supplied
|
// If fixed dimensions were supplied
|
||||||
if (requestedWidth.HasValue && requestedHeight.HasValue)
|
if (requestedWidth.HasValue && requestedHeight.HasValue)
|
||||||
@@ -3190,10 +3195,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
@"scale=trunc(min(max(iw\,ih*a)\,min({0}\,{1}*a))/{2})*{2}:trunc(min(max(iw/a\,ih)\,min({0}/a\,{1}))/2)*2",
|
@"scale=trunc(min(max(iw\,ih*{3})\,min({0}\,{1}*{3}))/{2})*{2}:trunc(min(max(iw/{3}\,ih)\,min({0}/{3}\,{1}))/2)*2",
|
||||||
maxWidthParam,
|
maxWidthParam,
|
||||||
maxHeightParam,
|
maxHeightParam,
|
||||||
scaleVal);
|
scaleVal,
|
||||||
|
targetAr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a fixed width was requested
|
// If a fixed width was requested
|
||||||
@@ -3209,8 +3215,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"scale={0}:trunc(ow/a/2)*2",
|
"scale={0}:trunc(ow/{1}/2)*2",
|
||||||
widthParam);
|
widthParam,
|
||||||
|
targetAr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a fixed height was requested
|
// If a fixed height was requested
|
||||||
@@ -3220,9 +3227,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"scale=trunc(oh*a/{1})*{1}:{0}",
|
"scale=trunc(oh*{2}/{1})*{1}:{0}",
|
||||||
heightParam,
|
heightParam,
|
||||||
scaleVal);
|
scaleVal,
|
||||||
|
targetAr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a max width was requested
|
// If a max width was requested
|
||||||
@@ -3232,9 +3240,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
@"scale=trunc(min(max(iw\,ih*a)\,{0})/{1})*{1}:trunc(ow/a/2)*2",
|
@"scale=trunc(min(max(iw\,ih*{2})\,{0})/{1})*{1}:trunc(ow/{2}/2)*2",
|
||||||
maxWidthParam,
|
maxWidthParam,
|
||||||
scaleVal);
|
scaleVal,
|
||||||
|
targetAr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a max height was requested
|
// If a max height was requested
|
||||||
@@ -3244,9 +3253,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
return string.Format(
|
return string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
@"scale=trunc(oh*a/{1})*{1}:min(max(iw/a\,ih)\,{0})",
|
@"scale=trunc(oh*{2}/{1})*{1}:min(max(iw/{2}\,ih)\,{0})",
|
||||||
maxHeightParam,
|
maxHeightParam,
|
||||||
scaleVal);
|
scaleVal,
|
||||||
|
targetAr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
@@ -4291,6 +4301,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
// map from qsv to vaapi.
|
// map from qsv to vaapi.
|
||||||
mainFilters.Add("hwmap=derive_device=vaapi");
|
mainFilters.Add("hwmap=derive_device=vaapi");
|
||||||
|
mainFilters.Add("format=vaapi");
|
||||||
}
|
}
|
||||||
|
|
||||||
var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12");
|
var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12");
|
||||||
@@ -4300,6 +4311,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
{
|
{
|
||||||
// map from vaapi to qsv.
|
// map from vaapi to qsv.
|
||||||
mainFilters.Add("hwmap=derive_device=qsv");
|
mainFilters.Add("hwmap=derive_device=qsv");
|
||||||
|
mainFilters.Add("format=qsv");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4474,7 +4486,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// prefered vaapi + vulkan filters pipeline
|
// prefered vaapi + vulkan filters pipeline
|
||||||
if (_mediaEncoder.IsVaapiDeviceAmd
|
if (_mediaEncoder.IsVaapiDeviceAmd
|
||||||
&& isVaapiVkSupported
|
&& isVaapiVkSupported
|
||||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
|
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
|
||||||
|
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
|
||||||
{
|
{
|
||||||
// AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support.
|
// AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support.
|
||||||
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
|
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
|
||||||
|
|||||||
@@ -245,6 +245,21 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// <returns>A playlist.</returns>
|
/// <returns>A playlist.</returns>
|
||||||
IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
|
IReadOnlyList<string> GetPrimaryPlaylistM2tsFiles(string path);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the input path argument from <see cref="EncodingJobInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="state">The <see cref="EncodingJobInfo"/>.</param>
|
||||||
|
/// <returns>The input path argument.</returns>
|
||||||
|
string GetInputPathArgument(EncodingJobInfo state);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the input path argument.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The item path.</param>
|
||||||
|
/// <param name="mediaSource">The <see cref="MediaSourceInfo"/>.</param>
|
||||||
|
/// <returns>The input path argument.</returns>
|
||||||
|
string GetInputPathArgument(string path, MediaSourceInfo mediaSource);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a FFmpeg concat config for the source.
|
/// Generates a FFmpeg concat config for the source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -89,15 +89,28 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||||||
string outputPath,
|
string outputPath,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
var shouldExtractOneByOne = mediaSource.MediaAttachments.Any(a => a.FileName.Contains('/', StringComparison.OrdinalIgnoreCase) || a.FileName.Contains('\\', StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (shouldExtractOneByOne)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(outputPath))
|
var attachmentIndexes = mediaSource.MediaAttachments.Select(a => a.Index);
|
||||||
|
foreach (var i in attachmentIndexes)
|
||||||
{
|
{
|
||||||
await ExtractAllAttachmentsInternal(
|
var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture));
|
||||||
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
|
await ExtractAttachment(inputFile, mediaSource, i, newName, cancellationToken).ConfigureAwait(false);
|
||||||
outputPath,
|
}
|
||||||
false,
|
}
|
||||||
cancellationToken).ConfigureAwait(false);
|
else
|
||||||
|
{
|
||||||
|
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(outputPath))
|
||||||
|
{
|
||||||
|
await ExtractAllAttachmentsInternal(
|
||||||
|
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
|
||||||
|
outputPath,
|
||||||
|
false,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,8 @@ using MediaBrowser.Model.Entities;
|
|||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
using MediaBrowser.Model.MediaInfo;
|
||||||
using Microsoft.AspNetCore.Components.Forms;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using static Nikse.SubtitleEdit.Core.Common.IfoParser;
|
|
||||||
|
|
||||||
namespace MediaBrowser.MediaEncoding.Encoder
|
namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
{
|
{
|
||||||
@@ -621,7 +619,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
ImageFormat? targetFormat,
|
ImageFormat? targetFormat,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var inputArgument = GetInputArgument(inputFile, mediaSource);
|
var inputArgument = GetInputPathArgument(inputFile, mediaSource);
|
||||||
|
|
||||||
if (!isAudio)
|
if (!isAudio)
|
||||||
{
|
{
|
||||||
@@ -824,6 +822,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
options.EnableTonemapping = false;
|
options.EnableTonemapping = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (imageStream.Width is not null && imageStream.Height is not null && !string.IsNullOrEmpty(imageStream.AspectRatio))
|
||||||
|
{
|
||||||
|
// For hardware trickplay encoders, we need to re-calculate the size because they used fixed scale dimensions
|
||||||
|
var darParts = imageStream.AspectRatio.Split(':');
|
||||||
|
var (wa, ha) = (double.Parse(darParts[0], CultureInfo.InvariantCulture), double.Parse(darParts[1], CultureInfo.InvariantCulture));
|
||||||
|
// When dimension / DAR does not equal to 1:1, then the frames are most likely stored stretched.
|
||||||
|
// Note: this might be incorrect for 3D videos as the SAR stored might be per eye instead of per video, but we really can do little about it.
|
||||||
|
var shouldResetHeight = Math.Abs((imageStream.Width.Value * ha) - (imageStream.Height.Value * wa)) > .05;
|
||||||
|
if (shouldResetHeight)
|
||||||
|
{
|
||||||
|
// SAR = DAR * Height / Width
|
||||||
|
// RealHeight = Height / SAR = Height / (DAR * Height / Width) = Width / DAR
|
||||||
|
imageStream.Height = Convert.ToInt32(imageStream.Width.Value * ha / wa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) };
|
var baseRequest = new BaseEncodingJobOptions { MaxWidth = maxWidth, MaxFramerate = (float)(1.0 / interval.TotalSeconds) };
|
||||||
var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
|
var jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
|
||||||
{
|
{
|
||||||
@@ -945,7 +959,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
|
var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
|
||||||
timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
|
timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
|
||||||
|
|
||||||
while (isResponsive)
|
while (isResponsive && !cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -959,8 +973,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
// We don't actually expect the process to be finished in one timeout span, just that one image has been generated.
|
// We don't actually expect the process to be finished in one timeout span, just that one image has been generated.
|
||||||
}
|
}
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
|
var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
|
||||||
|
|
||||||
isResponsive = jpegCount > lastCount;
|
isResponsive = jpegCount > lastCount;
|
||||||
@@ -969,7 +981,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
if (!ranToCompletion)
|
if (!ranToCompletion)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Stopping trickplay extraction due to process inactivity.");
|
if (!isResponsive)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Trickplay process unresponsive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Stopping trickplay extraction.");
|
||||||
StopProcess(processWrapper, 1000);
|
StopProcess(processWrapper, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1137,16 +1154,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
|
var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
|
||||||
|
|
||||||
// Get all files from the BDMV/STREAMING directory
|
// Get all files from the BDMV/STREAMING directory
|
||||||
var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM"));
|
|
||||||
|
|
||||||
// Only return playable local .m2ts files
|
// Only return playable local .m2ts files
|
||||||
return directoryFiles
|
return validPlaybackFiles
|
||||||
.Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
|
.Select(f => _fileSystem.GetFileInfo(Path.Join(path, "BDMV", "STREAM", f)))
|
||||||
|
.Where(f => f.Exists)
|
||||||
.Select(f => f.FullName)
|
.Select(f => f.FullName)
|
||||||
.Order()
|
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetInputPathArgument(EncodingJobInfo state)
|
||||||
|
=> GetInputPathArgument(state.MediaPath, state.MediaSource);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetInputPathArgument(string path, MediaSourceInfo mediaSource)
|
||||||
|
{
|
||||||
|
return mediaSource.VideoType switch
|
||||||
|
{
|
||||||
|
VideoType.Dvd => GetInputArgument(GetPrimaryPlaylistVobFiles(path, null).ToList(), mediaSource),
|
||||||
|
VideoType.BluRay => GetInputArgument(GetPrimaryPlaylistM2tsFiles(path).ToList(), mediaSource),
|
||||||
|
_ => GetInputArgument(path, mediaSource)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
|
public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Model</PackageId>
|
<PackageId>Jellyfin.Model</PackageId>
|
||||||
<VersionPrefix>10.9.2</VersionPrefix>
|
<VersionPrefix>10.9.3</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ namespace MediaBrowser.Model.Search
|
|||||||
/// Gets or sets the matched term.
|
/// Gets or sets the matched term.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The matched term.</value>
|
/// <value>The matched term.</value>
|
||||||
public string MatchedTerm { get; set; }
|
public string? MatchedTerm { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the index number.
|
/// Gets or sets the index number.
|
||||||
|
|||||||
@@ -398,7 +398,8 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
|
|
||||||
foreach (var child in children)
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
if (!child.IsFolder)
|
// Exclude any folders and virtual items since they are only placeholders
|
||||||
|
if (!child.IsFolder && !child.IsVirtualItem)
|
||||||
{
|
{
|
||||||
var childDateCreated = child.DateCreated;
|
var childDateCreated = child.DateCreated;
|
||||||
if (childDateCreated > dateLastMediaAdded)
|
if (childDateCreated > dateLastMediaAdded)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -141,19 +142,15 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
&& item.SupportsLocalMetadata
|
&& item.SupportsLocalMetadata
|
||||||
&& !video.IsPlaceHolder)
|
&& !video.IsPlaceHolder)
|
||||||
{
|
{
|
||||||
if (!video.SubtitleFiles.SequenceEqual(
|
var externalFiles = new HashSet<string>(_subtitleResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
|
||||||
_subtitleResolver.GetExternalFiles(video, directoryService, false)
|
if (!new HashSet<string>(video.SubtitleFiles, StringComparer.Ordinal).SetEquals(externalFiles))
|
||||||
.Select(info => info.Path).ToList(),
|
|
||||||
StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
|
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!video.AudioFiles.SequenceEqual(
|
externalFiles = new HashSet<string>(_audioResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
|
||||||
_audioResolver.GetExternalFiles(video, directoryService, false)
|
if (!new HashSet<string>(video.AudioFiles, StringComparer.Ordinal).SetEquals(externalFiles))
|
||||||
.Select(info => info.Path).ToList(),
|
|
||||||
StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
|
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
|
||||||
return true;
|
return true;
|
||||||
@@ -161,14 +158,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item is Audio audio
|
if (item is Audio audio
|
||||||
&& item.SupportsLocalMetadata
|
&& item.SupportsLocalMetadata)
|
||||||
&& !audio.LyricFiles.SequenceEqual(
|
|
||||||
_lyricResolver.GetExternalFiles(audio, directoryService, false)
|
|
||||||
.Select(info => info.Path).ToList(),
|
|
||||||
StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path);
|
var externalFiles = new HashSet<string>(_lyricResolver.GetExternalFiles(audio, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
|
||||||
return true;
|
if (!new HashSet<string>(audio.LyricFiles, StringComparer.Ordinal).SetEquals(externalFiles))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
|
|||||||
bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
|
bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
|
||||||
bool replace = options.ReplaceAllImages;
|
bool replace = options.ReplaceAllImages;
|
||||||
|
|
||||||
if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false))
|
if (!enableDuringScan.GetValueOrDefault(false))
|
||||||
{
|
{
|
||||||
return ItemUpdateType.None;
|
return ItemUpdateType.None;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion("10.9.2")]
|
[assembly: AssemblyVersion("10.9.3")]
|
||||||
[assembly: AssemblyFileVersion("10.9.2")]
|
[assembly: AssemblyFileVersion("10.9.3")]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Extensions</PackageId>
|
<PackageId>Jellyfin.Extensions</PackageId>
|
||||||
<VersionPrefix>10.9.2</VersionPrefix>
|
<VersionPrefix>10.9.3</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -30,25 +30,8 @@ namespace Jellyfin.LiveTv.TunerHosts
|
|||||||
{
|
{
|
||||||
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
|
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
|
||||||
{
|
{
|
||||||
private static readonly string[] _disallowedMimeTypes =
|
private static readonly string[] _mimeTypesCanShareHttpStream = ["video/MP2T"];
|
||||||
{
|
private static readonly string[] _extensionsCanShareHttpStream = [".ts", ".tsv", ".m2t"];
|
||||||
"text/plain",
|
|
||||||
"text/html",
|
|
||||||
"video/x-matroska",
|
|
||||||
"video/mp4",
|
|
||||||
"application/vnd.apple.mpegurl",
|
|
||||||
"application/mpegurl",
|
|
||||||
"application/x-mpegurl",
|
|
||||||
"video/vnd.mpeg.dash.mpd"
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly string[] _disallowedSharedStreamExtensions =
|
|
||||||
{
|
|
||||||
".mkv",
|
|
||||||
".mp4",
|
|
||||||
".m3u8",
|
|
||||||
".mpd"
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
@@ -113,27 +96,33 @@ namespace Jellyfin.LiveTv.TunerHosts
|
|||||||
|
|
||||||
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
|
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
|
||||||
{
|
{
|
||||||
using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
|
var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path);
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
|
||||||
.SendAsync(message, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
if (string.IsNullOrEmpty(extension))
|
||||||
{
|
{
|
||||||
if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase))
|
try
|
||||||
{
|
{
|
||||||
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
|
using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
|
||||||
|
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||||
|
.SendAsync(message, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
if (_mimeTypesCanShareHttpStream.Contains(response.Content.Headers.ContentType?.MediaType, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("HEAD request to check MIME type failed, shared stream disabled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else if (_extensionsCanShareHttpStream.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Fallback to check path extension when the server does not support HEAD method
|
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
|
||||||
// Use UriBuilder to remove all query string as GetExtension will include them when used directly
|
|
||||||
var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path);
|
|
||||||
if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -903,6 +903,17 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get if the IPAddress is Link-local.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">The IP Address.</param>
|
||||||
|
/// <returns>Bool indicates if the address is link-local.</returns>
|
||||||
|
public bool IsLinkLocalAddress(IPAddress address)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(address);
|
||||||
|
return NetworkConstants.IPv4RFC3927LinkLocal.Contains(address) || address.IsIPv6LinkLocal;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool IsInLocalNetwork(IPAddress address)
|
public bool IsInLocalNetwork(IPAddress address)
|
||||||
{
|
{
|
||||||
@@ -1084,7 +1095,11 @@ public class NetworkManager : INetworkManager, IDisposable
|
|||||||
private bool MatchesExternalInterface(IPAddress source, out string result)
|
private bool MatchesExternalInterface(IPAddress source, out string result)
|
||||||
{
|
{
|
||||||
// Get the first external interface address that isn't a loopback.
|
// Get the first external interface address that isn't a loopback.
|
||||||
var extResult = _interfaces.Where(p => !IsInLocalNetwork(p.Address)).OrderBy(x => x.Index).ToArray();
|
var extResult = _interfaces
|
||||||
|
.Where(p => !IsInLocalNetwork(p.Address))
|
||||||
|
.Where(p => p.Address.AddressFamily.Equals(source.AddressFamily))
|
||||||
|
.Where(p => !IsLinkLocalAddress(p.Address))
|
||||||
|
.OrderBy(x => x.Index).ToArray();
|
||||||
|
|
||||||
// No external interface found
|
// No external interface found
|
||||||
if (extResult.Length == 0)
|
if (extResult.Length == 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user