mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-16 16:18:06 +00:00
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 |
8
.github/workflows/ci-codeql-analysis.yml
vendored
8
.github/workflows/ci-codeql-analysis.yml
vendored
@@ -20,18 +20,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
uses: github/codeql-action/init@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
uses: github/codeql-action/autobuild@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
|
||||
uses: github/codeql-action/analyze@9fdb3e49720b44c48891d036bb502feb25684276 # v3.25.6
|
||||
|
||||
108
.github/workflows/ci-openapi.yml
vendored
108
.github/workflows/ci-openapi.yml
vendored
@@ -3,6 +3,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request_target:
|
||||
|
||||
permissions: {}
|
||||
@@ -14,7 +16,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -39,7 +41,7 @@ jobs:
|
||||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -99,11 +101,24 @@ jobs:
|
||||
- id: read-diff
|
||||
name: Read openapi-diff output
|
||||
run: |
|
||||
# Read and fix markdown
|
||||
body=$(cat openapi-changes.md)
|
||||
body="${body//'%'/'%25'}"
|
||||
body="${body//$'\n'/'%0A'}"
|
||||
body="${body//$'\r'/'%0D'}"
|
||||
echo ::set-output name=body::$body
|
||||
# Write to workflow summary
|
||||
echo "$body" >> $GITHUB_STEP_SUMMARY
|
||||
# Set ApiChanged var
|
||||
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
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||
id: find-comment
|
||||
@@ -113,22 +128,15 @@ jobs:
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- name: Reply or edit difference comment (changed)
|
||||
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:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
edit-mode: replace
|
||||
body: |
|
||||
<!--openapi-diff-workflow-comment-->
|
||||
<details>
|
||||
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
|
||||
|
||||
${{ steps.read-diff.outputs.body }}
|
||||
|
||||
</details>
|
||||
body-path: openapi-changes-reply.md
|
||||
- name: Edit difference comment (unchanged)
|
||||
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:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
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.
|
||||
|
||||
publish:
|
||||
publish-unstable:
|
||||
name: OpenAPI - Publish Unstable Spec
|
||||
if: |
|
||||
github.event_name != 'pull_request_target' &&
|
||||
contains(github.repository_owner, 'jellyfin')
|
||||
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- openapi-head
|
||||
@@ -201,3 +207,65 @@ jobs:
|
||||
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
|
||||
fi
|
||||
) 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
|
||||
|
||||
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
runs-on: "${{ matrix.os }}"
|
||||
steps:
|
||||
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
|
||||
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
|
||||
with:
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@2a2d60ea1c7e811f54684179af6ac1ae8c1ce69a # 5.2.5
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@6b06171d1a131e7fd85121120a1c00c1ed03e033 # 5.3.0
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
||||
6
.github/workflows/commands.yml
vendored
6
.github/workflows/commands.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
reactions: eyes
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
|
||||
2
.github/workflows/issue-template-check.yml
vendored
2
.github/workflows/issue-template-check.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: pull in script
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
repository: jellyfin/jellyfin-triage-script
|
||||
- name: install python
|
||||
|
||||
2
.github/workflows/pull-request-conflict.yml
vendored
2
.github/workflows/pull-request-conflict.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||
steps:
|
||||
- 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'}}
|
||||
with:
|
||||
dirtyLabel: 'merge conflict'
|
||||
|
||||
4
.github/workflows/release-bump-version.yaml
vendored
4
.github/workflows/release-bump-version.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
yq-version: v4.9.8
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
|
||||
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
- [btopherjohnson](https://github.com/btopherjohnson)
|
||||
- [GeorgeH005](https://github.com/GeorgeH005)
|
||||
- [Vedant](https://github.com/viktory36/)
|
||||
- [NotSaifA](https://github.com/NotSaifA)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<VersionPrefix>10.9.2</VersionPrefix>
|
||||
<VersionPrefix>10.9.3</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Emby.Naming.TV;
|
||||
public static class TvParserHelpers
|
||||
{
|
||||
private static readonly string[] _continuingState = ["Pilot", "Returning Series", "Returning"];
|
||||
private static readonly string[] _endedState = ["Cancelled"];
|
||||
private static readonly string[] _endedState = ["Cancelled", "Canceled"];
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
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 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)
|
||||
{
|
||||
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(
|
||||
httpContext.User.GetClient(),
|
||||
httpContext.User.GetVersion(),
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Data</PackageId>
|
||||
<VersionPrefix>10.9.2</VersionPrefix>
|
||||
<VersionPrefix>10.9.3</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -81,6 +81,12 @@ public class TrickplayManager : ITrickplayManager
|
||||
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
|
||||
|
||||
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)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -267,7 +273,7 @@ public class TrickplayManager : ITrickplayManager
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Common</PackageId>
|
||||
<VersionPrefix>10.9.2</VersionPrefix>
|
||||
<VersionPrefix>10.9.3</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -58,6 +58,11 @@ public static class NetworkConstants
|
||||
/// </summary>
|
||||
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>
|
||||
/// IPv6 loopback as defined in RFC 4291.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Controller</PackageId>
|
||||
<VersionPrefix>10.9.2</VersionPrefix>
|
||||
<VersionPrefix>10.9.3</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -55,6 +55,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
private readonly Version _minKerneli915Hang = new Version(5, 18);
|
||||
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
|
||||
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 _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
||||
@@ -680,16 +681,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
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>
|
||||
/// Gets the audio encoder.
|
||||
/// </summary>
|
||||
@@ -1005,7 +996,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc");
|
||||
|
||||
if (IsVulkanFullSupported()
|
||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
|
||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
|
||||
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
|
||||
{
|
||||
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
|
||||
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
|
||||
@@ -1203,7 +1195,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
else
|
||||
{
|
||||
arg.Append(" -i ")
|
||||
.Append(GetInputPathArgument(state));
|
||||
.Append(_mediaEncoder.GetInputPathArgument(state));
|
||||
}
|
||||
|
||||
// sub2video for external graphical subtitles
|
||||
@@ -2083,6 +2075,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
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))
|
||||
{
|
||||
// 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.Value == 6)
|
||||
{
|
||||
if (!encodingOptions.DownMixAudioBoost.Equals(1))
|
||||
{
|
||||
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
switch (encodingOptions.DownMixStereoAlgorithm)
|
||||
{
|
||||
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");
|
||||
break;
|
||||
case DownMixStereoAlgorithms.NightmodeDialogue:
|
||||
@@ -2640,11 +2648,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
break;
|
||||
case DownMixStereoAlgorithms.None:
|
||||
default:
|
||||
if (!encodingOptions.DownMixAudioBoost.Equals(1))
|
||||
{
|
||||
filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -3161,7 +3164,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
int? requestedMaxHeight)
|
||||
{
|
||||
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 targetAr = isMjpeg ? "(a*sar)" : "a"; // manually calculate AR when using mjpeg encoder
|
||||
|
||||
// If fixed dimensions were supplied
|
||||
if (requestedWidth.HasValue && requestedHeight.HasValue)
|
||||
@@ -3190,10 +3195,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
return string.Format(
|
||||
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,
|
||||
maxHeightParam,
|
||||
scaleVal);
|
||||
scaleVal,
|
||||
targetAr);
|
||||
}
|
||||
|
||||
// If a fixed width was requested
|
||||
@@ -3209,8 +3215,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"scale={0}:trunc(ow/a/2)*2",
|
||||
widthParam);
|
||||
"scale={0}:trunc(ow/{1}/2)*2",
|
||||
widthParam,
|
||||
targetAr);
|
||||
}
|
||||
|
||||
// If a fixed height was requested
|
||||
@@ -3220,9 +3227,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"scale=trunc(oh*a/{1})*{1}:{0}",
|
||||
"scale=trunc(oh*{2}/{1})*{1}:{0}",
|
||||
heightParam,
|
||||
scaleVal);
|
||||
scaleVal,
|
||||
targetAr);
|
||||
}
|
||||
|
||||
// If a max width was requested
|
||||
@@ -3232,9 +3240,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
return string.Format(
|
||||
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,
|
||||
scaleVal);
|
||||
scaleVal,
|
||||
targetAr);
|
||||
}
|
||||
|
||||
// If a max height was requested
|
||||
@@ -3244,9 +3253,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
return string.Format(
|
||||
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,
|
||||
scaleVal);
|
||||
scaleVal,
|
||||
targetAr);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
@@ -4291,6 +4301,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
// map from qsv to vaapi.
|
||||
mainFilters.Add("hwmap=derive_device=vaapi");
|
||||
mainFilters.Add("format=vaapi");
|
||||
}
|
||||
|
||||
var tonemapFilter = GetHwTonemapFilter(options, "vaapi", "nv12");
|
||||
@@ -4300,6 +4311,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
// map from vaapi to qsv.
|
||||
mainFilters.Add("hwmap=derive_device=qsv");
|
||||
mainFilters.Add("format=qsv");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4474,7 +4486,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// prefered vaapi + vulkan filters pipeline
|
||||
if (_mediaEncoder.IsVaapiDeviceAmd
|
||||
&& isVaapiVkSupported
|
||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
|
||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop
|
||||
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
|
||||
{
|
||||
// AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support.
|
||||
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
|
||||
|
||||
@@ -245,6 +245,21 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// <returns>A playlist.</returns>
|
||||
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>
|
||||
/// Generates a FFmpeg concat config for the source.
|
||||
/// </summary>
|
||||
|
||||
@@ -89,15 +89,28 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
string outputPath,
|
||||
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(
|
||||
_mediaEncoder.GetInputArgument(inputFile, mediaSource),
|
||||
outputPath,
|
||||
false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
var newName = Path.Join(outputPath, i.ToString(CultureInfo.InvariantCulture));
|
||||
await ExtractAttachment(inputFile, mediaSource, i, newName, 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.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Nikse.SubtitleEdit.Core.Common.IfoParser;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
@@ -621,7 +619,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
ImageFormat? targetFormat,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputArgument = GetInputArgument(inputFile, mediaSource);
|
||||
var inputArgument = GetInputPathArgument(inputFile, mediaSource);
|
||||
|
||||
if (!isAudio)
|
||||
{
|
||||
@@ -824,6 +822,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
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 jobState = new EncodingJobInfo(TranscodingJobType.Progressive)
|
||||
{
|
||||
@@ -945,7 +959,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
var timeoutMs = _configurationManager.Configuration.ImageExtractionTimeoutMs;
|
||||
timeoutMs = timeoutMs <= 0 ? DefaultHdrImageExtractionTimeout : timeoutMs;
|
||||
|
||||
while (isResponsive)
|
||||
while (isResponsive && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
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.
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var jpegCount = _fileSystem.GetFilePaths(targetDirectory).Count();
|
||||
|
||||
isResponsive = jpegCount > lastCount;
|
||||
@@ -969,7 +981,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1137,16 +1154,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
var validPlaybackFiles = _blurayExaminer.GetDiscInfo(path).Files;
|
||||
|
||||
// Get all files from the BDMV/STREAMING directory
|
||||
var directoryFiles = _fileSystem.GetFiles(Path.Join(path, "BDMV", "STREAM"));
|
||||
|
||||
// Only return playable local .m2ts files
|
||||
return directoryFiles
|
||||
.Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
|
||||
return validPlaybackFiles
|
||||
.Select(f => _fileSystem.GetFileInfo(Path.Join(path, "BDMV", "STREAM", f)))
|
||||
.Where(f => f.Exists)
|
||||
.Select(f => f.FullName)
|
||||
.Order()
|
||||
.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 />
|
||||
public void GenerateConcatConfig(MediaSourceInfo source, string concatFilePath)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Model</PackageId>
|
||||
<VersionPrefix>10.9.2</VersionPrefix>
|
||||
<VersionPrefix>10.9.3</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace MediaBrowser.Model.Search
|
||||
/// Gets or sets the matched term.
|
||||
/// </summary>
|
||||
/// <value>The matched term.</value>
|
||||
public string MatchedTerm { get; set; }
|
||||
public string? MatchedTerm { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index number.
|
||||
|
||||
@@ -398,7 +398,8 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
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;
|
||||
if (childDateCreated > dateLastMediaAdded)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -141,19 +142,15 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
&& item.SupportsLocalMetadata
|
||||
&& !video.IsPlaceHolder)
|
||||
{
|
||||
if (!video.SubtitleFiles.SequenceEqual(
|
||||
_subtitleResolver.GetExternalFiles(video, directoryService, false)
|
||||
.Select(info => info.Path).ToList(),
|
||||
StringComparer.Ordinal))
|
||||
var externalFiles = new HashSet<string>(_subtitleResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
|
||||
if (!new HashSet<string>(video.SubtitleFiles, StringComparer.Ordinal).SetEquals(externalFiles))
|
||||
{
|
||||
_logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!video.AudioFiles.SequenceEqual(
|
||||
_audioResolver.GetExternalFiles(video, directoryService, false)
|
||||
.Select(info => info.Path).ToList(),
|
||||
StringComparer.Ordinal))
|
||||
externalFiles = new HashSet<string>(_audioResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
|
||||
if (!new HashSet<string>(video.AudioFiles, StringComparer.Ordinal).SetEquals(externalFiles))
|
||||
{
|
||||
_logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
|
||||
return true;
|
||||
@@ -161,14 +158,14 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
}
|
||||
|
||||
if (item is Audio audio
|
||||
&& item.SupportsLocalMetadata
|
||||
&& !audio.LyricFiles.SequenceEqual(
|
||||
_lyricResolver.GetExternalFiles(audio, directoryService, false)
|
||||
.Select(info => info.Path).ToList(),
|
||||
StringComparer.Ordinal))
|
||||
&& item.SupportsLocalMetadata)
|
||||
{
|
||||
_logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path);
|
||||
return true;
|
||||
var externalFiles = new HashSet<string>(_lyricResolver.GetExternalFiles(audio, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase);
|
||||
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;
|
||||
|
||||
@@ -101,7 +101,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>,
|
||||
bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan;
|
||||
bool replace = options.ReplaceAllImages;
|
||||
|
||||
if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false))
|
||||
if (!enableDuringScan.GetValueOrDefault(false))
|
||||
{
|
||||
return ItemUpdateType.None;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion("10.9.2")]
|
||||
[assembly: AssemblyFileVersion("10.9.2")]
|
||||
[assembly: AssemblyVersion("10.9.3")]
|
||||
[assembly: AssemblyFileVersion("10.9.3")]
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Extensions</PackageId>
|
||||
<VersionPrefix>10.9.2</VersionPrefix>
|
||||
<VersionPrefix>10.9.3</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -30,25 +30,8 @@ namespace Jellyfin.LiveTv.TunerHosts
|
||||
{
|
||||
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
|
||||
{
|
||||
private static readonly string[] _disallowedMimeTypes =
|
||||
{
|
||||
"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 static readonly string[] _mimeTypesCanShareHttpStream = ["video/MP2T"];
|
||||
private static readonly string[] _extensionsCanShareHttpStream = [".ts", ".tsv", ".m2t"];
|
||||
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
@@ -113,27 +96,33 @@ namespace Jellyfin.LiveTv.TunerHosts
|
||||
|
||||
if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
|
||||
{
|
||||
using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(message, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var extension = Path.GetExtension(new UriBuilder(mediaSource.Path).Path);
|
||||
|
||||
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
|
||||
// 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);
|
||||
}
|
||||
return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -903,6 +903,17 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
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/>
|
||||
public bool IsInLocalNetwork(IPAddress address)
|
||||
{
|
||||
@@ -1084,7 +1095,11 @@ public class NetworkManager : INetworkManager, IDisposable
|
||||
private bool MatchesExternalInterface(IPAddress source, out string result)
|
||||
{
|
||||
// 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
|
||||
if (extResult.Length == 0)
|
||||
|
||||
Reference in New Issue
Block a user