mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-08-05 08:40:11 -04:00
Merge remote-tracking branch 'upstream/master' into localization-test
This commit is contained in:
commit
45e0f7893b
@ -7,7 +7,7 @@ parameters:
|
|||||||
default: "ubuntu-latest"
|
default: "ubuntu-latest"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.103
|
default: 5.0.302
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: CompatibilityCheck
|
- job: CompatibilityCheck
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
parameters:
|
parameters:
|
||||||
LinuxImage: 'ubuntu-latest'
|
LinuxImage: 'ubuntu-latest'
|
||||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
DotNetSdkVersion: 5.0.103
|
DotNetSdkVersion: 5.0.302
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Build
|
- job: Build
|
||||||
|
@ -10,7 +10,7 @@ parameters:
|
|||||||
default: "tests/**/*Tests.csproj"
|
default: "tests/**/*Tests.csproj"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.103
|
default: 5.0.302
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Test
|
- job: Test
|
||||||
|
@ -6,7 +6,7 @@ variables:
|
|||||||
- name: RestoreBuildProjects
|
- name: RestoreBuildProjects
|
||||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
value: 5.0.103
|
value: 5.0.302
|
||||||
|
|
||||||
pr:
|
pr:
|
||||||
autoCancel: true
|
autoCancel: true
|
||||||
|
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -17,6 +17,7 @@ assignees: ''
|
|||||||
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
|
- Browser: [e.g. Firefox 72, Chrome 80, Safari 13]
|
||||||
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
|
- Jellyfin Version: [e.g. 10.4.3, nightly 20191231]
|
||||||
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
|
- Playback: [Direct Play, Remux, Direct Stream, Transcode]
|
||||||
|
- Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
|
||||||
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
|
- Installed Plugins: [e.g. none, Fanart, Anime, etc.]
|
||||||
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
|
- Reverse Proxy: [e.g. none, nginx, apache, etc.]
|
||||||
- Base URL: [e.g. none, yes: /example]
|
- Base URL: [e.g. none, yes: /example]
|
||||||
@ -33,7 +34,13 @@ assignees: ''
|
|||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
<!-- A clear and concise description of what you expected to happen. -->
|
<!-- A clear and concise description of what you expected to happen. -->
|
||||||
|
|
||||||
**Logs**
|
**Server Logs**
|
||||||
|
<!-- Please paste any log errors. -->
|
||||||
|
|
||||||
|
**FFmpeg Logs**
|
||||||
|
<!-- Please paste any log errors. -->
|
||||||
|
|
||||||
|
**Browser Console Logs**
|
||||||
<!-- Please paste any log errors. -->
|
<!-- Please paste any log errors. -->
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
|
6
.github/stale.yml
vendored
6
.github/stale.yml
vendored
@ -17,9 +17,13 @@ staleLabel: stale
|
|||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
markComment: >
|
||||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||||
|
|
||||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or nightlies, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||||
|
|
||||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
closeComment: false
|
closeComment: false
|
||||||
|
|
||||||
|
# Disable automatic closing of pull requests
|
||||||
|
pulls:
|
||||||
|
daysUntilClose: false
|
||||||
|
48
.github/workflows/automation.yml
vendored
48
.github/workflows/automation.yml
vendored
@ -1,44 +1,56 @@
|
|||||||
name: Automation
|
name: Automation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request_target:
|
||||||
|
issue_comment:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
label:
|
||||||
|
name: Labeling
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Does PR has the stable backport label?
|
- name: Apply label
|
||||||
uses: Dreamcodeio/does-pr-has-label@v1.2
|
uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
||||||
id: checkLabel
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
|
||||||
with:
|
with:
|
||||||
label: stable backport
|
dirtyLabel: 'merge conflict'
|
||||||
|
repoToken: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
project:
|
||||||
|
name: Project board
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
|
steps:
|
||||||
- name: Remove from 'Current Release' project
|
- name: Remove from 'Current Release' project
|
||||||
uses: alex-page/github-project-automation-plus@v0.7.1
|
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||||
if: (github.event.pull_request || github.event.issue.pull_request) && !steps.checkLabel.outputs.hasLabel
|
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
project: Current Release
|
project: Current Release
|
||||||
action: delete
|
action: delete
|
||||||
repo-token: ${{ secrets.GH_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add to 'Release Next' project
|
- name: Add to 'Release Next' project
|
||||||
uses: alex-page/github-project-automation-plus@v0.7.1
|
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||||
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
project: Release Next
|
project: Release Next
|
||||||
column: In progress
|
column: In progress
|
||||||
repo-token: ${{ secrets.GH_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add to 'Current Release' project
|
- name: Add to 'Current Release' project
|
||||||
uses: alex-page/github-project-automation-plus@v0.7.1
|
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||||
if: (github.event.pull_request || github.event.issue.pull_request) && steps.checkLabel.outputs.hasLabel
|
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
project: Current Release
|
project: Current Release
|
||||||
column: In progress
|
column: In progress
|
||||||
repo-token: ${{ secrets.GH_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Check number of comments from the team member
|
- name: Check number of comments from the team member
|
||||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
|
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER'
|
||||||
@ -46,19 +58,19 @@ jobs:
|
|||||||
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
||||||
|
|
||||||
- name: Move issue to needs triage
|
- name: Move issue to needs triage
|
||||||
uses: alex-page/github-project-automation-plus@v0.7.1
|
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
project: Issue Triage for Main Repo
|
project: Issue Triage for Main Repo
|
||||||
column: Needs triage
|
column: Needs triage
|
||||||
repo-token: ${{ secrets.GH_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add issue to triage project
|
- name: Add issue to triage project
|
||||||
uses: alex-page/github-project-automation-plus@v0.7.1
|
uses: alex-page/github-project-automation-plus@v0.8.1
|
||||||
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
project: Issue Triage for Main Repo
|
project: Issue Triage for Main Repo
|
||||||
column: Pending response
|
column: Pending response
|
||||||
repo-token: ${{ secrets.GH_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
119
.github/workflows/commands.yml
vendored
Normal file
119
.github/workflows/commands.yml
vendored
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
name: Commands
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types:
|
||||||
|
- created
|
||||||
|
- edited
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rebase:
|
||||||
|
name: Rebase
|
||||||
|
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Notify as seen
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ github.event.comment.id }}
|
||||||
|
reactions: '+1'
|
||||||
|
|
||||||
|
- name: Checkout the latest code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Automatic Rebase
|
||||||
|
uses: cirrus-actions/rebase@1.4
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
|
check-backport:
|
||||||
|
name: Check Backport
|
||||||
|
if: ${{ ( github.event.issue.pull_request && contains(github.event.comment.body, '@jellyfin-bot check backport') ) || github.event.label.name == 'stable backport' || contains(github.event.pull_request.labels.*.name, 'stable backport' ) }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Notify as seen
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ github.event.comment.id }}
|
||||||
|
reactions: eyes
|
||||||
|
|
||||||
|
- name: Checkout the latest code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Notify as running
|
||||||
|
id: comment_running
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
issue-number: ${{ github.event.issue.number }}
|
||||||
|
body: |
|
||||||
|
Running backport tests...
|
||||||
|
|
||||||
|
- name: Perform test backport
|
||||||
|
id: run_tests
|
||||||
|
run: |
|
||||||
|
set +o errexit
|
||||||
|
git config --global user.name "Jellyfin Bot"
|
||||||
|
git config --global user.email "team@jellyfin.org"
|
||||||
|
CURRENT_BRANCH="origin/${GITHUB_HEAD_REF}"
|
||||||
|
git checkout master
|
||||||
|
git merge --no-ff ${CURRENT_BRANCH}
|
||||||
|
MERGE_COMMIT_HASH=$( git log -q -1 | head -1 | awk '{ print $2 }' )
|
||||||
|
git fetch --all
|
||||||
|
CURRENT_STABLE=$( git branch -r | grep 'origin/release' | sort -rV | head -1 | awk -F '/' '{ print $NF }' )
|
||||||
|
stable_branch="Current stable release branch: ${CURRENT_STABLE}"
|
||||||
|
echo ${stable_branch}
|
||||||
|
echo ::set-output name=branch::${stable_branch}
|
||||||
|
git checkout -t origin/${CURRENT_STABLE} -b ${CURRENT_STABLE}
|
||||||
|
git cherry-pick -sx -m1 ${MERGE_COMMIT_HASH} &>output.txt
|
||||||
|
retcode=$?
|
||||||
|
cat output.txt | grep -v 'hint:'
|
||||||
|
output="$( grep -v 'hint:' output.txt )"
|
||||||
|
output="${output//'%'/'%25'}"
|
||||||
|
output="${output//$'\n'/'%0A'}"
|
||||||
|
output="${output//$'\r'/'%0D'}"
|
||||||
|
echo ::set-output name=output::$output
|
||||||
|
exit ${retcode}
|
||||||
|
|
||||||
|
- name: Notify with result success
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null && success() }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
||||||
|
body: |
|
||||||
|
${{ steps.run_tests.outputs.branch }}
|
||||||
|
Output from `git cherry-pick`:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
${{ steps.run_tests.outputs.output }}
|
||||||
|
reactions: hooray
|
||||||
|
|
||||||
|
- name: Notify with result failure
|
||||||
|
uses: peter-evans/create-or-update-comment@v1.4.5
|
||||||
|
if: ${{ github.event.comment != null && failure() }}
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
comment-id: ${{ steps.comment_running.outputs.comment-id }}
|
||||||
|
body: |
|
||||||
|
${{ steps.run_tests.outputs.branch }}
|
||||||
|
Output from `git cherry-pick`:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
${{ steps.run_tests.outputs.output }}
|
||||||
|
reactions: confused
|
43
.github/workflows/label-commenter-config.yml
vendored
43
.github/workflows/label-commenter-config.yml
vendored
@ -1,43 +0,0 @@
|
|||||||
comment:
|
|
||||||
header: Hello @{{ issue.user.login }}
|
|
||||||
footer: "\
|
|
||||||
---\n\n
|
|
||||||
> This is an automated comment created by the [peaceiris/actions-label-commenter]. \
|
|
||||||
Responding to the bot or mentioning it won't have any effect.\n\n
|
|
||||||
[peaceiris/actions-label-commenter]: https://github.com/peaceiris/actions-label-commenter
|
|
||||||
"
|
|
||||||
|
|
||||||
labels:
|
|
||||||
- name: stable backport
|
|
||||||
labeled:
|
|
||||||
pr:
|
|
||||||
body: |
|
|
||||||
This pull request has been tagged as a stable backport. It will be cherry-picked into the next stable point release.
|
|
||||||
|
|
||||||
Please observe the following:
|
|
||||||
|
|
||||||
* Any dependent PRs that this PR requires **must** be tagged for stable backporting as well.
|
|
||||||
|
|
||||||
* Any issue(s) this PR fixes or closes **should** target the current stable release or a previous stable release to which a fix has not yet entered the current stable release.
|
|
||||||
|
|
||||||
* This PR **must** be test cherry-picked against the current release branch (`release-X.Y.z` where X and Y are numbers). It must apply cleanly, or a diff of the expected change must be provided.
|
|
||||||
|
|
||||||
To do this, run the following commands from your local copy of the Jellyfin repository:
|
|
||||||
|
|
||||||
1. `git checkout master`
|
|
||||||
|
|
||||||
1. `git merge --no-ff <myPullRequestBranch>`
|
|
||||||
|
|
||||||
1. `git log` -> `commit xxxxxxxxx`, grab hash
|
|
||||||
|
|
||||||
1. `git checkout release-X.Y.z` replacing X and Y with the *current* stable version (e.g. `release-10.7.z`)
|
|
||||||
|
|
||||||
1. `git cherry-pick -sx -m1 <hash>`
|
|
||||||
|
|
||||||
Ensure the `cherry-pick` applies cleanly. If it does not, fix any merge conflicts *preserving as much of the original code as possible*, and make note of the resulting diff.
|
|
||||||
|
|
||||||
Test your changes with a build to ensure they are successful. If not, adjust the diff accordingly.
|
|
||||||
|
|
||||||
**Do not** push your merges to either branch. Use `git reset --hard HEAD~1` to revert both branches to their original state.
|
|
||||||
|
|
||||||
Reply to this PR with a comment beginning "Cherry-pick test completed." and including the merge-conflict-fixing diff(s) if applicable.
|
|
22
.github/workflows/label-commenter.yml
vendored
22
.github/workflows/label-commenter.yml
vendored
@ -1,22 +0,0 @@
|
|||||||
name: Label Commenter
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types:
|
|
||||||
- labeled
|
|
||||||
- unlabeled
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- labeled
|
|
||||||
- unlabeled
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
comment:
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
ref: master
|
|
||||||
|
|
||||||
- name: Label Commenter
|
|
||||||
uses: peaceiris/actions-label-commenter@v1
|
|
17
.github/workflows/merge-conflicts.yml
vendored
17
.github/workflows/merge-conflicts.yml
vendored
@ -1,17 +0,0 @@
|
|||||||
name: 'Merge Conflicts'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
pull_request_target:
|
|
||||||
types:
|
|
||||||
- synchronize
|
|
||||||
jobs:
|
|
||||||
triage:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: eps1lon/actions-label-merge-conflict@v2.0.1
|
|
||||||
with:
|
|
||||||
dirtyLabel: 'merge conflict'
|
|
||||||
repoToken: ${{ secrets.GH_TOKEN }}
|
|
27
.github/workflows/rebase.yml
vendored
27
.github/workflows/rebase.yml
vendored
@ -1,27 +0,0 @@
|
|||||||
name: Automatic Rebase
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
rebase:
|
|
||||||
name: Rebase
|
|
||||||
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '@jellyfin-bot rebase') && github.event.comment.author_association == 'MEMBER'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Notify as seen
|
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.5
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
|
||||||
comment-id: ${{ github.event.comment.id }}
|
|
||||||
reactions: '+1'
|
|
||||||
|
|
||||||
- name: Checkout the latest code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Automatic Rebase
|
|
||||||
uses: cirrus-actions/rebase@1.4
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -268,6 +268,7 @@ doc/
|
|||||||
# Deployment artifacts
|
# Deployment artifacts
|
||||||
dist
|
dist
|
||||||
*.exe
|
*.exe
|
||||||
|
*.dll
|
||||||
|
|
||||||
# BenchmarkDotNet artifacts
|
# BenchmarkDotNet artifacts
|
||||||
BenchmarkDotNet.Artifacts
|
BenchmarkDotNet.Artifacts
|
||||||
|
@ -70,6 +70,7 @@
|
|||||||
- [marius-luca-87](https://github.com/marius-luca-87)
|
- [marius-luca-87](https://github.com/marius-luca-87)
|
||||||
- [mark-monteiro](https://github.com/mark-monteiro)
|
- [mark-monteiro](https://github.com/mark-monteiro)
|
||||||
- [Matt07211](https://github.com/Matt07211)
|
- [Matt07211](https://github.com/Matt07211)
|
||||||
|
- [Maxr1998](https://github.com/Maxr1998)
|
||||||
- [mcarlton00](https://github.com/mcarlton00)
|
- [mcarlton00](https://github.com/mcarlton00)
|
||||||
- [mitchfizz05](https://github.com/mitchfizz05)
|
- [mitchfizz05](https://github.com/mitchfizz05)
|
||||||
- [MrTimscampi](https://github.com/MrTimscampi)
|
- [MrTimscampi](https://github.com/MrTimscampi)
|
||||||
@ -110,7 +111,7 @@
|
|||||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||||
- [sparky8251](https://github.com/sparky8251)
|
- [sparky8251](https://github.com/sparky8251)
|
||||||
- [spookbits](https://github.com/spookbits)
|
- [spookbits](https://github.com/spookbits)
|
||||||
- [ssenart] (https://github.com/ssenart)
|
- [ssenart](https://github.com/ssenart)
|
||||||
- [stanionascu](https://github.com/stanionascu)
|
- [stanionascu](https://github.com/stanionascu)
|
||||||
- [stevehayles](https://github.com/stevehayles)
|
- [stevehayles](https://github.com/stevehayles)
|
||||||
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
- [SuperSandro2000](https://github.com/SuperSandro2000)
|
||||||
@ -146,6 +147,7 @@
|
|||||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||||
- [skyfrk](https://github.com/skyfrk)
|
- [skyfrk](https://github.com/skyfrk)
|
||||||
- [ianjazz246](https://github.com/ianjazz246)
|
- [ianjazz246](https://github.com/ianjazz246)
|
||||||
|
- [peterspenler](https://github.com/peterspenler)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
14
Directory.Build.props
Normal file
14
Directory.Build.props
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<Project>
|
||||||
|
<!-- Sets defaults for all projects in the repo -->
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
|
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
@ -1,11 +1,11 @@
|
|||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=5.0
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=master
|
||||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& npm ci --no-audit \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
|
||||||
|
@ -5,12 +5,12 @@
|
|||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=5.0
|
||||||
|
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=master
|
||||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& npm ci --no-audit \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,12 +5,12 @@
|
|||||||
ARG DOTNET_VERSION=5.0
|
ARG DOTNET_VERSION=5.0
|
||||||
|
|
||||||
|
|
||||||
FROM node:alpine as web-builder
|
FROM node:lts-alpine as web-builder
|
||||||
ARG JELLYFIN_WEB_VERSION=master
|
ARG JELLYFIN_WEB_VERSION=master
|
||||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \
|
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||||
&& cd jellyfin-web-* \
|
&& cd jellyfin-web-* \
|
||||||
&& npm ci --no-audit \
|
&& npm ci --no-audit --unsafe-perm \
|
||||||
&& mv dist /dist
|
&& mv dist /dist
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||||
|
<Nullable>disable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
namespace Emby.Dlna.Configuration
|
namespace Emby.Dlna.Configuration
|
||||||
@ -74,7 +72,7 @@ namespace Emby.Dlna.Configuration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the default user account that the dlna server uses.
|
/// Gets or sets the default user account that the dlna server uses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string DefaultUserId { get; set; }
|
public string? DefaultUserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether playTo device profiles should be created.
|
/// Gets or sets a value indicating whether playTo device profiles should be created.
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -140,7 +138,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
/// <param name="profile">The <see cref="DeviceProfile"/>.</param>
|
||||||
/// <returns>The <see cref="User"/>.</returns>
|
/// <returns>The <see cref="User"/>.</returns>
|
||||||
private User GetUser(DeviceProfile profile)
|
private User? GetUser(DeviceProfile profile)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(profile.UserId))
|
if (!string.IsNullOrEmpty(profile.UserId))
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -8,9 +6,11 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
public class ControlResponse
|
public class ControlResponse
|
||||||
{
|
{
|
||||||
public ControlResponse()
|
public ControlResponse(string xml, bool isSuccessful)
|
||||||
{
|
{
|
||||||
Headers = new Dictionary<string, string>();
|
Headers = new Dictionary<string, string>();
|
||||||
|
Xml = xml;
|
||||||
|
IsSuccessful = isSuccessful;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<string, string> Headers { get; }
|
public IDictionary<string, string> Headers { get; }
|
||||||
|
@ -14,9 +14,9 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna.Profiles;
|
using Emby.Dlna.Profiles;
|
||||||
using Emby.Dlna.Server;
|
using Emby.Dlna.Server;
|
||||||
|
using Jellyfin.Extensions.Json;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Json;
|
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Dlna;
|
using MediaBrowser.Controller.Dlna;
|
||||||
using MediaBrowser.Controller.Drawing;
|
using MediaBrowser.Controller.Drawing;
|
||||||
|
@ -20,8 +20,7 @@
|
|||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
@ -31,10 +30,6 @@
|
|||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Images\logo120.jpg" />
|
<EmbeddedResource Include="Images\logo120.jpg" />
|
||||||
<EmbeddedResource Include="Images\logo120.png" />
|
<EmbeddedResource Include="Images\logo120.png" />
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -8,8 +6,10 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
public class EventSubscriptionResponse
|
public class EventSubscriptionResponse
|
||||||
{
|
{
|
||||||
public EventSubscriptionResponse()
|
public EventSubscriptionResponse(string content, string contentType)
|
||||||
{
|
{
|
||||||
|
Content = content;
|
||||||
|
ContentType = contentType;
|
||||||
Headers = new Dictionary<string, string>();
|
Headers = new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,11 +51,7 @@ namespace Emby.Dlna.Eventing
|
|||||||
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
return GetEventSubscriptionResponse(subscriptionId, requestedTimeoutString, timeoutSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EventSubscriptionResponse
|
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||||
{
|
|
||||||
Content = string.Empty,
|
|
||||||
ContentType = "text/plain"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl)
|
||||||
@ -103,20 +99,12 @@ namespace Emby.Dlna.Eventing
|
|||||||
|
|
||||||
_subscriptions.TryRemove(subscriptionId, out _);
|
_subscriptions.TryRemove(subscriptionId, out _);
|
||||||
|
|
||||||
return new EventSubscriptionResponse
|
return new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||||
{
|
|
||||||
Content = string.Empty,
|
|
||||||
ContentType = "text/plain"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
|
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
|
||||||
{
|
{
|
||||||
var response = new EventSubscriptionResponse
|
var response = new EventSubscriptionResponse(string.Empty, "text/plain");
|
||||||
{
|
|
||||||
Content = string.Empty,
|
|
||||||
ContentType = "text/plain"
|
|
||||||
};
|
|
||||||
|
|
||||||
response.Headers["SID"] = subscriptionId;
|
response.Headers["SID"] = subscriptionId;
|
||||||
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
|
response.Headers["TIMEOUT"] = string.IsNullOrEmpty(requestedTimeoutString) ? ("SECOND-" + timeoutSeconds.ToString(_usCulture)) : requestedTimeoutString;
|
||||||
|
@ -27,11 +27,9 @@ using MediaBrowser.Controller.TV;
|
|||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.System;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Rssdp;
|
using Rssdp;
|
||||||
using Rssdp.Infrastructure;
|
using Rssdp.Infrastructure;
|
||||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
|
||||||
|
|
||||||
namespace Emby.Dlna.Main
|
namespace Emby.Dlna.Main
|
||||||
{
|
{
|
||||||
@ -204,8 +202,8 @@ namespace Emby.Dlna.Main
|
|||||||
{
|
{
|
||||||
if (_communicationsServer == null)
|
if (_communicationsServer == null)
|
||||||
{
|
{
|
||||||
var enableMultiSocketBinding = OperatingSystem.Id == OperatingSystemId.Windows ||
|
var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
|
||||||
OperatingSystem.Id == OperatingSystemId.Linux;
|
OperatingSystem.IsLinux();
|
||||||
|
|
||||||
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
|
||||||
{
|
{
|
||||||
@ -268,7 +266,12 @@ namespace Emby.Dlna.Main
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_publisher = new SsdpDevicePublisher(_communicationsServer, _networkManager, OperatingSystem.Name, Environment.OSVersion.VersionString, _config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
_publisher = new SsdpDevicePublisher(
|
||||||
|
_communicationsServer,
|
||||||
|
_networkManager,
|
||||||
|
MediaBrowser.Common.System.OperatingSystem.Name,
|
||||||
|
Environment.OSVersion.VersionString,
|
||||||
|
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||||
{
|
{
|
||||||
LogFunction = LogMessage,
|
LogFunction = LogMessage,
|
||||||
SupportPnpRootDevice = false
|
SupportPnpRootDevice = false
|
||||||
|
@ -370,6 +370,42 @@ namespace Emby.Dlna.PlayTo
|
|||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
|
||||||
|
* Without that information, the next track command on the device does not work.
|
||||||
|
*/
|
||||||
|
public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
url = url.Replace("&", "&", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
|
||||||
|
|
||||||
|
var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (command == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dictionary = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "NextURI", url },
|
||||||
|
{ "NextURIMetaData", CreateDidlMeta(metaData) }
|
||||||
|
};
|
||||||
|
|
||||||
|
var service = GetAvTransportService();
|
||||||
|
|
||||||
|
if (service == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Unable to find service");
|
||||||
|
}
|
||||||
|
|
||||||
|
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
||||||
|
await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header: header, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
private static string CreateDidlMeta(string value)
|
private static string CreateDidlMeta(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
@ -1224,10 +1260,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs
|
PlaybackStart?.Invoke(this, new PlaybackStartEventArgs(mediaInfo));
|
||||||
{
|
|
||||||
MediaInfo = mediaInfo
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackProgress(UBaseObject mediaInfo)
|
private void OnPlaybackProgress(UBaseObject mediaInfo)
|
||||||
@ -1237,27 +1270,17 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs
|
PlaybackProgress?.Invoke(this, new PlaybackProgressEventArgs(mediaInfo));
|
||||||
{
|
|
||||||
MediaInfo = mediaInfo
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackStop(UBaseObject mediaInfo)
|
private void OnPlaybackStop(UBaseObject mediaInfo)
|
||||||
{
|
{
|
||||||
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
|
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs(mediaInfo));
|
||||||
{
|
|
||||||
MediaInfo = mediaInfo
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
|
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
|
||||||
{
|
{
|
||||||
MediaChanged?.Invoke(this, new MediaChangedEventArgs
|
MediaChanged?.Invoke(this, new MediaChangedEventArgs(old, newMedia));
|
||||||
{
|
|
||||||
OldMediaInfo = old,
|
|
||||||
NewMediaInfo = newMedia
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
#nullable disable
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@ -8,6 +6,12 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class MediaChangedEventArgs : EventArgs
|
public class MediaChangedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
|
public MediaChangedEventArgs(UBaseObject oldMediaInfo, UBaseObject newMediaInfo)
|
||||||
|
{
|
||||||
|
OldMediaInfo = oldMediaInfo;
|
||||||
|
NewMediaInfo = newMediaInfo;
|
||||||
|
}
|
||||||
|
|
||||||
public UBaseObject OldMediaInfo { get; set; }
|
public UBaseObject OldMediaInfo { get; set; }
|
||||||
|
|
||||||
public UBaseObject NewMediaInfo { get; set; }
|
public UBaseObject NewMediaInfo { get; set; }
|
||||||
|
@ -104,6 +104,22 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||||
|
*/
|
||||||
|
private async Task SendNextTrackMessage(int currentPlayListItemIndex, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (currentPlayListItemIndex >= 0 && currentPlayListItemIndex < _playlist.Count - 1)
|
||||||
|
{
|
||||||
|
// The current playing item is indeed in the play list and we are not yet at the end of the playlist.
|
||||||
|
var nextItemIndex = currentPlayListItemIndex + 1;
|
||||||
|
var nextItem = _playlist[nextItemIndex];
|
||||||
|
|
||||||
|
// Send the SetNextAvTransport message.
|
||||||
|
await _device.SetNextAvTransport(nextItem.StreamUrl, GetDlnaHeaders(nextItem), nextItem.Didl, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDeviceUnavailable()
|
private void OnDeviceUnavailable()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -158,6 +174,15 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var newItemProgress = GetProgressInfo(streamInfo);
|
var newItemProgress = GetProgressInfo(streamInfo);
|
||||||
|
|
||||||
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the playlist.
|
||||||
|
var currentItemIndex = _playlist.FindIndex(item => item.StreamInfo.ItemId == streamInfo.ItemId);
|
||||||
|
if (currentItemIndex >= 0)
|
||||||
|
{
|
||||||
|
_currentPlaylistIndex = currentItemIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendNextTrackMessage(currentItemIndex, CancellationToken.None);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -427,6 +452,11 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, info.SubtitleStreamIndex);
|
||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||||
|
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -625,6 +655,9 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
await SendNextTrackMessage(index, cancellationToken);
|
||||||
|
|
||||||
var streamInfo = currentitem.StreamInfo;
|
var streamInfo = currentitem.StreamInfo;
|
||||||
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo))
|
||||||
{
|
{
|
||||||
@ -738,6 +771,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||||
|
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||||
|
|
||||||
if (EnableClientSideSeek(newItem.StreamInfo))
|
if (EnableClientSideSeek(newItem.StreamInfo))
|
||||||
{
|
{
|
||||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||||
@ -763,6 +800,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
await _device.SetAvTransport(newItem.StreamUrl, GetDlnaHeaders(newItem), newItem.Didl, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Send a message to the DLNA device to notify what is the next track in the play list.
|
||||||
|
var newItemIndex = _playlist.FindIndex(item => item.StreamUrl == newItem.StreamUrl);
|
||||||
|
await SendNextTrackMessage(newItemIndex, CancellationToken.None);
|
||||||
|
|
||||||
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
if (EnableClientSideSeek(newItem.StreamInfo) && newPosition > 0)
|
||||||
{
|
{
|
||||||
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
await SeekAfterTransportChange(newPosition, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class PlaybackProgressEventArgs : EventArgs
|
public class PlaybackProgressEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
|
public PlaybackProgressEventArgs(UBaseObject mediaInfo)
|
||||||
|
{
|
||||||
|
MediaInfo = mediaInfo;
|
||||||
|
}
|
||||||
|
|
||||||
public UBaseObject MediaInfo { get; set; }
|
public UBaseObject MediaInfo { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class PlaybackStartEventArgs : EventArgs
|
public class PlaybackStartEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
|
public PlaybackStartEventArgs(UBaseObject mediaInfo)
|
||||||
|
{
|
||||||
|
MediaInfo = mediaInfo;
|
||||||
|
}
|
||||||
|
|
||||||
public UBaseObject MediaInfo { get; set; }
|
public UBaseObject MediaInfo { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -8,6 +6,11 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
public class PlaybackStoppedEventArgs : EventArgs
|
public class PlaybackStoppedEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
|
public PlaybackStoppedEventArgs(UBaseObject mediaInfo)
|
||||||
|
{
|
||||||
|
MediaInfo = mediaInfo;
|
||||||
|
}
|
||||||
|
|
||||||
public UBaseObject MediaInfo { get; set; }
|
public UBaseObject MediaInfo { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,9 @@ using System.IO;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
using Diacritics.Extensions;
|
||||||
using Emby.Dlna.Didl;
|
using Emby.Dlna.Didl;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Controller.Extensions;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Dlna.Service
|
namespace Emby.Dlna.Service
|
||||||
@ -95,11 +95,7 @@ namespace Emby.Dlna.Service
|
|||||||
|
|
||||||
var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
|
var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u=", StringComparison.Ordinal);
|
||||||
|
|
||||||
var controlResponse = new ControlResponse
|
var controlResponse = new ControlResponse(xml, true);
|
||||||
{
|
|
||||||
Xml = xml,
|
|
||||||
IsSuccessful = true
|
|
||||||
};
|
|
||||||
|
|
||||||
controlResponse.Headers.Add("EXT", string.Empty);
|
controlResponse.Headers.Add("EXT", string.Empty);
|
||||||
|
|
||||||
|
@ -46,11 +46,7 @@ namespace Emby.Dlna.Service
|
|||||||
writer.WriteEndDocument();
|
writer.WriteEndDocument();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ControlResponse
|
return new ControlResponse(builder.ToString(), false);
|
||||||
{
|
|
||||||
Xml = builder.ToString(),
|
|
||||||
IsSuccessful = false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,7 @@
|
|||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -30,8 +29,4 @@
|
|||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Audio
|
namespace Emby.Naming.Audio
|
||||||
{
|
{
|
||||||
@ -18,8 +18,8 @@ namespace Emby.Naming.Audio
|
|||||||
/// <returns>True if file at path is audio file.</returns>
|
/// <returns>True if file at path is audio file.</returns>
|
||||||
public static bool IsAudioFile(string path, NamingOptions options)
|
public static bool IsAudioFile(string path, NamingOptions options)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
return options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ namespace Emby.Naming.Common
|
|||||||
|
|
||||||
CleanStrings = new[]
|
CleanStrings = new[]
|
||||||
{
|
{
|
||||||
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
@"[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r3|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
|
||||||
@"(\[.*\])"
|
@"(\[.*\])"
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -277,14 +277,14 @@ namespace Emby.Naming.Common
|
|||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
|
|
||||||
new EpisodeExpression("[\\\\/\\._ \\[\\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)([^\\\\/]*)$")
|
new EpisodeExpression(@"[\\\/\._ \[\(-]([0-9]+)x([0-9]+(?:(?:[a-i]|\.[1-9])(?![0-9]))?)([^\\\/]*)$")
|
||||||
{
|
{
|
||||||
SupportsAbsoluteEpisodeNumbers = true
|
SupportsAbsoluteEpisodeNumbers = true
|
||||||
},
|
},
|
||||||
|
|
||||||
// Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
|
// Not a Kodi rule as well, but below rule also causes false positives for triple-digit episode names
|
||||||
// [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
|
// [bar] Foo - 1 [baz] special case of below expression to prevent false positives with digits in the series name
|
||||||
new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>[\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
|
new EpisodeExpression(@".*[\\\/]?.*?(\[.*?\])+.*?(?<seriesname>[-\w\s]+?)[\s_]*-[\s_]*(?<epnumber>[0-9]+).*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
@ -305,6 +305,12 @@ namespace Emby.Naming.Common
|
|||||||
|
|
||||||
// *** End Kodi Standard Naming
|
// *** End Kodi Standard Naming
|
||||||
|
|
||||||
|
// "Episode 16", "Episode 16 - Title"
|
||||||
|
new EpisodeExpression(@"[Ee]pisode (?<epnumber>[0-9]+)(-(?<endingepnumber>[0-9]+))?[^\\\/]*$")
|
||||||
|
{
|
||||||
|
IsNamed = true
|
||||||
|
},
|
||||||
|
|
||||||
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
|
new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]+)[xX](?<epnumber>[0-9]+)[^\\\/]*$")
|
||||||
{
|
{
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
@ -362,12 +368,6 @@ namespace Emby.Naming.Common
|
|||||||
IsOptimistic = true,
|
IsOptimistic = true,
|
||||||
IsNamed = true
|
IsNamed = true
|
||||||
},
|
},
|
||||||
// "Episode 16", "Episode 16 - Title"
|
|
||||||
new EpisodeExpression(@".*[\\\/][^\\\/]* (?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*[^\\\/]*$")
|
|
||||||
{
|
|
||||||
IsOptimistic = true,
|
|
||||||
IsNamed = true
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeWithoutSeasonExpressions = new[]
|
EpisodeWithoutSeasonExpressions = new[]
|
||||||
|
@ -9,12 +9,11 @@
|
|||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
<Nullable>enable</Nullable>
|
<AnalysisMode>AllDisabledByDefault</AnalysisMode>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||||
@ -23,11 +22,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="..\SharedVersion.cs" />
|
<Compile Include="../SharedVersion.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
<ProjectReference Include="../MediaBrowser.Common/MediaBrowser.Common.csproj" />
|
||||||
|
<ProjectReference Include="../MediaBrowser.Model/MediaBrowser.Model.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@ -50,7 +50,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -16,7 +16,7 @@ namespace Emby.Naming.TV
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
|
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions and passed to <see cref="StubResolver"/>, <see cref="Format3DParser"/> and <see cref="EpisodePathParser"/>.</param>
|
||||||
public EpisodeResolver(NamingOptions options)
|
public EpisodeResolver(NamingOptions options)
|
||||||
{
|
{
|
||||||
_options = options;
|
_options = options;
|
||||||
@ -62,8 +62,7 @@ namespace Emby.Naming.TV
|
|||||||
container = extension.TrimStart('.');
|
container = extension.TrimStart('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
var flags = new FlagParser(_options).GetFlags(path);
|
var format3DResult = Format3DParser.Parse(path, _options);
|
||||||
var format3DResult = new Format3DParser(_options).Parse(flags);
|
|
||||||
|
|
||||||
var parsingResult = new EpisodePathParser(_options)
|
var parsingResult = new EpisodePathParser(_options)
|
||||||
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Emby.Naming.Audio;
|
using Emby.Naming.Audio;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
@ -29,72 +28,75 @@ namespace Emby.Naming.Video
|
|||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
|
||||||
public ExtraResult GetExtraInfo(string path)
|
public ExtraResult GetExtraInfo(string path)
|
||||||
{
|
|
||||||
return _options.VideoExtraRules
|
|
||||||
.Select(i => GetExtraInfo(path, i))
|
|
||||||
.FirstOrDefault(i => i.ExtraType != null) ?? new ExtraResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
private ExtraResult GetExtraInfo(string path, ExtraRule rule)
|
|
||||||
{
|
{
|
||||||
var result = new ExtraResult();
|
var result = new ExtraResult();
|
||||||
|
|
||||||
if (rule.MediaType == MediaType.Audio)
|
for (var i = 0; i < _options.VideoExtraRules.Length; i++)
|
||||||
{
|
{
|
||||||
if (!AudioFileParser.IsAudioFile(path, _options))
|
var rule = _options.VideoExtraRules[i];
|
||||||
|
if (rule.MediaType == MediaType.Audio)
|
||||||
|
{
|
||||||
|
if (!AudioFileParser.IsAudioFile(path, _options))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.MediaType == MediaType.Video)
|
||||||
|
{
|
||||||
|
if (!VideoResolver.IsVideoFile(path, _options))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathSpan = path.AsSpan();
|
||||||
|
if (rule.RuleType == ExtraRuleType.Filename)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
||||||
|
|
||||||
|
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.RuleType == ExtraRuleType.Suffix)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileNameWithoutExtension(pathSpan);
|
||||||
|
|
||||||
|
if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.RuleType == ExtraRuleType.Regex)
|
||||||
|
{
|
||||||
|
var filename = Path.GetFileName(path);
|
||||||
|
|
||||||
|
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
if (regex.IsMatch(filename))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
||||||
|
{
|
||||||
|
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
|
||||||
|
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.ExtraType = rule.ExtraType;
|
||||||
|
result.Rule = rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.ExtraType != null)
|
||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (rule.MediaType == MediaType.Video)
|
|
||||||
{
|
|
||||||
if (!new VideoResolver(_options).IsVideoFile(path))
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule.RuleType == ExtraRuleType.Filename)
|
|
||||||
{
|
|
||||||
var filename = Path.GetFileNameWithoutExtension(path);
|
|
||||||
|
|
||||||
if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.Suffix)
|
|
||||||
{
|
|
||||||
var filename = Path.GetFileNameWithoutExtension(path);
|
|
||||||
|
|
||||||
if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.Regex)
|
|
||||||
{
|
|
||||||
var filename = Path.GetFileName(path);
|
|
||||||
|
|
||||||
var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
|
|
||||||
|
|
||||||
if (regex.IsMatch(filename))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (rule.RuleType == ExtraRuleType.DirectoryName)
|
|
||||||
{
|
|
||||||
var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
|
|
||||||
if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
result.ExtraType = rule.ExtraType;
|
|
||||||
result.Rule = rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using Emby.Naming.Common;
|
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Parses list of flags from filename based on delimiters.
|
|
||||||
/// </summary>
|
|
||||||
public class FlagParser
|
|
||||||
{
|
|
||||||
private readonly NamingOptions _options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="FlagParser"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters.</param>
|
|
||||||
public FlagParser(NamingOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calls GetFlags function with _options.VideoFlagDelimiters parameter.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Path to file.</param>
|
|
||||||
/// <returns>List of found flags.</returns>
|
|
||||||
public string[] GetFlags(string path)
|
|
||||||
{
|
|
||||||
return GetFlags(path, _options.VideoFlagDelimiters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses flags from filename based on delimiters.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">Path to file.</param>
|
|
||||||
/// <param name="delimiters">Delimiters used to extract flags.</param>
|
|
||||||
/// <returns>List of found flags.</returns>
|
|
||||||
public string[] GetFlags(string path, char[] delimiters)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
{
|
|
||||||
return Array.Empty<string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
|
|
||||||
|
|
||||||
var file = Path.GetFileName(path);
|
|
||||||
|
|
||||||
return file.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +1,37 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parste 3D format related flags.
|
/// Parse 3D format related flags.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Format3DParser
|
public static class Format3DParser
|
||||||
{
|
{
|
||||||
private readonly NamingOptions _options;
|
// Static default result to save on allocation costs.
|
||||||
|
private static readonly Format3DResult _defaultResult = new (false, null);
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="Format3DParser"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFlagDelimiters and passes options to <see cref="FlagParser"/>.</param>
|
|
||||||
public Format3DParser(NamingOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse 3D format related flags.
|
/// Parse 3D format related flags.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
|
/// <returns>Returns <see cref="Format3DResult"/> object.</returns>
|
||||||
public Format3DResult Parse(string path)
|
public static Format3DResult Parse(ReadOnlySpan<char> path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
int oldLen = _options.VideoFlagDelimiters.Length;
|
int oldLen = namingOptions.VideoFlagDelimiters.Length;
|
||||||
var delimiters = new char[oldLen + 1];
|
Span<char> delimiters = stackalloc char[oldLen + 1];
|
||||||
_options.VideoFlagDelimiters.CopyTo(delimiters, 0);
|
namingOptions.VideoFlagDelimiters.AsSpan().CopyTo(delimiters);
|
||||||
delimiters[oldLen] = ' ';
|
delimiters[oldLen] = ' ';
|
||||||
|
|
||||||
return Parse(new FlagParser(_options).GetFlags(path, delimiters));
|
return Parse(path, delimiters, namingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Format3DResult Parse(string[] videoFlags)
|
private static Format3DResult Parse(ReadOnlySpan<char> path, ReadOnlySpan<char> delimiters, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
foreach (var rule in _options.Format3DRules)
|
foreach (var rule in namingOptions.Format3DRules)
|
||||||
{
|
{
|
||||||
var result = Parse(videoFlags, rule);
|
var result = Parse(path, rule, delimiters);
|
||||||
|
|
||||||
if (result.Is3D)
|
if (result.Is3D)
|
||||||
{
|
{
|
||||||
@ -47,51 +39,43 @@ namespace Emby.Naming.Video
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Format3DResult();
|
return _defaultResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Format3DResult Parse(string[] videoFlags, Format3DRule rule)
|
private static Format3DResult Parse(ReadOnlySpan<char> path, Format3DRule rule, ReadOnlySpan<char> delimiters)
|
||||||
{
|
{
|
||||||
var result = new Format3DResult();
|
bool is3D = false;
|
||||||
|
string? format3D = null;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(rule.PrecedingToken))
|
// If there's no preceding token we just consider it found
|
||||||
|
var foundPrefix = string.IsNullOrEmpty(rule.PrecedingToken);
|
||||||
|
while (path.Length > 0)
|
||||||
{
|
{
|
||||||
result.Format3D = new[] { rule.Token }.FirstOrDefault(i => videoFlags.Contains(i, StringComparer.OrdinalIgnoreCase));
|
var index = path.IndexOfAny(delimiters);
|
||||||
result.Is3D = !string.IsNullOrEmpty(result.Format3D);
|
if (index == -1)
|
||||||
|
|
||||||
if (result.Is3D)
|
|
||||||
{
|
{
|
||||||
result.Tokens.Add(rule.Token);
|
index = path.Length - 1;
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var foundPrefix = false;
|
|
||||||
string? format = null;
|
|
||||||
|
|
||||||
foreach (var flag in videoFlags)
|
|
||||||
{
|
|
||||||
if (foundPrefix)
|
|
||||||
{
|
|
||||||
result.Tokens.Add(rule.PrecedingToken);
|
|
||||||
|
|
||||||
if (string.Equals(rule.Token, flag, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
format = flag;
|
|
||||||
result.Tokens.Add(rule.Token);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
|
var currentSlice = path[..index];
|
||||||
result.Format3D = format;
|
path = path[(index + 1)..];
|
||||||
|
|
||||||
|
if (!foundPrefix)
|
||||||
|
{
|
||||||
|
foundPrefix = currentSlice.Equals(rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
is3D = foundPrefix && currentSlice.Equals(rule.Token, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (is3D)
|
||||||
|
{
|
||||||
|
format3D = rule.Token;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return is3D ? new Format3DResult(true, format3D) : _defaultResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -10,27 +8,24 @@ namespace Emby.Naming.Video
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
|
/// Initializes a new instance of the <see cref="Format3DResult"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Format3DResult()
|
/// <param name="is3D">A value indicating whether the parsed string contains 3D tokens.</param>
|
||||||
|
/// <param name="format3D">The 3D format. Value might be null if [is3D] is <c>false</c>.</param>
|
||||||
|
public Format3DResult(bool is3D, string? format3D)
|
||||||
{
|
{
|
||||||
Tokens = new List<string>();
|
Is3D = is3D;
|
||||||
|
Format3D = format3D;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether [is3 d].
|
/// Gets a value indicating whether [is3 d].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
|
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
|
||||||
public bool Is3D { get; set; }
|
public bool Is3D { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the format3 d.
|
/// Gets the format3 d.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The format3 d.</value>
|
/// <value>The format3 d.</value>
|
||||||
public string? Format3D { get; set; }
|
public string? Format3D { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the tokens.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The tokens.</value>
|
|
||||||
public List<string> Tokens { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,10 +85,8 @@ namespace Emby.Naming.Video
|
|||||||
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
/// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
|
||||||
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
|
public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
|
||||||
{
|
{
|
||||||
var resolver = new VideoResolver(_options);
|
|
||||||
|
|
||||||
var list = files
|
var list = files
|
||||||
.Where(i => i.IsDirectory || resolver.IsVideoFile(i.FullName) || resolver.IsStubFile(i.FullName))
|
.Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
|
||||||
.OrderBy(i => i.FullName)
|
.OrderBy(i => i.FullName)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
@ -106,9 +107,9 @@ namespace Emby.Naming.Video
|
|||||||
/// Gets the file name without extension.
|
/// Gets the file name without extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The file name without extension.</value>
|
/// <value>The file name without extension.</value>
|
||||||
public string FileNameWithoutExtension => !IsDirectory
|
public ReadOnlySpan<char> FileNameWithoutExtension => !IsDirectory
|
||||||
? System.IO.Path.GetFileNameWithoutExtension(Path)
|
? System.IO.Path.GetFileNameWithoutExtension(Path.AsSpan())
|
||||||
: System.IO.Path.GetFileName(Path);
|
: System.IO.Path.GetFileName(Path.AsSpan());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
@ -12,31 +12,19 @@ namespace Emby.Naming.Video
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves alternative versions and extras from list of video files.
|
/// Resolves alternative versions and extras from list of video files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class VideoListResolver
|
public static class VideoListResolver
|
||||||
{
|
{
|
||||||
private readonly NamingOptions _options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="VideoListResolver"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing CleanStringRegexes and VideoFlagDelimiters and passes options to <see cref="StackResolver"/> and <see cref="VideoResolver"/>.</param>
|
|
||||||
public VideoListResolver(NamingOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves alternative versions and extras from list of video files.
|
/// Resolves alternative versions and extras from list of video files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="files">List of related video files.</param>
|
/// <param name="files">List of related video files.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
|
||||||
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
|
||||||
public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
|
public static IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
|
||||||
{
|
{
|
||||||
var videoResolver = new VideoResolver(_options);
|
|
||||||
|
|
||||||
var videoInfos = files
|
var videoInfos = files
|
||||||
.Select(i => videoResolver.Resolve(i.FullName, i.IsDirectory))
|
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
|
||||||
.OfType<VideoFileInfo>()
|
.OfType<VideoFileInfo>()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@ -46,7 +34,7 @@ namespace Emby.Naming.Video
|
|||||||
.Where(i => i.ExtraType == null)
|
.Where(i => i.ExtraType == null)
|
||||||
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
.Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
|
||||||
|
|
||||||
var stackResult = new StackResolver(_options)
|
var stackResult = new StackResolver(namingOptions)
|
||||||
.Resolve(nonExtras).ToList();
|
.Resolve(nonExtras).ToList();
|
||||||
|
|
||||||
var remainingFiles = videoInfos
|
var remainingFiles = videoInfos
|
||||||
@ -59,23 +47,17 @@ namespace Emby.Naming.Video
|
|||||||
{
|
{
|
||||||
var info = new VideoInfo(stack.Name)
|
var info = new VideoInfo(stack.Name)
|
||||||
{
|
{
|
||||||
Files = stack.Files.Select(i => videoResolver.Resolve(i, stack.IsDirectoryStack))
|
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
|
||||||
.OfType<VideoFileInfo>()
|
.OfType<VideoFileInfo>()
|
||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
info.Year = info.Files[0].Year;
|
info.Year = info.Files[0].Year;
|
||||||
|
|
||||||
var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) };
|
var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
|
||||||
|
|
||||||
var extras = GetExtras(remainingFiles, extraBaseNames);
|
|
||||||
|
|
||||||
if (extras.Count > 0)
|
if (extras.Count > 0)
|
||||||
{
|
{
|
||||||
remainingFiles = remainingFiles
|
|
||||||
.Except(extras)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
info.Extras = extras;
|
info.Extras = extras;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,15 +70,12 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
foreach (var media in standaloneMedia)
|
foreach (var media in standaloneMedia)
|
||||||
{
|
{
|
||||||
var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } };
|
var info = new VideoInfo(media.Name) { Files = new[] { media } };
|
||||||
|
|
||||||
info.Year = info.Files[0].Year;
|
info.Year = info.Files[0].Year;
|
||||||
|
|
||||||
var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
|
remainingFiles.Remove(media);
|
||||||
|
var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
|
||||||
remainingFiles = remainingFiles
|
|
||||||
.Except(extras.Concat(new[] { media }))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
info.Extras = extras;
|
info.Extras = extras;
|
||||||
|
|
||||||
@ -105,8 +84,7 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
if (supportMultiVersion)
|
if (supportMultiVersion)
|
||||||
{
|
{
|
||||||
list = GetVideosGroupedByVersion(list)
|
list = GetVideosGroupedByVersion(list, namingOptions);
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's only one resolved video, use the folder name as well to find extras
|
// If there's only one resolved video, use the folder name as well to find extras
|
||||||
@ -114,19 +92,14 @@ namespace Emby.Naming.Video
|
|||||||
{
|
{
|
||||||
var info = list[0];
|
var info = list[0];
|
||||||
var videoPath = list[0].Files[0].Path;
|
var videoPath = list[0].Files[0].Path;
|
||||||
var parentPath = Path.GetDirectoryName(videoPath);
|
var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(parentPath))
|
if (!parentPath.IsEmpty)
|
||||||
{
|
{
|
||||||
var folderName = Path.GetFileName(parentPath);
|
var folderName = Path.GetFileName(parentPath);
|
||||||
if (!string.IsNullOrEmpty(folderName))
|
if (!folderName.IsEmpty)
|
||||||
{
|
{
|
||||||
var extras = GetExtras(remainingFiles, new List<string> { folderName });
|
var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
|
||||||
|
|
||||||
remainingFiles = remainingFiles
|
|
||||||
.Except(extras)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
extras.AddRange(info.Extras);
|
extras.AddRange(info.Extras);
|
||||||
info.Extras = extras;
|
info.Extras = extras;
|
||||||
}
|
}
|
||||||
@ -164,96 +137,168 @@ namespace Emby.Naming.Video
|
|||||||
// Whatever files are left, just add them
|
// Whatever files are left, just add them
|
||||||
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
|
list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
|
||||||
{
|
{
|
||||||
Files = new List<VideoFileInfo> { i },
|
Files = new[] { i },
|
||||||
Year = i.Year
|
Year = i.Year
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
|
private static List<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
if (videos.Count == 0)
|
if (videos.Count == 0)
|
||||||
{
|
{
|
||||||
return videos;
|
return videos;
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<VideoInfo>();
|
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path.AsSpan()));
|
||||||
|
|
||||||
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path));
|
if (folderName.Length <= 1 || !HaveSameYear(videos))
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(folderName)
|
|
||||||
&& folderName.Length > 1
|
|
||||||
&& videos.All(i => i.Files.Count == 1
|
|
||||||
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path))
|
|
||||||
&& HaveSameYear(videos))
|
|
||||||
{
|
{
|
||||||
var ordered = videos.OrderBy(i => i.Name).ToList();
|
return videos;
|
||||||
|
|
||||||
list.Add(ordered[0]);
|
|
||||||
|
|
||||||
var alternateVersionsLen = ordered.Count - 1;
|
|
||||||
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
|
||||||
for (int i = 0; i < alternateVersionsLen; i++)
|
|
||||||
{
|
|
||||||
alternateVersions[i] = ordered[i + 1].Files[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
list[0].AlternateVersions = alternateVersions;
|
|
||||||
list[0].Name = folderName;
|
|
||||||
var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
|
|
||||||
extras.AddRange(list[0].Extras);
|
|
||||||
list[0].Extras = extras;
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return videos;
|
// Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
|
||||||
}
|
for (var i = 0; i < videos.Count; i++)
|
||||||
|
|
||||||
private bool HaveSameYear(List<VideoInfo> videos)
|
|
||||||
{
|
|
||||||
return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
|
|
||||||
{
|
|
||||||
string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
|
|
||||||
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
// Remove the folder name before cleaning as we don't care about cleaning that part
|
var video = videos[i];
|
||||||
if (folderName.Length <= testFilename.Length)
|
if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
|
||||||
{
|
{
|
||||||
testFilename = testFilename.Substring(folderName.Length).Trim();
|
return videos;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
|
|
||||||
{
|
|
||||||
testFilename = cleanName.Trim().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The CleanStringParser should have removed common keywords etc.
|
|
||||||
return string.IsNullOrEmpty(testFilename)
|
|
||||||
|| testFilename[0] == '-'
|
|
||||||
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
// The list is created and overwritten in the caller, so we are allowed to do in-place sorting
|
||||||
}
|
videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
|
||||||
|
|
||||||
private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames)
|
var list = new List<VideoInfo>
|
||||||
{
|
|
||||||
foreach (var name in baseNames.ToList())
|
|
||||||
{
|
{
|
||||||
var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
|
videos[0]
|
||||||
baseNames.Add(trimmedName);
|
};
|
||||||
|
|
||||||
|
var alternateVersionsLen = videos.Count - 1;
|
||||||
|
var alternateVersions = new VideoFileInfo[alternateVersionsLen];
|
||||||
|
var extras = new List<VideoFileInfo>(list[0].Extras);
|
||||||
|
for (int i = 0; i < alternateVersionsLen; i++)
|
||||||
|
{
|
||||||
|
var video = videos[i + 1];
|
||||||
|
alternateVersions[i] = video.Files[0];
|
||||||
|
extras.AddRange(video.Extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
return remainingFiles
|
list[0].AlternateVersions = alternateVersions;
|
||||||
.Where(i => i.ExtraType != null)
|
list[0].Name = folderName.ToString();
|
||||||
.Where(i => baseNames.Any(b =>
|
list[0].Extras = extras;
|
||||||
i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
.ToList();
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
|
||||||
|
{
|
||||||
|
if (videos.Count == 1)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstYear = videos[0].Year ?? -1;
|
||||||
|
for (var i = 1; i < videos.Count; i++)
|
||||||
|
{
|
||||||
|
if ((videos[i].Year ?? -1) != firstYear)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
|
||||||
|
{
|
||||||
|
var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
|
||||||
|
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the folder name before cleaning as we don't care about cleaning that part
|
||||||
|
if (folderName.Length <= testFilename.Length)
|
||||||
|
{
|
||||||
|
testFilename = testFilename[folderName.Length..].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// There are no span overloads for regex unfortunately
|
||||||
|
var tmpTestFilename = testFilename.ToString();
|
||||||
|
if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
|
||||||
|
{
|
||||||
|
tmpTestFilename = cleanName.Trim().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CleanStringParser should have removed common keywords etc.
|
||||||
|
return string.IsNullOrEmpty(tmpTestFilename)
|
||||||
|
|| testFilename[0] == '-'
|
||||||
|
|| Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
|
||||||
|
{
|
||||||
|
return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
|
||||||
|
{
|
||||||
|
if (baseName.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="remainingFiles">The list of remaining filenames.</param>
|
||||||
|
/// <param name="baseName">The base name to use for the comparison.</param>
|
||||||
|
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
||||||
|
/// <returns>A list of video extras for [baseName].</returns>
|
||||||
|
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
|
||||||
|
{
|
||||||
|
return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="remainingFiles">The list of remaining filenames.</param>
|
||||||
|
/// <param name="firstBaseName">The first base name to use for the comparison.</param>
|
||||||
|
/// <param name="secondBaseName">The second base name to use for the comparison.</param>
|
||||||
|
/// <param name="videoFlagDelimiters">The video flag delimiters.</param>
|
||||||
|
/// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
|
||||||
|
private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
|
||||||
|
{
|
||||||
|
var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
|
||||||
|
var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
|
||||||
|
|
||||||
|
var result = new List<VideoFileInfo>();
|
||||||
|
for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
|
||||||
|
{
|
||||||
|
var file = remainingFiles[pos];
|
||||||
|
if (file.ExtraType == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filename = file.FileNameWithoutExtension;
|
||||||
|
if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
|
||||||
|
|| StartsWith(filename, secondBaseName, trimmedSecondBaseName))
|
||||||
|
{
|
||||||
|
result.Add(file);
|
||||||
|
remainingFiles.RemoveAt(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,46 +1,36 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
|
|
||||||
namespace Emby.Naming.Video
|
namespace Emby.Naming.Video
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves <see cref="VideoFileInfo"/> from file path.
|
/// Resolves <see cref="VideoFileInfo"/> from file path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class VideoResolver
|
public static class VideoResolver
|
||||||
{
|
{
|
||||||
private readonly NamingOptions _options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="VideoResolver"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"><see cref="NamingOptions"/> object containing VideoFileExtensions, StubFileExtensions, CleanStringRegexes and CleanDateTimeRegexes
|
|
||||||
/// and passes options in <see cref="StubResolver"/>, <see cref="FlagParser"/>, <see cref="Format3DParser"/> and <see cref="ExtraResolver"/>.</param>
|
|
||||||
public VideoResolver(NamingOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the directory.
|
/// Resolves the directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
public VideoFileInfo? ResolveDirectory(string? path)
|
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
return Resolve(path, true);
|
return Resolve(path, true, namingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the file.
|
/// Resolves the file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
public VideoFileInfo? ResolveFile(string? path)
|
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
return Resolve(path, false);
|
return Resolve(path, false, namingOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -48,10 +38,11 @@ namespace Emby.Naming.Video
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
|
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
|
/// <param name="parseName">Whether or not the name should be parsed for info.</param>
|
||||||
/// <returns>VideoFileInfo.</returns>
|
/// <returns>VideoFileInfo.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
|
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
|
||||||
public VideoFileInfo? Resolve(string? path, bool isDirectory, bool parseName = true)
|
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
{
|
{
|
||||||
@ -59,18 +50,18 @@ namespace Emby.Naming.Video
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool isStub = false;
|
bool isStub = false;
|
||||||
string? container = null;
|
ReadOnlySpan<char> container = ReadOnlySpan<char>.Empty;
|
||||||
string? stubType = null;
|
string? stubType = null;
|
||||||
|
|
||||||
if (!isDirectory)
|
if (!isDirectory)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
|
|
||||||
// Check supported extensions
|
// Check supported extensions
|
||||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
if (!namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// It's not supported. Check stub extensions
|
// It's not supported. Check stub extensions
|
||||||
if (!StubResolver.TryResolveFile(path, _options, out stubType))
|
if (!StubResolver.TryResolveFile(path, namingOptions, out stubType))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -81,25 +72,22 @@ namespace Emby.Naming.Video
|
|||||||
container = extension.TrimStart('.');
|
container = extension.TrimStart('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
var flags = new FlagParser(_options).GetFlags(path);
|
var format3DResult = Format3DParser.Parse(path, namingOptions);
|
||||||
var format3DResult = new Format3DParser(_options).Parse(flags);
|
|
||||||
|
|
||||||
var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
|
var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
|
||||||
|
|
||||||
var name = isDirectory
|
var name = Path.GetFileNameWithoutExtension(path);
|
||||||
? Path.GetFileName(path)
|
|
||||||
: Path.GetFileNameWithoutExtension(path);
|
|
||||||
|
|
||||||
int? year = null;
|
int? year = null;
|
||||||
|
|
||||||
if (parseName)
|
if (parseName)
|
||||||
{
|
{
|
||||||
var cleanDateTimeResult = CleanDateTime(name);
|
var cleanDateTimeResult = CleanDateTime(name, namingOptions);
|
||||||
name = cleanDateTimeResult.Name;
|
name = cleanDateTimeResult.Name;
|
||||||
year = cleanDateTimeResult.Year;
|
year = cleanDateTimeResult.Year;
|
||||||
|
|
||||||
if (extraResult.ExtraType == null
|
if (extraResult.ExtraType == null
|
||||||
&& TryCleanString(name, out ReadOnlySpan<char> newName))
|
&& TryCleanString(name, namingOptions, out ReadOnlySpan<char> newName))
|
||||||
{
|
{
|
||||||
name = newName.ToString();
|
name = newName.ToString();
|
||||||
}
|
}
|
||||||
@ -107,7 +95,7 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
return new VideoFileInfo(
|
return new VideoFileInfo(
|
||||||
path: path,
|
path: path,
|
||||||
container: container,
|
container: container.IsEmpty ? null : container.ToString(),
|
||||||
isStub: isStub,
|
isStub: isStub,
|
||||||
name: name,
|
name: name,
|
||||||
year: year,
|
year: year,
|
||||||
@ -123,43 +111,47 @@ namespace Emby.Naming.Video
|
|||||||
/// Determines if path is video file based on extension.
|
/// Determines if path is video file based on extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>True if is video file.</returns>
|
/// <returns>True if is video file.</returns>
|
||||||
public bool IsVideoFile(string path)
|
public static bool IsVideoFile(string path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
return _options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
return namingOptions.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if path is video file stub based on extension.
|
/// Determines if path is video file stub based on extension.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Path to file.</param>
|
/// <param name="path">Path to file.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>True if is video file stub.</returns>
|
/// <returns>True if is video file stub.</returns>
|
||||||
public bool IsStubFile(string path)
|
public static bool IsStubFile(string path, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path.AsSpan());
|
||||||
return _options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
return namingOptions.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to clean name of clutter.
|
/// Tries to clean name of clutter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">Raw name.</param>
|
/// <param name="name">Raw name.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="newName">Clean name.</param>
|
/// <param name="newName">Clean name.</param>
|
||||||
/// <returns>True if cleaning of name was successful.</returns>
|
/// <returns>True if cleaning of name was successful.</returns>
|
||||||
public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
|
public static bool TryCleanString([NotNullWhen(true)] string? name, NamingOptions namingOptions, out ReadOnlySpan<char> newName)
|
||||||
{
|
{
|
||||||
return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
|
return CleanStringParser.TryClean(name, namingOptions.CleanStringRegexes, out newName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to get name and year from raw name.
|
/// Tries to get name and year from raw name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">Raw name.</param>
|
/// <param name="name">Raw name.</param>
|
||||||
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
|
/// <returns>Returns <see cref="CleanDateTimeResult"/> with name and optional year.</returns>
|
||||||
public CleanDateTimeResult CleanDateTime(string name)
|
public static CleanDateTimeResult CleanDateTime(string name, NamingOptions namingOptions)
|
||||||
{
|
{
|
||||||
return CleanDateTimeParser.Clean(name, _options.CleanDateTimeRegexes);
|
return CleanDateTimeParser.Clean(name, namingOptions.CleanDateTimeRegexes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,6 @@
|
|||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -77,7 +77,6 @@ namespace Emby.Notifications
|
|||||||
{
|
{
|
||||||
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
|
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
|
||||||
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
|
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
|
||||||
_appHost.HasUpdateAvailableChanged += OnAppHostHasUpdateAvailableChanged;
|
|
||||||
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
|
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@ -132,25 +131,6 @@ namespace Emby.Notifications
|
|||||||
return _config.GetConfiguration<NotificationOptions>("notifications");
|
return _config.GetConfiguration<NotificationOptions>("notifications");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnAppHostHasUpdateAvailableChanged(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (!_appHost.HasUpdateAvailable)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var type = NotificationType.ApplicationUpdateAvailable.ToString();
|
|
||||||
|
|
||||||
var notification = new NotificationRequest
|
|
||||||
{
|
|
||||||
Description = "Please see jellyfin.org for details.",
|
|
||||||
NotificationType = type,
|
|
||||||
Name = _localization.GetLocalizedString("NewVersionIsAvailable")
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendNotification(notification, null).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
|
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
|
||||||
{
|
{
|
||||||
if (!FilterItem(e.Item))
|
if (!FilterItem(e.Item))
|
||||||
@ -325,7 +305,6 @@ namespace Emby.Notifications
|
|||||||
|
|
||||||
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
|
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
|
||||||
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
|
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
|
||||||
_appHost.HasUpdateAvailableChanged -= OnAppHostHasUpdateAvailableChanged;
|
|
||||||
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
|
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
@ -22,10 +22,6 @@
|
|||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
|
@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
CachePath = cacheDirectoryPath;
|
CachePath = cacheDirectoryPath;
|
||||||
WebPath = webDirectoryPath;
|
WebPath = webDirectoryPath;
|
||||||
|
|
||||||
DataPath = Path.Combine(ProgramDataPath, "data");
|
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
/// Gets the folder path to the data directory.
|
/// Gets the folder path to the data directory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The data directory.</value>
|
/// <value>The data directory.</value>
|
||||||
public string DataPath
|
public string DataPath => _dataPath;
|
||||||
{
|
|
||||||
get => _dataPath;
|
|
||||||
private set => _dataPath = Directory.CreateDirectory(value).FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string VirtualDataPath => "%AppDataPath%";
|
public string VirtualDataPath => "%AppDataPath%";
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
|
|
||||||
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The _configuration sync lock.
|
||||||
|
/// </summary>
|
||||||
|
private readonly object _configurationSyncLock = new object();
|
||||||
|
|
||||||
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
||||||
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
||||||
|
|
||||||
@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private bool _configurationLoaded;
|
private bool _configurationLoaded;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _configuration sync lock.
|
|
||||||
/// </summary>
|
|
||||||
private readonly object _configurationSyncLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _configuration.
|
/// The _configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -297,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public object GetConfiguration(string key)
|
public object GetConfiguration(string key)
|
||||||
{
|
{
|
||||||
return _configurations.GetOrAdd(key, k =>
|
return _configurations.GetOrAdd(
|
||||||
{
|
key,
|
||||||
var file = GetConfigurationFile(key);
|
(k, configurationManager) =>
|
||||||
|
|
||||||
var configurationInfo = _configurationStores
|
|
||||||
.FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
|
|
||||||
|
|
||||||
if (configurationInfo == null)
|
|
||||||
{
|
{
|
||||||
throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
|
var file = configurationManager.GetConfigurationFile(k);
|
||||||
}
|
|
||||||
|
|
||||||
var configurationType = configurationInfo.ConfigurationType;
|
var configurationInfo = Array.Find(
|
||||||
|
configurationManager._configurationStores,
|
||||||
|
i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
lock (_configurationSyncLock)
|
if (configurationInfo == null)
|
||||||
{
|
{
|
||||||
return LoadConfiguration(file, configurationType);
|
throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
var configurationType = configurationInfo.ConfigurationType;
|
||||||
|
|
||||||
|
lock (configurationManager._configurationSyncLock)
|
||||||
|
{
|
||||||
|
return configurationManager.LoadConfiguration(file, configurationType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private object LoadConfiguration(string path, Type configurationType)
|
private object LoadConfiguration(string path, Type configurationType)
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -35,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
|
// Note: CreateInstance returns null for Nullable<T>, e.g. CreateInstance(typeof(int?)) returns null.
|
||||||
|
configuration = Activator.CreateInstance(type)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
using var stream = new MemoryStream(buffer?.Length ?? 0);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -101,7 +103,6 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Prometheus.DotNetRuntime;
|
using Prometheus.DotNetRuntime;
|
||||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
|
||||||
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
|
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations
|
namespace Emby.Server.Implementations
|
||||||
@ -148,13 +149,7 @@ namespace Emby.Server.Implementations
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OperatingSystem.Id == OperatingSystemId.Windows
|
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
|
||||||
|| OperatingSystem.Id == OperatingSystemId.Darwin)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -719,7 +714,7 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
|
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
|
||||||
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
||||||
logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
|
logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name);
|
||||||
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
||||||
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
||||||
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
||||||
@ -1096,11 +1091,10 @@ namespace Emby.Server.Implementations
|
|||||||
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
||||||
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
||||||
CachePath = ApplicationPaths.CachePath,
|
CachePath = ApplicationPaths.CachePath,
|
||||||
OperatingSystem = OperatingSystem.Id.ToString(),
|
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
|
||||||
OperatingSystemDisplayName = OperatingSystem.Name,
|
OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
|
||||||
CanSelfRestart = CanSelfRestart,
|
CanSelfRestart = CanSelfRestart,
|
||||||
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
||||||
HasUpdateAvailable = HasUpdateAvailable,
|
|
||||||
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
||||||
ServerName = FriendlyName,
|
ServerName = FriendlyName,
|
||||||
LocalAddress = GetSmartApiUrl(source),
|
LocalAddress = GetSmartApiUrl(source),
|
||||||
@ -1116,16 +1110,16 @@ namespace Emby.Server.Implementations
|
|||||||
.Select(i => new WakeOnLanInfo(i))
|
.Select(i => new WakeOnLanInfo(i))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
|
public PublicSystemInfo GetPublicSystemInfo(IPAddress address)
|
||||||
{
|
{
|
||||||
return new PublicSystemInfo
|
return new PublicSystemInfo
|
||||||
{
|
{
|
||||||
Version = ApplicationVersionString,
|
Version = ApplicationVersionString,
|
||||||
ProductName = ApplicationProductName,
|
ProductName = ApplicationProductName,
|
||||||
Id = SystemId,
|
Id = SystemId,
|
||||||
OperatingSystem = OperatingSystem.Id.ToString(),
|
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
|
||||||
ServerName = FriendlyName,
|
ServerName = FriendlyName,
|
||||||
LocalAddress = GetSmartApiUrl(source),
|
LocalAddress = GetSmartApiUrl(address),
|
||||||
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1134,7 +1128,7 @@ namespace Emby.Server.Implementations
|
|||||||
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
@ -1143,7 +1137,7 @@ namespace Emby.Server.Implementations
|
|||||||
return PublishedServerUrl.Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
string smart = NetManager.GetBindInterface(remoteAddr, out port);
|
||||||
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
||||||
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@ -1206,14 +1200,14 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string GetLocalApiUrl(string host, string scheme = null, int? port = null)
|
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
|
||||||
{
|
{
|
||||||
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
||||||
// not. For consistency, always trim the trailing slash.
|
// not. For consistency, always trim the trailing slash.
|
||||||
return new UriBuilder
|
return new UriBuilder
|
||||||
{
|
{
|
||||||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
||||||
Host = host,
|
Host = hostname,
|
||||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
||||||
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||||
}.ToString().TrimEnd('/');
|
}.ToString().TrimEnd('/');
|
||||||
@ -1250,26 +1244,6 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
protected abstract void ShutdownInternal();
|
protected abstract void ShutdownInternal();
|
||||||
|
|
||||||
public event EventHandler HasUpdateAvailableChanged;
|
|
||||||
|
|
||||||
private bool _hasUpdateAvailable;
|
|
||||||
|
|
||||||
public bool HasUpdateAvailable
|
|
||||||
{
|
|
||||||
get => _hasUpdateAvailable;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
var fireEvent = value && !_hasUpdateAvailable;
|
|
||||||
|
|
||||||
_hasUpdateAvailable = value;
|
|
||||||
|
|
||||||
if (fireEvent)
|
|
||||||
{
|
|
||||||
HasUpdateAvailableChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
||||||
{
|
{
|
||||||
var assemblies = _allConcreteTypes
|
var assemblies = _allConcreteTypes
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@ -9,7 +11,7 @@ using System.Threading.Tasks;
|
|||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using MediaBrowser.Common.Progress;
|
using MediaBrowser.Common.Progress;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.Where(i => i != null)
|
.Where(i => i != null)
|
||||||
.GroupBy(x => x.Id)
|
.GroupBy(x => x!.Id) // We removed the null values
|
||||||
.Select(x => x.First())
|
.Select(x => x.First())
|
||||||
.ToList();
|
.ToList()!; // Again... the list doesn't contain any null values
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -80,12 +82,10 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
|
|
||||||
internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded)
|
internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded)
|
||||||
{
|
{
|
||||||
var existingFolders = FindFolders(path)
|
var existingFolder = FindFolders(path).FirstOrDefault();
|
||||||
.ToList();
|
if (existingFolder != null)
|
||||||
|
|
||||||
if (existingFolders.Count > 0)
|
|
||||||
{
|
{
|
||||||
return existingFolders[0];
|
return existingFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!createIfNeeded)
|
if (!createIfNeeded)
|
||||||
@ -162,9 +162,9 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
DateCreated = DateTime.UtcNow
|
DateCreated = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
parentFolder.AddChild(collection, CancellationToken.None);
|
parentFolder.AddChild(collection);
|
||||||
|
|
||||||
if (options.ItemIdList.Length > 0)
|
if (options.ItemIdList.Count > 0)
|
||||||
{
|
{
|
||||||
await AddToCollectionAsync(
|
await AddToCollectionAsync(
|
||||||
collection.Id,
|
collection.Id,
|
||||||
@ -248,11 +248,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
|
|
||||||
if (fireEvent)
|
if (fireEvent)
|
||||||
{
|
{
|
||||||
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs
|
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
|
||||||
{
|
|
||||||
Collection = collection,
|
|
||||||
ItemsChanged = itemList
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -304,11 +300,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
},
|
},
|
||||||
RefreshPriority.High);
|
RefreshPriority.High);
|
||||||
|
|
||||||
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
|
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
|
||||||
{
|
|
||||||
Collection = collection,
|
|
||||||
ItemsChanged = itemList
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
|
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
|
||||||
{
|
{
|
||||||
if (row[1].SQLiteType != SQLiteType.Null)
|
if (row.TryGetString(1, out var columnName))
|
||||||
{
|
{
|
||||||
var name = row[1].ToString();
|
columnNames.Add(columnName);
|
||||||
|
|
||||||
columnNames.Add(name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
public class ManagedConnection : IDisposable
|
public class ManagedConnection : IDisposable
|
||||||
{
|
{
|
||||||
private SQLiteDatabaseConnection _db;
|
private SQLiteDatabaseConnection? _db;
|
||||||
private readonly SemaphoreSlim _writeLock;
|
private readonly SemaphoreSlim _writeLock;
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
@ -54,12 +54,12 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return _db.RunInTransaction(action, mode);
|
return _db.RunInTransaction(action, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql)
|
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
|
||||||
{
|
{
|
||||||
return _db.Query(sql);
|
return _db.Query(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql, params object[] values)
|
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
|
||||||
{
|
{
|
||||||
return _db.Query(sql, values);
|
return _db.Query(sql, values);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#nullable disable
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Guid ReadGuidFromBlob(this IResultSetValue result)
|
public static Guid ReadGuidFromBlob(this ResultSetValue result)
|
||||||
{
|
{
|
||||||
return new Guid(result.ToBlob());
|
return new Guid(result.ToBlob());
|
||||||
}
|
}
|
||||||
@ -85,7 +86,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
private static string GetDateTimeKindFormat(DateTimeKind kind)
|
private static string GetDateTimeKindFormat(DateTimeKind kind)
|
||||||
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
|
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
|
||||||
|
|
||||||
public static DateTime ReadDateTime(this IResultSetValue result)
|
public static DateTime ReadDateTime(this ResultSetValue result)
|
||||||
{
|
{
|
||||||
var dateText = result.ToString();
|
var dateText = result.ToString();
|
||||||
|
|
||||||
@ -96,49 +97,139 @@ namespace Emby.Server.Implementations.Data
|
|||||||
DateTimeStyles.None).ToUniversalTime();
|
DateTimeStyles.None).ToUniversalTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DateTime? TryReadDateTime(this IResultSetValue result)
|
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
|
||||||
{
|
{
|
||||||
var dateText = result.ToString();
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dateText = item.ToString();
|
||||||
|
|
||||||
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
|
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
|
||||||
{
|
{
|
||||||
return dateTimeResult.ToUniversalTime();
|
result = dateTimeResult.ToUniversalTime();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
result = default;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsDBNull(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
|
||||||
{
|
{
|
||||||
return result[index].SQLiteType == SQLiteType.Null;
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ReadGuidFromBlob();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetString(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool IsDbNull(this ResultSetValue result)
|
||||||
|
{
|
||||||
|
return result.SQLiteType == SQLiteType.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
|
||||||
{
|
{
|
||||||
return result[index].ToString();
|
return result[index].ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool GetBoolean(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
|
||||||
{
|
{
|
||||||
return result[index].ToBool();
|
return result[index].ToBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int GetInt32(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
|
||||||
{
|
{
|
||||||
return result[index].ToInt();
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ToBool();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
|
||||||
|
{
|
||||||
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ToInt();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
|
||||||
{
|
{
|
||||||
return result[index].ToInt64();
|
return result[index].ToInt64();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float GetFloat(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
|
||||||
{
|
{
|
||||||
return result[index].ToFloat();
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ToInt64();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index)
|
public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
|
||||||
|
{
|
||||||
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ToFloat();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
|
||||||
|
{
|
||||||
|
var item = reader[index];
|
||||||
|
if (item.IsDbNull())
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = item.ToDouble();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index)
|
||||||
{
|
{
|
||||||
return result[index].ReadGuidFromBlob();
|
return result[index].ReadGuidFromBlob();
|
||||||
}
|
}
|
||||||
@ -350,7 +441,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement)
|
public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
|
||||||
{
|
{
|
||||||
while (statement.MoveNext())
|
while (statement.MoveNext())
|
||||||
{
|
{
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -348,16 +350,16 @@ namespace Emby.Server.Implementations.Data
|
|||||||
/// Read a row from the specified reader into the provided userData object.
|
/// Read a row from the specified reader into the provided userData object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reader"></param>
|
/// <param name="reader"></param>
|
||||||
private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
|
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
|
||||||
{
|
{
|
||||||
var userData = new UserItemData();
|
var userData = new UserItemData();
|
||||||
|
|
||||||
userData.Key = reader[0].ToString();
|
userData.Key = reader[0].ToString();
|
||||||
// userData.UserId = reader[1].ReadGuidFromBlob();
|
// userData.UserId = reader[1].ReadGuidFromBlob();
|
||||||
|
|
||||||
if (reader[2].SQLiteType != SQLiteType.Null)
|
if (reader.TryGetDouble(2, out var rating))
|
||||||
{
|
{
|
||||||
userData.Rating = reader[2].ToDouble();
|
userData.Rating = rating;
|
||||||
}
|
}
|
||||||
|
|
||||||
userData.Played = reader[3].ToBool();
|
userData.Played = reader[3].ToBool();
|
||||||
@ -365,19 +367,19 @@ namespace Emby.Server.Implementations.Data
|
|||||||
userData.IsFavorite = reader[5].ToBool();
|
userData.IsFavorite = reader[5].ToBool();
|
||||||
userData.PlaybackPositionTicks = reader[6].ToInt64();
|
userData.PlaybackPositionTicks = reader[6].ToInt64();
|
||||||
|
|
||||||
if (reader[7].SQLiteType != SQLiteType.Null)
|
if (reader.TryReadDateTime(7, out var lastPlayedDate))
|
||||||
{
|
{
|
||||||
userData.LastPlayedDate = reader[7].TryReadDateTime();
|
userData.LastPlayedDate = lastPlayedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reader[8].SQLiteType != SQLiteType.Null)
|
if (reader.TryGetInt32(8, out var audioStreamIndex))
|
||||||
{
|
{
|
||||||
userData.AudioStreamIndex = reader[8].ToInt();
|
userData.AudioStreamIndex = audioStreamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reader[9].SQLiteType != SQLiteType.Null)
|
if (reader.TryGetInt32(9, out var subtitleStreamIndex))
|
||||||
{
|
{
|
||||||
userData.SubtitleStreamIndex = reader[9].ToInt();
|
userData.SubtitleStreamIndex = subtitleStreamIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
return userData;
|
return userData;
|
||||||
|
@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
/// This holds all the types in the running assemblies
|
/// This holds all the types in the running assemblies
|
||||||
/// so that we can de-serialize properly when we don't have strong types.
|
/// so that we can de-serialize properly when we don't have strong types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>();
|
private readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the type.
|
/// Gets the type.
|
||||||
@ -21,26 +21,16 @@ namespace Emby.Server.Implementations.Data
|
|||||||
/// <param name="typeName">Name of the type.</param>
|
/// <param name="typeName">Name of the type.</param>
|
||||||
/// <returns>Type.</returns>
|
/// <returns>Type.</returns>
|
||||||
/// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
|
/// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
|
||||||
public Type GetType(string typeName)
|
public Type? GetType(string typeName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(typeName))
|
if (string.IsNullOrEmpty(typeName))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(typeName));
|
throw new ArgumentNullException(nameof(typeName));
|
||||||
}
|
}
|
||||||
|
|
||||||
return _typeMap.GetOrAdd(typeName, LookupType);
|
return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
|
||||||
}
|
.Select(a => a.GetType(k))
|
||||||
|
.FirstOrDefault(t => t != null));
|
||||||
/// <summary>
|
|
||||||
/// Lookups the type.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="typeName">Name of the type.</param>
|
|
||||||
/// <returns>Type.</returns>
|
|
||||||
private Type LookupType(string typeName)
|
|
||||||
{
|
|
||||||
return AppDomain.CurrentDomain.GetAssemblies()
|
|
||||||
.Select(a => a.GetType(typeName))
|
|
||||||
.FirstOrDefault(t => t != null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
|
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
|
||||||
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
|
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
|
||||||
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
|
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
|
||||||
|
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||||
@ -23,15 +24,15 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.8" />
|
||||||
<PackageReference Include="Mono.Nat" Version="3.0.1" />
|
<PackageReference Include="Mono.Nat" Version="3.0.1" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.0.0" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
|
||||||
<PackageReference Include="sharpcompress" Version="0.28.2" />
|
<PackageReference Include="sharpcompress" Version="0.28.3" />
|
||||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.2.0" />
|
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||||
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
|
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
@ -43,11 +44,13 @@
|
|||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
|
||||||
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
||||||
<NoWarn>AD0001</NoWarn>
|
<NoWarn>AD0001</NoWarn>
|
||||||
<AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
|
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)' == 'Release'">
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -106,8 +108,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
NatUtility.StartDiscovery();
|
NatUtility.StartDiscovery();
|
||||||
|
|
||||||
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
|
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
|
||||||
|
|
||||||
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Stop()
|
private void Stop()
|
||||||
@ -118,13 +118,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
|
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
|
||||||
|
|
||||||
_timer?.Dispose();
|
_timer?.Dispose();
|
||||||
|
|
||||||
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
|
||||||
{
|
|
||||||
NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
|
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -56,8 +54,8 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_udpServer = new UdpServer(_logger, _appHost, _config);
|
_udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
|
||||||
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
|
_udpServer.Start(_cancellationTokenSource.Token);
|
||||||
}
|
}
|
||||||
catch (SocketException ex)
|
catch (SocketException ex)
|
||||||
{
|
{
|
||||||
|
@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
|
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
|
||||||
|
|
||||||
private readonly object _syncLock = new object();
|
private readonly object _syncLock = new object();
|
||||||
private Timer _updateTimer;
|
private Timer? _updateTimer;
|
||||||
|
|
||||||
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
|
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
|
||||||
{
|
{
|
||||||
@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
|
private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
|
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
|
||||||
{
|
{
|
||||||
@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
|
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
|
if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem>? keys))
|
||||||
{
|
{
|
||||||
keys = new List<BaseItem>();
|
keys = new List<BaseItem>();
|
||||||
_changedItems[e.UserId] = keys;
|
_changedItems[e.UserId] = keys;
|
||||||
@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateTimerCallback(object state)
|
private void UpdateTimerCallback(object? state)
|
||||||
{
|
{
|
||||||
lock (_syncLock)
|
lock (_syncLock)
|
||||||
{
|
{
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Security;
|
using MediaBrowser.Controller.Security;
|
||||||
@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
{
|
{
|
||||||
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
|
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
|
||||||
{
|
{
|
||||||
return (AuthorizationInfo)cached;
|
return (AuthorizationInfo)cached!; // Cache should never contain null
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetAuthorization(requestContext);
|
return GetAuthorization(requestContext);
|
||||||
@ -55,15 +55,15 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
}
|
}
|
||||||
|
|
||||||
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
|
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
|
||||||
in Dictionary<string, string> auth,
|
in Dictionary<string, string>? auth,
|
||||||
in IHeaderDictionary headers,
|
in IHeaderDictionary headers,
|
||||||
in IQueryCollection queryString)
|
in IQueryCollection queryString)
|
||||||
{
|
{
|
||||||
string deviceId = null;
|
string? deviceId = null;
|
||||||
string device = null;
|
string? device = null;
|
||||||
string client = null;
|
string? client = null;
|
||||||
string version = null;
|
string? version = null;
|
||||||
string token = null;
|
string? token = null;
|
||||||
|
|
||||||
if (auth != null)
|
if (auth != null)
|
||||||
{
|
{
|
||||||
@ -206,7 +206,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
/// <param name="httpReq">The HTTP req.</param>
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
|
private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
|
||||||
{
|
{
|
||||||
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
|
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
auth = httpReq.Request.Headers[HeaderNames.Authorization];
|
auth = httpReq.Request.Headers[HeaderNames.Authorization];
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetAuthorization(auth);
|
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpReq">The HTTP req.</param>
|
/// <param name="httpReq">The HTTP req.</param>
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
|
private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
|
||||||
{
|
{
|
||||||
var auth = httpReq.Headers["X-Emby-Authorization"];
|
var auth = httpReq.Headers["X-Emby-Authorization"];
|
||||||
|
|
||||||
@ -232,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
auth = httpReq.Headers[HeaderNames.Authorization];
|
auth = httpReq.Headers[HeaderNames.Authorization];
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetAuthorization(auth);
|
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -240,43 +240,43 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="authorizationHeader">The authorization header.</param>
|
/// <param name="authorizationHeader">The authorization header.</param>
|
||||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||||
private Dictionary<string, string> GetAuthorization(string authorizationHeader)
|
private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
|
||||||
{
|
{
|
||||||
if (authorizationHeader == null)
|
if (authorizationHeader == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var parts = authorizationHeader.Split(' ', 2);
|
var firstSpace = authorizationHeader.IndexOf(' ');
|
||||||
|
|
||||||
// There should be at least to parts
|
// There should be at least two parts
|
||||||
if (parts.Length != 2)
|
if (firstSpace == -1)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var acceptedNames = new[] { "MediaBrowser", "Emby" };
|
var name = authorizationHeader[..firstSpace];
|
||||||
|
|
||||||
// It has to be a digest request
|
if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
|
||||||
if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
|
&& !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove uptil the first space
|
authorizationHeader = authorizationHeader[(firstSpace + 1)..];
|
||||||
authorizationHeader = parts[1];
|
|
||||||
parts = authorizationHeader.Split(',');
|
|
||||||
|
|
||||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var item in parts)
|
foreach (var item in authorizationHeader.Split(','))
|
||||||
{
|
{
|
||||||
var param = item.Trim().Split('=', 2);
|
var trimmedItem = item.Trim();
|
||||||
|
var firstEqualsSign = trimmedItem.IndexOf('=');
|
||||||
|
|
||||||
if (param.Length == 2)
|
if (firstEqualsSign > 0)
|
||||||
{
|
{
|
||||||
var value = NormalizeValue(param[1].Trim('"'));
|
var key = trimmedItem[..firstEqualsSign].ToString();
|
||||||
result[param[0]] = value;
|
var value = NormalizeValue(trimmedItem[(firstEqualsSign + 1)..].Trim('"').ToString());
|
||||||
|
result[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,14 +36,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
return GetSession((HttpContext)requestContext);
|
return GetSession((HttpContext)requestContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User GetUser(HttpContext requestContext)
|
public User? GetUser(HttpContext requestContext)
|
||||||
{
|
{
|
||||||
var session = GetSession(requestContext);
|
var session = GetSession(requestContext);
|
||||||
|
|
||||||
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
|
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User GetUser(object requestContext)
|
public User? GetUser(object requestContext)
|
||||||
{
|
{
|
||||||
return GetUser(((HttpRequest)requestContext).HttpContext);
|
return GetUser(((HttpRequest)requestContext).HttpContext);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.IO.Pipelines;
|
using System.IO.Pipelines;
|
||||||
@ -9,7 +7,7 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -6,11 +6,11 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.System;
|
using MediaBrowser.Model.System;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.IO
|
namespace Emby.Server.Implementations.IO
|
||||||
{
|
{
|
||||||
@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
|
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
|
||||||
private readonly string _tempPath;
|
private readonly string _tempPath;
|
||||||
private static readonly bool _isEnvironmentCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
|
||||||
|
|
||||||
public ManagedFileSystem(
|
public ManagedFileSystem(
|
||||||
ILogger<ManagedFileSystem> logger,
|
ILogger<ManagedFileSystem> logger,
|
||||||
@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
/// <param name="filename">The filename.</param>
|
/// <param name="filename">The filename.</param>
|
||||||
/// <returns>System.String.</returns>
|
/// <returns>System.String.</returns>
|
||||||
/// <exception cref="ArgumentNullException">filename</exception>
|
/// <exception cref="ArgumentNullException">filename</exception>
|
||||||
public virtual string ResolveShortcut(string filename)
|
public virtual string? ResolveShortcut(string filename)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(filename))
|
if (string.IsNullOrEmpty(filename))
|
||||||
{
|
{
|
||||||
@ -243,8 +243,8 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
result.Length = fileInfo.Length;
|
result.Length = fileInfo.Length;
|
||||||
|
|
||||||
// Issue #2354 get the size of files behind symbolic links
|
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
|
||||||
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -401,7 +401,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual void SetHidden(string path, bool isHidden)
|
public virtual void SetHidden(string path, bool isHidden)
|
||||||
{
|
{
|
||||||
if (OperatingSystem.Id != OperatingSystemId.Windows)
|
if (!OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -425,7 +425,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
|
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
|
||||||
{
|
{
|
||||||
if (OperatingSystem.Id != OperatingSystemId.Windows)
|
if (!OperatingSystem.IsWindows())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -601,7 +601,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return GetFiles(path, null, false, recursive);
|
return GetFiles(path, null, false, recursive);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||||
{
|
{
|
||||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
@ -618,13 +618,13 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
files = files.Where(i =>
|
files = files.Where(i =>
|
||||||
{
|
{
|
||||||
var ext = i.Extension;
|
var ext = i.Extension.AsSpan();
|
||||||
if (ext == null)
|
if (ext.IsEmpty)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
|
return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -636,8 +636,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
var directoryInfo = new DirectoryInfo(path);
|
var directoryInfo = new DirectoryInfo(path);
|
||||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
|
return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", enumerationOptions));
|
||||||
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
|
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
|
||||||
@ -655,7 +654,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
return GetFilePaths(path, null, false, recursive);
|
return GetFilePaths(path, null, false, recursive);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||||
{
|
{
|
||||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
@ -672,13 +671,13 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
files = files.Where(i =>
|
files = files.Where(i =>
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(i);
|
var ext = Path.GetExtension(i.AsSpan());
|
||||||
if (ext == null)
|
if (ext.IsEmpty)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
|
return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public string Extension => ".mblink";
|
public string Extension => ".mblink";
|
||||||
|
|
||||||
public string Resolve(string shortcutPath)
|
public string? Resolve(string shortcutPath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(shortcutPath))
|
if (string.IsNullOrEmpty(shortcutPath))
|
||||||
{
|
{
|
||||||
|
@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
{
|
{
|
||||||
public class StreamHelper : IStreamHelper
|
public class StreamHelper : IStreamHelper
|
||||||
{
|
{
|
||||||
public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
|
public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||||
try
|
try
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations
|
namespace Emby.Server.Implementations
|
||||||
{
|
{
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -191,7 +193,7 @@ namespace Emby.Server.Implementations.Images
|
|||||||
InputPaths = GetStripCollageImagePaths(primaryItem, items).ToArray()
|
InputPaths = GetStripCollageImagePaths(primaryItem, items).ToArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.InputPaths.Length == 0)
|
if (options.InputPaths.Count == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
if (parent != null)
|
if (parent != null)
|
||||||
{
|
{
|
||||||
// Don't resolve these into audio files
|
// Don't resolve these into audio files
|
||||||
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
|
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
|
||||||
&& _libraryManager.IsAudioFile(filename))
|
&& _libraryManager.IsAudioFile(filename))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using DotNet.Globbing;
|
using DotNet.Globbing;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -19,6 +21,7 @@ using Emby.Server.Implementations.Playlists;
|
|||||||
using Emby.Server.Implementations.ScheduledTasks;
|
using Emby.Server.Implementations.ScheduledTasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
|
using Jellyfin.Extensions;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Progress;
|
using MediaBrowser.Common.Progress;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
@ -694,25 +697,32 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<BaseItem> ResolveFileList(
|
private IEnumerable<BaseItem> ResolveFileList(
|
||||||
IEnumerable<FileSystemMetadata> fileList,
|
IReadOnlyList<FileSystemMetadata> fileList,
|
||||||
IDirectoryService directoryService,
|
IDirectoryService directoryService,
|
||||||
Folder parent,
|
Folder parent,
|
||||||
string collectionType,
|
string collectionType,
|
||||||
IItemResolver[] resolvers,
|
IItemResolver[] resolvers,
|
||||||
LibraryOptions libraryOptions)
|
LibraryOptions libraryOptions)
|
||||||
{
|
{
|
||||||
return fileList.Select(f =>
|
// Given that fileList is a list we can save enumerator allocations by indexing
|
||||||
|
for (var i = 0; i < fileList.Count; i++)
|
||||||
{
|
{
|
||||||
|
var file = fileList[i];
|
||||||
|
BaseItem result = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions);
|
result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error resolving path {path}", f.FullName);
|
_logger.LogError(ex, "Error resolving path {Path}", file.FullName);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}).Where(i => i != null);
|
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
yield return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1063,17 +1073,17 @@ namespace Emby.Server.Implementations.Library
|
|||||||
// Start by just validating the children of the root, but go no further
|
// Start by just validating the children of the root, but go no further
|
||||||
await RootFolder.ValidateChildren(
|
await RootFolder.ValidateChildren(
|
||||||
new SimpleProgress<double>(),
|
new SimpleProgress<double>(),
|
||||||
cancellationToken,
|
|
||||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
|
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
|
||||||
recursive: false).ConfigureAwait(false);
|
recursive: false,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
await GetUserRootFolder().ValidateChildren(
|
await GetUserRootFolder().ValidateChildren(
|
||||||
new SimpleProgress<double>(),
|
new SimpleProgress<double>(),
|
||||||
cancellationToken,
|
|
||||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
|
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
|
||||||
recursive: false).ConfigureAwait(false);
|
recursive: false,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// Quickly scan CollectionFolders for changes
|
// Quickly scan CollectionFolders for changes
|
||||||
foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
|
foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
|
||||||
@ -1093,7 +1103,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
|
innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
|
||||||
|
|
||||||
// Validate the entire media library
|
// Validate the entire media library
|
||||||
await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);
|
await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
progress.Report(96);
|
progress.Report(96);
|
||||||
|
|
||||||
@ -2074,7 +2084,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return new List<Folder>();
|
return new List<Folder>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>().ToList());
|
return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren)
|
public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren)
|
||||||
@ -2099,10 +2109,10 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return GetCollectionFoldersInternal(item, allUserRootChildren);
|
return GetCollectionFoldersInternal(item, allUserRootChildren);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Folder> GetCollectionFoldersInternal(BaseItem item, List<Folder> allUserRootChildren)
|
private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
|
||||||
{
|
{
|
||||||
return allUserRootChildren
|
return allUserRootChildren
|
||||||
.Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase))
|
.Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path.AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2110,9 +2120,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
if (!(item is CollectionFolder collectionFolder))
|
if (!(item is CollectionFolder collectionFolder))
|
||||||
{
|
{
|
||||||
|
// List.Find is more performant than FirstOrDefault due to enumerator allocation
|
||||||
collectionFolder = GetCollectionFolders(item)
|
collectionFolder = GetCollectionFolders(item)
|
||||||
.OfType<CollectionFolder>()
|
.Find(folder => folder is CollectionFolder) as CollectionFolder;
|
||||||
.FirstOrDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
|
return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
|
||||||
@ -2498,8 +2508,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsVideoFile(string path)
|
public bool IsVideoFile(string path)
|
||||||
{
|
{
|
||||||
var resolver = new VideoResolver(GetNamingOptions());
|
return VideoResolver.IsVideoFile(path, GetNamingOptions());
|
||||||
return resolver.IsVideoFile(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -2531,9 +2540,10 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
|
episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
|
||||||
// Resolve from parent folder if it's not the Season folder
|
// Resolve from parent folder if it's not the Season folder
|
||||||
if (episodeInfo == null && episode.Parent.GetType() == typeof(Folder))
|
var parent = episode.GetParent();
|
||||||
|
if (episodeInfo == null && parent.GetType() == typeof(Folder))
|
||||||
{
|
{
|
||||||
episodeInfo = resolver.Resolve(episode.Parent.Path, true, null, null, isAbsoluteNaming);
|
episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
|
||||||
if (episodeInfo != null)
|
if (episodeInfo != null)
|
||||||
{
|
{
|
||||||
// add the container
|
// add the container
|
||||||
@ -2677,6 +2687,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public NamingOptions GetNamingOptions()
|
public NamingOptions GetNamingOptions()
|
||||||
{
|
{
|
||||||
if (_namingOptions == null)
|
if (_namingOptions == null)
|
||||||
@ -2690,13 +2701,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public ItemLookupInfo ParseName(string name)
|
public ItemLookupInfo ParseName(string name)
|
||||||
{
|
{
|
||||||
var resolver = new VideoResolver(GetNamingOptions());
|
var namingOptions = GetNamingOptions();
|
||||||
|
var result = VideoResolver.CleanDateTime(name, namingOptions);
|
||||||
var result = resolver.CleanDateTime(name);
|
|
||||||
|
|
||||||
return new ItemLookupInfo
|
return new ItemLookupInfo
|
||||||
{
|
{
|
||||||
Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name,
|
Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name,
|
||||||
Year = result.Year
|
Year = result.Year
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -2710,9 +2720,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
|
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var videoListResolver = new VideoListResolver(namingOptions);
|
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
|
||||||
|
|
||||||
var videos = videoListResolver.Resolve(fileSystemChildren);
|
|
||||||
|
|
||||||
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
|
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
@ -2756,9 +2764,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
|
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var videoListResolver = new VideoListResolver(namingOptions);
|
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
|
||||||
|
|
||||||
var videos = videoListResolver.Resolve(fileSystemChildren);
|
|
||||||
|
|
||||||
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
|
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -10,7 +12,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -13,7 +15,7 @@ using Jellyfin.Data.Entities;
|
|||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
@ -350,7 +352,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
private string[] NormalizeLanguage(string language)
|
private string[] NormalizeLanguage(string language)
|
||||||
{
|
{
|
||||||
if (language == null)
|
if (string.IsNullOrEmpty(language))
|
||||||
{
|
{
|
||||||
return Array.Empty<string>();
|
return Array.Empty<string>();
|
||||||
}
|
}
|
||||||
@ -379,8 +381,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
|
var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
|
||||||
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
|
|
||||||
|
|
||||||
var defaultAudioIndex = source.DefaultAudioStreamIndex;
|
var defaultAudioIndex = source.DefaultAudioStreamIndex;
|
||||||
var audioLangage = defaultAudioIndex == null
|
var audioLangage = defaultAudioIndex == null
|
||||||
@ -409,9 +410,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
|
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
|
||||||
? Array.Empty<string>()
|
|
||||||
: NormalizeLanguage(user.AudioLanguagePreference);
|
|
||||||
|
|
||||||
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
|
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable enable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using MediaBrowser.Common.Providers;
|
using MediaBrowser.Common.Providers;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user