mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-18 03:19:06 -07:00
commit
984e415c66
@ -16,7 +16,7 @@ jobs:
|
||||
- job: main_build
|
||||
displayName: Main Build
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
vmImage: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
release:
|
||||
@ -35,12 +35,14 @@ jobs:
|
||||
inputs:
|
||||
command: restore
|
||||
projects: '$(RestoreBuildProjects)'
|
||||
enabled: false
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Build
|
||||
inputs:
|
||||
projects: '$(RestoreBuildProjects)'
|
||||
arguments: '--configuration $(BuildConfiguration)'
|
||||
enabled: false
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Test
|
||||
@ -66,38 +68,38 @@ jobs:
|
||||
# artifactName: 'jellyfin-build-$(BuildConfiguration)'
|
||||
# zipAfterPublish: true
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
- task: PublishPipelineArtifact@0
|
||||
displayName: 'Publish Artifact Naming'
|
||||
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
|
||||
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
|
||||
artifactName: 'Jellyfin.Naming'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
- task: PublishPipelineArtifact@0
|
||||
displayName: 'Publish Artifact Controller'
|
||||
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
|
||||
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
|
||||
artifactName: 'Jellyfin.Controller'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
- task: PublishPipelineArtifact@0
|
||||
displayName: 'Publish Artifact Model'
|
||||
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
|
||||
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
|
||||
artifactName: 'Jellyfin.Model'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
- task: PublishPipelineArtifact@0
|
||||
displayName: 'Publish Artifact Common'
|
||||
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
|
||||
inputs:
|
||||
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
|
||||
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
|
||||
artifactName: 'Jellyfin.Common'
|
||||
|
||||
- job: dotnet_compat
|
||||
displayName: Compatibility Check
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
vmImage: ubuntu-latest
|
||||
dependsOn: main_build
|
||||
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds)
|
||||
strategy:
|
||||
@ -118,45 +120,23 @@ jobs:
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
- task: DownloadBuildArtifacts@0
|
||||
displayName: Download the Reference Assembly Build Artifact
|
||||
inputs:
|
||||
buildType: 'specific' # Options: current, specific
|
||||
project: $(System.TeamProjectId) # Required when buildType == Specific
|
||||
pipeline: $(System.DefinitionId) # Required when buildType == Specific, not sure if this will take a name too
|
||||
#specificBuildWithTriggering: false # Optional
|
||||
buildVersionToDownload: 'latestFromBranch' # Required when buildType == Specific# Options: latest, latestFromBranch, specific
|
||||
allowPartiallySucceededBuilds: false # Optional
|
||||
branchName: '$(System.PullRequest.TargetBranch)' # Required when buildType == Specific && BuildVersionToDownload == LatestFromBranch
|
||||
#buildId: # Required when buildType == Specific && BuildVersionToDownload == Specific
|
||||
#tags: # Optional
|
||||
downloadType: 'single' # Options: single, specific
|
||||
artifactName: '$(NugetPackageName)'# Required when downloadType == Single
|
||||
#itemPattern: '**' # Optional
|
||||
downloadPath: '$(System.ArtifactsDirectory)/current-artifacts'
|
||||
#parallelizationLimit: '8' # Optional
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Nuget Assembly to current-release folder
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional
|
||||
contents: '**/*.dll'
|
||||
targetFolder: $(System.ArtifactsDirectory)/current-release
|
||||
cleanTargetFolder: true # Optional
|
||||
overWrite: true # Optional
|
||||
flattenFolders: true # Optional
|
||||
|
||||
- task: DownloadBuildArtifacts@0
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download the New Assembly Build Artifact
|
||||
inputs:
|
||||
buildType: 'current' # Options: current, specific
|
||||
allowPartiallySucceededBuilds: false # Optional
|
||||
downloadType: 'single' # Options: single, specific
|
||||
artifactName: '$(NugetPackageName)' # Required when downloadType == Single
|
||||
downloadPath: '$(System.ArtifactsDirectory)/new-artifacts'
|
||||
source: 'current' # Options: current, specific
|
||||
#preferTriggeringPipeline: false # Optional
|
||||
#tags: # Optional
|
||||
artifact: '$(NugetPackageName)' # Optional
|
||||
#patterns: '**' # Optional
|
||||
path: '$(System.ArtifactsDirectory)/new-artifacts'
|
||||
#project: # Required when source == Specific
|
||||
#pipeline: # Required when source == Specific
|
||||
runVersion: 'latest' # Required when source == Specific. Options: latest, latestFromBranch, specific
|
||||
#runBranch: 'refs/heads/master' # Required when source == Specific && runVersion == LatestFromBranch
|
||||
#runId: # Required when source == Specific && runVersion == Specific
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Artifact Assembly to new-release folder
|
||||
displayName: Copy New Assembly to new-release folder
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
|
||||
contents: '**/*.dll'
|
||||
@ -165,10 +145,35 @@ jobs:
|
||||
overWrite: true # Optional
|
||||
flattenFolders: true # Optional
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download the Reference Assembly Build Artifact
|
||||
inputs:
|
||||
source: 'specific' # Options: current, specific
|
||||
#preferTriggeringPipeline: false # Optional
|
||||
#tags: # Optional
|
||||
artifact: '$(NugetPackageName)' # Optional
|
||||
#patterns: '**' # Optional
|
||||
path: '$(System.ArtifactsDirectory)/current-artifacts'
|
||||
project: '$(System.TeamProjectId)' # Required when source == Specific
|
||||
pipeline: '$(System.DefinitionId)' # Required when source == Specific
|
||||
runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
|
||||
runBranch: 'refs/heads/$(System.PullRequest.TargetBranch)' # Required when source == Specific && runVersion == LatestFromBranch
|
||||
#runId: # Required when source == Specific && runVersion == Specific
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy Reference Assembly to current-release folder
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional
|
||||
contents: '**/*.dll'
|
||||
targetFolder: $(System.ArtifactsDirectory)/current-release
|
||||
cleanTargetFolder: true # Optional
|
||||
overWrite: true # Optional
|
||||
flattenFolders: true # Optional
|
||||
|
||||
- task: DownloadGitHubRelease@0
|
||||
displayName: Download ABI compatibility check tool from GitHub
|
||||
inputs:
|
||||
connection: Jellyfin GitHub
|
||||
connection: Jellyfin Release Download
|
||||
userRepository: EraYaN/dotnet-compatibility
|
||||
defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
|
||||
#version: # Required when defaultVersionType != Latest
|
||||
@ -185,7 +190,7 @@ jobs:
|
||||
- task: CmdLine@2
|
||||
displayName: Execute ABI compatibility check tool
|
||||
inputs:
|
||||
script: 'dotnet tools/CompatibilityCheckerCoreCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName)'
|
||||
script: 'dotnet tools/CompatibilityCheckerCoreCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines'
|
||||
workingDirectory: $(System.ArtifactsDirectory) # Optional
|
||||
#failOnStderr: false # Optional
|
||||
|
||||
|
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
CONTRIBUTORS.md merge=union
|
||||
|
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -30,6 +30,7 @@ assignees: ''
|
||||
- OS: [e.g. Docker, Debian, Windows]
|
||||
- Browser: [e.g. Firefox, Chrome, Safari]
|
||||
- Jellyfin Version: [e.g. 10.0.1]
|
||||
- Reverse proxy: [e.g. no, nginx, apache, etc.]
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
|
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
20
.github/ISSUE_TEMPLATE/enhancement-request.md
vendored
@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Enhancement request
|
||||
about: Suggest an modification to an existing feature
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,14 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new feature
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the feature you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
32
.github/ISSUE_TEMPLATE/media_playback.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/media_playback.md
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Media playback issue
|
||||
about: Create a media playback issue report
|
||||
title: ''
|
||||
labels: mediaplayback
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Media Info of the file**
|
||||
<!-- Use the Media Info tool (set to text format, download here: https://mediaarea.net/en/MediaInfo) or copy the info from the web ui for the file with the playback issue. -->
|
||||
|
||||
**Logs**
|
||||
<!-- Please paste any log message from during the playback issue, for example the ffmpeg command line can be very useful. -->
|
||||
|
||||
**Stats for Nerds Screenshots**
|
||||
<!-- If available, add screenshots of the stats for nerds screen to help show the issue problem. -->
|
||||
|
||||
**Server System (please complete the following information):**
|
||||
- OS: [e.g. Docker on Linux, Docker on Windows, Debian, Windows]
|
||||
- Jellyfin Version: [e.g. 10.0.1]
|
||||
- Hardware settings & device: [e.g. NVENC on GTX1060, VAAPI on Intel i7 8700K]
|
||||
- Reverse proxy: [e.g. no, nginx, apache, etc.]
|
||||
- Other hardware notes: [e.g. Media mounted in CIFS/SMB share, Media mounted from Google Drive]
|
||||
|
||||
**Client System (please complete the following information):**
|
||||
- Device: [e.g. Apple iPhone XS, Xbox One S, LG OLED55C8, Samsung Galaxy Note9, Custom HTPC]
|
||||
- OS: [e.g. iOS, Android, Windows, macOS]
|
||||
- Client: [e.g. Web/Browser, webOS, Android, Android TV, Electron]
|
||||
- Browser (if Web client): [e.g. Firefox, Chrome, Safari]
|
||||
- Client and Browser Version: [e.g. 10.3.4 and 68.0]
|
||||
|
22
.github/stale.yml
vendored
Normal file
22
.github/stale.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- regression
|
||||
- security
|
||||
- dotnet-3.0-future
|
||||
- roadmap
|
||||
- future
|
||||
- feature
|
||||
- enhancement
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
Issues go stale after 60d of inactivity. Mark the issue as fresh by adding a comment or commit. Stale issues close after an additional 7d of inactivity.
|
||||
If this issue is safe to close now please do so.
|
||||
If you have any questions you can reach us on [Matrix or Social Media](https://jellyfin.readthedocs.io/en/latest/getting-help/).
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -239,11 +239,6 @@ pip-log.txt
|
||||
##########
|
||||
.idea/
|
||||
|
||||
##########
|
||||
# Visual Studio Code
|
||||
##########
|
||||
.vscode/
|
||||
|
||||
#########################
|
||||
# Build artifacts
|
||||
#########################
|
||||
@ -266,3 +261,6 @@ deployment/collect-dist/
|
||||
jellyfin_version.ini
|
||||
|
||||
ci/
|
||||
|
||||
# Doxygen
|
||||
doc/
|
||||
|
@ -23,7 +23,11 @@
|
||||
- [fruhnow](https://github.com/fruhnow)
|
||||
- [Lynxy](https://github.com/Lynxy)
|
||||
- [fasheng](https://github.com/fasheng)
|
||||
- [ploughpuff](https://github.com/ploughpuff)
|
||||
- [ploughpuff](https://github.com/ploughpuff)
|
||||
- [pjeanjean](https://github.com/pjeanjean)
|
||||
- [DrPandemic](https://github.com/drpandemic)
|
||||
- [joern-h](https://github.com/joern-h)
|
||||
- [Khinenw](https://github.com/HelloWorld017)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
10
Dockerfile
10
Dockerfile
@ -1,6 +1,6 @@
|
||||
ARG DOTNET_VERSION=2
|
||||
ARG DOTNET_VERSION=2.2
|
||||
|
||||
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk as builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
@ -8,7 +8,7 @@ RUN bash -c "source deployment/common.build.sh && \
|
||||
build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin"
|
||||
|
||||
FROM jellyfin/ffmpeg as ffmpeg
|
||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime
|
||||
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}
|
||||
# libfontconfig1 is required for Skia
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||
@ -21,8 +21,8 @@ RUN apt-get update \
|
||||
COPY --from=ffmpeg / /
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
|
||||
ARG JELLYFIN_WEB_VERSION=10.2.2
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
ARG JELLYFIN_WEB_VERSION=v10.3.7
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
||||
|
@ -3,12 +3,7 @@
|
||||
ARG DOTNET_VERSION=3.0
|
||||
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-arm as qemu
|
||||
FROM alpine as qemu_extract
|
||||
COPY --from=qemu /usr/bin qemu-arm-static.tar.gz
|
||||
RUN tar -xzvf qemu-arm-static.tar.gz
|
||||
|
||||
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch as builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
@ -21,8 +16,9 @@ RUN bash -c "source deployment/common.build.sh && \
|
||||
build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
|
||||
|
||||
|
||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7
|
||||
COPY --from=qemu_extract qemu-arm-static /usr/bin
|
||||
FROM multiarch/qemu-user-static:x86_64-arm as qemu
|
||||
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7
|
||||
COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
@ -30,8 +26,8 @@ RUN apt-get update \
|
||||
&& chmod 777 /cache /config /media
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
|
||||
ARG JELLYFIN_WEB_VERSION=10.2.2
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
ARG JELLYFIN_WEB_VERSION=v10.3.7
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
||||
|
@ -3,13 +3,7 @@
|
||||
ARG DOTNET_VERSION=3.0
|
||||
|
||||
|
||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||
FROM alpine as qemu_extract
|
||||
COPY --from=qemu /usr/bin qemu-aarch64-static.tar.gz
|
||||
RUN tar -xzvf qemu-aarch64-static.tar.gz
|
||||
|
||||
|
||||
FROM microsoft/dotnet:${DOTNET_VERSION}-sdk-stretch as builder
|
||||
FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
|
||||
WORKDIR /repo
|
||||
COPY . .
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
@ -22,8 +16,9 @@ RUN bash -c "source deployment/common.build.sh && \
|
||||
build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
|
||||
|
||||
|
||||
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8
|
||||
COPY --from=qemu_extract qemu-aarch64-static /usr/bin
|
||||
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
|
||||
FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8
|
||||
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
@ -31,8 +26,8 @@ RUN apt-get update \
|
||||
&& chmod 777 /cache /config /media
|
||||
COPY --from=builder /jellyfin /jellyfin
|
||||
|
||||
ARG JELLYFIN_WEB_VERSION=10.2.2
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
ARG JELLYFIN_WEB_VERSION=v10.3.7
|
||||
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
&& rm -rf /jellyfin/jellyfin-web \
|
||||
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web
|
||||
|
||||
|
@ -181,19 +181,6 @@ namespace Emby.Dlna.Didl
|
||||
writer.WriteFullEndElement();
|
||||
}
|
||||
|
||||
private string GetMimeType(string input)
|
||||
{
|
||||
var mime = MimeTypes.GetMimeType(input);
|
||||
|
||||
// TODO: Instead of being hard-coded here, this should probably be moved into all of the existing profiles
|
||||
if (string.Equals(mime, "video/mp2t", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mime = "video/mpeg";
|
||||
}
|
||||
|
||||
return mime;
|
||||
}
|
||||
|
||||
private void AddVideoResource(DlnaOptions options, XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
||||
{
|
||||
if (streamInfo == null)
|
||||
@ -384,7 +371,7 @@ namespace Emby.Dlna.Didl
|
||||
var filename = url.Substring(0, url.IndexOf('?'));
|
||||
|
||||
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
|
||||
? GetMimeType(filename)
|
||||
? MimeTypes.GetMimeType(filename)
|
||||
: mediaProfile.MimeType;
|
||||
|
||||
writer.WriteAttributeString("protocolInfo", string.Format(
|
||||
@ -520,7 +507,7 @@ namespace Emby.Dlna.Didl
|
||||
var filename = url.Substring(0, url.IndexOf('?'));
|
||||
|
||||
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
|
||||
? GetMimeType(filename)
|
||||
? MimeTypes.GetMimeType(filename)
|
||||
: mediaProfile.MimeType;
|
||||
|
||||
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container,
|
||||
@ -545,17 +532,10 @@ namespace Emby.Dlna.Didl
|
||||
}
|
||||
|
||||
public static bool IsIdRoot(string id)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id)
|
||||
=> string.IsNullOrWhiteSpace(id)
|
||||
|| string.Equals(id, "0", StringComparison.OrdinalIgnoreCase)
|
||||
// Samsung sometimes uses 1 as root
|
||||
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
|
||||
{
|
||||
@ -920,8 +900,6 @@ namespace Emby.Dlna.Didl
|
||||
}
|
||||
}
|
||||
|
||||
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
|
||||
|
||||
if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG");
|
||||
@ -930,6 +908,9 @@ namespace Emby.Dlna.Didl
|
||||
AddImageResElement(item, writer, 4096, 4096, "png", "PNG_LRG");
|
||||
AddImageResElement(item, writer, 160, 160, "png", "PNG_TN");
|
||||
}
|
||||
|
||||
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
|
||||
|
||||
}
|
||||
|
||||
private void AddEmbeddedImageAsCover(string name, XmlWriter writer)
|
||||
@ -970,7 +951,7 @@ namespace Emby.Dlna.Didl
|
||||
|
||||
writer.WriteAttributeString("protocolInfo", string.Format(
|
||||
"http-get:*:{0}:{1}",
|
||||
GetMimeType("file." + format),
|
||||
MimeTypes.GetMimeType("file." + format),
|
||||
contentFeatures
|
||||
));
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 13 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.2 KiB |
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.PlayTo;
|
||||
@ -247,7 +248,7 @@ namespace Emby.Dlna.Main
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
if (address.AddressFamily == IpAddressFamily.InterNetworkV6)
|
||||
if (address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
// Not support IPv6 right now
|
||||
continue;
|
||||
|
@ -102,9 +102,10 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
_sessionManager.ReportSessionEnded(_session.Id);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Could throw if the session is already gone
|
||||
_logger.LogError(ex, "Error reporting the end of session {Id}", _session.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,20 +113,14 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var info = e.Argument;
|
||||
|
||||
info.Headers.TryGetValue("NTS", out string nts);
|
||||
|
||||
if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty;
|
||||
|
||||
if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty;
|
||||
|
||||
if (usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 &&
|
||||
!_disposed)
|
||||
if (!_disposed
|
||||
&& info.Headers.TryGetValue("USN", out string usn)
|
||||
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| (info.Headers.TryGetValue("NT", out string nt)
|
||||
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
|
||||
{
|
||||
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
OnDeviceUnavailable();
|
||||
}
|
||||
OnDeviceUnavailable();
|
||||
}
|
||||
}
|
||||
|
||||
@ -612,22 +607,34 @@ namespace Emby.Dlna.PlayTo
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
|
||||
_device.PlaybackStart -= _device_PlaybackStart;
|
||||
_device.PlaybackProgress -= _device_PlaybackProgress;
|
||||
_device.PlaybackStopped -= _device_PlaybackStopped;
|
||||
_device.MediaChanged -= _device_MediaChanged;
|
||||
//_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
|
||||
_device.OnDeviceUnavailable = null;
|
||||
|
||||
_device.Dispose();
|
||||
}
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_device.Dispose();
|
||||
}
|
||||
|
||||
_device.PlaybackStart -= _device_PlaybackStart;
|
||||
_device.PlaybackProgress -= _device_PlaybackProgress;
|
||||
_device.PlaybackStopped -= _device_PlaybackStopped;
|
||||
_device.MediaChanged -= _device_MediaChanged;
|
||||
_deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft;
|
||||
_device.OnDeviceUnavailable = null;
|
||||
_device = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
|
||||
|
||||
private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
@ -14,7 +15,6 @@ using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -172,7 +172,7 @@ namespace Emby.Dlna.PlayTo
|
||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||
|
||||
string serverAddress;
|
||||
if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IpAddressInfo.Any) || info.LocalIpAddress.Equals(IpAddressInfo.IPv6Any))
|
||||
if (info.LocalIpAddress == null || info.LocalIpAddress.Equals(IPAddress.Any) || info.LocalIpAddress.Equals(IPAddress.IPv6Any))
|
||||
{
|
||||
serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
@ -34,16 +34,13 @@ namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
var cancellationToken = CancellationToken.None;
|
||||
|
||||
using (var response = await PostSoapDataAsync(NormalizeServiceUrl(baseUrl, service.ControlUrl), "\"" + service.ServiceType + "#" + command + "\"", postData, header, logRequest, cancellationToken)
|
||||
var url = NormalizeServiceUrl(baseUrl, service.ControlUrl);
|
||||
using (var response = await PostSoapDataAsync(url, '\"' + service.ServiceType + '#' + command + '\"', postData, header, logRequest, cancellationToken)
|
||||
.ConfigureAwait(false))
|
||||
using (var stream = response.Content)
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
using (var stream = response.Content)
|
||||
{
|
||||
using (var reader = new StreamReader(stream, Encoding.UTF8))
|
||||
{
|
||||
return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
}
|
||||
return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,15 +118,18 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
}
|
||||
|
||||
private Task<HttpResponseInfo> PostSoapDataAsync(string url,
|
||||
private Task<HttpResponseInfo> PostSoapDataAsync(
|
||||
string url,
|
||||
string soapAction,
|
||||
string postData,
|
||||
string header,
|
||||
bool logRequest,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!soapAction.StartsWith("\""))
|
||||
soapAction = "\"" + soapAction + "\"";
|
||||
if (soapAction[0] != '\"')
|
||||
{
|
||||
soapAction = '\"' + soapAction + '\"';
|
||||
}
|
||||
|
||||
var options = new HttpRequestOptions
|
||||
{
|
||||
@ -155,7 +155,6 @@ namespace Emby.Dlna.PlayTo
|
||||
}
|
||||
|
||||
options.RequestContentType = "text/xml";
|
||||
options.AppendCharsetToMimeType = true;
|
||||
options.RequestContent = postData;
|
||||
|
||||
return _httpClient.Post(options);
|
||||
|
@ -9,7 +9,7 @@ namespace Emby.Dlna.Profiles
|
||||
{
|
||||
Name = "Dish Hopper-Joey";
|
||||
|
||||
ProtocolInfo = "http-get:*:video/mp2t:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*";
|
||||
ProtocolInfo = "http-get:*:video/mp2t:*,http-get:*:video/mpeg:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*";
|
||||
|
||||
Identification = new DeviceIdentification
|
||||
{
|
||||
|
@ -28,7 +28,7 @@
|
||||
<MaxStaticBitrate>140000000</MaxStaticBitrate>
|
||||
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
|
||||
<MaxStaticMusicBitrate xsi:nil="true" />
|
||||
<ProtocolInfo>http-get:*:video/mp2t:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*</ProtocolInfo>
|
||||
<ProtocolInfo>http-get:*:video/mp2t:http-get:*:video/mpeg:*,http-get:*:video/MP1S:*,http-get:*:video/mpeg2:*,http-get:*:video/mp4:*,http-get:*:video/x-matroska:*,http-get:*:audio/mpeg:*,http-get:*:audio/mpeg3:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/mp4a-latm:*,http-get:*:image/jpeg:*</ProtocolInfo>
|
||||
<TimelineOffsetSeconds>0</TimelineOffsetSeconds>
|
||||
<RequiresPlainVideoItems>false</RequiresPlainVideoItems>
|
||||
<RequiresPlainFolders>false</RequiresPlainFolders>
|
||||
|
@ -33,27 +33,29 @@ namespace Emby.Naming.Audio
|
||||
|
||||
// Normalize
|
||||
// Remove whitespace
|
||||
filename = filename.Replace("-", " ");
|
||||
filename = filename.Replace(".", " ");
|
||||
filename = filename.Replace("(", " ");
|
||||
filename = filename.Replace(")", " ");
|
||||
filename = filename.Replace('-', ' ');
|
||||
filename = filename.Replace('.', ' ');
|
||||
filename = filename.Replace('(', ' ');
|
||||
filename = filename.Replace(')', ' ');
|
||||
filename = Regex.Replace(filename, @"\s+", " ");
|
||||
|
||||
filename = filename.TrimStart();
|
||||
|
||||
foreach (var prefix in _options.AlbumStackingPrefixes)
|
||||
{
|
||||
if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) != 0)
|
||||
{
|
||||
var tmp = filename.Substring(prefix.Length);
|
||||
continue;
|
||||
}
|
||||
|
||||
tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty;
|
||||
var tmp = filename.Substring(prefix.Length);
|
||||
|
||||
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
result.IsMultiPart = true;
|
||||
break;
|
||||
}
|
||||
tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty;
|
||||
|
||||
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
|
||||
{
|
||||
result.IsMultiPart = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,11 +7,13 @@ namespace Emby.Naming.Audio
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the part.
|
||||
/// </summary>
|
||||
/// <value>The part.</value>
|
||||
public string Part { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is multi part.
|
||||
/// </summary>
|
||||
|
@ -12,35 +12,56 @@ namespace Emby.Naming.AudioBook
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the container.
|
||||
/// </summary>
|
||||
/// <value>The container.</value>
|
||||
public string Container { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the part number.
|
||||
/// </summary>
|
||||
/// <value>The part number.</value>
|
||||
public int? PartNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the chapter number.
|
||||
/// </summary>
|
||||
/// <value>The chapter number.</value>
|
||||
public int? ChapterNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type.
|
||||
/// </summary>
|
||||
/// <value>The type.</value>
|
||||
public bool IsDirectory { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int CompareTo(AudioBookFileInfo other)
|
||||
{
|
||||
if (ReferenceEquals(this, other)) return 0;
|
||||
if (ReferenceEquals(null, other)) return 1;
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(null, other))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var chapterNumberComparison = Nullable.Compare(ChapterNumber, other.ChapterNumber);
|
||||
if (chapterNumberComparison != 0) return chapterNumberComparison;
|
||||
if (chapterNumberComparison != 0)
|
||||
{
|
||||
return chapterNumberComparison;
|
||||
}
|
||||
|
||||
var partNumberComparison = Nullable.Compare(PartNumber, other.PartNumber);
|
||||
if (partNumberComparison != 0) return partNumberComparison;
|
||||
if (partNumberComparison != 0)
|
||||
{
|
||||
return partNumberComparison;
|
||||
}
|
||||
|
||||
return string.Compare(Path, other.Path, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
@ -14,14 +15,13 @@ namespace Emby.Naming.AudioBook
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public AudioBookFilePathParserResult Parse(string path, bool IsDirectory)
|
||||
public AudioBookFilePathParserResult Parse(string path)
|
||||
{
|
||||
var result = Parse(path);
|
||||
return !result.Success ? new AudioBookFilePathParserResult() : result;
|
||||
}
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
private AudioBookFilePathParserResult Parse(string path)
|
||||
{
|
||||
var result = new AudioBookFilePathParserResult();
|
||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
foreach (var expression in _options.AudioBookPartsExpressions)
|
||||
@ -40,6 +40,7 @@ namespace Emby.Naming.AudioBook
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.PartNumber.HasValue)
|
||||
{
|
||||
var value = match.Groups["part"];
|
||||
|
@ -3,7 +3,9 @@ namespace Emby.Naming.AudioBook
|
||||
public class AudioBookFilePathParserResult
|
||||
{
|
||||
public int? PartNumber { get; set; }
|
||||
|
||||
public int? ChapterNumber { get; set; }
|
||||
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -7,33 +7,40 @@ namespace Emby.Naming.AudioBook
|
||||
/// </summary>
|
||||
public class AudioBookInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
public int? Year { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the files.
|
||||
/// </summary>
|
||||
/// <value>The files.</value>
|
||||
public List<AudioBookFileInfo> Files { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the extras.
|
||||
/// </summary>
|
||||
/// <value>The extras.</value>
|
||||
public List<AudioBookFileInfo> Extras { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the alternate versions.
|
||||
/// </summary>
|
||||
/// <value>The alternate versions.</value>
|
||||
public List<AudioBookFileInfo> AlternateVersions { get; set; }
|
||||
|
||||
public AudioBookInfo()
|
||||
{
|
||||
Files = new List<AudioBookFileInfo>();
|
||||
Extras = new List<AudioBookFileInfo>();
|
||||
AlternateVersions = new List<AudioBookFileInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the year.
|
||||
/// </summary>
|
||||
public int? Year { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the files.
|
||||
/// </summary>
|
||||
/// <value>The files.</value>
|
||||
public List<AudioBookFileInfo> Files { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extras.
|
||||
/// </summary>
|
||||
/// <value>The extras.</value>
|
||||
public List<AudioBookFileInfo> Extras { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alternate versions.
|
||||
/// </summary>
|
||||
/// <value>The alternate versions.</value>
|
||||
public List<AudioBookFileInfo> AlternateVersions { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ namespace Emby.Naming.AudioBook
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public IEnumerable<AudioBookInfo> Resolve(List<FileSystemMetadata> files)
|
||||
public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
|
||||
{
|
||||
var audioBookResolver = new AudioBookResolver(_options);
|
||||
|
||||
|
@ -24,19 +24,21 @@ namespace Emby.Naming.AudioBook
|
||||
return Resolve(path, true);
|
||||
}
|
||||
|
||||
public AudioBookFileInfo Resolve(string path, bool IsDirectory = false)
|
||||
public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
if (IsDirectory) // TODO
|
||||
// TODO
|
||||
if (isDirectory)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
|
||||
// Check supported extensions
|
||||
if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
@ -45,8 +47,7 @@ namespace Emby.Naming.AudioBook
|
||||
|
||||
var container = extension.TrimStart('.');
|
||||
|
||||
var parsingResult = new AudioBookFilePathParser(_options)
|
||||
.Parse(path, IsDirectory);
|
||||
var parsingResult = new AudioBookFilePathParser(_options).Parse(path);
|
||||
|
||||
return new AudioBookFileInfo
|
||||
{
|
||||
@ -54,7 +55,7 @@ namespace Emby.Naming.AudioBook
|
||||
Container = container,
|
||||
PartNumber = parsingResult.PartNumber,
|
||||
ChapterNumber = parsingResult.ChapterNumber,
|
||||
IsDirectory = IsDirectory
|
||||
IsDirectory = isDirectory
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -6,17 +6,28 @@ namespace Emby.Naming.Common
|
||||
public class EpisodeExpression
|
||||
{
|
||||
private string _expression;
|
||||
public string Expression { get => _expression;
|
||||
set { _expression = value; _regex = null; } }
|
||||
private Regex _regex;
|
||||
|
||||
public string Expression
|
||||
{
|
||||
get => _expression;
|
||||
set
|
||||
{
|
||||
_expression = value;
|
||||
_regex = null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsByDate { get; set; }
|
||||
|
||||
public bool IsOptimistic { get; set; }
|
||||
|
||||
public bool IsNamed { get; set; }
|
||||
|
||||
public bool SupportsAbsoluteEpisodeNumbers { get; set; }
|
||||
|
||||
public string[] DateTimeFormats { get; set; }
|
||||
|
||||
private Regex _regex;
|
||||
public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled));
|
||||
|
||||
public EpisodeExpression(string expression, bool byDate)
|
||||
|
@ -6,10 +6,12 @@ namespace Emby.Naming.Common
|
||||
/// The audio
|
||||
/// </summary>
|
||||
Audio = 0,
|
||||
|
||||
/// <summary>
|
||||
/// The photo
|
||||
/// </summary>
|
||||
Photo = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The video
|
||||
/// </summary>
|
||||
|
@ -8,19 +8,25 @@ namespace Emby.Naming.Common
|
||||
public class NamingOptions
|
||||
{
|
||||
public string[] AudioFileExtensions { get; set; }
|
||||
|
||||
public string[] AlbumStackingPrefixes { get; set; }
|
||||
|
||||
public string[] SubtitleFileExtensions { get; set; }
|
||||
|
||||
public char[] SubtitleFlagDelimiters { get; set; }
|
||||
|
||||
public string[] SubtitleForcedFlags { get; set; }
|
||||
|
||||
public string[] SubtitleDefaultFlags { get; set; }
|
||||
|
||||
public EpisodeExpression[] EpisodeExpressions { get; set; }
|
||||
|
||||
public string[] EpisodeWithoutSeasonExpressions { get; set; }
|
||||
|
||||
public string[] EpisodeMultiPartExpressions { get; set; }
|
||||
|
||||
public string[] VideoFileExtensions { get; set; }
|
||||
|
||||
public string[] StubFileExtensions { get; set; }
|
||||
|
||||
public string[] AudioBookPartsExpressions { get; set; }
|
||||
@ -28,12 +34,14 @@ namespace Emby.Naming.Common
|
||||
public StubTypeRule[] StubTypes { get; set; }
|
||||
|
||||
public char[] VideoFlagDelimiters { get; set; }
|
||||
|
||||
public Format3DRule[] Format3DRules { get; set; }
|
||||
|
||||
public string[] VideoFileStackingExpressions { get; set; }
|
||||
public string[] CleanDateTimes { get; set; }
|
||||
public string[] CleanStrings { get; set; }
|
||||
|
||||
public string[] CleanDateTimes { get; set; }
|
||||
|
||||
public string[] CleanStrings { get; set; }
|
||||
|
||||
public EpisodeExpression[] MultipleEpisodeExpressions { get; set; }
|
||||
|
||||
@ -41,7 +49,7 @@ namespace Emby.Naming.Common
|
||||
|
||||
public NamingOptions()
|
||||
{
|
||||
VideoFileExtensions = new string[]
|
||||
VideoFileExtensions = new[]
|
||||
{
|
||||
".m4v",
|
||||
".3gp",
|
||||
@ -106,53 +114,53 @@ namespace Emby.Naming.Common
|
||||
{
|
||||
new StubTypeRule
|
||||
{
|
||||
StubType = "dvd",
|
||||
Token = "dvd"
|
||||
StubType = "dvd",
|
||||
Token = "dvd"
|
||||
},
|
||||
new StubTypeRule
|
||||
{
|
||||
StubType = "hddvd",
|
||||
Token = "hddvd"
|
||||
StubType = "hddvd",
|
||||
Token = "hddvd"
|
||||
},
|
||||
new StubTypeRule
|
||||
{
|
||||
StubType = "bluray",
|
||||
Token = "bluray"
|
||||
StubType = "bluray",
|
||||
Token = "bluray"
|
||||
},
|
||||
new StubTypeRule
|
||||
{
|
||||
StubType = "bluray",
|
||||
Token = "brrip"
|
||||
StubType = "bluray",
|
||||
Token = "brrip"
|
||||
},
|
||||
new StubTypeRule
|
||||
{
|
||||
StubType = "bluray",
|
||||
Token = "bd25"
|
||||
StubType = "bluray",
|
||||
Token = "bd25"
|
||||
},
|
||||
new StubTypeRule
|
||||
{
|
||||
StubType = "bluray",
|
||||
Token = "bd50"
|
||||
StubType = "bluray",
|
||||
Token = "bd50"
|
||||
},
|
||||
new StubTypeRule
|
||||
{
|
||||
StubType = "vhs",
|
||||
Token = "vhs"
|
||||
StubType = "vhs",
|
||||
Token = "vhs"
|
||||
},
|
||||
new StubTypeRule
|
||||
{
|
||||
StubType = "tv",
|
||||
Token = "HDTV"
|
||||
StubType = "tv",
|
||||
Token = "HDTV"
|
||||
},
|
||||
new StubTypeRule
|
||||
{
|
||||
StubType = "tv",
|
||||
Token = "PDTV"
|
||||
StubType = "tv",
|
||||
Token = "PDTV"
|
||||
},
|
||||
new StubTypeRule
|
||||
{
|
||||
StubType = "tv",
|
||||
Token = "DSR"
|
||||
StubType = "tv",
|
||||
Token = "DSR"
|
||||
}
|
||||
};
|
||||
|
||||
@ -286,7 +294,7 @@ namespace Emby.Naming.Common
|
||||
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
|
||||
new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true)
|
||||
{
|
||||
DateTimeFormats = new []
|
||||
DateTimeFormats = new[]
|
||||
{
|
||||
"yyyy.MM.dd",
|
||||
"yyyy-MM-dd",
|
||||
@ -295,7 +303,7 @@ namespace Emby.Naming.Common
|
||||
},
|
||||
new EpisodeExpression("([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})", true)
|
||||
{
|
||||
DateTimeFormats = new []
|
||||
DateTimeFormats = new[]
|
||||
{
|
||||
"dd.MM.yyyy",
|
||||
"dd-MM-yyyy",
|
||||
@ -348,9 +356,7 @@ namespace Emby.Naming.Common
|
||||
},
|
||||
|
||||
// "1-12 episode title"
|
||||
new EpisodeExpression(@"([0-9]+)-([0-9]+)")
|
||||
{
|
||||
},
|
||||
new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
|
||||
|
||||
// "01 - blah.avi", "01-blah.avi"
|
||||
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$")
|
||||
@ -427,7 +433,7 @@ namespace Emby.Naming.Common
|
||||
Token = "_trailer",
|
||||
MediaType = MediaType.Video
|
||||
},
|
||||
new ExtraRule
|
||||
new ExtraRule
|
||||
{
|
||||
ExtraType = "trailer",
|
||||
RuleType = ExtraRuleType.Suffix,
|
||||
@ -462,7 +468,7 @@ namespace Emby.Naming.Common
|
||||
Token = "_sample",
|
||||
MediaType = MediaType.Video
|
||||
},
|
||||
new ExtraRule
|
||||
new ExtraRule
|
||||
{
|
||||
ExtraType = "sample",
|
||||
RuleType = ExtraRuleType.Suffix,
|
||||
@ -476,7 +482,6 @@ namespace Emby.Naming.Common
|
||||
Token = "theme",
|
||||
MediaType = MediaType.Audio
|
||||
},
|
||||
|
||||
new ExtraRule
|
||||
{
|
||||
ExtraType = "scene",
|
||||
@ -526,8 +531,8 @@ namespace Emby.Naming.Common
|
||||
Token = "-short",
|
||||
MediaType = MediaType.Video
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Format3DRules = new[]
|
||||
{
|
||||
// Kodi rules:
|
||||
@ -648,12 +653,10 @@ namespace Emby.Naming.Common
|
||||
@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
|
||||
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$"
|
||||
|
||||
}.Select(i => new EpisodeExpression(i)
|
||||
{
|
||||
IsNamed = true
|
||||
|
||||
}).ToArray();
|
||||
{
|
||||
IsNamed = true
|
||||
}).ToArray();
|
||||
|
||||
VideoFileExtensions = extensions
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
@ -10,7 +10,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@ -18,6 +18,18 @@
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code analysers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -5,6 +5,7 @@ namespace Emby.Naming.Extensions
|
||||
{
|
||||
public static class StringExtensions
|
||||
{
|
||||
// TODO: @bond remove this when moving to netstandard2.1
|
||||
public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
@ -1,30 +0,0 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Emby.Naming
|
||||
{
|
||||
internal static class StringExtensions
|
||||
{
|
||||
public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
var previousIndex = 0;
|
||||
var index = str.IndexOf(oldValue, comparison);
|
||||
|
||||
while (index != -1)
|
||||
{
|
||||
sb.Append(str.Substring(previousIndex, index - previousIndex));
|
||||
sb.Append(newValue);
|
||||
index += oldValue.Length;
|
||||
|
||||
previousIndex = index;
|
||||
index = str.IndexOf(oldValue, index, comparison);
|
||||
}
|
||||
|
||||
sb.Append(str.Substring(previousIndex));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
@ -7,16 +7,19 @@ namespace Emby.Naming.Subtitles
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the language.
|
||||
/// </summary>
|
||||
/// <value>The language.</value>
|
||||
public string Language { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is default.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is default; otherwise, <c>false</c>.</value>
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is forced.
|
||||
/// </summary>
|
||||
|
@ -7,31 +7,37 @@ namespace Emby.Naming.TV
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the container.
|
||||
/// </summary>
|
||||
/// <value>The container.</value>
|
||||
public string Container { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the series.
|
||||
/// </summary>
|
||||
/// <value>The name of the series.</value>
|
||||
public string SeriesName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format3 d.
|
||||
/// </summary>
|
||||
/// <value>The format3 d.</value>
|
||||
public string Format3D { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [is3 d].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
|
||||
public bool Is3D { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is stub.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value>
|
||||
public bool IsStub { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the stub.
|
||||
/// </summary>
|
||||
@ -39,12 +45,17 @@ namespace Emby.Naming.TV
|
||||
public string StubType { get; set; }
|
||||
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
public int? EpisodeNumber { get; set; }
|
||||
|
||||
public int? EndingEpsiodeNumber { get; set; }
|
||||
|
||||
public int? Year { get; set; }
|
||||
|
||||
public int? Month { get; set; }
|
||||
|
||||
public int? Day { get; set; }
|
||||
|
||||
public bool IsByDate { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -15,12 +15,12 @@ namespace Emby.Naming.TV
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public EpisodePathParserResult Parse(string path, bool IsDirectory, bool? isNamed = null, bool? isOptimistic = null, bool? supportsAbsoluteNumbers = null, bool fillExtendedInfo = true)
|
||||
public EpisodePathParserResult Parse(string path, bool isDirectory, bool? isNamed = null, bool? isOptimistic = null, bool? supportsAbsoluteNumbers = null, bool fillExtendedInfo = true)
|
||||
{
|
||||
// Added to be able to use regex patterns which require a file extension.
|
||||
// There were no failed tests without this block, but to be safe, we can keep it until
|
||||
// the regex which require file extensions are modified so that they don't need them.
|
||||
if (IsDirectory)
|
||||
if (isDirectory)
|
||||
{
|
||||
path += ".mp4";
|
||||
}
|
||||
@ -29,28 +29,20 @@ namespace Emby.Naming.TV
|
||||
|
||||
foreach (var expression in _options.EpisodeExpressions)
|
||||
{
|
||||
if (supportsAbsoluteNumbers.HasValue)
|
||||
if (supportsAbsoluteNumbers.HasValue
|
||||
&& expression.SupportsAbsoluteEpisodeNumbers != supportsAbsoluteNumbers.Value)
|
||||
{
|
||||
if (expression.SupportsAbsoluteEpisodeNumbers != supportsAbsoluteNumbers.Value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isNamed.HasValue)
|
||||
if (isNamed.HasValue && expression.IsNamed != isNamed.Value)
|
||||
{
|
||||
if (expression.IsNamed != isNamed.Value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isOptimistic.HasValue)
|
||||
if (isOptimistic.HasValue && expression.IsOptimistic != isOptimistic.Value)
|
||||
{
|
||||
if (expression.IsOptimistic != isOptimistic.Value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentResult = Parse(path, expression);
|
||||
@ -97,7 +89,8 @@ namespace Emby.Naming.TV
|
||||
DateTime date;
|
||||
if (expression.DateTimeFormats.Length > 0)
|
||||
{
|
||||
if (DateTime.TryParseExact(match.Groups[0].Value,
|
||||
if (DateTime.TryParseExact(
|
||||
match.Groups[0].Value,
|
||||
expression.DateTimeFormats,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None,
|
||||
@ -109,15 +102,12 @@ namespace Emby.Naming.TV
|
||||
result.Success = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (DateTime.TryParse(match.Groups[0].Value, out date))
|
||||
{
|
||||
if (DateTime.TryParse(match.Groups[0].Value, out date))
|
||||
{
|
||||
result.Year = date.Year;
|
||||
result.Month = date.Month;
|
||||
result.Day = date.Day;
|
||||
result.Success = true;
|
||||
}
|
||||
result.Year = date.Year;
|
||||
result.Month = date.Month;
|
||||
result.Day = date.Day;
|
||||
result.Success = true;
|
||||
}
|
||||
|
||||
// TODO: Only consider success if date successfully parsed?
|
||||
@ -142,7 +132,8 @@ namespace Emby.Naming.TV
|
||||
// or a 'p' or 'i' as what you would get with a pixel resolution specification.
|
||||
// It avoids erroneous parsing of something like "series-s09e14-1080p.mkv" as a multi-episode from E14 to E108
|
||||
int nextIndex = endingNumberGroup.Index + endingNumberGroup.Length;
|
||||
if (nextIndex >= name.Length || "0123456789iIpP".IndexOf(name[nextIndex]) == -1)
|
||||
if (nextIndex >= name.Length
|
||||
|| "0123456789iIpP".IndexOf(name[nextIndex]) == -1)
|
||||
{
|
||||
if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
|
||||
{
|
||||
@ -160,6 +151,7 @@ namespace Emby.Naming.TV
|
||||
{
|
||||
result.SeasonNumber = num;
|
||||
}
|
||||
|
||||
if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
|
||||
{
|
||||
result.EpisodeNumber = num;
|
||||
@ -171,8 +163,11 @@ namespace Emby.Naming.TV
|
||||
// Invalidate match when the season is 200 through 1927 or above 2500
|
||||
// because it is an error unless the TV show is intentionally using false season numbers.
|
||||
// It avoids erroneous parsing of something like "Series Special (1920x1080).mkv" as being season 1920 episode 1080.
|
||||
if (result.SeasonNumber >= 200 && result.SeasonNumber < 1928 || result.SeasonNumber > 2500)
|
||||
if ((result.SeasonNumber >= 200 && result.SeasonNumber < 1928)
|
||||
|| result.SeasonNumber > 2500)
|
||||
{
|
||||
result.Success = false;
|
||||
}
|
||||
|
||||
result.IsByDate = expression.IsByDate;
|
||||
}
|
||||
|
@ -3,14 +3,21 @@ namespace Emby.Naming.TV
|
||||
public class EpisodePathParserResult
|
||||
{
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
public int? EpisodeNumber { get; set; }
|
||||
|
||||
public int? EndingEpsiodeNumber { get; set; }
|
||||
|
||||
public string SeriesName { get; set; }
|
||||
|
||||
public bool Success { get; set; }
|
||||
|
||||
public bool IsByDate { get; set; }
|
||||
|
||||
public int? Year { get; set; }
|
||||
|
||||
public int? Month { get; set; }
|
||||
|
||||
public int? Day { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,13 @@ namespace Emby.Naming.TV
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public EpisodeInfo Resolve(string path, bool IsDirectory, bool? isNamed = null, bool? isOptimistic = null, bool? supportsAbsoluteNumbers = null, bool fillExtendedInfo = true)
|
||||
public EpisodeInfo Resolve(
|
||||
string path,
|
||||
bool isDirectory,
|
||||
bool? isNamed = null,
|
||||
bool? isOptimistic = null,
|
||||
bool? supportsAbsoluteNumbers = null,
|
||||
bool fillExtendedInfo = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
@ -26,7 +32,7 @@ namespace Emby.Naming.TV
|
||||
string container = null;
|
||||
string stubType = null;
|
||||
|
||||
if (!IsDirectory)
|
||||
if (!isDirectory)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
// Check supported extensions
|
||||
@ -52,7 +58,7 @@ namespace Emby.Naming.TV
|
||||
var format3DResult = new Format3DParser(_options).Parse(flags);
|
||||
|
||||
var parsingResult = new EpisodePathParser(_options)
|
||||
.Parse(path, IsDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
||||
.Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
|
||||
|
||||
return new EpisodeInfo
|
||||
{
|
||||
|
@ -3,30 +3,24 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Naming.Extensions;
|
||||
|
||||
namespace Emby.Naming.TV
|
||||
{
|
||||
public class SeasonPathParser
|
||||
{
|
||||
private readonly NamingOptions _options;
|
||||
|
||||
public SeasonPathParser(NamingOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
|
||||
{
|
||||
var result = new SeasonPathParserResult();
|
||||
|
||||
var seasonNumberInfo = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
|
||||
|
||||
result.SeasonNumber = seasonNumberInfo.Item1;
|
||||
result.SeasonNumber = seasonNumberInfo.seasonNumber;
|
||||
|
||||
if (result.SeasonNumber.HasValue)
|
||||
{
|
||||
result.Success = true;
|
||||
result.IsSeasonFolder = seasonNumberInfo.Item2;
|
||||
result.IsSeasonFolder = seasonNumberInfo.isSeasonFolder;
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -35,7 +29,7 @@ namespace Emby.Naming.TV
|
||||
/// <summary>
|
||||
/// A season folder must contain one of these somewhere in the name
|
||||
/// </summary>
|
||||
private static readonly string[] SeasonFolderNames =
|
||||
private static readonly string[] _seasonFolderNames =
|
||||
{
|
||||
"season",
|
||||
"sæson",
|
||||
@ -54,19 +48,23 @@ namespace Emby.Naming.TV
|
||||
/// <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 Tuple<int?, bool> GetSeasonNumberFromPath(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
|
||||
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPath(
|
||||
string path,
|
||||
bool supportSpecialAliases,
|
||||
bool supportNumericSeasonFolders)
|
||||
{
|
||||
var filename = Path.GetFileName(path);
|
||||
var filename = Path.GetFileName(path) ?? string.Empty;
|
||||
|
||||
if (supportSpecialAliases)
|
||||
{
|
||||
if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Tuple<int?, bool>(0, true);
|
||||
return (0, true);
|
||||
}
|
||||
|
||||
if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new Tuple<int?, bool>(0, true);
|
||||
return (0, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,7 +72,7 @@ namespace Emby.Naming.TV
|
||||
{
|
||||
if (int.TryParse(filename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return new Tuple<int?, bool>(val, true);
|
||||
return (val, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,12 +82,12 @@ namespace Emby.Naming.TV
|
||||
|
||||
if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
|
||||
{
|
||||
return new Tuple<int?, bool>(val, true);
|
||||
return (val, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Look for one of the season folder names
|
||||
foreach (var name in SeasonFolderNames)
|
||||
foreach (var name in _seasonFolderNames)
|
||||
{
|
||||
var index = filename.IndexOf(name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@ -107,10 +105,10 @@ namespace Emby.Naming.TV
|
||||
|
||||
var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var resultNumber = parts.Select(GetSeasonNumberFromPart).FirstOrDefault(i => i.HasValue);
|
||||
return new Tuple<int?, bool>(resultNumber, true);
|
||||
return (resultNumber, true);
|
||||
}
|
||||
|
||||
private int? GetSeasonNumberFromPart(string part)
|
||||
private static int? GetSeasonNumberFromPart(string part)
|
||||
{
|
||||
if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@ -132,7 +130,7 @@ namespace Emby.Naming.TV
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
private Tuple<int?, bool> GetSeasonNumberFromPathSubstring(string path)
|
||||
private static (int? seasonNumber, bool isSeasonFolder) GetSeasonNumberFromPathSubstring(string path)
|
||||
{
|
||||
var numericStart = -1;
|
||||
var length = 0;
|
||||
@ -174,10 +172,10 @@ namespace Emby.Naming.TV
|
||||
|
||||
if (numericStart == -1)
|
||||
{
|
||||
return new Tuple<int?, bool>(null, isSeasonFolder);
|
||||
return (null, isSeasonFolder);
|
||||
}
|
||||
|
||||
return new Tuple<int?, bool>(int.Parse(path.Substring(numericStart, length), CultureInfo.InvariantCulture), isSeasonFolder);
|
||||
return (int.Parse(path.Substring(numericStart, length), CultureInfo.InvariantCulture), isSeasonFolder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,13 @@ namespace Emby.Naming.TV
|
||||
/// </summary>
|
||||
/// <value>The season number.</value>
|
||||
public int? SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="SeasonPathParserResult"/> is success.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if success; otherwise, <c>false</c>.</value>
|
||||
public bool Success { get; set; }
|
||||
|
||||
public bool IsSeasonFolder { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,8 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
var extension = Path.GetExtension(name) ?? string.Empty;
|
||||
// Check supported extensions
|
||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) &&
|
||||
!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)
|
||||
&& !_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
// Dummy up a file extension because the expressions will fail without one
|
||||
// This is tricky because we can't just check Path.GetExtension for empty
|
||||
@ -38,7 +38,6 @@ namespace Emby.Naming.Video
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
var result = _options.CleanDateTimeRegexes.Select(i => Clean(name, i))
|
||||
@ -69,14 +68,15 @@ namespace Emby.Naming.Video
|
||||
|
||||
var match = expression.Match(name);
|
||||
|
||||
if (match.Success && match.Groups.Count == 4)
|
||||
if (match.Success
|
||||
&& match.Groups.Count == 4
|
||||
&& match.Groups[1].Success
|
||||
&& match.Groups[2].Success
|
||||
&& int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
|
||||
{
|
||||
if (match.Groups[1].Success && match.Groups[2].Success && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
|
||||
{
|
||||
name = match.Groups[1].Value;
|
||||
result.Year = year;
|
||||
result.HasChanged = true;
|
||||
}
|
||||
name = match.Groups[1].Value;
|
||||
result.Year = year;
|
||||
result.HasChanged = true;
|
||||
}
|
||||
|
||||
result.Name = name;
|
||||
|
@ -56,7 +56,6 @@ namespace Emby.Naming.Video
|
||||
result.Rule = rule;
|
||||
}
|
||||
}
|
||||
|
||||
else if (rule.RuleType == ExtraRuleType.Suffix)
|
||||
{
|
||||
var filename = Path.GetFileNameWithoutExtension(path);
|
||||
@ -67,7 +66,6 @@ namespace Emby.Naming.Video
|
||||
result.Rule = rule;
|
||||
}
|
||||
}
|
||||
|
||||
else if (rule.RuleType == ExtraRuleType.Regex)
|
||||
{
|
||||
var filename = Path.GetFileName(path);
|
||||
|
@ -15,9 +15,9 @@ namespace Emby.Naming.Video
|
||||
Files = new List<string>();
|
||||
}
|
||||
|
||||
public bool ContainsFile(string file, bool IsDirectory)
|
||||
public bool ContainsFile(string file, bool isDirectory)
|
||||
{
|
||||
if (IsDirectoryStack == IsDirectory)
|
||||
if (IsDirectoryStack == isDirectory)
|
||||
{
|
||||
return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
@ -15,10 +15,12 @@ namespace Emby.Naming.Video
|
||||
|
||||
public Format3DResult Parse(string path)
|
||||
{
|
||||
var delimeters = _options.VideoFlagDelimiters.ToList();
|
||||
delimeters.Add(' ');
|
||||
int oldLen = _options.VideoFlagDelimiters.Length;
|
||||
var delimeters = new char[oldLen + 1];
|
||||
_options.VideoFlagDelimiters.CopyTo(delimeters, 0);
|
||||
delimeters[oldLen] = ' ';
|
||||
|
||||
return Parse(new FlagParser(_options).GetFlags(path, delimeters.ToArray()));
|
||||
return Parse(new FlagParser(_options).GetFlags(path, delimeters));
|
||||
}
|
||||
|
||||
internal Format3DResult Parse(string[] videoFlags)
|
||||
@ -66,8 +68,10 @@ namespace Emby.Naming.Video
|
||||
format = flag;
|
||||
result.Tokens.Add(rule.Token);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
|
@ -4,25 +4,27 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
public class Format3DResult
|
||||
{
|
||||
public Format3DResult()
|
||||
{
|
||||
Tokens = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [is3 d].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
|
||||
public bool Is3D { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format3 d.
|
||||
/// </summary>
|
||||
/// <value>The format3 d.</value>
|
||||
public string Format3D { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tokens.
|
||||
/// </summary>
|
||||
/// <value>The tokens.</value>
|
||||
public List<string> Tokens { get; set; }
|
||||
|
||||
public Format3DResult()
|
||||
{
|
||||
Tokens = new List<string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,17 +40,24 @@ namespace Emby.Naming.Video
|
||||
var result = new StackResult();
|
||||
foreach (var directory in files.GroupBy(file => file.IsDirectory ? file.FullName : Path.GetDirectoryName(file.FullName)))
|
||||
{
|
||||
var stack = new FileStack();
|
||||
stack.Name = Path.GetFileName(directory.Key);
|
||||
stack.IsDirectoryStack = false;
|
||||
var stack = new FileStack()
|
||||
{
|
||||
Name = Path.GetFileName(directory.Key),
|
||||
IsDirectoryStack = false
|
||||
};
|
||||
foreach (var file in directory)
|
||||
{
|
||||
if (file.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
stack.Files.Add(file.FullName);
|
||||
}
|
||||
|
||||
result.Stacks.Add(stack);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -114,16 +121,16 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (stack.Files.Count == 0)
|
||||
{
|
||||
stack.Name = title1 + ignore1;
|
||||
stack.IsDirectoryStack = file1.IsDirectory;
|
||||
//stack.Name = title1 + ignore1 + extension1;
|
||||
stack.Files.Add(file1.FullName);
|
||||
}
|
||||
|
||||
stack.Files.Add(file2.FullName);
|
||||
}
|
||||
else
|
||||
|
@ -9,24 +9,32 @@ namespace Emby.Naming.Video
|
||||
{
|
||||
public static StubResult ResolveFile(string path, NamingOptions options)
|
||||
{
|
||||
var result = new StubResult();
|
||||
var extension = Path.GetExtension(path) ?? string.Empty;
|
||||
|
||||
if (options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
if (path == null)
|
||||
{
|
||||
result.IsStub = true;
|
||||
return default(StubResult);
|
||||
}
|
||||
|
||||
path = Path.GetFileNameWithoutExtension(path);
|
||||
var extension = Path.GetExtension(path);
|
||||
|
||||
var token = (Path.GetExtension(path) ?? string.Empty).TrimStart('.');
|
||||
if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return default(StubResult);
|
||||
}
|
||||
|
||||
foreach (var rule in options.StubTypes)
|
||||
var result = new StubResult()
|
||||
{
|
||||
IsStub = true
|
||||
};
|
||||
|
||||
path = Path.GetFileNameWithoutExtension(path);
|
||||
var token = Path.GetExtension(path).TrimStart('.');
|
||||
|
||||
foreach (var rule in options.StubTypes)
|
||||
{
|
||||
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.StubType = rule.StubType;
|
||||
break;
|
||||
}
|
||||
result.StubType = rule.StubType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ namespace Emby.Naming.Video
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value>
|
||||
public bool IsStub { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the stub.
|
||||
/// </summary>
|
||||
|
@ -7,6 +7,7 @@ namespace Emby.Naming.Video
|
||||
/// </summary>
|
||||
/// <value>The token.</value>
|
||||
public string Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the stub.
|
||||
/// </summary>
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
namespace Emby.Naming.Video
|
||||
{
|
||||
/// <summary>
|
||||
@ -11,56 +10,67 @@ namespace Emby.Naming.Video
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the container.
|
||||
/// </summary>
|
||||
/// <value>The container.</value>
|
||||
public string Container { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the year.
|
||||
/// </summary>
|
||||
/// <value>The year.</value>
|
||||
public int? Year { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the extra, e.g. trailer, theme song, behing the scenes, etc.
|
||||
/// </summary>
|
||||
/// <value>The type of the extra.</value>
|
||||
public string ExtraType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extra rule.
|
||||
/// </summary>
|
||||
/// <value>The extra rule.</value>
|
||||
public ExtraRule ExtraRule { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format3 d.
|
||||
/// </summary>
|
||||
/// <value>The format3 d.</value>
|
||||
public string Format3D { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [is3 d].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
|
||||
public bool Is3D { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is stub.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value>
|
||||
public bool IsStub { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the stub.
|
||||
/// </summary>
|
||||
/// <value>The type of the stub.</value>
|
||||
public string StubType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type.
|
||||
/// </summary>
|
||||
/// <value>The type.</value>
|
||||
public bool IsDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file name without extension.
|
||||
/// </summary>
|
||||
|
@ -12,21 +12,25 @@ namespace Emby.Naming.Video
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the year.
|
||||
/// </summary>
|
||||
/// <value>The year.</value>
|
||||
public int? Year { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the files.
|
||||
/// </summary>
|
||||
/// <value>The files.</value>
|
||||
public List<VideoFileInfo> Files { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extras.
|
||||
/// </summary>
|
||||
/// <value>The extras.</value>
|
||||
public List<VideoFileInfo> Extras { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alternate versions.
|
||||
/// </summary>
|
||||
|
@ -53,7 +53,7 @@ namespace Emby.Naming.Video
|
||||
Name = stack.Name
|
||||
};
|
||||
|
||||
info.Year = info.Files.First().Year;
|
||||
info.Year = info.Files[0].Year;
|
||||
|
||||
var extraBaseNames = new List<string>
|
||||
{
|
||||
@ -87,7 +87,7 @@ namespace Emby.Naming.Video
|
||||
Name = media.Name
|
||||
};
|
||||
|
||||
info.Year = info.Files.First().Year;
|
||||
info.Year = info.Files[0].Year;
|
||||
|
||||
var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
|
||||
|
||||
@ -115,7 +115,7 @@ namespace Emby.Naming.Video
|
||||
|
||||
if (!string.IsNullOrEmpty(parentPath))
|
||||
{
|
||||
var folderName = Path.GetFileName(Path.GetDirectoryName(videoPath));
|
||||
var folderName = Path.GetFileName(parentPath);
|
||||
if (!string.IsNullOrEmpty(folderName))
|
||||
{
|
||||
var extras = GetExtras(remainingFiles, new List<string> { folderName });
|
||||
@ -163,9 +163,7 @@ namespace Emby.Naming.Video
|
||||
Year = i.Year
|
||||
}));
|
||||
|
||||
var orderedList = list.OrderBy(i => i.Name);
|
||||
|
||||
return orderedList;
|
||||
return list.OrderBy(i => i.Name);
|
||||
}
|
||||
|
||||
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos)
|
||||
@ -179,23 +177,21 @@ namespace Emby.Naming.Video
|
||||
|
||||
var folderName = Path.GetFileName(Path.GetDirectoryName(videos[0].Files[0].Path));
|
||||
|
||||
if (!string.IsNullOrEmpty(folderName) && folderName.Length > 1)
|
||||
if (!string.IsNullOrEmpty(folderName)
|
||||
&& folderName.Length > 1
|
||||
&& videos.All(i => i.Files.Count == 1
|
||||
&& IsEligibleForMultiVersion(folderName, i.Files[0].Path))
|
||||
&& HaveSameYear(videos))
|
||||
{
|
||||
if (videos.All(i => i.Files.Count == 1 && IsEligibleForMultiVersion(folderName, i.Files[0].Path)))
|
||||
{
|
||||
if (HaveSameYear(videos))
|
||||
{
|
||||
var ordered = videos.OrderBy(i => i.Name).ToList();
|
||||
var ordered = videos.OrderBy(i => i.Name).ToList();
|
||||
|
||||
list.Add(ordered[0]);
|
||||
list.Add(ordered[0]);
|
||||
|
||||
list[0].AlternateVersions = ordered.Skip(1).Select(i => i.Files[0]).ToList();
|
||||
list[0].Name = folderName;
|
||||
list[0].Extras.AddRange(ordered.Skip(1).SelectMany(i => i.Extras));
|
||||
list[0].AlternateVersions = ordered.Skip(1).Select(i => i.Files[0]).ToList();
|
||||
list[0].Name = folderName;
|
||||
list[0].Extras.AddRange(ordered.Skip(1).SelectMany(i => i.Extras));
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
return videos;
|
||||
@ -213,9 +209,9 @@ namespace Emby.Naming.Video
|
||||
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
testFilename = testFilename.Substring(folderName.Length).Trim();
|
||||
return string.IsNullOrEmpty(testFilename) ||
|
||||
testFilename.StartsWith("-") ||
|
||||
string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)) ;
|
||||
return string.IsNullOrEmpty(testFilename)
|
||||
|| testFilename[0] == '-'
|
||||
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -38,10 +38,11 @@ namespace Emby.Naming.Video
|
||||
/// Resolves the specified path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="IsDirectory">if set to <c>true</c> [is folder].</param>
|
||||
/// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
|
||||
/// <param name="parseName">Whether or not the name should be parsed for info</param>
|
||||
/// <returns>VideoFileInfo.</returns>
|
||||
/// <exception cref="ArgumentNullException">path</exception>
|
||||
public VideoFileInfo Resolve(string path, bool IsDirectory, bool parseName = true)
|
||||
public VideoFileInfo Resolve(string path, bool isDirectory, bool parseName = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
@ -52,9 +53,10 @@ namespace Emby.Naming.Video
|
||||
string container = null;
|
||||
string stubType = null;
|
||||
|
||||
if (!IsDirectory)
|
||||
if (!isDirectory)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
|
||||
// Check supported extensions
|
||||
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
@ -79,7 +81,7 @@ namespace Emby.Naming.Video
|
||||
|
||||
var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
|
||||
|
||||
var name = IsDirectory
|
||||
var name = isDirectory
|
||||
? Path.GetFileName(path)
|
||||
: Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
@ -108,7 +110,7 @@ namespace Emby.Naming.Video
|
||||
Is3D = format3DResult.Is3D,
|
||||
Format3D = format3DResult.Format3D,
|
||||
ExtraType = extraResult.ExtraType,
|
||||
IsDirectory = IsDirectory,
|
||||
IsDirectory = isDirectory,
|
||||
ExtraRule = extraResult.Rule
|
||||
};
|
||||
}
|
||||
|
@ -20,7 +20,10 @@ namespace Emby.Photos
|
||||
public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private IImageProcessor _imageProcessor;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
// These are causing taglib to hang
|
||||
private string[] _includextensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" };
|
||||
|
||||
public PhotoProvider(ILogger logger, IImageProcessor imageProcessor)
|
||||
{
|
||||
@ -28,75 +31,55 @@ namespace Emby.Photos
|
||||
_imageProcessor = imageProcessor;
|
||||
}
|
||||
|
||||
public string Name => "Embedded Information";
|
||||
|
||||
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
var file = directoryService.GetFile(item.Path);
|
||||
if (file != null && file.LastWriteTimeUtc != item.DateModified)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return (file != null && file.LastWriteTimeUtc != item.DateModified);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// These are causing taglib to hang
|
||||
private string[] _includextensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" };
|
||||
|
||||
public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
item.SetImagePath(ImageType.Primary, item.Path);
|
||||
|
||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||
if (_includextensions.Contains(Path.GetExtension(item.Path) ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
if (_includextensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var file = TagLib.File.Create(item.Path))
|
||||
{
|
||||
var image = file as TagLib.Image.File;
|
||||
|
||||
var tag = file.GetTag(TagTypes.TiffIFD) as IFDTag;
|
||||
|
||||
if (tag != null)
|
||||
if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
|
||||
{
|
||||
var structure = tag.Structure;
|
||||
|
||||
if (structure != null)
|
||||
if (structure != null
|
||||
&& structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) is SubIFDEntry exif)
|
||||
{
|
||||
var exif = structure.GetEntry(0, (ushort)IFDEntryTag.ExifIFD) as SubIFDEntry;
|
||||
|
||||
if (exif != null)
|
||||
var exifStructure = exif.Structure;
|
||||
if (exifStructure != null)
|
||||
{
|
||||
var exifStructure = exif.Structure;
|
||||
|
||||
if (exifStructure != null)
|
||||
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
|
||||
if (entry != null)
|
||||
{
|
||||
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
|
||||
item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator;
|
||||
}
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
double val = entry.Value.Numerator;
|
||||
val /= entry.Value.Denominator;
|
||||
item.Aperture = val;
|
||||
}
|
||||
|
||||
entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
|
||||
|
||||
if (entry != null)
|
||||
{
|
||||
double val = entry.Value.Numerator;
|
||||
val /= entry.Value.Denominator;
|
||||
item.ShutterSpeed = val;
|
||||
}
|
||||
entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
|
||||
if (entry != null)
|
||||
{
|
||||
item.ShutterSpeed = (double)entry.Value.Numerator / entry.Value.Denominator;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (image != null)
|
||||
if (file is TagLib.Image.File image)
|
||||
{
|
||||
item.CameraMake = image.ImageTag.Make;
|
||||
item.CameraModel = image.ImageTag.Model;
|
||||
@ -116,12 +99,10 @@ namespace Emby.Photos
|
||||
|
||||
item.Overview = image.ImageTag.Comment;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title))
|
||||
if (!string.IsNullOrWhiteSpace(image.ImageTag.Title)
|
||||
&& !item.LockedFields.Contains(MetadataFields.Name))
|
||||
{
|
||||
if (!item.LockedFields.Contains(MetadataFields.Name))
|
||||
{
|
||||
item.Name = image.ImageTag.Title;
|
||||
}
|
||||
item.Name = image.ImageTag.Title;
|
||||
}
|
||||
|
||||
var dateTaken = image.ImageTag.DateTime;
|
||||
@ -140,12 +121,9 @@ namespace Emby.Photos
|
||||
{
|
||||
item.Orientation = null;
|
||||
}
|
||||
else
|
||||
else if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
|
||||
{
|
||||
if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out ImageOrientation orientation))
|
||||
{
|
||||
item.Orientation = orientation;
|
||||
}
|
||||
item.Orientation = orientation;
|
||||
}
|
||||
|
||||
item.ExposureTime = image.ImageTag.ExposureTime;
|
||||
@ -195,7 +173,5 @@ namespace Emby.Photos
|
||||
const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
public string Name => "Embedded Information";
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,10 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@ -29,31 +27,39 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
public class ActivityLogEntryPoint : IServerEntryPoint
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IInstallationManager _installationManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly IActivityManager _activityManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ISubtitleManager _subManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
|
||||
public ActivityLogEntryPoint(ISessionManager sessionManager, IDeviceManager deviceManager, ITaskManager taskManager, IActivityManager activityManager, ILocalizationManager localization, IInstallationManager installationManager, ILibraryManager libraryManager, ISubtitleManager subManager, IUserManager userManager, IServerConfigurationManager config, IServerApplicationHost appHost)
|
||||
public ActivityLogEntryPoint(
|
||||
ILogger<ActivityLogEntryPoint> logger,
|
||||
ISessionManager sessionManager,
|
||||
IDeviceManager deviceManager,
|
||||
ITaskManager taskManager,
|
||||
IActivityManager activityManager,
|
||||
ILocalizationManager localization,
|
||||
IInstallationManager installationManager,
|
||||
ISubtitleManager subManager,
|
||||
IUserManager userManager,
|
||||
IServerApplicationHost appHost)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
_deviceManager = deviceManager;
|
||||
_taskManager = taskManager;
|
||||
_activityManager = activityManager;
|
||||
_localization = localization;
|
||||
_installationManager = installationManager;
|
||||
_libraryManager = libraryManager;
|
||||
_subManager = subManager;
|
||||
_userManager = userManager;
|
||||
_config = config;
|
||||
_appHost = appHost;
|
||||
_deviceManager = deviceManager;
|
||||
}
|
||||
|
||||
public Task RunAsync()
|
||||
@ -83,8 +89,6 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
_deviceManager.CameraImageUploaded += OnCameraImageUploaded;
|
||||
|
||||
_appHost.ApplicationUpdated += OnApplicationUpdated;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@ -124,7 +128,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
//_logger.LogWarning("PlaybackStopped reported with null media info.");
|
||||
_logger.LogWarning("PlaybackStopped reported with null media info.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -155,7 +159,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
//_logger.LogWarning("PlaybackStart reported with null media info.");
|
||||
_logger.LogWarning("PlaybackStart reported with null media info.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -203,6 +207,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
return NotificationType.AudioPlayback.ToString();
|
||||
}
|
||||
|
||||
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NotificationType.VideoPlayback.ToString();
|
||||
@ -217,6 +222,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
return NotificationType.AudioPlaybackStopped.ToString();
|
||||
}
|
||||
|
||||
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NotificationType.VideoPlaybackStopped.ToString();
|
||||
@ -275,16 +281,6 @@ namespace Emby.Server.Implementations.Activity
|
||||
});
|
||||
}
|
||||
|
||||
private void OnApplicationUpdated(object sender, GenericEventArgs<PackageVersionInfo> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
{
|
||||
Name = string.Format(_localization.GetLocalizedString("MessageApplicationUpdatedTo"), e.Argument.versionStr),
|
||||
Type = NotificationType.ApplicationUpdateInstalled.ToString(),
|
||||
Overview = e.Argument.description
|
||||
});
|
||||
}
|
||||
|
||||
private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
|
||||
{
|
||||
CreateLogEntry(new ActivityLogEntry
|
||||
@ -415,6 +411,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
vals.Add(e.Result.ErrorMessage);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
|
||||
{
|
||||
vals.Add(e.Result.LongErrorMessage);
|
||||
@ -424,7 +421,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
Name = string.Format(_localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
|
||||
Type = NotificationType.TaskFailed.ToString(),
|
||||
Overview = string.Join(Environment.NewLine, vals.ToArray()),
|
||||
Overview = string.Join(Environment.NewLine, vals),
|
||||
ShortOverview = runningTime,
|
||||
Severity = LogLevel.Error
|
||||
});
|
||||
@ -460,8 +457,6 @@ namespace Emby.Server.Implementations.Activity
|
||||
_userManager.UserLockedOut -= OnUserLockedOut;
|
||||
|
||||
_deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
|
||||
|
||||
_appHost.ApplicationUpdated -= OnApplicationUpdated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -503,6 +498,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
values.Add(CreateValueString(span.Hours, "hour"));
|
||||
}
|
||||
|
||||
// Number of minutes
|
||||
if (span.Minutes >= 1)
|
||||
{
|
||||
@ -526,6 +522,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
builder.Append(values[i]);
|
||||
}
|
||||
|
||||
// Return result
|
||||
return builder.ToString();
|
||||
}
|
||||
|
@ -15,14 +15,14 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
public class ActivityRepository : BaseSqliteRepository, IActivityRepository
|
||||
{
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
protected IFileSystem FileSystem { get; private set; }
|
||||
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public ActivityRepository(ILoggerFactory loggerFactory, IServerApplicationPaths appPaths, IFileSystem fileSystem)
|
||||
: base(loggerFactory.CreateLogger(nameof(ActivityRepository)))
|
||||
{
|
||||
DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db");
|
||||
FileSystem = fileSystem;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
Logger.LogError(ex, "Error loading database file. Will reset and retry.");
|
||||
|
||||
FileSystem.DeleteFile(DbFilePath);
|
||||
_fileSystem.DeleteFile(DbFilePath);
|
||||
|
||||
InitializeInternal();
|
||||
}
|
||||
@ -43,10 +43,8 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
private void InitializeInternal()
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
RunDefaultInitialization(connection);
|
||||
|
||||
connection.RunQueries(new[]
|
||||
{
|
||||
"create table if not exists ActivityLog (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Overview TEXT, ShortOverview TEXT, Type TEXT NOT NULL, ItemId TEXT, UserId TEXT, DateCreated DATETIME NOT NULL, LogSeverity TEXT NOT NULL)",
|
||||
@ -85,8 +83,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
throw new ArgumentNullException(nameof(entry));
|
||||
}
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = CreateConnection())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
@ -124,8 +121,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
throw new ArgumentNullException(nameof(entry));
|
||||
}
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = CreateConnection())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
@ -159,8 +155,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
|
||||
public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit)
|
||||
{
|
||||
using (WriteLock.Read())
|
||||
using (var connection = CreateConnection(true))
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
var commandText = BaseActivitySelectText;
|
||||
var whereClauses = new List<string>();
|
||||
@ -218,7 +213,7 @@ namespace Emby.Server.Implementations.Activity
|
||||
var list = new List<ActivityLogEntry>();
|
||||
var result = new QueryResult<ActivityLogEntry>();
|
||||
|
||||
var statements = PrepareAllSafe(db, statementTexts).ToList();
|
||||
var statements = PrepareAll(db, statementTexts).ToList();
|
||||
|
||||
using (var statement = statements[0])
|
||||
{
|
||||
|
@ -6,6 +6,8 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
@ -154,11 +156,6 @@ namespace Emby.Server.Implementations
|
||||
/// </summary>
|
||||
public event EventHandler HasPendingRestartChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [application updated].
|
||||
/// </summary>
|
||||
public event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
||||
/// </summary>
|
||||
@ -173,11 +170,17 @@ namespace Emby.Server.Implementations
|
||||
/// <value>The logger.</value>
|
||||
protected ILogger Logger { get; set; }
|
||||
|
||||
private IPlugin[] _plugins;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugins.
|
||||
/// </summary>
|
||||
/// <value>The plugins.</value>
|
||||
public IPlugin[] Plugins { get; protected set; }
|
||||
public IPlugin[] Plugins
|
||||
{
|
||||
get => _plugins;
|
||||
protected set => _plugins = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logger factory.
|
||||
@ -200,7 +203,7 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// The disposable parts
|
||||
/// </summary>
|
||||
protected readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
||||
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration manager.
|
||||
@ -216,8 +219,9 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
#if BETA
|
||||
return PackageVersionClass.Beta;
|
||||
#endif
|
||||
#else
|
||||
return PackageVersionClass.Release;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -229,11 +233,6 @@ namespace Emby.Server.Implementations
|
||||
/// <value>The server configuration manager.</value>
|
||||
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
|
||||
|
||||
protected virtual IResourceFileManager CreateResourceFileManager()
|
||||
{
|
||||
return new ResourceFileManager(HttpResultFactory, LoggerFactory, FileSystemManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user manager.
|
||||
/// </summary>
|
||||
@ -340,7 +339,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
protected IProcessFactory ProcessFactory { get; private set; }
|
||||
|
||||
protected ICryptoProvider CryptographyProvider = new CryptographyProvider();
|
||||
protected readonly IXmlSerializer XmlSerializer;
|
||||
|
||||
protected ISocketFactory SocketFactory { get; private set; }
|
||||
@ -369,9 +367,6 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
_configuration = configuration;
|
||||
|
||||
// hack alert, until common can target .net core
|
||||
BaseExtensions.CryptographyProvider = CryptographyProvider;
|
||||
|
||||
XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory);
|
||||
|
||||
NetworkManager = networkManager;
|
||||
@ -426,7 +421,7 @@ namespace Emby.Server.Implementations
|
||||
/// Gets the current application user agent
|
||||
/// </summary>
|
||||
/// <value>The application user agent.</value>
|
||||
public string ApplicationUserAgent => Name.Replace(' ','-') + "/" + ApplicationVersion;
|
||||
public string ApplicationUserAgent => Name.Replace(' ','-') + '/' + ApplicationVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the email address for use within a comment section of a user agent field.
|
||||
@ -617,8 +612,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
DiscoverTypes();
|
||||
|
||||
SetHttpLimit();
|
||||
|
||||
await RegisterResources(serviceCollection).ConfigureAwait(false);
|
||||
|
||||
FindParts();
|
||||
@ -686,11 +679,6 @@ namespace Emby.Server.Implementations
|
||||
await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected virtual IHttpClient CreateHttpClient()
|
||||
{
|
||||
return new HttpClientManager.HttpClientManager(ApplicationPaths, LoggerFactory, FileSystemManager, () => ApplicationUserAgent);
|
||||
}
|
||||
|
||||
public static IStreamHelper StreamHelper { get; set; }
|
||||
|
||||
/// <summary>
|
||||
@ -716,7 +704,11 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton(FileSystemManager);
|
||||
serviceCollection.AddSingleton<TvDbClientManager>();
|
||||
|
||||
HttpClient = CreateHttpClient();
|
||||
HttpClient = new HttpClientManager.HttpClientManager(
|
||||
ApplicationPaths,
|
||||
LoggerFactory.CreateLogger<HttpClientManager.HttpClientManager>(),
|
||||
FileSystemManager,
|
||||
() => ApplicationUserAgent);
|
||||
serviceCollection.AddSingleton(HttpClient);
|
||||
|
||||
serviceCollection.AddSingleton(NetworkManager);
|
||||
@ -735,13 +727,12 @@ namespace Emby.Server.Implementations
|
||||
ApplicationHost.StreamHelper = new StreamHelper();
|
||||
serviceCollection.AddSingleton(StreamHelper);
|
||||
|
||||
serviceCollection.AddSingleton(CryptographyProvider);
|
||||
serviceCollection.AddSingleton(typeof(ICryptoProvider), typeof(CryptographyProvider));
|
||||
|
||||
SocketFactory = new SocketFactory();
|
||||
serviceCollection.AddSingleton(SocketFactory);
|
||||
|
||||
InstallationManager = new InstallationManager(LoggerFactory, this, ApplicationPaths, HttpClient, JsonSerializer, ServerConfigurationManager, FileSystemManager, CryptographyProvider, ZipClient, PackageRuntime);
|
||||
serviceCollection.AddSingleton(InstallationManager);
|
||||
serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager));
|
||||
|
||||
ZipClient = new ZipClient();
|
||||
serviceCollection.AddSingleton(ZipClient);
|
||||
@ -763,10 +754,6 @@ namespace Emby.Server.Implementations
|
||||
UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager);
|
||||
serviceCollection.AddSingleton(UserDataManager);
|
||||
|
||||
UserRepository = GetUserRepository();
|
||||
// This is only needed for disposal purposes. If removing this, make sure to have the manager handle disposing it
|
||||
serviceCollection.AddSingleton(UserRepository);
|
||||
|
||||
var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager);
|
||||
serviceCollection.AddSingleton<IDisplayPreferencesRepository>(displayPreferencesRepo);
|
||||
|
||||
@ -776,6 +763,8 @@ namespace Emby.Server.Implementations
|
||||
AuthenticationRepository = GetAuthenticationRepository();
|
||||
serviceCollection.AddSingleton(AuthenticationRepository);
|
||||
|
||||
UserRepository = GetUserRepository();
|
||||
|
||||
UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager);
|
||||
serviceCollection.AddSingleton(UserManager);
|
||||
|
||||
@ -816,7 +805,6 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton(TVSeriesManager);
|
||||
|
||||
DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
|
||||
|
||||
serviceCollection.AddSingleton(DeviceManager);
|
||||
|
||||
MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
|
||||
@ -831,10 +819,10 @@ namespace Emby.Server.Implementations
|
||||
DtoService = new DtoService(LoggerFactory, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ProviderManager, this, () => MediaSourceManager, () => LiveTvManager);
|
||||
serviceCollection.AddSingleton(DtoService);
|
||||
|
||||
ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager);
|
||||
ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager);
|
||||
serviceCollection.AddSingleton(ChannelManager);
|
||||
|
||||
SessionManager = new SessionManager(UserDataManager, LoggerFactory, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, JsonSerializer, this, HttpClient, AuthenticationRepository, DeviceManager, MediaSourceManager);
|
||||
SessionManager = new SessionManager(UserDataManager, LoggerFactory, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, this, AuthenticationRepository, DeviceManager, MediaSourceManager);
|
||||
serviceCollection.AddSingleton(SessionManager);
|
||||
|
||||
serviceCollection.AddSingleton<IDlnaManager>(
|
||||
@ -891,7 +879,7 @@ namespace Emby.Server.Implementations
|
||||
SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(LibraryManager, LoggerFactory, ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory);
|
||||
serviceCollection.AddSingleton(SubtitleEncoder);
|
||||
|
||||
serviceCollection.AddSingleton(CreateResourceFileManager());
|
||||
serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager));
|
||||
|
||||
displayPreferencesRepo.Initialize();
|
||||
|
||||
@ -908,8 +896,6 @@ namespace Emby.Server.Implementations
|
||||
_serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
}
|
||||
|
||||
public virtual string PackageRuntime => "netcore";
|
||||
|
||||
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
|
||||
{
|
||||
// Distinct these to prevent users from reporting problems that aren't actually problems
|
||||
@ -918,8 +904,7 @@ namespace Emby.Server.Implementations
|
||||
.Distinct();
|
||||
|
||||
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
||||
// FIXME: @bond this logs the kernel version, not the OS version
|
||||
logger.LogInformation("Operating system: {OS} {OSVersion}", OperatingSystem.Name, Environment.OSVersion.Version);
|
||||
logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
|
||||
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
||||
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
||||
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
||||
@ -929,19 +914,6 @@ namespace Emby.Server.Implementations
|
||||
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
|
||||
}
|
||||
|
||||
private void SetHttpLimit()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Increase the max http request limit
|
||||
ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error setting http limit");
|
||||
}
|
||||
}
|
||||
|
||||
private X509Certificate2 GetCertificate(CertificateInfo info)
|
||||
{
|
||||
var certificateLocation = info?.Path;
|
||||
@ -1035,7 +1007,6 @@ namespace Emby.Server.Implementations
|
||||
Video.LiveTvManager = LiveTvManager;
|
||||
Folder.UserViewManager = UserViewManager;
|
||||
UserView.TVSeriesManager = TVSeriesManager;
|
||||
UserView.PlaylistManager = PlaylistManager;
|
||||
UserView.CollectionManager = CollectionManager;
|
||||
BaseItem.MediaSourceManager = MediaSourceManager;
|
||||
CollectionFolder.XmlSerializer = XmlSerializer;
|
||||
@ -1044,11 +1015,47 @@ namespace Emby.Server.Implementations
|
||||
AuthenticatedAttribute.AuthService = AuthService;
|
||||
}
|
||||
|
||||
private async void PluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> args)
|
||||
{
|
||||
string dir = Path.Combine(ApplicationPaths.PluginsPath, args.Argument.name);
|
||||
var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories)
|
||||
.Select(x => Assembly.LoadFrom(x))
|
||||
.SelectMany(x => x.ExportedTypes)
|
||||
.Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType)
|
||||
.ToList();
|
||||
|
||||
types.AddRange(types);
|
||||
|
||||
var plugins = types.Where(x => x.IsAssignableFrom(typeof(IPlugin)))
|
||||
.Select(CreateInstanceSafe)
|
||||
.Where(x => x != null)
|
||||
.Cast<IPlugin>()
|
||||
.Select(LoadPlugin)
|
||||
.Where(x => x != null)
|
||||
.ToArray();
|
||||
|
||||
int oldLen = _plugins.Length;
|
||||
Array.Resize<IPlugin>(ref _plugins, _plugins.Length + plugins.Length);
|
||||
plugins.CopyTo(_plugins, oldLen);
|
||||
|
||||
var entries = types.Where(x => x.IsAssignableFrom(typeof(IServerEntryPoint)))
|
||||
.Select(CreateInstanceSafe)
|
||||
.Where(x => x != null)
|
||||
.Cast<IServerEntryPoint>()
|
||||
.ToList();
|
||||
|
||||
await Task.WhenAll(StartEntryPoints(entries, true));
|
||||
await Task.WhenAll(StartEntryPoints(entries, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the parts.
|
||||
/// </summary>
|
||||
protected void FindParts()
|
||||
{
|
||||
InstallationManager = _serviceProvider.GetService<IInstallationManager>();
|
||||
InstallationManager.PluginInstalled += PluginInstalled;
|
||||
|
||||
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
|
||||
{
|
||||
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
|
||||
@ -1088,7 +1095,7 @@ namespace Emby.Server.Implementations
|
||||
MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>());
|
||||
|
||||
NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
|
||||
UserManager.AddParts(GetExports<IAuthenticationProvider>());
|
||||
UserManager.AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
|
||||
|
||||
IsoManager.AddParts(GetExports<IIsoMounter>());
|
||||
}
|
||||
@ -1131,7 +1138,7 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error loading plugin {pluginName}", plugin.GetType().FullName);
|
||||
Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1145,10 +1152,32 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
Logger.LogInformation("Loading assemblies");
|
||||
|
||||
AllConcreteTypes = GetComposablePartAssemblies()
|
||||
.SelectMany(x => x.ExportedTypes)
|
||||
.Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
|
||||
.ToArray();
|
||||
AllConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
|
||||
}
|
||||
|
||||
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
foreach (var ass in assemblies)
|
||||
{
|
||||
Type[] exportedTypes;
|
||||
try
|
||||
{
|
||||
exportedTypes = ass.GetExportedTypes();
|
||||
}
|
||||
catch (TypeLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (Type type in exportedTypes)
|
||||
{
|
||||
if (type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
|
||||
{
|
||||
yield return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private CertificateInfo CertificateInfo { get; set; }
|
||||
@ -1310,10 +1339,21 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
if (Directory.Exists(ApplicationPaths.PluginsPath))
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly))
|
||||
foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories))
|
||||
{
|
||||
Logger.LogInformation("Loading assembly {Path}", file);
|
||||
yield return Assembly.LoadFrom(file);
|
||||
Assembly plugAss;
|
||||
try
|
||||
{
|
||||
plugAss = Assembly.LoadFrom(file);
|
||||
}
|
||||
catch (FileLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load assembly {Path}", file);
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
|
||||
yield return plugAss;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1372,14 +1412,23 @@ namespace Emby.Server.Implementations
|
||||
public async Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken)
|
||||
{
|
||||
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
|
||||
var wanAddress = await GetWanApiUrl(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string wanAddress;
|
||||
|
||||
if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns))
|
||||
{
|
||||
wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns);
|
||||
}
|
||||
|
||||
return new SystemInfo
|
||||
{
|
||||
HasPendingRestart = HasPendingRestart,
|
||||
IsShuttingDown = IsShuttingDown,
|
||||
Version = ApplicationVersion,
|
||||
ProductName = ApplicationProductName,
|
||||
WebSocketPortNumber = HttpPort,
|
||||
CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(),
|
||||
Id = SystemId,
|
||||
@ -1422,10 +1471,21 @@ namespace Emby.Server.Implementations
|
||||
public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken)
|
||||
{
|
||||
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
|
||||
var wanAddress = await GetWanApiUrl(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string wanAddress;
|
||||
|
||||
if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns))
|
||||
{
|
||||
wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns);
|
||||
}
|
||||
return new PublicSystemInfo
|
||||
{
|
||||
Version = ApplicationVersion,
|
||||
ProductName = ApplicationProductName,
|
||||
Id = SystemId,
|
||||
OperatingSystem = OperatingSystem.Id.ToString(),
|
||||
WanAddress = wanAddress,
|
||||
@ -1460,7 +1520,7 @@ namespace Emby.Server.Implementations
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<string> GetWanApiUrl(CancellationToken cancellationToken)
|
||||
public async Task<string> GetWanApiUrlFromExternal(CancellationToken cancellationToken)
|
||||
{
|
||||
const string Url = "http://ipv4.icanhazip.com";
|
||||
try
|
||||
@ -1471,44 +1531,96 @@ namespace Emby.Server.Implementations
|
||||
LogErrorResponseBody = false,
|
||||
LogErrors = false,
|
||||
LogRequest = false,
|
||||
TimeoutMs = 10000,
|
||||
BufferContent = false,
|
||||
CancellationToken = cancellationToken
|
||||
}).ConfigureAwait(false))
|
||||
{
|
||||
return GetLocalApiUrl(response.ReadToEnd().Trim());
|
||||
string res = await response.ReadToEndAsync().ConfigureAwait(false);
|
||||
return GetWanApiUrl(res.Trim());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error getting WAN Ip address information");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetLocalApiUrl(IpAddressInfo ipAddress)
|
||||
/// <summary>
|
||||
/// Removes the scope id from IPv6 addresses.
|
||||
/// </summary>
|
||||
/// <param name="address">The IPv6 address.</param>
|
||||
/// <returns>The IPv6 address without the scope id.</returns>
|
||||
private string RemoveScopeId(string address)
|
||||
{
|
||||
if (ipAddress.AddressFamily == IpAddressFamily.InterNetworkV6)
|
||||
var index = address.IndexOf('%');
|
||||
if (index == -1)
|
||||
{
|
||||
return GetLocalApiUrl("[" + ipAddress.Address + "]");
|
||||
return address;
|
||||
}
|
||||
|
||||
return GetLocalApiUrl(ipAddress.Address);
|
||||
return address.Substring(0, index);
|
||||
}
|
||||
|
||||
public string GetLocalApiUrl(IPAddress ipAddress)
|
||||
{
|
||||
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
var str = RemoveScopeId(ipAddress.ToString());
|
||||
|
||||
return GetLocalApiUrl("[" + str + "]");
|
||||
}
|
||||
|
||||
return GetLocalApiUrl(ipAddress.ToString());
|
||||
}
|
||||
|
||||
public string GetLocalApiUrl(string host)
|
||||
{
|
||||
if (EnableHttps)
|
||||
{
|
||||
return string.Format("https://{0}:{1}",
|
||||
host,
|
||||
HttpsPort.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return string.Format("http://{0}:{1}",
|
||||
host,
|
||||
HttpPort.ToString(CultureInfo.InvariantCulture));
|
||||
host,
|
||||
HttpPort.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public Task<List<IpAddressInfo>> GetLocalIpAddresses(CancellationToken cancellationToken)
|
||||
public string GetWanApiUrl(IPAddress ipAddress)
|
||||
{
|
||||
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
var str = RemoveScopeId(ipAddress.ToString());
|
||||
|
||||
return GetWanApiUrl("[" + str + "]");
|
||||
}
|
||||
|
||||
return GetWanApiUrl(ipAddress.ToString());
|
||||
}
|
||||
|
||||
public string GetWanApiUrl(string host)
|
||||
{
|
||||
if (EnableHttps)
|
||||
{
|
||||
return string.Format("https://{0}:{1}",
|
||||
host,
|
||||
ServerConfigurationManager.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return string.Format("http://{0}:{1}",
|
||||
host,
|
||||
ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
|
||||
{
|
||||
return GetLocalIpAddressesInternal(true, 0, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<List<IpAddressInfo>> GetLocalIpAddressesInternal(bool allowLoopback, int limit, CancellationToken cancellationToken)
|
||||
private async Task<List<IPAddress>> GetLocalIpAddressesInternal(bool allowLoopback, int limit, CancellationToken cancellationToken)
|
||||
{
|
||||
var addresses = ServerConfigurationManager
|
||||
.Configuration
|
||||
@ -1522,13 +1634,13 @@ namespace Emby.Server.Implementations
|
||||
addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
|
||||
}
|
||||
|
||||
var resultList = new List<IpAddressInfo>();
|
||||
var resultList = new List<IPAddress>();
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
if (!allowLoopback)
|
||||
{
|
||||
if (address.Equals(IpAddressInfo.Loopback) || address.Equals(IpAddressInfo.IPv6Loopback))
|
||||
if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -1549,7 +1661,7 @@ namespace Emby.Server.Implementations
|
||||
return resultList;
|
||||
}
|
||||
|
||||
private IpAddressInfo NormalizeConfiguredLocalAddress(string address)
|
||||
private IPAddress NormalizeConfiguredLocalAddress(string address)
|
||||
{
|
||||
var index = address.Trim('/').IndexOf('/');
|
||||
|
||||
@ -1558,7 +1670,7 @@ namespace Emby.Server.Implementations
|
||||
address = address.Substring(index + 1);
|
||||
}
|
||||
|
||||
if (NetworkManager.TryParseIpAddress(address.Trim('/'), out IpAddressInfo result))
|
||||
if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
@ -1568,10 +1680,10 @@ namespace Emby.Server.Implementations
|
||||
|
||||
private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private async Task<bool> IsIpAddressValidAsync(IpAddressInfo address, CancellationToken cancellationToken)
|
||||
private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
|
||||
{
|
||||
if (address.Equals(IpAddressInfo.Loopback) ||
|
||||
address.Equals(IpAddressInfo.IPv6Loopback))
|
||||
if (address.Equals(IPAddress.Loopback) ||
|
||||
address.Equals(IPAddress.IPv6Loopback))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -1599,15 +1711,13 @@ namespace Emby.Server.Implementations
|
||||
LogErrorResponseBody = false,
|
||||
LogErrors = LogPing,
|
||||
LogRequest = LogPing,
|
||||
TimeoutMs = 5000,
|
||||
BufferContent = false,
|
||||
|
||||
CancellationToken = cancellationToken
|
||||
}, "POST").ConfigureAwait(false))
|
||||
}, HttpMethod.Post).ConfigureAwait(false))
|
||||
{
|
||||
using (var reader = new StreamReader(response.Content))
|
||||
{
|
||||
var result = reader.ReadToEnd();
|
||||
var result = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid);
|
||||
@ -1756,24 +1866,6 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when [application updated].
|
||||
/// </summary>
|
||||
/// <param name="package">The package.</param>
|
||||
protected void OnApplicationUpdated(PackageVersionInfo package)
|
||||
{
|
||||
Logger.LogInformation("Application has been updated to version {0}", package.versionStr);
|
||||
|
||||
ApplicationUpdated?.Invoke(
|
||||
this,
|
||||
new GenericEventArgs<PackageVersionInfo>()
|
||||
{
|
||||
Argument = package
|
||||
});
|
||||
|
||||
NotifyPendingRestart();
|
||||
}
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
@ -1818,8 +1910,12 @@ namespace Emby.Server.Implementations
|
||||
Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
UserRepository.Dispose();
|
||||
}
|
||||
|
||||
UserRepository = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@ -20,7 +19,6 @@ using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Channels;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
@ -40,11 +38,8 @@ namespace Emby.Server.Implementations.Channels
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IProviderManager _providerManager;
|
||||
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
public ChannelManager(
|
||||
IUserManager userManager,
|
||||
IDtoService dtoService,
|
||||
@ -54,8 +49,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
IFileSystem fileSystem,
|
||||
IUserDataManager userDataManager,
|
||||
IJsonSerializer jsonSerializer,
|
||||
ILocalizationManager localization,
|
||||
IHttpClient httpClient,
|
||||
IProviderManager providerManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@ -66,8 +59,6 @@ namespace Emby.Server.Implementations.Channels
|
||||
_fileSystem = fileSystem;
|
||||
_userDataManager = userDataManager;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_localization = localization;
|
||||
_httpClient = httpClient;
|
||||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
|
@ -74,23 +74,14 @@ namespace Emby.Server.Implementations.Configuration
|
||||
/// </summary>
|
||||
private void UpdateMetadataPath()
|
||||
{
|
||||
string metadataPath;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Configuration.MetadataPath))
|
||||
{
|
||||
metadataPath = GetInternalMetadataPath();
|
||||
((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
|
||||
}
|
||||
else
|
||||
{
|
||||
metadataPath = Path.Combine(Configuration.MetadataPath, "metadata");
|
||||
((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Configuration.MetadataPath;
|
||||
}
|
||||
|
||||
((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = metadataPath;
|
||||
}
|
||||
|
||||
private string GetInternalMetadataPath()
|
||||
{
|
||||
return Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -4,7 +4,6 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
|
||||
namespace Emby.Server.Implementations.Cryptography
|
||||
@ -136,7 +135,7 @@ namespace Emby.Server.Implementations.Cryptography
|
||||
{
|
||||
return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
|
||||
}
|
||||
|
||||
|
||||
public byte[] ComputeHash(PasswordHash hash)
|
||||
{
|
||||
int iterations = _defaultIterations;
|
||||
|
@ -1,183 +1,144 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public abstract class BaseSqliteRepository : IDisposable
|
||||
{
|
||||
protected string DbFilePath { get; set; }
|
||||
protected ReaderWriterLockSlim WriteLock;
|
||||
|
||||
protected ILogger Logger { get; private set; }
|
||||
private bool _disposed = false;
|
||||
|
||||
protected BaseSqliteRepository(ILogger logger)
|
||||
{
|
||||
Logger = logger;
|
||||
|
||||
WriteLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the DB file.
|
||||
/// </summary>
|
||||
/// <value>Path to the DB file.</value>
|
||||
protected string DbFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
/// </summary>
|
||||
/// <value>The logger.</value>
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default connection flags.
|
||||
/// </summary>
|
||||
/// <value>The default connection flags.</value>
|
||||
protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transaction mode.
|
||||
/// </summary>
|
||||
/// <value>The transaction mode.</value>>
|
||||
protected TransactionMode TransactionMode => TransactionMode.Deferred;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transaction mode for read-only operations.
|
||||
/// </summary>
|
||||
/// <value>The transaction mode.</value>
|
||||
protected TransactionMode ReadTransactionMode => TransactionMode.Deferred;
|
||||
|
||||
internal static int ThreadSafeMode { get; set; }
|
||||
/// <summary>
|
||||
/// Gets the cache size.
|
||||
/// </summary>
|
||||
/// <value>The cache size or null.</value>
|
||||
protected virtual int? CacheSize => null;
|
||||
|
||||
static BaseSqliteRepository()
|
||||
/// <summary>
|
||||
/// Gets the journal mode.
|
||||
/// </summary>
|
||||
/// <value>The journal mode.</value>
|
||||
protected virtual string JournalMode => "WAL";
|
||||
|
||||
/// <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.Default;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the synchronous mode.
|
||||
/// </summary>
|
||||
/// <value>The synchronous mode or null.</value>
|
||||
/// <see cref="SynchronousMode"/>
|
||||
protected virtual SynchronousMode? Synchronous => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the write lock.
|
||||
/// </summary>
|
||||
/// <value>The write lock.</value>
|
||||
protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the write connection.
|
||||
/// </summary>
|
||||
/// <value>The write connection.</value>
|
||||
protected SQLiteDatabaseConnection WriteConnection { get; set; }
|
||||
|
||||
protected ManagedConnection GetConnection(bool _ = false)
|
||||
{
|
||||
SQLite3.EnableSharedCache = false;
|
||||
|
||||
int rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MEMSTATUS, 0);
|
||||
//CheckOk(rc);
|
||||
|
||||
rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MULTITHREAD, 1);
|
||||
//rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SINGLETHREAD, 1);
|
||||
//rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SERIALIZED, 1);
|
||||
//CheckOk(rc);
|
||||
|
||||
rc = raw.sqlite3_enable_shared_cache(1);
|
||||
|
||||
ThreadSafeMode = raw.sqlite3_threadsafe();
|
||||
}
|
||||
|
||||
private static bool _versionLogged;
|
||||
|
||||
private string _defaultWal;
|
||||
protected ManagedConnection _connection;
|
||||
|
||||
protected virtual bool EnableSingleConnection => true;
|
||||
|
||||
protected ManagedConnection CreateConnection(bool isReadOnly = false)
|
||||
{
|
||||
if (_connection != null)
|
||||
WriteLock.Wait();
|
||||
if (WriteConnection != null)
|
||||
{
|
||||
return _connection;
|
||||
return new ManagedConnection(WriteConnection, WriteLock);
|
||||
}
|
||||
|
||||
lock (WriteLock)
|
||||
WriteConnection = SQLite3.Open(
|
||||
DbFilePath,
|
||||
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
|
||||
null);
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
if (!_versionLogged)
|
||||
{
|
||||
_versionLogged = true;
|
||||
Logger.LogInformation("Sqlite version: " + SQLite3.Version);
|
||||
Logger.LogInformation("Sqlite compiler options: " + string.Join(",", SQLite3.CompilerOptions.ToArray()));
|
||||
}
|
||||
|
||||
ConnectionFlags connectionFlags;
|
||||
|
||||
if (isReadOnly)
|
||||
{
|
||||
//Logger.LogInformation("Opening read connection");
|
||||
//connectionFlags = ConnectionFlags.ReadOnly;
|
||||
connectionFlags = ConnectionFlags.Create;
|
||||
connectionFlags |= ConnectionFlags.ReadWrite;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Logger.LogInformation("Opening write connection");
|
||||
connectionFlags = ConnectionFlags.Create;
|
||||
connectionFlags |= ConnectionFlags.ReadWrite;
|
||||
}
|
||||
|
||||
if (EnableSingleConnection)
|
||||
{
|
||||
connectionFlags |= ConnectionFlags.PrivateCache;
|
||||
}
|
||||
else
|
||||
{
|
||||
connectionFlags |= ConnectionFlags.SharedCached;
|
||||
}
|
||||
|
||||
connectionFlags |= ConnectionFlags.NoMutex;
|
||||
|
||||
var db = SQLite3.Open(DbFilePath, connectionFlags, null);
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_defaultWal))
|
||||
{
|
||||
_defaultWal = db.Query("PRAGMA journal_mode").SelectScalarString().First();
|
||||
|
||||
Logger.LogInformation("Default journal_mode for {0} is {1}", DbFilePath, _defaultWal);
|
||||
}
|
||||
|
||||
var queries = new List<string>
|
||||
{
|
||||
//"PRAGMA cache size=-10000"
|
||||
//"PRAGMA read_uncommitted = true",
|
||||
"PRAGMA synchronous=Normal"
|
||||
};
|
||||
|
||||
if (CacheSize.HasValue)
|
||||
{
|
||||
queries.Add("PRAGMA cache_size=" + CacheSize.Value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
if (EnableTempStoreMemory)
|
||||
{
|
||||
queries.Add("PRAGMA temp_store = memory");
|
||||
}
|
||||
else
|
||||
{
|
||||
queries.Add("PRAGMA temp_store = file");
|
||||
}
|
||||
|
||||
foreach (var query in queries)
|
||||
{
|
||||
db.Execute(query);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
using (db)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
_connection = new ManagedConnection(db, false);
|
||||
|
||||
return _connection;
|
||||
WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
||||
}
|
||||
|
||||
if (Synchronous.HasValue)
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||
}
|
||||
|
||||
if (PageSize.HasValue)
|
||||
{
|
||||
WriteConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||
}
|
||||
|
||||
WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||
|
||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
||||
WriteConnection.Execute("VACUUM");
|
||||
|
||||
return new ManagedConnection(WriteConnection, WriteLock);
|
||||
}
|
||||
|
||||
public IStatement PrepareStatement(ManagedConnection connection, string sql)
|
||||
{
|
||||
return connection.PrepareStatement(sql);
|
||||
}
|
||||
|
||||
public IStatement PrepareStatementSafe(ManagedConnection connection, string sql)
|
||||
{
|
||||
return connection.PrepareStatement(sql);
|
||||
}
|
||||
=> connection.PrepareStatement(sql);
|
||||
|
||||
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
|
||||
{
|
||||
return connection.PrepareStatement(sql);
|
||||
}
|
||||
=> connection.PrepareStatement(sql);
|
||||
|
||||
public IStatement PrepareStatementSafe(IDatabaseConnection connection, string sql)
|
||||
{
|
||||
return connection.PrepareStatement(sql);
|
||||
}
|
||||
|
||||
public List<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql)
|
||||
{
|
||||
return PrepareAllSafe(connection, sql);
|
||||
}
|
||||
|
||||
public List<IStatement> PrepareAllSafe(IDatabaseConnection connection, IEnumerable<string> sql)
|
||||
{
|
||||
return sql.Select(connection.PrepareStatement).ToList();
|
||||
}
|
||||
public IEnumerable<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql)
|
||||
=> sql.Select(connection.PrepareStatement);
|
||||
|
||||
protected bool TableExists(ManagedConnection connection, string name)
|
||||
{
|
||||
@ -199,103 +160,9 @@ namespace Emby.Server.Implementations.Data
|
||||
}, ReadTransactionMode);
|
||||
}
|
||||
|
||||
protected void RunDefaultInitialization(ManagedConnection db)
|
||||
{
|
||||
var queries = new List<string>
|
||||
{
|
||||
"PRAGMA journal_mode=WAL",
|
||||
"PRAGMA page_size=4096",
|
||||
"PRAGMA synchronous=Normal"
|
||||
};
|
||||
|
||||
if (EnableTempStoreMemory)
|
||||
{
|
||||
queries.AddRange(new List<string>
|
||||
{
|
||||
"pragma default_temp_store = memory",
|
||||
"pragma temp_store = memory"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
queries.AddRange(new List<string>
|
||||
{
|
||||
"pragma temp_store = file"
|
||||
});
|
||||
}
|
||||
|
||||
db.ExecuteAll(string.Join(";", queries));
|
||||
Logger.LogInformation("PRAGMA synchronous=" + db.Query("PRAGMA synchronous").SelectScalarString().First());
|
||||
}
|
||||
|
||||
protected virtual bool EnableTempStoreMemory => false;
|
||||
|
||||
protected virtual int? CacheSize => null;
|
||||
|
||||
private bool _disposed;
|
||||
protected void CheckDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(GetType().Name, "Object has been disposed and cannot be accessed.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private readonly object _disposeLock = new object();
|
||||
|
||||
/// <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 (dispose)
|
||||
{
|
||||
DisposeConnection();
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_disposeLock)
|
||||
{
|
||||
using (WriteLock.Write())
|
||||
{
|
||||
if (_connection != null)
|
||||
{
|
||||
using (_connection)
|
||||
{
|
||||
_connection.Close();
|
||||
}
|
||||
_connection = null;
|
||||
}
|
||||
|
||||
CloseConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error disposing database");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CloseConnection()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
|
||||
{
|
||||
var list = new List<string>();
|
||||
var columnNames = new List<string>();
|
||||
|
||||
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
|
||||
{
|
||||
@ -303,11 +170,11 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var name = row[1].ToString();
|
||||
|
||||
list.Add(name);
|
||||
columnNames.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
return columnNames;
|
||||
}
|
||||
|
||||
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||
@ -319,61 +186,103 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL");
|
||||
}
|
||||
|
||||
protected void CheckDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(GetType().Name, "Object has been disposed and cannot be accessed.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
if (dispose)
|
||||
{
|
||||
WriteLock.Wait();
|
||||
try
|
||||
{
|
||||
WriteConnection.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
WriteLock.Release();
|
||||
}
|
||||
|
||||
WriteLock.Dispose();
|
||||
}
|
||||
|
||||
WriteConnection = null;
|
||||
WriteLock = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReaderWriterLockSlimExtensions
|
||||
/// <summary>
|
||||
/// The disk synchronization mode, controls how aggressively SQLite will write data
|
||||
/// all the way out to physical storage.
|
||||
/// </summary>
|
||||
public enum SynchronousMode
|
||||
{
|
||||
private sealed class ReadLockToken : IDisposable
|
||||
{
|
||||
private ReaderWriterLockSlim _sync;
|
||||
public ReadLockToken(ReaderWriterLockSlim sync)
|
||||
{
|
||||
_sync = sync;
|
||||
sync.EnterReadLock();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
if (_sync != null)
|
||||
{
|
||||
_sync.ExitReadLock();
|
||||
_sync = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
private sealed class WriteLockToken : IDisposable
|
||||
{
|
||||
private ReaderWriterLockSlim _sync;
|
||||
public WriteLockToken(ReaderWriterLockSlim sync)
|
||||
{
|
||||
_sync = sync;
|
||||
sync.EnterWriteLock();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
if (_sync != null)
|
||||
{
|
||||
_sync.ExitWriteLock();
|
||||
_sync = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// SQLite continues without syncing as soon as it has handed data off to the operating system
|
||||
/// </summary>
|
||||
Off = 0,
|
||||
|
||||
public static IDisposable Read(this ReaderWriterLockSlim obj)
|
||||
{
|
||||
//if (BaseSqliteRepository.ThreadSafeMode > 0)
|
||||
//{
|
||||
// return new DummyToken();
|
||||
//}
|
||||
return new WriteLockToken(obj);
|
||||
}
|
||||
/// <summary>
|
||||
/// SQLite database engine will still sync at the most critical moments
|
||||
/// </summary>
|
||||
Normal = 1,
|
||||
|
||||
public static IDisposable Write(this ReaderWriterLockSlim obj)
|
||||
{
|
||||
//if (BaseSqliteRepository.ThreadSafeMode > 0)
|
||||
//{
|
||||
// return new DummyToken();
|
||||
//}
|
||||
return new WriteLockToken(obj);
|
||||
}
|
||||
/// <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
|
||||
}
|
||||
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
|
@ -1,79 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
public class ManagedConnection : IDisposable
|
||||
{
|
||||
private SQLiteDatabaseConnection db;
|
||||
private readonly bool _closeOnDispose;
|
||||
private SQLiteDatabaseConnection _db;
|
||||
private readonly SemaphoreSlim _writeLock;
|
||||
private bool _disposed = false;
|
||||
|
||||
public ManagedConnection(SQLiteDatabaseConnection db, bool closeOnDispose)
|
||||
public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
|
||||
{
|
||||
this.db = db;
|
||||
_closeOnDispose = closeOnDispose;
|
||||
_db = db;
|
||||
_writeLock = writeLock;
|
||||
}
|
||||
|
||||
public IStatement PrepareStatement(string sql)
|
||||
{
|
||||
return db.PrepareStatement(sql);
|
||||
return _db.PrepareStatement(sql);
|
||||
}
|
||||
|
||||
public IEnumerable<IStatement> PrepareAll(string sql)
|
||||
{
|
||||
return db.PrepareAll(sql);
|
||||
return _db.PrepareAll(sql);
|
||||
}
|
||||
|
||||
public void ExecuteAll(string sql)
|
||||
{
|
||||
db.ExecuteAll(sql);
|
||||
_db.ExecuteAll(sql);
|
||||
}
|
||||
|
||||
public void Execute(string sql, params object[] values)
|
||||
{
|
||||
db.Execute(sql, values);
|
||||
_db.Execute(sql, values);
|
||||
}
|
||||
|
||||
public void RunQueries(string[] sql)
|
||||
{
|
||||
db.RunQueries(sql);
|
||||
_db.RunQueries(sql);
|
||||
}
|
||||
|
||||
public void RunInTransaction(Action<IDatabaseConnection> action, TransactionMode mode)
|
||||
{
|
||||
db.RunInTransaction(action, mode);
|
||||
_db.RunInTransaction(action, mode);
|
||||
}
|
||||
|
||||
public T RunInTransaction<T>(Func<IDatabaseConnection, T> action, TransactionMode mode)
|
||||
{
|
||||
return db.RunInTransaction(action, mode);
|
||||
return _db.RunInTransaction(action, mode);
|
||||
}
|
||||
|
||||
public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql)
|
||||
{
|
||||
return db.Query(sql);
|
||||
return _db.Query(sql);
|
||||
}
|
||||
|
||||
public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql, params object[] values)
|
||||
{
|
||||
return db.Query(sql, values);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
using (db)
|
||||
{
|
||||
|
||||
}
|
||||
return _db.Query(sql, values);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_closeOnDispose)
|
||||
if (_disposed)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
_writeLock.Release();
|
||||
|
||||
_db = null; // Don't dispose it
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,13 +18,13 @@ namespace Emby.Server.Implementations.Data
|
||||
/// </summary>
|
||||
public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
|
||||
{
|
||||
protected IFileSystem FileSystem { get; private set; }
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public SqliteDisplayPreferencesRepository(ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IFileSystem fileSystem)
|
||||
: base(loggerFactory.CreateLogger(nameof(SqliteDisplayPreferencesRepository)))
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
FileSystem = fileSystem;
|
||||
_fileSystem = fileSystem;
|
||||
DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
|
||||
}
|
||||
|
||||
@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
Logger.LogError(ex, "Error loading database file. Will reset and retry.");
|
||||
|
||||
FileSystem.DeleteFile(DbFilePath);
|
||||
_fileSystem.DeleteFile(DbFilePath);
|
||||
|
||||
InitializeInternal();
|
||||
}
|
||||
@ -61,10 +61,8 @@ namespace Emby.Server.Implementations.Data
|
||||
/// <returns>Task.</returns>
|
||||
private void InitializeInternal()
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
RunDefaultInitialization(connection);
|
||||
|
||||
string[] queries = {
|
||||
|
||||
"create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
|
||||
@ -90,22 +88,20 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
throw new ArgumentNullException(nameof(displayPreferences));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(displayPreferences.Id))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(displayPreferences.Id));
|
||||
throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
SaveDisplayPreferences(displayPreferences, userId, client, db);
|
||||
}, TransactionMode);
|
||||
}
|
||||
SaveDisplayPreferences(displayPreferences, userId, client, db);
|
||||
}, TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,18 +137,15 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
foreach (var displayPreference in displayPreferences)
|
||||
{
|
||||
foreach (var displayPreference in displayPreferences)
|
||||
{
|
||||
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
|
||||
}
|
||||
}, TransactionMode);
|
||||
}
|
||||
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
|
||||
}
|
||||
}, TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,27 +166,24 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
var guidId = displayPreferencesId.GetMD5();
|
||||
|
||||
using (WriteLock.Read())
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
using (var connection = CreateConnection(true))
|
||||
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
|
||||
{
|
||||
statement.TryBind("@id", guidId.ToGuidBlob());
|
||||
statement.TryBind("@userId", userId.ToGuidBlob());
|
||||
statement.TryBind("@client", client);
|
||||
statement.TryBind("@id", guidId.ToGuidBlob());
|
||||
statement.TryBind("@userId", userId.ToGuidBlob());
|
||||
statement.TryBind("@client", client);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return Get(row);
|
||||
}
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return Get(row);
|
||||
}
|
||||
|
||||
return new DisplayPreferences
|
||||
{
|
||||
Id = guidId.ToString("N")
|
||||
};
|
||||
}
|
||||
|
||||
return new DisplayPreferences
|
||||
{
|
||||
Id = guidId.ToString("N")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,18 +197,15 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
var list = new List<DisplayPreferences>();
|
||||
|
||||
using (WriteLock.Read())
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
using (var connection = CreateConnection(true))
|
||||
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
|
||||
{
|
||||
statement.TryBind("@userId", userId.ToGuidBlob());
|
||||
statement.TryBind("@userId", userId.ToGuidBlob());
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(Get(row));
|
||||
}
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(Get(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
public static void Attach(ManagedConnection db, string path, string alias)
|
||||
public static void Attach(SQLiteDatabaseConnection db, string path, string alias)
|
||||
{
|
||||
var commandText = string.Format("attach @path as {0};", alias);
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
@ -33,14 +32,14 @@ namespace Emby.Server.Implementations.Data
|
||||
/// Opens the connection to the database
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
public void Initialize(ReaderWriterLockSlim writeLock, ManagedConnection managedConnection, IUserManager userManager)
|
||||
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
|
||||
{
|
||||
_connection = managedConnection;
|
||||
|
||||
WriteLock.Dispose();
|
||||
WriteLock = writeLock;
|
||||
WriteLock = dbLock;
|
||||
WriteConnection?.Dispose();
|
||||
WriteConnection = dbConnection;
|
||||
|
||||
using (var connection = CreateConnection())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
var userDatasTableExists = TableExists(connection, "UserDatas");
|
||||
var userDataTableExists = TableExists(connection, "userdata");
|
||||
@ -129,8 +128,6 @@ namespace Emby.Server.Implementations.Data
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override bool EnableTempStoreMemory => true;
|
||||
|
||||
/// <summary>
|
||||
/// Saves the user data.
|
||||
/// </summary>
|
||||
@ -178,15 +175,12 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
SaveUserData(db, internalUserId, key, userData);
|
||||
}, TransactionMode);
|
||||
}
|
||||
SaveUserData(db, internalUserId, key, userData);
|
||||
}, TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,18 +243,15 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
foreach (var userItemData in userDataList)
|
||||
{
|
||||
foreach (var userItemData in userDataList)
|
||||
{
|
||||
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
|
||||
}
|
||||
}, TransactionMode);
|
||||
}
|
||||
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
|
||||
}
|
||||
}, TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,28 +272,26 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
throw new ArgumentNullException(nameof(internalUserId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
using (WriteLock.Read())
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
using (var connection = CreateConnection(true))
|
||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
||||
{
|
||||
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", internalUserId);
|
||||
statement.TryBind("@Key", key);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
statement.TryBind("@UserId", internalUserId);
|
||||
statement.TryBind("@Key", key);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return ReadRow(row);
|
||||
}
|
||||
return ReadRow(row);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,18 +324,15 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
var list = new List<UserItemData>();
|
||||
|
||||
using (WriteLock.Read())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
|
||||
{
|
||||
statement.TryBind("@UserId", internalUserId);
|
||||
statement.TryBind("@UserId", internalUserId);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(ReadRow(row));
|
||||
}
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(ReadRow(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -392,15 +378,5 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
return userData;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool dispose)
|
||||
{
|
||||
// handled by library database
|
||||
}
|
||||
|
||||
protected override void CloseConnection()
|
||||
{
|
||||
// handled by library database
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,10 +40,8 @@ namespace Emby.Server.Implementations.Data
|
||||
/// <returns>Task.</returns>
|
||||
public void Initialize()
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
RunDefaultInitialization(connection);
|
||||
|
||||
var localUsersTableExists = TableExists(connection, "LocalUsersv2");
|
||||
|
||||
connection.RunQueries(new[] {
|
||||
@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Data
|
||||
TryMigrateToLocalUsersTable(connection);
|
||||
}
|
||||
|
||||
RemoveEmptyPasswordHashes();
|
||||
RemoveEmptyPasswordHashes(connection);
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,13 +73,13 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveEmptyPasswordHashes()
|
||||
private void RemoveEmptyPasswordHashes(ManagedConnection connection)
|
||||
{
|
||||
foreach (var user in RetrieveAllUsers())
|
||||
foreach (var user in RetrieveAllUsers(connection))
|
||||
{
|
||||
// If the user password is the sha1 hash of the empty string, remove it
|
||||
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
|
||||
|| !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
|
||||
&& !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -89,22 +87,16 @@ namespace Emby.Server.Implementations.Data
|
||||
user.Password = null;
|
||||
var serialized = _jsonSerializer.SerializeToBytes(user);
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = CreateConnection())
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
|
||||
{
|
||||
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
|
||||
{
|
||||
statement.TryBind("@InternalId", user.InternalId);
|
||||
statement.TryBind("@data", serialized);
|
||||
statement.MoveNext();
|
||||
}
|
||||
|
||||
}, TransactionMode);
|
||||
}
|
||||
statement.TryBind("@InternalId", user.InternalId);
|
||||
statement.TryBind("@data", serialized);
|
||||
statement.MoveNext();
|
||||
}
|
||||
}, TransactionMode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -119,31 +111,28 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
var serialized = _jsonSerializer.SerializeToBytes(user);
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
|
||||
{
|
||||
using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
|
||||
{
|
||||
statement.TryBind("@guid", user.Id.ToGuidBlob());
|
||||
statement.TryBind("@data", serialized);
|
||||
statement.TryBind("@guid", user.Id.ToGuidBlob());
|
||||
statement.TryBind("@data", serialized);
|
||||
|
||||
statement.MoveNext();
|
||||
}
|
||||
statement.MoveNext();
|
||||
}
|
||||
|
||||
var createdUser = GetUser(user.Id, false);
|
||||
var createdUser = GetUser(user.Id, connection);
|
||||
|
||||
if (createdUser == null)
|
||||
{
|
||||
throw new ApplicationException("created user should never be null");
|
||||
}
|
||||
if (createdUser == null)
|
||||
{
|
||||
throw new ApplicationException("created user should never be null");
|
||||
}
|
||||
|
||||
user.InternalId = createdUser.InternalId;
|
||||
user.InternalId = createdUser.InternalId;
|
||||
|
||||
}, TransactionMode);
|
||||
}
|
||||
}, TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,39 +145,30 @@ namespace Emby.Server.Implementations.Data
|
||||
|
||||
var serialized = _jsonSerializer.SerializeToBytes(user);
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
|
||||
{
|
||||
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
|
||||
{
|
||||
statement.TryBind("@InternalId", user.InternalId);
|
||||
statement.TryBind("@data", serialized);
|
||||
statement.MoveNext();
|
||||
}
|
||||
statement.TryBind("@InternalId", user.InternalId);
|
||||
statement.TryBind("@data", serialized);
|
||||
statement.MoveNext();
|
||||
}
|
||||
|
||||
}, TransactionMode);
|
||||
}
|
||||
}, TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
private User GetUser(Guid guid, bool openLock)
|
||||
private User GetUser(Guid guid, ManagedConnection connection)
|
||||
{
|
||||
using (openLock ? WriteLock.Read() : null)
|
||||
using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
|
||||
{
|
||||
using (var connection = CreateConnection(true))
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
|
||||
{
|
||||
statement.TryBind("@guid", guid);
|
||||
statement.TryBind("@guid", guid);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return GetUser(row);
|
||||
}
|
||||
}
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return GetUser(row);
|
||||
}
|
||||
}
|
||||
|
||||
@ -216,20 +196,22 @@ namespace Emby.Server.Implementations.Data
|
||||
/// <returns>IEnumerable{User}.</returns>
|
||||
public List<User> RetrieveAllUsers()
|
||||
{
|
||||
var list = new List<User>();
|
||||
|
||||
using (WriteLock.Read())
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
using (var connection = CreateConnection(true))
|
||||
{
|
||||
foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
|
||||
{
|
||||
list.Add(GetUser(row));
|
||||
}
|
||||
}
|
||||
return new List<User>(RetrieveAllUsers(connection));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
/// <summary>
|
||||
/// Retrieve all users from the database
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{User}.</returns>
|
||||
private IEnumerable<User> RetrieveAllUsers(ManagedConnection connection)
|
||||
{
|
||||
foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
|
||||
{
|
||||
yield return GetUser(row);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -245,19 +227,16 @@ namespace Emby.Server.Implementations.Data
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
using (WriteLock.Write())
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
using (var connection = CreateConnection())
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id"))
|
||||
{
|
||||
using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id"))
|
||||
{
|
||||
statement.TryBind("@id", user.InternalId);
|
||||
statement.MoveNext();
|
||||
}
|
||||
}, TransactionMode);
|
||||
}
|
||||
statement.TryBind("@id", user.InternalId);
|
||||
statement.MoveNext();
|
||||
}
|
||||
}, TransactionMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,14 +89,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
var channelTuples = new List<Tuple<BaseItemDto, LiveTvChannel>>();
|
||||
|
||||
var index = 0;
|
||||
var allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var dto = GetBaseItemDtoInternal(item, options, allCollectionFolders, user, owner);
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||
|
||||
var tvChannel = item as LiveTvChannel;
|
||||
if (tvChannel != null)
|
||||
if (item is LiveTvChannel tvChannel)
|
||||
{
|
||||
channelTuples.Add(new Tuple<BaseItemDto, LiveTvChannel>(dto, tvChannel));
|
||||
}
|
||||
@ -105,9 +102,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
programTuples.Add(new Tuple<BaseItem, BaseItemDto>(item, dto));
|
||||
}
|
||||
|
||||
var byName = item as IItemByName;
|
||||
|
||||
if (byName != null)
|
||||
if (item is IItemByName byName)
|
||||
{
|
||||
if (options.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
@ -130,8 +125,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (programTuples.Count > 0)
|
||||
{
|
||||
var task = _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user);
|
||||
Task.WaitAll(task);
|
||||
_livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (channelTuples.Count > 0)
|
||||
@ -144,8 +138,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null)
|
||||
{
|
||||
var allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
|
||||
var dto = GetBaseItemDtoInternal(item, options, allCollectionFolders, user, owner);
|
||||
var dto = GetBaseItemDtoInternal(item, options, user, owner);
|
||||
var tvChannel = item as LiveTvChannel;
|
||||
if (tvChannel != null)
|
||||
{
|
||||
@ -188,7 +181,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
});
|
||||
}
|
||||
|
||||
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, List<Folder> allCollectionFolders, User user = null, BaseItem owner = null)
|
||||
private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null)
|
||||
{
|
||||
var dto = new BaseItemDto
|
||||
{
|
||||
@ -312,6 +305,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
{
|
||||
path = path.TrimStart('.');
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
fileExtensionContainer = path;
|
||||
@ -325,8 +319,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null)
|
||||
{
|
||||
var allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
|
||||
var dto = GetBaseItemDtoInternal(item, options, allCollectionFolders, user);
|
||||
var dto = GetBaseItemDtoInternal(item, options, user);
|
||||
|
||||
if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts))
|
||||
{
|
||||
@ -1051,14 +1044,15 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaStreams = dto.MediaSources.Where(i => string.Equals(i.Id, item.Id.ToString("N"), StringComparison.OrdinalIgnoreCase))
|
||||
string id = item.Id.ToString("N");
|
||||
mediaStreams = dto.MediaSources.Where(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase))
|
||||
.SelectMany(i => i.MediaStreams)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaStreams = _mediaSourceManager().GetStaticMediaSources(item, true).First().MediaStreams.ToArray();
|
||||
mediaStreams = _mediaSourceManager().GetStaticMediaSources(item, true)[0].MediaStreams.ToArray();
|
||||
}
|
||||
|
||||
dto.MediaStreams = mediaStreams;
|
||||
|
@ -20,6 +20,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IPNetwork2" Version="2.4.0.126" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
|
||||
@ -31,10 +32,9 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.4.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.22.0" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.5.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.23.0" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="1.0.0" />
|
||||
<PackageReference Include="UTF.Unknown" Version="1.0.0-beta1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@ -52,8 +52,8 @@
|
||||
|
||||
<!-- Code analysers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.3" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -388,7 +388,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(),
|
||||
|
||||
CollectionFolders = GetTopParentIds(newAndRemoved, user, allUserRootChildren).ToArray()
|
||||
CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
@ -407,7 +407,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
return item.SourceType == SourceType.Library;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetTopParentIds(List<BaseItem> items, User user, List<Folder> allUserRootChildren)
|
||||
private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
|
||||
{
|
||||
var list = new List<string>();
|
||||
|
||||
|
@ -1,18 +0,0 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpClientManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Class HttpClientInfo
|
||||
/// </summary>
|
||||
public class HttpClientInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the last timeout.
|
||||
/// </summary>
|
||||
/// <value>The last timeout.</value>
|
||||
public DateTime LastTimeout { get; set; }
|
||||
public HttpClient HttpClient { get; set; }
|
||||
}
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Cache;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -24,30 +23,24 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
/// </summary>
|
||||
public class HttpClientManager : IHttpClient
|
||||
{
|
||||
/// <summary>
|
||||
/// When one request to a host times out, we'll ban all other requests for this period of time, to prevent scans from stalling
|
||||
/// </summary>
|
||||
private const int TimeoutSeconds = 30;
|
||||
|
||||
/// <summary>
|
||||
/// The _logger
|
||||
/// </summary>
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// The _app paths
|
||||
/// </summary>
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly Func<string> _defaultUserAgentFn;
|
||||
|
||||
/// <summary>
|
||||
/// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
|
||||
/// DON'T dispose it after use.
|
||||
/// </summary>
|
||||
/// <value>The HTTP clients.</value>
|
||||
private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpClientManager" /> class.
|
||||
/// </summary>
|
||||
public HttpClientManager(
|
||||
IApplicationPaths appPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger<HttpClientManager> logger,
|
||||
IFileSystem fileSystem,
|
||||
Func<string> defaultUserAgentFn)
|
||||
{
|
||||
@ -55,46 +48,33 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
{
|
||||
throw new ArgumentNullException(nameof(appPaths));
|
||||
}
|
||||
if (loggerFactory == null)
|
||||
|
||||
if (logger == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(loggerFactory));
|
||||
throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
_logger = loggerFactory.CreateLogger("HttpClient");
|
||||
_logger = logger;
|
||||
_fileSystem = fileSystem;
|
||||
_appPaths = appPaths;
|
||||
_defaultUserAgentFn = defaultUserAgentFn;
|
||||
|
||||
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
|
||||
ServicePointManager.Expect100Continue = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests.
|
||||
/// DON'T dispose it after use.
|
||||
/// Gets the correct http client for the given url.
|
||||
/// </summary>
|
||||
/// <value>The HTTP clients.</value>
|
||||
private readonly ConcurrentDictionary<string, HttpClientInfo> _httpClients = new ConcurrentDictionary<string, HttpClientInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets
|
||||
/// </summary>
|
||||
/// <param name="host">The host.</param>
|
||||
/// <param name="enableHttpCompression">if set to <c>true</c> [enable HTTP compression].</param>
|
||||
/// <param name="url">The url.</param>
|
||||
/// <returns>HttpClient.</returns>
|
||||
/// <exception cref="ArgumentNullException">host</exception>
|
||||
private HttpClientInfo GetHttpClient(string host, bool enableHttpCompression)
|
||||
private HttpClient GetHttpClient(string url)
|
||||
{
|
||||
if (string.IsNullOrEmpty(host))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(host));
|
||||
}
|
||||
|
||||
var key = host + enableHttpCompression;
|
||||
var key = GetHostFromUrl(url);
|
||||
|
||||
if (!_httpClients.TryGetValue(key, out var client))
|
||||
{
|
||||
client = new HttpClientInfo();
|
||||
client = new HttpClient()
|
||||
{
|
||||
BaseAddress = new Uri(url)
|
||||
};
|
||||
|
||||
_httpClients.TryAdd(key, client);
|
||||
}
|
||||
@ -102,119 +82,84 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
return client;
|
||||
}
|
||||
|
||||
private WebRequest GetRequest(HttpRequestOptions options, string method)
|
||||
private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method)
|
||||
{
|
||||
string url = options.Url;
|
||||
|
||||
var uriAddress = new Uri(url);
|
||||
string userInfo = uriAddress.UserInfo;
|
||||
if (!string.IsNullOrWhiteSpace(userInfo))
|
||||
{
|
||||
_logger.LogInformation("Found userInfo in url: {0} ... url: {1}", userInfo, url);
|
||||
url = url.Replace(userInfo + "@", string.Empty);
|
||||
_logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
|
||||
url = url.Replace(userInfo + '@', string.Empty);
|
||||
}
|
||||
|
||||
var request = WebRequest.Create(url);
|
||||
var request = new HttpRequestMessage(method, url);
|
||||
|
||||
if (request is HttpWebRequest httpWebRequest)
|
||||
AddRequestHeaders(request, options);
|
||||
|
||||
switch (options.DecompressionMethod)
|
||||
{
|
||||
AddRequestHeaders(httpWebRequest, options);
|
||||
|
||||
if (options.EnableHttpCompression)
|
||||
{
|
||||
httpWebRequest.AutomaticDecompression = DecompressionMethods.Deflate;
|
||||
if (options.DecompressionMethod.HasValue
|
||||
&& options.DecompressionMethod.Value == CompressionMethod.Gzip)
|
||||
{
|
||||
httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
httpWebRequest.AutomaticDecompression = DecompressionMethods.None;
|
||||
}
|
||||
|
||||
httpWebRequest.KeepAlive = options.EnableKeepAlive;
|
||||
|
||||
if (!string.IsNullOrEmpty(options.Host))
|
||||
{
|
||||
httpWebRequest.Host = options.Host;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(options.Referer))
|
||||
{
|
||||
httpWebRequest.Referer = options.Referer;
|
||||
}
|
||||
case CompressionMethod.Deflate | CompressionMethod.Gzip:
|
||||
request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" });
|
||||
break;
|
||||
case CompressionMethod.Deflate:
|
||||
request.Headers.Add(HeaderNames.AcceptEncoding, "deflate");
|
||||
break;
|
||||
case CompressionMethod.Gzip:
|
||||
request.Headers.Add(HeaderNames.AcceptEncoding, "gzip");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache);
|
||||
if (options.EnableKeepAlive)
|
||||
{
|
||||
request.Headers.Add(HeaderNames.Connection, "Keep-Alive");
|
||||
}
|
||||
|
||||
request.Method = method;
|
||||
request.Timeout = options.TimeoutMs;
|
||||
//request.Headers.Add(HeaderNames.CacheControl, "no-cache");
|
||||
|
||||
/*
|
||||
if (!string.IsNullOrWhiteSpace(userInfo))
|
||||
{
|
||||
var parts = userInfo.Split(':');
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
request.Credentials = GetCredential(url, parts[0], parts[1]);
|
||||
// TODO: .net core ??
|
||||
request.PreAuthenticate = true;
|
||||
request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private static CredentialCache GetCredential(string url, string username, string password)
|
||||
{
|
||||
//ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3;
|
||||
var credentialCache = new CredentialCache();
|
||||
credentialCache.Add(new Uri(url), "Basic", new NetworkCredential(username, password));
|
||||
return credentialCache;
|
||||
}
|
||||
|
||||
private void AddRequestHeaders(HttpWebRequest request, HttpRequestOptions options)
|
||||
private void AddRequestHeaders(HttpRequestMessage request, HttpRequestOptions options)
|
||||
{
|
||||
var hasUserAgent = false;
|
||||
|
||||
foreach (var header in options.RequestHeaders)
|
||||
{
|
||||
if (string.Equals(header.Key, HeaderNames.Accept, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
request.Accept = header.Value;
|
||||
}
|
||||
else if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SetUserAgent(request, header.Value);
|
||||
hasUserAgent = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Set(header.Key, header.Value);
|
||||
}
|
||||
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (!hasUserAgent && options.EnableDefaultUserAgent)
|
||||
{
|
||||
SetUserAgent(request, _defaultUserAgentFn());
|
||||
request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn());
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetUserAgent(HttpWebRequest request, string userAgent)
|
||||
{
|
||||
request.UserAgent = userAgent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response internal.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>Task{HttpResponseInfo}.</returns>
|
||||
public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
|
||||
{
|
||||
return SendAsync(options, "GET");
|
||||
}
|
||||
=> SendAsync(options, HttpMethod.Get);
|
||||
|
||||
/// <summary>
|
||||
/// Performs a GET request and returns the resulting stream
|
||||
@ -233,9 +178,16 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="httpMethod">The HTTP method.</param>
|
||||
/// <returns>Task{HttpResponseInfo}.</returns>
|
||||
/// <exception cref="HttpException">
|
||||
/// </exception>
|
||||
public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
|
||||
public Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
|
||||
=> SendAsync(options, new HttpMethod(httpMethod));
|
||||
|
||||
/// <summary>
|
||||
/// send as an asynchronous operation.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="httpMethod">The HTTP method.</param>
|
||||
/// <returns>Task{HttpResponseInfo}.</returns>
|
||||
public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod)
|
||||
{
|
||||
if (options.CacheMode == CacheMode.None)
|
||||
{
|
||||
@ -294,186 +246,66 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, string httpMethod)
|
||||
private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod)
|
||||
{
|
||||
ValidateParams(options);
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
|
||||
var client = GetHttpClient(options.Url);
|
||||
|
||||
if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds)
|
||||
var httpWebRequest = GetRequestMessage(options, httpMethod);
|
||||
|
||||
if (options.RequestContentBytes != null
|
||||
|| !string.IsNullOrEmpty(options.RequestContent)
|
||||
|| httpMethod == HttpMethod.Post)
|
||||
{
|
||||
throw new HttpException(string.Format("Cancelling connection to {0} due to a previous timeout.", options.Url))
|
||||
if (options.RequestContentBytes != null)
|
||||
{
|
||||
IsTimedOut = true
|
||||
};
|
||||
}
|
||||
|
||||
var httpWebRequest = GetRequest(options, httpMethod);
|
||||
|
||||
if (options.RequestContentBytes != null ||
|
||||
!string.IsNullOrEmpty(options.RequestContent) ||
|
||||
string.Equals(httpMethod, "post", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = options.RequestContentBytes ?? Encoding.UTF8.GetBytes(options.RequestContent ?? string.Empty);
|
||||
|
||||
var contentType = options.RequestContentType ?? "application/x-www-form-urlencoded";
|
||||
|
||||
if (options.AppendCharsetToMimeType)
|
||||
{
|
||||
contentType = contentType.TrimEnd(';') + "; charset=\"utf-8\"";
|
||||
}
|
||||
|
||||
httpWebRequest.ContentType = contentType;
|
||||
(await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length);
|
||||
httpWebRequest.Content = new ByteArrayContent(options.RequestContentBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
else if (options.RequestContent != null)
|
||||
{
|
||||
throw new HttpException(ex.Message) { IsTimedOut = true };
|
||||
httpWebRequest.Content = new StringContent(
|
||||
options.RequestContent,
|
||||
null,
|
||||
options.RequestContentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpWebRequest.Content = new ByteArrayContent(Array.Empty<byte>());
|
||||
}
|
||||
}
|
||||
|
||||
if (options.ResourcePool != null)
|
||||
{
|
||||
await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow - client.LastTimeout).TotalSeconds < TimeoutSeconds)
|
||||
{
|
||||
options.ResourcePool?.Release();
|
||||
|
||||
throw new HttpException($"Connection to {options.Url} timed out") { IsTimedOut = true };
|
||||
}
|
||||
|
||||
if (options.LogRequest)
|
||||
{
|
||||
if (options.LogRequestAsDebug)
|
||||
{
|
||||
_logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url);
|
||||
}
|
||||
_logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url);
|
||||
}
|
||||
|
||||
try
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var response = await client.SendAsync(
|
||||
httpWebRequest,
|
||||
options.BufferContent ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead,
|
||||
options.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
return new HttpResponseInfo(response.Headers, response.Content.Headers)
|
||||
{
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!options.BufferContent)
|
||||
{
|
||||
var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false);
|
||||
|
||||
var httpResponse = (HttpWebResponse)response;
|
||||
|
||||
EnsureSuccessStatusCode(client, httpResponse, options);
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
return GetResponseInfo(httpResponse, httpResponse.GetResponseStream(), GetContentLength(httpResponse), httpResponse);
|
||||
}
|
||||
|
||||
using (var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false))
|
||||
{
|
||||
var httpResponse = (HttpWebResponse)response;
|
||||
|
||||
EnsureSuccessStatusCode(client, httpResponse, options);
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var stream = httpResponse.GetResponseStream())
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
return GetResponseInfo(httpResponse, memoryStream, memoryStream.Length, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
throw GetCancellationException(options, client, options.CancellationToken, ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw GetException(ex, options, client);
|
||||
}
|
||||
finally
|
||||
{
|
||||
options.ResourcePool?.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, Stream content, long? contentLength, IDisposable disposable)
|
||||
{
|
||||
var responseInfo = new HttpResponseInfo(disposable)
|
||||
{
|
||||
Content = content,
|
||||
StatusCode = httpResponse.StatusCode,
|
||||
ContentType = httpResponse.ContentType,
|
||||
ContentLength = contentLength,
|
||||
ResponseUrl = httpResponse.ResponseUri.ToString()
|
||||
Content = stream,
|
||||
StatusCode = response.StatusCode,
|
||||
ContentType = response.Content.Headers.ContentType?.MediaType,
|
||||
ContentLength = response.Content.Headers.ContentLength,
|
||||
ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
|
||||
};
|
||||
|
||||
if (httpResponse.Headers != null)
|
||||
{
|
||||
SetHeaders(httpResponse.Headers, responseInfo);
|
||||
}
|
||||
|
||||
return responseInfo;
|
||||
}
|
||||
|
||||
private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, string tempFile, long? contentLength)
|
||||
{
|
||||
var responseInfo = new HttpResponseInfo
|
||||
{
|
||||
TempFilePath = tempFile,
|
||||
StatusCode = httpResponse.StatusCode,
|
||||
ContentType = httpResponse.ContentType,
|
||||
ContentLength = contentLength
|
||||
};
|
||||
|
||||
if (httpResponse.Headers != null)
|
||||
{
|
||||
SetHeaders(httpResponse.Headers, responseInfo);
|
||||
}
|
||||
|
||||
return responseInfo;
|
||||
}
|
||||
|
||||
private static void SetHeaders(WebHeaderCollection headers, HttpResponseInfo responseInfo)
|
||||
{
|
||||
foreach (var key in headers.AllKeys)
|
||||
{
|
||||
responseInfo.Headers[key] = headers[key];
|
||||
}
|
||||
}
|
||||
|
||||
public Task<HttpResponseInfo> Post(HttpRequestOptions options)
|
||||
{
|
||||
return SendAsync(options, "POST");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a POST request
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="postData">Params to add to the POST data.</param>
|
||||
/// <returns>stream on success, null on failure</returns>
|
||||
public async Task<Stream> Post(HttpRequestOptions options, Dictionary<string, string> postData)
|
||||
{
|
||||
options.SetPostData(postData);
|
||||
|
||||
var response = await Post(options).ConfigureAwait(false);
|
||||
|
||||
return response.Content;
|
||||
}
|
||||
=> SendAsync(options, HttpMethod.Post);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the contents of a given url into a temporary location
|
||||
@ -483,7 +315,6 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
public async Task<string> GetTempFile(HttpRequestOptions options)
|
||||
{
|
||||
var response = await GetTempFileResponse(options).ConfigureAwait(false);
|
||||
|
||||
return response.TempFilePath;
|
||||
}
|
||||
|
||||
@ -502,44 +333,28 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var httpWebRequest = GetRequest(options, "GET");
|
||||
|
||||
if (options.ResourcePool != null)
|
||||
{
|
||||
await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
var httpWebRequest = GetRequestMessage(options, HttpMethod.Get);
|
||||
|
||||
options.Progress.Report(0);
|
||||
|
||||
if (options.LogRequest)
|
||||
{
|
||||
if (options.LogRequestAsDebug)
|
||||
{
|
||||
_logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
|
||||
}
|
||||
_logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
|
||||
}
|
||||
|
||||
var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression);
|
||||
var client = GetHttpClient(options.Url);
|
||||
|
||||
try
|
||||
{
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var response = await httpWebRequest.GetResponseAsync().ConfigureAwait(false))
|
||||
using (var response = (await client.SendAsync(httpWebRequest, options.CancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
var httpResponse = (HttpWebResponse)response;
|
||||
|
||||
EnsureSuccessStatusCode(client, httpResponse, options);
|
||||
await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
|
||||
|
||||
options.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var contentLength = GetContentLength(httpResponse);
|
||||
|
||||
using (var stream = httpResponse.GetResponseStream())
|
||||
using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
|
||||
{
|
||||
await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
|
||||
@ -547,35 +362,29 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
|
||||
options.Progress.Report(100);
|
||||
|
||||
return GetResponseInfo(httpResponse, tempFile, contentLength);
|
||||
var responseInfo = new HttpResponseInfo(response.Headers, response.Content.Headers)
|
||||
{
|
||||
TempFilePath = tempFile,
|
||||
StatusCode = response.StatusCode,
|
||||
ContentType = response.Content.Headers.ContentType?.MediaType,
|
||||
ContentLength = response.Content.Headers.ContentLength
|
||||
};
|
||||
|
||||
return responseInfo;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DeleteTempFile(tempFile);
|
||||
throw GetException(ex, options, client);
|
||||
}
|
||||
finally
|
||||
{
|
||||
options.ResourcePool?.Release();
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
|
||||
throw GetException(ex, options);
|
||||
}
|
||||
}
|
||||
|
||||
private static long? GetContentLength(HttpWebResponse response)
|
||||
{
|
||||
var length = response.ContentLength;
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
protected static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
private Exception GetException(Exception ex, HttpRequestOptions options, HttpClientInfo client)
|
||||
private Exception GetException(Exception ex, HttpRequestOptions options)
|
||||
{
|
||||
if (ex is HttpException)
|
||||
{
|
||||
@ -589,7 +398,7 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
{
|
||||
if (options.LogErrors)
|
||||
{
|
||||
_logger.LogError(webException, "Error {status} getting response from {url}", webException.Status, options.Url);
|
||||
_logger.LogError(webException, "Error {Status} getting response from {Url}", webException.Status, options.Url);
|
||||
}
|
||||
|
||||
var exception = new HttpException(webException.Message, webException);
|
||||
@ -599,11 +408,6 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
if (response != null)
|
||||
{
|
||||
exception.StatusCode = response.StatusCode;
|
||||
|
||||
if ((int)response.StatusCode == 429)
|
||||
{
|
||||
client.LastTimeout = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -624,29 +428,17 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
|
||||
if (operationCanceledException != null)
|
||||
{
|
||||
return GetCancellationException(options, client, options.CancellationToken, operationCanceledException);
|
||||
return GetCancellationException(options, options.CancellationToken, operationCanceledException);
|
||||
}
|
||||
|
||||
if (options.LogErrors)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting response from {url}", options.Url);
|
||||
_logger.LogError(ex, "Error getting response from {Url}", options.Url);
|
||||
}
|
||||
|
||||
return ex;
|
||||
}
|
||||
|
||||
private void DeleteTempFile(string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(file);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Might not have been created at all. No need to worry.
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateParams(HttpRequestOptions options)
|
||||
{
|
||||
if (string.IsNullOrEmpty(options.Url))
|
||||
@ -682,11 +474,10 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
/// Throws the cancellation exception.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <param name="exception">The exception.</param>
|
||||
/// <returns>Exception.</returns>
|
||||
private Exception GetCancellationException(HttpRequestOptions options, HttpClientInfo client, CancellationToken cancellationToken, OperationCanceledException exception)
|
||||
private Exception GetCancellationException(HttpRequestOptions options, CancellationToken cancellationToken, OperationCanceledException exception)
|
||||
{
|
||||
// If the HttpClient's timeout is reached, it will cancel the Task internally
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
@ -698,8 +489,6 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
_logger.LogError(msg);
|
||||
}
|
||||
|
||||
client.LastTimeout = DateTime.UtcNow;
|
||||
|
||||
// Throw an HttpException so that the caller doesn't think it was cancelled by user code
|
||||
return new HttpException(msg, exception)
|
||||
{
|
||||
@ -710,91 +499,20 @@ namespace Emby.Server.Implementations.HttpClientManager
|
||||
return exception;
|
||||
}
|
||||
|
||||
private void EnsureSuccessStatusCode(HttpClientInfo client, HttpWebResponse response, HttpRequestOptions options)
|
||||
private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options)
|
||||
{
|
||||
var statusCode = response.StatusCode;
|
||||
|
||||
var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299;
|
||||
|
||||
if (isSuccessful)
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (options.LogErrorResponseBody)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var stream = response.GetResponseStream())
|
||||
{
|
||||
if (stream != null)
|
||||
{
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var msg = reader.ReadToEnd();
|
||||
var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
_logger.LogError("HTTP request failed with message: {Message}", msg);
|
||||
|
||||
_logger.LogError(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
throw new HttpException(response.StatusDescription)
|
||||
throw new HttpException(response.ReasonPhrase)
|
||||
{
|
||||
StatusCode = response.StatusCode
|
||||
};
|
||||
}
|
||||
|
||||
private static Task<WebResponse> GetResponseAsync(WebRequest request, TimeSpan timeout)
|
||||
{
|
||||
var taskCompletion = new TaskCompletionSource<WebResponse>();
|
||||
|
||||
var asyncTask = Task.Factory.FromAsync(request.BeginGetResponse, request.EndGetResponse, null);
|
||||
|
||||
ThreadPool.RegisterWaitForSingleObject((asyncTask as IAsyncResult).AsyncWaitHandle, TimeoutCallback, request, timeout, true);
|
||||
var callback = new TaskCallback { taskCompletion = taskCompletion };
|
||||
asyncTask.ContinueWith(callback.OnSuccess, TaskContinuationOptions.NotOnFaulted);
|
||||
|
||||
// Handle errors
|
||||
asyncTask.ContinueWith(callback.OnError, TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
return taskCompletion.Task;
|
||||
}
|
||||
|
||||
private static void TimeoutCallback(object state, bool timedOut)
|
||||
{
|
||||
if (timedOut && state != null)
|
||||
{
|
||||
var request = (WebRequest)state;
|
||||
request.Abort();
|
||||
}
|
||||
}
|
||||
|
||||
private class TaskCallback
|
||||
{
|
||||
public TaskCompletionSource<WebResponse> taskCompletion;
|
||||
|
||||
public void OnSuccess(Task<WebResponse> task)
|
||||
{
|
||||
taskCompletion.TrySetResult(task.Result);
|
||||
}
|
||||
|
||||
public void OnError(Task<WebResponse> task)
|
||||
{
|
||||
if (task.Exception == null)
|
||||
{
|
||||
taskCompletion.TrySetException(Enumerable.Empty<Exception>());
|
||||
}
|
||||
else
|
||||
{
|
||||
taskCompletion.TrySetException(task.Exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rangeHeader))
|
||||
{
|
||||
Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
|
||||
StatusCode = HttpStatusCode.OK;
|
||||
}
|
||||
else
|
||||
@ -99,10 +100,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
RangeStart = requestedRange.Key;
|
||||
RangeLength = 1 + RangeEnd - RangeStart;
|
||||
|
||||
// Content-Length is the length of what we're serving, not the original content
|
||||
var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture);
|
||||
Headers[HeaderNames.ContentLength] = lengthString;
|
||||
var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
|
||||
Headers[HeaderNames.ContentRange] = rangeString;
|
||||
|
||||
Logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Range: {2}", Path, RangeHeader, rangeString);
|
||||
Logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
@ -11,7 +10,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Net;
|
||||
using Emby.Server.Implementations.Services;
|
||||
using Emby.Server.Implementations.SocketSharp;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
@ -127,12 +125,12 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
|
||||
{
|
||||
var attributes = requestDtoType.GetTypeInfo().GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
|
||||
var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
|
||||
|
||||
var serviceType = GetServiceTypeByRequest(requestDtoType);
|
||||
if (serviceType != null)
|
||||
{
|
||||
attributes.AddRange(serviceType.GetTypeInfo().GetCustomAttributes(true).OfType<IHasRequestFilter>());
|
||||
attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
|
||||
}
|
||||
|
||||
attributes.Sort((x, y) => x.Priority - y.Priority);
|
||||
@ -154,7 +152,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
QueryString = e.QueryString ?? new QueryCollection()
|
||||
};
|
||||
|
||||
connection.Closed += Connection_Closed;
|
||||
connection.Closed += OnConnectionClosed;
|
||||
|
||||
lock (_webSocketConnections)
|
||||
{
|
||||
@ -164,7 +162,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
|
||||
}
|
||||
|
||||
private void Connection_Closed(object sender, EventArgs e)
|
||||
private void OnConnectionClosed(object sender, EventArgs e)
|
||||
{
|
||||
lock (_webSocketConnections)
|
||||
{
|
||||
@ -203,6 +201,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
case DirectoryNotFoundException _:
|
||||
case FileNotFoundException _:
|
||||
case ResourceNotFoundException _: return 404;
|
||||
case MethodNotAllowedException _: return 405;
|
||||
case RemoteServiceUnavailableException _: return 502;
|
||||
default: return 500;
|
||||
}
|
||||
@ -322,14 +321,14 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
private static string NormalizeConfiguredLocalAddress(string address)
|
||||
{
|
||||
var index = address.Trim('/').IndexOf('/');
|
||||
|
||||
var add = address.AsSpan().Trim('/');
|
||||
int index = add.IndexOf('/');
|
||||
if (index != -1)
|
||||
{
|
||||
address = address.Substring(index + 1);
|
||||
add = add.Slice(index + 1);
|
||||
}
|
||||
|
||||
return address.Trim('/');
|
||||
return add.TrimStart('/').ToString();
|
||||
}
|
||||
|
||||
private bool ValidateHost(string host)
|
||||
@ -399,8 +398,8 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
|
||||
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 ||
|
||||
urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -572,7 +571,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, httpReq.OperationName, cancellationToken).ConfigureAwait(false);
|
||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -613,21 +612,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
var pathInfo = httpReq.PathInfo;
|
||||
|
||||
var pathParts = pathInfo.TrimStart('/').Split('/');
|
||||
if (pathParts.Length == 0)
|
||||
{
|
||||
Logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
var restPath = ServiceHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out string contentType);
|
||||
pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
|
||||
var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
|
||||
if (restPath != null)
|
||||
{
|
||||
return new ServiceHandler
|
||||
{
|
||||
RestPath = restPath,
|
||||
ResponseContentType = contentType
|
||||
};
|
||||
return new ServiceHandler(restPath, contentType);
|
||||
}
|
||||
|
||||
Logger.LogError("Could not find handler for {PathInfo}", pathInfo);
|
||||
@ -637,6 +626,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
private static Task Write(IResponse response, string text)
|
||||
{
|
||||
var bOutput = Encoding.UTF8.GetBytes(text);
|
||||
response.OriginalResponse.ContentLength = bOutput.Length;
|
||||
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
|
||||
}
|
||||
|
||||
@ -655,11 +645,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO what is this?
|
||||
var httpsUrl = url
|
||||
.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
RedirectToUrl(httpRes, url);
|
||||
}
|
||||
}
|
||||
@ -684,10 +669,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
UrlPrefixes = urlPrefixes.ToArray();
|
||||
ServiceController = new ServiceController();
|
||||
|
||||
Logger.LogInformation("Calling ServiceStack AppHost.Init");
|
||||
|
||||
var types = services.Select(r => r.GetType()).ToArray();
|
||||
|
||||
var types = services.Select(r => r.GetType());
|
||||
ServiceController.Init(this, types);
|
||||
|
||||
ResponseFilters = new Action<IRequest, IResponse, object>[]
|
||||
@ -823,19 +805,15 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
Logger.LogDebug("Websocket message received: {0}", result.MessageType);
|
||||
|
||||
var tasks = _webSocketListeners.Select(i => Task.Run(async () =>
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
try
|
||||
foreach (var x in _webSocketListeners)
|
||||
{
|
||||
await i.ProcessMessage(result).ConfigureAwait(false);
|
||||
yield return x.ProcessMessageAsync(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "{0} failed processing WebSocket message {1}", i.GetType().Name, result.MessageType ?? string.Empty);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
return Task.WhenAll(tasks);
|
||||
return Task.WhenAll(GetTasks());
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires))
|
||||
{
|
||||
responseHeaders[HeaderNames.Expires] = "-1";
|
||||
responseHeaders[HeaderNames.Expires] = "0";
|
||||
}
|
||||
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
content = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
result = new StreamWriter(content, contentType);
|
||||
result = new StreamWriter(content, contentType, contentLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
|
||||
{
|
||||
responseHeaders[HeaderNames.Expires] = "-1";
|
||||
responseHeaders[HeaderNames.Expires] = "0";
|
||||
}
|
||||
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
bytes = Array.Empty<byte>();
|
||||
}
|
||||
|
||||
result = new StreamWriter(bytes, contentType);
|
||||
result = new StreamWriter(bytes, contentType, contentLength);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -190,7 +190,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
|
||||
{
|
||||
responseHeaders[HeaderNames.Expires] = "-1";
|
||||
responseHeaders[HeaderNames.Expires] = "0";
|
||||
}
|
||||
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
responseHeaders[HeaderNames.Expires] = "-1";
|
||||
responseHeaders[HeaderNames.Expires] = "0";
|
||||
|
||||
return ToOptimizedResultInternal(requestContext, result, responseHeaders);
|
||||
}
|
||||
@ -335,13 +335,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
var result = new StreamWriter(Array.Empty<byte>(), contentType);
|
||||
var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = new StreamWriter(content, contentType);
|
||||
var result = new StreamWriter(content, contentType, contentLength);
|
||||
AddResponseHeaders(result, responseHeaders);
|
||||
return result;
|
||||
}
|
||||
@ -581,6 +581,11 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
}
|
||||
else
|
||||
{
|
||||
if (totalContentLength.HasValue)
|
||||
{
|
||||
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (isHeadRequest)
|
||||
{
|
||||
using (stream)
|
||||
@ -624,7 +629,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (lastModifiedDate.HasValue)
|
||||
{
|
||||
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.ToString();
|
||||
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,6 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
RangeStart = requestedRange.Key;
|
||||
RangeLength = 1 + RangeEnd - RangeStart;
|
||||
|
||||
Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
|
||||
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
|
||||
|
||||
if (RangeStart > 0 && SourceStream.CanSeek)
|
||||
|
@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
public void FilterResponse(IRequest req, IResponse res, object dto)
|
||||
{
|
||||
// Try to prevent compatibility view
|
||||
res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
|
||||
res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
|
||||
res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
||||
res.AddHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
@ -58,6 +58,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
if (length > 0)
|
||||
{
|
||||
res.OriginalResponse.ContentLength = length;
|
||||
res.SendChunked = false;
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
// This code is executed before the service
|
||||
var auth = AuthorizationContext.GetAuthorizationInfo(request);
|
||||
|
||||
if (!IsExemptFromAuthenticationToken(auth, authAttribtues, request))
|
||||
if (!IsExemptFromAuthenticationToken(authAttribtues, request))
|
||||
{
|
||||
ValidateSecurityToken(request, auth.Token);
|
||||
}
|
||||
@ -122,7 +122,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsExemptFromAuthenticationToken(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request)
|
||||
private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request)
|
||||
{
|
||||
if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
|
||||
{
|
||||
|
@ -14,8 +14,6 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
public class StreamWriter : IAsyncStreamWriter, IHasHeaders
|
||||
{
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the source stream.
|
||||
/// </summary>
|
||||
@ -52,6 +50,13 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
SourceStream = source;
|
||||
|
||||
Headers["Content-Type"] = contentType;
|
||||
|
||||
if (source.CanSeek)
|
||||
{
|
||||
Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
Headers[HeaderNames.ContentType] = contentType;
|
||||
}
|
||||
|
||||
@ -60,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="contentType">Type of the content.</param>
|
||||
public StreamWriter(byte[] source, string contentType)
|
||||
public StreamWriter(byte[] source, string contentType, int contentLength)
|
||||
{
|
||||
if (string.IsNullOrEmpty(contentType))
|
||||
{
|
||||
@ -69,6 +74,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
|
||||
SourceBytes = source;
|
||||
|
||||
Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
|
||||
Headers[HeaderNames.ContentType] = contentType;
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,6 @@ using System.Threading;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.System;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
@ -61,6 +57,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
AddAffectedPath(path);
|
||||
}
|
||||
|
||||
RestartTimer();
|
||||
}
|
||||
|
||||
@ -103,6 +100,7 @@ namespace Emby.Server.Implementations.IO
|
||||
AddAffectedPath(affectedFile);
|
||||
}
|
||||
}
|
||||
|
||||
RestartTimer();
|
||||
}
|
||||
|
||||
|
@ -9,9 +9,7 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
@ -21,6 +19,7 @@ namespace Emby.Server.Implementations.IO
|
||||
/// The file system watchers
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// The affected paths
|
||||
/// </summary>
|
||||
@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.IO
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
// This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to.
|
||||
// This is an arbitrary amount of time, but delay it because file system writes often trigger events long after the file was actually written to.
|
||||
// Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
|
||||
// But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
|
||||
await Task.Delay(45000).ConfigureAwait(false);
|
||||
@ -162,10 +161,10 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
public void Start()
|
||||
{
|
||||
LibraryManager.ItemAdded += LibraryManager_ItemAdded;
|
||||
LibraryManager.ItemRemoved += LibraryManager_ItemRemoved;
|
||||
LibraryManager.ItemAdded += OnLibraryManagerItemAdded;
|
||||
LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
|
||||
|
||||
var pathsToWatch = new List<string> { };
|
||||
var pathsToWatch = new List<string>();
|
||||
|
||||
var paths = LibraryManager
|
||||
.RootFolder
|
||||
@ -204,7 +203,7 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (e.Parent is AggregateFolder)
|
||||
{
|
||||
@ -217,7 +216,7 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (e.Parent is AggregateFolder)
|
||||
{
|
||||
@ -244,7 +243,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
return lst.Any(str =>
|
||||
{
|
||||
//this should be a little quicker than examining each actual parent folder...
|
||||
// this should be a little quicker than examining each actual parent folder...
|
||||
var compare = str.TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
|
||||
@ -260,19 +259,10 @@ namespace Emby.Server.Implementations.IO
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
// Seeing a crash in the mono runtime due to an exception being thrown on a different thread
|
||||
Logger.LogInformation("Skipping realtime monitor for {0} because the path does not exist", path);
|
||||
Logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (OperatingSystem.Id != OperatingSystemId.Windows)
|
||||
{
|
||||
if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || path.StartsWith("smb://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// not supported
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Already being watched
|
||||
if (_fileSystemWatchers.ContainsKey(path))
|
||||
{
|
||||
@ -286,23 +276,21 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
var newWatcher = new FileSystemWatcher(path, "*")
|
||||
{
|
||||
IncludeSubdirectories = true
|
||||
IncludeSubdirectories = true,
|
||||
InternalBufferSize = 65536,
|
||||
NotifyFilter = NotifyFilters.CreationTime |
|
||||
NotifyFilters.DirectoryName |
|
||||
NotifyFilters.FileName |
|
||||
NotifyFilters.LastWrite |
|
||||
NotifyFilters.Size |
|
||||
NotifyFilters.Attributes
|
||||
};
|
||||
|
||||
newWatcher.InternalBufferSize = 65536;
|
||||
|
||||
newWatcher.NotifyFilter = NotifyFilters.CreationTime |
|
||||
NotifyFilters.DirectoryName |
|
||||
NotifyFilters.FileName |
|
||||
NotifyFilters.LastWrite |
|
||||
NotifyFilters.Size |
|
||||
NotifyFilters.Attributes;
|
||||
|
||||
newWatcher.Created += watcher_Changed;
|
||||
newWatcher.Deleted += watcher_Changed;
|
||||
newWatcher.Renamed += watcher_Changed;
|
||||
newWatcher.Changed += watcher_Changed;
|
||||
newWatcher.Error += watcher_Error;
|
||||
newWatcher.Created += OnWatcherChanged;
|
||||
newWatcher.Deleted += OnWatcherChanged;
|
||||
newWatcher.Renamed += OnWatcherChanged;
|
||||
newWatcher.Changed += OnWatcherChanged;
|
||||
newWatcher.Error += OnWatcherError;
|
||||
|
||||
if (_fileSystemWatchers.TryAdd(path, newWatcher))
|
||||
{
|
||||
@ -343,32 +331,16 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
using (watcher)
|
||||
{
|
||||
Logger.LogInformation("Stopping directory watching for path {path}", watcher.Path);
|
||||
Logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
|
||||
|
||||
watcher.Created -= watcher_Changed;
|
||||
watcher.Deleted -= watcher_Changed;
|
||||
watcher.Renamed -= watcher_Changed;
|
||||
watcher.Changed -= watcher_Changed;
|
||||
watcher.Error -= watcher_Error;
|
||||
watcher.Created -= OnWatcherChanged;
|
||||
watcher.Deleted -= OnWatcherChanged;
|
||||
watcher.Renamed -= OnWatcherChanged;
|
||||
watcher.Changed -= OnWatcherChanged;
|
||||
watcher.Error -= OnWatcherError;
|
||||
|
||||
try
|
||||
{
|
||||
watcher.EnableRaisingEvents = false;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Seeing this under mono on linux sometimes
|
||||
// Collection was modified; enumeration operation may not execute.
|
||||
}
|
||||
watcher.EnableRaisingEvents = false;
|
||||
}
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
// the dispose method on FileSystemWatcher is sometimes throwing NotImplementedException on Xamarin Android
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -385,7 +357,7 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <param name="watcher">The watcher.</param>
|
||||
private void RemoveWatcherFromList(FileSystemWatcher watcher)
|
||||
{
|
||||
_fileSystemWatchers.TryRemove(watcher.Path, out var removed);
|
||||
_fileSystemWatchers.TryRemove(watcher.Path, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -393,12 +365,12 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
|
||||
void watcher_Error(object sender, ErrorEventArgs e)
|
||||
private void OnWatcherError(object sender, ErrorEventArgs e)
|
||||
{
|
||||
var ex = e.GetException();
|
||||
var dw = (FileSystemWatcher)sender;
|
||||
|
||||
Logger.LogError(ex, "Error in Directory watcher for: {path}", dw.Path);
|
||||
Logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
|
||||
|
||||
DisposeWatcher(dw, true);
|
||||
}
|
||||
@ -408,15 +380,11 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
|
||||
void watcher_Changed(object sender, FileSystemEventArgs e)
|
||||
private void OnWatcherChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
//logger.LogDebug("Changed detected of type " + e.ChangeType + " to " + e.FullPath);
|
||||
|
||||
var path = e.FullPath;
|
||||
|
||||
ReportFileSystemChanged(path);
|
||||
ReportFileSystemChanged(e.FullPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -446,25 +414,22 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
if (_fileSystem.AreEqual(i, path))
|
||||
{
|
||||
Logger.LogDebug("Ignoring change to {path}", path);
|
||||
Logger.LogDebug("Ignoring change to {Path}", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_fileSystem.ContainsSubPath(i, path))
|
||||
{
|
||||
Logger.LogDebug("Ignoring change to {path}", path);
|
||||
Logger.LogDebug("Ignoring change to {Path}", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Go up a level
|
||||
var parent = Path.GetDirectoryName(i);
|
||||
if (!string.IsNullOrEmpty(parent))
|
||||
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
|
||||
{
|
||||
if (_fileSystem.AreEqual(parent, path))
|
||||
{
|
||||
Logger.LogDebug("Ignoring change to {path}", path);
|
||||
return true;
|
||||
}
|
||||
Logger.LogDebug("Ignoring change to {Path}", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -487,8 +452,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
lock (_activeRefreshers)
|
||||
{
|
||||
var refreshers = _activeRefreshers.ToList();
|
||||
foreach (var refresher in refreshers)
|
||||
foreach (var refresher in _activeRefreshers)
|
||||
{
|
||||
// Path is already being refreshed
|
||||
if (_fileSystem.AreEqual(path, refresher.Path))
|
||||
@ -536,8 +500,8 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
LibraryManager.ItemAdded -= LibraryManager_ItemAdded;
|
||||
LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved;
|
||||
LibraryManager.ItemAdded -= OnLibraryManagerItemAdded;
|
||||
LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
|
||||
|
||||
foreach (var watcher in _fileSystemWatchers.Values.ToList())
|
||||
{
|
||||
@ -565,17 +529,20 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
refresher.Dispose();
|
||||
}
|
||||
|
||||
_activeRefreshers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -19,24 +19,17 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
protected ILogger Logger;
|
||||
|
||||
private readonly bool _supportsAsyncFileStreams;
|
||||
private char[] _invalidFileNameChars;
|
||||
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
|
||||
|
||||
private readonly string _tempPath;
|
||||
|
||||
private readonly bool _isEnvironmentCaseInsensitive;
|
||||
|
||||
public ManagedFileSystem(
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger<ManagedFileSystem> logger,
|
||||
IApplicationPaths applicationPaths)
|
||||
{
|
||||
Logger = loggerFactory.CreateLogger("FileSystem");
|
||||
_supportsAsyncFileStreams = true;
|
||||
Logger = logger;
|
||||
_tempPath = applicationPaths.TempDirectory;
|
||||
|
||||
SetInvalidFileNameChars(OperatingSystem.Id == OperatingSystemId.Windows);
|
||||
|
||||
_isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
|
||||
}
|
||||
|
||||
@ -45,20 +38,6 @@ namespace Emby.Server.Implementations.IO
|
||||
_shortcutHandlers.Add(handler);
|
||||
}
|
||||
|
||||
protected void SetInvalidFileNameChars(bool enableManagedInvalidFileNameChars)
|
||||
{
|
||||
if (enableManagedInvalidFileNameChars)
|
||||
{
|
||||
_invalidFileNameChars = Path.GetInvalidFileNameChars();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Be consistent across platforms because the windows server will fail to query network shares that don't follow windows conventions
|
||||
// https://referencesource.microsoft.com/#mscorlib/system/io/path.cs
|
||||
_invalidFileNameChars = new char[] { '\"', '<', '>', '|', '\0', (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31, ':', '*', '?', '\\', '/' };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified filename is shortcut.
|
||||
/// </summary>
|
||||
@ -92,20 +71,22 @@ namespace Emby.Server.Implementations.IO
|
||||
var extension = Path.GetExtension(filename);
|
||||
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
return handler.Resolve(filename);
|
||||
}
|
||||
|
||||
return null;
|
||||
return handler?.Resolve(filename);
|
||||
}
|
||||
|
||||
public virtual string MakeAbsolutePath(string folderPath, string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath)) return filePath;
|
||||
if (string.IsNullOrWhiteSpace(filePath)
|
||||
// stream
|
||||
|| filePath.Contains("://"))
|
||||
{
|
||||
return filePath;
|
||||
}
|
||||
|
||||
if (filePath.Contains(@"://")) return filePath; //stream
|
||||
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') return filePath; //absolute local path
|
||||
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/')
|
||||
{
|
||||
return filePath; // absolute local path
|
||||
}
|
||||
|
||||
// unc path
|
||||
if (filePath.StartsWith("\\\\"))
|
||||
@ -125,9 +106,7 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
try
|
||||
{
|
||||
string path = System.IO.Path.Combine(folderPath, filePath);
|
||||
path = System.IO.Path.GetFullPath(path);
|
||||
return path;
|
||||
return Path.Combine(Path.GetFullPath(folderPath), filePath);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
@ -166,7 +145,7 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(shortcutPath);
|
||||
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
|
||||
var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (handler != null)
|
||||
{
|
||||
@ -244,12 +223,13 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info)
|
||||
{
|
||||
var result = new FileSystemMetadata();
|
||||
|
||||
result.Exists = info.Exists;
|
||||
result.FullName = info.FullName;
|
||||
result.Extension = info.Extension;
|
||||
result.Name = info.Name;
|
||||
var result = new FileSystemMetadata
|
||||
{
|
||||
Exists = info.Exists,
|
||||
FullName = info.FullName,
|
||||
Extension = info.Extension,
|
||||
Name = info.Name
|
||||
};
|
||||
|
||||
if (result.Exists)
|
||||
{
|
||||
@ -260,8 +240,7 @@ namespace Emby.Server.Implementations.IO
|
||||
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
|
||||
//}
|
||||
|
||||
var fileInfo = info as FileInfo;
|
||||
if (fileInfo != null)
|
||||
if (info is FileInfo fileInfo)
|
||||
{
|
||||
result.Length = fileInfo.Length;
|
||||
result.DirectoryName = fileInfo.DirectoryName;
|
||||
@ -307,7 +286,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
var builder = new StringBuilder(filename);
|
||||
|
||||
foreach (var c in _invalidFileNameChars)
|
||||
foreach (var c in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
builder = builder.Replace(c, ' ');
|
||||
}
|
||||
@ -394,7 +373,7 @@ namespace Emby.Server.Implementations.IO
|
||||
/// <returns>FileStream.</returns>
|
||||
public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false)
|
||||
{
|
||||
if (_supportsAsyncFileStreams && isAsync)
|
||||
if (isAsync)
|
||||
{
|
||||
return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous);
|
||||
}
|
||||
@ -666,7 +645,6 @@ namespace Emby.Server.Implementations.IO
|
||||
public virtual bool IsPathFile(string path)
|
||||
{
|
||||
// Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\
|
||||
|
||||
if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 &&
|
||||
!path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@ -674,8 +652,6 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
//return Path.IsPathRooted(path);
|
||||
}
|
||||
|
||||
public virtual void DeleteFile(string path)
|
||||
@ -686,13 +662,14 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
public virtual List<FileSystemMetadata> GetDrives()
|
||||
{
|
||||
// Only include drives in the ready state or this method could end up being very slow, waiting for drives to timeout
|
||||
return DriveInfo.GetDrives().Where(d => d.IsReady).Select(d => new FileSystemMetadata
|
||||
// check for ready state to avoid waiting for drives to timeout
|
||||
// some drives on linux have no actual size or are used for other purposes
|
||||
return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram)
|
||||
.Select(d => new FileSystemMetadata
|
||||
{
|
||||
Name = d.Name,
|
||||
FullName = d.RootDirectory.FullName,
|
||||
IsDirectory = true
|
||||
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
|
@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO
|
||||
try
|
||||
{
|
||||
int read;
|
||||
while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
|
||||
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (onStarted != null)
|
||||
{
|
||||
@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO
|
||||
if (emptyReadLimit <= 0)
|
||||
{
|
||||
int read;
|
||||
while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
|
||||
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return;
|
||||
@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
eofCount = 0;
|
||||
|
||||
await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -109,64 +109,6 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> CopyToAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
|
||||
try
|
||||
{
|
||||
int bytesRead;
|
||||
int totalBytesRead = 0;
|
||||
|
||||
while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
var bytesToWrite = bytesRead;
|
||||
|
||||
if (bytesToWrite > 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
return totalBytesRead;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
|
||||
try
|
||||
{
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0)
|
||||
{
|
||||
var bytesToWrite = Math.Min(bytesRead, copyLength);
|
||||
|
||||
if (bytesToWrite > 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
copyLength -= bytesToWrite;
|
||||
|
||||
if (copyLength <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
|
||||
@ -208,7 +150,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -225,7 +167,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
totalBytesRead += bytesRead;
|
||||
}
|
||||
|
@ -1,355 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for streaming data with throttling support.
|
||||
/// </summary>
|
||||
public class ThrottledStream : Stream
|
||||
{
|
||||
/// <summary>
|
||||
/// A constant used to specify an infinite number of bytes that can be transferred per second.
|
||||
/// </summary>
|
||||
public const long Infinite = 0;
|
||||
|
||||
#region Private members
|
||||
/// <summary>
|
||||
/// The base stream.
|
||||
/// </summary>
|
||||
private readonly Stream _baseStream;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum bytes per second that can be transferred through the base stream.
|
||||
/// </summary>
|
||||
private long _maximumBytesPerSecond;
|
||||
|
||||
/// <summary>
|
||||
/// The number of bytes that has been transferred since the last throttle.
|
||||
/// </summary>
|
||||
private long _byteCount;
|
||||
|
||||
/// <summary>
|
||||
/// The start time in milliseconds of the last throttle.
|
||||
/// </summary>
|
||||
private long _start;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the current milliseconds.
|
||||
/// </summary>
|
||||
/// <value>The current milliseconds.</value>
|
||||
protected long CurrentMilliseconds => Environment.TickCount;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum bytes per second that can be transferred through the base stream.
|
||||
/// </summary>
|
||||
/// <value>The maximum bytes per second.</value>
|
||||
public long MaximumBytesPerSecond
|
||||
{
|
||||
get => _maximumBytesPerSecond;
|
||||
set
|
||||
{
|
||||
if (MaximumBytesPerSecond != value)
|
||||
{
|
||||
_maximumBytesPerSecond = value;
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the current stream supports reading.
|
||||
/// </summary>
|
||||
/// <returns>true if the stream supports reading; otherwise, false.</returns>
|
||||
public override bool CanRead => _baseStream.CanRead;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the current stream supports seeking.
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
/// <returns>true if the stream supports seeking; otherwise, false.</returns>
|
||||
public override bool CanSeek => _baseStream.CanSeek;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the current stream supports writing.
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
/// <returns>true if the stream supports writing; otherwise, false.</returns>
|
||||
public override bool CanWrite => _baseStream.CanWrite;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length in bytes of the stream.
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
/// <returns>A long value representing the length of the stream in bytes.</returns>
|
||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support seeking. </exception>
|
||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
||||
public override long Length => _baseStream.Length;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position within the current stream.
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
/// <returns>The current position within the stream.</returns>
|
||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
|
||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support seeking. </exception>
|
||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
||||
public override long Position
|
||||
{
|
||||
get => _baseStream.Position;
|
||||
set => _baseStream.Position = value;
|
||||
}
|
||||
#endregion
|
||||
|
||||
public long MinThrottlePosition;
|
||||
|
||||
#region Ctor
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="T:ThrottledStream"/> class.
|
||||
/// </summary>
|
||||
/// <param name="baseStream">The base stream.</param>
|
||||
/// <param name="maximumBytesPerSecond">The maximum bytes per second that can be transferred through the base stream.</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <see cref="baseStream"/> is a null reference.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <see cref="maximumBytesPerSecond"/> is a negative value.</exception>
|
||||
public ThrottledStream(Stream baseStream, long maximumBytesPerSecond)
|
||||
{
|
||||
if (baseStream == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(baseStream));
|
||||
}
|
||||
|
||||
if (maximumBytesPerSecond < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maximumBytesPerSecond),
|
||||
maximumBytesPerSecond, "The maximum number of bytes per second can't be negative.");
|
||||
}
|
||||
|
||||
_baseStream = baseStream;
|
||||
_maximumBytesPerSecond = maximumBytesPerSecond;
|
||||
_start = CurrentMilliseconds;
|
||||
_byteCount = 0;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public methods
|
||||
/// <summary>
|
||||
/// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
|
||||
/// </summary>
|
||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs.</exception>
|
||||
public override void Flush()
|
||||
{
|
||||
_baseStream.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
|
||||
/// </summary>
|
||||
/// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and (offset + count - 1) replaced by the bytes read from the current source.</param>
|
||||
/// <param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream.</param>
|
||||
/// <param name="count">The maximum number of bytes to be read from the current stream.</param>
|
||||
/// <returns>
|
||||
/// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.
|
||||
/// </returns>
|
||||
/// <exception cref="T:System.ArgumentException">The sum of offset and count is larger than the buffer length. </exception>
|
||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support reading. </exception>
|
||||
/// <exception cref="T:System.ArgumentNullException">buffer is null. </exception>
|
||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
|
||||
/// <exception cref="T:System.ArgumentOutOfRangeException">offset or count is negative. </exception>
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
Throttle(count);
|
||||
|
||||
return _baseStream.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the position within the current stream.
|
||||
/// </summary>
|
||||
/// <param name="offset">A byte offset relative to the origin parameter.</param>
|
||||
/// <param name="origin">A value of type <see cref="T:System.IO.SeekOrigin"></see> indicating the reference point used to obtain the new position.</param>
|
||||
/// <returns>
|
||||
/// The new position within the current stream.
|
||||
/// </returns>
|
||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
|
||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support seeking, such as if the stream is constructed from a pipe or console output. </exception>
|
||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
return _baseStream.Seek(offset, origin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the length of the current stream.
|
||||
/// </summary>
|
||||
/// <param name="value">The desired length of the current stream in bytes.</param>
|
||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. </exception>
|
||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
|
||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
_baseStream.SetLength(value);
|
||||
}
|
||||
|
||||
private long _bytesWritten;
|
||||
|
||||
/// <summary>
|
||||
/// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
|
||||
/// </summary>
|
||||
/// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
|
||||
/// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
|
||||
/// <param name="count">The number of bytes to be written to the current stream.</param>
|
||||
/// <exception cref="T:System.IO.IOException">An I/O error occurs. </exception>
|
||||
/// <exception cref="T:System.NotSupportedException">The base stream does not support writing. </exception>
|
||||
/// <exception cref="T:System.ObjectDisposedException">Methods were called after the stream was closed. </exception>
|
||||
/// <exception cref="T:System.ArgumentNullException">buffer is null. </exception>
|
||||
/// <exception cref="T:System.ArgumentException">The sum of offset and count is greater than the buffer length. </exception>
|
||||
/// <exception cref="T:System.ArgumentOutOfRangeException">offset or count is negative. </exception>
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
Throttle(count);
|
||||
|
||||
_baseStream.Write(buffer, offset, count);
|
||||
|
||||
_bytesWritten += count;
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
await ThrottleAsync(count, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await _baseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_bytesWritten += count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A <see cref="T:System.String"></see> that represents the current <see cref="T:System.Object"></see>.
|
||||
/// </returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return _baseStream.ToString();
|
||||
}
|
||||
#endregion
|
||||
|
||||
private bool ThrottleCheck(int bufferSizeInBytes)
|
||||
{
|
||||
if (_bytesWritten < MinThrottlePosition)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the buffer isn't empty.
|
||||
if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Protected methods
|
||||
/// <summary>
|
||||
/// Throttles for the specified buffer size in bytes.
|
||||
/// </summary>
|
||||
/// <param name="bufferSizeInBytes">The buffer size in bytes.</param>
|
||||
protected void Throttle(int bufferSizeInBytes)
|
||||
{
|
||||
if (!ThrottleCheck(bufferSizeInBytes))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_byteCount += bufferSizeInBytes;
|
||||
long elapsedMilliseconds = CurrentMilliseconds - _start;
|
||||
|
||||
if (elapsedMilliseconds > 0)
|
||||
{
|
||||
// Calculate the current bps.
|
||||
long bps = _byteCount * 1000L / elapsedMilliseconds;
|
||||
|
||||
// If the bps are more then the maximum bps, try to throttle.
|
||||
if (bps > _maximumBytesPerSecond)
|
||||
{
|
||||
// Calculate the time to sleep.
|
||||
long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond;
|
||||
int toSleep = (int)(wakeElapsed - elapsedMilliseconds);
|
||||
|
||||
if (toSleep > 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
// The time to sleep is more then a millisecond, so sleep.
|
||||
var task = Task.Delay(toSleep);
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Eatup ThreadAbortException.
|
||||
}
|
||||
|
||||
// A sleep has been done, reset.
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ThrottleAsync(int bufferSizeInBytes, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!ThrottleCheck(bufferSizeInBytes))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_byteCount += bufferSizeInBytes;
|
||||
long elapsedMilliseconds = CurrentMilliseconds - _start;
|
||||
|
||||
if (elapsedMilliseconds > 0)
|
||||
{
|
||||
// Calculate the current bps.
|
||||
long bps = _byteCount * 1000L / elapsedMilliseconds;
|
||||
|
||||
// If the bps are more then the maximum bps, try to throttle.
|
||||
if (bps > _maximumBytesPerSecond)
|
||||
{
|
||||
// Calculate the time to sleep.
|
||||
long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond;
|
||||
int toSleep = (int)(wakeElapsed - elapsedMilliseconds);
|
||||
|
||||
if (toSleep > 1)
|
||||
{
|
||||
// The time to sleep is more then a millisecond, so sleep.
|
||||
await Task.Delay(toSleep, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// A sleep has been done, reset.
|
||||
Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will reset the bytecount to 0 and reset the start time to the current time.
|
||||
/// </summary>
|
||||
protected void Reset()
|
||||
{
|
||||
long difference = CurrentMilliseconds - _start;
|
||||
|
||||
// Only reset counters when a known history is available of more then 1 second.
|
||||
if (difference > 1000)
|
||||
{
|
||||
_byteCount = 0;
|
||||
_start = CurrentMilliseconds;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
@ -16,12 +16,10 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
private bool _ignoreDotPrefix;
|
||||
|
||||
/// <summary>
|
||||
/// Any folder named in this list will be ignored - can be added to at runtime for extensibility
|
||||
/// Any folder named in this list will be ignored
|
||||
/// </summary>
|
||||
public static readonly string[] IgnoreFolders =
|
||||
private static readonly string[] _ignoreFolders =
|
||||
{
|
||||
"metadata",
|
||||
"ps3_update",
|
||||
@ -42,25 +40,14 @@ namespace Emby.Server.Implementations.Library
|
||||
"$RECYCLE.BIN",
|
||||
"System Volume Information",
|
||||
".grab",
|
||||
|
||||
// macos
|
||||
".AppleDouble"
|
||||
|
||||
};
|
||||
|
||||
public CoreResolutionIgnoreRule(ILibraryManager libraryManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
|
||||
_ignoreDotPrefix = Environment.OSVersion.Platform != PlatformID.Win32NT;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shoulds the ignore.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo">The file information.</param>
|
||||
/// <param name="parent">The parent.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns>
|
||||
/// <inheritdoc />
|
||||
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
|
||||
{
|
||||
// Don't ignore top level folders
|
||||
@ -72,46 +59,17 @@ namespace Emby.Server.Implementations.Library
|
||||
var filename = fileInfo.Name;
|
||||
var path = fileInfo.FullName;
|
||||
|
||||
// Handle mac .DS_Store
|
||||
// https://github.com/MediaBrowser/MediaBrowser/issues/427
|
||||
if (_ignoreDotPrefix)
|
||||
// Ignore hidden files on UNIX
|
||||
if (Environment.OSVersion.Platform != PlatformID.Win32NT
|
||||
&& filename[0] == '.')
|
||||
{
|
||||
if (filename.IndexOf('.') == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ignore hidden files and folders
|
||||
//if (fileInfo.IsHidden)
|
||||
//{
|
||||
// if (parent == null)
|
||||
// {
|
||||
// var parentFolderName = Path.GetFileName(_fileSystem.GetDirectoryName(path));
|
||||
|
||||
// if (string.Equals(parentFolderName, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
// if (string.Equals(parentFolderName, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Sometimes these are marked hidden
|
||||
// if (_fileSystem.IsRootPath(path))
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// return true;
|
||||
//}
|
||||
|
||||
if (fileInfo.IsDirectory)
|
||||
{
|
||||
// Ignore any folders in our list
|
||||
if (IgnoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
|
||||
if (_ignoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -119,8 +77,9 @@ namespace Emby.Server.Implementations.Library
|
||||
if (parent != null)
|
||||
{
|
||||
// Ignore trailer folders but allow it at the collection level
|
||||
if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase) &&
|
||||
!(parent is AggregateFolder) && !(parent is UserRootFolder))
|
||||
if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)
|
||||
&& !(parent is AggregateFolder)
|
||||
&& !(parent is UserRootFolder))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -141,22 +100,17 @@ namespace Emby.Server.Implementations.Library
|
||||
if (parent != null)
|
||||
{
|
||||
// Don't resolve these into audio files
|
||||
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename) && _libraryManager.IsAudioFile(filename))
|
||||
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename)
|
||||
&& _libraryManager.IsAudioFile(filename))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore samples
|
||||
var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("-", " ", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("_", " ", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("!", " ", StringComparison.OrdinalIgnoreCase);
|
||||
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
|
||||
|
||||
if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return m.Success;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.Library
|
||||
public string Name => "Default";
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
|
||||
// This is dumb and an artifact of the backwards way auth providers were designed.
|
||||
// This version of authenticate was never meant to be called, but needs to be here for interface compat
|
||||
// Only the providers that don't provide local user support use this
|
||||
@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
// This is the verson that we need to use for local users. Because reasons.
|
||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
|
||||
{
|
||||
@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
|
||||
string hash = user.Password;
|
||||
user.Password = string.Format("$SHA1${0}", hash);
|
||||
}
|
||||
|
||||
|
||||
if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
|
||||
{
|
||||
string hash = user.EasyPassword;
|
||||
@ -165,6 +165,34 @@ namespace Emby.Server.Implementations.Library
|
||||
return user.Password;
|
||||
}
|
||||
|
||||
public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
|
||||
{
|
||||
ConvertPasswordFormat(user);
|
||||
|
||||
if (newPassword != null)
|
||||
{
|
||||
newPasswordHash = string.Format("$SHA1${0}", GetHashedString(user, newPassword));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newPasswordHash))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(newPasswordHash));
|
||||
}
|
||||
|
||||
user.EasyPassword = newPasswordHash;
|
||||
}
|
||||
|
||||
public string GetEasyPasswordHash(User user)
|
||||
{
|
||||
// This should be removed in the future. This was added to let user login after
|
||||
// Jellyfin 10.3.3 failed to save a well formatted PIN.
|
||||
ConvertPasswordFormat(user);
|
||||
|
||||
return string.IsNullOrEmpty(user.EasyPassword)
|
||||
? null
|
||||
: (new PasswordHash(user.EasyPassword)).Hash;
|
||||
}
|
||||
|
||||
public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
|
||||
{
|
||||
passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);
|
||||
|
@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
public class DefaultPasswordResetProvider : IPasswordResetProvider
|
||||
{
|
||||
public string Name => "Default Password Reset Provider";
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
private readonly string _passwordResetFileBase;
|
||||
private readonly string _passwordResetFileBaseDir;
|
||||
private readonly string _passwordResetFileBaseName = "passwordreset";
|
||||
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ICryptoProvider _crypto;
|
||||
|
||||
public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider)
|
||||
{
|
||||
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
|
||||
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName);
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_userManager = userManager;
|
||||
_crypto = cryptoProvider;
|
||||
}
|
||||
|
||||
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||
{
|
||||
SerializablePasswordReset spr;
|
||||
HashSet<string> usersreset = new HashSet<string>();
|
||||
foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*"))
|
||||
{
|
||||
using (var str = File.OpenRead(resetfile))
|
||||
{
|
||||
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (spr.ExpirationDate < DateTime.Now)
|
||||
{
|
||||
File.Delete(resetfile);
|
||||
}
|
||||
else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var resetUser = _userManager.GetUserByName(spr.UserName);
|
||||
if (resetUser == null)
|
||||
{
|
||||
throw new Exception($"User with a username of {spr.UserName} not found");
|
||||
}
|
||||
|
||||
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
|
||||
usersreset.Add(resetUser.Name);
|
||||
File.Delete(resetfile);
|
||||
}
|
||||
}
|
||||
|
||||
if (usersreset.Count < 1)
|
||||
{
|
||||
throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new PinRedeemResult
|
||||
{
|
||||
Success = true,
|
||||
UsersReset = usersreset.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
|
||||
{
|
||||
string pin = string.Empty;
|
||||
using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create())
|
||||
{
|
||||
byte[] bytes = new byte[4];
|
||||
cryptoRandom.GetBytes(bytes);
|
||||
pin = BitConverter.ToString(bytes);
|
||||
}
|
||||
|
||||
DateTime expireTime = DateTime.Now.AddMinutes(30);
|
||||
string filePath = _passwordResetFileBase + user.InternalId + ".json";
|
||||
SerializablePasswordReset spr = new SerializablePasswordReset
|
||||
{
|
||||
ExpirationDate = expireTime,
|
||||
Pin = pin,
|
||||
PinFile = filePath,
|
||||
UserName = user.Name
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
using (FileStream fileStream = File.OpenWrite(filePath))
|
||||
{
|
||||
_jsonSerializer.SerializeToStream(spr, fileStream);
|
||||
await fileStream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new Exception($"Error serializing or writing password reset for {user.Name} to location: {filePath}", e);
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = ForgotPasswordAction.PinCode,
|
||||
PinExpirationDate = expireTime,
|
||||
PinFile = filePath
|
||||
};
|
||||
}
|
||||
|
||||
private class SerializablePasswordReset : PasswordPinCreationResult
|
||||
{
|
||||
public string Pin { get; set; }
|
||||
|
||||
public string UserName { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
47
Emby.Server.Implementations/Library/InvalidAuthProvider.cs
Normal file
47
Emby.Server.Implementations/Library/InvalidAuthProvider.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
public class InvalidAuthProvider : IAuthenticationProvider
|
||||
{
|
||||
public string Name => "InvalidOrMissingAuthenticationProvider";
|
||||
|
||||
public bool IsEnabled => true;
|
||||
|
||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password)
|
||||
{
|
||||
throw new SecurityException("User Account cannot login with this provider. The Normal provider for this user cannot be found");
|
||||
}
|
||||
|
||||
public Task<bool> HasPassword(User user)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public Task ChangePassword(User user, string newPassword)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash)
|
||||
{
|
||||
// Nothing here
|
||||
}
|
||||
|
||||
public string GetPasswordHash(User user)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string GetEasyPasswordHash(User user)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
@ -2368,7 +2368,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public int? GetSeasonNumberFromPath(string path)
|
||||
{
|
||||
return new SeasonPathParser(GetNamingOptions()).Parse(path, true, true).SeasonNumber;
|
||||
return new SeasonPathParser().Parse(path, true, true).SeasonNumber;
|
||||
}
|
||||
|
||||
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -11,7 +12,6 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
@ -27,7 +27,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
/// <value>The priority.</value>
|
||||
public override ResolverPriority Priority => ResolverPriority.Third;
|
||||
|
||||
public MultiItemResolverResult ResolveMultiple(Folder parent,
|
||||
public MultiItemResolverResult ResolveMultiple(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
@ -45,7 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return result;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveMultipleInternal(Folder parent,
|
||||
private MultiItemResolverResult ResolveMultipleInternal(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
@ -90,7 +92,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return null;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveVideos<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions, string collectionType, bool parseName)
|
||||
private MultiItemResolverResult ResolveVideos<T>(
|
||||
Folder parent,
|
||||
IEnumerable<FileSystemMetadata> fileSystemEntries,
|
||||
IDirectoryService directoryService,
|
||||
bool suppportMultiEditions,
|
||||
string collectionType,
|
||||
bool parseName)
|
||||
where T : Video, new()
|
||||
{
|
||||
var files = new List<FileSystemMetadata>();
|
||||
@ -103,8 +111,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
// This is a hack but currently no better way to resolve a sometimes ambiguous situation
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
{
|
||||
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -114,11 +122,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
{
|
||||
leftOver.Add(child);
|
||||
}
|
||||
else if (IsIgnored(child.Name))
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
else if (!IsIgnored(child.Name))
|
||||
{
|
||||
files.Add(child);
|
||||
}
|
||||
@ -167,17 +171,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
private static bool IsIgnored(string filename)
|
||||
{
|
||||
// Ignore samples
|
||||
var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("-", " ", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("_", " ", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("!", " ", StringComparison.OrdinalIgnoreCase);
|
||||
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
|
||||
|
||||
if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return m.Success;
|
||||
}
|
||||
|
||||
private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
|
||||
|
@ -14,6 +14,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"folder",
|
||||
"thumb",
|
||||
"landscape",
|
||||
"fanart",
|
||||
"backdrop",
|
||||
"poster",
|
||||
"cover",
|
||||
"logo",
|
||||
"default"
|
||||
};
|
||||
|
||||
public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
|
||||
{
|
||||
@ -31,10 +43,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
if (!args.IsDirectory)
|
||||
{
|
||||
// Must be an image file within a photo collection
|
||||
var collectionType = args.GetCollectionType();
|
||||
var collectionType = args.CollectionType;
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) ||
|
||||
(string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
|
||||
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
|
||||
|| (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
|
||||
{
|
||||
if (IsImageFile(args.Path, _imageProcessor))
|
||||
{
|
||||
@ -74,43 +86,29 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
}
|
||||
|
||||
internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename)
|
||||
{
|
||||
if (imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> IgnoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"folder",
|
||||
"thumb",
|
||||
"landscape",
|
||||
"fanart",
|
||||
"backdrop",
|
||||
"poster",
|
||||
"cover",
|
||||
"logo",
|
||||
"default"
|
||||
};
|
||||
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
||||
{
|
||||
var filename = Path.GetFileNameWithoutExtension(path) ?? string.Empty;
|
||||
if (path == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
}
|
||||
|
||||
if (IgnoreFiles.Contains(filename))
|
||||
var filename = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
if (_ignoreFiles.Contains(filename))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IgnoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return imageProcessor.SupportedInputFormats.Contains(Path.GetExtension(path).TrimStart('.'), StringComparer.Ordinal);
|
||||
string extension = Path.GetExtension(path).TrimStart('.');
|
||||
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
|
||||
var path = args.Path;
|
||||
|
||||
var seasonParserResult = new SeasonPathParser(namingOptions).Parse(path, true, true);
|
||||
var seasonParserResult = new SeasonPathParser().Parse(path, true, true);
|
||||
|
||||
var season = new Season
|
||||
{
|
||||
|
@ -194,9 +194,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
||||
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
|
||||
private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager)
|
||||
{
|
||||
var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
|
||||
|
||||
var seasonNumber = new SeasonPathParser(namingOptions).Parse(path, isTvContentType, isTvContentType).SeasonNumber;
|
||||
var seasonNumber = new SeasonPathParser().Parse(path, isTvContentType, isTvContentType).SeasonNumber;
|
||||
|
||||
return seasonNumber.HasValue;
|
||||
}
|
||||
|
@ -79,6 +79,11 @@ namespace Emby.Server.Implementations.Library
|
||||
private IAuthenticationProvider[] _authenticationProviders;
|
||||
private DefaultAuthenticationProvider _defaultAuthenticationProvider;
|
||||
|
||||
private InvalidAuthProvider _invalidAuthProvider;
|
||||
|
||||
private IPasswordResetProvider[] _passwordResetProviders;
|
||||
private DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
||||
|
||||
public UserManager(
|
||||
ILoggerFactory loggerFactory,
|
||||
IServerConfigurationManager configurationManager,
|
||||
@ -102,8 +107,6 @@ namespace Emby.Server.Implementations.Library
|
||||
_fileSystem = fileSystem;
|
||||
ConfigurationManager = configurationManager;
|
||||
_users = Array.Empty<User>();
|
||||
|
||||
DeletePinFile();
|
||||
}
|
||||
|
||||
public NameIdPair[] GetAuthenticationProviders()
|
||||
@ -120,11 +123,31 @@ namespace Emby.Server.Implementations.Library
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders)
|
||||
public NameIdPair[] GetPasswordResetProviders()
|
||||
{
|
||||
return _passwordResetProviders
|
||||
.Where(i => i.IsEnabled)
|
||||
.OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
|
||||
.ThenBy(i => i.Name)
|
||||
.Select(i => new NameIdPair
|
||||
{
|
||||
Name = i.Name,
|
||||
Id = GetPasswordResetProviderId(i)
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders,IEnumerable<IPasswordResetProvider> passwordResetProviders)
|
||||
{
|
||||
_authenticationProviders = authenticationProviders.ToArray();
|
||||
|
||||
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
|
||||
|
||||
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
|
||||
|
||||
_passwordResetProviders = passwordResetProviders.ToArray();
|
||||
|
||||
_defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
|
||||
}
|
||||
|
||||
#region UserUpdated Event
|
||||
@ -199,9 +222,8 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_users = LoadUsers();
|
||||
|
||||
var users = Users.ToList();
|
||||
var users = LoadUsers();
|
||||
_users = users.ToArray();
|
||||
|
||||
// If there are no local users with admin rights, make them all admins
|
||||
if (!users.Any(i => i.Policy.IsAdministrator))
|
||||
@ -258,27 +280,37 @@ namespace Emby.Server.Implementations.Library
|
||||
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var success = false;
|
||||
string updatedUsername = null;
|
||||
IAuthenticationProvider authenticationProvider = null;
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
|
||||
authenticationProvider = authResult.Item1;
|
||||
success = authResult.Item2;
|
||||
updatedUsername = authResult.Item2;
|
||||
success = authResult.Item3;
|
||||
}
|
||||
else
|
||||
{
|
||||
// user is null
|
||||
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
|
||||
authenticationProvider = authResult.Item1;
|
||||
success = authResult.Item2;
|
||||
updatedUsername = authResult.Item2;
|
||||
success = authResult.Item3;
|
||||
|
||||
if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider))
|
||||
{
|
||||
user = await CreateUser(username).ConfigureAwait(false);
|
||||
// We should trust the user that the authprovider says, not what was typed
|
||||
if (updatedUsername != username)
|
||||
{
|
||||
username = updatedUsername;
|
||||
}
|
||||
|
||||
var hasNewUserPolicy = authenticationProvider as IHasNewUserPolicy;
|
||||
if (hasNewUserPolicy != null)
|
||||
// Search the database for the user again; the authprovider might have created it
|
||||
user = Users
|
||||
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
|
||||
{
|
||||
var policy = hasNewUserPolicy.GetNewUserPolicy();
|
||||
UpdateUserPolicy(user, policy, true);
|
||||
@ -342,11 +374,21 @@ namespace Emby.Server.Implementations.Library
|
||||
return provider.GetType().FullName;
|
||||
}
|
||||
|
||||
private static string GetPasswordResetProviderId(IPasswordResetProvider provider)
|
||||
{
|
||||
return provider.GetType().FullName;
|
||||
}
|
||||
|
||||
private IAuthenticationProvider GetAuthenticationProvider(User user)
|
||||
{
|
||||
return GetAuthenticationProviders(user).First();
|
||||
}
|
||||
|
||||
private IPasswordResetProvider GetPasswordResetProvider(User user)
|
||||
{
|
||||
return GetPasswordResetProviders(user)[0];
|
||||
}
|
||||
|
||||
private IAuthenticationProvider[] GetAuthenticationProviders(User user)
|
||||
{
|
||||
var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
|
||||
@ -360,38 +402,67 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (providers.Length == 0)
|
||||
{
|
||||
providers = new IAuthenticationProvider[] { _defaultAuthenticationProvider };
|
||||
// Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
|
||||
_logger.LogWarning("User {UserName} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected", user.Name, user.Policy.AuthenticationProviderId);
|
||||
providers = new IAuthenticationProvider[] { _invalidAuthProvider };
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
private async Task<bool> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
|
||||
private IPasswordResetProvider[] GetPasswordResetProviders(User user)
|
||||
{
|
||||
var passwordResetProviderId = user?.Policy.PasswordResetProviderId;
|
||||
|
||||
var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
|
||||
|
||||
if (!string.IsNullOrEmpty(passwordResetProviderId))
|
||||
{
|
||||
providers = providers.Where(i => string.Equals(passwordResetProviderId, GetPasswordResetProviderId(i), StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
}
|
||||
|
||||
if (providers.Length == 0)
|
||||
{
|
||||
providers = new IPasswordResetProvider[] { _defaultPasswordResetProvider };
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
private async Task<Tuple<string, bool>> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requiresResolvedUser = provider as IRequiresResolvedUser;
|
||||
ProviderAuthenticationResult authenticationResult = null;
|
||||
if (requiresResolvedUser != null)
|
||||
{
|
||||
await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
|
||||
authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await provider.Authenticate(username, password).ConfigureAwait(false);
|
||||
authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
if(authenticationResult.Username != username)
|
||||
{
|
||||
_logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
|
||||
username = authenticationResult.Username;
|
||||
}
|
||||
|
||||
return new Tuple<string, bool>(username, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name);
|
||||
|
||||
return false;
|
||||
return new Tuple<string, bool>(username, false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Tuple<IAuthenticationProvider, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
|
||||
private async Task<Tuple<IAuthenticationProvider, string, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint)
|
||||
{
|
||||
string updatedUsername = null;
|
||||
bool success = false;
|
||||
IAuthenticationProvider authenticationProvider = null;
|
||||
|
||||
@ -404,17 +475,20 @@ namespace Emby.Server.Implementations.Library
|
||||
if (password == null)
|
||||
{
|
||||
// legacy
|
||||
success = string.Equals(_defaultAuthenticationProvider.GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
success = string.Equals(GetAuthenticationProvider(user).GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var provider in GetAuthenticationProviders(user))
|
||||
{
|
||||
success = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
|
||||
var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
|
||||
updatedUsername = providerAuthResult.Item1;
|
||||
success = providerAuthResult.Item2;
|
||||
|
||||
if (success)
|
||||
{
|
||||
authenticationProvider = provider;
|
||||
username = updatedUsername;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -427,16 +501,16 @@ namespace Emby.Server.Implementations.Library
|
||||
if (password == null)
|
||||
{
|
||||
// legacy
|
||||
success = string.Equals(GetLocalPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
success = string.Equals(GetLocalPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
|
||||
success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<IAuthenticationProvider, bool>(authenticationProvider, success);
|
||||
return new Tuple<IAuthenticationProvider, string, bool>(authenticationProvider, username, success);
|
||||
}
|
||||
|
||||
private void UpdateInvalidLoginAttemptCount(User user, int newValue)
|
||||
@ -476,46 +550,40 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
private string GetLocalPasswordHash(User user)
|
||||
{
|
||||
return string.IsNullOrEmpty(user.EasyPassword)
|
||||
? null
|
||||
: user.EasyPassword;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the users from the repository
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{User}.</returns>
|
||||
private User[] LoadUsers()
|
||||
private List<User> LoadUsers()
|
||||
{
|
||||
var users = UserRepository.RetrieveAllUsers();
|
||||
|
||||
// There always has to be at least one user.
|
||||
if (users.Count == 0)
|
||||
if (users.Count != 0)
|
||||
{
|
||||
var defaultName = Environment.UserName;
|
||||
if (string.IsNullOrWhiteSpace(defaultName))
|
||||
{
|
||||
defaultName = "MyJellyfinUser";
|
||||
}
|
||||
var name = MakeValidUsername(defaultName);
|
||||
|
||||
var user = InstantiateNewUser(name);
|
||||
|
||||
user.DateLastSaved = DateTime.UtcNow;
|
||||
|
||||
UserRepository.CreateUser(user);
|
||||
|
||||
users.Add(user);
|
||||
|
||||
user.Policy.IsAdministrator = true;
|
||||
user.Policy.EnableContentDeletion = true;
|
||||
user.Policy.EnableRemoteControlOfOtherUsers = true;
|
||||
UpdateUserPolicy(user, user.Policy, false);
|
||||
return users;
|
||||
}
|
||||
|
||||
return users.ToArray();
|
||||
var defaultName = Environment.UserName;
|
||||
if (string.IsNullOrWhiteSpace(defaultName))
|
||||
{
|
||||
defaultName = "MyJellyfinUser";
|
||||
}
|
||||
|
||||
var name = MakeValidUsername(defaultName);
|
||||
|
||||
var user = InstantiateNewUser(name);
|
||||
|
||||
user.DateLastSaved = DateTime.UtcNow;
|
||||
|
||||
UserRepository.CreateUser(user);
|
||||
|
||||
user.Policy.IsAdministrator = true;
|
||||
user.Policy.EnableContentDeletion = true;
|
||||
user.Policy.EnableRemoteControlOfOtherUsers = true;
|
||||
UpdateUserPolicy(user, user.Policy, false);
|
||||
|
||||
return new List<User> { user };
|
||||
}
|
||||
|
||||
public UserDto GetUserDto(User user, string remoteEndPoint = null)
|
||||
@ -526,7 +594,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result;
|
||||
bool hasConfiguredEasyPassword = string.IsNullOrEmpty(GetLocalPasswordHash(user));
|
||||
bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user));
|
||||
|
||||
bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
|
||||
hasConfiguredEasyPassword :
|
||||
@ -814,17 +882,7 @@ namespace Emby.Server.Implementations.Library
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (newPassword != null)
|
||||
{
|
||||
newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newPasswordHash))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(newPasswordHash));
|
||||
}
|
||||
|
||||
user.EasyPassword = newPasswordHash;
|
||||
GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordHash);
|
||||
|
||||
UpdateUser(user);
|
||||
|
||||
@ -844,159 +902,51 @@ namespace Emby.Server.Implementations.Library
|
||||
Id = Guid.NewGuid(),
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
UsesIdForConfigurationPath = true,
|
||||
//Salt = BCrypt.GenerateSalt()
|
||||
UsesIdForConfigurationPath = true
|
||||
};
|
||||
}
|
||||
|
||||
private string PasswordResetFile => Path.Combine(ConfigurationManager.ApplicationPaths.ProgramDataPath, "passwordreset.txt");
|
||||
|
||||
private string _lastPin;
|
||||
private PasswordPinCreationResult _lastPasswordPinCreationResult;
|
||||
private int _pinAttempts;
|
||||
|
||||
private async Task<PasswordPinCreationResult> CreatePasswordResetPin()
|
||||
{
|
||||
var num = new Random().Next(1, 9999);
|
||||
|
||||
var path = PasswordResetFile;
|
||||
|
||||
var pin = num.ToString("0000", CultureInfo.InvariantCulture);
|
||||
_lastPin = pin;
|
||||
|
||||
var time = TimeSpan.FromMinutes(5);
|
||||
var expiration = DateTime.UtcNow.Add(time);
|
||||
|
||||
var text = new StringBuilder();
|
||||
|
||||
var localAddress = (await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false)) ?? string.Empty;
|
||||
|
||||
text.AppendLine("Use your web browser to visit:");
|
||||
text.AppendLine(string.Empty);
|
||||
text.AppendLine(localAddress + "/web/index.html#!/forgotpasswordpin.html");
|
||||
text.AppendLine(string.Empty);
|
||||
text.AppendLine("Enter the following pin code:");
|
||||
text.AppendLine(string.Empty);
|
||||
text.AppendLine(pin);
|
||||
text.AppendLine(string.Empty);
|
||||
|
||||
var localExpirationTime = expiration.ToLocalTime();
|
||||
// Tuesday, 22 August 2006 06:30 AM
|
||||
text.AppendLine("The pin code will expire at " + localExpirationTime.ToString("f1", CultureInfo.CurrentCulture));
|
||||
|
||||
File.WriteAllText(path, text.ToString(), Encoding.UTF8);
|
||||
|
||||
var result = new PasswordPinCreationResult
|
||||
{
|
||||
PinFile = path,
|
||||
ExpirationDate = expiration
|
||||
};
|
||||
|
||||
_lastPasswordPinCreationResult = result;
|
||||
_pinAttempts = 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
|
||||
{
|
||||
DeletePinFile();
|
||||
|
||||
var user = string.IsNullOrWhiteSpace(enteredUsername) ?
|
||||
null :
|
||||
GetUserByName(enteredUsername);
|
||||
|
||||
var action = ForgotPasswordAction.InNetworkRequired;
|
||||
string pinFile = null;
|
||||
DateTime? expirationDate = null;
|
||||
|
||||
if (user != null && !user.Policy.IsAdministrator)
|
||||
if (user != null && isInNetwork)
|
||||
{
|
||||
action = ForgotPasswordAction.ContactAdmin;
|
||||
var passwordResetProvider = GetPasswordResetProvider(user);
|
||||
return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isInNetwork)
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
action = ForgotPasswordAction.PinCode;
|
||||
}
|
||||
|
||||
var result = await CreatePasswordResetPin().ConfigureAwait(false);
|
||||
pinFile = result.PinFile;
|
||||
expirationDate = result.ExpirationDate;
|
||||
Action = action,
|
||||
PinFile = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
return new ForgotPasswordResult
|
||||
{
|
||||
Action = action,
|
||||
PinFile = pinFile,
|
||||
PinExpirationDate = expirationDate
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
|
||||
{
|
||||
DeletePinFile();
|
||||
|
||||
var usersReset = new List<string>();
|
||||
|
||||
var valid = !string.IsNullOrWhiteSpace(_lastPin) &&
|
||||
string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) &&
|
||||
_lastPasswordPinCreationResult != null &&
|
||||
_lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow;
|
||||
|
||||
if (valid)
|
||||
foreach (var provider in _passwordResetProviders)
|
||||
{
|
||||
_lastPin = null;
|
||||
_lastPasswordPinCreationResult = null;
|
||||
|
||||
foreach (var user in Users)
|
||||
var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
|
||||
if (result.Success)
|
||||
{
|
||||
await ResetPassword(user).ConfigureAwait(false);
|
||||
|
||||
if (user.Policy.IsDisabled)
|
||||
{
|
||||
user.Policy.IsDisabled = false;
|
||||
UpdateUserPolicy(user, user.Policy, true);
|
||||
}
|
||||
usersReset.Add(user.Name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_pinAttempts++;
|
||||
if (_pinAttempts >= 3)
|
||||
{
|
||||
_lastPin = null;
|
||||
_lastPasswordPinCreationResult = null;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return new PinRedeemResult
|
||||
{
|
||||
Success = valid,
|
||||
UsersReset = usersReset.ToArray()
|
||||
Success = false,
|
||||
UsersReset = Array.Empty<string>()
|
||||
};
|
||||
}
|
||||
|
||||
private void DeletePinFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
_fileSystem.DeleteFile(PasswordResetFile);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordPinCreationResult
|
||||
{
|
||||
public string PinFile { get; set; }
|
||||
public DateTime ExpirationDate { get; set; }
|
||||
}
|
||||
|
||||
public UserPolicy GetUserPolicy(User user)
|
||||
{
|
||||
var path = GetPolicyFilePath(user);
|
||||
|
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
UserAgent = "Emby/3.0",
|
||||
|
||||
// Shouldn't matter but may cause issues
|
||||
EnableHttpCompression = false
|
||||
DecompressionMethod = CompressionMethod.None
|
||||
};
|
||||
|
||||
using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false))
|
||||
|
@ -261,7 +261,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
public string HomePageUrl => "https://github.com/jellyfin/jellyfin";
|
||||
|
||||
public async Task RefreshSeriesTimers(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public async Task RefreshSeriesTimers(CancellationToken cancellationToken)
|
||||
{
|
||||
var seriesTimers = await GetSeriesTimersAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@ -271,7 +271,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshTimers(CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public async Task RefreshTimers(CancellationToken cancellationToken)
|
||||
{
|
||||
var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user