mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-11-03 19:17:24 -05:00 
			
		
		
		
	Merge branch 'master' into comparisons
This commit is contained in:
		
						commit
						6648b7d7da
					
				
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							@ -33,7 +33,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**
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										36
									
								
								.github/workflows/automation.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								.github/workflows/automation.yml
									
									
									
									
										vendored
									
									
								
							@ -1,26 +1,36 @@
 | 
				
			|||||||
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
 | 
				
			||||||
    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
 | 
				
			||||||
 | 
					    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.7.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.7.1
 | 
				
			||||||
@ -29,16 +39,16 @@ jobs:
 | 
				
			|||||||
        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.7.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'
 | 
				
			||||||
@ -52,7 +62,7 @@ jobs:
 | 
				
			|||||||
        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.7.1
 | 
				
			||||||
@ -61,4 +71,4 @@ jobs:
 | 
				
			|||||||
        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)
 | 
				
			||||||
@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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))
 | 
				
			||||||
 | 
				
			|||||||
@ -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,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 MediaBrowser.Common.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);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -23,11 +23,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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -29,36 +29,33 @@ 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();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (var i = 0; i < _options.VideoExtraRules.Length; i++)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var rule = _options.VideoExtraRules[i];
 | 
				
			||||||
                if (rule.MediaType == MediaType.Audio)
 | 
					                if (rule.MediaType == MediaType.Audio)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    if (!AudioFileParser.IsAudioFile(path, _options))
 | 
					                    if (!AudioFileParser.IsAudioFile(path, _options))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                    return result;
 | 
					                        continue;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                else if (rule.MediaType == MediaType.Video)
 | 
					                else if (rule.MediaType == MediaType.Video)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                if (!new VideoResolver(_options).IsVideoFile(path))
 | 
					                    if (!VideoResolver.IsVideoFile(path, _options))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                    return result;
 | 
					                        continue;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var pathSpan = path.AsSpan();
 | 
				
			||||||
                if (rule.RuleType == ExtraRuleType.Filename)
 | 
					                if (rule.RuleType == ExtraRuleType.Filename)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                var filename = Path.GetFileNameWithoutExtension(path);
 | 
					                    var filename = Path.GetFileNameWithoutExtension(pathSpan);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (string.Equals(filename, rule.Token, StringComparison.OrdinalIgnoreCase))
 | 
					                    if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        result.ExtraType = rule.ExtraType;
 | 
					                        result.ExtraType = rule.ExtraType;
 | 
				
			||||||
                        result.Rule = rule;
 | 
					                        result.Rule = rule;
 | 
				
			||||||
@ -66,9 +63,9 @@ namespace Emby.Naming.Video
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
                else if (rule.RuleType == ExtraRuleType.Suffix)
 | 
					                else if (rule.RuleType == ExtraRuleType.Suffix)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                var filename = Path.GetFileNameWithoutExtension(path);
 | 
					                    var filename = Path.GetFileNameWithoutExtension(pathSpan);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (filename.IndexOf(rule.Token, StringComparison.OrdinalIgnoreCase) > 0)
 | 
					                    if (filename.Contains(rule.Token, StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        result.ExtraType = rule.ExtraType;
 | 
					                        result.ExtraType = rule.ExtraType;
 | 
				
			||||||
                        result.Rule = rule;
 | 
					                        result.Rule = rule;
 | 
				
			||||||
@ -88,14 +85,20 @@ namespace Emby.Naming.Video
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
                else if (rule.RuleType == ExtraRuleType.DirectoryName)
 | 
					                else if (rule.RuleType == ExtraRuleType.DirectoryName)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                var directoryName = Path.GetFileName(Path.GetDirectoryName(path));
 | 
					                    var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
 | 
				
			||||||
                if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase))
 | 
					                    if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        result.ExtraType = rule.ExtraType;
 | 
					                        result.ExtraType = rule.ExtraType;
 | 
				
			||||||
                        result.Rule = rule;
 | 
					                        result.Rule = rule;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (result.ExtraType != null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    return result;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            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);
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                var currentSlice = path[..index];
 | 
				
			||||||
 | 
					                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;
 | 
					                    break;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					 | 
				
			||||||
                    foundPrefix = string.Equals(flag, rule.PrecedingToken, StringComparison.OrdinalIgnoreCase);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                result.Is3D = foundPrefix && !string.IsNullOrEmpty(format);
 | 
					            return is3D ? new Format3DResult(true, format3D) : _defaultResult;
 | 
				
			||||||
                result.Format3D = format;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return result;
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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]);
 | 
					            // 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++)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                var video = videos[i];
 | 
				
			||||||
 | 
					                if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    return videos;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                var alternateVersionsLen = ordered.Count - 1;
 | 
					            // 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));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var list = new List<VideoInfo>
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                videos[0]
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var alternateVersionsLen = videos.Count - 1;
 | 
				
			||||||
            var alternateVersions = new VideoFileInfo[alternateVersionsLen];
 | 
					            var alternateVersions = new VideoFileInfo[alternateVersionsLen];
 | 
				
			||||||
 | 
					            var extras = new List<VideoFileInfo>(list[0].Extras);
 | 
				
			||||||
            for (int i = 0; i < alternateVersionsLen; i++)
 | 
					            for (int i = 0; i < alternateVersionsLen; i++)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                    alternateVersions[i] = ordered[i + 1].Files[0];
 | 
					                var video = videos[i + 1];
 | 
				
			||||||
 | 
					                alternateVersions[i] = video.Files[0];
 | 
				
			||||||
 | 
					                extras.AddRange(video.Extras);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            list[0].AlternateVersions = alternateVersions;
 | 
					            list[0].AlternateVersions = alternateVersions;
 | 
				
			||||||
                list[0].Name = folderName;
 | 
					            list[0].Name = folderName.ToString();
 | 
				
			||||||
                var extras = ordered.Skip(1).SelectMany(i => i.Extras).ToList();
 | 
					 | 
				
			||||||
                extras.AddRange(list[0].Extras);
 | 
					 | 
				
			||||||
            list[0].Extras = extras;
 | 
					            list[0].Extras = extras;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return list;
 | 
					            return list;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return videos;
 | 
					        private static bool HaveSameYear(IReadOnlyList<VideoInfo> videos)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (videos.Count == 1)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private bool HaveSameYear(List<VideoInfo> videos)
 | 
					            var firstYear = videos[0].Year ?? -1;
 | 
				
			||||||
 | 
					            for (var i = 1; i < videos.Count; i++)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
            return videos.Select(i => i.Year ?? -1).Distinct().Count() < 2;
 | 
					                if ((videos[i].Year ?? -1) != firstYear)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private bool IsEligibleForMultiVersion(string folderName, string testFilePath)
 | 
					            return true;
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            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
 | 
					 | 
				
			||||||
                if (folderName.Length <= testFilename.Length)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    testFilename = testFilename.Substring(folderName.Length).Trim();
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
 | 
					        private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
 | 
				
			||||||
 | 
					            if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                    testFilename = cleanName.Trim().ToString();
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                // The CleanStringParser should have removed common keywords etc.
 | 
					 | 
				
			||||||
                return string.IsNullOrEmpty(testFilename)
 | 
					 | 
				
			||||||
                       || testFilename[0] == '-'
 | 
					 | 
				
			||||||
                       || Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private List<VideoFileInfo> GetExtras(IEnumerable<VideoFileInfo> remainingFiles, List<string> baseNames)
 | 
					            // Remove the folder name before cleaning as we don't care about cleaning that part
 | 
				
			||||||
 | 
					            if (folderName.Length <= testFilename.Length)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
            foreach (var name in baseNames.ToList())
 | 
					                testFilename = testFilename[folderName.Length..].Trim();
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                var trimmedName = name.TrimEnd().TrimEnd(_options.VideoFlagDelimiters).TrimEnd();
 | 
					 | 
				
			||||||
                baseNames.Add(trimmedName);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return remainingFiles
 | 
					            // There are no span overloads for regex unfortunately
 | 
				
			||||||
                .Where(i => i.ExtraType != null)
 | 
					            var tmpTestFilename = testFilename.ToString();
 | 
				
			||||||
                .Where(i => baseNames.Any(b =>
 | 
					            if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
 | 
				
			||||||
                    i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase)))
 | 
					            {
 | 
				
			||||||
                .ToList();
 | 
					                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 MediaBrowser.Common.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);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
					                (k, configurationManager) =>
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                var file = GetConfigurationFile(key);
 | 
					                    var file = configurationManager.GetConfigurationFile(k);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                var configurationInfo = _configurationStores
 | 
					                    var configurationInfo = Array.Find(
 | 
				
			||||||
                    .FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
 | 
					                        configurationManager._configurationStores,
 | 
				
			||||||
 | 
					                        i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (configurationInfo == null)
 | 
					                    if (configurationInfo == null)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                    throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
 | 
					                        throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    var configurationType = configurationInfo.ConfigurationType;
 | 
					                    var configurationType = configurationInfo.ConfigurationType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                lock (_configurationSyncLock)
 | 
					                    lock (configurationManager._configurationSyncLock)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                    return LoadConfiguration(file, configurationType);
 | 
					                        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;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Globalization;
 | 
					using System.Globalization;
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
@ -164,7 +166,7 @@ namespace Emby.Server.Implementations.Collections
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                parentFolder.AddChild(collection, CancellationToken.None);
 | 
					                parentFolder.AddChild(collection, CancellationToken.None);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (options.ItemIdList.Length > 0)
 | 
					                if (options.ItemIdList.Count > 0)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    await AddToCollectionAsync(
 | 
					                    await AddToCollectionAsync(
 | 
				
			||||||
                        collection.Id,
 | 
					                        collection.Id,
 | 
				
			||||||
@ -248,11 +250,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 +302,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;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static string GetString(this IReadOnlyList<IResultSetValue> result, int index)
 | 
					            result = item.ReadGuidFromBlob();
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index)
 | 
					            result = item.ToBool();
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index)
 | 
					            result = item.ToInt64();
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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" />
 | 
				
			||||||
@ -27,11 +28,11 @@
 | 
				
			|||||||
    <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.7" />
 | 
				
			||||||
    <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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -44,6 +45,7 @@
 | 
				
			|||||||
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
					    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 | 
				
			||||||
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
					    <GenerateDocumentationFile>true</GenerateDocumentationFile>
 | 
				
			||||||
    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
 | 
					    <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
 | 
				
			||||||
 | 
					    <Nullable>enable</Nullable>
 | 
				
			||||||
    <!-- 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>
 | 
					    <AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
 | 
				
			||||||
 | 
				
			|||||||
@ -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 MediaBrowser.Common.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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -7,6 +7,7 @@ using System.IO;
 | 
				
			|||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using System.Runtime.InteropServices;
 | 
					using System.Runtime.InteropServices;
 | 
				
			||||||
using MediaBrowser.Common.Configuration;
 | 
					using MediaBrowser.Common.Configuration;
 | 
				
			||||||
 | 
					using MediaBrowser.Common.Extensions;
 | 
				
			||||||
using MediaBrowser.Model.IO;
 | 
					using MediaBrowser.Model.IO;
 | 
				
			||||||
using MediaBrowser.Model.System;
 | 
					using MediaBrowser.Model.System;
 | 
				
			||||||
using Microsoft.Extensions.Logging;
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
@ -61,7 +62,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 +244,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
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
@ -601,7 +602,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 +619,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 +637,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 +655,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 +672,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;
 | 
				
			||||||
@ -694,25 +696,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;
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (result != null)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    yield return result;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            }).Where(i => i != null);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
@ -1063,17 +1072,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 +1102,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 +2083,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 +2108,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 +2119,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 +2507,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 />
 | 
				
			||||||
@ -2677,6 +2685,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 +2699,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 +2718,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 +2762,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;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pragma warning disable CS1591
 | 
					#pragma warning disable CS1591
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,3 @@
 | 
				
			|||||||
#nullable enable
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.IO;
 | 
					using System.IO;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pragma warning disable CS1591
 | 
					#pragma warning disable CS1591
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using System.Threading.Tasks;
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pragma warning disable CS1591
 | 
					#pragma warning disable CS1591
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
@ -45,11 +47,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
 | 
				
			|||||||
        protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
 | 
					        protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
 | 
				
			||||||
              where TVideoType : Video, new()
 | 
					              where TVideoType : Video, new()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
 | 
					            var namingOptions = LibraryManager.GetNamingOptions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // If the path is a file check for a matching extensions
 | 
					            // If the path is a file check for a matching extensions
 | 
				
			||||||
            var parser = new VideoResolver(namingOptions);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (args.IsDirectory)
 | 
					            if (args.IsDirectory)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                TVideoType video = null;
 | 
					                TVideoType video = null;
 | 
				
			||||||
@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 | 
				
			|||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
 | 
					                        if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            videoInfo = parser.ResolveDirectory(args.Path);
 | 
					                            videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            if (videoInfo == null)
 | 
					                            if (videoInfo == null)
 | 
				
			||||||
                            {
 | 
					                            {
 | 
				
			||||||
@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                        if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
 | 
					                        if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            videoInfo = parser.ResolveDirectory(args.Path);
 | 
					                            videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            if (videoInfo == null)
 | 
					                            if (videoInfo == null)
 | 
				
			||||||
                            {
 | 
					                            {
 | 
				
			||||||
@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    else if (IsDvdFile(filename))
 | 
					                    else if (IsDvdFile(filename))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        videoInfo = parser.ResolveDirectory(args.Path);
 | 
					                        videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        if (videoInfo == null)
 | 
					                        if (videoInfo == null)
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            else
 | 
					            else
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var videoInfo = parser.Resolve(args.Path, false, false);
 | 
					                var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (videoInfo == null)
 | 
					                if (videoInfo == null)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@ -165,13 +165,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected void SetVideoType(Video video, VideoFileInfo videoInfo)
 | 
					        protected void SetVideoType(Video video, VideoFileInfo videoInfo)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var extension = Path.GetExtension(video.Path);
 | 
					            var extension = Path.GetExtension(video.Path.AsSpan());
 | 
				
			||||||
            video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) ||
 | 
					            video.VideoType = extension.Equals(".iso", StringComparison.OrdinalIgnoreCase)
 | 
				
			||||||
                string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ?
 | 
					                              || extension.Equals(".img", StringComparison.OrdinalIgnoreCase)
 | 
				
			||||||
              VideoType.Iso :
 | 
					                ? VideoType.Iso
 | 
				
			||||||
              VideoType.VideoFile;
 | 
					                : VideoType.VideoFile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
 | 
					            video.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
 | 
				
			||||||
            video.IsPlaceHolder = videoInfo.IsStub;
 | 
					            video.IsPlaceHolder = videoInfo.IsStub;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (videoInfo.IsStub)
 | 
					            if (videoInfo.IsStub)
 | 
				
			||||||
@ -193,11 +193,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            if (video.VideoType == VideoType.Iso)
 | 
					            if (video.VideoType == VideoType.Iso)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1)
 | 
					                if (video.Path.Contains("dvd", StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    video.IsoType = IsoType.Dvd;
 | 
					                    video.IsoType = IsoType.Dvd;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1)
 | 
					                else if (video.Path.Contains("bluray", StringComparison.OrdinalIgnoreCase))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    video.IsoType = IsoType.BluRay;
 | 
					                    video.IsoType = IsoType.BluRay;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@ -250,10 +250,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected void Set3DFormat(Video video)
 | 
					        protected void Set3DFormat(Video video)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
 | 
					            var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
 | 
				
			||||||
 | 
					 | 
				
			||||||
            var resolver = new Format3DParser(namingOptions);
 | 
					 | 
				
			||||||
            var result = resolver.Parse(video.Path);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Set3DFormat(video, result.Is3D, result.Format3D);
 | 
					            Set3DFormat(video, result.Is3D, result.Format3D);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pragma warning disable CS1591
 | 
					#pragma warning disable CS1591
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using MediaBrowser.Controller.Entities;
 | 
					using MediaBrowser.Controller.Entities;
 | 
				
			||||||
using MediaBrowser.Controller.Library;
 | 
					using MediaBrowser.Controller.Library;
 | 
				
			||||||
using MediaBrowser.Controller.Resolvers;
 | 
					using MediaBrowser.Controller.Resolvers;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using MediaBrowser.Controller.Entities;
 | 
					using MediaBrowser.Controller.Entities;
 | 
				
			||||||
using MediaBrowser.Controller.Library;
 | 
					using MediaBrowser.Controller.Library;
 | 
				
			||||||
using MediaBrowser.Controller.Resolvers;
 | 
					using MediaBrowser.Controller.Resolvers;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.IO;
 | 
					using System.IO;
 | 
				
			||||||
using MediaBrowser.Controller.Entities;
 | 
					using MediaBrowser.Controller.Entities;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,12 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Collections.Generic;
 | 
					using System.Collections.Generic;
 | 
				
			||||||
using System.IO;
 | 
					using System.IO;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using System.Text.RegularExpressions;
 | 
					using System.Text.RegularExpressions;
 | 
				
			||||||
using Emby.Naming.Video;
 | 
					using Emby.Naming.Video;
 | 
				
			||||||
 | 
					using MediaBrowser.Common.Extensions;
 | 
				
			||||||
using MediaBrowser.Controller.Drawing;
 | 
					using MediaBrowser.Controller.Drawing;
 | 
				
			||||||
using MediaBrowser.Controller.Entities;
 | 
					using MediaBrowser.Controller.Entities;
 | 
				
			||||||
using MediaBrowser.Controller.Entities.Movies;
 | 
					using MediaBrowser.Controller.Entities.Movies;
 | 
				
			||||||
@ -255,10 +258,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
 | 
					            var namingOptions = LibraryManager.GetNamingOptions();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var resolver = new VideoListResolver(namingOptions);
 | 
					            var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
 | 
				
			||||||
            var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var result = new MultiItemResolverResult
 | 
					            var result = new MultiItemResolverResult
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 | 
				
			|||||||
            return returnVideo;
 | 
					            return returnVideo;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private bool IsInvalid(Folder parent, string collectionType)
 | 
					        private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (parent != null)
 | 
					            if (parent != null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -545,12 +547,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (string.IsNullOrEmpty(collectionType))
 | 
					            if (collectionType.IsEmpty)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase);
 | 
					            return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using MediaBrowser.Controller.Drawing;
 | 
					using MediaBrowser.Controller.Drawing;
 | 
				
			||||||
using MediaBrowser.Controller.Entities;
 | 
					using MediaBrowser.Controller.Entities;
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
using System.Linq;
 | 
					using System.Linq;
 | 
				
			||||||
using MediaBrowser.Controller.Entities;
 | 
					using MediaBrowser.Controller.Entities;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System.Globalization;
 | 
					using System.Globalization;
 | 
				
			||||||
using Emby.Naming.TV;
 | 
					using Emby.Naming.TV;
 | 
				
			||||||
using MediaBrowser.Controller.Entities.TV;
 | 
					using MediaBrowser.Controller.Entities.TV;
 | 
				
			||||||
 | 
				
			|||||||
@ -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 MediaBrowser.Controller.Entities;
 | 
					using MediaBrowser.Controller.Entities;
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
@ -220,7 +222,7 @@ namespace Emby.Server.Implementations.Library
 | 
				
			|||||||
            var hasRuntime = runtimeTicks > 0;
 | 
					            var hasRuntime = runtimeTicks > 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // If a position has been reported, and if we know the duration
 | 
					            // If a position has been reported, and if we know the duration
 | 
				
			||||||
            if (positionTicks > 0 && hasRuntime && !(item is AudioBook))
 | 
					            if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
 | 
					                var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -239,7 +241,7 @@ namespace Emby.Server.Implementations.Library
 | 
				
			|||||||
                {
 | 
					                {
 | 
				
			||||||
                    // Enforce MinResumeDuration
 | 
					                    // Enforce MinResumeDuration
 | 
				
			||||||
                    var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
 | 
					                    var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
 | 
				
			||||||
                    if (durationSeconds < _config.Configuration.MinResumeDurationSeconds && !(item is Book))
 | 
					                    if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        positionTicks = 0;
 | 
					                        positionTicks = 0;
 | 
				
			||||||
                        data.Played = playedToCompletion = true;
 | 
					                        data.Played = playedToCompletion = true;
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pragma warning disable CS1591
 | 
					#pragma warning disable CS1591
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,6 @@ using MediaBrowser.Controller.LiveTv;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Emby.Server.Implementations.LiveTv.EmbyTV
 | 
					namespace Emby.Server.Implementations.LiveTv.EmbyTV
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					 | 
				
			||||||
    internal class EpgChannelData
 | 
					    internal class EpgChannelData
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -39,13 +38,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public ChannelInfo GetChannelById(string id)
 | 
					        public ChannelInfo? GetChannelById(string id)
 | 
				
			||||||
            => _channelsById.GetValueOrDefault(id);
 | 
					            => _channelsById.GetValueOrDefault(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public ChannelInfo GetChannelByNumber(string number)
 | 
					        public ChannelInfo? GetChannelByNumber(string number)
 | 
				
			||||||
            => _channelsByNumber.GetValueOrDefault(number);
 | 
					            => _channelsByNumber.GetValueOrDefault(number);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public ChannelInfo GetChannelByName(string name)
 | 
					        public ChannelInfo? GetChannelByName(string name)
 | 
				
			||||||
            => _channelsByName.GetValueOrDefault(name);
 | 
					            => _channelsByName.GetValueOrDefault(name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static string NormalizeName(string value)
 | 
					        public static string NormalizeName(string value)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					#nullable disable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pragma warning disable CS1591
 | 
					#pragma warning disable CS1591
 | 
				
			||||||
 | 
					
 | 
				
			||||||
using System;
 | 
					using System;
 | 
				
			||||||
 | 
				
			|||||||
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