Compare commits

..

1 Commits

Author SHA1 Message Date
Jellyfin Release Bot
327f92bb2e Bump version to 10.9.0 2024-05-11 14:23:58 -04:00
1461 changed files with 27089 additions and 91244 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.10",
"version": "8.0.4",
"commands": [
"dotnet-ef"
]

View File

@@ -0,0 +1,28 @@
{
"name": "Development Jellyfin Server - FFmpeg",
"image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
"postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust; sudo bash \"./.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh\"",
// reads the extensions list and installs them
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
"dotnetRuntimeVersions": "8.0",
"aspNetCoreRuntimeVersions": "8.0"
},
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
"preserve_apt_list": false,
"packages": ["libfontconfig1"]
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"dockerDashComposeVersion": "v2"
},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
},
"hostRequirements": {
"memory": "8gb",
"cpus": 4
}
}

View File

@@ -1,6 +1,6 @@
#!/bin/bash
## configure the following for a manual install of a specific version from the repo
## configure the following for a manuall install of a specific version from the repo
# wget https://repo.jellyfin.org/releases/server/ubuntu/versions/jellyfin-ffmpeg/6.0.1-1/jellyfin-ffmpeg6_6.0.1-1-jammy_amd64.deb -O ffmpeg.deb
@@ -29,4 +29,4 @@ Signed-By: /etc/apt/keyrings/jellyfin.gpg
EOF
sudo apt update -y
sudo apt install jellyfin-ffmpeg7 -y
sudo apt install jellyfin-ffmpeg6 -y

View File

@@ -1,23 +1,19 @@
{
"name": "Development Jellyfin Server",
"image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
"postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
"postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust",
// reads the extensions list and installs them
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
"features": {
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "none",
"dotnetRuntimeVersions": "9.0",
"aspNetCoreRuntimeVersions": "9.0"
"dotnetRuntimeVersions": "8.0",
"aspNetCoreRuntimeVersions": "8.0"
},
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
"preserve_apt_list": false,
"packages": [
"libfontconfig1"
]
"packages": ["libfontconfig1"]
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"dockerDashComposeVersion": "v2"

View File

@@ -192,344 +192,3 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
###############################
# C# Analyzer Rules #
###############################
### ERROR #
###########
# error on SA1000: The keyword 'new' should be followed by a space
dotnet_diagnostic.SA1000.severity = error
# error on SA1001: Commas should not be preceded by whitespace
dotnet_diagnostic.SA1001.severity = error
# error on SA1106: Code should not contain empty statements
dotnet_diagnostic.SA1106.severity = error
# error on SA1107: Code should not contain multiple statements on one line
dotnet_diagnostic.SA1107.severity = error
# error on SA1028: Code should not contain trailing whitespace
dotnet_diagnostic.SA1028.severity = error
# error on SA1117: The parameters should all be placed on the same line or each parameter should be placed on its own line
dotnet_diagnostic.SA1117.severity = error
# error on SA1137: Elements should have the same indentation
dotnet_diagnostic.SA1137.severity = error
# error on SA1142: Refer to tuple fields by name
dotnet_diagnostic.SA1142.severity = error
# error on SA1210: Using directives should be ordered alphabetically by the namespaces
dotnet_diagnostic.SA1210.severity = error
# error on SA1316: Tuple element names should use correct casing
dotnet_diagnostic.SA1316.severity = error
# error on SA1414: Tuple types in signatures should have element names
dotnet_diagnostic.SA1414.severity = error
# disable warning SA1513: Closing brace should be followed by blank line
dotnet_diagnostic.SA1513.severity = error
# error on SA1518: File is required to end with a single newline character
dotnet_diagnostic.SA1518.severity = error
# error on SA1629: Documentation text should end with a period
dotnet_diagnostic.SA1629.severity = error
# error on CA1001: Types that own disposable fields should be disposable
dotnet_diagnostic.CA1001.severity = error
# error on CA1012: Abstract types should not have public constructors
dotnet_diagnostic.CA1012.severity = error
# error on CA1063: Implement IDisposable correctly
dotnet_diagnostic.CA1063.severity = error
# error on CA1305: Specify IFormatProvider
dotnet_diagnostic.CA1305.severity = error
# error on CA1307: Specify StringComparison for clarity
dotnet_diagnostic.CA1307.severity = error
# error on CA1309: Use ordinal StringComparison
dotnet_diagnostic.CA1309.severity = error
# error on CA1310: Specify StringComparison for correctness
dotnet_diagnostic.CA1310.severity = error
# error on CA1513: Use 'ObjectDisposedException.ThrowIf' instead of explicitly throwing a new exception instance
dotnet_diagnostic.CA1513.severity = error
# error on CA1725: Parameter names should match base declaration
dotnet_diagnostic.CA1725.severity = error
# error on CA1725: Call async methods when in an async method
dotnet_diagnostic.CA1727.severity = error
# error on CA1813: Avoid unsealed attributes
dotnet_diagnostic.CA1813.severity = error
# error on CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string
dotnet_diagnostic.CA1834.severity = error
# error on CA1843: Do not use 'WaitAll' with a single task
dotnet_diagnostic.CA1843.severity = error
# error on CA1845: Use span-based 'string.Concat'
dotnet_diagnostic.CA1845.severity = error
# error on CA1849: Call async methods when in an async method
dotnet_diagnostic.CA1849.severity = error
# error on CA1851: Possible multiple enumerations of IEnumerable collection
dotnet_diagnostic.CA1851.severity = error
# error on CA1854: Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup
dotnet_diagnostic.CA1854.severity = error
# error on CA1860: Avoid using 'Enumerable.Any()' extension method
dotnet_diagnostic.CA1860.severity = error
# error on CA1861: Avoid constant arrays as arguments
dotnet_diagnostic.CA1861.severity = error
# error on CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons
dotnet_diagnostic.CA1862.severity = error
# error on CA1863: Use 'CompositeFormat'
dotnet_diagnostic.CA1863.severity = error
# error on CA1864: Prefer the 'IDictionary.TryAdd(TKey, TValue)' method
dotnet_diagnostic.CA1864.severity = error
# error on CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char
dotnet_diagnostic.CA1865.severity = error
dotnet_diagnostic.CA1866.severity = error
dotnet_diagnostic.CA1867.severity = error
# error on CA1868: Unnecessary call to 'Contains' for sets
dotnet_diagnostic.CA1868.severity = error
# error on CA1869: Cache and reuse 'JsonSerializerOptions' instances
dotnet_diagnostic.CA1869.severity = error
# error on CA1870: Use a cached 'SearchValues' instance
dotnet_diagnostic.CA1870.severity = error
# error on CA1871: Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull'
dotnet_diagnostic.CA1871.severity = error
# error on CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'
dotnet_diagnostic.CA1872.severity = error
# error on CA2016: Forward the CancellationToken parameter to methods that take one
# or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token
dotnet_diagnostic.CA2016.severity = error
# error on CA2201: Exception type System.Exception is not sufficiently specific
dotnet_diagnostic.CA2201.severity = error
# error on CA2215: Dispose methods should call base class dispose
dotnet_diagnostic.CA2215.severity = error
# error on CA2249: Use 'string.Contains' instead of 'string.IndexOf' to improve readability
dotnet_diagnostic.CA2249.severity = error
# error on CA2254: Template should be a static expression
dotnet_diagnostic.CA2254.severity = error
################
### SUGGESTION #
################
# disable warning CA1014: Mark assemblies with CLSCompliantAttribute
dotnet_diagnostic.CA1014.severity = suggestion
# disable warning CA1024: Use properties where appropriate
dotnet_diagnostic.CA1024.severity = suggestion
# disable warning CA1031: Do not catch general exception types
dotnet_diagnostic.CA1031.severity = suggestion
# disable warning CA1032: Implement standard exception constructors
dotnet_diagnostic.CA1032.severity = suggestion
# disable warning CA1040: Avoid empty interfaces
dotnet_diagnostic.CA1040.severity = suggestion
# disable warning CA1062: Validate arguments of public methods
dotnet_diagnostic.CA1062.severity = suggestion
# TODO: enable when false positives are fixed
# disable warning CA1508: Avoid dead conditional code
dotnet_diagnostic.CA1508.severity = suggestion
# disable warning CA1515: Consider making public types internal
dotnet_diagnostic.CA1515.severity = suggestion
# disable warning CA1716: Identifiers should not match keywords
dotnet_diagnostic.CA1716.severity = suggestion
# disable warning CA1720: Identifiers should not contain type names
dotnet_diagnostic.CA1720.severity = suggestion
# disable warning CA1724: Type names should not match namespaces
dotnet_diagnostic.CA1724.severity = suggestion
# disable warning CA1805: Do not initialize unnecessarily
dotnet_diagnostic.CA1805.severity = suggestion
# disable warning CA1812: internal class that is apparently never instantiated.
# If so, remove the code from the assembly.
# If this class is intended to contain only static members, make it static
dotnet_diagnostic.CA1812.severity = suggestion
# disable warning CA1822: Member does not access instance data and can be marked as static
dotnet_diagnostic.CA1822.severity = suggestion
# CA1859: Use concrete types when possible for improved performance
dotnet_diagnostic.CA1859.severity = suggestion
# TODO: Enable
# CA1861: Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array
dotnet_diagnostic.CA1861.severity = suggestion
# disable warning CA2000: Dispose objects before losing scope
dotnet_diagnostic.CA2000.severity = suggestion
# disable warning CA2253: Named placeholders should not be numeric values
dotnet_diagnostic.CA2253.severity = suggestion
# disable warning CA5394: Do not use insecure randomness
dotnet_diagnostic.CA5394.severity = suggestion
# error on CA3003: Review code for file path injection vulnerabilities
dotnet_diagnostic.CA3003.severity = suggestion
# error on CA3006: Review code for process command injection vulnerabilities
dotnet_diagnostic.CA3006.severity = suggestion
###############
### DISABLED #
###############
# disable warning SA1009: Closing parenthesis should be followed by a space.
dotnet_diagnostic.SA1009.severity = none
# disable warning SA1011: Closing square bracket should be followed by a space.
dotnet_diagnostic.SA1011.severity = none
# disable warning SA1101: Prefix local calls with 'this.'
dotnet_diagnostic.SA1101.severity = none
# disable warning SA1108: Block statements should not contain embedded comments
dotnet_diagnostic.SA1108.severity = none
# disable warning SA1118: Parameter must not span multiple lines.
dotnet_diagnostic.SA1118.severity = none
# disable warning SA1128:: Put constructor initializers on their own line
dotnet_diagnostic.SA1128.severity = none
# disable warning SA1130: Use lambda syntax
dotnet_diagnostic.SA1130.severity = none
# disable warning SA1200: 'using' directive must appear within a namespace declaration
dotnet_diagnostic.SA1200.severity = none
# disable warning SA1202: 'public' members must come before 'private' members
dotnet_diagnostic.SA1202.severity = none
# disable warning SA1204: Static members must appear before non-static members
dotnet_diagnostic.SA1204.severity = none
# disable warning SA1309: Fields must not begin with an underscore
dotnet_diagnostic.SA1309.severity = none
# disable warning SA1311: Static readonly fields should begin with upper-case letter
dotnet_diagnostic.SA1311.severity = none
# disable warning SA1413: Use trailing comma in multi-line initializers
dotnet_diagnostic.SA1413.severity = none
# disable warning SA1512: Single-line comments must not be followed by blank line
dotnet_diagnostic.SA1512.severity = none
# disable warning SA1515: Single-line comment should be preceded by blank line
dotnet_diagnostic.SA1515.severity = none
# disable warning SA1600: Elements should be documented
dotnet_diagnostic.SA1600.severity = none
# disable warning SA1601: Partial elements should be documented
dotnet_diagnostic.SA1601.severity = none
# disable warning SA1602: Enumeration items should be documented
dotnet_diagnostic.SA1602.severity = none
# disable warning SA1633: The file header is missing or not located at the top of the file
dotnet_diagnostic.SA1633.severity = none
# disable warning CA1054: Change the type of parameter url from string to System.Uri
dotnet_diagnostic.CA1054.severity = none
# disable warning CA1055: URI return values should not be strings
dotnet_diagnostic.CA1055.severity = none
# disable warning CA1056: URI properties should not be strings
dotnet_diagnostic.CA1056.severity = none
# disable warning CA1303: Do not pass literals as localized parameters
dotnet_diagnostic.CA1303.severity = none
# disable warning CA1308: Normalize strings to uppercase
dotnet_diagnostic.CA1308.severity = none
# disable warning CA1848: Use the LoggerMessage delegates
dotnet_diagnostic.CA1848.severity = none
# disable warning CA2101: Specify marshaling for P/Invoke string arguments
dotnet_diagnostic.CA2101.severity = none
# disable warning CA2234: Pass System.Uri objects instead of strings
dotnet_diagnostic.CA2234.severity = none
# error on RS0030: Do not used banned APIs
dotnet_diagnostic.RS0030.severity = error
# disable warning IDISP001: Dispose created
dotnet_diagnostic.IDISP001.severity = suggestion
# TODO: Enable when false positives are fixed
# disable warning IDISP003: Dispose previous before re-assigning
dotnet_diagnostic.IDISP003.severity = suggestion
# disable warning IDISP004: Don't ignore created IDisposable
dotnet_diagnostic.IDISP004.severity = suggestion
# disable warning IDISP007: Don't dispose injected
dotnet_diagnostic.IDISP007.severity = suggestion
# disable warning IDISP008: Don't assign member with injected and created disposables
dotnet_diagnostic.IDISP008.severity = suggestion
[tests/**.{cs,vb}]
# disable warning SA0001: XML comment analysis is disabled due to project configuration
dotnet_diagnostic.SA0001.severity = none
# disable warning CA1707: Identifiers should not contain underscores
dotnet_diagnostic.CA1707.severity = none
# disable warning CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = none
# disable warning CA2234: Pass system uri objects instead of strings
dotnet_diagnostic.CA2234.severity = suggestion
# disable warning xUnit1028: Test methods must have a supported return type.
dotnet_diagnostic.xUnit1028.severity = none
# CA1826: Do not use Enumerable methods on indexable collections
dotnet_diagnostic.CA1826.severity = suggestion

View File

@@ -1,132 +1,58 @@
name: Issue Report
description: File an issue report
title: "[Issue]: "
labels: [bug, triage]
type: Bug
body:
- type: markdown
id: introduction
attributes:
value: |
### Thank you for taking the time to report an issue!
Please keep in mind that Jellyfin is a [free and open-source](https://jellyfin.org/docs/general/about) project, made up entirely and exclusively of **volunteers** who donate their free time to the project.
- type: checkboxes
id: before-posting
attributes:
label: "This issue respects the following points:"
description: All conditions are **required**. Failure to comply with any of these conditions may cause your issue to be closed without comment.
options:
- label: This is a **bug**, not a question or a configuration issue; Please visit our [forum or chat rooms](https://jellyfin.org/contact/) first to troubleshoot with volunteers, before creating a report.
required: true
- label: This issue is **not** already reported on [GitHub](https://github.com/jellyfin/jellyfin/issues?q=is%3Aopen+is%3Aissue) _(I've searched it)_.
required: true
- label: I'm using an up to date version of Jellyfin Server stable, unstable or master; We generally do not support previous older versions. If possible, please update to the latest version before opening an issue.
required: true
- label: I agree to follow Jellyfin's [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct).
required: true
- label: This report addresses only a single issue; If you encounter multiple issues, kindly create separate reports for each one.
required: true
- type: markdown
id: preliminary-information
attributes:
value: |
### General preliminary information
Please keep the following in mind when creating this issue:
1. Fill in as much of the template as possible. When you are unsure about the relevancy of a section, do include the information requested in that section. Only leave out information in sections when you are completely sure about it not being relevant.
2. Provide as much detail as possible. Do not assume other people to know what is going on.
3. Keep everything readable and structured. Nobody enjoys reading poorly written reports that are difficult to understand.
4. Keep an eye on your report as long as it is open, your involvement might be requested at a later moment.
5. Keep the title short and descriptive. The title is not the place to write down a full description of the issue.
6. When deciding to leave out information in a field, leave it blank and empty. Avoid writing things such as `n/a` for empty fields.
Thanks for taking the time to report an issue. Before submitting a report, please do the following:
1. Please head to our forum or chat rooms and troubleshoot with volunteers if you haven't already. Links can be found here: https://jellyfin.org/contact/
2. Please search the bug tracker for similar issues. If you do find one, please comment there instead of opening a new bug report.
3. If you decide to open a new report, please provide as much detail as possible.
4. Please **ONLY** report **ONE** issue per report. If you are experiencing multiple issues, please open multiple reports.
- type: textarea
id: bug-description
id: what-happened
attributes:
label: Description of the bug
description: Please provide a detailed description on the bug you encountered, in a readable and comprehensible way.
label: Please describe your bug
description: Also tell us, what did you expect to happen?
placeholder: |
After upgrading to version x.y.z of Jellyfin, the "login disclaimer" is showing incorrect text. It appears to me that it is appending the server name to the end of the login disclaimer, and showing that to a user. It might be a regression from pull request x. I have tried rebooting my host as well as my container multiple times. I tested this functionality on different clients, and it happens to all the tested clients (client x, y, z), that support the login disclaimer functionality. This makes me believe it is a server side issue.
The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
If you are using an old release of Jellyfin, please also explain why.
validations:
required: true
- type: textarea
id: repro-steps
attributes:
label: Reproduction steps
description: Reproduction steps should be complete and self-contained. Anyone can reproduce this issue by following these steps. Furthermore, the steps should be clear and easy to follow.
label: Reproduction Steps
placeholder: |
1. Sign in on the Jellyfin web client, with an admin account, using a browser of your choice.
2. Navigate to the dashboard.
3. Select "general".
4. Change the login disclaimer to something like "I am a cool disclaimer!"
5. Save the settings.
6. Sign out.
7. Make sure you are on the sign in screen. Otherwise, navigate to the sign in screen manually.
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: What is the current _bug_ behavior?
description: Write down the incorrect behavior that currently happens after following the reproduction steps.
placeholder: |
The login disclaimer on the sign in screen has the server name appended to the text. The text shown is: "I am a cool disclaimer!jellyfinserver".
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: What is the expected _correct_ behavior?
description: Write down the correct expected behavior that is supposed to happen after following the reproduction steps.
placeholder: |
The login disclaimer on the sign in screen should only show the configured text. The text that should be shown is: "I am a cool disclaimer!".
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: true
- type: dropdown
id: version
attributes:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
label: Jellyfin Version
description: What version of Jellyfin are you running?
options:
- 10.10.0+
- Master
- Unstable
- Older*
- 10.8.13
- 10.8.12
- 10.8.11 or older (please specify)
- Unstable (master branch)
validations:
required: true
- type: input
id: version-master
id: version-other
attributes:
label: "Specify commit id"
description: Fill in this field in case the option 'master' is selected. Provide the commit id it was built on.
placeholder: |
610e56baafc3011e1bfa043bdabb567bda0c2ab0
- type: input
id: version-unstable
attributes:
label: "Specify unstable release number"
description: Fill in this field in case the option 'unstable' is selected. Provide the unstable release number.
placeholder: |
2024050906
- type: input
id: version-older
attributes:
label: "Specify version number"
description: Fill in this field in case the option 'older' is selected. Provide the version number.
placeholder: |
x.y.z
- type: input
id: build-version
attributes:
label: "Specify the build version"
description: Please provide the build version that is shown in the dashboard.
validations:
required: true
label: "if other:"
placeholder: Other
- type: textarea
id: environment-information
attributes:
label: Environment
description: |
Accurately fill in as much environment details as possible. If a certain environment field is not shown in the template below, but you consider useful information, please include it.
Examples:
- **OS**: [e.g. Debian 11, Windows 10]
- **Linux Kernel**: [e.g. none, 5.15, 6.1, etc.]
@@ -141,9 +67,7 @@ body:
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example]
- **Networking**: [e.g. Host, Bridge/NAT]
- **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD]
- **Media Storage**: [e.g. Local HDD, SMB Share]
- **External Integrations**: [e.g. Jellystat, Jellyseerr]
- **Storage**: [e.g. local, NFS, cloud]
value: |
- OS:
- Linux Kernel:
@@ -158,29 +82,26 @@ body:
- Reverse Proxy:
- Base URL:
- Networking:
- Jellyfin Data Storage:
- Media Storage:
- External Integrations:
- Storage:
render: markdown
validations:
required: true
- type: markdown
id: general-information-logs
attributes:
value: |
When providing logs, please keep the following things in mind:
1. **DO NOT** use external paste services. If logs are too large to paste into the field, upload them as text files.
When providing logs, please keep the following things in mind.
1. **DO NOT** use external paste services.
2. Please provide complete logs.
- For server logs, ensure to capture all relevant information, encompassing both the events leading up to and following the occurrence of the issue. Typically, providing 10 *lines preceding and succeeding* the problem should be adequate.
- For server logs, include everything you think is important plus *10 lines before and after*
- For ffmpeg logs, please provide the entire file unmodified.
3. Please do not run logs through any translation program. We exclusively accept raw, untranslated logs. Particularly exercise caution if your browser automatically translates pages by default.
- Do not forget to censor out personal information such as public IP addresses.
3. Please do not run logs through any translation program. Especially beware if your browser translates pages by default.
4. Please do not include logs as screenshots, with the only exception being client logs in browsers.
- type: textarea
id: jellyfin-logs
id: logs
attributes:
label: Jellyfin logs
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
render: shell
validations:
required: true
@@ -188,20 +109,24 @@ body:
id: ffmpeg-logs
attributes:
label: FFmpeg logs
description: Relevant FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log. This field is considered mandatory for transcoding related issues. It's also important to include the specific codec details.
description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log.
placeholder: This field is mandatory for debugging hardware transcoding issues. It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
render: shell
- type: textarea
id: browser-logs
id: browserlogs
attributes:
label: Client / Browser logs
description: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
label: Please attach any browser or client logs here
placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
- type: textarea
id: screenshots
attributes:
label: Relevant screenshots or videos
description: Attach relevant screenshots or videos related to this report.
- type: textarea
id: additional-information
label: Please attach any screenshots here
placeholder: Images can be pasted directly into the textbox and will be hosted by github.
- type: checkboxes
id: terms
attributes:
label: Additional information
description: Any additional information that might be useful to this issue.
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
dotnet-version: '9.0.x'
dotnet-version: '8.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3

View File

@@ -1,159 +0,0 @@
name: ABI Compatibility
on:
pull_request_target:
permissions: {}
jobs:
abi-head:
name: ABI - HEAD
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with:
dotnet-version: '9.0.x'
- name: Build
run: |
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: abi-head
retention-days: 14
if-no-files-found: error
path: out/
abi-base:
name: ABI - BASE
if: ${{ github.base_ref != '' }}
runs-on: ubuntu-latest
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
with:
dotnet-version: '9.0.x'
- name: Checkout common ancestor
env:
HEAD_REF: ${{ github.head_ref }}
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Build
run: |
dotnet build Jellyfin.Server -o ./out
- name: Upload Head
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: abi-base
retention-days: 14
if-no-files-found: error
path: out/
abi-diff:
permissions:
pull-requests: write # to create or update comment (peter-evans/create-or-update-comment)
name: ABI - Difference
if: ${{ github.event_name == 'pull_request_target' }}
runs-on: ubuntu-latest
needs:
- abi-head
- abi-base
steps:
- name: Download abi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: abi-head
path: abi-head
- name: Download abi-base
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: abi-base
path: abi-base
- name: Setup ApiCompat
run: |
dotnet tool install --global Microsoft.DotNet.ApiCompat.Tool
- name: Run ApiCompat
id: diff
run: |
{
echo 'body<<EOF'
for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
printf "\n${file}\n${COMPAT_OUTPUT}\n"
fi
done
echo EOF
} >> $GITHUB_OUTPUT
- name: Find difference comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: abi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
token: ${{ secrets.JF_BOT_TOKEN }}
body: |
<!--abi-diff-workflow-comment-->
<details>
<summary>ABI Difference</summary>
```
${{ steps.diff.outputs.body }}
```
</details>
- name: Reply or edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
token: ${{ secrets.JF_BOT_TOKEN }}
body: |
<!--abi-diff-workflow-comment-->
<details>
<summary>ABI Difference</summary>
No changes to the ABI found. See history of this comment for previous changes.
</details>

View File

@@ -3,8 +3,6 @@ on:
push:
branches:
- master
tags:
- 'v*'
pull_request_target:
permissions: {}
@@ -16,23 +14,23 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
dotnet-version: '9.0.x'
dotnet-version: '8.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: openapi-head
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
openapi-base:
name: OpenAPI - BASE
@@ -41,7 +39,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -55,18 +53,18 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
dotnet-version: '9.0.x'
dotnet-version: '8.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: openapi-base
retention-days: 14
if-no-files-found: error
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net9.0/openapi.json
path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net8.0/openapi.json
openapi-diff:
permissions:
@@ -80,12 +78,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-base
path: openapi-base
@@ -101,42 +99,36 @@ jobs:
- id: read-diff
name: Read openapi-diff output
run: |
# Read and fix markdown
body=$(cat openapi-changes.md)
# Write to workflow summary
echo "$body" >> $GITHUB_STEP_SUMMARY
# Set ApiChanged var
if [ "$body" != '' ]; then
echo "ApiChanged=1" >> "$GITHUB_OUTPUT"
else
echo "ApiChanged=0" >> "$GITHUB_OUTPUT"
fi
# Add header/footer for diff comment
echo '<!--openapi-diff-workflow-comment-->' > openapi-changes-reply.md
echo "<details>" >> openapi-changes-reply.md
echo "<summary>Changes in OpenAPI specification found. Expand to see details.</summary>" >> openapi-changes-reply.md
echo "" >> openapi-changes-reply.md
echo "$body" >> openapi-changes-reply.md
echo "" >> openapi-changes-reply.md
echo "</details>" >> openapi-changes-reply.md
body="${body//'%'/'%25'}"
body="${body//$'\n'/'%0A'}"
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.read-diff.outputs.ApiChanged == '1' }}
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body-path: openapi-changes-reply.md
body: |
<!--openapi-diff-workflow-comment-->
<details>
<summary>Changes in OpenAPI specification found. Expand to see details.</summary>
${{ steps.read-diff.outputs.body }}
</details>
- name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: ${{ steps.read-diff.outputs.ApiChanged == '0' && steps.find-comment.outputs.comment-id != '' }}
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
@@ -146,9 +138,11 @@ jobs:
No changes to OpenAPI specification found. See history of this comment for previous changes.
publish-unstable:
publish:
name: OpenAPI - Publish Unstable Spec
if: ${{ github.event_name != 'pull_request_target' && !startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
if: |
github.event_name != 'pull_request_target' &&
contains(github.repository_owner, 'jellyfin')
runs-on: ubuntu-latest
needs:
- openapi-head
@@ -158,12 +152,12 @@ jobs:
run: |-
echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (unstable) to repository server
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@@ -172,7 +166,7 @@ jobs:
strip_components: 1
target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (unstable) into place
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
@@ -207,65 +201,3 @@ jobs:
sudo ln -s unstable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-unstable_previous.json
fi
) 200>/run/workflows/openapi-unstable.lock
publish-stable:
name: OpenAPI - Publish Stable Spec
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.repository_owner, 'jellyfin') }}
runs-on: ubuntu-latest
needs:
- openapi-head
steps:
- name: Set version number
id: version
run: |-
echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
- name: Download openapi-head
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: openapi-head
path: openapi-head
- name: Upload openapi.json (stable) to repository server
uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
source: openapi-head/openapi.json
strip_components: 1
target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
- name: Move openapi.json (stable) into place
uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
with:
host: "${{ secrets.REPO_HOST }}"
username: "${{ secrets.REPO_USER }}"
key: "${{ secrets.REPO_KEY }}"
debug: false
script_stop: false
script: |
if ! test -d /run/workflows; then
sudo mkdir -p /run/workflows
sudo chown ${{ secrets.REPO_USER }} /run/workflows
fi
(
flock -x -w 300 200 || exit 1
TGT_DIR="/srv/repository/main/openapi"
LAST_SPEC="$( ls -lt ${TGT_DIR}/stable/ | grep 'jellyfin-openapi' | head -1 | awk '{ print $NF }' )"
# If new and previous spec don't differ (diff retcode 0), remove incoming and finish
if diff /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/${LAST_SPEC} &>/dev/null; then
rm -r /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}
exit 0
fi
# Move new spec into place
sudo mv /srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}/openapi.json ${TGT_DIR}/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json
# Delete previous jellyfin-openapi-stable_previous.json
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Move current jellyfin-openapi-stable.json symlink to jellyfin-openapi-stable_previous.json
sudo mv ${TGT_DIR}/jellyfin-openapi-stable.json ${TGT_DIR}/jellyfin-openapi-stable_previous.json
# Create new jellyfin-openapi-stable.json symlink
sudo ln -s stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}.json ${TGT_DIR}/jellyfin-openapi-stable.json
# Check that the previous openapi stable spec link is correct
if [[ "$( readlink ${TGT_DIR}/jellyfin-openapi-stable_previous.json )" != "stable/${LAST_SPEC}" ]]; then
sudo rm ${TGT_DIR}/jellyfin-openapi-stable_previous.json
sudo ln -s stable/${LAST_SPEC} ${TGT_DIR}/jellyfin-openapi-stable_previous.json
fi
) 200>/run/workflows/openapi-stable.lock

View File

@@ -9,20 +9,19 @@ on:
pull_request:
env:
SDK_VERSION: "9.0.x"
SDK_VERSION: "8.0.x"
jobs:
run-tests:
strategy:
matrix:
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
fail-fast: false
runs-on: "${{ matrix.os }}"
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5.0.0
- uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0
with:
dotnet-version: ${{ env.SDK_VERSION }}
@@ -35,7 +34,7 @@ jobs:
--verbosity minimal
- name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@9870ed167742d546b99962ff815fcc1098355ed8 # v5.4.17
uses: danielpalme/ReportGenerator-GitHub-Action@2a2d60ea1c7e811f54684179af6ac1ae8c1ce69a # 5.2.5
with:
reports: "**/coverage.cobertura.xml"
targetdir: "merged/"

View File

@@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -34,19 +34,107 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
check-backport:
permissions:
contents: read
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@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
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@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
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@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
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@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
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
rename:
name: Rename
if: contains(github.event.comment.body, '@jellyfin-bot rename') && github.event.comment.author_association == 'MEMBER'
runs-on: ubuntu-latest
steps:
- name: pull in script
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
with:
python-version: '3.14'
python-version: '3.12'
cache: 'pip'
- name: install python packages
run: pip install -r rename/requirements.txt

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true

View File

@@ -10,13 +10,13 @@ jobs:
issues: write
steps:
- name: pull in script
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
repository: jellyfin/jellyfin-triage-script
- name: install python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
with:
python-version: '3.14'
python-version: '3.12'
cache: 'pip'
- name: install python packages
run: pip install -r main-repo-triage/requirements.txt

View File

@@ -12,10 +12,10 @@ jobs:
label:
name: Labeling
runs-on: ubuntu-latest
if: ${{ github.repository == 'jellyfin/jellyfin' && github.event.issue.pull_request }}
if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps:
- name: Apply label
uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
ascending: true

View File

@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with:
ref: ${{ env.TAG_BRANCH }}

View File

@@ -1,13 +1,12 @@
{
"recommendations": [
"recommendations": [
"ms-dotnettools.csharp",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"ms-dotnettools.vscode-dotnet-runtime",
"ms-dotnettools.csdevkit",
"alexcvzz.vscode-sqlite"
],
"unwantedRecommendations": [
"ms-dotnettools.csdevkit"
],
"unwantedRecommendations": [
]
]
}

6
.vscode/launch.json vendored
View File

@@ -6,7 +6,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
"args": [],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -22,7 +22,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
"args": ["--nowebclient"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",
@@ -34,7 +34,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net9.0/jellyfin.dll",
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
"cwd": "${workspaceFolder}/Jellyfin.Server",
"console": "internalConsole",

View File

@@ -1,3 +0,0 @@
{
"dotnet.preferVisualStudioCodeFileSystemWatcher": true
}

View File

@@ -27,11 +27,9 @@
- [cryptobank](https://github.com/cryptobank)
- [cvium](https://github.com/cvium)
- [dannymichel](https://github.com/dannymichel)
- [darioackermann](https://github.com/darioackermann)
- [DaveChild](https://github.com/DaveChild)
- [DavidFair](https://github.com/DavidFair)
- [Delgan](https://github.com/Delgan)
- [Derpipose](https://github.com/Derpipose)
- [dcrdev](https://github.com/dcrdev)
- [dhartung](https://github.com/dhartung)
- [dinki](https://github.com/dinki)
@@ -62,13 +60,11 @@
- [ikomhoog](https://github.com/ikomhoog)
- [iwalton3](https://github.com/iwalton3)
- [jftuga](https://github.com/jftuga)
- [jkhsjdhjs](https://github.com/jkhsjdhjs)
- [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
- [JustAMan](https://github.com/JustAMan)
- [justinfenn](https://github.com/justinfenn)
- [JPVenson](https://github.com/JPVenson)
- [KerryRJ](https://github.com/KerryRJ)
- [Larvitar](https://github.com/Larvitar)
- [LeoVerto](https://github.com/LeoVerto)
@@ -141,7 +137,6 @@
- [ThibaultNocchi](https://github.com/ThibaultNocchi)
- [thornbill](https://github.com/thornbill)
- [ThreeFive-O](https://github.com/ThreeFive-O)
- [tjwalkr3](https://github.com/tjwalkr3)
- [TrisMcC](https://github.com/TrisMcC)
- [trumblejoe](https://github.com/trumblejoe)
- [TtheCreator](https://github.com/TtheCreator)
@@ -188,23 +183,6 @@
- [btopherjohnson](https://github.com/btopherjohnson)
- [GeorgeH005](https://github.com/GeorgeH005)
- [Vedant](https://github.com/viktory36/)
- [NotSaifA](https://github.com/NotSaifA)
- [HonestlyWhoKnows](https://github.com/honestlywhoknows)
- [TheMelmacian](https://github.com/TheMelmacian)
- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
- [pret0rian8](https://github.com/pret0rian)
- [jaina heartles](https://github.com/heartles)
- [oxixes](https://github.com/oxixes)
- [elfalem](https://github.com/elfalem)
- [Kenneth Cochran](https://github.com/kennethcochran)
- [benedikt257](https://github.com/benedikt257)
- [revam](https://github.com/revam)
- [allesmi](https://github.com/allesmi)
- [ThunderClapLP](https://github.com/ThunderClapLP)
- [Shoham Peller](https://github.com/spellr)
- [theshoeshiner](https://github.com/theshoeshiner)
- [TokerX](https://github.com/TokerX)
- [GeneMarks](https://github.com/GeneMarks)
# Emby Contributors
@@ -277,7 +255,3 @@
- [JPUC1143](https://github.com/Jpuc1143/)
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
- [Robert Lützner](https://github.com/rluetzner)
- [Nathan McCrina](https://github.com/nfmccrina)
- [Martin Reuter](https://github.com/reuterma24)
- [Michael McElroy](https://github.com/mcmcelro)
- [Soumyadip Auddy](https://github.com/SoumyadipAuddy)

View File

@@ -3,11 +3,11 @@
<PropertyGroup>
<Nullable>enable</Nullable>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>NU1902;NU1903</WarningsNotAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@@ -19,9 +19,4 @@
<AdditionalFiles Include="$(MSBuildThisFileDirectory)/stylecop.json" />
</ItemGroup>
<!-- Custom Analyzers -->
<ItemGroup Condition=" '$(MSBuildProjectName)' != 'Jellyfin.CodeAnalysis' AND '$(Configuration)' == 'Debug' ">
<ProjectReference Include="$(MSBuildThisFileDirectory)src/Jellyfin.CodeAnalysis/Jellyfin.CodeAnalysis.csproj" OutputItemType="Analyzer" />
</ItemGroup>
</Project>

View File

@@ -4,96 +4,89 @@
</PropertyGroup>
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
<ItemGroup Label="Package Dependencies">
<PackageVersion Include="AsyncKeyedLock" Version="7.1.7" />
<PackageVersion Include="AsyncKeyedLock" Version="6.4.2" />
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BDInfo" Version="0.8.0" />
<PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.2" />
<PackageVersion Include="BlurHashSharp" Version="1.3.2" />
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Diacritics" Version="4.0.17" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="Diacritics" Version="3.3.29" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="FsCheck.Xunit" Version="3.3.1" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.4.3" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.2" />
<PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
<PackageVersion Include="Ignore" Version="0.2.1" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="4.0.12" />
<PackageVersion Include="LrcParser" Version="2025.623.0" />
<PackageVersion Include="libse" Version="4.0.5" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.10" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.10" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.10" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageVersion Include="MimeTypes" Version="2.5.2" />
<PackageVersion Include="Morestachio" Version="5.0.1.631" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.4" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="1.1.0.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
<PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Polly" Version="8.6.4" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.2.0" />
<!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
<PackageVersion Include="SkiaSharp" Version="3.116.1" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
<PackageVersion Include="SkiaSharp" Version="2.88.8" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.8" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="3.2.1" />
<PackageVersion Include="Svg.Skia" Version="1.0.0.18" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.10" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.3" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="z440.atl.core" Version="7.6.0" />
<PackageVersion Include="TMDbLib" Version="2.3.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="TMDbLib" Version="2.2.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.5.23" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.8" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.7.1" />
</ItemGroup>
</Project>
</Project>

View File

@@ -21,8 +21,8 @@ namespace Emby.Naming.Common
/// </summary>
public NamingOptions()
{
VideoFileExtensions =
[
VideoFileExtensions = new[]
{
".001",
".3g2",
".3gp",
@@ -77,10 +77,10 @@ namespace Emby.Naming.Common
".wmv",
".wtv",
".xvid"
];
};
VideoFlagDelimiters =
[
VideoFlagDelimiters = new[]
{
'(',
')',
'-',
@@ -88,15 +88,15 @@ namespace Emby.Naming.Common
'_',
'[',
']'
];
};
StubFileExtensions =
[
StubFileExtensions = new[]
{
".disc"
];
};
StubTypes =
[
StubTypes = new[]
{
new StubTypeRule(
stubType: "dvd",
token: "dvd"),
@@ -136,32 +136,32 @@ namespace Emby.Naming.Common
new StubTypeRule(
stubType: "tv",
token: "DSR")
];
};
VideoFileStackingRules =
[
VideoFileStackingRules = new[]
{
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
];
};
CleanDateTimes =
[
CleanDateTimes = new[]
{
@"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*",
@"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19[0-9]{2}|20[0-9]{2})(?![0-9]+|\W[0-9]{2}\W[0-9]{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19[0-9]{2}|20[0-9]{2})*"
];
};
CleanStrings =
[
CleanStrings = new[]
{
@"^\s*(?<cleaned>.+?)[ _\,\.\(\)\[\]\-](3d|sbs|tab|hsbs|htab|mvc|HDR|HDC|UHD|UltraHD|4k|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multi|subs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|cd[1-9]|r5|bd5|bd|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|480p|480i|576p|576i|720p|720i|1080p|1080i|2160p|hrhd|hrhdtv|hddvd|bluray|blu-ray|x264|x265|h264|h265|xvid|xvidvd|xxx|www.www|AAC|DTS|\[.*\])([ _\,\.\(\)\[\]\-]|$)",
@"^(?<cleaned>.+?)(\[.*\])",
@"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
@"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
@"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
];
};
SubtitleFileExtensions =
[
SubtitleFileExtensions = new[]
{
".ass",
".mks",
".sami",
@@ -171,29 +171,27 @@ namespace Emby.Naming.Common
".sub",
".sup",
".vtt",
];
};
LyricFileExtensions =
[
LyricFileExtensions = new[]
{
".lrc",
".elrc",
".txt"
];
};
AlbumStackingPrefixes =
[
AlbumStackingPrefixes = new[]
{
"cd",
"digital media",
"disc",
"disk",
"vol",
"volume",
"part",
"act"
];
"volume"
};
ArtistSubfolders =
[
ArtistSubfolders = new[]
{
"albums",
"broadcasts",
"bootlegs",
@@ -208,10 +206,10 @@ namespace Emby.Naming.Common
"soundtracks",
"spokenwords",
"streets"
];
};
AudioFileExtensions =
[
AudioFileExtensions = new[]
{
".669",
".3gp",
".aa",
@@ -240,8 +238,6 @@ namespace Emby.Naming.Common
".dsp",
".dts",
".dvf",
".eac3",
".ec3",
".far",
".flac",
".gdm",
@@ -292,33 +288,33 @@ namespace Emby.Naming.Common
".xm",
".xsp",
".ymf"
];
};
MediaFlagDelimiters =
[
MediaFlagDelimiters = new[]
{
'.'
];
};
MediaForcedFlags =
[
MediaForcedFlags = new[]
{
"foreign",
"forced"
];
};
MediaDefaultFlags =
[
MediaDefaultFlags = new[]
{
"default"
];
};
MediaHearingImpairedFlags =
[
MediaHearingImpairedFlags = new[]
{
"cc",
"hi",
"sdh"
];
};
EpisodeExpressions =
[
EpisodeExpressions = new[]
{
// *** Begin Kodi Standard Naming
// <!-- foo.s01.e01, foo.s01_e01, S01E02 foo, S01 - E02 -->
new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![Ss]([0-9]+)[][ ._-]*[Ee]([0-9]+))[^\\\/])*)?[Ss](?<seasonnumber>[0-9]+)[][ ._-]*[Ee](?<epnumber>[0-9]+)([^\\/]*)$")
@@ -331,23 +327,23 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
{
DateTimeFormats =
[
DateTimeFormats = new[]
{
"yyyy.MM.dd",
"yyyy-MM-dd",
"yyyy_MM_dd",
"yyyy MM dd"
]
}
},
new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
{
DateTimeFormats =
[
DateTimeFormats = new[]
{
"dd.MM.yyyy",
"dd-MM-yyyy",
"dd_MM_yyyy",
"dd MM yyyy"
]
}
},
// This isn't a Kodi naming rule, but the expression below causes false episode numbers for
@@ -471,18 +467,10 @@ namespace Emby.Naming.Common
{
IsNamed = true
},
};
// Anime style expression
// "[Group][Series Name][21][1080p][FLAC][HASH]"
// "[Group] Series Name [04][BDRIP]"
new EpisodeExpression(@"(?:\[(?:[^\]]+)\]\s*)?(?<seriesname>\[[^\]]+\]|[^[\]]+)\s*\[(?<epnumber>[0-9]+)\]")
{
IsNamed = true
},
];
VideoExtraRules =
[
VideoExtraRules = new[]
{
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.DirectoryName,
@@ -573,18 +561,6 @@ namespace Emby.Naming.Common
"trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Filename,
"sample",
MediaType.Video),
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
@@ -606,7 +582,13 @@ namespace Emby.Naming.Common
new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Suffix,
"- trailer",
" trailer",
MediaType.Video),
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Filename,
"sample",
MediaType.Video),
new ExtraRule(
@@ -630,9 +612,15 @@ namespace Emby.Naming.Common
new ExtraRule(
ExtraType.Sample,
ExtraRuleType.Suffix,
"- sample",
" sample",
MediaType.Video),
new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
@@ -692,14 +680,14 @@ namespace Emby.Naming.Common
ExtraRuleType.Suffix,
"-other",
MediaType.Video)
];
};
AllExtrasTypesFolderNames = VideoExtraRules
.Where(i => i.RuleType == ExtraRuleType.DirectoryName)
.ToDictionary(i => i.Token, i => i.ExtraType, StringComparer.OrdinalIgnoreCase);
Format3DRules =
[
Format3DRules = new[]
{
// Kodi rules:
new Format3DRule(
precedingToken: "3d",
@@ -726,10 +714,10 @@ namespace Emby.Naming.Common
new Format3DRule("tab"),
new Format3DRule("sbs3d"),
new Format3DRule("mvc")
];
};
AudioBookPartsExpressions =
[
AudioBookPartsExpressions = new[]
{
// Detect specified chapters, like CH 01
@"ch(?:apter)?[\s_-]?(?<chapter>[0-9]+)",
// Detect specified parts, like Part 02
@@ -742,14 +730,14 @@ namespace Emby.Naming.Common
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number.
@"dis(?:c|k)[\s_-]?(?<chapter>[0-9]+)"
];
};
AudioBookNamesExpressions =
[
AudioBookNamesExpressions = new[]
{
// Detect year usually in brackets after name Batman (2020)
@"^(?<name>.+?)\s*\(\s*(?<year>[0-9]{4})\s*\)\s*$",
@"^\s*(?<name>[^ ].*?)\s*$"
];
};
MultipleEpisodeExpressions = new[]
{
@@ -889,12 +877,12 @@ namespace Emby.Naming.Common
/// <summary>
/// Gets list of clean datetime regular expressions.
/// </summary>
public Regex[] CleanDateTimeRegexes { get; private set; } = [];
public Regex[] CleanDateTimeRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
/// Gets list of clean string regular expressions.
/// </summary>
public Regex[] CleanStringRegexes { get; private set; } = [];
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
/// <summary>
/// Compiles raw regex strings into regexes.

View File

@@ -6,7 +6,7 @@
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
@@ -36,7 +36,7 @@
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Naming</PackageId>
<VersionPrefix>10.11.3</VersionPrefix>
<VersionPrefix>10.9.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>

View File

@@ -97,21 +97,17 @@ namespace Emby.Naming.ExternalFiles
if (culture is not null && pathInfo.Language is null)
{
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
? culture.Name
: culture.ThreeLetterISOLanguageName;
pathInfo.Language = culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
else if (culture is not null && pathInfo.Language == "hin")
{
// Hindi language code "hi" collides with a hearing impaired flag - use as Hindi only if no other language is set
pathInfo.IsHearingImpaired = true;
pathInfo.Language = culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase)
? culture.Name
: culture.ThreeLetterISOLanguageName;
pathInfo.Language = culture.ThreeLetterISOLanguageName;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);
}
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Equals(s, StringComparison.OrdinalIgnoreCase)))
else if (_namingOptions.MediaHearingImpairedFlags.Any(s => currentSliceWithoutSeparator.Contains(s, StringComparison.OrdinalIgnoreCase)))
{
pathInfo.IsHearingImpaired = true;
extraString = extraString.Replace(currentSlice, string.Empty, StringComparison.OrdinalIgnoreCase);

View File

@@ -1,40 +1,41 @@
using System;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
namespace Emby.Naming.TV
{
/// <summary>
/// Class to parse season paths.
/// </summary>
public static partial class SeasonPathParser
public static class SeasonPathParser
{
private static readonly Regex CleanNameRegex = new(@"[ ._\-\[\]]", RegexOptions.Compiled);
[GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPre();
[GeneratedRegex(@"^\s*(?:[[]*|[]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>\d+?)(?=\d{3,4}p|[^\d]|$)(?!\s*[Ee]\d)(?<rightpart>.*)$", RegexOptions.IgnoreCase)]
private static partial Regex ProcessPost();
[GeneratedRegex(@"[sS](\d{1,4})(?!\d|[eE]\d)(?=\.|_|-|\[|\]|\s|$)", RegexOptions.None)]
private static partial Regex SeasonPrefix();
/// <summary>
/// A season folder must contain one of these somewhere in the name.
/// </summary>
private static readonly string[] _seasonFolderNames =
{
"season",
"sæson",
"temporada",
"saison",
"staffel",
"series",
"сезон",
"stagione"
};
/// <summary>
/// Attempts to parse season number from path.
/// </summary>
/// <param name="path">Path to season.</param>
/// <param name="parentPath">Folder name of the parent.</param>
/// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
/// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
/// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders)
public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
{
var result = new SeasonPathParserResult();
var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders);
var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
result.SeasonNumber = seasonNumber;
@@ -51,70 +52,89 @@ namespace Emby.Naming.TV
/// Gets the season number from path.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="parentFolderName">The parent folder name.</param>
/// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
/// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
/// <returns>System.Nullable{System.Int32}.</returns>
private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
string path,
string? parentFolderName,
bool supportSpecialAliases,
bool supportNumericSeasonFolders)
{
var fileName = Path.GetFileName(path);
string filename = Path.GetFileName(path);
var seasonPrefixMatch = SeasonPrefix().Match(fileName);
if (seasonPrefixMatch.Success &&
int.TryParse(seasonPrefixMatch.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
if (supportSpecialAliases)
{
return (val, true);
if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase))
{
return (0, true);
}
if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase))
{
return (0, true);
}
}
string filename = CleanNameRegex.Replace(fileName, string.Empty);
if (parentFolderName is not null)
if (supportNumericSeasonFolders)
{
var cleanParent = CleanNameRegex.Replace(parentFolderName, string.Empty);
filename = filename.Replace(cleanParent, string.Empty, StringComparison.OrdinalIgnoreCase);
if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return (val, true);
}
}
if (supportSpecialAliases &&
(filename.Equals("specials", StringComparison.OrdinalIgnoreCase) ||
filename.Equals("extras", StringComparison.OrdinalIgnoreCase)))
if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase))
{
return (0, true);
var testFilename = filename.AsSpan().Slice(1);
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
return (val, true);
}
}
if (supportNumericSeasonFolders &&
int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out val))
// Look for one of the season folder names
foreach (var name in _seasonFolderNames)
{
return (val, true);
if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
{
var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
if (result.SeasonNumber.HasValue)
{
return result;
}
break;
}
}
var preMatch = ProcessPre().Match(filename);
if (preMatch.Success)
var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
return CheckMatch(preMatch);
}
else
{
var postMatch = ProcessPost().Match(filename);
return CheckMatch(postMatch);
}
}
private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
{
var numberString = match.Groups["seasonnumber"];
if (numberString.Success)
{
if (int.TryParse(numberString.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var seasonNumber))
if (TryGetSeasonNumberFromPart(part, out int seasonNumber))
{
return (seasonNumber, true);
}
}
return (null, false);
return (null, true);
}
private static bool TryGetSeasonNumberFromPart(ReadOnlySpan<char> part, out int seasonNumber)
{
seasonNumber = 0;
if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
seasonNumber = value;
return true;
}
return false;
}
/// <summary>

View File

@@ -12,7 +12,7 @@ namespace Emby.Naming.TV
/// <summary>
/// Regex that matches strings of at least 2 characters separated by a dot or underscore.
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
/// preserving names like "S.H.O.W".
/// preserving namings like "S.H.O.W".
/// </summary>
[GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
private static partial Regex SeriesNameRegex();

View File

@@ -1,44 +0,0 @@
using System;
using System.Linq;
using MediaBrowser.Model.Entities;
namespace Emby.Naming.TV;
/// <summary>
/// Helper class for TV metadata parsing.
/// </summary>
public static class TvParserHelpers
{
private static readonly string[] _continuingState = ["Pilot", "Returning Series", "Returning"];
private static readonly string[] _endedState = ["Cancelled", "Canceled"];
/// <summary>
/// Tries to parse a string into <see cref="SeriesStatus"/>.
/// </summary>
/// <param name="status">The status string.</param>
/// <param name="enumValue">The <see cref="SeriesStatus"/>.</param>
/// <returns>Returns true if parsing was successful.</returns>
public static bool TryParseSeriesStatus(string status, out SeriesStatus? enumValue)
{
if (Enum.TryParse(status, true, out SeriesStatus seriesStatus))
{
enumValue = seriesStatus;
return true;
}
if (_continuingState.Contains(status, StringComparer.OrdinalIgnoreCase))
{
enumValue = SeriesStatus.Continuing;
return true;
}
if (_endedState.Contains(status, StringComparer.OrdinalIgnoreCase))
{
enumValue = SeriesStatus.Ended;
return true;
}
enumValue = null;
return false;
}
}

View File

@@ -18,49 +18,68 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">Path to file.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>Returns <see cref="ExtraResult"/> object.</returns>
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "")
public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
{
ExtraResult result = new ExtraResult();
var result = new ExtraResult();
bool isAudioFile = AudioFileParser.IsAudioFile(path, namingOptions);
bool isVideoFile = VideoResolver.IsVideoFile(path, namingOptions);
ReadOnlySpan<char> pathSpan = path.AsSpan();
ReadOnlySpan<char> fileName = Path.GetFileName(pathSpan);
ReadOnlySpan<char> fileNameWithoutExtension = Path.GetFileNameWithoutExtension(pathSpan);
// Trim the digits from the end of the filename so we can recognize things like -trailer2
ReadOnlySpan<char> trimmedFileNameWithoutExtension = fileNameWithoutExtension.TrimEnd(_digits);
ReadOnlySpan<char> directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
foreach (ExtraRule rule in namingOptions.VideoExtraRules)
for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
{
if ((rule.MediaType == MediaType.Audio && !isAudioFile)
|| (rule.MediaType == MediaType.Video && !isVideoFile))
var rule = namingOptions.VideoExtraRules[i];
if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
|| (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
{
continue;
}
bool isMatch = rule.RuleType switch
var pathSpan = path.AsSpan();
if (rule.RuleType == ExtraRuleType.Filename)
{
ExtraRuleType.Filename => fileNameWithoutExtension.Equals(rule.Token, StringComparison.OrdinalIgnoreCase),
ExtraRuleType.Suffix => trimmedFileNameWithoutExtension.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase),
ExtraRuleType.Regex => Regex.IsMatch(fileName, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled),
ExtraRuleType.DirectoryName => directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase),
_ => false,
};
var filename = Path.GetFileNameWithoutExtension(pathSpan);
if (!isMatch)
if (filename.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Suffix)
{
continue;
// Trim the digits from the end of the filename so we can recognize things like -trailer2
var filename = Path.GetFileNameWithoutExtension(pathSpan).TrimEnd(_digits);
if (filename.EndsWith(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
var filename = Path.GetFileName(path.AsSpan());
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
if (isMatch)
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
else if (rule.RuleType == ExtraRuleType.DirectoryName)
{
var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{
result.ExtraType = rule.ExtraType;
result.Rule = rule;
}
}
result.ExtraType = rule.ExtraType;
result.Rule = rule;
return result;
if (result.ExtraType is not null)
{
return result;
}
}
return result;

View File

@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Emby.Naming.Video;

View File

@@ -132,7 +132,7 @@ namespace Emby.Naming.Video
}
}
private sealed class StackMetadata
private class StackMetadata
{
public StackMetadata(bool isDirectory, bool isNumerical, string partType)
{

View File

@@ -27,9 +27,8 @@ namespace Emby.Naming.Video
/// <param name="namingOptions">The naming options.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "")
public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
{
// Filter out all extras, otherwise they could cause stacks to not be resolved
// See the unit test TestStackedWithTrailer
@@ -66,7 +65,7 @@ namespace Emby.Naming.Video
{
var info = new VideoInfo(stack.Name)
{
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot))
Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
.OfType<VideoFileInfo>()
.ToList()
};
@@ -142,9 +141,7 @@ namespace Emby.Naming.Video
{
if (group.Key)
{
videos.InsertRange(0, group
.OrderByDescending(x => ResolutionRegex().Match(x.Files[0].FileNameWithoutExtension.ToString()).Value, new AlphanumericComparator())
.ThenBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
}
else
{

View File

@@ -17,11 +17,10 @@ namespace Emby.Naming.Video
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="parseName">Whether to parse the name or use the filename.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>VideoFileInfo.</returns>
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
{
return Resolve(path, true, namingOptions, parseName, libraryRoot);
return Resolve(path, true, namingOptions, parseName);
}
/// <summary>
@@ -29,11 +28,10 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="path">The path.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>VideoFileInfo.</returns>
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions, string? libraryRoot = "")
public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
{
return Resolve(path, false, namingOptions, libraryRoot: libraryRoot);
return Resolve(path, false, namingOptions);
}
/// <summary>
@@ -43,10 +41,9 @@ namespace Emby.Naming.Video
/// <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="libraryRoot">Top-level folder for the containing library.</param>
/// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
{
if (string.IsNullOrEmpty(path))
{
@@ -78,7 +75,7 @@ namespace Emby.Naming.Video
var format3DResult = Format3DParser.Parse(path, namingOptions);
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions, libraryRoot);
var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
var name = Path.GetFileNameWithoutExtension(path);

View File

@@ -19,7 +19,7 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

View File

@@ -26,7 +26,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
private readonly ILogger<PhotoProvider> _logger;
private readonly IImageProcessor _imageProcessor;
// Other extensions might cause taglib to hang
// These are causing taglib to hang
private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"];
/// <summary>
@@ -49,7 +49,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
if (item.IsFileProtocol)
{
var file = directoryService.GetFile(item.Path);
return file is not null && item.HasChanged(file.LastWriteTimeUtc);
return file is not null && file.LastWriteTimeUtc != item.DateModified;
}
return false;
@@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
var dateTaken = image.ImageTag.DateTime;
if (dateTaken.HasValue)
{
item.DateCreated = dateTaken.Value.ToUniversalTime();
item.DateCreated = dateTaken.Value;
item.PremiereDate = dateTaken.Value;
item.ProductionYear = dateTaken.Value.Year;
}

View File

@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
namespace Emby.Server.Implementations.AppBase
@@ -33,101 +30,80 @@ namespace Emby.Server.Implementations.AppBase
ConfigurationDirectoryPath = configurationDirectoryPath;
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
/// <inheritdoc/>
/// <summary>
/// Gets the path to the program data folder.
/// </summary>
/// <value>The program data path.</value>
public string ProgramDataPath { get; }
/// <inheritdoc/>
public string WebPath { get; }
/// <inheritdoc/>
/// <summary>
/// Gets the path to the system folder.
/// </summary>
/// <value>The path to the system folder.</value>
public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
/// <inheritdoc/>
/// <summary>
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
public string DataPath { get; }
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";
/// <inheritdoc/>
/// <summary>
/// Gets the image cache path.
/// </summary>
/// <value>The image cache path.</value>
public string ImageCachePath => Path.Combine(CachePath, "images");
/// <inheritdoc/>
/// <summary>
/// Gets the path to the plugin directory.
/// </summary>
/// <value>The plugins path.</value>
public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
/// <inheritdoc/>
/// <summary>
/// Gets the path to the plugin configurations directory.
/// </summary>
/// <value>The plugin configurations path.</value>
public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
/// <inheritdoc/>
/// <summary>
/// Gets the path to the log directory.
/// </summary>
/// <value>The log directory path.</value>
public string LogDirectoryPath { get; }
/// <inheritdoc/>
/// <summary>
/// Gets the path to the application configuration root directory.
/// </summary>
/// <value>The configuration directory path.</value>
public string ConfigurationDirectoryPath { get; }
/// <inheritdoc/>
/// <summary>
/// Gets the path to the system configuration file.
/// </summary>
/// <value>The system configuration file path.</value>
public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
/// <inheritdoc/>
/// <summary>
/// Gets or sets the folder path to the cache directory.
/// </summary>
/// <value>The cache directory.</value>
public string CachePath { get; set; }
/// <inheritdoc/>
public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
/// <inheritdoc />
public string TrickplayPath => Path.Combine(DataPath, "trickplay");
/// <inheritdoc />
public string BackupPath => Path.Combine(DataPath, "backups");
/// <inheritdoc />
public virtual void MakeSanityCheckOrThrow()
{
CreateAndCheckMarker(ConfigurationDirectoryPath, "config");
CreateAndCheckMarker(LogDirectoryPath, "log");
CreateAndCheckMarker(PluginsPath, "plugin");
CreateAndCheckMarker(ProgramDataPath, "data");
CreateAndCheckMarker(CachePath, "cache");
CreateAndCheckMarker(DataPath, "data");
}
/// <inheritdoc />
public void CreateAndCheckMarker(string path, string markerName, bool recursive = false)
{
Directory.CreateDirectory(path);
CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
}
private IEnumerable<string> GetMarkers(string path, bool recursive = false)
{
return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
}
private void CheckOrCreateMarker(string path, string markerName, bool recursive = false)
{
string? otherMarkers = null;
try
{
otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => !Path.GetFileName(e.AsSpan()).Equals(markerName, StringComparison.OrdinalIgnoreCase));
}
catch
{
// Error while checking for marker files, assume none exist and keep going
// TODO: add some logging
}
if (otherMarkers is not null)
{
throw new InvalidOperationException($"Expected to find only {markerName} but found marker for {otherMarkers}.");
}
var markerPath = Path.Combine(path, markerName);
if (!File.Exists(markerPath))
{
FileHelper.CreateEmpty(markerPath);
}
}
/// <summary>
/// Gets the folder path to the temp directory within the cache folder.
/// </summary>
/// <value>The temp directory.</value>
public string TempDirectory => Path.Combine(CachePath, "temp");
}
}

View File

@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
@@ -20,7 +19,7 @@ namespace Emby.Server.Implementations.AppBase
public abstract class BaseConfigurationManager : IConfigurationManager
{
private readonly ConcurrentDictionary<string, object> _configurations = new();
private readonly Lock _configurationSyncLock = new();
private readonly object _configurationSyncLock = new();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@@ -227,7 +226,6 @@ namespace Emby.Server.Implementations.AppBase
Logger.LogInformation("Setting cache path: {Path}", cachePath);
((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
}
/// <summary>

View File

@@ -15,7 +15,6 @@ using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Emby.Naming.Common;
using Emby.Photos;
using Emby.Server.Implementations.Chapters;
using Emby.Server.Implementations.Collections;
using Emby.Server.Implementations.Configuration;
using Emby.Server.Implementations.Cryptography;
@@ -40,10 +39,7 @@ using Jellyfin.Drawing;
using Jellyfin.MediaEncoding.Hls.Playlist;
using Jellyfin.Networking.Manager;
using Jellyfin.Networking.Udp;
using Jellyfin.Server.Implementations.FullSystemBackup;
using Jellyfin.Server.Implementations.Item;
using Jellyfin.Server.Implementations.MediaSegments;
using Jellyfin.Server.Implementations.SystemBackupService;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
@@ -59,14 +55,10 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LibraryTaskScheduler;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
@@ -90,6 +82,7 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Lyric;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.Tmdb;
@@ -97,6 +90,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -271,15 +265,8 @@ namespace Emby.Server.Implementations
? Environment.MachineName
: ConfigurationManager.Configuration.ServerName;
public string RestoreBackupPath { get; set; }
public string ExpandVirtualPath(string path)
{
if (path is null)
{
return null;
}
var appPaths = ApplicationPaths;
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
@@ -414,12 +401,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
var ffmpegValid = Resolve<IMediaEncoder>().SetFFmpegPath();
if (!ffmpegValid)
{
throw new FfmpegException("Failed to find valid ffmpeg");
}
Resolve<IMediaEncoder>().SetFFmpegPath();
Logger.LogInformation("ServerId: {ServerId}", SystemId);
Logger.LogInformation("Core startup complete");
@@ -440,7 +422,7 @@ namespace Emby.Server.Implementations
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
{
_disposableParts.Add(DotNetRuntimeStatsBuilder.Default().StartCollecting());
DotNetRuntimeStatsBuilder.Default().StartCollecting();
}
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
@@ -477,7 +459,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IApplicationHost>(this);
serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton<IBackupService, BackupService>();
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
@@ -505,20 +486,13 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<IItemRepository, BaseItemRepository>();
serviceCollection.AddSingleton<IPeopleRepository, PeopleRepository>();
serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
serviceCollection.AddSingleton<IKeyframeRepository, KeyframeRepository>();
serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
serviceCollection.AddSingleton<EncodingHelper>();
serviceCollection.AddSingleton<IPathManager, PathManager>();
serviceCollection.AddSingleton<IExternalDataManager, ExternalDataManager>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@@ -553,7 +527,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
serviceCollection.AddSingleton<ILimitedConcurrencyLibraryScheduler, LimitedConcurrencyLibraryScheduler>();
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
@@ -563,12 +536,13 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
serviceCollection.AddSingleton<IAuthService, AuthService>();
serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
serviceCollection.AddSingleton<IKeyframeManager, KeyframeManager>();
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
@@ -578,17 +552,28 @@ namespace Emby.Server.Implementations
serviceCollection.AddScoped<DynamicHlsHelper>();
serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>();
serviceCollection.AddSingleton<IDirectoryService, DirectoryService>();
serviceCollection.AddSingleton<IMediaSegmentManager, MediaSegmentManager>();
}
/// <summary>
/// Create services registered with the service container that need to be initialized at application startup.
/// </summary>
/// <param name="startupConfig">The configuration used to initialise the application.</param>
/// <returns>A task representing the service initialization operation.</returns>
public async Task InitializeServices(IConfiguration startupConfig)
public async Task InitializeServices()
{
var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().CreateDbContextAsync().ConfigureAwait(false);
await using (jellyfinDb.ConfigureAwait(false))
{
if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
{
Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false);
Logger.LogInformation("EFCore migrations applied successfully");
}
}
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);
@@ -614,7 +599,7 @@ namespace Emby.Server.Implementations
// Don't use an empty string password
password = string.IsNullOrWhiteSpace(password) ? null : password;
var localCert = X509CertificateLoader.LoadPkcs12FromFile(path, password, X509KeyStorageFlags.UserKeySet);
var localCert = new X509Certificate2(path, password, X509KeyStorageFlags.UserKeySet);
if (!localCert.HasPrivateKey)
{
Logger.LogError("No private key included in SSL cert {CertificateLocation}.", path);
@@ -636,26 +621,22 @@ namespace Emby.Server.Implementations
private void SetStaticProperties()
{
// For now there's no real way to inject these properly
BaseItem.ChapterManager = Resolve<IChapterManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>();
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
Video.RecordingsManager = Resolve<IRecordingsManager>();
Folder.UserViewManager = Resolve<IUserViewManager>();
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
UserView.CollectionManager = Resolve<ICollectionManager>();
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.ApplicationHost = this;
Folder.UserViewManager = Resolve<IUserViewManager>();
Folder.CollectionManager = Resolve<ICollectionManager>();
Folder.LimitedConcurrencyLibraryScheduler = Resolve<ILimitedConcurrencyLibraryScheduler>();
Episode.MediaEncoder = Resolve<IMediaEncoder>();
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
Video.RecordingsManager = Resolve<IRecordingsManager>();
}
/// <summary>
@@ -683,8 +664,7 @@ namespace Emby.Server.Implementations
GetExports<IMetadataService>(),
GetExports<IMetadataProvider>(),
GetExports<IMetadataSaver>(),
GetExports<IExternalId>(),
GetExports<IExternalUrlProvider>());
GetExports<IExternalId>());
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
}

View File

@@ -1,300 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Chapters;
/// <summary>
/// The chapter manager.
/// </summary>
public class ChapterManager : IChapterManager
{
private readonly IFileSystem _fileSystem;
private readonly ILogger<ChapterManager> _logger;
private readonly IMediaEncoder _encoder;
private readonly IChapterRepository _chapterRepository;
private readonly ILibraryManager _libraryManager;
private readonly IPathManager _pathManager;
/// <summary>
/// The first chapter ticks.
/// </summary>
private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
/// <summary>
/// Initializes a new instance of the <see cref="ChapterManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{ChapterManager}"/>.</param>
/// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
/// <param name="encoder">The <see cref="IMediaEncoder"/>.</param>
/// <param name="chapterRepository">The <see cref="IChapterRepository"/>.</param>
/// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
/// <param name="pathManager">The <see cref="IPathManager"/>.</param>
public ChapterManager(
ILogger<ChapterManager> logger,
IFileSystem fileSystem,
IMediaEncoder encoder,
IChapterRepository chapterRepository,
ILibraryManager libraryManager,
IPathManager pathManager)
{
_logger = logger;
_fileSystem = fileSystem;
_encoder = encoder;
_chapterRepository = chapterRepository;
_libraryManager = libraryManager;
_pathManager = pathManager;
}
/// <summary>
/// Determines whether [is eligible for chapter image extraction] [the specified video].
/// </summary>
/// <param name="video">The video.</param>
/// <param name="libraryOptions">The library options for the video.</param>
/// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns>
private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions)
{
if (video.IsPlaceHolder)
{
return false;
}
if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction)
{
return false;
}
if (video.IsShortcut)
{
return false;
}
if (!video.IsCompleteMedia)
{
return false;
}
// Can't extract images if there are no video streams
return video.DefaultVideoStreamIndex.HasValue;
}
private long GetAverageDurationBetweenChapters(IReadOnlyList<ChapterInfo> chapters)
{
if (chapters.Count < 2)
{
return 0;
}
long sum = 0;
for (int i = 1; i < chapters.Count; i++)
{
sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks;
}
return sum / chapters.Count;
}
/// <inheritdoc />
public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
{
if (chapters.Count == 0)
{
return true;
}
var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (!IsEligibleForChapterImageExtraction(video, libraryOptions))
{
extractImages = false;
}
var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
var threshold = TimeSpan.FromSeconds(1).Ticks;
if (averageChapterDuration < threshold)
{
_logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold);
extractImages = false;
}
var success = true;
var changesMade = false;
var runtimeTicks = video.RunTimeTicks ?? 0;
var currentImages = GetSavedChapterImages(video, directoryService);
foreach (var chapter in chapters)
{
if (chapter.StartPositionTicks >= runtimeTicks)
{
_logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name);
break;
}
var path = _pathManager.GetChapterImagePath(video, chapter.StartPositionTicks);
if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase))
{
if (extractImages)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
// Add some time for the first chapter to make sure we don't end up with a black image
var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks);
var inputPath = video.Path;
var directoryPath = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
var container = video.Container;
var mediaSource = new MediaSourceInfo
{
VideoType = video.VideoType,
IsoType = video.IsoType,
Protocol = video.PathProtocol ?? MediaProtocol.File,
};
_logger.LogInformation("Extracting chapter image for {Name} at {Path}", video.Name, inputPath);
var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false);
File.Copy(tempFile, path, true);
try
{
_fileSystem.DeleteFile(tempFile);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile);
}
chapter.ImagePath = path;
chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
changesMade = true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path));
success = false;
break;
}
}
else if (!string.IsNullOrEmpty(chapter.ImagePath))
{
chapter.ImagePath = null;
changesMade = true;
}
}
else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase))
{
chapter.ImagePath = path;
chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
changesMade = true;
}
else if (libraryOptions?.EnableChapterImageExtraction != true)
{
// We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image
chapter.ImagePath = null;
changesMade = true;
}
}
if (saveChapters && changesMade)
{
SaveChapters(video, chapters);
}
DeleteDeadImages(currentImages, chapters);
return success;
}
/// <inheritdoc />
public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
{
// Remove any chapters that are outside of the runtime of the video
var validChapters = chapters.Where(c => c.StartPositionTicks < video.RunTimeTicks).ToList();
_chapterRepository.SaveChapters(video.Id, validChapters);
}
/// <inheritdoc />
public ChapterInfo? GetChapter(Guid baseItemId, int index)
{
return _chapterRepository.GetChapter(baseItemId, index);
}
/// <inheritdoc />
public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId)
{
return _chapterRepository.GetChapters(baseItemId);
}
/// <inheritdoc />
public async Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken)
{
await _chapterRepository.DeleteChaptersAsync(itemId, cancellationToken).ConfigureAwait(false);
}
private IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
{
var path = _pathManager.GetChapterImageFolderPath(video);
if (!Directory.Exists(path))
{
return [];
}
try
{
return directoryService.GetFilePaths(path);
}
catch (IOException)
{
return [];
}
}
private void DeleteDeadImages(IEnumerable<string> images, IEnumerable<ChapterInfo> chapters)
{
var existingImages = chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i));
var deadImages = images
.Except(existingImages, StringComparer.OrdinalIgnoreCase)
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var image in deadImages)
{
_logger.LogDebug("Deleting dead chapter image {Path}", image);
try
{
_fileSystem.DeleteFile(image!);
}
catch (IOException ex)
{
_logger.LogError(ex, "Error deleting {Path}.", image);
}
}
}
}

View File

@@ -4,7 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Entities;
@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
var libraryOptions = new LibraryOptions
{
PathInfos = [new MediaPathInfo(path)],
PathInfos = new[] { new MediaPathInfo(path) },
EnableRealtimeMonitor = false,
SaveLocalMetadata = true
};
@@ -104,8 +104,6 @@ namespace Emby.Server.Implementations.Collections
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.boxsets, libraryOptions, true).ConfigureAwait(false);
_libraryManager.RootFolder.Children = null;
return FindFolders(path).First();
}
@@ -152,15 +150,15 @@ namespace Emby.Server.Implementations.Collections
try
{
var info = Directory.CreateDirectory(path);
Directory.CreateDirectory(path);
var collection = new BoxSet
{
Name = name,
Path = path,
IsLocked = options.IsLocked,
ProviderIds = options.ProviderIds,
DateCreated = info.CreationTimeUtc,
DateModified = info.LastWriteTimeUtc
DateCreated = DateTime.UtcNow
};
parentFolder.AddChild(collection);
@@ -206,7 +204,7 @@ namespace Emby.Server.Implementations.Collections
{
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
{
throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId);
throw new ArgumentException("No collection exists with the supplied Id");
}
List<BaseItem>? itemList = null;
@@ -220,7 +218,7 @@ namespace Emby.Server.Implementations.Collections
if (item is null)
{
throw new ArgumentException("No item exists with the supplied Id " + id);
throw new ArgumentException("No item exists with the supplied Id");
}
if (!currentLinkedChildrenIds.Contains(id))

View File

@@ -17,11 +17,9 @@ namespace Emby.Server.Implementations
{ DefaultRedirectKey, "web/" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" },
{ FfmpegSkipValidationKey, bool.FalseString },
{ FfmpegImgExtractPerfTradeoffKey, bool.FalseString },
{ DetectNetworkChangeKey, bool.TrueString }
{ SqliteCacheSizeKey, "20000" }
};
}
}

View File

@@ -0,0 +1,213 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using Jellyfin.Extensions;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data
{
public abstract class BaseSqliteRepository : IDisposable
{
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="BaseSqliteRepository"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
protected BaseSqliteRepository(ILogger<BaseSqliteRepository> logger)
{
Logger = logger;
}
/// <summary>
/// Gets or sets the path to the DB file.
/// </summary>
protected string DbFilePath { get; set; }
/// <summary>
/// Gets or sets the number of write connections to create.
/// </summary>
/// <value>Path to the DB file.</value>
protected int WriteConnectionsCount { get; set; } = 1;
/// <summary>
/// Gets or sets the number of read connections to create.
/// </summary>
protected int ReadConnectionsCount { get; set; } = 1;
/// <summary>
/// Gets the logger.
/// </summary>
/// <value>The logger.</value>
protected ILogger<BaseSqliteRepository> Logger { get; }
/// <summary>
/// Gets the cache size.
/// </summary>
/// <value>The cache size or null.</value>
protected virtual int? CacheSize => null;
/// <summary>
/// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
/// </summary>
protected virtual string LockingMode => "NORMAL";
/// <summary>
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
/// </summary>
/// <value>The journal mode.</value>
protected virtual string JournalMode => "WAL";
/// <summary>
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
/// The default (-1) is overriden to prevent unconstrained WAL size, as reported by users.
/// </summary>
/// <value>The journal size limit.</value>
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
/// <summary>
/// Gets the page size.
/// </summary>
/// <value>The page size or null.</value>
protected virtual int? PageSize => null;
/// <summary>
/// Gets the temp store mode.
/// </summary>
/// <value>The temp store mode.</value>
/// <see cref="TempStoreMode"/>
protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
/// <summary>
/// Gets the synchronous mode.
/// </summary>
/// <value>The synchronous mode or null.</value>
/// <see cref="SynchronousMode"/>
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
public virtual void Initialize()
{
// Configuration and pragmas can affect VACUUM so it needs to be last.
using (var connection = GetConnection())
{
connection.Execute("VACUUM");
}
}
protected SqliteConnection GetConnection()
{
var connection = new SqliteConnection($"Filename={DbFilePath}");
connection.Open();
if (CacheSize.HasValue)
{
connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(LockingMode))
{
connection.Execute("PRAGMA locking_mode=" + LockingMode);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
connection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (JournalSizeLimit.HasValue)
{
connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
}
if (Synchronous.HasValue)
{
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
connection.Execute("PRAGMA page_size=" + PageSize.Value);
}
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
return connection;
}
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
{
var command = connection.CreateCommand();
command.CommandText = sql;
return command;
}
protected bool TableExists(SqliteConnection connection, string name)
{
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
foreach (var row in statement.ExecuteQuery())
{
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
protected List<string> GetColumnNames(SqliteConnection connection, string table)
{
var columnNames = new List<string>();
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
{
if (row.TryGetString(1, out var columnName))
{
columnNames.Add(columnName);
}
}
return columnNames;
}
protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
{
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
{
return;
}
connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL");
}
protected void CheckDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
if (_disposed)
{
return;
}
_disposed = true;
}
}
}

View File

@@ -1,119 +1,66 @@
#pragma warning disable CS1591
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data;
public class CleanDatabaseScheduledTask : ILibraryPostScanTask
namespace Emby.Server.Implementations.Data
{
private readonly ILibraryManager _libraryManager;
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IPathManager _pathManager;
public CleanDatabaseScheduledTask(
ILibraryManager libraryManager,
ILogger<CleanDatabaseScheduledTask> logger,
IDbContextFactory<JellyfinDbContext> dbProvider,
IPathManager pathManager)
public class CleanDatabaseScheduledTask : ILibraryPostScanTask
{
_libraryManager = libraryManager;
_logger = logger;
_dbProvider = dbProvider;
_pathManager = pathManager;
}
private readonly ILibraryManager _libraryManager;
private readonly ILogger<CleanDatabaseScheduledTask> _logger;
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
}
private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
{
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
public CleanDatabaseScheduledTask(ILibraryManager libraryManager, ILogger<CleanDatabaseScheduledTask> logger)
{
HasDeadParentId = true
});
_libraryManager = libraryManager;
_logger = logger;
}
var numComplete = 0;
var numItems = itemIds.Count + 1;
_logger.LogDebug("Cleaning {Number} items with dead parents", numItems);
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
foreach (var itemId in itemIds)
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
CleanDeadItems(cancellationToken, progress);
return Task.CompletedTask;
}
var item = _libraryManager.GetItemById(itemId);
if (item is not null)
private void CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
{
var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
{
_logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
HasDeadParentId = true
});
foreach (var mediaSource in item.GetMediaSources(false))
var numComplete = 0;
var numItems = itemIds.Count;
_logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
foreach (var itemId in itemIds)
{
cancellationToken.ThrowIfCancellationRequested();
var item = _libraryManager.GetItemById(itemId);
if (item is not null)
{
// Delete extracted data
var mediaSourceItem = _libraryManager.GetItemById(mediaSource.Id);
if (mediaSourceItem is null)
{
continue;
}
_logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
var extractedDataFolders = _pathManager.GetExtractedDataPaths(mediaSourceItem);
foreach (var folder in extractedDataFolders)
_libraryManager.DeleteItem(item, new DeleteOptions
{
if (Directory.Exists(folder))
{
try
{
Directory.Delete(folder, true);
}
catch (Exception e)
{
_logger.LogWarning("Failed to remove {Folder}: {Exception}", folder, e.Message);
}
}
}
DeleteFileLocation = false
});
}
// Delete item
_libraryManager.DeleteItem(item, new DeleteOptions
{
DeleteFileLocation = false
});
numComplete++;
double percent = numComplete;
percent /= numItems;
progress.Report(percent * 100);
}
numComplete++;
double percent = numComplete;
percent /= numItems;
subProgress.Report(percent * 100);
progress.Report(100);
}
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
await using (context.ConfigureAwait(false))
{
var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
await using (transaction.ConfigureAwait(false))
{
await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
subProgress.Report(50);
await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
subProgress.Report(100);
}
}
progress.Report(100);
}
}

View File

@@ -1,64 +0,0 @@
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Threading.Channels;
using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
namespace Emby.Server.Implementations.Data;
/// <inheritdoc />
public class ItemTypeLookup : IItemTypeLookup
{
/// <inheritdoc />
public IReadOnlyList<string> MusicGenreTypes { get; } = [
typeof(Audio).FullName!,
typeof(MusicVideo).FullName!,
typeof(MusicAlbum).FullName!,
typeof(MusicArtist).FullName!,
];
/// <inheritdoc />
public IReadOnlyDictionary<BaseItemKind, string> BaseItemKindNames { get; } = new Dictionary<BaseItemKind, string>()
{
{ BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName! },
{ BaseItemKind.Audio, typeof(Audio).FullName! },
{ BaseItemKind.AudioBook, typeof(AudioBook).FullName! },
{ BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName! },
{ BaseItemKind.Book, typeof(Book).FullName! },
{ BaseItemKind.BoxSet, typeof(BoxSet).FullName! },
{ BaseItemKind.Channel, typeof(Channel).FullName! },
{ BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName! },
{ BaseItemKind.Episode, typeof(Episode).FullName! },
{ BaseItemKind.Folder, typeof(Folder).FullName! },
{ BaseItemKind.Genre, typeof(Genre).FullName! },
{ BaseItemKind.Movie, typeof(Movie).FullName! },
{ BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName! },
{ BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName! },
{ BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName! },
{ BaseItemKind.MusicArtist, typeof(MusicArtist).FullName! },
{ BaseItemKind.MusicGenre, typeof(MusicGenre).FullName! },
{ BaseItemKind.MusicVideo, typeof(MusicVideo).FullName! },
{ BaseItemKind.Person, typeof(Person).FullName! },
{ BaseItemKind.Photo, typeof(Photo).FullName! },
{ BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName! },
{ BaseItemKind.Playlist, typeof(Playlist).FullName! },
{ BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName! },
{ BaseItemKind.Season, typeof(Season).FullName! },
{ BaseItemKind.Series, typeof(Series).FullName! },
{ BaseItemKind.Studio, typeof(Studio).FullName! },
{ BaseItemKind.Trailer, typeof(Trailer).FullName! },
{ BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName! },
{ BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName! },
{ BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName! },
{ BaseItemKind.UserView, typeof(UserView).FullName! },
{ BaseItemKind.Video, typeof(Video).FullName! },
{ BaseItemKind.Year, typeof(Year).FullName! }
}.ToFrozenDictionary();
}

View File

@@ -127,16 +127,8 @@ namespace Emby.Server.Implementations.Data
return false;
}
try
{
result = reader.GetGuid(index);
return true;
}
catch
{
result = Guid.Empty;
return false;
}
result = reader.GetGuid(index);
return true;
}
public static bool TryGetString(this SqliteDataReader reader, int index, out string result)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,369 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Data
{
public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
{
private readonly IUserManager _userManager;
public SqliteUserDataRepository(
ILogger<SqliteUserDataRepository> logger,
IServerConfigurationManager config,
IUserManager userManager)
: base(logger)
{
_userManager = userManager;
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
}
/// <summary>
/// Opens the connection to the database.
/// </summary>
public override void Initialize()
{
base.Initialize();
using (var connection = GetConnection())
{
var userDatasTableExists = TableExists(connection, "UserDatas");
var userDataTableExists = TableExists(connection, "userdata");
var users = userDatasTableExists ? null : _userManager.Users;
using var transaction = connection.BeginTransaction();
connection.Execute(string.Join(
';',
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
"drop index if exists idx_userdata",
"drop index if exists idx_userdata1",
"drop index if exists idx_userdata2",
"drop index if exists userdataindex1",
"drop index if exists userdataindex",
"drop index if exists userdataindex3",
"drop index if exists userdataindex4",
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)",
"create index if not exists UserDatasIndex5 on UserDatas (key, userId, lastPlayedDate)"));
if (!userDataTableExists)
{
transaction.Commit();
return;
}
var existingColumnNames = GetColumnNames(connection, "userdata");
AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
if (userDatasTableExists)
{
return;
}
ImportUserIds(connection, users);
connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
transaction.Commit();
}
}
private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
{
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
using (var statement = db.PrepareStatement("update userdata set InternalUserId=@InternalUserId where UserId=@UserId"))
{
foreach (var user in users)
{
if (!userIdsWithUserData.Contains(user.Id))
{
continue;
}
statement.TryBind("@UserId", user.Id);
statement.TryBind("@InternalUserId", user.InternalId);
statement.ExecuteNonQuery();
}
}
}
private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
{
var list = new List<Guid>();
using (var statement = PrepareStatement(db, "select DISTINCT UserId from UserData where UserId not null"))
{
foreach (var row in statement.ExecuteQuery())
{
try
{
list.Add(row.GetGuid(0));
}
catch (Exception ex)
{
Logger.LogError(ex, "Error while getting user");
}
}
}
return list;
}
/// <inheritdoc />
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userData);
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
ArgumentException.ThrowIfNullOrEmpty(key);
PersistUserData(userId, key, userData, cancellationToken);
}
/// <inheritdoc />
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userData);
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
PersistAllUserData(userId, userData, cancellationToken);
}
/// <summary>
/// Persists the user data.
/// </summary>
/// <param name="internalUserId">The user id.</param>
/// <param name="key">The key.</param>
/// <param name="userData">The user data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{
SaveUserData(connection, internalUserId, key, userData);
transaction.Commit();
}
}
private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
{
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
{
statement.TryBind("@userId", internalUserId);
statement.TryBind("@key", key);
if (userData.Rating.HasValue)
{
statement.TryBind("@rating", userData.Rating.Value);
}
else
{
statement.TryBindNull("@rating");
}
statement.TryBind("@played", userData.Played);
statement.TryBind("@playCount", userData.PlayCount);
statement.TryBind("@isFavorite", userData.IsFavorite);
statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks);
if (userData.LastPlayedDate.HasValue)
{
statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue());
}
else
{
statement.TryBindNull("@lastPlayedDate");
}
if (userData.AudioStreamIndex.HasValue)
{
statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value);
}
else
{
statement.TryBindNull("@AudioStreamIndex");
}
if (userData.SubtitleStreamIndex.HasValue)
{
statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value);
}
else
{
statement.TryBindNull("@SubtitleStreamIndex");
}
statement.ExecuteNonQuery();
}
}
/// <summary>
/// Persist all user data for the specified user.
/// </summary>
private void PersistAllUserData(long internalUserId, UserItemData[] userDataList, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
using (var transaction = connection.BeginTransaction())
{
foreach (var userItemData in userDataList)
{
SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
}
transaction.Commit();
}
}
/// <summary>
/// Gets the user data.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="key">The key.</param>
/// <returns>Task{UserItemData}.</returns>
/// <exception cref="ArgumentNullException">
/// userId
/// or
/// key.
/// </exception>
public UserItemData GetUserData(long userId, string key)
{
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
ArgumentException.ThrowIfNullOrEmpty(key);
using (var connection = GetConnection())
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
statement.TryBind("@UserId", userId);
statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery())
{
return ReadRow(row);
}
}
return null;
}
}
public UserItemData GetUserData(long userId, List<string> keys)
{
ArgumentNullException.ThrowIfNull(keys);
if (keys.Count == 0)
{
return null;
}
return GetUserData(userId, keys[0]);
}
/// <summary>
/// Return all user-data associated with the given user.
/// </summary>
/// <param name="userId">The internal user id.</param>
/// <returns>The list of user item data.</returns>
public List<UserItemData> GetAllUserData(long userId)
{
if (userId <= 0)
{
throw new ArgumentNullException(nameof(userId));
}
var list = new List<UserItemData>();
using (var connection = GetConnection())
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
{
statement.TryBind("@UserId", userId);
foreach (var row in statement.ExecuteQuery())
{
list.Add(ReadRow(row));
}
}
}
return list;
}
/// <summary>
/// Read a row from the specified reader into the provided userData object.
/// </summary>
/// <param name="reader">The list of result set values.</param>
/// <returns>The user item data.</returns>
private UserItemData ReadRow(SqliteDataReader reader)
{
var userData = new UserItemData();
userData.Key = reader[0].ToString();
// userData.UserId = reader[1].ReadGuidFromBlob();
if (reader.TryGetDouble(2, out var rating))
{
userData.Rating = rating;
}
userData.Played = reader.GetBoolean(3);
userData.PlayCount = reader.GetInt32(4);
userData.IsFavorite = reader.GetBoolean(5);
userData.PlaybackPositionTicks = reader.GetInt64(6);
if (reader.TryReadDateTime(7, out var lastPlayedDate))
{
userData.LastPlayedDate = lastPlayedDate;
}
if (reader.TryGetInt32(8, out var audioStreamIndex))
{
userData.AudioStreamIndex = audioStreamIndex;
}
if (reader.TryGetInt32(9, out var subtitleStreamIndex))
{
userData.SubtitleStreamIndex = subtitleStreamIndex;
}
return userData;
}
}
}

View File

@@ -0,0 +1,30 @@
namespace Emby.Server.Implementations.Data;
/// <summary>
/// The disk synchronization mode, controls how aggressively SQLite will write data
/// all the way out to physical storage.
/// </summary>
public enum SynchronousMode
{
/// <summary>
/// SQLite continues without syncing as soon as it has handed data off to the operating system.
/// </summary>
Off = 0,
/// <summary>
/// SQLite database engine will still sync at the most critical moments.
/// </summary>
Normal = 1,
/// <summary>
/// SQLite database engine will use the xSync method of the VFS
/// to ensure that all content is safely written to the disk surface prior to continuing.
/// </summary>
Full = 2,
/// <summary>
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
/// </summary>
Extra = 3
}

View File

@@ -0,0 +1,23 @@
namespace Emby.Server.Implementations.Data;
/// <summary>
/// Storage mode used by temporary database files.
/// </summary>
public enum TempStoreMode
{
/// <summary>
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
/// is used to determine where temporary tables and indices are stored.
/// </summary>
Default = 0,
/// <summary>
/// Temporary tables and indices are stored in a file.
/// </summary>
File = 1,
/// <summary>
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
/// </summary>
Memory = 2
}

View File

@@ -4,7 +4,6 @@ using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using MediaBrowser.Common.Configuration;
using Microsoft.Extensions.Logging;
@@ -14,7 +13,7 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IApplicationPaths _appPaths;
private readonly ILogger<DeviceId> _logger;
private readonly Lock _syncLock = new();
private readonly object _syncLock = new object();
private string? _id;

View File

@@ -1,23 +1,24 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Trickplay;
@@ -38,80 +39,10 @@ namespace Emby.Server.Implementations.Dto
{
public class DtoService : IDtoService
{
private static readonly FrozenDictionary<BaseItemKind, BaseItemKind[]> _relatedItemKinds = new Dictionary<BaseItemKind, BaseItemKind[]>
{
{
BaseItemKind.Genre, [
BaseItemKind.Audio,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist,
BaseItemKind.MusicVideo,
BaseItemKind.Series,
BaseItemKind.Trailer
]
},
{
BaseItemKind.MusicArtist, [
BaseItemKind.Audio,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicVideo
]
},
{
BaseItemKind.MusicGenre, [
BaseItemKind.Audio,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist,
BaseItemKind.MusicVideo
]
},
{
BaseItemKind.Person, [
BaseItemKind.Audio,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist,
BaseItemKind.MusicVideo,
BaseItemKind.Series,
BaseItemKind.Trailer
]
},
{
BaseItemKind.Studio, [
BaseItemKind.Audio,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist,
BaseItemKind.MusicVideo,
BaseItemKind.Series,
BaseItemKind.Trailer
]
},
{
BaseItemKind.Year, [
BaseItemKind.Audio,
BaseItemKind.Episode,
BaseItemKind.Movie,
BaseItemKind.LiveTvProgram,
BaseItemKind.MusicAlbum,
BaseItemKind.MusicArtist,
BaseItemKind.MusicVideo,
BaseItemKind.Series,
BaseItemKind.Trailer
]
}
}.ToFrozenDictionary();
private readonly ILogger<DtoService> _logger;
private readonly ILibraryManager _libraryManager;
private readonly IUserDataManager _userDataRepository;
private readonly IItemRepository _itemRepo;
private readonly IImageProcessor _imageProcessor;
private readonly IProviderManager _providerManager;
@@ -122,24 +53,24 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ITrickplayManager _trickplayManager;
private readonly IChapterManager _chapterManager;
public DtoService(
ILogger<DtoService> logger,
ILibraryManager libraryManager,
IUserDataManager userDataRepository,
IItemRepository itemRepo,
IImageProcessor imageProcessor,
IProviderManager providerManager,
IRecordingsManager recordingsManager,
IApplicationHost appHost,
IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory,
ITrickplayManager trickplayManager,
IChapterManager chapterManager)
ITrickplayManager trickplayManager)
{
_logger = logger;
_libraryManager = libraryManager;
_userDataRepository = userDataRepository;
_itemRepo = itemRepo;
_imageProcessor = imageProcessor;
_providerManager = providerManager;
_recordingsManager = recordingsManager;
@@ -147,18 +78,17 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory;
_trickplayManager = trickplayManager;
_chapterManager = chapterManager;
}
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
/// <inheritdoc />
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null)
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
{
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
var returnItems = new BaseItemDto[accessibleItems.Count];
List<(BaseItem, BaseItemDto)>? programTuples = null;
List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
List<(BaseItem, BaseItemDto)> programTuples = null;
List<(BaseItemDto, LiveTvChannel)> channelTuples = null;
for (int index = 0; index < accessibleItems.Count; index++)
{
@@ -167,16 +97,28 @@ namespace Emby.Server.Implementations.Dto
if (item is LiveTvChannel tvChannel)
{
(channelTuples ??= []).Add((dto, tvChannel));
(channelTuples ??= new()).Add((dto, tvChannel));
}
else if (item is LiveTvProgram)
{
(programTuples ??= []).Add((item, dto));
(programTuples ??= new()).Add((item, dto));
}
if (options.ContainsField(ItemFields.ItemCounts))
if (item is IItemByName byName)
{
SetItemByNameInfo(dto, user);
if (options.ContainsField(ItemFields.ItemCounts))
{
var libraryItems = byName.GetTaggedItems(new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = new DtoOptions(false)
{
EnableImages = false
}
});
SetItemByNameInfo(item, dto, libraryItems);
}
}
returnItems[index] = dto;
@@ -195,7 +137,7 @@ namespace Emby.Server.Implementations.Dto
return returnItems;
}
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null)
{
var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel)
@@ -207,15 +149,35 @@ namespace Emby.Server.Implementations.Dto
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
}
if (options.ContainsField(ItemFields.ItemCounts))
if (item is IItemByName itemByName
&& options.ContainsField(ItemFields.ItemCounts))
{
SetItemByNameInfo(dto, user);
SetItemByNameInfo(
item,
dto,
GetTaggedItems(
itemByName,
user,
new DtoOptions(false)
{
EnableImages = false
}));
}
return dto;
}
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
private static IList<BaseItem> GetTaggedItems(IItemByName byName, User user, DtoOptions options)
{
return byName.GetTaggedItems(
new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = options
});
}
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null)
{
var dto = new BaseItemDto
{
@@ -330,7 +292,7 @@ namespace Emby.Server.Implementations.Dto
}
var path = mediaSource.Path;
string? fileExtensionContainer = null;
string fileExtensionContainer = null;
if (!string.IsNullOrEmpty(path))
{
@@ -354,16 +316,11 @@ namespace Emby.Server.Implementations.Dto
}
}
/// <inheritdoc />
/// TODO refactor this to use the new SetItemByNameInfo.
/// Some callers already have the counts extracted so no reason to retrieve them again.
public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null)
public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null)
{
var dto = GetBaseItemDtoInternal(item, options, user);
if (options.ContainsField(ItemFields.ItemCounts)
&& taggedItems is not null
&& taggedItems.Count != 0)
if (taggedItems is not null && options.ContainsField(ItemFields.ItemCounts))
{
SetItemByNameInfo(item, dto, taggedItems);
}
@@ -371,58 +328,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
private void SetItemByNameInfo(BaseItemDto dto, User? user)
{
if (!_relatedItemKinds.TryGetValue(dto.Type, out var relatedItemKinds))
{
return;
}
var query = new InternalItemsQuery(user)
{
Recursive = true,
DtoOptions = new DtoOptions(false) { EnableImages = false },
IncludeItemTypes = relatedItemKinds
};
switch (dto.Type)
{
case BaseItemKind.Genre:
case BaseItemKind.MusicGenre:
query.GenreIds = [dto.Id];
break;
case BaseItemKind.MusicArtist:
query.ArtistIds = [dto.Id];
break;
case BaseItemKind.Person:
query.PersonIds = [dto.Id];
break;
case BaseItemKind.Studio:
query.StudioIds = [dto.Id];
break;
case BaseItemKind.Year
when int.TryParse(dto.Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year):
query.Years = [year];
break;
default:
return;
}
var counts = _libraryManager.GetItemCounts(query);
dto.AlbumCount = counts.AlbumCount;
dto.ArtistCount = counts.ArtistCount;
dto.EpisodeCount = counts.EpisodeCount;
dto.MovieCount = counts.MovieCount;
dto.MusicVideoCount = counts.MusicVideoCount;
dto.ProgramCount = counts.ProgramCount;
dto.SeriesCount = counts.SeriesCount;
dto.SongCount = counts.SongCount;
dto.TrailerCount = counts.TrailerCount;
dto.ChildCount = counts.TotalItemCount();
}
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IReadOnlyList<BaseItem> taggedItems)
private static void SetItemByNameInfo(BaseItem item, BaseItemDto dto, IList<BaseItem> taggedItems)
{
if (item is MusicArtist)
{
@@ -580,10 +486,10 @@ namespace Emby.Server.Implementations.Dto
return images
.Select(p => GetImageCacheTag(item, p))
.Where(i => i is not null)
.ToArray()!; // null values got filtered out
.ToArray();
}
private string? GetImageCacheTag(BaseItem item, ItemImageInfo image)
private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
{
try
{
@@ -602,7 +508,7 @@ namespace Emby.Server.Implementations.Dto
/// <param name="dto">The dto.</param>
/// <param name="item">The item.</param>
/// <param name="user">The requesting user.</param>
private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null)
private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null)
{
// Ordering by person type to ensure actors and artists are at the front.
// This is taking advantage of the fact that they both begin with A
@@ -646,7 +552,7 @@ namespace Emby.Server.Implementations.Dto
var list = new List<BaseItemPerson>();
Dictionary<string, Person> dictionary = people.Select(p => p.Name)
var dictionary = people.Select(p => p.Name)
.Distinct(StringComparer.OrdinalIgnoreCase).Select(c =>
{
try
@@ -659,9 +565,9 @@ namespace Emby.Server.Implementations.Dto
return null;
}
}).Where(i => i is not null)
.Where(i => user is null || i!.IsVisible(user))
.DistinctBy(x => x!.Name, StringComparer.OrdinalIgnoreCase)
.ToDictionary(i => i!.Name, StringComparer.OrdinalIgnoreCase)!; // null values got filtered out
.Where(i => user is null || i.IsVisible(user))
.DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < people.Count; i++)
{
@@ -674,19 +580,19 @@ namespace Emby.Server.Implementations.Dto
Type = person.Type
};
if (dictionary.TryGetValue(person.Name, out Person? entity))
if (dictionary.TryGetValue(person.Name, out Person entity))
{
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
baseItemPerson.Id = entity.Id;
if (dto.ImageBlurHashes is not null)
{
// Only add BlurHash for the person's image.
baseItemPerson.ImageBlurHashes = [];
baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
{
if (blurHash is not null)
{
baseItemPerson.ImageBlurHashes[imageType] = [];
baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
foreach (var (imageId, blurHashValue) in blurHash)
{
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
@@ -744,7 +650,7 @@ namespace Emby.Server.Implementations.Dto
return _libraryManager.GetGenreId(name);
}
private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
{
var image = item.GetImageInfo(imageType, imageIndex);
if (image is not null)
@@ -755,21 +661,16 @@ namespace Emby.Server.Implementations.Dto
return null;
}
private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
{
var tag = GetImageCacheTag(item, image);
if (tag is null)
{
return null;
}
if (!string.IsNullOrEmpty(image.BlurHash))
{
dto.ImageBlurHashes ??= [];
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
{
value = [];
value = new Dictionary<string, string>();
dto.ImageBlurHashes[image.Type] = value;
}
@@ -800,7 +701,7 @@ namespace Emby.Server.Implementations.Dto
if (hashes.Count > 0)
{
dto.ImageBlurHashes ??= [];
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
dto.ImageBlurHashes[imageType] = hashes;
}
@@ -815,7 +716,7 @@ namespace Emby.Server.Implementations.Dto
/// <param name="item">The item.</param>
/// <param name="owner">The owner.</param>
/// <param name="options">The options.</param>
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options)
private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem owner, DtoOptions options)
{
if (options.ContainsField(ItemFields.DateCreated))
{
@@ -847,7 +748,7 @@ namespace Emby.Server.Implementations.Dto
dto.AspectRatio = hasAspectRatio.AspectRatio;
}
dto.ImageBlurHashes = [];
dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
if (backdropLimit > 0)
@@ -863,7 +764,7 @@ namespace Emby.Server.Implementations.Dto
if (options.EnableImages)
{
dto.ImageTags = [];
dto.ImageTags = new Dictionary<ImageType, string>();
// Prevent implicitly captured closure
var currentItem = item;
@@ -1051,15 +952,30 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))])
.Where(e => e.Value.Length > 0)
dto.ArtistItems = hasArtist.Artists
// .Except(foundArtists, new DistinctNameComparer())
.Select(i =>
{
return new NameGuidPair
// This should not be necessary but we're seeing some cases of it
if (string.IsNullOrEmpty(i))
{
Name = i.Key,
Id = i.Value.First().Id
};
return null;
}
var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
{
EnableImages = false
});
if (artist is not null)
{
return new NameGuidPair
{
Name = artist.Name,
Id = artist.Id
};
}
return null;
}).Where(i => i is not null).ToArray();
}
@@ -1140,17 +1056,12 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.Chapters))
{
dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
dto.Chapters = _itemRepo.GetChapters(item);
}
if (options.ContainsField(ItemFields.Trickplay))
{
var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
dto.Trickplay = trickplay.ToDictionary(
mediaStream => mediaStream.Key,
mediaStream => mediaStream.Value.ToDictionary(
width => width.Key,
width => new TrickplayInfoDto(width.Value)));
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
}
dto.ExtraType = video.ExtraType;
@@ -1186,7 +1097,7 @@ namespace Emby.Server.Implementations.Dto
}
}
BaseItem[]? allExtras = null;
BaseItem[] allExtras = null;
if (options.ContainsField(ItemFields.SpecialFeatureCount))
{
@@ -1223,7 +1134,7 @@ namespace Emby.Server.Implementations.Dto
dto.SeasonId = episode.SeasonId;
dto.SeriesId = episode.SeriesId;
Series? episodeSeries = null;
Series episodeSeries = null;
// this block will add the series poster for episodes without a poster
// TODO maybe remove the if statement entirely
@@ -1251,10 +1162,8 @@ namespace Emby.Server.Implementations.Dto
}
// Add SeriesInfo
Series? series;
if (item is Series tmp)
if (item is Series series)
{
series = tmp;
dto.AirDays = series.AirDays;
dto.AirTime = series.AirTime;
dto.Status = series.Status?.ToString();
@@ -1355,7 +1264,7 @@ namespace Emby.Server.Implementations.Dto
}
}
private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
private BaseItem GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
{
if (currentItem is MusicAlbum musicAlbum)
{
@@ -1376,7 +1285,7 @@ namespace Emby.Server.Implementations.Dto
return parent;
}
private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner)
private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem owner)
{
if (!item.SupportsInheritedParentImages)
{
@@ -1396,7 +1305,7 @@ namespace Emby.Server.Implementations.Dto
return;
}
BaseItem? parent = null;
BaseItem parent = null;
var isFirst = true;
var imageTags = dto.ImageTags;
@@ -1469,7 +1378,7 @@ namespace Emby.Server.Implementations.Dto
}
}
private string GetMappedPath(BaseItem item, BaseItem? ownerItem)
private string GetMappedPath(BaseItem item, BaseItem ownerItem)
{
var path = item.Path;

View File

@@ -18,11 +18,9 @@
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
<ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
<ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
<ProjectReference Include="..\src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BitFaster.Caching" />
<PackageReference Include="DiscUtils.Udf" />
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
@@ -39,7 +37,7 @@
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
@@ -64,14 +62,10 @@
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ignore" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Localization\iso6392.txt" />
<EmbeddedResource Include="Localization\countries.json" />
<EmbeddedResource Include="Localization\Core\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.csv" />
</ItemGroup>
</Project>

View File

@@ -5,8 +5,8 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -34,7 +34,7 @@ public sealed class LibraryChangedNotifier : IHostedService, IDisposable
private readonly IUserManager _userManager;
private readonly ILogger<LibraryChangedNotifier> _logger;
private readonly Lock _libraryChangedSyncLock = new();
private readonly object _libraryChangedSyncLock = new();
private readonly List<Folder> _foldersAddedTo = new();
private readonly List<Folder> _foldersRemovedFrom = new();
private readonly List<BaseItem> _itemsAdded = new();

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -24,7 +25,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IUserManager _userManager;
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
private readonly Lock _syncLock = new();
private readonly object _syncLock = new();
private Timer? _updateTimer;
@@ -133,26 +134,19 @@ namespace Emby.Server.Implementations.EntryPoints
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)
{
var user = _userManager.GetUserById(userId)
?? throw new ArgumentException("Invalid user ID", nameof(userId));
var user = _userManager.GetUserById(userId);
return new UserDataChangeInfo
{
UserId = userId,
UserId = userId.ToString("N", CultureInfo.InvariantCulture),
UserDataList = changedItems
.DistinctBy(x => x.Id)
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
if (dto is null)
{
return null!;
}
dto.ItemId = i.Id;
dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture);
return dto;
})
.Where(e => e is not null)
.ToArray()
};
}

View File

@@ -1,8 +1,7 @@
#pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;

View File

@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer
RemoteEndPoint = remoteEndPoint;
_jsonOptions = JsonDefaults.Options;
LastActivityDate = DateTime.UtcNow;
LastActivityDate = DateTime.Now;
}
/// <inheritdoc />
@@ -82,17 +82,17 @@ namespace Emby.Server.Implementations.HttpServer
public WebSocketState State => _socket.State;
/// <inheritdoc />
public async Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
}
/// <inheritdoc />
public async Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
{
var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
await _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
}
/// <inheritdoc />
@@ -224,12 +224,12 @@ namespace Emby.Server.Implementations.HttpServer
return ret;
}
private async Task SendKeepAliveResponse()
private Task SendKeepAliveResponse()
{
LastKeepAliveDate = DateTime.UtcNow;
await SendAsync(
return SendAsync(
new OutboundKeepAliveMessage(),
CancellationToken.None).ConfigureAwait(false);
CancellationToken.None);
}
/// <inheritdoc />

View File

@@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.HttpServer
/// Processes the web socket message received.
/// </summary>
/// <param name="result">The result.</param>
private async Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
{
var tasks = new Task[_webSocketListeners.Length];
for (var i = 0; i < _webSocketListeners.Length; ++i)
@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.HttpServer
tasks[i] = _webSocketListeners[i].ProcessMessageAsync(result);
}
await Task.WhenAll(tasks).ConfigureAwait(false);
return Task.WhenAll(tasks);
}
}
}

View File

@@ -18,8 +18,8 @@ namespace Emby.Server.Implementations.IO
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _configurationManager;
private readonly List<string> _affectedPaths = new();
private readonly Lock _timerLock = new();
private readonly List<string> _affectedPaths = new List<string>();
private readonly object _timerLock = new object();
private Timer? _timer;
private bool _disposed;
@@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.IO
private void ProcessPathChanges(List<string> paths)
{
IEnumerable<BaseItem> itemsToRefresh = paths
.Distinct()
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(GetAffectedBaseItem)
.Where(item => item is not null)
.DistinctBy(x => x!.Id)!; // Removed null values in the previous .Where()

View File

@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.IO
.Where(IsLibraryMonitorEnabled)
.OfType<Folder>()
.SelectMany(f => f.PhysicalLocations)
.Distinct()
.Distinct(StringComparer.OrdinalIgnoreCase)
.Order();
foreach (var path in paths)
@@ -314,12 +314,6 @@ namespace Emby.Server.Implementations.IO
var ex = e.GetException();
var dw = (FileSystemWatcher)sender;
if (ex is UnauthorizedAccessException unauthorizedAccessException)
{
_logger.LogError(unauthorizedAccessException, "Permission error for Directory watcher: {Path}", dw.Path);
return;
}
_logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
DisposeWatcher(dw, true);

View File

@@ -6,7 +6,6 @@ using System.Linq;
using System.Security;
using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.IO;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@@ -81,14 +80,12 @@ namespace Emby.Server.Implementations.IO
public virtual string MakeAbsolutePath(string folderPath, string filePath)
{
// path is actually a stream
if (string.IsNullOrWhiteSpace(filePath))
if (string.IsNullOrWhiteSpace(filePath) || filePath.Contains("://", StringComparison.Ordinal))
{
return filePath;
}
var isAbsolutePath = Path.IsPathRooted(filePath) && (!OperatingSystem.IsWindows() || filePath[0] != '\\');
if (isAbsolutePath)
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/')
{
// absolute local path
return filePath;
@@ -100,10 +97,17 @@ namespace Emby.Server.Implementations.IO
return filePath;
}
var firstChar = filePath[0];
if (firstChar == '/')
{
// for this we don't really know
return filePath;
}
var filePathSpan = filePath.AsSpan();
// relative path on windows
if (filePath[0] == '\\')
// relative path
if (firstChar == '\\')
{
filePathSpan = filePathSpan.Slice(1);
}
@@ -150,31 +154,6 @@ namespace Emby.Server.Implementations.IO
}
}
/// <inheritdoc />
public void MoveDirectory(string source, string destination)
{
// Make sure parent directory of target exists
var parent = Directory.GetParent(destination);
parent?.Create();
try
{
Directory.Move(source, destination);
}
catch (IOException)
{
// Cross device move requires a copy
Directory.CreateDirectory(destination);
var sourceDir = new DirectoryInfo(source);
foreach (var file in sourceDir.EnumerateFiles())
{
file.CopyTo(Path.Combine(destination, file.Name), true);
}
sourceDir.Delete(true);
}
}
/// <summary>
/// Returns a <see cref="FileSystemMetadata"/> object for the specified file or directory path.
/// </summary>
@@ -253,40 +232,40 @@ namespace Emby.Server.Implementations.IO
{
result.IsDirectory = info is DirectoryInfo || (info.Attributes & FileAttributes.Directory) == FileAttributes.Directory;
// if (!result.IsDirectory)
// {
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
// }
if (info is FileInfo fileInfo)
{
result.CreationTimeUtc = GetCreationTimeUtc(info);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
if (fileInfo.LinkTarget is not null)
result.Length = fileInfo.Length;
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
{
try
{
var targetFileInfo = FileSystemHelper.ResolveLinkTarget(fileInfo, returnFinalTarget: true);
if (targetFileInfo is not null)
using (var fileHandle = File.OpenHandle(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
result.Exists = targetFileInfo.Exists;
if (result.Exists)
{
result.Length = targetFileInfo.Length;
result.CreationTimeUtc = GetCreationTimeUtc(targetFileInfo);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(targetFileInfo);
}
}
else
{
result.Exists = false;
result.Length = RandomAccess.GetLength(fileHandle);
}
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
_logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
catch (UnauthorizedAccessException ex)
{
_logger.LogError(ex, "Reading the file at {Path} failed due to a permissions exception.", fileInfo.FullName);
}
}
else
{
result.Length = fileInfo.Length;
}
}
result.CreationTimeUtc = GetCreationTimeUtc(info);
result.LastWriteTimeUtc = GetLastWriteTimeUtc(info);
}
else
{
@@ -353,7 +332,11 @@ namespace Emby.Server.Implementations.IO
}
}
/// <inheritdoc />
/// <summary>
/// Gets the creation time UTC.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>DateTime.</returns>
public virtual DateTime GetCreationTimeUtc(string path)
{
return GetCreationTimeUtc(GetFileSystemInfo(path));
@@ -390,7 +373,11 @@ namespace Emby.Server.Implementations.IO
}
}
/// <inheritdoc />
/// <summary>
/// Gets the last write time UTC.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>DateTime.</returns>
public virtual DateTime GetLastWriteTimeUtc(string path)
{
return GetLastWriteTimeUtc(GetFileSystemInfo(path));
@@ -407,7 +394,7 @@ namespace Emby.Server.Implementations.IO
var info = new FileInfo(path);
if (info.Exists &&
(info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden != isHidden)
((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
{
if (isHidden)
{
@@ -435,8 +422,8 @@ namespace Emby.Server.Implementations.IO
return;
}
if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly == readOnly
&& (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden == isHidden)
if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
{
return;
}
@@ -464,7 +451,11 @@ namespace Emby.Server.Implementations.IO
File.SetAttributes(path, attributes);
}
/// <inheritdoc />
/// <summary>
/// Swaps the files.
/// </summary>
/// <param name="file1">The file1.</param>
/// <param name="file2">The file2.</param>
public virtual void SwapFiles(string file1, string file2)
{
ArgumentException.ThrowIfNullOrEmpty(file1);
@@ -480,7 +471,7 @@ namespace Emby.Server.Implementations.IO
File.Copy(file1, temp1, true);
File.Copy(file2, file1, true);
File.Move(temp1, file2, true);
File.Copy(temp1, file2, true);
}
/// <inheritdoc />
@@ -497,17 +488,8 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual bool AreEqual(string path1, string path2)
{
if (string.IsNullOrWhiteSpace(path1) || string.IsNullOrWhiteSpace(path2))
{
return false;
}
var normalized1 = Path.TrimEndingDirectorySeparator(path1);
var normalized2 = Path.TrimEndingDirectorySeparator(path2);
return string.Equals(
normalized1,
normalized2,
return Path.TrimEndingDirectorySeparator(path1).Equals(
Path.TrimEndingDirectorySeparator(path2),
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
@@ -549,8 +531,8 @@ namespace Emby.Server.Implementations.IO
return DriveInfo.GetDrives()
.Where(
d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
&& d.IsReady
&& d.TotalSize != 0)
&& d.IsReady
&& d.TotalSize != 0)
.Select(d => new FileSystemMetadata
{
Name = d.Name,
@@ -568,36 +550,22 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
{
return GetFiles(path, "*", recursive);
return GetFiles(path, null, false, recursive);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, bool recursive = false)
{
return GetFiles(path, searchPattern, null, false, recursive);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive)
{
return GetFiles(path, "*", extensions, enableCaseSensitiveExtensions, recursive);
}
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, 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);
// On linux and macOS the search pattern is case-sensitive
// On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
{
searchPattern = searchPattern.EndsWith(extensions[0], StringComparison.Ordinal) ? searchPattern : searchPattern + extensions[0];
return ToMetadata(new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions));
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
}
var files = new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions);
var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
if (extensions is not null && extensions.Count > 0)
{
@@ -619,9 +587,6 @@ namespace Emby.Server.Implementations.IO
/// <inheritdoc />
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
// Note: any of unhandled exceptions thrown by this method may cause the caller to believe the whole path is not accessible.
// But what causing the exception may be a single file under that path. This could lead to unexpected behavior.
// For example, the scanner will remove everything in that path due to unhandled errors.
var directoryInfo = new DirectoryInfo(path);
var enumerationOptions = GetEnumerationOptions(recursive);
@@ -650,7 +615,7 @@ namespace Emby.Server.Implementations.IO
{
var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and macOS the search pattern is case-sensitive
// On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Length == 1)
{

View File

@@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
@@ -43,11 +42,13 @@ namespace Emby.Server.Implementations.Images
protected IImageProcessor ImageProcessor { get; set; }
protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
= [ImageType.Primary];
= new ImageType[] { ImageType.Primary };
/// <inheritdoc />
public string Name => "Dynamic Image Provider";
protected virtual int MaxImageAgeDays => 7;
public int Order => 0;
protected virtual bool Supports(BaseItem item) => true;
@@ -115,9 +116,9 @@ namespace Emby.Server.Implementations.Images
var mimeType = MimeTypes.GetMimeType(outputPath);
if (string.Equals(mimeType, MediaTypeNames.Application.Octet, StringComparison.OrdinalIgnoreCase))
if (string.Equals(mimeType, "application/octet-stream", StringComparison.OrdinalIgnoreCase))
{
mimeType = MediaTypeNames.Image.Png;
mimeType = "image/png";
}
await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false);
@@ -290,14 +291,8 @@ namespace Emby.Server.Implementations.Images
protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
{
var path = image.Path;
if (!string.IsNullOrEmpty(path))
{
var modificationDate = FileSystem.GetLastWriteTimeUtc(path);
return image.DateModified != modificationDate;
}
return false;
var age = DateTime.UtcNow - image.DateModified;
return age.TotalDays > MaxImageAgeDays;
}
protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@@ -12,6 +11,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{
@@ -33,12 +33,12 @@ namespace Emby.Server.Implementations.Images
Parent = item,
Recursive = true,
DtoOptions = new DtoOptions(true),
ImageTypes = [ImageType.Primary],
OrderBy =
[
ImageTypes = new ImageType[] { ImageType.Primary },
OrderBy = new (ItemSortBy, SortOrder)[]
{
(ItemSortBy.IsFolder, SortOrder.Ascending),
(ItemSortBy.SortName, SortOrder.Ascending)
],
},
Limit = 1
});
}

View File

@@ -6,7 +6,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;

View File

@@ -2,7 +2,6 @@
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;

View File

@@ -1,10 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
@@ -18,13 +15,5 @@ namespace Emby.Server.Implementations.Images
: base(fileSystem, providerManager, applicationPaths, imageProcessor, libraryManager)
{
}
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
var items = base.GetItemsWithImages(item);
// Ignore any folders because they can have generated collages
return items.Where(i => i is not Folder).ToList();
}
}
}

View File

@@ -4,7 +4,6 @@
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;

View File

@@ -37,33 +37,46 @@ namespace Emby.Server.Implementations.Library
return false;
}
// Don't ignore top level folders
if (fileInfo.IsDirectory && parent is AggregateFolder)
{
return false;
}
if (IgnorePatterns.ShouldIgnore(fileInfo.FullName))
{
return true;
}
// Don't ignore top level folders
if (fileInfo.IsDirectory
&& (parent is AggregateFolder || (parent?.IsTopParent ?? false)))
{
return false;
}
if (parent is null)
{
return false;
}
var filename = fileInfo.Name;
if (fileInfo.IsDirectory)
{
// Ignore extras for unsupported types
return _namingOptions.AllExtrasTypesFolderNames.ContainsKey(fileInfo.Name)
&& parent is not UserRootFolder;
if (parent is not null)
{
// Ignore extras folders but allow it at the collection level
if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
&& parent is not AggregateFolder
&& parent is not UserRootFolder)
{
return true;
}
}
}
else
{
if (parent is not null)
{
// Don't resolve these into audio files
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& AudioFileParser.IsAudioFile(filename, _namingOptions))
{
return true;
}
}
}
// Don't resolve theme songs
return Path.GetFileNameWithoutExtension(fileInfo.Name.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
&& AudioFileParser.IsAudioFile(fileInfo.Name, _namingOptions);
return false;
}
}
}

View File

@@ -1,96 +0,0 @@
using System;
using System.IO;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library;
/// <summary>
/// Resolver rule class for ignoring files via .ignore.
/// </summary>
public class DotIgnoreIgnoreRule : IResolverIgnoreRule
{
private static readonly bool IsWindows = OperatingSystem.IsWindows();
private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
{
for (var current = directory; current is not null; current = current.Parent)
{
var ignorePath = Path.Join(current.FullName, ".ignore");
if (File.Exists(ignorePath))
{
return new FileInfo(ignorePath);
}
}
return null;
}
/// <inheritdoc />
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent) => IsIgnored(fileInfo, parent);
/// <summary>
/// Checks whether or not the file is ignored.
/// </summary>
/// <param name="fileInfo">The file information.</param>
/// <param name="parent">The parent BaseItem.</param>
/// <returns>True if the file should be ignored.</returns>
public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
{
var searchDirectory = fileInfo.IsDirectory
? new DirectoryInfo(fileInfo.FullName)
: new DirectoryInfo(Path.GetDirectoryName(fileInfo.FullName) ?? string.Empty);
if (string.IsNullOrEmpty(searchDirectory.FullName))
{
return false;
}
var ignoreFile = FindIgnoreFile(searchDirectory);
if (ignoreFile is null)
{
return false;
}
// Fast path in case the ignore files isn't a symlink and is empty
if (ignoreFile.LinkTarget is null && ignoreFile.Length == 0)
{
// Ignore directory if we just have the file
return true;
}
var content = GetFileContent(ignoreFile);
return string.IsNullOrWhiteSpace(content)
|| CheckIgnoreRules(fileInfo.FullName, content, fileInfo.IsDirectory);
}
private static bool CheckIgnoreRules(string path, string ignoreFileContent, bool isDirectory)
{
// If file has content, base ignoring off the content .gitignore-style rules
var rules = ignoreFileContent.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var ignore = new Ignore.Ignore();
ignore.Add(rules);
// Mitigate the problem of the Ignore library not handling Windows paths correctly.
// See https://github.com/jellyfin/jellyfin/issues/15484
var pathToCheck = IsWindows ? path.NormalizePath('/') : path;
// Add trailing slash for directories to match "folder/"
if (isDirectory)
{
pathToCheck = string.Concat(pathToCheck.AsSpan().TrimEnd('/'), "/");
}
return ignore.IsIgnored(pathToCheck);
}
private static string GetFileContent(FileInfo ignoreFile)
{
ignoreFile = FileSystemHelper.ResolveLinkTarget(ignoreFile, returnFinalTarget: true) ?? ignoreFile;
return ignoreFile.Exists
? File.ReadAllText(ignoreFile.FullName)
: string.Empty;
}
}

View File

@@ -1,77 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.MediaSegments;
using MediaBrowser.Controller.Trickplay;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library;
/// <summary>
/// IExternalDataManager implementation.
/// </summary>
public class ExternalDataManager : IExternalDataManager
{
private readonly IKeyframeManager _keyframeManager;
private readonly IMediaSegmentManager _mediaSegmentManager;
private readonly IPathManager _pathManager;
private readonly ITrickplayManager _trickplayManager;
private readonly IChapterManager _chapterManager;
private readonly ILogger<ExternalDataManager> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ExternalDataManager"/> class.
/// </summary>
/// <param name="keyframeManager">The keyframe manager.</param>
/// <param name="mediaSegmentManager">The media segment manager.</param>
/// <param name="pathManager">The path manager.</param>
/// <param name="trickplayManager">The trickplay manager.</param>
/// <param name="chapterManager">The chapter manager.</param>
/// <param name="logger">The logger.</param>
public ExternalDataManager(
IKeyframeManager keyframeManager,
IMediaSegmentManager mediaSegmentManager,
IPathManager pathManager,
ITrickplayManager trickplayManager,
IChapterManager chapterManager,
ILogger<ExternalDataManager> logger)
{
_keyframeManager = keyframeManager;
_mediaSegmentManager = mediaSegmentManager;
_pathManager = pathManager;
_trickplayManager = trickplayManager;
_chapterManager = chapterManager;
_logger = logger;
}
/// <inheritdoc/>
public async Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken)
{
var validPaths = _pathManager.GetExtractedDataPaths(item).Where(Directory.Exists).ToList();
var itemId = item.Id;
if (validPaths.Count > 0)
{
foreach (var path in validPaths)
{
try
{
Directory.Delete(path, true);
}
catch (Exception ex)
{
_logger.LogWarning("Unable to prune external item data at {Path}: {Exception}", path, ex);
}
}
}
await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false);
await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false);
await _chapterManager.DeleteChapterDataAsync(itemId, cancellationToken).ConfigureAwait(false);
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using DotNet.Globbing;
namespace Emby.Server.Implementations.Library
@@ -48,12 +49,6 @@ namespace Emby.Server.Implementations.Library
"**/.wd_tv",
"**/lost+found/**",
"**/lost+found",
"**/subs/**",
"**/subs",
// Trickplay files
"**/*.trickplay",
"**/*.trickplay/**",
// WMC temp recording directories that will constantly be written to
"**/TempRec/**",

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.MediaEncoding.Keyframes;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Persistence;
namespace Emby.Server.Implementations.Library;
/// <summary>
/// Manager for Keyframe data.
/// </summary>
public class KeyframeManager : IKeyframeManager
{
private readonly IKeyframeRepository _repository;
/// <summary>
/// Initializes a new instance of the <see cref="KeyframeManager"/> class.
/// </summary>
/// <param name="repository">The keyframe repository.</param>
public KeyframeManager(IKeyframeRepository repository)
{
_repository = repository;
}
/// <inheritdoc />
public IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId)
{
return _repository.GetKeyframeData(itemId);
}
/// <inheritdoc />
public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken)
{
await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
{
await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -13,10 +12,8 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AsyncKeyedLock;
using Jellyfin.Data;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
@@ -41,7 +38,7 @@ namespace Emby.Server.Implementations.Library
public class MediaSourceManager : IMediaSourceManager, IDisposable
{
// Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message.
private const char LiveStreamIdDelimiter = '_';
private const char LiveStreamIdDelimeter = '_';
private readonly IServerApplicationHost _appHost;
private readonly IItemRepository _itemRepo;
@@ -54,8 +51,7 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
private readonly IDirectoryService _directoryService;
private readonly IMediaStreamRepository _mediaStreamRepository;
private readonly IMediaAttachmentRepository _mediaAttachmentRepository;
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -73,9 +69,7 @@ namespace Emby.Server.Implementations.Library
IFileSystem fileSystem,
IUserDataManager userDataManager,
IMediaEncoder mediaEncoder,
IDirectoryService directoryService,
IMediaStreamRepository mediaStreamRepository,
IMediaAttachmentRepository mediaAttachmentRepository)
IDirectoryService directoryService)
{
_appHost = appHost;
_itemRepo = itemRepo;
@@ -88,8 +82,6 @@ namespace Emby.Server.Implementations.Library
_localizationManager = localizationManager;
_appPaths = applicationPaths;
_directoryService = directoryService;
_mediaStreamRepository = mediaStreamRepository;
_mediaAttachmentRepository = mediaAttachmentRepository;
}
public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -97,9 +89,9 @@ namespace Emby.Server.Implementations.Library
_providers = providers.ToArray();
}
public IReadOnlyList<MediaStream> GetMediaStreams(MediaStreamQuery query)
public List<MediaStream> GetMediaStreams(MediaStreamQuery query)
{
var list = _mediaStreamRepository.GetMediaStreams(query);
var list = _itemRepo.GetMediaStreams(query);
foreach (var stream in list)
{
@@ -121,15 +113,10 @@ namespace Emby.Server.Implementations.Library
return true;
}
if (stream.IsPgsSubtitleStream)
{
return true;
}
return false;
}
public IReadOnlyList<MediaStream> GetMediaStreams(Guid itemId)
public List<MediaStream> GetMediaStreams(Guid itemId)
{
var list = GetMediaStreams(new MediaStreamQuery
{
@@ -139,7 +126,7 @@ namespace Emby.Server.Implementations.Library
return GetMediaStreamsForItem(list);
}
private IReadOnlyList<MediaStream> GetMediaStreamsForItem(IReadOnlyList<MediaStream> streams)
private List<MediaStream> GetMediaStreamsForItem(List<MediaStream> streams)
{
foreach (var stream in streams)
{
@@ -153,13 +140,13 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
public IReadOnlyList<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
{
return _mediaAttachmentRepository.GetMediaAttachments(query);
return _itemRepo.GetMediaAttachments(query);
}
/// <inheritdoc />
public IReadOnlyList<MediaAttachment> GetMediaAttachments(Guid itemId)
public List<MediaAttachment> GetMediaAttachments(Guid itemId)
{
return GetMediaAttachments(new MediaAttachmentQuery
{
@@ -167,7 +154,7 @@ namespace Emby.Server.Implementations.Library
});
}
public async Task<IReadOnlyList<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
public async Task<List<MediaSourceInfo>> GetPlaybackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
{
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
@@ -220,17 +207,12 @@ namespace Emby.Server.Implementations.Library
list.Add(source);
}
return SortMediaSources(list).ToArray();
return SortMediaSources(list);
}
/// <inheritdoc />>
public MediaProtocol GetPathProtocol(string path)
{
if (string.IsNullOrEmpty(path))
{
return MediaProtocol.File;
}
if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
{
return MediaProtocol.Rtsp;
@@ -320,7 +302,7 @@ namespace Emby.Server.Implementations.Library
private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource)
{
var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimiter;
var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimeter;
if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
@@ -345,7 +327,7 @@ namespace Emby.Server.Implementations.Library
return sources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
}
public IReadOnlyList<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
public List<MediaSourceInfo> GetStaticMediaSources(BaseItem item, bool enablePathSubstitution, User user = null)
{
ArgumentNullException.ThrowIfNull(item);
@@ -384,7 +366,7 @@ namespace Emby.Server.Implementations.Library
var culture = _localizationManager.FindLanguageInfo(language);
if (culture is not null)
{
return culture.Name.Contains('-', StringComparison.OrdinalIgnoreCase) ? [culture.Name] : culture.ThreeLetterISOLanguageNames;
return culture.ThreeLetterISOLanguageNames;
}
return [language];
@@ -392,8 +374,7 @@ namespace Emby.Server.Implementations.Library
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
if (userData is not null
&& userData.SubtitleStreamIndex.HasValue
if (userData.SubtitleStreamIndex.HasValue
&& user.RememberSubtitleSelections
&& user.SubtitleMode != SubtitlePlaybackMode.None
&& allowRememberingSelection)
@@ -425,14 +406,13 @@ namespace Emby.Server.Implementations.Library
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
if (userData is not null && userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
{
var index = userData.AudioStreamIndex.Value;
// Make sure the saved index is still valid
if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
{
source.DefaultAudioStreamIndex = index;
source.DefaultAudioIndexSource = AudioIndexSource.User;
return;
}
}
@@ -440,15 +420,6 @@ namespace Emby.Server.Implementations.Library
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
if (user.PlayDefaultAudioTrack)
{
source.DefaultAudioIndexSource |= AudioIndexSource.Default;
}
if (preferredAudio.Count > 0)
{
source.DefaultAudioIndexSource |= AudioIndexSource.Language;
}
}
public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
@@ -458,7 +429,7 @@ namespace Emby.Server.Implementations.Library
if (mediaType == MediaType.Video)
{
var userData = item is null ? null : _userDataManager.GetUserData(user, item);
var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item);
var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections;
@@ -476,7 +447,7 @@ namespace Emby.Server.Implementations.Library
}
}
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{
return sources.OrderBy(i =>
{
@@ -493,7 +464,8 @@ namespace Emby.Server.Implementations.Library
return stream?.Width ?? 0;
})
.Where(i => i.Type != MediaSourceType.Placeholder);
.Where(i => i.Type != MediaSourceType.Placeholder)
.ToList();
}
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -662,7 +634,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Error parsing cached media info.");
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
}
finally
{
@@ -686,17 +658,17 @@ namespace Emby.Server.Implementations.Library
mediaInfo = await _mediaEncoder.GetMediaInfo(
new MediaInfoRequest
{
MediaSource = mediaSource,
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
ExtractChapters = false
},
{
MediaSource = mediaSource,
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
ExtractChapters = false
},
cancellationToken).ConfigureAwait(false);
if (cacheFilePath is not null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
FileStream createStream = AsyncFile.Create(cacheFilePath);
FileStream createStream = File.Create(cacheFilePath);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
@@ -799,13 +771,9 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(id);
var info = GetLiveStreamInfo(id);
if (info is null)
{
return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(null, null));
}
return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
// TODO probably shouldn't throw here but it is kept for "backwards compatibility"
var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
}
public ILiveStream GetLiveStreamInfo(string id)
@@ -832,7 +800,7 @@ namespace Emby.Server.Implementations.Library
return result.Item1;
}
public async Task<IReadOnlyList<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken)
{
var stream = new MediaSourceInfo
{
@@ -855,7 +823,10 @@ namespace Emby.Server.Implementations.Library
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
return [stream];
return new List<MediaSourceInfo>
{
stream
};
}
public async Task CloseLiveStream(string id)
@@ -887,11 +858,11 @@ namespace Emby.Server.Implementations.Library
{
ArgumentException.ThrowIfNullOrEmpty(key);
var keys = key.Split(LiveStreamIdDelimiter, 2);
var keys = key.Split(LiveStreamIdDelimeter, 2);
var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase));
var splitIndex = key.IndexOf(LiveStreamIdDelimiter, StringComparison.Ordinal);
var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
var keyId = key.Substring(splitIndex + 1);
return (provider, keyId);

View File

@@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Model.Entities;
@@ -39,48 +39,46 @@ namespace Emby.Server.Implementations.Library
return null;
}
// Sort in the following order: Default > No tag > Forced
var sortedStreams = streams
.Where(i => i.Type == MediaStreamType.Subtitle)
.OrderByDescending(x => x.IsExternal)
.ThenByDescending(x => x.IsDefault)
.ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
.ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
.ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language))
.ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
.ThenByDescending(x => x.IsForced)
.ThenByDescending(x => x.IsDefault)
.ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase))
.ToList();
MediaStream? stream = null;
if (mode == SubtitlePlaybackMode.Default)
{
// Load subtitles according to external, default and forced flags.
stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced);
// Load subtitles according to external, forced and default flags.
stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
}
else if (mode == SubtitlePlaybackMode.Smart)
{
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
// If no subtitles of preferred language available, use none.
// If the audio language is one of the user's preferred subtitle languages behave like OnlyForced.
// If no subtitles of preferred language available, use default behaviour.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages));
stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
}
else
{
stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
// Respect forced flag.
stream = sortedStreams.FirstOrDefault(x => x.IsForced);
}
}
else if (mode == SubtitlePlaybackMode.Always)
{
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour.
stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ??
BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour.
stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
// Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
// Only load subtitles that are flagged forced.
stream = sortedStreams.FirstOrDefault(x => x.IsForced);
}
return stream?.Index;
@@ -112,72 +110,40 @@ namespace Emby.Server.Implementations.Library
if (mode == SubtitlePlaybackMode.Default)
{
// Prefer embedded metadata over smart logic
// Load subtitles according to external, default, and forced flags.
filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced)
filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault)
.ToList();
}
else if (mode == SubtitlePlaybackMode.Smart)
{
// Prefer smart logic over embedded metadata
// Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior.
if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
{
filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
.ToList();
}
else
{
filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
}
}
else if (mode == SubtitlePlaybackMode.Always)
{
// Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior.
filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages))
.ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages);
// Always load the most suitable full subtitles
filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
}
else if (mode == SubtitlePlaybackMode.OnlyForced)
{
// Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
// Always load the most suitable full subtitles
filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
}
// If filteredStreams is null, initialize it as an empty list to avoid null reference errors
filteredStreams ??= new List<MediaStream>();
// Load forced subs if we have found no suitable full subtitles
var iterStreams = filteredStreams is null || filteredStreams.Count == 0
? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
: filteredStreams;
foreach (var stream in filteredStreams)
foreach (var stream in iterStreams)
{
stream.Score = GetStreamScore(stream, preferredLanguages);
}
}
private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> preferredLanguages)
{
// If preferredLanguages is empty, treat it as "any language" (wildcard)
return preferredLanguages.Count == 0 ||
preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase);
}
private static bool IsLanguageUndefined(string language)
{
// Check for null, empty, or known placeholders
return string.IsNullOrEmpty(language) ||
language.Equals("und", StringComparison.OrdinalIgnoreCase) ||
language.Equals("unknown", StringComparison.OrdinalIgnoreCase) ||
language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) ||
language.Equals("mul", StringComparison.OrdinalIgnoreCase) ||
language.Equals("zxx", StringComparison.OrdinalIgnoreCase);
}
private static List<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> preferredLanguages)
{
return sortedStreams
.Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language)))
.OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
.ThenByDescending(s => IsLanguageUndefined(s.Language))
.ToList();
}
internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
{
var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));

View File

@@ -2,11 +2,9 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -26,35 +24,37 @@ namespace Emby.Server.Implementations.Library
_libraryManager = libraryManager;
}
public IReadOnlyList<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
public List<BaseItem> GetInstantMixFromSong(Audio item, User? user, DtoOptions dtoOptions)
{
var instantMixItems = GetInstantMixFromGenres(item.Genres, user, dtoOptions);
var list = new List<BaseItem>
{
item
};
return [item, .. instantMixItems.Where(i => !i.Id.Equals(item.Id))];
list.AddRange(GetInstantMixFromGenres(item.Genres, user, dtoOptions));
return list;
}
/// <inheritdoc />
public IReadOnlyList<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
}
public IReadOnlyList<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
public IReadOnlyList<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
public List<BaseItem> GetInstantMixFromFolder(Folder item, User? user, DtoOptions dtoOptions)
{
var genres = item
.GetRecursiveChildren(
user,
new InternalItemsQuery(user)
{
IncludeItemTypes = [BaseItemKind.Audio],
DtoOptions = dtoOptions
},
out _)
.GetRecursiveChildren(user, new InternalItemsQuery(user)
{
IncludeItemTypes = [BaseItemKind.Audio],
DtoOptions = dtoOptions
})
.Cast<Audio>()
.SelectMany(i => i.Genres)
.Concat(item.Genres)
@@ -63,12 +63,12 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenres(genres, user, dtoOptions);
}
public IReadOnlyList<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
public List<BaseItem> GetInstantMixFromPlaylist(Playlist item, User? user, DtoOptions dtoOptions)
{
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
}
public IReadOnlyList<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
public List<BaseItem> GetInstantMixFromGenres(IEnumerable<string> genres, User? user, DtoOptions dtoOptions)
{
var genreIds = genres.DistinctNames().Select(i =>
{
@@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
}
public IReadOnlyList<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
public List<BaseItem> GetInstantMixFromGenreIds(Guid[] genreIds, User? user, DtoOptions dtoOptions)
{
return _libraryManager.GetItemList(new InternalItemsQuery(user)
{
@@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.Library
});
}
public IReadOnlyList<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
public List<BaseItem> GetInstantMixFromItem(BaseItem item, User? user, DtoOptions dtoOptions)
{
if (item is MusicGenre)
{

View File

@@ -1,101 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.IO;
namespace Emby.Server.Implementations.Library;
/// <summary>
/// IPathManager implementation.
/// </summary>
public class PathManager : IPathManager
{
private readonly IServerConfigurationManager _config;
private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="PathManager"/> class.
/// </summary>
/// <param name="config">The server configuration manager.</param>
/// <param name="appPaths">The application paths.</param>
public PathManager(
IServerConfigurationManager config,
IApplicationPaths appPaths)
{
_config = config;
_appPaths = appPaths;
}
private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
/// <inheritdoc />
public string GetAttachmentPath(string mediaSourceId, string fileName)
{
return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
}
/// <inheritdoc />
public string GetAttachmentFolderPath(string mediaSourceId)
{
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
return Path.Join(AttachmentCachePath, id[..2], id);
}
/// <inheritdoc />
public string GetSubtitleFolderPath(string mediaSourceId)
{
var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
return Path.Join(SubtitleCachePath, id[..2], id);
}
/// <inheritdoc />
public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
{
return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
}
/// <inheritdoc />
public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
{
var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan();
return saveWithMedia
? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(Path.GetFileName(item.Path), ".trickplay"))
: Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id);
}
/// <inheritdoc/>
public string GetChapterImageFolderPath(BaseItem item)
{
return Path.Combine(item.GetInternalMetadataPath(), "chapters");
}
/// <inheritdoc/>
public string GetChapterImagePath(BaseItem item, long chapterPositionTicks)
{
var filename = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg";
return Path.Combine(GetChapterImageFolderPath(item), filename);
}
/// <inheritdoc/>
public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
{
var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
return [
GetAttachmentFolderPath(mediaSourceId),
GetSubtitleFolderPath(mediaSourceId),
GetTrickplayDirectory(item, false),
GetTrickplayDirectory(item, true),
GetChapterImageFolderPath(item)
];
}
}

View File

@@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Library
item.GetParents().Any(i => i.IsLocked);
// Make sure DateCreated and DateModified have values
var fileInfo = directoryService.GetFileSystemEntry(item.Path);
var fileInfo = directoryService.GetFile(item.Path);
if (fileInfo is null)
{
return false;
@@ -136,33 +136,23 @@ namespace Emby.Server.Implementations.Library
if (config.UseFileCreationTimeForDateAdded)
{
var fileCreationDate = info?.CreationTimeUtc;
if (fileCreationDate is not null)
// directoryService.getFile may return null
if (info is not null)
{
var dateCreated = fileCreationDate;
if (dateCreated == DateTime.MinValue)
var dateCreated = info.CreationTimeUtc;
if (dateCreated.Equals(DateTime.MinValue))
{
dateCreated = DateTime.UtcNow;
}
item.DateCreated = dateCreated.Value;
item.DateCreated = dateCreated;
}
}
else
{
item.DateCreated = DateTime.UtcNow;
}
if (info is not null && !info.IsDirectory)
{
item.Size = info.Length;
}
var fileModificationDate = info?.LastWriteTimeUtc;
if (fileModificationDate.HasValue)
{
item.DateModified = fileModificationDate.Value;
}
}
}
}

View File

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

View File

@@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
_ => _videoResolvers
};
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
{
var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
var extraResult = GetExtraInfo(path, _namingOptions);
if (extraResult.ExtraType is null)
{
extraType = null;

View File

@@ -68,7 +68,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
var justName = Path.GetFileName(item.Path.AsSpan());
var id = justName.GetAttributeValue("tmdbid");
item.TrySetProviderId(MetadataProvider.Tmdb, id);
if (!string.IsNullOrEmpty(id))
{
item.SetProviderId(MetadataProvider.Tmdb, id);
}
}
}
}

View File

@@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
}
var videoInfos = files
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath))
.Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
.Where(f => f is not null)
.ToList();
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
var result = new MultiItemResolverResult
{
@@ -369,21 +369,26 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
// We need to only look at the name of this actual item (not parents)
var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
var tmdbid = justName.GetAttributeValue("tmdbid");
// If not in a mixed folder and ID not found in folder path, check filename
if (string.IsNullOrEmpty(tmdbid) && !item.IsInMixedFolder)
if (!justName.IsEmpty)
{
tmdbid = Path.GetFileName(item.Path.AsSpan()).GetAttributeValue("tmdbid");
}
// Check for TMDb id
var tmdbid = justName.GetAttributeValue("tmdbid");
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
if (!string.IsNullOrWhiteSpace(tmdbid))
{
item.SetProviderId(MetadataProvider.Tmdb, tmdbid);
}
}
if (!string.IsNullOrEmpty(item.Path))
{
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
item.TrySetProviderId(MetadataProvider.Imdb, imdbid);
if (!string.IsNullOrWhiteSpace(imdbid))
{
item.SetProviderId(MetadataProvider.Imdb, imdbid);
}
}
}
}
@@ -408,11 +413,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
if (child.IsDirectory)
{
if (NamingOptions.AllExtrasTypesFolderNames.ContainsKey(filename))
{
continue;
}
if (IsDvdDirectory(child.FullName, filename, directoryService))
{
var movie = new T
@@ -464,17 +464,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
var videoPath = result.Items[0].Path;
var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
var hasOtherSubfolders = multiDiscFolders.Count > 0;
if (!hasPhotos && !hasOtherSubfolders)
if (!hasPhotos)
{
var movie = (T)result.Items[0];
movie.IsInMixedFolder = false;
if (collectionType == CollectionType.movies || collectionType is null)
{
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
}
movie.Name = Path.GetFileName(movie.ContainingFolderPath);
return movie;
}
}

View File

@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (args.IsDirectory)
{
// It's a playlist if the path is a directory with [playlist] in its name
// It's a boxset if the path is a directory with [playlist] in its name
var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
if (string.IsNullOrEmpty(filename))
{

View File

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

View File

@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (child.IsDirectory)
{
if (IsSeasonFolder(child.FullName, path, isTvContentType))
if (IsSeasonFolder(child.FullName, isTvContentType))
{
_logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true;
@@ -155,12 +155,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// Determines whether [is season folder] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
/// <param name="parentPath">The parentpath.</param>
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType)
private static bool IsSeasonFolder(string path, bool isTvContentType)
{
var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber;
var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
return seasonNumber.HasValue;
}
@@ -187,25 +186,46 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var justName = Path.GetFileName(path.AsSpan());
var imdbId = justName.GetAttributeValue("imdbid");
item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
if (!string.IsNullOrEmpty(imdbId))
{
item.SetProviderId(MetadataProvider.Imdb, imdbId);
}
var tvdbId = justName.GetAttributeValue("tvdbid");
item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
if (!string.IsNullOrEmpty(tvdbId))
{
item.SetProviderId(MetadataProvider.Tvdb, tvdbId);
}
var tvmazeId = justName.GetAttributeValue("tvmazeid");
item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId);
if (!string.IsNullOrEmpty(tvmazeId))
{
item.SetProviderId(MetadataProvider.TvMaze, tvmazeId);
}
var tmdbId = justName.GetAttributeValue("tmdbid");
item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
if (!string.IsNullOrEmpty(tmdbId))
{
item.SetProviderId(MetadataProvider.Tmdb, tmdbId);
}
var anidbId = justName.GetAttributeValue("anidbid");
item.TrySetProviderId("AniDB", anidbId);
if (!string.IsNullOrEmpty(anidbId))
{
item.SetProviderId("AniDB", anidbId);
}
var aniListId = justName.GetAttributeValue("anilistid");
item.TrySetProviderId("AniList", aniListId);
if (!string.IsNullOrEmpty(aniListId))
{
item.SetProviderId("AniList", aniListId);
}
var aniSearchId = justName.GetAttributeValue("anisearchid");
item.TrySetProviderId("AniSearch", aniSearchId);
if (!string.IsNullOrEmpty(aniSearchId))
{
item.SetProviderId("AniSearch", aniSearchId);
}
}
}
}

View File

@@ -3,9 +3,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -172,7 +171,7 @@ namespace Emby.Server.Implementations.Library
}
};
IReadOnlyList<BaseItem> mediaItems;
List<BaseItem> mediaItems;
if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
{

View File

@@ -4,13 +4,13 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library;
@@ -43,26 +43,14 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
/// <inheritdoc />
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var posters = GetItemsWithImageType(ImageType.Primary)
.Select(x => x.GetImages(ImageType.Primary).FirstOrDefault()?.Path)
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => path!)
.ToList();
var backdrops = GetItemsWithImageType(ImageType.Thumb)
.Select(x => x.GetImages(ImageType.Thumb).FirstOrDefault()?.Path)
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => path!)
.ToList();
var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList();
var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList();
if (backdrops.Count == 0)
{
// Thumb images fit better because they include the title in the image but are not provided with TMDb.
// Using backdrops as a fallback to generate an image at all
_logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen");
backdrops = GetItemsWithImageType(ImageType.Backdrop)
.Select(x => x.GetImages(ImageType.Backdrop).FirstOrDefault()?.Path)
.Where(path => !string.IsNullOrEmpty(path))
.Select(path => path!)
.ToList();
backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList();
}
_imageEncoder.CreateSplashscreen(posters, backdrops);
@@ -77,15 +65,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
CollapseBoxSetItems = false,
Recursive = true,
DtoOptions = new DtoOptions(false),
ImageTypes = [imageType],
ImageTypes = new[] { imageType },
Limit = 30,
// TODO max parental rating configurable
MaxParentalRating = new(10, null),
OrderBy =
[
MaxParentalRating = 10,
OrderBy = new[]
{
(ItemSortBy.Random, SortOrder.Ascending)
],
IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series]
},
IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series }
});
}
}

View File

@@ -1,20 +1,20 @@
#pragma warning disable RS0030 // Do not use banned APIs
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using BitFaster.Caching.Lru;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Microsoft.EntityFrameworkCore;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book;
@@ -25,28 +25,32 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class UserDataManager : IUserDataManager
{
private readonly IServerConfigurationManager _config;
private readonly IDbContextFactory<JellyfinDbContext> _repository;
private readonly FastConcurrentLru<string, UserItemData> _cache;
private readonly ConcurrentDictionary<string, UserItemData> _userData =
new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
private readonly IServerConfigurationManager _config;
private readonly IUserManager _userManager;
private readonly IUserDataRepository _repository;
/// <summary>
/// Initializes a new instance of the <see cref="UserDataManager"/> class.
/// </summary>
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="repository">Instance of the <see cref="IDbContextFactory{JellyfinDbContext}"/> interface.</param>
public UserDataManager(
IServerConfigurationManager config,
IDbContextFactory<JellyfinDbContext> repository)
IUserManager userManager,
IUserDataRepository repository)
{
_config = config;
_userManager = userManager;
_repository = repository;
_cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public event EventHandler<UserDataSaveEventArgs>? UserDataSaved;
public event EventHandler<UserDataSaveEventArgs> UserDataSaved;
public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
{
var user = _userManager.GetUserById(userId);
SaveUserData(user, item, userData, reason, cancellationToken);
}
/// <inheritdoc />
public void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(userData);
@@ -57,30 +61,15 @@ namespace Emby.Server.Implementations.Library
var keys = item.GetUserDataKeys();
using var dbContext = _repository.CreateDbContext();
using var transaction = dbContext.Database.BeginTransaction();
var userId = user.InternalId;
foreach (var key in keys)
{
userData.Key = key;
var userDataEntry = Map(userData, user.Id, item.Id);
if (dbContext.UserData.Any(f => f.ItemId == userDataEntry.ItemId && f.UserId == userDataEntry.UserId && f.CustomDataKey == userDataEntry.CustomDataKey))
{
dbContext.UserData.Attach(userDataEntry).State = EntityState.Modified;
}
else
{
dbContext.UserData.Add(userDataEntry);
}
_repository.SaveUserData(userId, key, userData, cancellationToken);
}
dbContext.SaveChanges();
transaction.Commit();
var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, item.Id);
_cache.AddOrUpdate(cacheKey, userData);
item.UserData = dbContext.UserData.Where(e => e.ItemId == item.Id).AsNoTracking().ToArray(); // rehydrate the cached userdata
_userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
{
@@ -92,14 +81,14 @@ namespace Emby.Server.Implementations.Library
});
}
/// <inheritdoc />
public void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason)
{
ArgumentNullException.ThrowIfNull(user);
ArgumentNullException.ThrowIfNull(item);
ArgumentNullException.ThrowIfNull(reason);
ArgumentNullException.ThrowIfNull(userDataDto);
var userData = GetUserData(user, item) ?? throw new InvalidOperationException("UserData should not be null.");
var userData = GetUserData(user, item);
if (userDataDto.PlaybackPositionTicks.HasValue)
{
@@ -139,91 +128,65 @@ namespace Emby.Server.Implementations.Library
SaveUserData(user, item, userData, reason, CancellationToken.None);
}
private UserData Map(UserItemData dto, Guid userId, Guid itemId)
/// <summary>
/// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="userData">The user item data.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken)
{
return new UserData()
{
ItemId = itemId,
CustomDataKey = dto.Key,
Item = null,
User = null,
AudioStreamIndex = dto.AudioStreamIndex,
IsFavorite = dto.IsFavorite,
LastPlayedDate = dto.LastPlayedDate,
Likes = dto.Likes,
PlaybackPositionTicks = dto.PlaybackPositionTicks,
PlayCount = dto.PlayCount,
Played = dto.Played,
Rating = dto.Rating,
UserId = userId,
SubtitleStreamIndex = dto.SubtitleStreamIndex,
};
var user = _userManager.GetUserById(userId);
_repository.SaveAllUserData(user.InternalId, userData, cancellationToken);
}
private static UserItemData Map(UserData dto)
/// <summary>
/// Retrieve all user data for the given user.
/// </summary>
/// <param name="userId">The user id.</param>
/// <returns>A <see cref="List{UserItemData}"/> containing all of the user's item data.</returns>
public List<UserItemData> GetAllUserData(Guid userId)
{
return new UserItemData()
{
Key = dto.CustomDataKey!,
AudioStreamIndex = dto.AudioStreamIndex,
IsFavorite = dto.IsFavorite,
LastPlayedDate = dto.LastPlayedDate,
Likes = dto.Likes,
PlaybackPositionTicks = dto.PlaybackPositionTicks,
PlayCount = dto.PlayCount,
Played = dto.Played,
Rating = dto.Rating,
SubtitleStreamIndex = dto.SubtitleStreamIndex,
};
var user = _userManager.GetUserById(userId);
return _repository.GetAllUserData(user.InternalId);
}
private UserItemData? GetUserData(User user, Guid itemId, List<string> keys)
public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys)
{
var cacheKey = GetCacheKey(user.InternalId, itemId);
var user = _userManager.GetUserById(userId);
if (_cache.TryGet(cacheKey, out var data))
return GetUserData(user, itemId, keys);
}
public UserItemData GetUserData(User user, Guid itemId, List<string> keys)
{
var userId = user.InternalId;
var cacheKey = GetCacheKey(userId, itemId);
return _userData.GetOrAdd(cacheKey, _ => GetUserDataInternal(userId, keys));
}
private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
{
var userData = _repository.GetUserData(internalUserId, keys);
if (userData is not null)
{
return data;
return userData;
}
data = GetUserDataInternal(user.Id, itemId, keys);
if (data is null)
if (keys.Count > 0)
{
return new UserItemData()
return new UserItemData
{
Key = keys[0],
Key = keys[0]
};
}
return _cache.GetOrAdd(cacheKey, _ => data);
}
private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
{
if (keys.Count == 0)
{
return null;
}
using var context = _repository.CreateDbContext();
var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray();
if (userData.Length > 0)
{
var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N"));
if (directDataReference is not null)
{
return Map(directDataReference);
}
return Map(userData.First());
}
return new UserItemData
{
Key = keys.Last()!
};
return null;
}
/// <summary>
@@ -235,29 +198,30 @@ namespace Emby.Server.Implementations.Library
return internalUserId.ToString(CultureInfo.InvariantCulture) + "-" + itemId.ToString("N", CultureInfo.InvariantCulture);
}
/// <inheritdoc />
public UserItemData? GetUserData(User user, BaseItem item)
public UserItemData GetUserData(User user, BaseItem item)
{
return item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault() ?? new UserItemData()
{
Key = item.GetUserDataKeys()[0],
};
return GetUserData(user, item.Id, item.GetUserDataKeys());
}
public UserItemData GetUserData(Guid userId, BaseItem item)
{
return GetUserData(userId, item.Id, item.GetUserDataKeys());
}
public UserItemDataDto GetUserDataDto(BaseItem item, User user)
{
var userData = GetUserData(user, item);
var dto = GetUserItemDataDto(userData);
item.FillUserDataDtoValues(dto, userData, null, user, new DtoOptions());
return dto;
}
/// <inheritdoc />
public UserItemDataDto? GetUserDataDto(BaseItem item, User user)
=> GetUserDataDto(item, null, user, new DtoOptions());
/// <inheritdoc />
public UserItemDataDto? GetUserDataDto(BaseItem item, BaseItemDto? itemDto, User user, DtoOptions options)
public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options)
{
var userData = GetUserData(user, item);
if (userData is null)
{
return null;
}
var dto = GetUserItemDataDto(userData, item.Id);
var dto = GetUserItemDataDto(userData);
item.FillUserDataDtoValues(dto, userData, itemDto, user, options);
return dto;
@@ -267,10 +231,9 @@ namespace Emby.Server.Implementations.Library
/// Converts a UserItemData to a DTOUserItemData.
/// </summary>
/// <param name="data">The data.</param>
/// <param name="itemId">The reference key to an Item.</param>
/// <returns>DtoUserItemData.</returns>
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
private UserItemDataDto GetUserItemDataDto(UserItemData data, Guid itemId)
private UserItemDataDto GetUserItemDataDto(UserItemData data)
{
ArgumentNullException.ThrowIfNull(data);
@@ -283,7 +246,6 @@ namespace Emby.Server.Implementations.Library
Rating = data.Rating,
Played = data.Played,
LastPlayedDate = data.LastPlayedDate,
ItemId = itemId,
Key = data.Key
};
}
@@ -308,7 +270,7 @@ namespace Emby.Server.Implementations.Library
// ignore progress during the beginning
positionTicks = 0;
}
else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= (runtimeTicks - TimeSpan.TicksPerSecond))
else if (pctIn > _config.Configuration.MaxResumePct || positionTicks >= runtimeTicks)
{
// mark as completed close to the end
positionTicks = 0;

View File

@@ -6,10 +6,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Jellyfin.Data;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -18,6 +16,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
@@ -28,15 +27,17 @@ namespace Emby.Server.Implementations.Library
{
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localizationManager;
private readonly IUserManager _userManager;
private readonly IChannelManager _channelManager;
private readonly ILiveTvManager _liveTvManager;
private readonly IServerConfigurationManager _config;
public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IChannelManager channelManager, ILiveTvManager liveTvManager, IServerConfigurationManager config)
public UserViewManager(ILibraryManager libraryManager, ILocalizationManager localizationManager, IUserManager userManager, IChannelManager channelManager, ILiveTvManager liveTvManager, IServerConfigurationManager config)
{
_libraryManager = libraryManager;
_localizationManager = localizationManager;
_userManager = userManager;
_channelManager = channelManager;
_liveTvManager = liveTvManager;
_config = config;
@@ -44,7 +45,11 @@ namespace Emby.Server.Implementations.Library
public Folder[] GetUserViews(UserViewQuery query)
{
var user = query.User;
var user = _userManager.GetUserById(query.UserId);
if (user is null)
{
throw new ArgumentException("User id specified in the query does not exist.", nameof(query));
}
var folders = _libraryManager.GetUserRootFolder()
.GetChildren(user, true)
@@ -120,14 +125,14 @@ namespace Emby.Server.Implementations.Library
{
var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
{
UserId = user.Id
UserId = query.UserId
}).GetAwaiter().GetResult();
var channels = channelResult.Items;
list.AddRange(channels);
if (_liveTvManager.GetEnabledUsers().Select(i => i.Id).Contains(user.Id))
if (_liveTvManager.GetEnabledUsers().Select(i => i.Id).Contains(query.UserId))
{
list.Add(_liveTvManager.GetInternalLiveTvFolder(CancellationToken.None));
}
@@ -202,7 +207,9 @@ namespace Emby.Server.Implementations.Library
public List<Tuple<BaseItem, List<BaseItem>>> GetLatestItems(LatestItemsQuery request, DtoOptions options)
{
var libraryItems = GetItemsForLatestItems(request.User, request, options);
var user = _userManager.GetUserById(request.UserId);
var libraryItems = GetItemsForLatestItems(user, request, options);
var list = new List<Tuple<BaseItem, List<BaseItem>>>();
@@ -310,40 +317,39 @@ namespace Emby.Server.Implementations.Library
}
}
MediaType[] mediaTypes = [];
var mediaTypes = new List<MediaType>();
if (includeItemTypes.Length == 0)
{
HashSet<MediaType> tmpMediaTypes = [];
foreach (var parent in parents.OfType<ICollectionFolder>())
{
switch (parent.CollectionType)
{
case CollectionType.books:
tmpMediaTypes.Add(MediaType.Book);
tmpMediaTypes.Add(MediaType.Audio);
mediaTypes.Add(MediaType.Book);
mediaTypes.Add(MediaType.Audio);
break;
case CollectionType.music:
tmpMediaTypes.Add(MediaType.Audio);
mediaTypes.Add(MediaType.Audio);
break;
case CollectionType.photos:
tmpMediaTypes.Add(MediaType.Photo);
tmpMediaTypes.Add(MediaType.Video);
mediaTypes.Add(MediaType.Photo);
mediaTypes.Add(MediaType.Video);
break;
case CollectionType.homevideos:
tmpMediaTypes.Add(MediaType.Photo);
tmpMediaTypes.Add(MediaType.Video);
mediaTypes.Add(MediaType.Photo);
mediaTypes.Add(MediaType.Video);
break;
default:
tmpMediaTypes.Add(MediaType.Video);
mediaTypes.Add(MediaType.Video);
break;
}
}
mediaTypes = tmpMediaTypes.ToArray();
mediaTypes = mediaTypes.Distinct().ToList();
}
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Length == 0
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
? new[]
{
BaseItemKind.Person,
@@ -369,31 +375,12 @@ namespace Emby.Server.Implementations.Library
Limit = limit * 5,
IsPlayed = isPlayed,
DtoOptions = options,
MediaTypes = mediaTypes
MediaTypes = mediaTypes.ToArray()
};
if (request.GroupItems)
if (parents.Count == 0)
{
var collectionType = parents
.Select(parent => parent switch
{
ICollectionFolder collectionFolder => collectionFolder.CollectionType,
UserView userView => userView.CollectionType,
_ => null
})
.FirstOrDefault(type => type is not null);
if (collectionType == CollectionType.tvshows)
{
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows);
}
if (collectionType == CollectionType.music)
{
query.Limit = limit;
return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
}
return _libraryManager.GetItemList(query, false);
}
return _libraryManager.GetItemList(query, parents);

View File

@@ -5,44 +5,45 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Class ArtistsPostScanTask.
/// </summary>
public class ArtistsPostScanTask : ILibraryPostScanTask
namespace Emby.Server.Implementations.Library.Validators
{
/// <summary>
/// The _library manager.
/// Class ArtistsPostScanTask.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly ILogger<ArtistsValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public ArtistsPostScanTask(
ILibraryManager libraryManager,
ILogger<ArtistsValidator> logger,
IItemRepository itemRepo)
public class ArtistsPostScanTask : ILibraryPostScanTask
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// The _library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly ILogger<ArtistsValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
/// <summary>
/// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public ArtistsPostScanTask(
ILibraryManager libraryManager,
ILogger<ArtistsValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
}

View File

@@ -10,101 +10,102 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Class ArtistsValidator.
/// </summary>
public class ArtistsValidator
namespace Emby.Server.Implementations.Library.Validators
{
/// <summary>
/// The library manager.
/// Class ArtistsValidator.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<ArtistsValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
public class ArtistsValidator
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetAllArtistNames();
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<ArtistsValidator> _logger;
private readonly IItemRepository _itemRepo;
var numComplete = 0;
var count = names.Count;
foreach (var name in names)
/// <summary>
/// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
{
try
{
var item = _libraryManager.GetArtist(name);
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
IncludeItemTypes = [BaseItemKind.MusicArtist],
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>().ToList();
var names = _itemRepo.GetAllArtistNames();
foreach (var item in deadEntities)
{
if (!item.IsAccessedByName)
var numComplete = 0;
var count = names.Count;
foreach (var name in names)
{
continue;
}
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
_libraryManager.DeleteItem(
item,
new DeleteOptions
try
{
DeleteFileLocation = false
},
false);
}
var item = _libraryManager.GetArtist(name);
progress.Report(100);
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {ArtistName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
IsDeadArtist = true,
IsLocked = false
}).Cast<MusicArtist>().ToList();
foreach (var item in deadEntities)
{
if (!item.IsAccessedByName)
{
continue;
}
_logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
}
progress.Report(100);
}
}
}

View File

@@ -4,150 +4,153 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations.Enums;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Class CollectionPostScanTask.
/// </summary>
public class CollectionPostScanTask : ILibraryPostScanTask
namespace Emby.Server.Implementations.Library.Validators
{
private readonly ILibraryManager _libraryManager;
private readonly ICollectionManager _collectionManager;
private readonly ILogger<CollectionPostScanTask> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
/// Class CollectionPostScanTask.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="collectionManager">The collection manager.</param>
/// <param name="logger">The logger.</param>
public CollectionPostScanTask(
ILibraryManager libraryManager,
ICollectionManager collectionManager,
ILogger<CollectionPostScanTask> logger)
public class CollectionPostScanTask : ILibraryPostScanTask
{
_libraryManager = libraryManager;
_collectionManager = collectionManager;
_logger = logger;
}
private readonly ILibraryManager _libraryManager;
private readonly ICollectionManager _collectionManager;
private readonly ILogger<CollectionPostScanTask> _logger;
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
foreach (var library in _libraryManager.RootFolder.Children)
/// <summary>
/// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="collectionManager">The collection manager.</param>
/// <param name="logger">The logger.</param>
public CollectionPostScanTask(
ILibraryManager libraryManager,
ICollectionManager collectionManager,
ILogger<CollectionPostScanTask> logger)
{
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
{
continue;
}
var startIndex = 0;
var pagesize = 1000;
while (true)
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
MediaTypes = [MediaType.Video],
IncludeItemTypes = [BaseItemKind.Movie],
IsVirtualItem = false,
OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)],
Parent = library,
StartIndex = startIndex,
Limit = pagesize,
Recursive = true
});
foreach (var m in movies)
{
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
{
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
{
movieList.Add(movie.Id);
}
else
{
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
}
}
}
if (movies.Count < pagesize)
{
break;
}
startIndex += pagesize;
}
_libraryManager = libraryManager;
_collectionManager = collectionManager;
_logger = logger;
}
var numComplete = 0;
var count = collectionNameMoviesMap.Count;
if (count == 0)
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
foreach (var library in _libraryManager.RootFolder.Children)
{
if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
{
continue;
}
var startIndex = 0;
var pagesize = 1000;
while (true)
{
var movies = _libraryManager.GetItemList(new InternalItemsQuery
{
MediaTypes = new[] { MediaType.Video },
IncludeItemTypes = new[] { BaseItemKind.Movie },
IsVirtualItem = false,
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
Parent = library,
StartIndex = startIndex,
Limit = pagesize,
Recursive = true
});
foreach (var m in movies)
{
if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
{
if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
{
movieList.Add(movie.Id);
}
else
{
collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
}
}
}
if (movies.Count < pagesize)
{
break;
}
startIndex += pagesize;
}
}
var numComplete = 0;
var count = collectionNameMoviesMap.Count;
if (count == 0)
{
progress.Report(100);
return;
}
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.BoxSet },
CollapseBoxSetItems = false,
Recursive = true
});
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
{
try
{
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
if (boxSet is null)
{
// won't automatically create collection if only one movie in it
if (movieIds.Count >= 2)
{
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
Name = collectionName,
IsLocked = true
});
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
}
}
else
{
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
}
}
progress.Report(100);
return;
}
var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.BoxSet],
CollapseBoxSetItems = false,
Recursive = true
});
foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
{
try
{
var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
if (boxSet is null)
{
// won't automatically create collection if only one movie in it
if (movieIds.Count >= 2)
{
boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
{
Name = collectionName,
}).ConfigureAwait(false);
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
}
}
else
{
await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
}
}
progress.Report(100);
}
}

View File

@@ -5,44 +5,45 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Class GenresPostScanTask.
/// </summary>
public class GenresPostScanTask : ILibraryPostScanTask
namespace Emby.Server.Implementations.Library.Validators
{
/// <summary>
/// The _library manager.
/// Class GenresPostScanTask.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly ILogger<GenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public GenresPostScanTask(
ILibraryManager libraryManager,
ILogger<GenresValidator> logger,
IItemRepository itemRepo)
public class GenresPostScanTask : ILibraryPostScanTask
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// The _library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly ILogger<GenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
/// <summary>
/// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public GenresPostScanTask(
ILibraryManager libraryManager,
ILogger<GenresValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
}

View File

@@ -1,103 +1,81 @@
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Class GenresValidator.
/// </summary>
public class GenresValidator
namespace Emby.Server.Implementations.Library.Validators
{
/// <summary>
/// The library manager.
/// Class GenresValidator.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly IItemRepository _itemRepo;
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<GenresValidator> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="GenresValidator"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
public class GenresValidator
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetGenreNames();
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<GenresValidator> _logger;
var numComplete = 0;
var count = names.Count;
foreach (var name in names)
/// <summary>
/// Initializes a new instance of the <see cref="GenresValidator"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
{
try
{
var item = _libraryManager.GetGenre(name);
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {GenreName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
IsDeadGenre = true,
IsLocked = false
});
var names = _itemRepo.GetGenreNames();
foreach (var item in deadEntities)
{
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
var numComplete = 0;
var count = names.Count;
_libraryManager.DeleteItem(
item,
new DeleteOptions
foreach (var name in names)
{
try
{
DeleteFileLocation = false
},
false);
}
var item = _libraryManager.GetGenre(name);
progress.Report(100);
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {GenreName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
progress.Report(100);
}
}
}

View File

@@ -5,44 +5,45 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Class MusicGenresPostScanTask.
/// </summary>
public class MusicGenresPostScanTask : ILibraryPostScanTask
namespace Emby.Server.Implementations.Library.Validators
{
/// <summary>
/// The library manager.
/// Class MusicGenresPostScanTask.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly ILogger<MusicGenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public MusicGenresPostScanTask(
ILibraryManager libraryManager,
ILogger<MusicGenresValidator> logger,
IItemRepository itemRepo)
public class MusicGenresPostScanTask : ILibraryPostScanTask
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly ILogger<MusicGenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
/// <summary>
/// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public MusicGenresPostScanTask(
ILibraryManager libraryManager,
ILogger<MusicGenresValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
}

View File

@@ -5,76 +5,77 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Class MusicGenresValidator.
/// </summary>
public class MusicGenresValidator
namespace Emby.Server.Implementations.Library.Validators
{
/// <summary>
/// The library manager.
/// Class MusicGenresValidator.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<MusicGenresValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
public class MusicGenresValidator
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetMusicGenreNames();
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<MusicGenresValidator> _logger;
private readonly IItemRepository _itemRepo;
var numComplete = 0;
var count = names.Count;
foreach (var name in names)
/// <summary>
/// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
{
try
{
var item = _libraryManager.GetMusicGenre(name);
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {GenreName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
progress.Report(100);
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetMusicGenreNames();
var numComplete = 0;
var count = names.Count;
foreach (var name in names)
{
try
{
var item = _libraryManager.GetMusicGenre(name);
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {GenreName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
progress.Report(100);
}
}
}

View File

@@ -1,5 +1,5 @@
using System;
using System.Linq;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
@@ -9,112 +9,119 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Class PeopleValidator.
/// </summary>
public class PeopleValidator
namespace Emby.Server.Implementations.Library.Validators
{
/// <summary>
/// The _library manager.
/// Class PeopleValidator.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The _logger.
/// </summary>
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="PeopleValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="fileSystem">The file system.</param>
public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
public class PeopleValidator
{
_libraryManager = libraryManager;
_logger = logger;
_fileSystem = fileSystem;
}
/// <summary>
/// The _library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Validates the people.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param>
/// <returns>Task.</returns>
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
{
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
/// <summary>
/// The _logger.
/// </summary>
private readonly ILogger _logger;
var numComplete = 0;
private readonly IFileSystem _fileSystem;
var numPeople = people.Count;
IProgress<double> subProgress = new Progress<double>((val) => progress.Report(val / 2));
_logger.LogDebug("Will refresh {Amount} people", numPeople);
foreach (var person in people)
/// <summary>
/// Initializes a new instance of the <see cref="PeopleValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="fileSystem">The file system.</param>
public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
{
cancellationToken.ThrowIfCancellationRequested();
_libraryManager = libraryManager;
_logger = logger;
_fileSystem = fileSystem;
}
try
/// <summary>
/// Validates the people.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="progress">The progress.</param>
/// <returns>Task.</returns>
public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
{
var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
var numComplete = 0;
var numPeople = people.Count;
_logger.LogDebug("Will refresh {0} people", numPeople);
foreach (var person in people)
{
var item = _libraryManager.GetPerson(person);
if (item is null)
cancellationToken.ThrowIfCancellationRequested();
try
{
_logger.LogWarning("Failed to get person: {Name}", person);
continue;
var item = _libraryManager.GetPerson(person);
if (item is null)
{
_logger.LogWarning("Failed to get person: {Name}", person);
continue;
}
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
};
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error validating IBN entry {Person}", person);
}
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
};
// Update progress
numComplete++;
double percent = numComplete;
percent /= numPeople;
await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
progress.Report(100 * percent);
}
catch (OperationCanceledException)
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
throw;
}
catch (Exception ex)
IncludeItemTypes = [BaseItemKind.Person],
IsDeadPerson = true,
IsLocked = false
});
foreach (var item in deadEntities)
{
_logger.LogError(ex, "Error validating IBN entry {Person}", person);
_logger.LogInformation(
"Deleting dead {2} {0} {1}.",
item.Id.ToString("N", CultureInfo.InvariantCulture),
item.Name,
item.GetType().Name);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
}
// Update progress
numComplete++;
double percent = numComplete;
percent /= numPeople;
progress.Report(100);
subProgress.Report(100 * percent);
_logger.LogInformation("People validation complete");
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = [BaseItemKind.Person],
IsDeadPerson = true,
IsLocked = false
});
subProgress = new Progress<double>((val) => progress.Report((val / 2) + 50));
var i = 0;
foreach (var item in deadEntities.Chunk(500))
{
_libraryManager.DeleteItemsUnsafeFast(item);
subProgress.Report(100f / deadEntities.Count * (i++ * 100));
}
progress.Report(100);
_logger.LogInformation("People validation complete");
}
}

View File

@@ -5,45 +5,46 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Class MusicGenresPostScanTask.
/// </summary>
public class StudiosPostScanTask : ILibraryPostScanTask
namespace Emby.Server.Implementations.Library.Validators
{
/// <summary>
/// The _library manager.
/// Class MusicGenresPostScanTask.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly ILogger<StudiosValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public StudiosPostScanTask(
ILibraryManager libraryManager,
ILogger<StudiosValidator> logger,
IItemRepository itemRepo)
public class StudiosPostScanTask : ILibraryPostScanTask
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// The _library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
private readonly ILogger<StudiosValidator> _logger;
private readonly IItemRepository _itemRepo;
/// <summary>
/// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public StudiosPostScanTask(
ILibraryManager libraryManager,
ILogger<StudiosValidator> logger,
IItemRepository itemRepo)
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
}
}
}

View File

@@ -8,97 +8,98 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library.Validators;
/// <summary>
/// Class StudiosValidator.
/// </summary>
public class StudiosValidator
namespace Emby.Server.Implementations.Library.Validators
{
/// <summary>
/// The library manager.
/// Class StudiosValidator.
/// </summary>
private readonly ILibraryManager _libraryManager;
private readonly IItemRepository _itemRepo;
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<StudiosValidator> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="StudiosValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
public class StudiosValidator
{
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
/// <summary>
/// The library manager.
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
var names = _itemRepo.GetStudioNames();
private readonly IItemRepository _itemRepo;
var numComplete = 0;
var count = names.Count;
/// <summary>
/// The logger.
/// </summary>
private readonly ILogger<StudiosValidator> _logger;
foreach (var name in names)
/// <summary>
/// Initializes a new instance of the <see cref="StudiosValidator" /> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
/// <param name="logger">The logger.</param>
/// <param name="itemRepo">The item repository.</param>
public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
{
try
{
var item = _libraryManager.GetStudio(name);
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {StudioName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
_libraryManager = libraryManager;
_logger = logger;
_itemRepo = itemRepo;
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
/// <summary>
/// Runs the specified progress.
/// </summary>
/// <param name="progress">The progress.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
IncludeItemTypes = [BaseItemKind.Studio],
IsDeadStudio = true,
IsLocked = false
});
var names = _itemRepo.GetStudioNames();
foreach (var item in deadEntities)
{
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
var numComplete = 0;
var count = names.Count;
_libraryManager.DeleteItem(
item,
new DeleteOptions
foreach (var name in names)
{
try
{
DeleteFileLocation = false
},
false);
}
var item = _libraryManager.GetStudio(name);
progress.Report(100);
await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Don't clutter the log
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {StudioName}", name);
}
numComplete++;
double percent = numComplete;
percent /= count;
percent *= 100;
progress.Report(percent);
}
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { BaseItemKind.Studio },
IsDeadStudio = true,
IsLocked = false
});
foreach (var item in deadEntities)
{
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
_libraryManager.DeleteItem(
item,
new DeleteOptions
{
DeleteFileLocation = false
},
false);
}
progress.Report(100);
}
}
}

View File

@@ -1,3 +1 @@
{
"Albums": "аальбомқәа"
}
{}

View File

@@ -5,12 +5,12 @@
"Favorites": "Gunstelinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
"ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album kunstenaars",
"HeaderAlbumArtists": "Kunstenaars se Album",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
"Movies": "Flieks",
"Shows": "Televisie Reekse",
"HeaderContinueWatching": "Hou aan kyk",
"HeaderContinueWatching": "Kyk Verder",
"HeaderFavoriteEpisodes": "Gunsteling Episodes",
"Photos": "Foto's",
"Playlists": "Snitlyste",
@@ -19,7 +19,7 @@
"Sync": "Sinkroniseer",
"HeaderFavoriteSongs": "Gunsteling Liedjies",
"Songs": "Liedjies",
"DeviceOnlineWithName": "{0} is aanlyn",
"DeviceOnlineWithName": "{0} is gekoppel",
"DeviceOfflineWithName": "{0} is ontkoppel",
"Collections": "Versamelings",
"Inherit": "Ontvang",
@@ -61,7 +61,7 @@
"NotificationOptionPluginInstalled": "Inprop module geïnstalleer",
"NotificationOptionPluginError": "Inprop module het misluk",
"NotificationOptionNewLibraryContent": "Nuwe inhoud bygevoeg",
"NotificationOptionInstallationFailed": "Installasie mislukking",
"NotificationOptionInstallationFailed": "Installering het misluk",
"NotificationOptionCameraImageUploaded": "Kamera foto is opgelaai",
"NotificationOptionAudioPlaybackStopped": "Oudio terugspeel het gestop",
"NotificationOptionAudioPlayback": "Oudio terugspeel het begin",
@@ -86,9 +86,9 @@
"HomeVideos": "Tuis Videos",
"HeaderRecordingGroups": "Groep Opnames",
"Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aanmeldpoging van {0}",
"FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
"ChapterNameValue": "Hoofstuk {0}",
"CameraImageUploadedFrom": "'n Nuwe kamera foto is opgelaai vanaf {0}",
"CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}",
"AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer",
"Albums": "Albums",
"TasksChannelsCategory": "Internet kanale",
@@ -114,8 +114,8 @@
"TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
"TaskRefreshChapterImages": "Verkry Hoofstuk Beelde",
"Undefined": "Ongedefineerd",
"Forced": "Geforseerd",
"Default": "Standaard",
"Forced": "Geforseer",
"Default": "Oorspronklik",
"TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.",
"TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon",
"TaskOptimizeDatabaseDescription": "Komprimeer databasis en verkort vrye ruimte. As hierdie taak uitgevoer word nadat die media versameling geskandeer is of ander veranderings aangebring is wat databasisaanpassings impliseer, kan dit die prestasie verbeter.",
@@ -125,15 +125,5 @@
"External": "Ekstern",
"HearingImpaired": "gehoorgestremd",
"TaskRefreshTrickplayImages": "Genereer Fopspeel Beelde",
"TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling.",
"TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
"TaskAudioNormalization": "Odio Normalisering",
"TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
"TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.",
"TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
"TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
"TaskExtractMediaSegments": "Media Segment Skandeer",
"TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
"TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
"TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
"TaskRefreshTrickplayImagesDescription": "Skep fopspeel voorskou vir videos in aangeskakelde media versameling."
}

Some files were not shown because too many files have changed in this diff Show More