Compare commits

..

94 Commits

Author SHA1 Message Date
Jellyfin Release Bot
ab4315742f Bump version to 10.9.4 2024-06-01 18:38:59 -04:00
Joshua M. Boniface
2ddb15c784 Merge pull request #11743 from Shadowghost/fix-replace
Fix replace logic
2024-06-01 18:33:31 -04:00
Joshua M. Boniface
95c7d997c1 Merge pull request #11823 from gnattu/env-disable-second-level-cache
Add Env Var to disable second level cache
2024-06-01 18:32:54 -04:00
Joshua M. Boniface
e2c909f50f Merge pull request #11762 from Shadowghost/fix-lyrics
Mark Audio as RequiresDeserialization and backfill data
2024-06-01 18:32:36 -04:00
Joshua M. Boniface
a53ea029fa Merge pull request #11719 from Shadowghost/fix-season-names
Move NFO series season name parsing to own local provider
2024-06-01 18:31:55 -04:00
Joshua M. Boniface
869dab2ba2 Merge pull request #11873 from thornbill/fix-first-time-setup-guest-but-for-real
Fix FirstTimeSetupHandler allowing public access
2024-06-01 18:31:31 -04:00
Joshua M. Boniface
d2be2ee480 Merge pull request #11910 from Bond-009/audionormout
Audio normalization: parse ffmpeg output line by line
2024-06-01 16:07:05 -04:00
Bond_009
bc8ef94f0d Audio normalization: parse ffmpeg output line by line
Should prevent OOM error
Also optimized the regex so it can bail out earlier
2024-06-01 17:50:12 +02:00
Bill Thornton
ed1b880359 Remove api key check and simplify conditions 2024-05-31 16:31:15 -04:00
Bill Thornton
7221e7ca68 Fix FirstTimeSetupHandler failing for users and update tests 2024-05-31 14:09:04 -04:00
gnattu
0392daa103 Relax remuxing requirement for LiveTV (#11851) 2024-05-31 07:01:47 -06:00
gnattu
d602b6dbc5 Fix multi-part album folder being detected as artist folder (#11886) 2024-05-31 07:01:28 -06:00
Cody Robibero
b8a0cf6a9e Merge pull request #11859 from gnattu/use-ffprobe-audio-metadata-fallback 2024-05-31 07:00:56 -06:00
Cody Robibero
26419c64f5 Merge pull request #11894 from gnattu/escape-concated-path 2024-05-31 06:58:00 -06:00
Bill Thornton
a71e2d9f0a Update FirstTimeSetupHandler.cs
Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2024-05-31 03:18:33 -04:00
gnattu
cfe67ff17d Add extra white space
Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2024-05-31 15:14:29 +08:00
gnattu
78e3ee15f9 Don't use interpolated strings
Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2024-05-31 14:59:57 +08:00
gnattu
2cb74e3dd0 Escape tmpConcatPath for DVD and BD folder
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-31 14:54:00 +08:00
Joshua M. Boniface
8e979bdb4b Merge pull request #11882 from Shadowghost/fix-season-missing-episode
Fix missing episodes query for seasons
2024-05-30 19:52:20 -04:00
Shadowghost
e099fd6141 Fix missing episodes query for seasons 2024-05-30 20:26:26 +02:00
Bill Thornton
35962bcc42 Fix FirstTimeSetupHandler api key test 2024-05-30 12:08:52 -04:00
Tim Eisele
ae584beaac Return missing episodes for series when no user defined (#11806) 2024-05-30 09:32:37 -06:00
Cody Robibero
563033786f Merge pull request #11876 from Bond-009/enableLib 2024-05-30 09:32:00 -06:00
Joshua M. Boniface
f8c7f36a34 Merge pull request #11867 from Shadowghost/stable-dep-upgrade
Upgrade dependencies
2024-05-30 10:36:30 -04:00
Shadowghost
4746c88633 Apply review suggestion 2024-05-30 09:21:23 +02:00
Shadowghost
b7d6bedbbb Apply review suggestion 2024-05-30 08:54:44 +02:00
Bond_009
8db79c05dd Don't check if admin has access to library when updating
The access check also checks if the library is enabled, this makes it impossible to enable disabled libraries.
Regression from  #11171
2024-05-29 22:27:29 +02:00
Bill Thornton
8fa7ff647a Defer standard authentication checks to DefaultAuthorizationHandler 2024-05-29 14:35:41 -04:00
Bond-009
d0336cd67e Merge pull request #11857 from gnattu/fix-ffprobe-useragent
Fix ffprobe -user_agent parameter
2024-05-29 10:00:36 +02:00
Shadowghost
cfab4eb2fc Fix xUnit 2024-05-28 22:08:33 +02:00
Shadowghost
5f1c5009d3 Upgrade dependencies 2024-05-28 22:08:33 +02:00
gnattu
97d7151289 Don't use finally block for tag fallback
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-28 20:24:40 +08:00
gnattu
475fa36ea3 Use better exception logging
Co-authored-by: Claus Vium <cvium@users.noreply.github.com>
2024-05-28 20:23:03 +08:00
gnattu
e8d1ee0934 Use music metadata from ffprobe when TagLib fails
TagLib has its own limitations, which cause it to fail on certain audio files or extract incomplete information from the tags. Use the information from ffprobe when TagLib fails to extract data.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-28 14:24:36 +08:00
gnattu
d07ec4ad0f Fix ffprobe -user_agent parameter
The PR #10448 was doing it wrong and actually did not work. Even its unit testing was testing for a wrong output.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-28 03:18:46 +08:00
Shadowghost
684dfedbcc Do not remove already set people on locked items 2024-05-27 19:14:17 +02:00
Jellyfin Release Bot
730b01fb14 Bump version to 10.9.3 2024-05-26 20:00:29 -04:00
gnattu
45e8872cc0 Extract media attachment one by one if the filename appears to be a path (#11812) 2024-05-26 11:40:28 -06:00
Shadowghost
cc2c00d764 Respect locked fields when updating children from parent 2024-05-26 17:01:19 +02:00
gnattu
402a5e2c9f Use simpler config value
Only true and false are supported now

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-26 10:45:38 +08:00
Joshua M. Boniface
bcf884ccfa Update GitHub workflows from Master
Backport fixes from:
 - #11780
 - #11779
 - #11769
 - #11733
 - #11515
2024-05-25 11:55:48 -04:00
gnattu
2eece01acc Filter invalid IPs on external interface matching (#11766) 2024-05-25 09:44:03 -06:00
gnattu
ef985896e2 Use SharedStream for LiveTV more restrictively (#11805) 2024-05-25 09:43:53 -06:00
Nyanmisaka
5e7514243c Fix the IOSurf error in QSV transcoding (#11830) 2024-05-25 09:00:30 -06:00
gnattu
b9c0fc69e8 Add Env Var to disable second level cache
This is an attempt to track down possible causes of remaining database lockups. Add an environment variable to disable the second-level cache entirely to see if it works better on systems that still experience lockups.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-25 01:12:00 +08:00
NotSaifA
4a344bebc0 Trickplay: kill ffmpeg when task is cancelled (#11790) 2024-05-24 07:50:55 -06:00
Tim Eisele
b2d54b82fa Improve reliability of HasChanged check (#11792) 2024-05-24 07:50:09 -06:00
gnattu
e7b1162cb3 Force more compatible transcoding profile for LiveTV (#11801) 2024-05-24 07:36:59 -06:00
Tim Eisele
d89e5a0074 Exclude virtual items from DateLastMediaAdded calculation (#11804) 2024-05-24 07:36:39 -06:00
Tim Eisele
4a54e5ddeb Add Canceled to ended state (#11808) 2024-05-24 07:36:19 -06:00
Bond-009
d9232e05f1 Merge pull request #11798 from gnattu/fix-trickplay-image-height
Recalculate trickplay image height for anamorphic videos
2024-05-24 14:19:37 +02:00
Bond-009
52be8be28f Merge pull request #11754 from Shadowghost/fix-bd-chapter-images
Fix BD/DVD folder chapter image extraction
2024-05-24 14:18:10 +02:00
Bond-009
ab6c2424db Merge pull request #11802 from crobibero/nullable-match-term
Mark SearchHint.MatchedTerm as nullable
2024-05-24 14:17:59 +02:00
Bond-009
eb437e7163 Merge pull request #11799 from nyanmisaka/fix-va-vk-interop-chk
Disable VA-VK interop on not supported kernel versions
2024-05-24 14:17:51 +02:00
Bond-009
2ddf2a7866 Merge pull request #11781 from Bond-009/orderts
Retain order blu-ray segments
2024-05-24 14:16:45 +02:00
Bond-009
60232ce9be Merge pull request #11788 from gnattu/override-too-small-trickplay-image-interval
Override too small trickplay image interval
2024-05-24 14:16:33 +02:00
gnattu
952995f796 Ignore NullOrEmpty imageStream.AspectRatio
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 23:08:29 +08:00
gnattu
933a285bf5 Use single quote
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 23:03:35 +08:00
gnattu
f8da69f8e5 Allow decimal aspect ratio
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 23:02:57 +08:00
Cody Robibero
d5e29bfce3 here you go Niels 2024-05-23 07:16:56 -06:00
nyanmisaka
ab36c4c011 Disable VA-VK interop on not supported kernel versions
Signed-off-by: nyanmisaka <nst799610810@gmail.com>
2024-05-23 14:53:00 +08:00
gnattu
c6e29647fc Recalculate trickplay image height for anamorphic videos
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-23 12:36:43 +08:00
Shadowghost
95a6291c34 Fixes 2024-05-22 19:26:19 +02:00
gnattu
fa2bff30f6 Explicitly use decimal
Co-authored-by: Cody Robibero <cody@robibe.ro>
2024-05-22 21:17:00 +08:00
Shadowghost
58041e1f9d Fix backwards merge 2024-05-22 13:44:45 +02:00
gnattu
447f73caf4 Fix bitrate calculation accuracy
Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-22 13:41:54 +08:00
Shadowghost
9145be6bfc Remove local metadata stop logic 2024-05-22 07:38:53 +02:00
gnattu
86129589ef Override too small trickplay image interval
Some users may set this value too low, causing the trickplay generation to fail. Reset this interval to the minimum valid value of 1 second when the user-configured value is too small to prevent generation failures.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-22 13:35:28 +08:00
Shadowghost
f5a8fca22f Don't drop existing metadata if item gets local locked 2024-05-21 23:22:44 +02:00
Shadowghost
2a612611b8 Extend minimum local metadata requirements 2024-05-21 23:14:28 +02:00
Shadowghost
0b64426cf2 Fix local locked and StopRefreshIfLocalMetadataFound logic 2024-05-21 23:08:00 +02:00
Shadowghost
f3bf9bcdc8 Fix final merge logic 2024-05-21 21:47:29 +02:00
Niels van Velzen
06a5ddda5e Merge pull request #11774 from Bond-009/downmix
Apply audio boost when downmixing regardless of downmixalgo
2024-05-21 21:40:22 +02:00
Shadowghost
8a5a93ee80 Allow removal of all people from an item 2024-05-21 21:14:30 +02:00
Bond_009
6777f47e0e Retain order blu-ray segments 2024-05-21 20:19:15 +02:00
Bond_009
cf04e1d8e5 Apply audio boost when downmixing regardless of downmixalgo
This is what I'd expect from the naming
Should fix #11771
2024-05-21 16:24:43 +02:00
Bond-009
d608f1e3cc Merge pull request #11713 from gnattu/fix-vt-h264-profile
Fix VideoToolbox H264 constrained profile option
2024-05-21 13:58:01 +02:00
Bond-009
86f5c93434 Merge pull request #11739 from Shadowghost/fix-trickplay
Do not run trickplay on scan if disabled
2024-05-21 13:57:41 +02:00
Bond-009
4fcbeef5e6 Merge pull request #11738 from crobibero/non-user-session
Don't require user when getting current session
2024-05-21 13:57:31 +02:00
Shadowghost
7d983ae0dd Mark Audio as RequiresDeserialization and backfill data 2024-05-20 22:56:22 +02:00
Shadowghost
a2ab34ef4c Rename provider to be run after normal season NFO provider 2024-05-20 20:12:52 +02:00
Shadowghost
77abafca8e Fix BD/DVD folder chapter image extraction 2024-05-20 13:19:05 +02:00
Shadowghost
e67eb48540 Never revert locked state 2024-05-20 12:49:26 +02:00
Shadowghost
37d7e8f5bf Fix replacement logic 2024-05-20 12:24:57 +02:00
Shadowghost
e6eef8bece Set path for season folders 2024-05-20 01:45:12 +02:00
Shadowghost
52cfd9f261 Properly pass replace flag to remote provider logic 2024-05-19 19:59:47 +02:00
Shadowghost
9a9e8e2648 Prevent infite loop 2024-05-19 18:36:18 +02:00
Shadowghost
2cebd5e05f Do not run trickplay on scan if disabled 2024-05-19 16:52:59 +02:00
Shadowghost
7fa72260ca Only set season name if it's unset 2024-05-19 16:50:51 +02:00
Cody Robibero
b19b346670 Don't require user when getting current session 2024-05-19 08:25:38 -06:00
Shadowghost
99de0ca45f Directly use ParentId 2024-05-18 21:28:07 +02:00
Shadowghost
c106b399d7 Apply review suggestion 2024-05-18 20:41:04 +02:00
Shadowghost
c274062e87 Move NFO series season name parsing to own local provider 2024-05-18 20:29:08 +02:00
gnattu
53de8c0805 Fix VideoToolbox H264 constrained profile option
Like a lot of other encoders they need an underscore between two words.

Signed-off-by: gnattu <gnattuoc@me.com>
2024-05-18 19:53:23 +08:00
77 changed files with 1159 additions and 683 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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/"

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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 }}

View File

@@ -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

View File

@@ -16,7 +16,7 @@
<PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.4.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.5.0" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
@@ -25,15 +25,15 @@
<PackageVersion Include="libse" Version="4.0.5" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.6" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
@@ -42,14 +42,14 @@
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.4" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.4" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.6" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.6" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
@@ -74,7 +74,7 @@
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.6.2" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
@@ -85,8 +85,8 @@
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.1" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.7.1" />
<PackageVersion Include="xunit" Version="2.8.1" />
</ItemGroup>
</Project>
</Project>

View File

@@ -36,7 +36,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.4</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -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"/>.

View File

@@ -19,7 +19,8 @@ namespace Emby.Server.Implementations
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" }
{ SqliteCacheSizeKey, "20000" },
{ SqliteDisableSecondLevelCacheKey, bool.FalseString }
};
}
}

View File

@@ -1298,16 +1298,15 @@ namespace Emby.Server.Implementations.Data
&& type != typeof(Book)
&& type != typeof(LiveTvProgram)
&& type != typeof(AudioBook)
&& type != typeof(Audio)
&& type != typeof(MusicAlbum);
}
private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query)
{
return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query));
return GetItem(reader, query, HasProgramAttributes(query), HasEpisodeAttributes(query), HasServiceName(query), HasStartDate(query), HasTrailerTypes(query), HasArtistFields(query), HasSeriesFields(query), false);
}
private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields)
private BaseItem GetItem(SqliteDataReader reader, InternalItemsQuery query, bool enableProgramAttributes, bool hasEpisodeAttributes, bool hasServiceName, bool queryHasStartDate, bool hasTrailerTypes, bool hasArtistFields, bool hasSeriesFields, bool skipDeserialization)
{
var typeString = reader.GetString(0);
@@ -1320,7 +1319,7 @@ namespace Emby.Server.Implementations.Data
BaseItem item = null;
if (TypeRequiresDeserialization(type))
if (TypeRequiresDeserialization(type) && !skipDeserialization)
{
try
{
@@ -2562,7 +2561,7 @@ namespace Emby.Server.Implementations.Data
foreach (var row in statement.ExecuteQuery())
{
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, query.SkipDeserialization);
if (item is not null)
{
items.Add(item);
@@ -2774,7 +2773,7 @@ namespace Emby.Server.Implementations.Data
foreach (var row in statement.ExecuteQuery())
{
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
if (item is not null)
{
list.Add(item);
@@ -5021,7 +5020,7 @@ AND Type = @InternalPersonType)");
foreach (var row in statement.ExecuteQuery())
{
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields);
var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields, false);
if (item is not null)
{
var countStartColumn = columns.Count - 1;
@@ -5222,19 +5221,20 @@ AND Type = @InternalPersonType)");
throw new ArgumentNullException(nameof(itemId));
}
ArgumentNullException.ThrowIfNull(people);
CheckDisposed();
using var connection = GetConnection();
using var transaction = connection.BeginTransaction();
// First delete chapters
// Delete all existing people first
using var command = connection.CreateCommand();
command.CommandText = "delete from People where ItemId=@ItemId";
command.TryBind("@ItemId", itemId);
command.ExecuteNonQuery();
InsertPeople(itemId, people, connection);
if (people is not null)
{
InsertPeople(itemId, people, connection);
}
transaction.Commit();
}

View File

@@ -2812,8 +2812,10 @@ namespace Emby.Server.Implementations.Library
}
_itemRepository.UpdatePeople(item.Id, people);
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
if (people is not null)
{
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
}
}
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool removeOnFailure)

View File

@@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Emby.Naming.Audio;
using Emby.Naming.Common;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities.Audio;
@@ -85,6 +86,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
var albumParser = new AlbumParser(_namingOptions);
var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
@@ -100,6 +102,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
}
}
// If the folder is a multi-disc folder, then it is not an artist folder
if (albumParser.IsMultiPart(fileSystemInfo.FullName))
{
return;
}
// If we contain a music album assume we are an artist folder
if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
{

View File

@@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
IndexNumber = seasonParserResult.SeasonNumber,
SeriesId = series.Id,
SeriesName = series.Name
SeriesName = series.Name,
Path = seasonParserResult.IsSeasonFolder ? path : args.Parent.Path
};
if (!season.IndexNumber.HasValue || !seasonParserResult.IsSeasonFolder)
@@ -78,27 +79,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
}
}
if (season.IndexNumber.HasValue)
if (season.IndexNumber.HasValue && string.IsNullOrEmpty(season.Name))
{
var seasonNumber = season.IndexNumber.Value;
if (string.IsNullOrEmpty(season.Name))
{
var seasonNames = series.GetSeasonNames();
if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
{
season.Name = seasonName;
}
else
{
season.Name = seasonNumber == 0 ?
args.LibraryOptions.SeasonZeroDisplayName :
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameSeasonNumber"),
seasonNumber,
args.LibraryOptions.PreferredMetadataLanguage);
}
}
season.Name = seasonNumber == 0 ?
args.LibraryOptions.SeasonZeroDisplayName :
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameSeasonNumber"),
seasonNumber,
args.LibraryOptions.PreferredMetadataLanguage);
}
return season;

View File

@@ -8,6 +8,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@@ -69,7 +70,7 @@ public partial class AudioNormalizationTask : IScheduledTask
/// <inheritdoc />
public string Key => "AudioNormalization";
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
[GeneratedRegex(@"^\s+I:\s+(.*?)\s+LUFS")]
private static partial Regex LUFSRegex();
/// <inheritdoc />
@@ -179,16 +180,17 @@ public partial class AudioNormalizationTask : IScheduledTask
}
using var reader = process.StandardError;
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
MatchCollection split = LUFSRegex().Matches(output);
if (split.Count != 0)
await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
{
return float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
Match match = LUFSRegex().Match(line);
if (match.Success)
{
return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
}
}
_logger.LogError("Failed to find LUFS value in output:\n{Output}", output);
_logger.LogError("Failed to find LUFS value in output");
return null;
}
}

View File

@@ -1202,7 +1202,8 @@ namespace Emby.Server.Implementations.Session
new DtoOptions(false)
{
EnableImages = false
})
},
user.DisplayMissingEpisodes)
.Where(i => !i.IsVirtualItem)
.SkipWhile(i => !i.Id.Equals(episode.Id))
.ToList();

View File

@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using MediaBrowser.Common.Configuration;
using Microsoft.AspNetCore.Authorization;
@@ -24,24 +25,31 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
{
// Succeed if the startup wizard / first time setup is not complete
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
context.Succeed(requirement);
}
else if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
// Succeed if user is admin
else if (context.User.IsInRole(UserRoles.Administrator))
{
context.Fail();
}
else if (!requirement.RequireAdmin && context.User.IsInRole(UserRoles.Guest))
{
context.Fail();
}
else
{
// Any user-specific checks are handled in the DefaultAuthorizationHandler.
context.Succeed(requirement);
}
// Fail if admin is required and user is not admin
else if (requirement.RequireAdmin)
{
context.Fail();
}
// Succeed if admin is not required and user is not guest
else if (context.User.IsInRole(UserRoles.User))
{
context.Succeed(requirement);
}
// Any user-specific checks are handled in the DefaultAuthorizationHandler.
return Task.CompletedTask;
}
}

View File

@@ -290,17 +290,35 @@ public class ItemUpdateController : BaseJellyfinApiController
{
foreach (var season in rseries.Children.OfType<Season>())
{
season.OfficialRating = request.OfficialRating;
if (!season.LockedFields.Contains(MetadataField.OfficialRating))
{
season.OfficialRating = request.OfficialRating;
}
season.CustomRating = request.CustomRating;
season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
if (!season.LockedFields.Contains(MetadataField.Tags))
{
season.Tags = season.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
}
season.OnMetadataChanged();
await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
foreach (var ep in season.Children.OfType<Episode>())
{
ep.OfficialRating = request.OfficialRating;
if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
{
ep.OfficialRating = request.OfficialRating;
}
ep.CustomRating = request.CustomRating;
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
if (!ep.LockedFields.Contains(MetadataField.Tags))
{
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
}
ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
@@ -310,9 +328,18 @@ public class ItemUpdateController : BaseJellyfinApiController
{
foreach (var ep in season.Children.OfType<Episode>())
{
ep.OfficialRating = request.OfficialRating;
if (!ep.LockedFields.Contains(MetadataField.OfficialRating))
{
ep.OfficialRating = request.OfficialRating;
}
ep.CustomRating = request.CustomRating;
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
if (!ep.LockedFields.Contains(MetadataField.Tags))
{
ep.Tags = ep.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
}
ep.OnMetadataChanged();
await ep.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}
@@ -321,9 +348,18 @@ public class ItemUpdateController : BaseJellyfinApiController
{
foreach (BaseItem track in album.Children)
{
track.OfficialRating = request.OfficialRating;
if (!track.LockedFields.Contains(MetadataField.OfficialRating))
{
track.OfficialRating = request.OfficialRating;
}
track.CustomRating = request.CustomRating;
track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
if (!track.LockedFields.Contains(MetadataField.Tags))
{
track.Tags = track.Tags.Concat(addedTags).Except(removedTags).Distinct().ToArray();
}
track.OnMetadataChanged();
await track.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
}

View File

@@ -319,7 +319,7 @@ public class LibraryStructureController : BaseJellyfinApiController
public ActionResult UpdateLibraryOptions(
[FromBody] UpdateLibraryOptionsDto request)
{
var item = _libraryManager.GetItemById<CollectionFolder>(request.Id, User.GetUserId());
var item = _libraryManager.GetItemById<CollectionFolder>(request.Id);
if (item is null)
{
return NotFound();

View File

@@ -231,6 +231,7 @@ public class TvShowsController : BaseJellyfinApiController
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var shouldIncludeMissingEpisodes = (user is not null && user.DisplayMissingEpisodes) || User.GetIsApiKey();
if (seasonId.HasValue) // Season id was supplied. Get episodes by season id.
{
@@ -240,7 +241,7 @@ public class TvShowsController : BaseJellyfinApiController
return NotFound("No season exists with Id " + seasonId);
}
episodes = seasonItem.GetEpisodes(user, dtoOptions);
episodes = seasonItem.GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes);
}
else if (season.HasValue) // Season number was supplied. Get episodes by season number
{
@@ -256,7 +257,7 @@ public class TvShowsController : BaseJellyfinApiController
episodes = seasonItem is null ?
new List<BaseItem>()
: ((Season)seasonItem).GetEpisodes(user, dtoOptions);
: ((Season)seasonItem).GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes);
}
else // No season number or season id was supplied. Returning all episodes.
{
@@ -265,7 +266,7 @@ public class TvShowsController : BaseJellyfinApiController
return NotFound("Series not found");
}
episodes = series.GetEpisodes(user, dtoOptions).ToList();
episodes = series.GetEpisodes(user, dtoOptions, shouldIncludeMissingEpisodes).ToList();
}
// Filter after the fact in case the ui doesn't want them

View File

@@ -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(),

View File

@@ -142,6 +142,20 @@ public static class StreamingHelpers
}
else
{
// Enforce more restrictive transcoding profile for LiveTV due to compatability reasons
// Cap the MaxStreamingBitrate to 30Mbps, 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
streamingRequest.VideoBitRate = streamingRequest.VideoBitRate > 30000000 ? 30000000 : streamingRequest.VideoBitRate;
if (streamingRequest.SegmentContainer 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
streamingRequest.SegmentContainer = "ts";
streamingRequest.VideoCodec = "h264";
}
var liveStreamInfo = await mediaSourceManager.GetLiveStreamWithDirectStreamProvider(streamingRequest.LiveStreamId, cancellationToken).ConfigureAwait(false);
mediaSource = liveStreamInfo.Item1;
state.DirectStreamProvider = liveStreamInfo.Item2;

View File

@@ -18,7 +18,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Data</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.4</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -16,21 +16,28 @@ public static class ServiceCollectionExtensions
/// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
/// </summary>
/// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
/// <param name="disableSecondLevelCache">Whether second level cache disabled..</param>
/// <returns>The updated service collection.</returns>
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection, bool disableSecondLevelCache)
{
serviceCollection.AddEFSecondLevelCache(options =>
options.UseMemoryCacheProvider()
.CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
.UseCacheKeyPrefix("EF_")
// Don't cache null values. Remove this optional setting if it's not necessary.
.SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
if (!disableSecondLevelCache)
{
serviceCollection.AddEFSecondLevelCache(options =>
options.UseMemoryCacheProvider()
.CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
.UseCacheKeyPrefix("EF_")
// Don't cache null values. Remove this optional setting if it's not necessary.
.SkipCachingResults(result => result.Value is null or EFTableRows { RowsCount: 0 }));
}
serviceCollection.AddPooledDbContextFactory<JellyfinDbContext>((serviceProvider, opt) =>
{
var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}")
.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
var dbOpt = opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}");
if (!disableSecondLevelCache)
{
dbOpt.AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>());
}
});
return serviceCollection;

View File

@@ -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);
}

View File

@@ -85,6 +85,6 @@ public static class WebHostBuilderExtensions
logger.LogInformation("Kestrel listening to unix socket {SocketPath}", socketPath);
}
})
.UseStartup(_ => new Startup(appHost));
.UseStartup(_ => new Startup(appHost, startupConfig));
}
}

View File

@@ -44,7 +44,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.FixPlaylistOwner),
typeof(Routines.MigrateRatingLevels),
typeof(Routines.AddDefaultCastReceivers),
typeof(Routines.UpdateDefaultPluginRepository)
typeof(Routines.UpdateDefaultPluginRepository),
typeof(Routines.FixAudioData),
};
/// <summary>

View File

@@ -0,0 +1,104 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Migrations.Routines
{
/// <summary>
/// Fixes the data column of audio types to be deserializable.
/// </summary>
internal class FixAudioData : IMigrationRoutine
{
private const string DbFilename = "library.db";
private readonly ILogger<FixAudioData> _logger;
private readonly IServerApplicationPaths _applicationPaths;
private readonly IItemRepository _itemRepository;
public FixAudioData(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IItemRepository itemRepository)
{
_applicationPaths = applicationPaths;
_itemRepository = itemRepository;
_logger = loggerFactory.CreateLogger<FixAudioData>();
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{CF6FABC2-9FBE-4933-84A5-FFE52EF22A58}");
/// <inheritdoc/>
public string Name => "FixAudioData";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/>
public void Perform()
{
var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
// Back up the database before modifying any entries
for (int i = 1; ; i++)
{
var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
if (!File.Exists(bakPath))
{
try
{
File.Copy(dbPath, bakPath);
_logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
throw;
}
}
}
_logger.LogInformation("Backfilling audio lyrics data to database.");
var startIndex = 0;
var records = _itemRepository.GetCount(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Audio],
});
while (startIndex < records)
{
var results = _itemRepository.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Audio],
StartIndex = startIndex,
Limit = 100,
SkipDeserialization = true
})
.Cast<Audio>()
.ToList();
foreach (var audio in results)
{
var lyricMediaStreams = audio.GetMediaStreams().Where(s => s.Type == MediaStreamType.Lyric).Select(s => s.Path).ToList();
if (lyricMediaStreams.Count > 0)
{
audio.HasLyrics = true;
audio.LyricFiles = lyricMediaStreams;
}
}
_itemRepository.SaveItems(results, CancellationToken.None);
startIndex += 100;
}
}
}
}

View File

@@ -40,15 +40,18 @@ namespace Jellyfin.Server
{
private readonly CoreAppHost _serverApplicationHost;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IConfiguration _startupConfig;
/// <summary>
/// Initializes a new instance of the <see cref="Startup" /> class.
/// </summary>
/// <param name="appHost">The server application host.</param>
public Startup(CoreAppHost appHost)
/// <param name="startupConfig">The server startupConfig.</param>
public Startup(CoreAppHost appHost, IConfiguration startupConfig)
{
_serverApplicationHost = appHost;
_serverConfigurationManager = appHost.ConfigurationManager;
_startupConfig = startupConfig;
}
/// <summary>
@@ -67,7 +70,7 @@ namespace Jellyfin.Server
// TODO remove once this is fixed upstream https://github.com/dotnet/aspnetcore/issues/34371
services.AddSingleton<IActionResultExecutor<PhysicalFileResult>, SymlinkFollowingPhysicalFileResultExecutor>();
services.AddJellyfinApi(_serverApplicationHost.GetApiPluginAssemblies(), _serverConfigurationManager.GetNetworkConfiguration());
services.AddJellyfinDbContext();
services.AddJellyfinDbContext(_startupConfig.GetSqliteSecondLevelCacheDisabled());
services.AddJellyfinApiSwagger();
// configure custom legacy authentication

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.4</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -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>

View File

@@ -751,9 +751,6 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public virtual bool SupportsAncestors => true;
[JsonIgnore]
public virtual bool StopRefreshIfLocalMetadataFound => true;
[JsonIgnore]
protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;

View File

@@ -51,6 +51,7 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>();
VideoTypes = Array.Empty<VideoType>();
Years = Array.Empty<int>();
SkipDeserialization = false;
}
public InternalItemsQuery(User? user)
@@ -358,6 +359,8 @@ namespace MediaBrowser.Controller.Entities
public string? SeriesTimerId { get; set; }
public bool SkipDeserialization { get; set; }
public void SetUser(User user)
{
MaxParentalRating = user.MaxParentalAgeRating;

View File

@@ -45,9 +45,6 @@ namespace MediaBrowser.Controller.Entities.Movies
set => TmdbCollectionName = value;
}
[JsonIgnore]
public override bool StopRefreshIfLocalMetadataFound => false;
public override double GetDefaultPrimaryImageAspectRatio()
{
// hack for tv plugins

View File

@@ -159,7 +159,7 @@ namespace MediaBrowser.Controller.Entities.TV
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
var items = GetEpisodes(user, query.DtoOptions).Where(filter);
var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
return PostFilterAndSort(items, query, false);
}
@@ -169,30 +169,31 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary>
/// <param name="user">The user.</param>
/// <param name="options">The options to use.</param>
/// <param name="shouldIncludeMissingEpisodes">If missing episodes should be included.</param>
/// <returns>Set of episodes.</returns>
public List<BaseItem> GetEpisodes(User user, DtoOptions options)
public List<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
return GetEpisodes(Series, user, options);
return GetEpisodes(Series, user, options, shouldIncludeMissingEpisodes);
}
public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options)
public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
return GetEpisodes(series, user, null, options);
return GetEpisodes(series, user, null, options, shouldIncludeMissingEpisodes);
}
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options)
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options);
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
}
public List<BaseItem> GetEpisodes()
{
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true));
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
}
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
{
return GetEpisodes(user, new DtoOptions(true));
return GetEpisodes(user, new DtoOptions(true), true);
}
protected override bool GetBlockUnratedValue(User user)

View File

@@ -25,12 +25,9 @@ namespace MediaBrowser.Controller.Entities.TV
/// </summary>
public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer
{
private readonly Dictionary<int, string> _seasonNames;
public Series()
{
AirDays = Array.Empty<DayOfWeek>();
_seasonNames = new Dictionary<int, string>();
}
public DayOfWeek[] AirDays { get; set; }
@@ -72,9 +69,6 @@ namespace MediaBrowser.Controller.Entities.TV
/// <value>The status.</value>
public SeriesStatus? Status { get; set; }
[JsonIgnore]
public override bool StopRefreshIfLocalMetadataFound => false;
public override double GetDefaultPrimaryImageAspectRatio()
{
double value = 2;
@@ -212,26 +206,6 @@ namespace MediaBrowser.Controller.Entities.TV
return LibraryManager.GetItemList(query);
}
public Dictionary<int, string> GetSeasonNames()
{
var newSeasons = Children.OfType<Season>()
.Where(s => s.IndexNumber.HasValue)
.Where(s => !_seasonNames.ContainsKey(s.IndexNumber.Value))
.DistinctBy(s => s.IndexNumber);
foreach (var season in newSeasons)
{
SetSeasonName(season.IndexNumber.Value, season.Name);
}
return _seasonNames;
}
public void SetSeasonName(int index, string name)
{
_seasonNames[index] = name;
}
private void SetSeasonQueryOptions(InternalItemsQuery query, User user)
{
var seriesKey = GetUniqueSeriesKey(this);
@@ -276,7 +250,7 @@ namespace MediaBrowser.Controller.Entities.TV
return LibraryManager.GetItemsResult(query);
}
public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options)
public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
var seriesKey = GetUniqueSeriesKey(this);
@@ -286,10 +260,10 @@ namespace MediaBrowser.Controller.Entities.TV
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
DtoOptions = options,
};
if (user is null || !user.DisplayMissingEpisodes)
if (!shouldIncludeMissingEpisodes)
{
query.IsMissing = false;
}
@@ -299,7 +273,7 @@ namespace MediaBrowser.Controller.Entities.TV
var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
var allEpisodes = allItems.OfType<Season>()
.SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options))
.SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes))
.Reverse();
// Specials could appear twice based on above - once in season 0, once in the aired season
@@ -311,8 +285,7 @@ namespace MediaBrowser.Controller.Entities.TV
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
{
// Refresh bottom up, children first, then the boxset
// By then hopefully the movies within will have Tmdb collection values
// Refresh bottom up, seasons and episodes first, then the series
var items = GetRecursiveChildren();
var totalItems = items.Count;
@@ -375,7 +348,7 @@ namespace MediaBrowser.Controller.Entities.TV
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
}
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options)
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
@@ -392,24 +365,22 @@ namespace MediaBrowser.Controller.Entities.TV
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
DtoOptions = options
};
if (user is not null)
if (!shouldIncludeMissingEpisodes)
{
if (!user.DisplayMissingEpisodes)
{
query.IsMissing = false;
}
query.IsMissing = false;
}
var allItems = LibraryManager.GetItemList(query);
return GetSeasonEpisodes(parentSeason, user, allItems, options);
return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
}
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options)
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
{
if (allSeriesEpisodes is null)
{
return GetSeasonEpisodes(parentSeason, user, options);
return GetSeasonEpisodes(parentSeason, user, options, shouldIncludeMissingEpisodes);
}
var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);

View File

@@ -23,9 +23,6 @@ namespace MediaBrowser.Controller.Entities
TrailerTypes = Array.Empty<TrailerType>();
}
[JsonIgnore]
public override bool StopRefreshIfLocalMetadataFound => false;
public TrailerType[] TrailerTypes { get; set; }
public override double GetDefaultPrimaryImageAspectRatio()

View File

@@ -64,6 +64,11 @@ namespace MediaBrowser.Controller.Extensions
/// </summary>
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
/// <summary>
/// Disable second level cache of sqlite.
/// </summary>
public const string SqliteDisableSecondLevelCacheKey = "sqlite:disableSecondLevelCache";
/// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary>
@@ -128,5 +133,15 @@ namespace MediaBrowser.Controller.Extensions
/// <returns>The sqlite cache size.</returns>
public static int? GetSqliteCacheSize(this IConfiguration configuration)
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
/// <summary>
/// Gets whether second level cache disabled from the <see cref="IConfiguration" />.
/// </summary>
/// <param name="configuration">The configuration to read the setting from.</param>
/// <returns>Whether second level cache disabled.</returns>
public static bool GetSqliteSecondLevelCacheDisabled(this IConfiguration configuration)
{
return configuration.GetValue<bool>(SqliteDisableSecondLevelCacheKey);
}
}
}

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Controller</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.4</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -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));
@@ -1197,13 +1189,14 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
_mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
arg.Append(" -f concat -safe 0 -i ")
.Append(tmpConcatPath);
arg.Append(" -f concat -safe 0 -i \"")
.Append(tmpConcatPath)
.Append("\" ");
}
else
{
arg.Append(" -i ")
.Append(GetInputPathArgument(state));
.Append(_mediaEncoder.GetInputPathArgument(state));
}
// sub2video for external graphical subtitles
@@ -2083,6 +2076,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
@@ -2316,7 +2321,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (request.VideoBitRate.HasValue
&& (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value))
{
return false;
// For LiveTV that has no bitrate, let's try copy if other conditions are met
if (string.IsNullOrWhiteSpace(request.LiveStreamId) || videoStream.BitRate.HasValue)
{
return false;
}
}
var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec);
@@ -2629,10 +2638,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 +2653,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 +3169,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 +3200,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 +3220,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 +3232,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 +3245,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 +3258,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 +4306,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 +4316,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// map from vaapi to qsv.
mainFilters.Add("hwmap=derive_device=qsv");
mainFilters.Add("format=qsv");
}
}
@@ -4474,7 +4491,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);

View File

@@ -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>

View File

@@ -11,6 +11,8 @@ namespace MediaBrowser.Controller.Providers
public ItemInfo(BaseItem item)
{
Path = item.Path;
ParentId = item.ParentId;
IndexNumber = item.IndexNumber;
ContainingFolderPath = item.ContainingFolderPath;
IsInMixedFolder = item.IsInMixedFolder;
@@ -27,6 +29,10 @@ namespace MediaBrowser.Controller.Providers
public string Path { get; set; }
public Guid ParentId { get; set; }
public int? IndexNumber { get; set; }
public string ContainingFolderPath { get; set; }
public VideoType VideoType { get; set; }

View File

@@ -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);
}
}
}
}

View File

@@ -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
{
@@ -458,9 +456,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
extraArgs += " -probesize " + ffmpegProbeSize;
}
if (request.MediaSource.RequiredHttpHeaders.TryGetValue("user_agent", out var userAgent))
if (request.MediaSource.RequiredHttpHeaders.TryGetValue("User-Agent", out var userAgent))
{
extraArgs += " -user_agent " + userAgent;
extraArgs += $" -user_agent \"{userAgent}\"";
}
if (request.MediaSource.Protocol == MediaProtocol.Rtsp)
@@ -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)
{

View File

@@ -8,7 +8,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Model</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.4</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -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.

View File

@@ -121,7 +121,8 @@ namespace MediaBrowser.Providers.Manager
var metadataResult = new MetadataResult<TItemType>
{
Item = itemOfType
Item = itemOfType,
People = LibraryManager.GetPeople(item)
};
bool hasRefreshedMetadata = true;
@@ -164,7 +165,7 @@ namespace MediaBrowser.Providers.Manager
}
// Next run remote image providers, but only if local image providers didn't throw an exception
if (!localImagesFailed && refreshOptions.ImageRefreshMode != MetadataRefreshMode.ValidationOnly)
if (!localImagesFailed && refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly)
{
var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
@@ -242,7 +243,7 @@ namespace MediaBrowser.Providers.Manager
protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
{
if (result.Item.SupportsPeople && result.People is not null)
if (result.Item.SupportsPeople)
{
var baseItem = result.Item;
@@ -398,7 +399,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)
@@ -654,26 +656,19 @@ namespace MediaBrowser.Providers.Manager
await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
}
if (item.IsLocked)
{
return refreshResult;
}
var temp = new MetadataResult<TItemType>
{
Item = CreateNew()
};
temp.Item.Path = item.Path;
temp.Item.Id = item.Id;
// If replacing all metadata, run internet providers first
if (options.ReplaceAllMetadata)
{
var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
.ConfigureAwait(false);
refreshResult.UpdateType |= remoteResult.UpdateType;
refreshResult.ErrorMessage = remoteResult.ErrorMessage;
refreshResult.Failures += remoteResult.Failures;
}
var hasLocalMetadata = false;
var foundImageTypes = new List<ImageType>();
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
{
var providerName = provider.GetType().Name;
@@ -719,15 +714,9 @@ namespace MediaBrowser.Providers.Manager
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
}
MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true);
MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true);
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
// Only one local provider allowed per item
if (item.IsLocked || localItem.Item.IsLocked || IsFullLocalMetadata(localItem.Item))
{
hasLocalMetadata = true;
}
break;
}
@@ -746,10 +735,10 @@ namespace MediaBrowser.Providers.Manager
}
}
// Local metadata is king - if any is found don't run remote providers
if (!options.ReplaceAllMetadata && (!hasLocalMetadata || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || !item.StopRefreshIfLocalMetadataFound))
var isLocalLocked = temp.Item.IsLocked;
if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly))
{
var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken)
.ConfigureAwait(false);
refreshResult.UpdateType |= remoteResult.UpdateType;
@@ -761,19 +750,20 @@ namespace MediaBrowser.Providers.Manager
{
if (refreshResult.UpdateType > ItemUpdateType.None)
{
if (hasLocalMetadata)
if (!options.RemoveOldMetadata)
{
// Add existing metadata to provider result if it does not exist there
MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
}
if (isLocalLocked)
{
MergeData(temp, metadata, item.LockedFields, true, true);
}
else
{
if (!options.RemoveOldMetadata)
{
MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
}
// Will always replace all metadata when Scan for new and updated files is used. Else, follow the options.
MergeData(temp, metadata, item.LockedFields, options.MetadataRefreshMode == MetadataRefreshMode.Default || options.ReplaceAllMetadata, false);
var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata;
MergeData(temp, metadata, item.LockedFields, shouldReplace, false);
}
}
}
@@ -786,16 +776,6 @@ namespace MediaBrowser.Providers.Manager
return refreshResult;
}
protected virtual bool IsFullLocalMetadata(TItemType item)
{
if (string.IsNullOrWhiteSpace(item.Name))
{
return false;
}
return true;
}
private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken)
{
Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
@@ -820,7 +800,7 @@ namespace MediaBrowser.Providers.Manager
return new TItemType();
}
private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken)
private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool replaceData, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken)
{
var refreshResult = new RefreshResult();
@@ -845,7 +825,7 @@ namespace MediaBrowser.Providers.Manager
{
result.Provider = provider.Name;
MergeData(result, temp, Array.Empty<MetadataField>(), false, false);
MergeData(result, temp, Array.Empty<MetadataField>(), replaceData, false);
MergeNewData(temp.Item, id);
refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
@@ -948,11 +928,7 @@ namespace MediaBrowser.Providers.Manager
if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
{
// Safeguard against incoming data having an empty name
if (!string.IsNullOrWhiteSpace(source.OriginalTitle))
{
target.OriginalTitle = source.OriginalTitle;
}
target.OriginalTitle = source.OriginalTitle;
}
if (replaceData || !target.CommunityRating.HasValue)
@@ -1015,7 +991,7 @@ namespace MediaBrowser.Providers.Manager
{
targetResult.People = sourceResult.People;
}
else if (targetResult.People is not null && sourceResult.People is not null)
else if (sourceResult.People is not null && sourceResult.People.Count >= 0)
{
MergePeople(sourceResult.People, targetResult.People);
}
@@ -1048,6 +1024,10 @@ namespace MediaBrowser.Providers.Manager
{
target.Studios = source.Studios;
}
else
{
target.Studios = target.Studios.Concat(source.Studios).Distinct().ToArray();
}
}
if (!lockedFields.Contains(MetadataField.Tags))
@@ -1056,6 +1036,10 @@ namespace MediaBrowser.Providers.Manager
{
target.Tags = source.Tags;
}
else
{
target.Tags = target.Tags.Concat(source.Tags).Distinct().ToArray();
}
}
if (!lockedFields.Contains(MetadataField.ProductionLocations))
@@ -1064,6 +1048,10 @@ namespace MediaBrowser.Providers.Manager
{
target.ProductionLocations = source.ProductionLocations;
}
else
{
target.Tags = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray();
}
}
foreach (var id in source.ProviderIds)
@@ -1081,17 +1069,28 @@ namespace MediaBrowser.Providers.Manager
}
}
if (replaceData || !target.CriticRating.HasValue)
{
target.CriticRating = source.CriticRating;
}
if (replaceData || target.RemoteTrailers.Count == 0)
{
target.RemoteTrailers = source.RemoteTrailers;
}
else
{
target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).Distinct().ToArray();
}
MergeAlbumArtist(source, target, replaceData);
MergeCriticRating(source, target, replaceData);
MergeTrailers(source, target, replaceData);
MergeVideoInfo(source, target, replaceData);
MergeDisplayOrder(source, target, replaceData);
if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
{
var forcedSortName = source.ForcedSortName;
if (!string.IsNullOrWhiteSpace(forcedSortName))
if (!string.IsNullOrEmpty(forcedSortName))
{
target.ForcedSortName = forcedSortName;
}
@@ -1099,22 +1098,44 @@ namespace MediaBrowser.Providers.Manager
if (mergeMetadataSettings)
{
target.LockedFields = source.LockedFields;
target.IsLocked = source.IsLocked;
if (replaceData || !target.IsLocked)
{
target.IsLocked = target.IsLocked || source.IsLocked;
}
if (target.LockedFields.Length == 0)
{
target.LockedFields = source.LockedFields;
}
else
{
target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
}
// Grab the value if it's there, but if not then don't overwrite with the default
if (source.DateCreated != default)
{
target.DateCreated = source.DateCreated;
}
target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
{
target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
}
if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage))
{
target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
}
}
}
private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
{
if (target is null)
{
target = new List<PersonInfo>();
}
foreach (var person in target)
{
var normalizedName = person.Name.RemoveDiacritics();
@@ -1143,7 +1164,6 @@ namespace MediaBrowser.Providers.Manager
if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
{
var displayOrder = sourceHasDisplayOrder.DisplayOrder;
if (!string.IsNullOrWhiteSpace(displayOrder))
{
targetHasDisplayOrder.DisplayOrder = displayOrder;
@@ -1161,22 +1181,10 @@ namespace MediaBrowser.Providers.Manager
{
targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
}
}
}
private static void MergeCriticRating(BaseItem source, BaseItem target, bool replaceData)
{
if (replaceData || !target.CriticRating.HasValue)
{
target.CriticRating = source.CriticRating;
}
}
private static void MergeTrailers(BaseItem source, BaseItem target, bool replaceData)
{
if (replaceData || target.RemoteTrailers.Count == 0)
{
target.RemoteTrailers = source.RemoteTrailers;
else if (sourceHasAlbumArtist.AlbumArtists.Count >= 0)
{
targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct().ToArray();
}
}
}
@@ -1184,7 +1192,7 @@ namespace MediaBrowser.Providers.Manager
{
if (source is Video sourceCast && target is Video targetCast)
{
if (replaceData || targetCast.Video3DFormat is null)
if (replaceData || !targetCast.Video3DFormat.HasValue)
{
targetCast.Video3DFormat = sourceCast.Video3DFormat;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -151,197 +152,211 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="tryExtractEmbeddedLyrics">Whether to extract embedded lyrics to lrc file. </param>
private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics)
{
using var file = TagLib.File.Create(audio.Path);
var tagTypes = file.TagTypesOnDisk;
Tag? tags = null;
try
{
using var file = TagLib.File.Create(audio.Path);
var tagTypes = file.TagTypesOnDisk;
if (tagTypes.HasFlag(TagTypes.Id3v2))
{
tags = file.GetTag(TagTypes.Id3v2);
}
else if (tagTypes.HasFlag(TagTypes.Ape))
{
tags = file.GetTag(TagTypes.Ape);
}
else if (tagTypes.HasFlag(TagTypes.FlacMetadata))
{
tags = file.GetTag(TagTypes.FlacMetadata);
}
else if (tagTypes.HasFlag(TagTypes.Apple))
{
tags = file.GetTag(TagTypes.Apple);
}
else if (tagTypes.HasFlag(TagTypes.Xiph))
{
tags = file.GetTag(TagTypes.Xiph);
}
else if (tagTypes.HasFlag(TagTypes.AudibleMetadata))
{
tags = file.GetTag(TagTypes.AudibleMetadata);
}
else if (tagTypes.HasFlag(TagTypes.Id3v1))
{
tags = file.GetTag(TagTypes.Id3v1);
}
if (tags is not null)
{
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
if (tagTypes.HasFlag(TagTypes.Id3v2))
{
var people = new List<PersonInfo>();
var albumArtists = tags.AlbumArtists;
foreach (var albumArtist in albumArtists)
tags = file.GetTag(TagTypes.Id3v2);
}
else if (tagTypes.HasFlag(TagTypes.Ape))
{
tags = file.GetTag(TagTypes.Ape);
}
else if (tagTypes.HasFlag(TagTypes.FlacMetadata))
{
tags = file.GetTag(TagTypes.FlacMetadata);
}
else if (tagTypes.HasFlag(TagTypes.Apple))
{
tags = file.GetTag(TagTypes.Apple);
}
else if (tagTypes.HasFlag(TagTypes.Xiph))
{
tags = file.GetTag(TagTypes.Xiph);
}
else if (tagTypes.HasFlag(TagTypes.AudibleMetadata))
{
tags = file.GetTag(TagTypes.AudibleMetadata);
}
else if (tagTypes.HasFlag(TagTypes.Id3v1))
{
tags = file.GetTag(TagTypes.Id3v1);
}
}
catch (Exception e)
{
_logger.LogWarning(e, "TagLib-Sharp does not support this audio");
}
tags ??= new TagLib.Id3v2.Tag();
tags.AlbumArtists ??= mediaInfo.AlbumArtists;
tags.Album ??= mediaInfo.Album;
tags.Title ??= mediaInfo.Name;
tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
tags.Performers ??= mediaInfo.Artists;
tags.Genres ??= mediaInfo.Genres;
tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track;
tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
var people = new List<PersonInfo>();
var albumArtists = tags.AlbumArtists;
foreach (var albumArtist in albumArtists)
{
if (!string.IsNullOrEmpty(albumArtist))
{
if (!string.IsNullOrEmpty(albumArtist))
PeopleHelper.AddPerson(people, new PersonInfo
{
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = albumArtist,
Type = PersonKind.AlbumArtist
});
}
Name = albumArtist,
Type = PersonKind.AlbumArtist
});
}
}
var performers = tags.Performers;
foreach (var performer in performers)
var performers = tags.Performers;
foreach (var performer in performers)
{
if (!string.IsNullOrEmpty(performer))
{
if (!string.IsNullOrEmpty(performer))
PeopleHelper.AddPerson(people, new PersonInfo
{
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = performer,
Type = PersonKind.Artist
});
}
Name = performer,
Type = PersonKind.Artist
});
}
}
foreach (var composer in tags.Composers)
foreach (var composer in tags.Composers)
{
if (!string.IsNullOrEmpty(composer))
{
if (!string.IsNullOrEmpty(composer))
PeopleHelper.AddPerson(people, new PersonInfo
{
PeopleHelper.AddPerson(people, new PersonInfo
{
Name = composer,
Type = PersonKind.Composer
});
}
Name = composer,
Type = PersonKind.Composer
});
}
}
_libraryManager.UpdatePeople(audio, people);
_libraryManager.UpdatePeople(audio, people);
if (options.ReplaceAllMetadata && performers.Length != 0)
if (options.ReplaceAllMetadata && performers.Length != 0)
{
audio.Artists = performers;
}
else if (!options.ReplaceAllMetadata
&& (audio.Artists is null || audio.Artists.Count == 0))
{
audio.Artists = performers;
}
if (albumArtists.Length == 0)
{
// Album artists not provided, fall back to performers (artists).
albumArtists = performers;
}
if (options.ReplaceAllMetadata && albumArtists.Length != 0)
{
audio.AlbumArtists = albumArtists;
}
else if (!options.ReplaceAllMetadata
&& (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0))
{
audio.AlbumArtists = albumArtists;
}
}
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title))
{
audio.Name = tags.Title;
}
if (options.ReplaceAllMetadata)
{
audio.Album = tags.Album;
audio.IndexNumber = Convert.ToInt32(tags.Track);
audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
}
else
{
audio.Album ??= tags.Album;
audio.IndexNumber ??= Convert.ToInt32(tags.Track);
audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc);
}
if (tags.Year != 0)
{
var year = Convert.ToInt32(tags.Year);
audio.ProductionYear = year;
if (!audio.PremiereDate.HasValue)
{
try
{
audio.Artists = performers;
audio.PremiereDate = new DateTime(year, 01, 01);
}
else if (!options.ReplaceAllMetadata
&& (audio.Artists is null || audio.Artists.Count == 0))
catch (ArgumentOutOfRangeException ex)
{
audio.Artists = performers;
}
if (albumArtists.Length == 0)
{
// Album artists not provided, fall back to performers (artists).
albumArtists = performers;
}
if (options.ReplaceAllMetadata && albumArtists.Length != 0)
{
audio.AlbumArtists = albumArtists;
}
else if (!options.ReplaceAllMetadata
&& (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0))
{
audio.AlbumArtists = albumArtists;
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, tags.Year);
}
}
}
if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title))
{
audio.Name = tags.Title;
}
if (!audio.LockedFields.Contains(MetadataField.Genres))
{
audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
: audio.Genres;
}
if (options.ReplaceAllMetadata)
{
audio.Album = tags.Album;
audio.IndexNumber = Convert.ToInt32(tags.Track);
audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
}
else
{
audio.Album ??= tags.Album;
audio.IndexNumber ??= Convert.ToInt32(tags.Track);
audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc);
}
if (!double.IsNaN(tags.ReplayGainTrackGain))
{
audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
}
if (tags.Year != 0)
{
var year = Convert.ToInt32(tags.Year);
audio.ProductionYear = year;
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
{
audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId);
}
if (!audio.PremiereDate.HasValue)
{
try
{
audio.PremiereDate = new DateTime(year, 01, 01);
}
catch (ArgumentOutOfRangeException ex)
{
_logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year.", audio.Path, tags.Year);
}
}
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
{
audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId);
}
if (!audio.LockedFields.Contains(MetadataField.Genres))
{
audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
: audio.Genres;
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
{
audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId);
}
if (!double.IsNaN(tags.ReplayGainTrackGain))
{
audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
{
audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId);
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
{
// Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`.
// See https://github.com/mono/taglib-sharp/issues/304
var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack);
if (trackMbId is not null)
{
audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId);
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
}
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
{
audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId);
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
{
audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId);
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
{
audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId);
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
{
// Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`.
// See https://github.com/mono/taglib-sharp/issues/304
var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack);
if (trackMbId is not null)
{
audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
}
}
// Save extracted lyrics if they exist,
// and if the audio doesn't yet have lyrics.
if (!string.IsNullOrWhiteSpace(tags.Lyrics)
&& tryExtractEmbeddedLyrics)
{
await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false);
}
// Save extracted lyrics if they exist,
// and if the audio doesn't yet have lyrics.
if (!string.IsNullOrWhiteSpace(tags.Lyrics)
&& tryExtractEmbeddedLyrics)
{
await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false);
}
}

View File

@@ -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;

View File

@@ -23,22 +23,6 @@ namespace MediaBrowser.Providers.Movies
{
}
/// <inheritdoc />
protected override bool IsFullLocalMetadata(Movie item)
{
if (string.IsNullOrWhiteSpace(item.Overview))
{
return false;
}
if (!item.ProductionYear.HasValue)
{
return false;
}
return base.IsFullLocalMetadata(item);
}
/// <inheritdoc />
protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{

View File

@@ -1,5 +1,6 @@
#pragma warning disable CS1591
using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -23,22 +24,6 @@ namespace MediaBrowser.Providers.Movies
{
}
/// <inheritdoc />
protected override bool IsFullLocalMetadata(Trailer item)
{
if (string.IsNullOrWhiteSpace(item.Overview))
{
return false;
}
if (!item.ProductionYear.HasValue)
{
return false;
}
return base.IsFullLocalMetadata(item);
}
/// <inheritdoc />
protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
@@ -48,6 +33,10 @@ namespace MediaBrowser.Providers.Movies
{
target.Item.TrailerTypes = source.Item.TrailerTypes;
}
else
{
target.Item.TrailerTypes = target.Item.TrailerTypes.Concat(source.Item.TrailerTypes).Distinct().ToArray();
}
}
}
}

View File

@@ -225,6 +225,10 @@ namespace MediaBrowser.Providers.Music
{
targetItem.Artists = sourceItem.Artists;
}
else
{
targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray();
}
if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist)))
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -60,6 +61,10 @@ namespace MediaBrowser.Providers.Music
{
targetItem.Artists = sourceItem.Artists;
}
else
{
targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray();
}
if (replaceData || string.IsNullOrEmpty(targetItem.Album))
{

View File

@@ -1,5 +1,6 @@
#pragma warning disable CS1591
using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -45,6 +46,10 @@ namespace MediaBrowser.Providers.Music
{
targetItem.Artists = sourceItem.Artists;
}
else
{
targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray();
}
}
}
}

View File

@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -49,8 +50,24 @@ namespace MediaBrowser.Providers.Playlists
if (mergeMetadataSettings)
{
targetItem.PlaylistMediaType = sourceItem.PlaylistMediaType;
targetItem.LinkedChildren = sourceItem.LinkedChildren;
targetItem.Shares = sourceItem.Shares;
if (replaceData || targetItem.LinkedChildren.Length == 0)
{
targetItem.LinkedChildren = sourceItem.LinkedChildren;
}
else
{
targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
}
if (replaceData || targetItem.Shares.Count == 0)
{
targetItem.Shares = sourceItem.Shares;
}
else
{
targetItem.Shares = sourceItem.Shares.Concat(targetItem.Shares).DistinctBy(s => s.UserId).ToArray();
}
}
}
}

View File

@@ -62,23 +62,7 @@ namespace MediaBrowser.Providers.TV
RemoveObsoleteEpisodes(item);
RemoveObsoleteSeasons(item);
await UpdateAndCreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
protected override bool IsFullLocalMetadata(Series item)
{
if (string.IsNullOrWhiteSpace(item.Overview))
{
return false;
}
if (!item.ProductionYear.HasValue)
{
return false;
}
return base.IsFullLocalMetadata(item);
await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -88,24 +72,6 @@ namespace MediaBrowser.Providers.TV
var sourceItem = source.Item;
var targetItem = target.Item;
var sourceSeasonNames = sourceItem.GetSeasonNames();
var targetSeasonNames = targetItem.GetSeasonNames();
if (replaceData)
{
foreach (var (number, name) in sourceSeasonNames)
{
targetItem.SetSeasonName(number, name);
}
}
else
{
var newSeasons = sourceSeasonNames.Where(s => !targetSeasonNames.ContainsKey(s.Key));
foreach (var (number, name) in newSeasons)
{
targetItem.SetSeasonName(number, name);
}
}
if (replaceData || string.IsNullOrEmpty(targetItem.AirTime))
{
@@ -162,7 +128,7 @@ namespace MediaBrowser.Providers.TV
private void RemoveObsoleteEpisodes(Series series)
{
var episodes = series.GetEpisodes(null, new DtoOptions()).OfType<Episode>().ToList();
var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList();
var numberOfEpisodes = episodes.Count;
// TODO: O(n^2), but can it be done faster without overcomplicating it?
for (var i = 0; i < numberOfEpisodes; i++)
@@ -218,14 +184,12 @@ namespace MediaBrowser.Providers.TV
/// <summary>
/// Creates seasons for all episodes if they don't exist.
/// If no season number can be determined, a dummy season will be created.
/// Updates seasons names.
/// </summary>
/// <param name="series">The series.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The async task.</returns>
private async Task UpdateAndCreateSeasonsAsync(Series series, CancellationToken cancellationToken)
private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken)
{
var seasonNames = series.GetSeasonNames();
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
var seasons = seriesChildren.OfType<Season>().ToList();
var uniqueSeasonNumbers = seriesChildren
@@ -237,23 +201,12 @@ namespace MediaBrowser.Providers.TV
foreach (var seasonNumber in uniqueSeasonNumbers)
{
// Null season numbers will have a 'dummy' season created because seasons are always required.
var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber);
if (!seasonNumber.HasValue || !seasonNames.TryGetValue(seasonNumber.Value, out var seasonName))
{
seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
}
if (existingSeason is null)
if (!seasons.Any(i => i.IndexNumber == seasonNumber))
{
var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber);
var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false);
series.AddChild(season);
}
else if (!existingSeason.LockedFields.Contains(MetadataField.Name) && !string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal))
{
existingSeason.Name = seasonName;
await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
}
}
}

View File

@@ -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;
}

View File

@@ -100,19 +100,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
// Season names are processed by SeriesNfoSeasonParser
case "namedseason":
{
var parsed = int.TryParse(reader.GetAttribute("number"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber);
var name = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(name) && parsed)
{
item.SetSeasonName(seasonNumber, name);
}
break;
}
reader.Skip();
break;
default:
base.FetchDataFromXmlNode(reader, itemResult);
break;

View File

@@ -0,0 +1,60 @@
using System.Globalization;
using System.Xml;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.XbmcMetadata.Parsers
{
/// <summary>
/// NFO parser for seasons based on series NFO.
/// </summary>
public class SeriesNfoSeasonParser : BaseNfoParser<Season>
{
/// <summary>
/// Initializes a new instance of the <see cref="SeriesNfoSeasonParser"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
public SeriesNfoSeasonParser(
ILogger logger,
IConfigurationManager config,
IProviderManager providerManager,
IUserManager userManager,
IUserDataManager userDataManager,
IDirectoryService directoryService)
: base(logger, config, providerManager, userManager, userDataManager, directoryService)
{
}
/// <inheritdoc />
protected override bool SupportsUrlAfterClosingXmlTag => true;
/// <inheritdoc />
protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Season> itemResult)
{
var item = itemResult.Item;
if (reader.Name == "namedseason")
{
var parsed = int.TryParse(reader.GetAttribute("number"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber);
var name = reader.ReadElementContentAsString();
if (parsed && !string.IsNullOrWhiteSpace(name) && item.IndexNumber.HasValue && seasonNumber == item.IndexNumber.Value)
{
item.Name = name;
}
}
else
{
reader.Skip();
}
}
}
}

View File

@@ -42,7 +42,10 @@ namespace MediaBrowser.XbmcMetadata.Providers
try
{
result.Item = new T();
result.Item = new T
{
IndexNumber = info.IndexNumber
};
Fetch(result, path, cancellationToken);
result.HasMetadata = true;

View File

@@ -0,0 +1,89 @@
using System.IO;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.XbmcMetadata.Parsers;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.XbmcMetadata.Providers
{
/// <summary>
/// NFO provider for seasons based on series NFO.
/// </summary>
public class SeriesNfoSeasonProvider : BaseNfoProvider<Season>
{
private readonly ILogger<SeriesNfoSeasonProvider> _logger;
private readonly IConfigurationManager _config;
private readonly IProviderManager _providerManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly IDirectoryService _directoryService;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="SeriesNfoSeasonProvider"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger{SeasonFromSeriesNfoProvider}"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="directoryService">Instance of the <see cref="IDirectoryService"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public SeriesNfoSeasonProvider(
ILogger<SeriesNfoSeasonProvider> logger,
IFileSystem fileSystem,
IConfigurationManager config,
IProviderManager providerManager,
IUserManager userManager,
IUserDataManager userDataManager,
IDirectoryService directoryService,
ILibraryManager libraryManager)
: base(fileSystem)
{
_logger = logger;
_config = config;
_providerManager = providerManager;
_userManager = userManager;
_userDataManager = userDataManager;
_directoryService = directoryService;
_libraryManager = libraryManager;
}
/// <inheritdoc />
protected override void Fetch(MetadataResult<Season> result, string path, CancellationToken cancellationToken)
{
new SeriesNfoSeasonParser(_logger, _config, _providerManager, _userManager, _userDataManager, _directoryService).Fetch(result, path, cancellationToken);
}
/// <inheritdoc />
protected override FileSystemMetadata? GetXmlFile(ItemInfo info, IDirectoryService directoryService)
{
var seasonPath = info.Path;
if (seasonPath is not null)
{
var path = Path.Combine(seasonPath, "tvshow.nfo");
if (Path.Exists(path))
{
return directoryService.GetFile(path);
}
}
var seriesPath = _libraryManager.GetItemById(info.ParentId)?.Path;
if (seriesPath is not null)
{
var path = Path.Combine(seriesPath, "tvshow.nfo");
if (Path.Exists(path))
{
return directoryService.GetFile(path);
}
}
return null;
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Reflection;
[assembly: AssemblyVersion("10.9.2")]
[assembly: AssemblyFileVersion("10.9.2")]
[assembly: AssemblyVersion("10.9.4")]
[assembly: AssemblyFileVersion("10.9.4")]

View File

@@ -15,7 +15,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Extensions</PackageId>
<VersionPrefix>10.9.2</VersionPrefix>
<VersionPrefix>10.9.4</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -1,7 +1,9 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
namespace Jellyfin.Extensions
{
@@ -48,11 +50,12 @@ namespace Jellyfin.Extensions
/// Reads all lines in the <see cref="TextReader" />.
/// </summary>
/// <param name="reader">The <see cref="TextReader" /> to read from.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>All lines in the stream.</returns>
public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader)
public static async IAsyncEnumerable<string> ReadAllLinesAsync(this TextReader reader, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
string? line;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) is not null)
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) is not null)
{
yield return line;
}

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -1,14 +1,19 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using AutoFixture;
using AutoFixture.AutoMoq;
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
using Jellyfin.Api.Auth.FirstTimeSetupPolicy;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
@@ -18,7 +23,9 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
{
private readonly Mock<IConfigurationManager> _configurationManagerMock;
private readonly List<IAuthorizationRequirement> _requirements;
private readonly DefaultAuthorizationHandler _defaultAuthorizationHandler;
private readonly FirstTimeSetupHandler _firstTimeSetupHandler;
private readonly IAuthorizationService _authorizationService;
private readonly Mock<IUserManager> _userManagerMock;
private readonly Mock<IHttpContextAccessor> _httpContextAccessor;
@@ -31,6 +38,21 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
_httpContextAccessor = fixture.Freeze<Mock<IHttpContextAccessor>>();
_firstTimeSetupHandler = fixture.Create<FirstTimeSetupHandler>();
_defaultAuthorizationHandler = fixture.Create<DefaultAuthorizationHandler>();
var services = new ServiceCollection();
services.AddAuthorizationCore();
services.AddLogging();
services.AddOptions();
services.AddSingleton<IAuthorizationHandler>(_defaultAuthorizationHandler);
services.AddSingleton<IAuthorizationHandler>(_firstTimeSetupHandler);
services.AddAuthorization(options =>
{
options.AddPolicy("FirstTime", policy => policy.Requirements.Add(new FirstTimeSetupRequirement()));
options.AddPolicy("FirstTimeNoAdmin", policy => policy.Requirements.Add(new FirstTimeSetupRequirement(false, false)));
options.AddPolicy("FirstTimeSchedule", policy => policy.Requirements.Add(new FirstTimeSetupRequirement(true, false)));
});
_authorizationService = services.BuildServiceProvider().GetRequiredService<IAuthorizationService>();
}
[Theory]
@@ -45,10 +67,9 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
_httpContextAccessor,
userRole);
var context = new AuthorizationHandlerContext(_requirements, claims, null);
var allowed = await _authorizationService.AuthorizeAsync(claims, "FirstTime");
await _firstTimeSetupHandler.HandleAsync(context);
Assert.True(context.HasSucceeded);
Assert.True(allowed.Succeeded);
}
[Theory]
@@ -63,17 +84,16 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
_httpContextAccessor,
userRole);
var context = new AuthorizationHandlerContext(_requirements, claims, null);
var allowed = await _authorizationService.AuthorizeAsync(claims, "FirstTime");
await _firstTimeSetupHandler.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
Assert.Equal(shouldSucceed, allowed.Succeeded);
}
[Theory]
[InlineData(UserRoles.Administrator, true)]
[InlineData(UserRoles.Guest, false)]
[InlineData(UserRoles.User, true)]
public async Task ShouldRequireUserIfNotRequiresAdmin(string userRole, bool shouldSucceed)
public async Task ShouldRequireUserIfNotAdministrator(string userRole, bool shouldSucceed)
{
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
var claims = TestHelpers.SetupUser(
@@ -81,24 +101,26 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupPolicy
_httpContextAccessor,
userRole);
var context = new AuthorizationHandlerContext(
new List<IAuthorizationRequirement> { new FirstTimeSetupRequirement(false, false) },
claims,
null);
var allowed = await _authorizationService.AuthorizeAsync(claims, "FirstTimeNoAdmin");
await _firstTimeSetupHandler.HandleAsync(context);
Assert.Equal(shouldSucceed, context.HasSucceeded);
Assert.Equal(shouldSucceed, allowed.Succeeded);
}
[Fact]
public async Task ShouldAllowAdminApiKeyIfStartupWizardComplete()
public async Task ShouldDisallowUserIfOutsideSchedule()
{
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
var claims = new ClaimsPrincipal(new ClaimsIdentity([new Claim(ClaimTypes.Role, UserRoles.Administrator)]));
var context = new AuthorizationHandlerContext(_requirements, claims, null);
AccessSchedule[] accessSchedules = { new AccessSchedule(DynamicDayOfWeek.Everyday, 0, 0, Guid.Empty) };
await _firstTimeSetupHandler.HandleAsync(context);
Assert.True(context.HasSucceeded);
TestHelpers.SetupConfigurationManager(_configurationManagerMock, true);
var claims = TestHelpers.SetupUser(
_userManagerMock,
_httpContextAccessor,
UserRoles.User,
accessSchedules);
var allowed = await _authorizationService.AuthorizeAsync(claims, "FirstTimeSchedule");
Assert.False(allowed.Succeeded);
}
}
}

View File

@@ -35,7 +35,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Protocol = MediaProtocol.Http,
RequiredHttpHeaders = new Dictionary<string, string>()
{
{ "user_agent", userAgent },
{ "User-Agent", userAgent },
}
},
ExtractChapters = false,
@@ -44,7 +44,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
var extraArg = encoder.GetExtraArguments(req);
Assert.Contains(userAgent, extraArg, StringComparison.InvariantCulture);
Assert.Contains($"-user_agent \"{userAgent}\"", extraArg, StringComparison.InvariantCulture);
}
}
}

View File

@@ -209,7 +209,7 @@ namespace Jellyfin.Providers.Tests.Manager
[InlineData(ImageType.Backdrop, 2, false)]
[InlineData(ImageType.Primary, 1, true)]
[InlineData(ImageType.Backdrop, 2, true)]
public async void RefreshImages_PopulatedItemPopulatedProviderDynamic_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
public async Task RefreshImages_PopulatedItemPopulatedProviderDynamic_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
{
var item = GetItemWithImages(imageType, imageCount, false);
@@ -261,7 +261,7 @@ namespace Jellyfin.Providers.Tests.Manager
[InlineData(ImageType.Backdrop, 2, true, MediaProtocol.File)]
[InlineData(ImageType.Primary, 1, false, MediaProtocol.File)]
[InlineData(ImageType.Backdrop, 2, false, MediaProtocol.File)]
public async void RefreshImages_EmptyItemPopulatedProviderDynamic_AddsImages(ImageType imageType, int imageCount, bool responseHasPath, MediaProtocol protocol)
public async Task RefreshImages_EmptyItemPopulatedProviderDynamic_AddsImages(ImageType imageType, int imageCount, bool responseHasPath, MediaProtocol protocol)
{
// Has to exist for querying DateModified time on file, results stored but not checked so not populating
BaseItem.FileSystem = Mock.Of<IFileSystem>();
@@ -311,7 +311,7 @@ namespace Jellyfin.Providers.Tests.Manager
[InlineData(ImageType.Primary, 1, true)]
[InlineData(ImageType.Backdrop, 1, true)]
[InlineData(ImageType.Backdrop, 2, true)]
public async void RefreshImages_PopulatedItemPopulatedProviderRemote_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
public async Task RefreshImages_PopulatedItemPopulatedProviderRemote_UpdatesImagesIfForced(ImageType imageType, int imageCount, bool forceRefresh)
{
var item = GetItemWithImages(imageType, imageCount, false);
@@ -366,7 +366,7 @@ namespace Jellyfin.Providers.Tests.Manager
[InlineData(ImageType.Backdrop, 0, false)] // empty item, no cache to check
[InlineData(ImageType.Backdrop, 1, false)] // populated item, cached so no download
[InlineData(ImageType.Backdrop, 1, true)] // populated item, forced to download
public async void RefreshImages_NonStubItemPopulatedProviderRemote_DownloadsIfNecessary(ImageType imageType, int initialImageCount, bool fullRefresh)
public async Task RefreshImages_NonStubItemPopulatedProviderRemote_DownloadsIfNecessary(ImageType imageType, int initialImageCount, bool fullRefresh)
{
var targetImageCount = 1;
@@ -429,7 +429,7 @@ namespace Jellyfin.Providers.Tests.Manager
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public async void RefreshImages_EmptyItemPopulatedProviderRemoteExtras_LimitsImages(ImageType imageType, int imageCount)
public async Task RefreshImages_EmptyItemPopulatedProviderRemoteExtras_LimitsImages(ImageType imageType, int imageCount)
{
var item = new Video();
@@ -473,7 +473,7 @@ namespace Jellyfin.Providers.Tests.Manager
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public async void RefreshImages_PopulatedItemEmptyProviderRemoteFullRefresh_DoesntClearImages(ImageType imageType, int imageCount)
public async Task RefreshImages_PopulatedItemEmptyProviderRemoteFullRefresh_DoesntClearImages(ImageType imageType, int imageCount)
{
var item = GetItemWithImages(imageType, imageCount, false);
@@ -501,7 +501,7 @@ namespace Jellyfin.Providers.Tests.Manager
[InlineData(9, false)]
[InlineData(10, true)]
[InlineData(null, true)]
public async void RefreshImages_ProviderRemote_FiltersByWidth(int? remoteImageWidth, bool expectedToUpdate)
public async Task RefreshImages_ProviderRemote_FiltersByWidth(int? remoteImageWidth, bool expectedToUpdate)
{
var imageType = ImageType.Primary;

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
@@ -19,7 +20,7 @@ namespace Jellyfin.Providers.Tests.Manager
[InlineData(true, true)]
public void MergeBaseItemData_MergeMetadataSettings_MergesWhenSet(bool mergeMetadataSettings, bool defaultDate)
{
var newLocked = new[] { MetadataField.Cast };
var newLocked = new[] { MetadataField.Genres, MetadataField.Cast };
var newString = "new";
var newDate = DateTime.Now;
@@ -77,7 +78,7 @@ namespace Jellyfin.Providers.Tests.Manager
[Theory]
[InlineData("Name", MetadataField.Name, false)]
[InlineData("OriginalTitle", null, false)]
[InlineData("OriginalTitle", null)]
[InlineData("OfficialRating", MetadataField.OfficialRating)]
[InlineData("CustomRating")]
[InlineData("Tagline")]

View File

@@ -64,7 +64,7 @@ public class AudioResolverTests
[InlineData("My.Video.mp3", false, true)]
[InlineData("My.Video.srt", true, false)]
[InlineData("My.Video.mp3", true, true)]
public async void GetExternalStreams_MixedFilenames_PicksAudio(string file, bool metadataDirectory, bool matches)
public async Task GetExternalStreams_MixedFilenames_PicksAudio(string file, bool metadataDirectory, bool matches)
{
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();

View File

@@ -37,7 +37,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
}
[Fact]
public async void GetImage_NoStreams_ReturnsNoImage()
public async Task GetImage_NoStreams_ReturnsNoImage()
{
var input = new Movie();
@@ -55,7 +55,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
[InlineData("clearlogo.png", null, 1, ImageType.Logo, ImageFormat.Png)] // extract extension from name
[InlineData("backdrop", "image/bmp", 2, ImageType.Backdrop, ImageFormat.Bmp)] // extract extension from mimetype
[InlineData("poster", null, 3, ImageType.Primary, ImageFormat.Jpg)] // default extension to jpg
public async void GetImage_Attachment_ReturnsCorrectSelection(string filename, string? mimetype, int targetIndex, ImageType type, ImageFormat? expectedFormat)
public async Task GetImage_Attachment_ReturnsCorrectSelection(string filename, string? mimetype, int targetIndex, ImageType type, ImageFormat? expectedFormat)
{
var attachments = new List<MediaAttachment>();
string pathPrefix = "path";
@@ -103,7 +103,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
[InlineData(null, "mjpeg", 1, ImageType.Primary, ImageFormat.Jpg)]
[InlineData(null, "png", 1, ImageType.Primary, ImageFormat.Png)]
[InlineData(null, "webp", 1, ImageType.Primary, ImageFormat.Webp)]
public async void GetImage_Embedded_ReturnsCorrectSelection(string? label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat)
public async Task GetImage_Embedded_ReturnsCorrectSelection(string? label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat)
{
var streams = new List<MediaStream>();
for (int i = 1; i <= targetIndex; i++)

View File

@@ -182,7 +182,7 @@ public class MediaInfoResolverTests
[Theory]
[InlineData("https://url.com/My.Video.mkv")]
[InlineData(VideoDirectoryPath)] // valid but no files found for this test
public async void GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path)
public async Task GetExternalStreams_BadPaths_ReturnsNoSubtitles(string path)
{
// need a media source manager capable of returning something other than file protocol
var mediaSourceManager = new Mock<IMediaSourceManager>();
@@ -285,7 +285,7 @@ public class MediaInfoResolverTests
[Theory]
[MemberData(nameof(GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly_Data))]
public async void GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly(string file, MediaStream[] inputStreams, MediaStream[] expectedStreams)
public async Task GetExternalStreams_MergeMetadata_HandlesOverridesCorrectly(string file, MediaStream[] inputStreams, MediaStream[] expectedStreams)
{
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();
@@ -335,7 +335,7 @@ public class MediaInfoResolverTests
[InlineData(1, 2)]
[InlineData(2, 1)]
[InlineData(2, 2)]
public async void GetExternalStreams_StreamIndex_HandlesFilesAndContainers(int fileCount, int streamCount)
public async Task GetExternalStreams_StreamIndex_HandlesFilesAndContainers(int fileCount, int streamCount)
{
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();

View File

@@ -64,7 +64,7 @@ public class SubtitleResolverTests
[InlineData("My.Video.mp3", false, false)]
[InlineData("My.Video.srt", true, true)]
[InlineData("My.Video.mp3", true, false)]
public async void GetExternalStreams_MixedFilenames_PicksSubtitles(string file, bool metadataDirectory, bool matches)
public async Task GetExternalStreams_MixedFilenames_PicksSubtitles(string file, bool metadataDirectory, bool matches)
{
BaseItem.MediaSourceManager = Mock.Of<IMediaSourceManager>();

View File

@@ -34,7 +34,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
[Theory]
[MemberData(nameof(GetImage_UnsupportedInput_ReturnsNoImage_TestData))]
public async void GetImage_UnsupportedInput_ReturnsNoImage(Video input)
public async Task GetImage_UnsupportedInput_ReturnsNoImage(Video input)
{
var mediaSourceManager = GetMediaSourceManager(input, null, new List<MediaStream>());
var videoImageProvider = new VideoImageProvider(mediaSourceManager, Mock.Of<IMediaEncoder>(), new NullLogger<VideoImageProvider>());
@@ -47,7 +47,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
[Theory]
[InlineData(1, 1)] // default not first stream
[InlineData(5, 0)] // default out of valid range
public async void GetImage_DefaultVideoStreams_ReturnsCorrectStreamImage(int defaultIndex, int targetIndex)
public async Task GetImage_DefaultVideoStreams_ReturnsCorrectStreamImage(int defaultIndex, int targetIndex)
{
var input = new Movie { DefaultVideoStreamIndex = defaultIndex };
@@ -80,7 +80,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo
[Theory]
[InlineData(null, 10)] // default time
[InlineData(500, 50)] // calculated time
public async void GetImage_TimeSpan_SelectsCorrectTime(int? runTimeSeconds, long expectedSeconds)
public async Task GetImage_TimeSpan_SelectsCorrectTime(int? runTimeSeconds, long expectedSeconds)
{
MediaStream targetStream = new() { Type = MediaStreamType.Video, Index = 0 };
var input = new Movie