Merge pull request #5 from jellyfin/master

Merge up to latest master
This commit is contained in:
LogicalPhallacy 2019-08-06 00:26:19 -07:00 committed by GitHub
commit 984e415c66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
289 changed files with 7917 additions and 9840 deletions

View File

@ -16,7 +16,7 @@ jobs:
- job: main_build - job: main_build
displayName: Main Build displayName: Main Build
pool: pool:
vmImage: ubuntu-16.04 vmImage: ubuntu-latest
strategy: strategy:
matrix: matrix:
release: release:
@ -35,12 +35,14 @@ jobs:
inputs: inputs:
command: restore command: restore
projects: '$(RestoreBuildProjects)' projects: '$(RestoreBuildProjects)'
enabled: false
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: Build displayName: Build
inputs: inputs:
projects: '$(RestoreBuildProjects)' projects: '$(RestoreBuildProjects)'
arguments: '--configuration $(BuildConfiguration)' arguments: '--configuration $(BuildConfiguration)'
enabled: false
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: Test displayName: Test
@ -66,38 +68,38 @@ jobs:
# artifactName: 'jellyfin-build-$(BuildConfiguration)' # artifactName: 'jellyfin-build-$(BuildConfiguration)'
# zipAfterPublish: true # zipAfterPublish: true
- task: PublishBuildArtifacts@1 - task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Naming' displayName: 'Publish Artifact Naming'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs: inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll' targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
artifactName: 'Jellyfin.Naming' artifactName: 'Jellyfin.Naming'
- task: PublishBuildArtifacts@1 - task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Controller' displayName: 'Publish Artifact Controller'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs: inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll' targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
artifactName: 'Jellyfin.Controller' artifactName: 'Jellyfin.Controller'
- task: PublishBuildArtifacts@1 - task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Model' displayName: 'Publish Artifact Model'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs: inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll' targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
artifactName: 'Jellyfin.Model' artifactName: 'Jellyfin.Model'
- task: PublishBuildArtifacts@1 - task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Common' displayName: 'Publish Artifact Common'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs: inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll' targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common' artifactName: 'Jellyfin.Common'
- job: dotnet_compat - job: dotnet_compat
displayName: Compatibility Check displayName: Compatibility Check
pool: pool:
vmImage: ubuntu-16.04 vmImage: ubuntu-latest
dependsOn: main_build dependsOn: main_build
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds) condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds)
strategy: strategy:
@ -118,45 +120,23 @@ jobs:
steps: steps:
- checkout: none - checkout: none
- task: DownloadBuildArtifacts@0 - task: DownloadPipelineArtifact@2
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
displayName: Download the New Assembly Build Artifact displayName: Download the New Assembly Build Artifact
inputs: inputs:
buildType: 'current' # Options: current, specific source: 'current' # Options: current, specific
allowPartiallySucceededBuilds: false # Optional #preferTriggeringPipeline: false # Optional
downloadType: 'single' # Options: single, specific #tags: # Optional
artifactName: '$(NugetPackageName)' # Required when downloadType == Single artifact: '$(NugetPackageName)' # Optional
downloadPath: '$(System.ArtifactsDirectory)/new-artifacts' #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 - task: CopyFiles@2
displayName: Copy Artifact Assembly to new-release folder displayName: Copy New Assembly to new-release folder
inputs: inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
contents: '**/*.dll' contents: '**/*.dll'
@ -165,10 +145,35 @@ jobs:
overWrite: true # Optional overWrite: true # Optional
flattenFolders: 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 - task: DownloadGitHubRelease@0
displayName: Download ABI compatibility check tool from GitHub displayName: Download ABI compatibility check tool from GitHub
inputs: inputs:
connection: Jellyfin GitHub connection: Jellyfin Release Download
userRepository: EraYaN/dotnet-compatibility userRepository: EraYaN/dotnet-compatibility
defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
#version: # Required when defaultVersionType != Latest #version: # Required when defaultVersionType != Latest
@ -185,7 +190,7 @@ jobs:
- task: CmdLine@2 - task: CmdLine@2
displayName: Execute ABI compatibility check tool displayName: Execute ABI compatibility check tool
inputs: 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 workingDirectory: $(System.ArtifactsDirectory) # Optional
#failOnStderr: false # Optional #failOnStderr: false # Optional

2
.gitattributes vendored
View File

@ -1 +1,3 @@
* text=auto eol=lf
CONTRIBUTORS.md merge=union CONTRIBUTORS.md merge=union

View File

@ -30,6 +30,7 @@ assignees: ''
- OS: [e.g. Docker, Debian, Windows] - OS: [e.g. Docker, Debian, Windows]
- Browser: [e.g. Firefox, Chrome, Safari] - Browser: [e.g. Firefox, Chrome, Safari]
- Jellyfin Version: [e.g. 10.0.1] - Jellyfin Version: [e.g. 10.0.1]
- Reverse proxy: [e.g. no, nginx, apache, etc.]
**Additional context** **Additional context**
<!-- Add any other context about the problem here. --> <!-- Add any other context about the problem here. -->

View File

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

View File

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

View 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
View 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
View File

@ -239,11 +239,6 @@ pip-log.txt
########## ##########
.idea/ .idea/
##########
# Visual Studio Code
##########
.vscode/
######################### #########################
# Build artifacts # Build artifacts
######################### #########################
@ -266,3 +261,6 @@ deployment/collect-dist/
jellyfin_version.ini jellyfin_version.ini
ci/ ci/
# Doxygen
doc/

View File

@ -23,7 +23,11 @@
- [fruhnow](https://github.com/fruhnow) - [fruhnow](https://github.com/fruhnow)
- [Lynxy](https://github.com/Lynxy) - [Lynxy](https://github.com/Lynxy)
- [fasheng](https://github.com/fasheng) - [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 # Emby Contributors

View File

@ -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 WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 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" build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin"
FROM jellyfin/ffmpeg as ffmpeg 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 # libfontconfig1 is required for Skia
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \ && apt-get install --no-install-recommends --no-install-suggests -y \
@ -21,8 +21,8 @@ RUN apt-get update \
COPY --from=ffmpeg / / COPY --from=ffmpeg / /
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.2.2 ARG JELLYFIN_WEB_VERSION=v10.3.7
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \ && rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web

View File

@ -3,12 +3,7 @@
ARG DOTNET_VERSION=3.0 ARG DOTNET_VERSION=3.0
FROM multiarch/qemu-user-static:x86_64-arm as qemu FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
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
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 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" build_jellyfin Jellyfin.Server Release linux-arm /jellyfin"
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm32v7 FROM multiarch/qemu-user-static:x86_64-arm as qemu
COPY --from=qemu_extract qemu-arm-static /usr/bin 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 \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
@ -30,8 +26,8 @@ RUN apt-get update \
&& chmod 777 /cache /config /media && chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.2.2 ARG JELLYFIN_WEB_VERSION=v10.3.7
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \ && rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web

View File

@ -3,13 +3,7 @@
ARG DOTNET_VERSION=3.0 ARG DOTNET_VERSION=3.0
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder
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
WORKDIR /repo WORKDIR /repo
COPY . . COPY . .
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 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" build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin"
FROM microsoft/dotnet:${DOTNET_VERSION}-runtime-stretch-slim-arm64v8 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
COPY --from=qemu_extract qemu-aarch64-static /usr/bin 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 \ RUN apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
@ -31,8 +26,8 @@ RUN apt-get update \
&& chmod 777 /cache /config /media && chmod 777 /cache /config /media
COPY --from=builder /jellyfin /jellyfin COPY --from=builder /jellyfin /jellyfin
ARG JELLYFIN_WEB_VERSION=10.2.2 ARG JELLYFIN_WEB_VERSION=v10.3.7
RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/v${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& rm -rf /jellyfin/jellyfin-web \ && rm -rf /jellyfin/jellyfin-web \
&& mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web

2565
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@ -181,19 +181,6 @@ namespace Emby.Dlna.Didl
writer.WriteFullEndElement(); 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) private void AddVideoResource(DlnaOptions options, XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
{ {
if (streamInfo == null) if (streamInfo == null)
@ -384,7 +371,7 @@ namespace Emby.Dlna.Didl
var filename = url.Substring(0, url.IndexOf('?')); var filename = url.Substring(0, url.IndexOf('?'));
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType) var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
? GetMimeType(filename) ? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType; : mediaProfile.MimeType;
writer.WriteAttributeString("protocolInfo", string.Format( writer.WriteAttributeString("protocolInfo", string.Format(
@ -520,7 +507,7 @@ namespace Emby.Dlna.Didl
var filename = url.Substring(0, url.IndexOf('?')); var filename = url.Substring(0, url.IndexOf('?'));
var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType) var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType)
? GetMimeType(filename) ? MimeTypes.GetMimeType(filename)
: mediaProfile.MimeType; : mediaProfile.MimeType;
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container, var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container,
@ -545,17 +532,10 @@ namespace Emby.Dlna.Didl
} }
public static bool IsIdRoot(string id) public static bool IsIdRoot(string id)
{ => string.IsNullOrWhiteSpace(id)
if (string.IsNullOrWhiteSpace(id)
|| string.Equals(id, "0", StringComparison.OrdinalIgnoreCase) || string.Equals(id, "0", StringComparison.OrdinalIgnoreCase)
// Samsung sometimes uses 1 as root // Samsung sometimes uses 1 as root
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase)) || string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
{
return true;
}
return false;
}
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null) 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)) if (!_profile.EnableSingleAlbumArtLimit || string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase))
{ {
AddImageResElement(item, writer, 4096, 4096, "jpg", "JPEG_LRG"); 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, 4096, 4096, "png", "PNG_LRG");
AddImageResElement(item, writer, 160, 160, "png", "PNG_TN"); AddImageResElement(item, writer, 160, 160, "png", "PNG_TN");
} }
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
} }
private void AddEmbeddedImageAsCover(string name, XmlWriter writer) private void AddEmbeddedImageAsCover(string name, XmlWriter writer)
@ -970,7 +951,7 @@ namespace Emby.Dlna.Didl
writer.WriteAttributeString("protocolInfo", string.Format( writer.WriteAttributeString("protocolInfo", string.Format(
"http-get:*:{0}:{1}", "http-get:*:{0}:{1}",
GetMimeType("file." + format), MimeTypes.GetMimeType("file." + format),
contentFeatures 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

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna.PlayTo; using Emby.Dlna.PlayTo;
@ -247,7 +248,7 @@ namespace Emby.Dlna.Main
foreach (var address in addresses) foreach (var address in addresses)
{ {
if (address.AddressFamily == IpAddressFamily.InterNetworkV6) if (address.AddressFamily == AddressFamily.InterNetworkV6)
{ {
// Not support IPv6 right now // Not support IPv6 right now
continue; continue;

View File

@ -102,9 +102,10 @@ namespace Emby.Dlna.PlayTo
{ {
_sessionManager.ReportSessionEnded(_session.Id); _sessionManager.ReportSessionEnded(_session.Id);
} }
catch catch (Exception ex)
{ {
// Could throw if the session is already gone // 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; var info = e.Argument;
info.Headers.TryGetValue("NTS", out string nts); if (!_disposed
&& info.Headers.TryGetValue("USN", out string usn)
if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty; && usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty; || (info.Headers.TryGetValue("NT", out string nt)
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
if (usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1 &&
!_disposed)
{ {
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1 || OnDeviceUnavailable();
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)
{
OnDeviceUnavailable();
}
} }
} }
@ -612,22 +607,34 @@ namespace Emby.Dlna.PlayTo
public void Dispose() public void Dispose()
{ {
if (!_disposed) Dispose(true);
{ GC.SuppressFinalize(this);
_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();
}
} }
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) private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken)
{ {

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@ -14,7 +15,6 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Events; using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -172,7 +172,7 @@ namespace Emby.Dlna.PlayTo
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName); _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
string serverAddress; 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); serverAddress = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
} }

View File

@ -34,16 +34,13 @@ namespace Emby.Dlna.PlayTo
{ {
var cancellationToken = CancellationToken.None; 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)) .ConfigureAwait(false))
using (var stream = response.Content)
using (var reader = new StreamReader(stream, Encoding.UTF8))
{ {
using (var stream = response.Content) return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace);
{
using (var reader = new StreamReader(stream, Encoding.UTF8))
{
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 soapAction,
string postData, string postData,
string header, string header,
bool logRequest, bool logRequest,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (!soapAction.StartsWith("\"")) if (soapAction[0] != '\"')
soapAction = "\"" + soapAction + "\""; {
soapAction = '\"' + soapAction + '\"';
}
var options = new HttpRequestOptions var options = new HttpRequestOptions
{ {
@ -155,7 +155,6 @@ namespace Emby.Dlna.PlayTo
} }
options.RequestContentType = "text/xml"; options.RequestContentType = "text/xml";
options.AppendCharsetToMimeType = true;
options.RequestContent = postData; options.RequestContent = postData;
return _httpClient.Post(options); return _httpClient.Post(options);

View File

@ -9,7 +9,7 @@ namespace Emby.Dlna.Profiles
{ {
Name = "Dish Hopper-Joey"; 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 Identification = new DeviceIdentification
{ {

View File

@ -28,7 +28,7 @@
<MaxStaticBitrate>140000000</MaxStaticBitrate> <MaxStaticBitrate>140000000</MaxStaticBitrate>
<MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate>
<MaxStaticMusicBitrate xsi:nil="true" /> <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> <TimelineOffsetSeconds>0</TimelineOffsetSeconds>
<RequiresPlainVideoItems>false</RequiresPlainVideoItems> <RequiresPlainVideoItems>false</RequiresPlainVideoItems>
<RequiresPlainFolders>false</RequiresPlainFolders> <RequiresPlainFolders>false</RequiresPlainFolders>

View File

@ -33,27 +33,29 @@ namespace Emby.Naming.Audio
// Normalize // Normalize
// Remove whitespace // 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 = Regex.Replace(filename, @"\s+", " ");
filename = filename.TrimStart(); filename = filename.TrimStart();
foreach (var prefix in _options.AlbumStackingPrefixes) 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)) tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty;
{
result.IsMultiPart = true; if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
break; {
} result.IsMultiPart = true;
break;
} }
} }

View File

@ -7,11 +7,13 @@ namespace Emby.Naming.Audio
/// </summary> /// </summary>
/// <value>The name.</value> /// <value>The name.</value>
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// Gets or sets the part. /// Gets or sets the part.
/// </summary> /// </summary>
/// <value>The part.</value> /// <value>The part.</value>
public string Part { get; set; } public string Part { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this instance is multi part. /// Gets or sets a value indicating whether this instance is multi part.
/// </summary> /// </summary>

View File

@ -12,35 +12,56 @@ namespace Emby.Naming.AudioBook
/// </summary> /// </summary>
/// <value>The path.</value> /// <value>The path.</value>
public string Path { get; set; } public string Path { get; set; }
/// <summary> /// <summary>
/// Gets or sets the container. /// Gets or sets the container.
/// </summary> /// </summary>
/// <value>The container.</value> /// <value>The container.</value>
public string Container { get; set; } public string Container { get; set; }
/// <summary> /// <summary>
/// Gets or sets the part number. /// Gets or sets the part number.
/// </summary> /// </summary>
/// <value>The part number.</value> /// <value>The part number.</value>
public int? PartNumber { get; set; } public int? PartNumber { get; set; }
/// <summary> /// <summary>
/// Gets or sets the chapter number. /// Gets or sets the chapter number.
/// </summary> /// </summary>
/// <value>The chapter number.</value> /// <value>The chapter number.</value>
public int? ChapterNumber { get; set; } public int? ChapterNumber { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type. /// Gets or sets the type.
/// </summary> /// </summary>
/// <value>The type.</value> /// <value>The type.</value>
public bool IsDirectory { get; set; } public bool IsDirectory { get; set; }
/// <inheritdoc/>
public int CompareTo(AudioBookFileInfo other) public int CompareTo(AudioBookFileInfo other)
{ {
if (ReferenceEquals(this, other)) return 0; if (ReferenceEquals(this, other))
if (ReferenceEquals(null, other)) return 1; {
return 0;
}
if (ReferenceEquals(null, other))
{
return 1;
}
var chapterNumberComparison = Nullable.Compare(ChapterNumber, other.ChapterNumber); var chapterNumberComparison = Nullable.Compare(ChapterNumber, other.ChapterNumber);
if (chapterNumberComparison != 0) return chapterNumberComparison; if (chapterNumberComparison != 0)
{
return chapterNumberComparison;
}
var partNumberComparison = Nullable.Compare(PartNumber, other.PartNumber); 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); return string.Compare(Path, other.Path, StringComparison.Ordinal);
} }
} }

View File

@ -1,3 +1,4 @@
using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -14,14 +15,13 @@ namespace Emby.Naming.AudioBook
_options = options; _options = options;
} }
public AudioBookFilePathParserResult Parse(string path, bool IsDirectory) public AudioBookFilePathParserResult Parse(string path)
{ {
var result = Parse(path); if (path == null)
return !result.Success ? new AudioBookFilePathParserResult() : result; {
} throw new ArgumentNullException(nameof(path));
}
private AudioBookFilePathParserResult Parse(string path)
{
var result = new AudioBookFilePathParserResult(); var result = new AudioBookFilePathParserResult();
var fileName = Path.GetFileNameWithoutExtension(path); var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions) foreach (var expression in _options.AudioBookPartsExpressions)
@ -40,6 +40,7 @@ namespace Emby.Naming.AudioBook
} }
} }
} }
if (!result.PartNumber.HasValue) if (!result.PartNumber.HasValue)
{ {
var value = match.Groups["part"]; var value = match.Groups["part"];

View File

@ -3,7 +3,9 @@ namespace Emby.Naming.AudioBook
public class AudioBookFilePathParserResult public class AudioBookFilePathParserResult
{ {
public int? PartNumber { get; set; } public int? PartNumber { get; set; }
public int? ChapterNumber { get; set; } public int? ChapterNumber { get; set; }
public bool Success { get; set; } public bool Success { get; set; }
} }
} }

View File

@ -7,33 +7,40 @@ namespace Emby.Naming.AudioBook
/// </summary> /// </summary>
public class AudioBookInfo 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() public AudioBookInfo()
{ {
Files = new List<AudioBookFileInfo>(); Files = new List<AudioBookFileInfo>();
Extras = new List<AudioBookFileInfo>(); Extras = new List<AudioBookFileInfo>();
AlternateVersions = 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; }
} }
} }

View File

@ -15,7 +15,7 @@ namespace Emby.Naming.AudioBook
_options = options; _options = options;
} }
public IEnumerable<AudioBookInfo> Resolve(List<FileSystemMetadata> files) public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
{ {
var audioBookResolver = new AudioBookResolver(_options); var audioBookResolver = new AudioBookResolver(_options);

View File

@ -24,19 +24,21 @@ namespace Emby.Naming.AudioBook
return Resolve(path, true); return Resolve(path, true);
} }
public AudioBookFileInfo Resolve(string path, bool IsDirectory = false) public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
{ {
if (string.IsNullOrEmpty(path)) if (string.IsNullOrEmpty(path))
{ {
throw new ArgumentNullException(nameof(path)); throw new ArgumentNullException(nameof(path));
} }
if (IsDirectory) // TODO // TODO
if (isDirectory)
{ {
return null; return null;
} }
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path);
// Check supported extensions // Check supported extensions
if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{ {
@ -45,8 +47,7 @@ namespace Emby.Naming.AudioBook
var container = extension.TrimStart('.'); var container = extension.TrimStart('.');
var parsingResult = new AudioBookFilePathParser(_options) var parsingResult = new AudioBookFilePathParser(_options).Parse(path);
.Parse(path, IsDirectory);
return new AudioBookFileInfo return new AudioBookFileInfo
{ {
@ -54,7 +55,7 @@ namespace Emby.Naming.AudioBook
Container = container, Container = container,
PartNumber = parsingResult.PartNumber, PartNumber = parsingResult.PartNumber,
ChapterNumber = parsingResult.ChapterNumber, ChapterNumber = parsingResult.ChapterNumber,
IsDirectory = IsDirectory IsDirectory = isDirectory
}; };
} }
} }

View File

@ -6,17 +6,28 @@ namespace Emby.Naming.Common
public class EpisodeExpression public class EpisodeExpression
{ {
private string _expression; private string _expression;
public string Expression { get => _expression; private Regex _regex;
set { _expression = value; _regex = null; } }
public string Expression
{
get => _expression;
set
{
_expression = value;
_regex = null;
}
}
public bool IsByDate { get; set; } public bool IsByDate { get; set; }
public bool IsOptimistic { get; set; } public bool IsOptimistic { get; set; }
public bool IsNamed { get; set; } public bool IsNamed { get; set; }
public bool SupportsAbsoluteEpisodeNumbers { get; set; } public bool SupportsAbsoluteEpisodeNumbers { get; set; }
public string[] DateTimeFormats { get; set; } public string[] DateTimeFormats { get; set; }
private Regex _regex;
public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled)); public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled));
public EpisodeExpression(string expression, bool byDate) public EpisodeExpression(string expression, bool byDate)

View File

@ -6,10 +6,12 @@ namespace Emby.Naming.Common
/// The audio /// The audio
/// </summary> /// </summary>
Audio = 0, Audio = 0,
/// <summary> /// <summary>
/// The photo /// The photo
/// </summary> /// </summary>
Photo = 1, Photo = 1,
/// <summary> /// <summary>
/// The video /// The video
/// </summary> /// </summary>

View File

@ -8,19 +8,25 @@ namespace Emby.Naming.Common
public class NamingOptions public class NamingOptions
{ {
public string[] AudioFileExtensions { get; set; } public string[] AudioFileExtensions { get; set; }
public string[] AlbumStackingPrefixes { get; set; } public string[] AlbumStackingPrefixes { get; set; }
public string[] SubtitleFileExtensions { get; set; } public string[] SubtitleFileExtensions { get; set; }
public char[] SubtitleFlagDelimiters { get; set; } public char[] SubtitleFlagDelimiters { get; set; }
public string[] SubtitleForcedFlags { get; set; } public string[] SubtitleForcedFlags { get; set; }
public string[] SubtitleDefaultFlags { get; set; } public string[] SubtitleDefaultFlags { get; set; }
public EpisodeExpression[] EpisodeExpressions { get; set; } public EpisodeExpression[] EpisodeExpressions { get; set; }
public string[] EpisodeWithoutSeasonExpressions { get; set; } public string[] EpisodeWithoutSeasonExpressions { get; set; }
public string[] EpisodeMultiPartExpressions { get; set; } public string[] EpisodeMultiPartExpressions { get; set; }
public string[] VideoFileExtensions { get; set; } public string[] VideoFileExtensions { get; set; }
public string[] StubFileExtensions { get; set; } public string[] StubFileExtensions { get; set; }
public string[] AudioBookPartsExpressions { get; set; } public string[] AudioBookPartsExpressions { get; set; }
@ -28,12 +34,14 @@ namespace Emby.Naming.Common
public StubTypeRule[] StubTypes { get; set; } public StubTypeRule[] StubTypes { get; set; }
public char[] VideoFlagDelimiters { get; set; } public char[] VideoFlagDelimiters { get; set; }
public Format3DRule[] Format3DRules { get; set; } public Format3DRule[] Format3DRules { get; set; }
public string[] VideoFileStackingExpressions { 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; } public EpisodeExpression[] MultipleEpisodeExpressions { get; set; }
@ -41,7 +49,7 @@ namespace Emby.Naming.Common
public NamingOptions() public NamingOptions()
{ {
VideoFileExtensions = new string[] VideoFileExtensions = new[]
{ {
".m4v", ".m4v",
".3gp", ".3gp",
@ -106,53 +114,53 @@ namespace Emby.Naming.Common
{ {
new StubTypeRule new StubTypeRule
{ {
StubType = "dvd", StubType = "dvd",
Token = "dvd" Token = "dvd"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "hddvd", StubType = "hddvd",
Token = "hddvd" Token = "hddvd"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "bluray", StubType = "bluray",
Token = "bluray" Token = "bluray"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "bluray", StubType = "bluray",
Token = "brrip" Token = "brrip"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "bluray", StubType = "bluray",
Token = "bd25" Token = "bd25"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "bluray", StubType = "bluray",
Token = "bd50" Token = "bd50"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "vhs", StubType = "vhs",
Token = "vhs" Token = "vhs"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "tv", StubType = "tv",
Token = "HDTV" Token = "HDTV"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "tv", StubType = "tv",
Token = "PDTV" Token = "PDTV"
}, },
new StubTypeRule new StubTypeRule
{ {
StubType = "tv", StubType = "tv",
Token = "DSR" Token = "DSR"
} }
}; };
@ -286,7 +294,7 @@ namespace Emby.Naming.Common
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"), new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true) new EpisodeExpression("([0-9]{4})[\\.-]([0-9]{2})[\\.-]([0-9]{2})", true)
{ {
DateTimeFormats = new [] DateTimeFormats = new[]
{ {
"yyyy.MM.dd", "yyyy.MM.dd",
"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) new EpisodeExpression("([0-9]{2})[\\.-]([0-9]{2})[\\.-]([0-9]{4})", true)
{ {
DateTimeFormats = new [] DateTimeFormats = new[]
{ {
"dd.MM.yyyy", "dd.MM.yyyy",
"dd-MM-yyyy", "dd-MM-yyyy",
@ -348,9 +356,7 @@ namespace Emby.Naming.Common
}, },
// "1-12 episode title" // "1-12 episode title"
new EpisodeExpression(@"([0-9]+)-([0-9]+)") new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
{
},
// "01 - blah.avi", "01-blah.avi" // "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\s?-\s?[^\\\/]*$")
@ -427,7 +433,7 @@ namespace Emby.Naming.Common
Token = "_trailer", Token = "_trailer",
MediaType = MediaType.Video MediaType = MediaType.Video
}, },
new ExtraRule new ExtraRule
{ {
ExtraType = "trailer", ExtraType = "trailer",
RuleType = ExtraRuleType.Suffix, RuleType = ExtraRuleType.Suffix,
@ -462,7 +468,7 @@ namespace Emby.Naming.Common
Token = "_sample", Token = "_sample",
MediaType = MediaType.Video MediaType = MediaType.Video
}, },
new ExtraRule new ExtraRule
{ {
ExtraType = "sample", ExtraType = "sample",
RuleType = ExtraRuleType.Suffix, RuleType = ExtraRuleType.Suffix,
@ -476,7 +482,6 @@ namespace Emby.Naming.Common
Token = "theme", Token = "theme",
MediaType = MediaType.Audio MediaType = MediaType.Audio
}, },
new ExtraRule new ExtraRule
{ {
ExtraType = "scene", ExtraType = "scene",
@ -526,8 +531,8 @@ namespace Emby.Naming.Common
Token = "-short", Token = "-short",
MediaType = MediaType.Video MediaType = MediaType.Video
} }
}; };
Format3DRules = new[] Format3DRules = new[]
{ {
// Kodi rules: // 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]?\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})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$",
@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<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) }.Select(i => new EpisodeExpression(i)
{ {
IsNamed = true IsNamed = true
}).ToArray();
}).ToArray();
VideoFileExtensions = extensions VideoFileExtensions = extensions
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
@ -10,7 +10,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@ -18,6 +18,18 @@
<PackageId>Jellyfin.Naming</PackageId> <PackageId>Jellyfin.Naming</PackageId>
<PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl> <PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl> <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> </PropertyGroup>
</Project> </Project>

View File

@ -5,6 +5,7 @@ namespace Emby.Naming.Extensions
{ {
public static class StringExtensions 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) public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();

View File

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

View File

@ -7,16 +7,19 @@ namespace Emby.Naming.Subtitles
/// </summary> /// </summary>
/// <value>The path.</value> /// <value>The path.</value>
public string Path { get; set; } public string Path { get; set; }
/// <summary> /// <summary>
/// Gets or sets the language. /// Gets or sets the language.
/// </summary> /// </summary>
/// <value>The language.</value> /// <value>The language.</value>
public string Language { get; set; } public string Language { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this instance is default. /// Gets or sets a value indicating whether this instance is default.
/// </summary> /// </summary>
/// <value><c>true</c> if this instance is default; otherwise, <c>false</c>.</value> /// <value><c>true</c> if this instance is default; otherwise, <c>false</c>.</value>
public bool IsDefault { get; set; } public bool IsDefault { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this instance is forced. /// Gets or sets a value indicating whether this instance is forced.
/// </summary> /// </summary>

View File

@ -7,31 +7,37 @@ namespace Emby.Naming.TV
/// </summary> /// </summary>
/// <value>The path.</value> /// <value>The path.</value>
public string Path { get; set; } public string Path { get; set; }
/// <summary> /// <summary>
/// Gets or sets the container. /// Gets or sets the container.
/// </summary> /// </summary>
/// <value>The container.</value> /// <value>The container.</value>
public string Container { get; set; } public string Container { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name of the series. /// Gets or sets the name of the series.
/// </summary> /// </summary>
/// <value>The name of the series.</value> /// <value>The name of the series.</value>
public string SeriesName { get; set; } public string SeriesName { get; set; }
/// <summary> /// <summary>
/// Gets or sets the format3 d. /// Gets or sets the format3 d.
/// </summary> /// </summary>
/// <value>The format3 d.</value> /// <value>The format3 d.</value>
public string Format3D { get; set; } public string Format3D { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether [is3 d]. /// Gets or sets a value indicating whether [is3 d].
/// </summary> /// </summary>
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value> /// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
public bool Is3D { get; set; } public bool Is3D { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this instance is stub. /// Gets or sets a value indicating whether this instance is stub.
/// </summary> /// </summary>
/// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value> /// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value>
public bool IsStub { get; set; } public bool IsStub { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the stub. /// Gets or sets the type of the stub.
/// </summary> /// </summary>
@ -39,12 +45,17 @@ namespace Emby.Naming.TV
public string StubType { get; set; } public string StubType { get; set; }
public int? SeasonNumber { get; set; } public int? SeasonNumber { get; set; }
public int? EpisodeNumber { get; set; } public int? EpisodeNumber { get; set; }
public int? EndingEpsiodeNumber { get; set; } public int? EndingEpsiodeNumber { get; set; }
public int? Year { get; set; } public int? Year { get; set; }
public int? Month { get; set; } public int? Month { get; set; }
public int? Day { get; set; } public int? Day { get; set; }
public bool IsByDate { get; set; } public bool IsByDate { get; set; }
} }
} }

View File

@ -15,12 +15,12 @@ namespace Emby.Naming.TV
_options = options; _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. // 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 // 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. // the regex which require file extensions are modified so that they don't need them.
if (IsDirectory) if (isDirectory)
{ {
path += ".mp4"; path += ".mp4";
} }
@ -29,28 +29,20 @@ namespace Emby.Naming.TV
foreach (var expression in _options.EpisodeExpressions) 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); var currentResult = Parse(path, expression);
@ -97,7 +89,8 @@ namespace Emby.Naming.TV
DateTime date; DateTime date;
if (expression.DateTimeFormats.Length > 0) if (expression.DateTimeFormats.Length > 0)
{ {
if (DateTime.TryParseExact(match.Groups[0].Value, if (DateTime.TryParseExact(
match.Groups[0].Value,
expression.DateTimeFormats, expression.DateTimeFormats,
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
DateTimeStyles.None, DateTimeStyles.None,
@ -109,15 +102,12 @@ namespace Emby.Naming.TV
result.Success = true; 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.Year = date.Year; result.Day = date.Day;
result.Month = date.Month; result.Success = true;
result.Day = date.Day;
result.Success = true;
}
} }
// TODO: Only consider success if date successfully parsed? // 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. // 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 // 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; 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)) if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{ {
@ -160,6 +151,7 @@ namespace Emby.Naming.TV
{ {
result.SeasonNumber = num; result.SeasonNumber = num;
} }
if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{ {
result.EpisodeNumber = num; result.EpisodeNumber = num;
@ -171,8 +163,11 @@ namespace Emby.Naming.TV
// Invalidate match when the season is 200 through 1927 or above 2500 // 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. // 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. // 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.Success = false;
}
result.IsByDate = expression.IsByDate; result.IsByDate = expression.IsByDate;
} }

View File

@ -3,14 +3,21 @@ namespace Emby.Naming.TV
public class EpisodePathParserResult public class EpisodePathParserResult
{ {
public int? SeasonNumber { get; set; } public int? SeasonNumber { get; set; }
public int? EpisodeNumber { get; set; } public int? EpisodeNumber { get; set; }
public int? EndingEpsiodeNumber { get; set; } public int? EndingEpsiodeNumber { get; set; }
public string SeriesName { get; set; } public string SeriesName { get; set; }
public bool Success { get; set; } public bool Success { get; set; }
public bool IsByDate { get; set; } public bool IsByDate { get; set; }
public int? Year { get; set; } public int? Year { get; set; }
public int? Month { get; set; } public int? Month { get; set; }
public int? Day { get; set; } public int? Day { get; set; }
} }
} }

View File

@ -15,7 +15,13 @@ namespace Emby.Naming.TV
_options = options; _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)) if (string.IsNullOrEmpty(path))
{ {
@ -26,7 +32,7 @@ namespace Emby.Naming.TV
string container = null; string container = null;
string stubType = null; string stubType = null;
if (!IsDirectory) if (!isDirectory)
{ {
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path);
// Check supported extensions // Check supported extensions
@ -52,7 +58,7 @@ namespace Emby.Naming.TV
var format3DResult = new Format3DParser(_options).Parse(flags); var format3DResult = new Format3DParser(_options).Parse(flags);
var parsingResult = new EpisodePathParser(_options) var parsingResult = new EpisodePathParser(_options)
.Parse(path, IsDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo); .Parse(path, isDirectory, isNamed, isOptimistic, supportsAbsoluteNumbers, fillExtendedInfo);
return new EpisodeInfo return new EpisodeInfo
{ {

View File

@ -3,30 +3,24 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Emby.Naming.Common; using Emby.Naming.Common;
using Emby.Naming.Extensions;
namespace Emby.Naming.TV namespace Emby.Naming.TV
{ {
public class SeasonPathParser public class SeasonPathParser
{ {
private readonly NamingOptions _options;
public SeasonPathParser(NamingOptions options)
{
_options = options;
}
public SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders) public SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
{ {
var result = new SeasonPathParserResult(); var result = new SeasonPathParserResult();
var seasonNumberInfo = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders); var seasonNumberInfo = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
result.SeasonNumber = seasonNumberInfo.Item1; result.SeasonNumber = seasonNumberInfo.seasonNumber;
if (result.SeasonNumber.HasValue) if (result.SeasonNumber.HasValue)
{ {
result.Success = true; result.Success = true;
result.IsSeasonFolder = seasonNumberInfo.Item2; result.IsSeasonFolder = seasonNumberInfo.isSeasonFolder;
} }
return result; return result;
@ -35,7 +29,7 @@ namespace Emby.Naming.TV
/// <summary> /// <summary>
/// A season folder must contain one of these somewhere in the name /// A season folder must contain one of these somewhere in the name
/// </summary> /// </summary>
private static readonly string[] SeasonFolderNames = private static readonly string[] _seasonFolderNames =
{ {
"season", "season",
"sæson", "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="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> /// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
/// <returns>System.Nullable{System.Int32}.</returns> /// <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 (supportSpecialAliases)
{ {
if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase)) if (string.Equals(filename, "specials", StringComparison.OrdinalIgnoreCase))
{ {
return new Tuple<int?, bool>(0, true); return (0, true);
} }
if (string.Equals(filename, "extras", StringComparison.OrdinalIgnoreCase)) 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)) 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)) 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 // 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); var index = filename.IndexOf(name, StringComparison.OrdinalIgnoreCase);
@ -107,10 +105,10 @@ namespace Emby.Naming.TV
var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries); var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
var resultNumber = parts.Select(GetSeasonNumberFromPart).FirstOrDefault(i => i.HasValue); 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)) if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
{ {
@ -132,7 +130,7 @@ namespace Emby.Naming.TV
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <returns>System.Nullable{System.Int32}.</returns> /// <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 numericStart = -1;
var length = 0; var length = 0;
@ -174,10 +172,10 @@ namespace Emby.Naming.TV
if (numericStart == -1) 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);
} }
} }
} }

View File

@ -7,11 +7,13 @@ namespace Emby.Naming.TV
/// </summary> /// </summary>
/// <value>The season number.</value> /// <value>The season number.</value>
public int? SeasonNumber { get; set; } public int? SeasonNumber { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this <see cref="SeasonPathParserResult"/> is success. /// Gets or sets a value indicating whether this <see cref="SeasonPathParserResult"/> is success.
/// </summary> /// </summary>
/// <value><c>true</c> if success; otherwise, <c>false</c>.</value> /// <value><c>true</c> if success; otherwise, <c>false</c>.</value>
public bool Success { get; set; } public bool Success { get; set; }
public bool IsSeasonFolder { get; set; } public bool IsSeasonFolder { get; set; }
} }
} }

View File

@ -27,8 +27,8 @@ namespace Emby.Naming.Video
{ {
var extension = Path.GetExtension(name) ?? string.Empty; var extension = Path.GetExtension(name) ?? string.Empty;
// Check supported extensions // Check supported extensions
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase) && if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)
!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) && !_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{ {
// Dummy up a file extension because the expressions will fail without one // 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 // This is tricky because we can't just check Path.GetExtension for empty
@ -38,7 +38,6 @@ namespace Emby.Naming.Video
} }
catch (ArgumentException) catch (ArgumentException)
{ {
} }
var result = _options.CleanDateTimeRegexes.Select(i => Clean(name, i)) var result = _options.CleanDateTimeRegexes.Select(i => Clean(name, i))
@ -69,14 +68,15 @@ namespace Emby.Naming.Video
var match = expression.Match(name); 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;
name = match.Groups[1].Value; result.HasChanged = true;
result.Year = year;
result.HasChanged = true;
}
} }
result.Name = name; result.Name = name;

View File

@ -56,7 +56,6 @@ namespace Emby.Naming.Video
result.Rule = rule; result.Rule = rule;
} }
} }
else if (rule.RuleType == ExtraRuleType.Suffix) else if (rule.RuleType == ExtraRuleType.Suffix)
{ {
var filename = Path.GetFileNameWithoutExtension(path); var filename = Path.GetFileNameWithoutExtension(path);
@ -67,7 +66,6 @@ namespace Emby.Naming.Video
result.Rule = rule; result.Rule = rule;
} }
} }
else if (rule.RuleType == ExtraRuleType.Regex) else if (rule.RuleType == ExtraRuleType.Regex)
{ {
var filename = Path.GetFileName(path); var filename = Path.GetFileName(path);

View File

@ -15,9 +15,9 @@ namespace Emby.Naming.Video
Files = new List<string>(); 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); return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
} }

View File

@ -15,10 +15,12 @@ namespace Emby.Naming.Video
public Format3DResult Parse(string path) public Format3DResult Parse(string path)
{ {
var delimeters = _options.VideoFlagDelimiters.ToList(); int oldLen = _options.VideoFlagDelimiters.Length;
delimeters.Add(' '); 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) internal Format3DResult Parse(string[] videoFlags)
@ -66,8 +68,10 @@ namespace Emby.Naming.Video
format = flag; format = flag;
result.Tokens.Add(rule.Token); result.Tokens.Add(rule.Token);
} }
break; break;
} }
foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase); foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase);
} }

View File

@ -4,25 +4,27 @@ namespace Emby.Naming.Video
{ {
public class Format3DResult public class Format3DResult
{ {
public Format3DResult()
{
Tokens = new List<string>();
}
/// <summary> /// <summary>
/// Gets or sets a value indicating whether [is3 d]. /// Gets or sets a value indicating whether [is3 d].
/// </summary> /// </summary>
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value> /// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
public bool Is3D { get; set; } public bool Is3D { get; set; }
/// <summary> /// <summary>
/// Gets or sets the format3 d. /// Gets or sets the format3 d.
/// </summary> /// </summary>
/// <value>The format3 d.</value> /// <value>The format3 d.</value>
public string Format3D { get; set; } public string Format3D { get; set; }
/// <summary> /// <summary>
/// Gets or sets the tokens. /// Gets or sets the tokens.
/// </summary> /// </summary>
/// <value>The tokens.</value> /// <value>The tokens.</value>
public List<string> Tokens { get; set; } public List<string> Tokens { get; set; }
public Format3DResult()
{
Tokens = new List<string>();
}
} }
} }

View File

@ -40,17 +40,24 @@ namespace Emby.Naming.Video
var result = new StackResult(); var result = new StackResult();
foreach (var directory in files.GroupBy(file => file.IsDirectory ? file.FullName : Path.GetDirectoryName(file.FullName))) foreach (var directory in files.GroupBy(file => file.IsDirectory ? file.FullName : Path.GetDirectoryName(file.FullName)))
{ {
var stack = new FileStack(); var stack = new FileStack()
stack.Name = Path.GetFileName(directory.Key); {
stack.IsDirectoryStack = false; Name = Path.GetFileName(directory.Key),
IsDirectoryStack = false
};
foreach (var file in directory) foreach (var file in directory)
{ {
if (file.IsDirectory) if (file.IsDirectory)
{
continue; continue;
}
stack.Files.Add(file.FullName); stack.Files.Add(file.FullName);
} }
result.Stacks.Add(stack); result.Stacks.Add(stack);
} }
return result; return result;
} }
@ -114,16 +121,16 @@ namespace Emby.Naming.Video
{ {
if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
{ {
if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase) && if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase)) && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
{ {
if (stack.Files.Count == 0) if (stack.Files.Count == 0)
{ {
stack.Name = title1 + ignore1; stack.Name = title1 + ignore1;
stack.IsDirectoryStack = file1.IsDirectory; stack.IsDirectoryStack = file1.IsDirectory;
//stack.Name = title1 + ignore1 + extension1;
stack.Files.Add(file1.FullName); stack.Files.Add(file1.FullName);
} }
stack.Files.Add(file2.FullName); stack.Files.Add(file2.FullName);
} }
else else

View File

@ -9,24 +9,32 @@ namespace Emby.Naming.Video
{ {
public static StubResult ResolveFile(string path, NamingOptions options) public static StubResult ResolveFile(string path, NamingOptions options)
{ {
var result = new StubResult(); if (path == null)
var extension = Path.GetExtension(path) ?? string.Empty;
if (options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{ {
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;
}
} }
} }

View File

@ -7,6 +7,7 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value> /// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value>
public bool IsStub { get; set; } public bool IsStub { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the stub. /// Gets or sets the type of the stub.
/// </summary> /// </summary>

View File

@ -7,6 +7,7 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <value>The token.</value> /// <value>The token.</value>
public string Token { get; set; } public string Token { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the stub. /// Gets or sets the type of the stub.
/// </summary> /// </summary>

View File

@ -1,4 +1,3 @@
namespace Emby.Naming.Video namespace Emby.Naming.Video
{ {
/// <summary> /// <summary>
@ -11,56 +10,67 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <value>The path.</value> /// <value>The path.</value>
public string Path { get; set; } public string Path { get; set; }
/// <summary> /// <summary>
/// Gets or sets the container. /// Gets or sets the container.
/// </summary> /// </summary>
/// <value>The container.</value> /// <value>The container.</value>
public string Container { get; set; } public string Container { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name. /// Gets or sets the name.
/// </summary> /// </summary>
/// <value>The name.</value> /// <value>The name.</value>
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// Gets or sets the year. /// Gets or sets the year.
/// </summary> /// </summary>
/// <value>The year.</value> /// <value>The year.</value>
public int? Year { get; set; } public int? Year { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the extra, e.g. trailer, theme song, behing the scenes, etc. /// Gets or sets the type of the extra, e.g. trailer, theme song, behing the scenes, etc.
/// </summary> /// </summary>
/// <value>The type of the extra.</value> /// <value>The type of the extra.</value>
public string ExtraType { get; set; } public string ExtraType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the extra rule. /// Gets or sets the extra rule.
/// </summary> /// </summary>
/// <value>The extra rule.</value> /// <value>The extra rule.</value>
public ExtraRule ExtraRule { get; set; } public ExtraRule ExtraRule { get; set; }
/// <summary> /// <summary>
/// Gets or sets the format3 d. /// Gets or sets the format3 d.
/// </summary> /// </summary>
/// <value>The format3 d.</value> /// <value>The format3 d.</value>
public string Format3D { get; set; } public string Format3D { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether [is3 d]. /// Gets or sets a value indicating whether [is3 d].
/// </summary> /// </summary>
/// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value> /// <value><c>true</c> if [is3 d]; otherwise, <c>false</c>.</value>
public bool Is3D { get; set; } public bool Is3D { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this instance is stub. /// Gets or sets a value indicating whether this instance is stub.
/// </summary> /// </summary>
/// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value> /// <value><c>true</c> if this instance is stub; otherwise, <c>false</c>.</value>
public bool IsStub { get; set; } public bool IsStub { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type of the stub. /// Gets or sets the type of the stub.
/// </summary> /// </summary>
/// <value>The type of the stub.</value> /// <value>The type of the stub.</value>
public string StubType { get; set; } public string StubType { get; set; }
/// <summary> /// <summary>
/// Gets or sets the type. /// Gets or sets the type.
/// </summary> /// </summary>
/// <value>The type.</value> /// <value>The type.</value>
public bool IsDirectory { get; set; } public bool IsDirectory { get; set; }
/// <summary> /// <summary>
/// Gets the file name without extension. /// Gets the file name without extension.
/// </summary> /// </summary>

View File

@ -12,21 +12,25 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <value>The name.</value> /// <value>The name.</value>
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// Gets or sets the year. /// Gets or sets the year.
/// </summary> /// </summary>
/// <value>The year.</value> /// <value>The year.</value>
public int? Year { get; set; } public int? Year { get; set; }
/// <summary> /// <summary>
/// Gets or sets the files. /// Gets or sets the files.
/// </summary> /// </summary>
/// <value>The files.</value> /// <value>The files.</value>
public List<VideoFileInfo> Files { get; set; } public List<VideoFileInfo> Files { get; set; }
/// <summary> /// <summary>
/// Gets or sets the extras. /// Gets or sets the extras.
/// </summary> /// </summary>
/// <value>The extras.</value> /// <value>The extras.</value>
public List<VideoFileInfo> Extras { get; set; } public List<VideoFileInfo> Extras { get; set; }
/// <summary> /// <summary>
/// Gets or sets the alternate versions. /// Gets or sets the alternate versions.
/// </summary> /// </summary>

View File

@ -53,7 +53,7 @@ namespace Emby.Naming.Video
Name = stack.Name Name = stack.Name
}; };
info.Year = info.Files.First().Year; info.Year = info.Files[0].Year;
var extraBaseNames = new List<string> var extraBaseNames = new List<string>
{ {
@ -87,7 +87,7 @@ namespace Emby.Naming.Video
Name = media.Name Name = media.Name
}; };
info.Year = info.Files.First().Year; info.Year = info.Files[0].Year;
var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension }); var extras = GetExtras(remainingFiles, new List<string> { media.FileNameWithoutExtension });
@ -115,7 +115,7 @@ namespace Emby.Naming.Video
if (!string.IsNullOrEmpty(parentPath)) if (!string.IsNullOrEmpty(parentPath))
{ {
var folderName = Path.GetFileName(Path.GetDirectoryName(videoPath)); var folderName = Path.GetFileName(parentPath);
if (!string.IsNullOrEmpty(folderName)) if (!string.IsNullOrEmpty(folderName))
{ {
var extras = GetExtras(remainingFiles, new List<string> { folderName }); var extras = GetExtras(remainingFiles, new List<string> { folderName });
@ -163,9 +163,7 @@ namespace Emby.Naming.Video
Year = i.Year Year = i.Year
})); }));
var orderedList = list.OrderBy(i => i.Name); return list.OrderBy(i => i.Name);
return orderedList;
} }
private IEnumerable<VideoInfo> GetVideosGroupedByVersion(List<VideoInfo> videos) 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)); 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))) var ordered = videos.OrderBy(i => i.Name).ToList();
{
if (HaveSameYear(videos))
{
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].AlternateVersions = ordered.Skip(1).Select(i => i.Files[0]).ToList();
list[0].Name = folderName; list[0].Name = folderName;
list[0].Extras.AddRange(ordered.Skip(1).SelectMany(i => i.Extras)); list[0].Extras.AddRange(ordered.Skip(1).SelectMany(i => i.Extras));
return list; return list;
}
}
} }
return videos; return videos;
@ -213,9 +209,9 @@ namespace Emby.Naming.Video
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase)) if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{ {
testFilename = testFilename.Substring(folderName.Length).Trim(); testFilename = testFilename.Substring(folderName.Length).Trim();
return string.IsNullOrEmpty(testFilename) || return string.IsNullOrEmpty(testFilename)
testFilename.StartsWith("-") || || testFilename[0] == '-'
string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)) ; || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
} }
return false; return false;

View File

@ -38,10 +38,11 @@ namespace Emby.Naming.Video
/// Resolves the specified path. /// Resolves the specified path.
/// </summary> /// </summary>
/// <param name="path">The path.</param> /// <param name="path">The path.</param>
/// <param name="IsDirectory">if set to <c>true</c> [is folder].</param> /// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
/// <param name="parseName">Whether or not the name should be parsed for info</param>
/// <returns>VideoFileInfo.</returns> /// <returns>VideoFileInfo.</returns>
/// <exception cref="ArgumentNullException">path</exception> /// <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)) if (string.IsNullOrEmpty(path))
{ {
@ -52,9 +53,10 @@ namespace Emby.Naming.Video
string container = null; string container = null;
string stubType = null; string stubType = null;
if (!IsDirectory) if (!isDirectory)
{ {
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path);
// Check supported extensions // Check supported extensions
if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{ {
@ -79,7 +81,7 @@ namespace Emby.Naming.Video
var extraResult = new ExtraResolver(_options).GetExtraInfo(path); var extraResult = new ExtraResolver(_options).GetExtraInfo(path);
var name = IsDirectory var name = isDirectory
? Path.GetFileName(path) ? Path.GetFileName(path)
: Path.GetFileNameWithoutExtension(path); : Path.GetFileNameWithoutExtension(path);
@ -108,7 +110,7 @@ namespace Emby.Naming.Video
Is3D = format3DResult.Is3D, Is3D = format3DResult.Is3D,
Format3D = format3DResult.Format3D, Format3D = format3DResult.Format3D,
ExtraType = extraResult.ExtraType, ExtraType = extraResult.ExtraType,
IsDirectory = IsDirectory, IsDirectory = isDirectory,
ExtraRule = extraResult.Rule ExtraRule = extraResult.Rule
}; };
} }

View File

@ -20,7 +20,10 @@ namespace Emby.Photos
public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor
{ {
private readonly ILogger _logger; 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) public PhotoProvider(ILogger logger, IImageProcessor imageProcessor)
{ {
@ -28,75 +31,55 @@ namespace Emby.Photos
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
} }
public string Name => "Embedded Information";
public bool HasChanged(BaseItem item, IDirectoryService directoryService) public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{ {
if (item.IsFileProtocol) if (item.IsFileProtocol)
{ {
var file = directoryService.GetFile(item.Path); var file = directoryService.GetFile(item.Path);
if (file != null && file.LastWriteTimeUtc != item.DateModified) return (file != null && file.LastWriteTimeUtc != item.DateModified);
{
return true;
}
} }
return false; 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) public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken)
{ {
item.SetImagePath(ImageType.Primary, item.Path); item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs // 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 try
{ {
using (var file = TagLib.File.Create(item.Path)) using (var file = TagLib.File.Create(item.Path))
{ {
var image = file as TagLib.Image.File; if (file.GetTag(TagTypes.TiffIFD) is IFDTag tag)
var tag = file.GetTag(TagTypes.TiffIFD) as IFDTag;
if (tag != null)
{ {
var structure = tag.Structure; 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; var exifStructure = exif.Structure;
if (exifStructure != null)
if (exif != null)
{ {
var exifStructure = exif.Structure; var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry;
if (entry != null)
if (exifStructure != null)
{ {
var entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ApertureValue) as RationalIFDEntry; item.Aperture = (double)entry.Value.Numerator / entry.Value.Denominator;
}
if (entry != null) entry = exifStructure.GetEntry(0, (ushort)ExifEntryTag.ShutterSpeedValue) as RationalIFDEntry;
{ if (entry != null)
double val = entry.Value.Numerator; {
val /= entry.Value.Denominator; item.ShutterSpeed = (double)entry.Value.Numerator / 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;
}
} }
} }
} }
} }
if (image != null) if (file is TagLib.Image.File image)
{ {
item.CameraMake = image.ImageTag.Make; item.CameraMake = image.ImageTag.Make;
item.CameraModel = image.ImageTag.Model; item.CameraModel = image.ImageTag.Model;
@ -116,12 +99,10 @@ namespace Emby.Photos
item.Overview = image.ImageTag.Comment; 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; var dateTaken = image.ImageTag.DateTime;
@ -140,12 +121,9 @@ namespace Emby.Photos
{ {
item.Orientation = null; 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; item.ExposureTime = image.ImageTag.ExposureTime;
@ -195,7 +173,5 @@ namespace Emby.Photos
const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport; const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport;
return Task.FromResult(result); return Task.FromResult(result);
} }
public string Name => "Embedded Information";
} }
} }

View File

@ -3,12 +3,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -29,31 +27,39 @@ namespace Emby.Server.Implementations.Activity
{ {
public class ActivityLogEntryPoint : IServerEntryPoint public class ActivityLogEntryPoint : IServerEntryPoint
{ {
private readonly ILogger _logger;
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly ITaskManager _taskManager; private readonly ITaskManager _taskManager;
private readonly IActivityManager _activityManager; private readonly IActivityManager _activityManager;
private readonly ILocalizationManager _localization; private readonly ILocalizationManager _localization;
private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subManager; private readonly ISubtitleManager _subManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IDeviceManager _deviceManager; 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; _sessionManager = sessionManager;
_deviceManager = deviceManager;
_taskManager = taskManager; _taskManager = taskManager;
_activityManager = activityManager; _activityManager = activityManager;
_localization = localization; _localization = localization;
_installationManager = installationManager; _installationManager = installationManager;
_libraryManager = libraryManager;
_subManager = subManager; _subManager = subManager;
_userManager = userManager; _userManager = userManager;
_config = config;
_appHost = appHost; _appHost = appHost;
_deviceManager = deviceManager;
} }
public Task RunAsync() public Task RunAsync()
@ -83,8 +89,6 @@ namespace Emby.Server.Implementations.Activity
_deviceManager.CameraImageUploaded += OnCameraImageUploaded; _deviceManager.CameraImageUploaded += OnCameraImageUploaded;
_appHost.ApplicationUpdated += OnApplicationUpdated;
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -124,7 +128,7 @@ namespace Emby.Server.Implementations.Activity
if (item == null) if (item == null)
{ {
//_logger.LogWarning("PlaybackStopped reported with null media info."); _logger.LogWarning("PlaybackStopped reported with null media info.");
return; return;
} }
@ -155,7 +159,7 @@ namespace Emby.Server.Implementations.Activity
if (item == null) if (item == null)
{ {
//_logger.LogWarning("PlaybackStart reported with null media info."); _logger.LogWarning("PlaybackStart reported with null media info.");
return; return;
} }
@ -203,6 +207,7 @@ namespace Emby.Server.Implementations.Activity
{ {
return NotificationType.AudioPlayback.ToString(); return NotificationType.AudioPlayback.ToString();
} }
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{ {
return NotificationType.VideoPlayback.ToString(); return NotificationType.VideoPlayback.ToString();
@ -217,6 +222,7 @@ namespace Emby.Server.Implementations.Activity
{ {
return NotificationType.AudioPlaybackStopped.ToString(); return NotificationType.AudioPlaybackStopped.ToString();
} }
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{ {
return NotificationType.VideoPlaybackStopped.ToString(); 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) private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
{ {
CreateLogEntry(new ActivityLogEntry CreateLogEntry(new ActivityLogEntry
@ -415,6 +411,7 @@ namespace Emby.Server.Implementations.Activity
{ {
vals.Add(e.Result.ErrorMessage); vals.Add(e.Result.ErrorMessage);
} }
if (!string.IsNullOrEmpty(e.Result.LongErrorMessage)) if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
{ {
vals.Add(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), Name = string.Format(_localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
Type = NotificationType.TaskFailed.ToString(), Type = NotificationType.TaskFailed.ToString(),
Overview = string.Join(Environment.NewLine, vals.ToArray()), Overview = string.Join(Environment.NewLine, vals),
ShortOverview = runningTime, ShortOverview = runningTime,
Severity = LogLevel.Error Severity = LogLevel.Error
}); });
@ -460,8 +457,6 @@ namespace Emby.Server.Implementations.Activity
_userManager.UserLockedOut -= OnUserLockedOut; _userManager.UserLockedOut -= OnUserLockedOut;
_deviceManager.CameraImageUploaded -= OnCameraImageUploaded; _deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
_appHost.ApplicationUpdated -= OnApplicationUpdated;
} }
/// <summary> /// <summary>
@ -503,6 +498,7 @@ namespace Emby.Server.Implementations.Activity
{ {
values.Add(CreateValueString(span.Hours, "hour")); values.Add(CreateValueString(span.Hours, "hour"));
} }
// Number of minutes // Number of minutes
if (span.Minutes >= 1) if (span.Minutes >= 1)
{ {
@ -526,6 +522,7 @@ namespace Emby.Server.Implementations.Activity
builder.Append(values[i]); builder.Append(values[i]);
} }
// Return result // Return result
return builder.ToString(); return builder.ToString();
} }

View File

@ -15,14 +15,14 @@ namespace Emby.Server.Implementations.Activity
{ {
public class ActivityRepository : BaseSqliteRepository, IActivityRepository public class ActivityRepository : BaseSqliteRepository, IActivityRepository
{ {
private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
protected IFileSystem FileSystem { get; private set; } private readonly IFileSystem _fileSystem;
public ActivityRepository(ILoggerFactory loggerFactory, IServerApplicationPaths appPaths, IFileSystem fileSystem) public ActivityRepository(ILoggerFactory loggerFactory, IServerApplicationPaths appPaths, IFileSystem fileSystem)
: base(loggerFactory.CreateLogger(nameof(ActivityRepository))) : base(loggerFactory.CreateLogger(nameof(ActivityRepository)))
{ {
DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db"); DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db");
FileSystem = fileSystem; _fileSystem = fileSystem;
} }
public void Initialize() public void Initialize()
@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.Activity
{ {
Logger.LogError(ex, "Error loading database file. Will reset and retry."); Logger.LogError(ex, "Error loading database file. Will reset and retry.");
FileSystem.DeleteFile(DbFilePath); _fileSystem.DeleteFile(DbFilePath);
InitializeInternal(); InitializeInternal();
} }
@ -43,10 +43,8 @@ namespace Emby.Server.Implementations.Activity
private void InitializeInternal() private void InitializeInternal()
{ {
using (var connection = CreateConnection()) using (var connection = GetConnection())
{ {
RunDefaultInitialization(connection);
connection.RunQueries(new[] 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)", "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)); throw new ArgumentNullException(nameof(entry));
} }
using (WriteLock.Write()) using (var connection = GetConnection())
using (var connection = CreateConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(db =>
{ {
@ -124,8 +121,7 @@ namespace Emby.Server.Implementations.Activity
throw new ArgumentNullException(nameof(entry)); throw new ArgumentNullException(nameof(entry));
} }
using (WriteLock.Write()) using (var connection = GetConnection())
using (var connection = CreateConnection())
{ {
connection.RunInTransaction(db => connection.RunInTransaction(db =>
{ {
@ -159,8 +155,7 @@ namespace Emby.Server.Implementations.Activity
public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit)
{ {
using (WriteLock.Read()) using (var connection = GetConnection(true))
using (var connection = CreateConnection(true))
{ {
var commandText = BaseActivitySelectText; var commandText = BaseActivitySelectText;
var whereClauses = new List<string>(); var whereClauses = new List<string>();
@ -218,7 +213,7 @@ namespace Emby.Server.Implementations.Activity
var list = new List<ActivityLogEntry>(); var list = new List<ActivityLogEntry>();
var result = new QueryResult<ActivityLogEntry>(); var result = new QueryResult<ActivityLogEntry>();
var statements = PrepareAllSafe(db, statementTexts).ToList(); var statements = PrepareAll(db, statementTexts).ToList();
using (var statement = statements[0]) using (var statement = statements[0])
{ {

View File

@ -6,6 +6,8 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
@ -154,11 +156,6 @@ namespace Emby.Server.Implementations
/// </summary> /// </summary>
public event EventHandler HasPendingRestartChanged; public event EventHandler HasPendingRestartChanged;
/// <summary>
/// Occurs when [application updated].
/// </summary>
public event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
/// <summary> /// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart. /// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary> /// </summary>
@ -173,11 +170,17 @@ namespace Emby.Server.Implementations
/// <value>The logger.</value> /// <value>The logger.</value>
protected ILogger Logger { get; set; } protected ILogger Logger { get; set; }
private IPlugin[] _plugins;
/// <summary> /// <summary>
/// Gets the plugins. /// Gets the plugins.
/// </summary> /// </summary>
/// <value>The plugins.</value> /// <value>The plugins.</value>
public IPlugin[] Plugins { get; protected set; } public IPlugin[] Plugins
{
get => _plugins;
protected set => _plugins = value;
}
/// <summary> /// <summary>
/// Gets or sets the logger factory. /// Gets or sets the logger factory.
@ -200,7 +203,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// The disposable parts /// The disposable parts
/// </summary> /// </summary>
protected readonly List<IDisposable> _disposableParts = new List<IDisposable>(); private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
/// <summary> /// <summary>
/// Gets the configuration manager. /// Gets the configuration manager.
@ -216,8 +219,9 @@ namespace Emby.Server.Implementations
{ {
#if BETA #if BETA
return PackageVersionClass.Beta; return PackageVersionClass.Beta;
#endif #else
return PackageVersionClass.Release; return PackageVersionClass.Release;
#endif
} }
} }
@ -229,11 +233,6 @@ namespace Emby.Server.Implementations
/// <value>The server configuration manager.</value> /// <value>The server configuration manager.</value>
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager; public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
protected virtual IResourceFileManager CreateResourceFileManager()
{
return new ResourceFileManager(HttpResultFactory, LoggerFactory, FileSystemManager);
}
/// <summary> /// <summary>
/// Gets or sets the user manager. /// Gets or sets the user manager.
/// </summary> /// </summary>
@ -340,7 +339,6 @@ namespace Emby.Server.Implementations
protected IProcessFactory ProcessFactory { get; private set; } protected IProcessFactory ProcessFactory { get; private set; }
protected ICryptoProvider CryptographyProvider = new CryptographyProvider();
protected readonly IXmlSerializer XmlSerializer; protected readonly IXmlSerializer XmlSerializer;
protected ISocketFactory SocketFactory { get; private set; } protected ISocketFactory SocketFactory { get; private set; }
@ -369,9 +367,6 @@ namespace Emby.Server.Implementations
{ {
_configuration = configuration; _configuration = configuration;
// hack alert, until common can target .net core
BaseExtensions.CryptographyProvider = CryptographyProvider;
XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory); XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory);
NetworkManager = networkManager; NetworkManager = networkManager;
@ -426,7 +421,7 @@ namespace Emby.Server.Implementations
/// Gets the current application user agent /// Gets the current application user agent
/// </summary> /// </summary>
/// <value>The application user agent.</value> /// <value>The application user agent.</value>
public string ApplicationUserAgent => Name.Replace(' ','-') + "/" + ApplicationVersion; public string ApplicationUserAgent => Name.Replace(' ','-') + '/' + ApplicationVersion;
/// <summary> /// <summary>
/// Gets the email address for use within a comment section of a user agent field. /// Gets the email address for use within a comment section of a user agent field.
@ -617,8 +612,6 @@ namespace Emby.Server.Implementations
DiscoverTypes(); DiscoverTypes();
SetHttpLimit();
await RegisterResources(serviceCollection).ConfigureAwait(false); await RegisterResources(serviceCollection).ConfigureAwait(false);
FindParts(); FindParts();
@ -686,11 +679,6 @@ namespace Emby.Server.Implementations
await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false); 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; } public static IStreamHelper StreamHelper { get; set; }
/// <summary> /// <summary>
@ -716,7 +704,11 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(FileSystemManager); serviceCollection.AddSingleton(FileSystemManager);
serviceCollection.AddSingleton<TvDbClientManager>(); serviceCollection.AddSingleton<TvDbClientManager>();
HttpClient = CreateHttpClient(); HttpClient = new HttpClientManager.HttpClientManager(
ApplicationPaths,
LoggerFactory.CreateLogger<HttpClientManager.HttpClientManager>(),
FileSystemManager,
() => ApplicationUserAgent);
serviceCollection.AddSingleton(HttpClient); serviceCollection.AddSingleton(HttpClient);
serviceCollection.AddSingleton(NetworkManager); serviceCollection.AddSingleton(NetworkManager);
@ -735,13 +727,12 @@ namespace Emby.Server.Implementations
ApplicationHost.StreamHelper = new StreamHelper(); ApplicationHost.StreamHelper = new StreamHelper();
serviceCollection.AddSingleton(StreamHelper); serviceCollection.AddSingleton(StreamHelper);
serviceCollection.AddSingleton(CryptographyProvider); serviceCollection.AddSingleton(typeof(ICryptoProvider), typeof(CryptographyProvider));
SocketFactory = new SocketFactory(); SocketFactory = new SocketFactory();
serviceCollection.AddSingleton(SocketFactory); serviceCollection.AddSingleton(SocketFactory);
InstallationManager = new InstallationManager(LoggerFactory, this, ApplicationPaths, HttpClient, JsonSerializer, ServerConfigurationManager, FileSystemManager, CryptographyProvider, ZipClient, PackageRuntime); serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager));
serviceCollection.AddSingleton(InstallationManager);
ZipClient = new ZipClient(); ZipClient = new ZipClient();
serviceCollection.AddSingleton(ZipClient); serviceCollection.AddSingleton(ZipClient);
@ -763,10 +754,6 @@ namespace Emby.Server.Implementations
UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager); UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager);
serviceCollection.AddSingleton(UserDataManager); 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); var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager);
serviceCollection.AddSingleton<IDisplayPreferencesRepository>(displayPreferencesRepo); serviceCollection.AddSingleton<IDisplayPreferencesRepository>(displayPreferencesRepo);
@ -776,6 +763,8 @@ namespace Emby.Server.Implementations
AuthenticationRepository = GetAuthenticationRepository(); AuthenticationRepository = GetAuthenticationRepository();
serviceCollection.AddSingleton(AuthenticationRepository); serviceCollection.AddSingleton(AuthenticationRepository);
UserRepository = GetUserRepository();
UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager);
serviceCollection.AddSingleton(UserManager); serviceCollection.AddSingleton(UserManager);
@ -816,7 +805,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(TVSeriesManager); serviceCollection.AddSingleton(TVSeriesManager);
DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager); DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
serviceCollection.AddSingleton(DeviceManager); serviceCollection.AddSingleton(DeviceManager);
MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder); 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); DtoService = new DtoService(LoggerFactory, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ProviderManager, this, () => MediaSourceManager, () => LiveTvManager);
serviceCollection.AddSingleton(DtoService); 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); 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(SessionManager);
serviceCollection.AddSingleton<IDlnaManager>( 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); SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(LibraryManager, LoggerFactory, ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory);
serviceCollection.AddSingleton(SubtitleEncoder); serviceCollection.AddSingleton(SubtitleEncoder);
serviceCollection.AddSingleton(CreateResourceFileManager()); serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager));
displayPreferencesRepo.Initialize(); displayPreferencesRepo.Initialize();
@ -908,8 +896,6 @@ namespace Emby.Server.Implementations
_serviceProvider = serviceCollection.BuildServiceProvider(); _serviceProvider = serviceCollection.BuildServiceProvider();
} }
public virtual string PackageRuntime => "netcore";
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths) public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
{ {
// Distinct these to prevent users from reporting problems that aren't actually problems // Distinct these to prevent users from reporting problems that aren't actually problems
@ -918,8 +904,7 @@ namespace Emby.Server.Implementations
.Distinct(); .Distinct();
logger.LogInformation("Arguments: {Args}", commandLineArgs); logger.LogInformation("Arguments: {Args}", commandLineArgs);
// FIXME: @bond this logs the kernel version, not the OS version logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
logger.LogInformation("Operating system: {OS} {OSVersion}", OperatingSystem.Name, Environment.OSVersion.Version);
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture); logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess); logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive); logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
@ -929,19 +914,6 @@ namespace Emby.Server.Implementations
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath); 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) private X509Certificate2 GetCertificate(CertificateInfo info)
{ {
var certificateLocation = info?.Path; var certificateLocation = info?.Path;
@ -1035,7 +1007,6 @@ namespace Emby.Server.Implementations
Video.LiveTvManager = LiveTvManager; Video.LiveTvManager = LiveTvManager;
Folder.UserViewManager = UserViewManager; Folder.UserViewManager = UserViewManager;
UserView.TVSeriesManager = TVSeriesManager; UserView.TVSeriesManager = TVSeriesManager;
UserView.PlaylistManager = PlaylistManager;
UserView.CollectionManager = CollectionManager; UserView.CollectionManager = CollectionManager;
BaseItem.MediaSourceManager = MediaSourceManager; BaseItem.MediaSourceManager = MediaSourceManager;
CollectionFolder.XmlSerializer = XmlSerializer; CollectionFolder.XmlSerializer = XmlSerializer;
@ -1044,11 +1015,47 @@ namespace Emby.Server.Implementations
AuthenticatedAttribute.AuthService = AuthService; 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> /// <summary>
/// Finds the parts. /// Finds the parts.
/// </summary> /// </summary>
protected void FindParts() protected void FindParts()
{ {
InstallationManager = _serviceProvider.GetService<IInstallationManager>();
InstallationManager.PluginInstalled += PluginInstalled;
if (!ServerConfigurationManager.Configuration.IsPortAuthorized) if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
{ {
ServerConfigurationManager.Configuration.IsPortAuthorized = true; ServerConfigurationManager.Configuration.IsPortAuthorized = true;
@ -1088,7 +1095,7 @@ namespace Emby.Server.Implementations
MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>()); MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>());
NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
UserManager.AddParts(GetExports<IAuthenticationProvider>()); UserManager.AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
IsoManager.AddParts(GetExports<IIsoMounter>()); IsoManager.AddParts(GetExports<IIsoMounter>());
} }
@ -1131,7 +1138,7 @@ namespace Emby.Server.Implementations
} }
catch (Exception ex) 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; return null;
} }
@ -1145,10 +1152,32 @@ namespace Emby.Server.Implementations
{ {
Logger.LogInformation("Loading assemblies"); Logger.LogInformation("Loading assemblies");
AllConcreteTypes = GetComposablePartAssemblies() AllConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
.SelectMany(x => x.ExportedTypes) }
.Where(type => type.IsClass && !type.IsAbstract && !type.IsInterface && !type.IsGenericType)
.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; } private CertificateInfo CertificateInfo { get; set; }
@ -1310,10 +1339,21 @@ namespace Emby.Server.Implementations
{ {
if (Directory.Exists(ApplicationPaths.PluginsPath)) 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); Assembly plugAss;
yield return Assembly.LoadFrom(file); 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) public async Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken)
{ {
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); 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 return new SystemInfo
{ {
HasPendingRestart = HasPendingRestart, HasPendingRestart = HasPendingRestart,
IsShuttingDown = IsShuttingDown, IsShuttingDown = IsShuttingDown,
Version = ApplicationVersion, Version = ApplicationVersion,
ProductName = ApplicationProductName,
WebSocketPortNumber = HttpPort, WebSocketPortNumber = HttpPort,
CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(), CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(),
Id = SystemId, Id = SystemId,
@ -1422,10 +1471,21 @@ namespace Emby.Server.Implementations
public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken) public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken)
{ {
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); 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 return new PublicSystemInfo
{ {
Version = ApplicationVersion, Version = ApplicationVersion,
ProductName = ApplicationProductName,
Id = SystemId, Id = SystemId,
OperatingSystem = OperatingSystem.Id.ToString(), OperatingSystem = OperatingSystem.Id.ToString(),
WanAddress = wanAddress, WanAddress = wanAddress,
@ -1460,7 +1520,7 @@ namespace Emby.Server.Implementations
return null; return null;
} }
public async Task<string> GetWanApiUrl(CancellationToken cancellationToken) public async Task<string> GetWanApiUrlFromExternal(CancellationToken cancellationToken)
{ {
const string Url = "http://ipv4.icanhazip.com"; const string Url = "http://ipv4.icanhazip.com";
try try
@ -1471,44 +1531,96 @@ namespace Emby.Server.Implementations
LogErrorResponseBody = false, LogErrorResponseBody = false,
LogErrors = false, LogErrors = false,
LogRequest = false, LogRequest = false,
TimeoutMs = 10000,
BufferContent = false, BufferContent = false,
CancellationToken = cancellationToken CancellationToken = cancellationToken
}).ConfigureAwait(false)) }).ConfigureAwait(false))
{ {
return GetLocalApiUrl(response.ReadToEnd().Trim()); string res = await response.ReadToEndAsync().ConfigureAwait(false);
return GetWanApiUrl(res.Trim());
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogError(ex, "Error getting WAN Ip address information"); Logger.LogError(ex, "Error getting WAN Ip address information");
} }
return null; 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) public string GetLocalApiUrl(string host)
{ {
if (EnableHttps)
{
return string.Format("https://{0}:{1}",
host,
HttpsPort.ToString(CultureInfo.InvariantCulture));
}
return string.Format("http://{0}:{1}", return string.Format("http://{0}:{1}",
host, host,
HttpPort.ToString(CultureInfo.InvariantCulture)); 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); 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 var addresses = ServerConfigurationManager
.Configuration .Configuration
@ -1522,13 +1634,13 @@ namespace Emby.Server.Implementations
addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces)); addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
} }
var resultList = new List<IpAddressInfo>(); var resultList = new List<IPAddress>();
foreach (var address in addresses) foreach (var address in addresses)
{ {
if (!allowLoopback) if (!allowLoopback)
{ {
if (address.Equals(IpAddressInfo.Loopback) || address.Equals(IpAddressInfo.IPv6Loopback)) if (address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback))
{ {
continue; continue;
} }
@ -1549,7 +1661,7 @@ namespace Emby.Server.Implementations
return resultList; return resultList;
} }
private IpAddressInfo NormalizeConfiguredLocalAddress(string address) private IPAddress NormalizeConfiguredLocalAddress(string address)
{ {
var index = address.Trim('/').IndexOf('/'); var index = address.Trim('/').IndexOf('/');
@ -1558,7 +1670,7 @@ namespace Emby.Server.Implementations
address = address.Substring(index + 1); address = address.Substring(index + 1);
} }
if (NetworkManager.TryParseIpAddress(address.Trim('/'), out IpAddressInfo result)) if (IPAddress.TryParse(address.Trim('/'), out IPAddress result))
{ {
return result; return result;
} }
@ -1568,10 +1680,10 @@ namespace Emby.Server.Implementations
private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase); 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) || if (address.Equals(IPAddress.Loopback) ||
address.Equals(IpAddressInfo.IPv6Loopback)) address.Equals(IPAddress.IPv6Loopback))
{ {
return true; return true;
} }
@ -1599,15 +1711,13 @@ namespace Emby.Server.Implementations
LogErrorResponseBody = false, LogErrorResponseBody = false,
LogErrors = LogPing, LogErrors = LogPing,
LogRequest = LogPing, LogRequest = LogPing,
TimeoutMs = 5000,
BufferContent = false, BufferContent = false,
CancellationToken = cancellationToken CancellationToken = cancellationToken
}, "POST").ConfigureAwait(false)) }, HttpMethod.Post).ConfigureAwait(false))
{ {
using (var reader = new StreamReader(response.Content)) 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); var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase);
_validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); _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; private bool _disposed = false;
/// <summary> /// <summary>
@ -1818,8 +1910,12 @@ namespace Emby.Server.Implementations
Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name); Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name);
} }
} }
UserRepository.Dispose();
} }
UserRepository = null;
_disposed = true; _disposed = true;
} }
} }

View File

@ -6,7 +6,6 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress; using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
@ -20,7 +19,6 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Channels; using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
@ -40,11 +38,8 @@ namespace Emby.Server.Implementations.Channels
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClient _httpClient;
private readonly IProviderManager _providerManager; private readonly IProviderManager _providerManager;
private readonly ILocalizationManager _localization;
public ChannelManager( public ChannelManager(
IUserManager userManager, IUserManager userManager,
IDtoService dtoService, IDtoService dtoService,
@ -54,8 +49,6 @@ namespace Emby.Server.Implementations.Channels
IFileSystem fileSystem, IFileSystem fileSystem,
IUserDataManager userDataManager, IUserDataManager userDataManager,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
ILocalizationManager localization,
IHttpClient httpClient,
IProviderManager providerManager) IProviderManager providerManager)
{ {
_userManager = userManager; _userManager = userManager;
@ -66,8 +59,6 @@ namespace Emby.Server.Implementations.Channels
_fileSystem = fileSystem; _fileSystem = fileSystem;
_userDataManager = userDataManager; _userDataManager = userDataManager;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_localization = localization;
_httpClient = httpClient;
_providerManager = providerManager; _providerManager = providerManager;
} }

View File

@ -74,23 +74,14 @@ namespace Emby.Server.Implementations.Configuration
/// </summary> /// </summary>
private void UpdateMetadataPath() private void UpdateMetadataPath()
{ {
string metadataPath;
if (string.IsNullOrWhiteSpace(Configuration.MetadataPath)) if (string.IsNullOrWhiteSpace(Configuration.MetadataPath))
{ {
metadataPath = GetInternalMetadataPath(); ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
} }
else 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> /// <summary>

View File

@ -4,7 +4,6 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Linq;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
namespace Emby.Server.Implementations.Cryptography namespace Emby.Server.Implementations.Cryptography
@ -136,7 +135,7 @@ namespace Emby.Server.Implementations.Cryptography
{ {
return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations); return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations);
} }
public byte[] ComputeHash(PasswordHash hash) public byte[] ComputeHash(PasswordHash hash)
{ {
int iterations = _defaultIterations; int iterations = _defaultIterations;

View File

@ -1,183 +1,144 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL;
using SQLitePCL.pretty; using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data namespace Emby.Server.Implementations.Data
{ {
public abstract class BaseSqliteRepository : IDisposable public abstract class BaseSqliteRepository : IDisposable
{ {
protected string DbFilePath { get; set; } private bool _disposed = false;
protected ReaderWriterLockSlim WriteLock;
protected ILogger Logger { get; private set; }
protected BaseSqliteRepository(ILogger logger) protected BaseSqliteRepository(ILogger logger)
{ {
Logger = 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; 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; 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; WriteLock.Wait();
if (WriteConnection != null)
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)
{ {
return _connection; return new ManagedConnection(WriteConnection, WriteLock);
} }
lock (WriteLock) WriteConnection = SQLite3.Open(
DbFilePath,
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
null);
if (CacheSize.HasValue)
{ {
if (!_versionLogged) WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
{
_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;
} }
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) public IStatement PrepareStatement(ManagedConnection connection, string sql)
{ => connection.PrepareStatement(sql);
return connection.PrepareStatement(sql);
}
public IStatement PrepareStatementSafe(ManagedConnection connection, string sql)
{
return connection.PrepareStatement(sql);
}
public IStatement PrepareStatement(IDatabaseConnection connection, string sql) public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
{ => connection.PrepareStatement(sql);
return connection.PrepareStatement(sql);
}
public IStatement PrepareStatementSafe(IDatabaseConnection connection, string sql) public IEnumerable<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql)
{ => sql.Select(connection.PrepareStatement);
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();
}
protected bool TableExists(ManagedConnection connection, string name) protected bool TableExists(ManagedConnection connection, string name)
{ {
@ -199,103 +160,9 @@ namespace Emby.Server.Implementations.Data
}, ReadTransactionMode); }, 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) 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 + ")")) foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
{ {
@ -303,11 +170,11 @@ namespace Emby.Server.Implementations.Data
{ {
var name = row[1].ToString(); 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) 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"); 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 /// <summary>
{ /// SQLite continues without syncing as soon as it has handed data off to the operating system
private ReaderWriterLockSlim _sync; /// </summary>
public ReadLockToken(ReaderWriterLockSlim sync) Off = 0,
{
_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;
}
}
}
public static IDisposable Read(this ReaderWriterLockSlim obj) /// <summary>
{ /// SQLite database engine will still sync at the most critical moments
//if (BaseSqliteRepository.ThreadSafeMode > 0) /// </summary>
//{ Normal = 1,
// return new DummyToken();
//}
return new WriteLockToken(obj);
}
public static IDisposable Write(this ReaderWriterLockSlim obj) /// <summary>
{ /// SQLite database engine will use the xSync method of the VFS
//if (BaseSqliteRepository.ThreadSafeMode > 0) /// to ensure that all content is safely written to the disk surface prior to continuing.
//{ /// </summary>
// return new DummyToken(); Full = 2,
//}
return new WriteLockToken(obj); /// <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
} }
} }

View File

@ -1,79 +1,78 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using SQLitePCL.pretty; using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data namespace Emby.Server.Implementations.Data
{ {
public class ManagedConnection : IDisposable public class ManagedConnection : IDisposable
{ {
private SQLiteDatabaseConnection db; private SQLiteDatabaseConnection _db;
private readonly bool _closeOnDispose; private readonly SemaphoreSlim _writeLock;
private bool _disposed = false;
public ManagedConnection(SQLiteDatabaseConnection db, bool closeOnDispose) public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
{ {
this.db = db; _db = db;
_closeOnDispose = closeOnDispose; _writeLock = writeLock;
} }
public IStatement PrepareStatement(string sql) public IStatement PrepareStatement(string sql)
{ {
return db.PrepareStatement(sql); return _db.PrepareStatement(sql);
} }
public IEnumerable<IStatement> PrepareAll(string sql) public IEnumerable<IStatement> PrepareAll(string sql)
{ {
return db.PrepareAll(sql); return _db.PrepareAll(sql);
} }
public void ExecuteAll(string sql) public void ExecuteAll(string sql)
{ {
db.ExecuteAll(sql); _db.ExecuteAll(sql);
} }
public void Execute(string sql, params object[] values) public void Execute(string sql, params object[] values)
{ {
db.Execute(sql, values); _db.Execute(sql, values);
} }
public void RunQueries(string[] sql) public void RunQueries(string[] sql)
{ {
db.RunQueries(sql); _db.RunQueries(sql);
} }
public void RunInTransaction(Action<IDatabaseConnection> action, TransactionMode mode) 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) 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) 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) public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql, params object[] values)
{ {
return db.Query(sql, values); return _db.Query(sql, values);
}
public void Close()
{
using (db)
{
}
} }
public void Dispose() public void Dispose()
{ {
if (_closeOnDispose) if (_disposed)
{ {
Close(); return;
} }
_writeLock.Release();
_db = null; // Don't dispose it
_disposed = true;
} }
} }
} }

View File

@ -18,13 +18,13 @@ namespace Emby.Server.Implementations.Data
/// </summary> /// </summary>
public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository 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) public SqliteDisplayPreferencesRepository(ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IFileSystem fileSystem)
: base(loggerFactory.CreateLogger(nameof(SqliteDisplayPreferencesRepository))) : base(loggerFactory.CreateLogger(nameof(SqliteDisplayPreferencesRepository)))
{ {
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
FileSystem = fileSystem; _fileSystem = fileSystem;
DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); 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."); Logger.LogError(ex, "Error loading database file. Will reset and retry.");
FileSystem.DeleteFile(DbFilePath); _fileSystem.DeleteFile(DbFilePath);
InitializeInternal(); InitializeInternal();
} }
@ -61,10 +61,8 @@ namespace Emby.Server.Implementations.Data
/// <returns>Task.</returns> /// <returns>Task.</returns>
private void InitializeInternal() private void InitializeInternal()
{ {
using (var connection = CreateConnection()) using (var connection = GetConnection())
{ {
RunDefaultInitialization(connection);
string[] queries = { string[] queries = {
"create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)", "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)); throw new ArgumentNullException(nameof(displayPreferences));
} }
if (string.IsNullOrEmpty(displayPreferences.Id)) if (string.IsNullOrEmpty(displayPreferences.Id))
{ {
throw new ArgumentNullException(nameof(displayPreferences.Id)); throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
} }
cancellationToken.ThrowIfCancellationRequested(); 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(); 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);
{ }
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); }, TransactionMode);
}
}, TransactionMode);
}
} }
} }
@ -173,27 +166,24 @@ namespace Emby.Server.Implementations.Data
var guidId = displayPreferencesId.GetMD5(); 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("@id", guidId.ToGuidBlob()); statement.TryBind("@client", client);
statement.TryBind("@userId", userId.ToGuidBlob());
statement.TryBind("@client", client);
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
{ {
return Get(row); 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>(); 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()) foreach (var row in statement.ExecuteQuery())
{ {
list.Add(Get(row)); list.Add(Get(row));
}
} }
} }
} }

View File

@ -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); var commandText = string.Format("attach @path as {0};", alias);

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SQLitePCL.pretty; using SQLitePCL.pretty;
@ -33,14 +32,14 @@ namespace Emby.Server.Implementations.Data
/// Opens the connection to the database /// Opens the connection to the database
/// </summary> /// </summary>
/// <returns>Task.</returns> /// <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.Dispose();
WriteLock = writeLock; WriteLock = dbLock;
WriteConnection?.Dispose();
WriteConnection = dbConnection;
using (var connection = CreateConnection()) using (var connection = GetConnection())
{ {
var userDatasTableExists = TableExists(connection, "UserDatas"); var userDatasTableExists = TableExists(connection, "UserDatas");
var userDataTableExists = TableExists(connection, "userdata"); var userDataTableExists = TableExists(connection, "userdata");
@ -129,8 +128,6 @@ namespace Emby.Server.Implementations.Data
return list; return list;
} }
protected override bool EnableTempStoreMemory => true;
/// <summary> /// <summary>
/// Saves the user data. /// Saves the user data.
/// </summary> /// </summary>
@ -178,15 +175,12 @@ namespace Emby.Server.Implementations.Data
{ {
cancellationToken.ThrowIfCancellationRequested(); 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(); 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);
{ }
SaveUserData(db, internalUserId, userItemData.Key, userItemData); }, TransactionMode);
}
}, TransactionMode);
}
} }
} }
@ -281,28 +272,26 @@ namespace Emby.Server.Implementations.Data
{ {
throw new ArgumentNullException(nameof(internalUserId)); throw new ArgumentNullException(nameof(internalUserId));
} }
if (string.IsNullOrEmpty(key)) if (string.IsNullOrEmpty(key))
{ {
throw new ArgumentNullException(nameof(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); return ReadRow(row);
statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery())
{
return ReadRow(row);
}
} }
return null;
} }
return null;
} }
} }
@ -335,18 +324,15 @@ namespace Emby.Server.Implementations.Data
var list = new List<UserItemData>(); 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()) foreach (var row in statement.ExecuteQuery())
{ {
list.Add(ReadRow(row)); list.Add(ReadRow(row));
}
} }
} }
} }
@ -392,15 +378,5 @@ namespace Emby.Server.Implementations.Data
return userData; return userData;
} }
protected override void Dispose(bool dispose)
{
// handled by library database
}
protected override void CloseConnection()
{
// handled by library database
}
} }
} }

View File

@ -40,10 +40,8 @@ namespace Emby.Server.Implementations.Data
/// <returns>Task.</returns> /// <returns>Task.</returns>
public void Initialize() public void Initialize()
{ {
using (var connection = CreateConnection()) using (var connection = GetConnection())
{ {
RunDefaultInitialization(connection);
var localUsersTableExists = TableExists(connection, "LocalUsersv2"); var localUsersTableExists = TableExists(connection, "LocalUsersv2");
connection.RunQueries(new[] { connection.RunQueries(new[] {
@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Data
TryMigrateToLocalUsersTable(connection); 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 the user password is the sha1 hash of the empty string, remove it
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal) 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; continue;
} }
@ -89,22 +87,16 @@ namespace Emby.Server.Implementations.Data
user.Password = null; user.Password = null;
var serialized = _jsonSerializer.SerializeToBytes(user); var serialized = _jsonSerializer.SerializeToBytes(user);
using (WriteLock.Write()) connection.RunInTransaction(db =>
using (var connection = CreateConnection())
{ {
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.TryBind("@InternalId", user.InternalId); statement.MoveNext();
statement.TryBind("@data", serialized); }
statement.MoveNext(); }, TransactionMode);
}
}, TransactionMode);
}
} }
} }
/// <summary> /// <summary>
@ -119,31 +111,28 @@ namespace Emby.Server.Implementations.Data
var serialized = _jsonSerializer.SerializeToBytes(user); 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) if (createdUser == null)
{ {
throw new ApplicationException("created user should never be 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); 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.TryBind("@InternalId", user.InternalId); statement.MoveNext();
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)) statement.TryBind("@guid", guid);
{
using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
{
statement.TryBind("@guid", guid);
foreach (var row in statement.ExecuteQuery()) foreach (var row in statement.ExecuteQuery())
{ {
return GetUser(row); return GetUser(row);
}
}
} }
} }
@ -216,20 +196,22 @@ namespace Emby.Server.Implementations.Data
/// <returns>IEnumerable{User}.</returns> /// <returns>IEnumerable{User}.</returns>
public List<User> RetrieveAllUsers() public List<User> RetrieveAllUsers()
{ {
var list = new List<User>(); using (var connection = GetConnection(true))
using (WriteLock.Read())
{ {
using (var connection = CreateConnection(true)) return new List<User>(RetrieveAllUsers(connection));
{
foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
{
list.Add(GetUser(row));
}
}
} }
}
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> /// <summary>
@ -245,19 +227,16 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(user)); 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();
statement.TryBind("@id", user.InternalId); }
statement.MoveNext(); }, TransactionMode);
}
}, TransactionMode);
}
} }
} }
} }

View File

@ -89,14 +89,11 @@ namespace Emby.Server.Implementations.Dto
var channelTuples = new List<Tuple<BaseItemDto, LiveTvChannel>>(); var channelTuples = new List<Tuple<BaseItemDto, LiveTvChannel>>();
var index = 0; var index = 0;
var allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList();
foreach (var item in items) 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 (item is LiveTvChannel tvChannel)
if (tvChannel != null)
{ {
channelTuples.Add(new Tuple<BaseItemDto, LiveTvChannel>(dto, 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)); programTuples.Add(new Tuple<BaseItem, BaseItemDto>(item, dto));
} }
var byName = item as IItemByName; if (item is IItemByName byName)
if (byName != null)
{ {
if (options.ContainsField(ItemFields.ItemCounts)) if (options.ContainsField(ItemFields.ItemCounts))
{ {
@ -130,8 +125,7 @@ namespace Emby.Server.Implementations.Dto
if (programTuples.Count > 0) if (programTuples.Count > 0)
{ {
var task = _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user); _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
Task.WaitAll(task);
} }
if (channelTuples.Count > 0) 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) 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, user, owner);
var dto = GetBaseItemDtoInternal(item, options, allCollectionFolders, user, owner);
var tvChannel = item as LiveTvChannel; var tvChannel = item as LiveTvChannel;
if (tvChannel != null) 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 var dto = new BaseItemDto
{ {
@ -312,6 +305,7 @@ namespace Emby.Server.Implementations.Dto
{ {
path = path.TrimStart('.'); path = path.TrimStart('.');
} }
if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase)) if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase))
{ {
fileExtensionContainer = path; fileExtensionContainer = path;
@ -325,8 +319,7 @@ namespace Emby.Server.Implementations.Dto
public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null) public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem> taggedItems, User user = null)
{ {
var allCollectionFolders = _libraryManager.GetUserRootFolder().Children.OfType<Folder>().ToList(); var dto = GetBaseItemDtoInternal(item, options, user);
var dto = GetBaseItemDtoInternal(item, options, allCollectionFolders, user);
if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts)) if (taggedItems != null && options.ContainsField(ItemFields.ItemCounts))
{ {
@ -1051,14 +1044,15 @@ namespace Emby.Server.Implementations.Dto
} }
else 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) .SelectMany(i => i.MediaStreams)
.ToArray(); .ToArray();
} }
} }
else else
{ {
mediaStreams = _mediaSourceManager().GetStaticMediaSources(item, true).First().MediaStreams.ToArray(); mediaStreams = _mediaSourceManager().GetStaticMediaSources(item, true)[0].MediaStreams.ToArray();
} }
dto.MediaStreams = mediaStreams; dto.MediaStreams = mediaStreams;

View File

@ -20,6 +20,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="IPNetwork2" Version="2.4.0.126" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" 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" /> <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.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" 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="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.4.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.5.0" />
<PackageReference Include="sharpcompress" Version="0.22.0" /> <PackageReference Include="sharpcompress" Version="0.23.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="1.0.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="1.0.0" />
<PackageReference Include="UTF.Unknown" Version="1.0.0-beta1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -52,8 +52,8 @@
<!-- Code analysers--> <!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.3" /> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
</ItemGroup> </ItemGroup>

View File

@ -388,7 +388,7 @@ namespace Emby.Server.Implementations.EntryPoints
FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(), 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; 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>(); var list = new List<string>();

View File

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

View File

@ -1,11 +1,10 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Cache; using System.Net.Http;
using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -24,30 +23,24 @@ namespace Emby.Server.Implementations.HttpClientManager
/// </summary> /// </summary>
public class HttpClientManager : IHttpClient 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; private readonly ILogger _logger;
/// <summary>
/// The _app paths
/// </summary>
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly Func<string> _defaultUserAgentFn; 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> /// <summary>
/// Initializes a new instance of the <see cref="HttpClientManager" /> class. /// Initializes a new instance of the <see cref="HttpClientManager" /> class.
/// </summary> /// </summary>
public HttpClientManager( public HttpClientManager(
IApplicationPaths appPaths, IApplicationPaths appPaths,
ILoggerFactory loggerFactory, ILogger<HttpClientManager> logger,
IFileSystem fileSystem, IFileSystem fileSystem,
Func<string> defaultUserAgentFn) Func<string> defaultUserAgentFn)
{ {
@ -55,46 +48,33 @@ namespace Emby.Server.Implementations.HttpClientManager
{ {
throw new ArgumentNullException(nameof(appPaths)); 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; _fileSystem = fileSystem;
_appPaths = appPaths; _appPaths = appPaths;
_defaultUserAgentFn = defaultUserAgentFn; _defaultUserAgentFn = defaultUserAgentFn;
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
ServicePointManager.Expect100Continue = false;
} }
/// <summary> /// <summary>
/// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests. /// Gets the correct http client for the given url.
/// DON'T dispose it after use.
/// </summary> /// </summary>
/// <value>The HTTP clients.</value> /// <param name="url">The url.</param>
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>
/// <returns>HttpClient.</returns> /// <returns>HttpClient.</returns>
/// <exception cref="ArgumentNullException">host</exception> private HttpClient GetHttpClient(string url)
private HttpClientInfo GetHttpClient(string host, bool enableHttpCompression)
{ {
if (string.IsNullOrEmpty(host)) var key = GetHostFromUrl(url);
{
throw new ArgumentNullException(nameof(host));
}
var key = host + enableHttpCompression;
if (!_httpClients.TryGetValue(key, out var client)) if (!_httpClients.TryGetValue(key, out var client))
{ {
client = new HttpClientInfo(); client = new HttpClient()
{
BaseAddress = new Uri(url)
};
_httpClients.TryAdd(key, client); _httpClients.TryAdd(key, client);
} }
@ -102,119 +82,84 @@ namespace Emby.Server.Implementations.HttpClientManager
return client; return client;
} }
private WebRequest GetRequest(HttpRequestOptions options, string method) private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method)
{ {
string url = options.Url; string url = options.Url;
var uriAddress = new Uri(url); var uriAddress = new Uri(url);
string userInfo = uriAddress.UserInfo; string userInfo = uriAddress.UserInfo;
if (!string.IsNullOrWhiteSpace(userInfo)) if (!string.IsNullOrWhiteSpace(userInfo))
{ {
_logger.LogInformation("Found userInfo in url: {0} ... url: {1}", userInfo, url); _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url);
url = url.Replace(userInfo + "@", string.Empty); 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); case CompressionMethod.Deflate | CompressionMethod.Gzip:
request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" });
if (options.EnableHttpCompression) break;
{ case CompressionMethod.Deflate:
httpWebRequest.AutomaticDecompression = DecompressionMethods.Deflate; request.Headers.Add(HeaderNames.AcceptEncoding, "deflate");
if (options.DecompressionMethod.HasValue break;
&& options.DecompressionMethod.Value == CompressionMethod.Gzip) case CompressionMethod.Gzip:
{ request.Headers.Add(HeaderNames.AcceptEncoding, "gzip");
httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip; break;
} default:
} break;
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;
}
} }
request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); if (options.EnableKeepAlive)
{
request.Headers.Add(HeaderNames.Connection, "Keep-Alive");
}
request.Method = method; //request.Headers.Add(HeaderNames.CacheControl, "no-cache");
request.Timeout = options.TimeoutMs;
/*
if (!string.IsNullOrWhiteSpace(userInfo)) if (!string.IsNullOrWhiteSpace(userInfo))
{ {
var parts = userInfo.Split(':'); var parts = userInfo.Split(':');
if (parts.Length == 2) if (parts.Length == 2)
{ {
request.Credentials = GetCredential(url, parts[0], parts[1]); request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]);
// TODO: .net core ??
request.PreAuthenticate = true;
} }
} }
*/
return request; return request;
} }
private static CredentialCache GetCredential(string url, string username, string password) private void AddRequestHeaders(HttpRequestMessage request, HttpRequestOptions options)
{
//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)
{ {
var hasUserAgent = false; var hasUserAgent = false;
foreach (var header in options.RequestHeaders) 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; hasUserAgent = true;
} }
else
{ request.Headers.Add(header.Key, header.Value);
request.Headers.Set(header.Key, header.Value);
}
} }
if (!hasUserAgent && options.EnableDefaultUserAgent) 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> /// <summary>
/// Gets the response internal. /// Gets the response internal.
/// </summary> /// </summary>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
/// <returns>Task{HttpResponseInfo}.</returns> /// <returns>Task{HttpResponseInfo}.</returns>
public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options) public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options)
{ => SendAsync(options, HttpMethod.Get);
return SendAsync(options, "GET");
}
/// <summary> /// <summary>
/// Performs a GET request and returns the resulting stream /// 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="options">The options.</param>
/// <param name="httpMethod">The HTTP method.</param> /// <param name="httpMethod">The HTTP method.</param>
/// <returns>Task{HttpResponseInfo}.</returns> /// <returns>Task{HttpResponseInfo}.</returns>
/// <exception cref="HttpException"> public Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod)
/// </exception> => SendAsync(options, new HttpMethod(httpMethod));
public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string 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) 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); ValidateParams(options);
options.CancellationToken.ThrowIfCancellationRequested(); 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 httpWebRequest.Content = new ByteArrayContent(options.RequestContentBytes);
};
}
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);
} }
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.LogRequest)
{ {
if (options.LogRequestAsDebug) _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url);
{
_logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), options.Url);
}
else
{
_logger.LogInformation("HttpClientManager {0}: {1}", httpMethod.ToUpper(CultureInfo.CurrentCulture), 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(); Content = stream,
StatusCode = response.StatusCode,
if (!options.BufferContent) ContentType = response.Content.Headers.ContentType?.MediaType,
{ ContentLength = response.Content.Headers.ContentLength,
var response = await GetResponseAsync(httpWebRequest, TimeSpan.FromMilliseconds(options.TimeoutMs)).ConfigureAwait(false); ResponseUrl = response.Content.Headers.ContentLocation?.ToString()
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()
}; };
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) public Task<HttpResponseInfo> Post(HttpRequestOptions options)
{ => SendAsync(options, HttpMethod.Post);
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;
}
/// <summary> /// <summary>
/// Downloads the contents of a given url into a temporary location /// 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) public async Task<string> GetTempFile(HttpRequestOptions options)
{ {
var response = await GetTempFileResponse(options).ConfigureAwait(false); var response = await GetTempFileResponse(options).ConfigureAwait(false);
return response.TempFilePath; return response.TempFilePath;
} }
@ -502,44 +333,28 @@ namespace Emby.Server.Implementations.HttpClientManager
options.CancellationToken.ThrowIfCancellationRequested(); options.CancellationToken.ThrowIfCancellationRequested();
var httpWebRequest = GetRequest(options, "GET"); var httpWebRequest = GetRequestMessage(options, HttpMethod.Get);
if (options.ResourcePool != null)
{
await options.ResourcePool.WaitAsync(options.CancellationToken).ConfigureAwait(false);
}
options.Progress.Report(0); options.Progress.Report(0);
if (options.LogRequest) if (options.LogRequest)
{ {
if (options.LogRequestAsDebug) _logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
{
_logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
}
else
{
_logger.LogInformation("HttpClientManager.GetTempFileResponse url: {0}", options.Url);
}
} }
var client = GetHttpClient(GetHostFromUrl(options.Url), options.EnableHttpCompression); var client = GetHttpClient(options.Url);
try try
{ {
options.CancellationToken.ThrowIfCancellationRequested(); 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; await EnsureSuccessStatusCode(response, options).ConfigureAwait(false);
EnsureSuccessStatusCode(client, httpResponse, options);
options.CancellationToken.ThrowIfCancellationRequested(); options.CancellationToken.ThrowIfCancellationRequested();
var contentLength = GetContentLength(httpResponse); using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
using (var stream = httpResponse.GetResponseStream())
using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true))
{ {
await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false);
@ -547,35 +362,29 @@ namespace Emby.Server.Implementations.HttpClientManager
options.Progress.Report(100); 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) catch (Exception ex)
{ {
DeleteTempFile(tempFile); if (File.Exists(tempFile))
throw GetException(ex, options, client); {
} File.Delete(tempFile);
finally }
{
options.ResourcePool?.Release(); throw GetException(ex, options);
} }
} }
private static long? GetContentLength(HttpWebResponse response) private Exception GetException(Exception ex, HttpRequestOptions options)
{
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)
{ {
if (ex is HttpException) if (ex is HttpException)
{ {
@ -589,7 +398,7 @@ namespace Emby.Server.Implementations.HttpClientManager
{ {
if (options.LogErrors) 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); var exception = new HttpException(webException.Message, webException);
@ -599,11 +408,6 @@ namespace Emby.Server.Implementations.HttpClientManager
if (response != null) if (response != null)
{ {
exception.StatusCode = response.StatusCode; 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) if (operationCanceledException != null)
{ {
return GetCancellationException(options, client, options.CancellationToken, operationCanceledException); return GetCancellationException(options, options.CancellationToken, operationCanceledException);
} }
if (options.LogErrors) 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; 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) private void ValidateParams(HttpRequestOptions options)
{ {
if (string.IsNullOrEmpty(options.Url)) if (string.IsNullOrEmpty(options.Url))
@ -682,11 +474,10 @@ namespace Emby.Server.Implementations.HttpClientManager
/// Throws the cancellation exception. /// Throws the cancellation exception.
/// </summary> /// </summary>
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <param name="exception">The exception.</param> /// <param name="exception">The exception.</param>
/// <returns>Exception.</returns> /// <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 the HttpClient's timeout is reached, it will cancel the Task internally
if (!cancellationToken.IsCancellationRequested) if (!cancellationToken.IsCancellationRequested)
@ -698,8 +489,6 @@ namespace Emby.Server.Implementations.HttpClientManager
_logger.LogError(msg); _logger.LogError(msg);
} }
client.LastTimeout = DateTime.UtcNow;
// Throw an HttpException so that the caller doesn't think it was cancelled by user code // Throw an HttpException so that the caller doesn't think it was cancelled by user code
return new HttpException(msg, exception) return new HttpException(msg, exception)
{ {
@ -710,91 +499,20 @@ namespace Emby.Server.Implementations.HttpClientManager
return exception; return exception;
} }
private void EnsureSuccessStatusCode(HttpClientInfo client, HttpWebResponse response, HttpRequestOptions options) private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options)
{ {
var statusCode = response.StatusCode; if (response.IsSuccessStatusCode)
var isSuccessful = statusCode >= HttpStatusCode.OK && statusCode <= (HttpStatusCode)299;
if (isSuccessful)
{ {
return; return;
} }
if (options.LogErrorResponseBody) var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
{ _logger.LogError("HTTP request failed with message: {Message}", msg);
try
{
using (var stream = response.GetResponseStream())
{
if (stream != null)
{
using (var reader = new StreamReader(stream))
{
var msg = reader.ReadToEnd();
_logger.LogError(msg); throw new HttpException(response.ReasonPhrase)
}
}
}
}
catch
{
}
}
throw new HttpException(response.StatusDescription)
{ {
StatusCode = response.StatusCode 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);
}
}
}
} }
} }

View File

@ -67,6 +67,7 @@ namespace Emby.Server.Implementations.HttpServer
if (string.IsNullOrWhiteSpace(rangeHeader)) if (string.IsNullOrWhiteSpace(rangeHeader))
{ {
Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture);
StatusCode = HttpStatusCode.OK; StatusCode = HttpStatusCode.OK;
} }
else else
@ -99,10 +100,13 @@ namespace Emby.Server.Implementations.HttpServer
RangeStart = requestedRange.Key; RangeStart = requestedRange.Key;
RangeLength = 1 + RangeEnd - RangeStart; 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}"; var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
Headers[HeaderNames.ContentRange] = rangeString; 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> /// <summary>

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Sockets; using System.Net.Sockets;
@ -11,7 +10,6 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.SocketSharp;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -127,12 +125,12 @@ namespace Emby.Server.Implementations.HttpServer
private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType) 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); var serviceType = GetServiceTypeByRequest(requestDtoType);
if (serviceType != null) 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); attributes.Sort((x, y) => x.Priority - y.Priority);
@ -154,7 +152,7 @@ namespace Emby.Server.Implementations.HttpServer
QueryString = e.QueryString ?? new QueryCollection() QueryString = e.QueryString ?? new QueryCollection()
}; };
connection.Closed += Connection_Closed; connection.Closed += OnConnectionClosed;
lock (_webSocketConnections) lock (_webSocketConnections)
{ {
@ -164,7 +162,7 @@ namespace Emby.Server.Implementations.HttpServer
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
} }
private void Connection_Closed(object sender, EventArgs e) private void OnConnectionClosed(object sender, EventArgs e)
{ {
lock (_webSocketConnections) lock (_webSocketConnections)
{ {
@ -203,6 +201,7 @@ namespace Emby.Server.Implementations.HttpServer
case DirectoryNotFoundException _: case DirectoryNotFoundException _:
case FileNotFoundException _: case FileNotFoundException _:
case ResourceNotFoundException _: return 404; case ResourceNotFoundException _: return 404;
case MethodNotAllowedException _: return 405;
case RemoteServiceUnavailableException _: return 502; case RemoteServiceUnavailableException _: return 502;
default: return 500; default: return 500;
} }
@ -322,14 +321,14 @@ namespace Emby.Server.Implementations.HttpServer
private static string NormalizeConfiguredLocalAddress(string address) private static string NormalizeConfiguredLocalAddress(string address)
{ {
var index = address.Trim('/').IndexOf('/'); var add = address.AsSpan().Trim('/');
int index = add.IndexOf('/');
if (index != -1) 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) private bool ValidateHost(string host)
@ -399,8 +398,8 @@ namespace Emby.Server.Implementations.HttpServer
if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1) 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 // 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 || if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
{ {
return true; return true;
} }
@ -572,7 +571,7 @@ namespace Emby.Server.Implementations.HttpServer
if (handler != null) 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 else
{ {
@ -613,21 +612,11 @@ namespace Emby.Server.Implementations.HttpServer
{ {
var pathInfo = httpReq.PathInfo; var pathInfo = httpReq.PathInfo;
var pathParts = pathInfo.TrimStart('/').Split('/'); pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
if (pathParts.Length == 0) var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
{
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);
if (restPath != null) if (restPath != null)
{ {
return new ServiceHandler return new ServiceHandler(restPath, contentType);
{
RestPath = restPath,
ResponseContentType = contentType
};
} }
Logger.LogError("Could not find handler for {PathInfo}", pathInfo); 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) private static Task Write(IResponse response, string text)
{ {
var bOutput = Encoding.UTF8.GetBytes(text); var bOutput = Encoding.UTF8.GetBytes(text);
response.OriginalResponse.ContentLength = bOutput.Length;
return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length); return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length);
} }
@ -655,11 +645,6 @@ namespace Emby.Server.Implementations.HttpServer
} }
else 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); RedirectToUrl(httpRes, url);
} }
} }
@ -684,10 +669,7 @@ namespace Emby.Server.Implementations.HttpServer
UrlPrefixes = urlPrefixes.ToArray(); UrlPrefixes = urlPrefixes.ToArray();
ServiceController = new ServiceController(); ServiceController = new ServiceController();
Logger.LogInformation("Calling ServiceStack AppHost.Init"); var types = services.Select(r => r.GetType());
var types = services.Select(r => r.GetType()).ToArray();
ServiceController.Init(this, types); ServiceController.Init(this, types);
ResponseFilters = new Action<IRequest, IResponse, object>[] ResponseFilters = new Action<IRequest, IResponse, object>[]
@ -823,19 +805,15 @@ namespace Emby.Server.Implementations.HttpServer
Logger.LogDebug("Websocket message received: {0}", result.MessageType); 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() public void Dispose()

View File

@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.HttpServer
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires)) if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires))
{ {
responseHeaders[HeaderNames.Expires] = "-1"; responseHeaders[HeaderNames.Expires] = "0";
} }
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.HttpServer
content = Array.Empty<byte>(); content = Array.Empty<byte>();
} }
result = new StreamWriter(content, contentType); result = new StreamWriter(content, contentType, contentLength);
} }
else else
{ {
@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.HttpServer
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
{ {
responseHeaders[HeaderNames.Expires] = "-1"; responseHeaders[HeaderNames.Expires] = "0";
} }
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.HttpServer
bytes = Array.Empty<byte>(); bytes = Array.Empty<byte>();
} }
result = new StreamWriter(bytes, contentType); result = new StreamWriter(bytes, contentType, contentLength);
} }
else else
{ {
@ -190,7 +190,7 @@ namespace Emby.Server.Implementations.HttpServer
if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
{ {
responseHeaders[HeaderNames.Expires] = "-1"; responseHeaders[HeaderNames.Expires] = "0";
} }
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer
responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
} }
responseHeaders[HeaderNames.Expires] = "-1"; responseHeaders[HeaderNames.Expires] = "0";
return ToOptimizedResultInternal(requestContext, result, responseHeaders); return ToOptimizedResultInternal(requestContext, result, responseHeaders);
} }
@ -335,13 +335,13 @@ namespace Emby.Server.Implementations.HttpServer
if (isHeadRequest) if (isHeadRequest)
{ {
var result = new StreamWriter(Array.Empty<byte>(), contentType); var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength);
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
return result; return result;
} }
else else
{ {
var result = new StreamWriter(content, contentType); var result = new StreamWriter(content, contentType, contentLength);
AddResponseHeaders(result, responseHeaders); AddResponseHeaders(result, responseHeaders);
return result; return result;
} }
@ -581,6 +581,11 @@ namespace Emby.Server.Implementations.HttpServer
} }
else else
{ {
if (totalContentLength.HasValue)
{
responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture);
}
if (isHeadRequest) if (isHeadRequest)
{ {
using (stream) using (stream)
@ -624,7 +629,7 @@ namespace Emby.Server.Implementations.HttpServer
if (lastModifiedDate.HasValue) if (lastModifiedDate.HasValue)
{ {
responseHeaders[HeaderNames.LastModified] = lastModifiedDate.ToString(); responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture);
} }
} }

View File

@ -96,6 +96,7 @@ namespace Emby.Server.Implementations.HttpServer
RangeStart = requestedRange.Key; RangeStart = requestedRange.Key;
RangeLength = 1 + RangeEnd - RangeStart; RangeLength = 1 + RangeEnd - RangeStart;
Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}";
if (RangeStart > 0 && SourceStream.CanSeek) if (RangeStart > 0 && SourceStream.CanSeek)

View File

@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.HttpServer
public void FilterResponse(IRequest req, IResponse res, object dto) public void FilterResponse(IRequest req, IResponse res, object dto)
{ {
// Try to prevent compatibility view // 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-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
res.AddHeader("Access-Control-Allow-Origin", "*"); res.AddHeader("Access-Control-Allow-Origin", "*");
@ -58,6 +58,7 @@ namespace Emby.Server.Implementations.HttpServer
if (length > 0) if (length > 0)
{ {
res.OriginalResponse.ContentLength = length;
res.SendChunked = false; res.SendChunked = false;
} }
} }

View File

@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
// This code is executed before the service // This code is executed before the service
var auth = AuthorizationContext.GetAuthorizationInfo(request); var auth = AuthorizationContext.GetAuthorizationInfo(request);
if (!IsExemptFromAuthenticationToken(auth, authAttribtues, request)) if (!IsExemptFromAuthenticationToken(authAttribtues, request))
{ {
ValidateSecurityToken(request, auth.Token); 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) if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard)
{ {

View File

@ -14,8 +14,6 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
public class StreamWriter : IAsyncStreamWriter, IHasHeaders public class StreamWriter : IAsyncStreamWriter, IHasHeaders
{ {
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
/// <summary> /// <summary>
/// Gets or sets the source stream. /// Gets or sets the source stream.
/// </summary> /// </summary>
@ -52,6 +50,13 @@ namespace Emby.Server.Implementations.HttpServer
SourceStream = source; SourceStream = source;
Headers["Content-Type"] = contentType;
if (source.CanSeek)
{
Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture);
}
Headers[HeaderNames.ContentType] = contentType; Headers[HeaderNames.ContentType] = contentType;
} }
@ -60,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
/// </summary> /// </summary>
/// <param name="source">The source.</param> /// <param name="source">The source.</param>
/// <param name="contentType">Type of the content.</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)) if (string.IsNullOrEmpty(contentType))
{ {
@ -69,6 +74,7 @@ namespace Emby.Server.Implementations.HttpServer
SourceBytes = source; SourceBytes = source;
Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture);
Headers[HeaderNames.ContentType] = contentType; Headers[HeaderNames.ContentType] = contentType;
} }

View File

@ -6,10 +6,6 @@ using System.Threading;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.IO namespace Emby.Server.Implementations.IO
@ -61,6 +57,7 @@ namespace Emby.Server.Implementations.IO
{ {
AddAffectedPath(path); AddAffectedPath(path);
} }
RestartTimer(); RestartTimer();
} }
@ -103,6 +100,7 @@ namespace Emby.Server.Implementations.IO
AddAffectedPath(affectedFile); AddAffectedPath(affectedFile);
} }
} }
RestartTimer(); RestartTimer();
} }

View File

@ -9,9 +9,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Server.Implementations.IO namespace Emby.Server.Implementations.IO
{ {
@ -21,6 +19,7 @@ namespace Emby.Server.Implementations.IO
/// The file system watchers /// The file system watchers
/// </summary> /// </summary>
private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
/// <summary> /// <summary>
/// The affected paths /// The affected paths
/// </summary> /// </summary>
@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.IO
throw new ArgumentNullException(nameof(path)); 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 // 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 // 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); await Task.Delay(45000).ConfigureAwait(false);
@ -162,10 +161,10 @@ namespace Emby.Server.Implementations.IO
public void Start() public void Start()
{ {
LibraryManager.ItemAdded += LibraryManager_ItemAdded; LibraryManager.ItemAdded += OnLibraryManagerItemAdded;
LibraryManager.ItemRemoved += LibraryManager_ItemRemoved; LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
var pathsToWatch = new List<string> { }; var pathsToWatch = new List<string>();
var paths = LibraryManager var paths = LibraryManager
.RootFolder .RootFolder
@ -204,7 +203,7 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="sender">The source of the event.</param> /// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</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) if (e.Parent is AggregateFolder)
{ {
@ -217,7 +216,7 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="sender">The source of the event.</param> /// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</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) if (e.Parent is AggregateFolder)
{ {
@ -244,7 +243,7 @@ namespace Emby.Server.Implementations.IO
return lst.Any(str => 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); var compare = str.TrimEnd(Path.DirectorySeparatorChar);
return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == 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)) if (!Directory.Exists(path))
{ {
// Seeing a crash in the mono runtime due to an exception being thrown on a different thread // 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; return;
} }
if (OperatingSystem.Id != OperatingSystemId.Windows)
{
if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || path.StartsWith("smb://", StringComparison.OrdinalIgnoreCase))
{
// not supported
return;
}
}
// Already being watched // Already being watched
if (_fileSystemWatchers.ContainsKey(path)) if (_fileSystemWatchers.ContainsKey(path))
{ {
@ -286,23 +276,21 @@ namespace Emby.Server.Implementations.IO
{ {
var newWatcher = new FileSystemWatcher(path, "*") 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.Created += OnWatcherChanged;
newWatcher.Deleted += OnWatcherChanged;
newWatcher.NotifyFilter = NotifyFilters.CreationTime | newWatcher.Renamed += OnWatcherChanged;
NotifyFilters.DirectoryName | newWatcher.Changed += OnWatcherChanged;
NotifyFilters.FileName | newWatcher.Error += OnWatcherError;
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;
if (_fileSystemWatchers.TryAdd(path, newWatcher)) if (_fileSystemWatchers.TryAdd(path, newWatcher))
{ {
@ -343,32 +331,16 @@ namespace Emby.Server.Implementations.IO
{ {
using (watcher) 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.Created -= OnWatcherChanged;
watcher.Deleted -= watcher_Changed; watcher.Deleted -= OnWatcherChanged;
watcher.Renamed -= watcher_Changed; watcher.Renamed -= OnWatcherChanged;
watcher.Changed -= watcher_Changed; watcher.Changed -= OnWatcherChanged;
watcher.Error -= watcher_Error; watcher.Error -= OnWatcherError;
try watcher.EnableRaisingEvents = false;
{
watcher.EnableRaisingEvents = false;
}
catch (InvalidOperationException)
{
// Seeing this under mono on linux sometimes
// Collection was modified; enumeration operation may not execute.
}
} }
}
catch (NotImplementedException)
{
// the dispose method on FileSystemWatcher is sometimes throwing NotImplementedException on Xamarin Android
}
catch
{
} }
finally finally
{ {
@ -385,7 +357,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="watcher">The watcher.</param> /// <param name="watcher">The watcher.</param>
private void RemoveWatcherFromList(FileSystemWatcher watcher) private void RemoveWatcherFromList(FileSystemWatcher watcher)
{ {
_fileSystemWatchers.TryRemove(watcher.Path, out var removed); _fileSystemWatchers.TryRemove(watcher.Path, out _);
} }
/// <summary> /// <summary>
@ -393,12 +365,12 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="sender">The source of the event.</param> /// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</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 ex = e.GetException();
var dw = (FileSystemWatcher)sender; 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); DisposeWatcher(dw, true);
} }
@ -408,15 +380,11 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
/// <param name="sender">The source of the event.</param> /// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</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 try
{ {
//logger.LogDebug("Changed detected of type " + e.ChangeType + " to " + e.FullPath); ReportFileSystemChanged(e.FullPath);
var path = e.FullPath;
ReportFileSystemChanged(path);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -446,25 +414,22 @@ namespace Emby.Server.Implementations.IO
{ {
if (_fileSystem.AreEqual(i, path)) if (_fileSystem.AreEqual(i, path))
{ {
Logger.LogDebug("Ignoring change to {path}", path); Logger.LogDebug("Ignoring change to {Path}", path);
return true; return true;
} }
if (_fileSystem.ContainsSubPath(i, path)) if (_fileSystem.ContainsSubPath(i, path))
{ {
Logger.LogDebug("Ignoring change to {path}", path); Logger.LogDebug("Ignoring change to {Path}", path);
return true; return true;
} }
// Go up a level // Go up a level
var parent = Path.GetDirectoryName(i); 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; return false;
@ -487,8 +452,7 @@ namespace Emby.Server.Implementations.IO
lock (_activeRefreshers) lock (_activeRefreshers)
{ {
var refreshers = _activeRefreshers.ToList(); foreach (var refresher in _activeRefreshers)
foreach (var refresher in refreshers)
{ {
// Path is already being refreshed // Path is already being refreshed
if (_fileSystem.AreEqual(path, refresher.Path)) if (_fileSystem.AreEqual(path, refresher.Path))
@ -536,8 +500,8 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
public void Stop() public void Stop()
{ {
LibraryManager.ItemAdded -= LibraryManager_ItemAdded; LibraryManager.ItemAdded -= OnLibraryManagerItemAdded;
LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved; LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
foreach (var watcher in _fileSystemWatchers.Values.ToList()) foreach (var watcher in _fileSystemWatchers.Values.ToList())
{ {
@ -565,17 +529,20 @@ namespace Emby.Server.Implementations.IO
{ {
refresher.Dispose(); refresher.Dispose();
} }
_activeRefreshers.Clear(); _activeRefreshers.Clear();
} }
} }
private bool _disposed; private bool _disposed = false;
/// <summary> /// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
Dispose(true); Dispose(true);
GC.SuppressFinalize(this);
} }
/// <summary> /// <summary>

View File

@ -19,24 +19,17 @@ namespace Emby.Server.Implementations.IO
{ {
protected ILogger Logger; protected ILogger Logger;
private readonly bool _supportsAsyncFileStreams;
private char[] _invalidFileNameChars;
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath; private readonly string _tempPath;
private readonly bool _isEnvironmentCaseInsensitive; private readonly bool _isEnvironmentCaseInsensitive;
public ManagedFileSystem( public ManagedFileSystem(
ILoggerFactory loggerFactory, ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths) IApplicationPaths applicationPaths)
{ {
Logger = loggerFactory.CreateLogger("FileSystem"); Logger = logger;
_supportsAsyncFileStreams = true;
_tempPath = applicationPaths.TempDirectory; _tempPath = applicationPaths.TempDirectory;
SetInvalidFileNameChars(OperatingSystem.Id == OperatingSystemId.Windows);
_isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows; _isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
} }
@ -45,20 +38,6 @@ namespace Emby.Server.Implementations.IO
_shortcutHandlers.Add(handler); _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> /// <summary>
/// Determines whether the specified filename is shortcut. /// Determines whether the specified filename is shortcut.
/// </summary> /// </summary>
@ -92,20 +71,22 @@ namespace Emby.Server.Implementations.IO
var extension = Path.GetExtension(filename); var extension = Path.GetExtension(filename);
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
if (handler != null) return handler?.Resolve(filename);
{
return handler.Resolve(filename);
}
return null;
} }
public virtual string MakeAbsolutePath(string folderPath, string filePath) 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] == '/')
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') return filePath; //absolute local path {
return filePath; // absolute local path
}
// unc path // unc path
if (filePath.StartsWith("\\\\")) if (filePath.StartsWith("\\\\"))
@ -125,9 +106,7 @@ namespace Emby.Server.Implementations.IO
} }
try try
{ {
string path = System.IO.Path.Combine(folderPath, filePath); return Path.Combine(Path.GetFullPath(folderPath), filePath);
path = System.IO.Path.GetFullPath(path);
return path;
} }
catch (ArgumentException) catch (ArgumentException)
{ {
@ -166,7 +145,7 @@ namespace Emby.Server.Implementations.IO
} }
var extension = Path.GetExtension(shortcutPath); 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) if (handler != null)
{ {
@ -244,12 +223,13 @@ namespace Emby.Server.Implementations.IO
private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info) private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info)
{ {
var result = new FileSystemMetadata(); var result = new FileSystemMetadata
{
result.Exists = info.Exists; Exists = info.Exists,
result.FullName = info.FullName; FullName = info.FullName,
result.Extension = info.Extension; Extension = info.Extension,
result.Name = info.Name; Name = info.Name
};
if (result.Exists) if (result.Exists)
{ {
@ -260,8 +240,7 @@ namespace Emby.Server.Implementations.IO
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; // result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
//} //}
var fileInfo = info as FileInfo; if (info is FileInfo fileInfo)
if (fileInfo != null)
{ {
result.Length = fileInfo.Length; result.Length = fileInfo.Length;
result.DirectoryName = fileInfo.DirectoryName; result.DirectoryName = fileInfo.DirectoryName;
@ -307,7 +286,7 @@ namespace Emby.Server.Implementations.IO
{ {
var builder = new StringBuilder(filename); var builder = new StringBuilder(filename);
foreach (var c in _invalidFileNameChars) foreach (var c in Path.GetInvalidFileNameChars())
{ {
builder = builder.Replace(c, ' '); builder = builder.Replace(c, ' ');
} }
@ -394,7 +373,7 @@ namespace Emby.Server.Implementations.IO
/// <returns>FileStream.</returns> /// <returns>FileStream.</returns>
public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false) 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); return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous);
} }
@ -666,7 +645,6 @@ namespace Emby.Server.Implementations.IO
public virtual bool IsPathFile(string path) public virtual bool IsPathFile(string path)
{ {
// Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\ // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\
if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 && if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 &&
!path.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{ {
@ -674,8 +652,6 @@ namespace Emby.Server.Implementations.IO
} }
return true; return true;
//return Path.IsPathRooted(path);
} }
public virtual void DeleteFile(string path) public virtual void DeleteFile(string path)
@ -686,13 +662,14 @@ namespace Emby.Server.Implementations.IO
public virtual List<FileSystemMetadata> GetDrives() 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 // check for ready state to avoid waiting for drives to timeout
return DriveInfo.GetDrives().Where(d => d.IsReady).Select(d => new FileSystemMetadata // 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, Name = d.Name,
FullName = d.RootDirectory.FullName, FullName = d.RootDirectory.FullName,
IsDirectory = true IsDirectory = true
}).ToList(); }).ToList();
} }

View File

@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO
try try
{ {
int read; 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(); cancellationToken.ThrowIfCancellationRequested();
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
if (onStarted != null) if (onStarted != null)
{ {
@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO
if (emptyReadLimit <= 0) if (emptyReadLimit <= 0)
{ {
int read; 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(); cancellationToken.ThrowIfCancellationRequested();
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false); await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
} }
return; return;
@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO
{ {
cancellationToken.ThrowIfCancellationRequested(); 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) if (bytesRead == 0)
{ {
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO
{ {
eofCount = 0; 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) public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
{ {
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize); byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
@ -208,7 +150,7 @@ namespace Emby.Server.Implementations.IO
if (bytesRead == 0) 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) 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; totalBytesRead += bytesRead;
} }

View File

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

View File

@ -1,10 +1,10 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
@ -16,12 +16,10 @@ namespace Emby.Server.Implementations.Library
{ {
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private bool _ignoreDotPrefix;
/// <summary> /// <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> /// </summary>
public static readonly string[] IgnoreFolders = private static readonly string[] _ignoreFolders =
{ {
"metadata", "metadata",
"ps3_update", "ps3_update",
@ -42,25 +40,14 @@ namespace Emby.Server.Implementations.Library
"$RECYCLE.BIN", "$RECYCLE.BIN",
"System Volume Information", "System Volume Information",
".grab", ".grab",
// macos
".AppleDouble"
}; };
public CoreResolutionIgnoreRule(ILibraryManager libraryManager) public CoreResolutionIgnoreRule(ILibraryManager libraryManager)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_ignoreDotPrefix = Environment.OSVersion.Platform != PlatformID.Win32NT;
} }
/// <summary> /// <inheritdoc />
/// 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>
public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent) public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent)
{ {
// Don't ignore top level folders // Don't ignore top level folders
@ -72,46 +59,17 @@ namespace Emby.Server.Implementations.Library
var filename = fileInfo.Name; var filename = fileInfo.Name;
var path = fileInfo.FullName; var path = fileInfo.FullName;
// Handle mac .DS_Store // Ignore hidden files on UNIX
// https://github.com/MediaBrowser/MediaBrowser/issues/427 if (Environment.OSVersion.Platform != PlatformID.Win32NT
if (_ignoreDotPrefix) && 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) if (fileInfo.IsDirectory)
{ {
// Ignore any folders in our list // Ignore any folders in our list
if (IgnoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase)) if (_ignoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
{ {
return true; return true;
} }
@ -119,8 +77,9 @@ namespace Emby.Server.Implementations.Library
if (parent != null) if (parent != null)
{ {
// Ignore trailer folders but allow it at the collection level // Ignore trailer folders but allow it at the collection level
if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase) && if (string.Equals(filename, BaseItem.TrailerFolderName, StringComparison.OrdinalIgnoreCase)
!(parent is AggregateFolder) && !(parent is UserRootFolder)) && !(parent is AggregateFolder)
&& !(parent is UserRootFolder))
{ {
return true; return true;
} }
@ -141,22 +100,17 @@ namespace Emby.Server.Implementations.Library
if (parent != null) if (parent != null)
{ {
// Don't resolve these into audio files // Don't resolve these into audio files
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename) && _libraryManager.IsAudioFile(filename)) if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename)
&& _libraryManager.IsAudioFile(filename))
{ {
return true; return true;
} }
} }
// Ignore samples // Ignore samples
var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase) Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
.Replace("-", " ", StringComparison.OrdinalIgnoreCase)
.Replace("_", " ", StringComparison.OrdinalIgnoreCase)
.Replace("!", " ", StringComparison.OrdinalIgnoreCase);
if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1) return m.Success;
{
return true;
}
} }
return false; return false;

View File

@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.Library
public string Name => "Default"; public string Name => "Default";
public bool IsEnabled => true; public bool IsEnabled => true;
// This is dumb and an artifact of the backwards way auth providers were designed. // 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 // 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 // Only the providers that don't provide local user support use this
@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.Library
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
// This is the verson that we need to use for local users. Because reasons. // This is the verson that we need to use for local users. Because reasons.
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser) public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
{ {
@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
string hash = user.Password; string hash = user.Password;
user.Password = string.Format("$SHA1${0}", hash); user.Password = string.Format("$SHA1${0}", hash);
} }
if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) if (user.EasyPassword != null && !user.EasyPassword.Contains("$"))
{ {
string hash = user.EasyPassword; string hash = user.EasyPassword;
@ -165,6 +165,34 @@ namespace Emby.Server.Implementations.Library
return user.Password; 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) public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash)
{ {
passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword);

View File

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

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

View File

@ -2368,7 +2368,7 @@ namespace Emby.Server.Implementations.Library
public int? GetSeasonNumberFromPath(string path) 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) public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video; using Emby.Naming.Video;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -11,7 +12,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Library.Resolvers.Movies namespace Emby.Server.Implementations.Library.Resolvers.Movies
@ -27,7 +27,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// <value>The priority.</value> /// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Third; public override ResolverPriority Priority => ResolverPriority.Third;
public MultiItemResolverResult ResolveMultiple(Folder parent, public MultiItemResolverResult ResolveMultiple(
Folder parent,
List<FileSystemMetadata> files, List<FileSystemMetadata> files,
string collectionType, string collectionType,
IDirectoryService directoryService) IDirectoryService directoryService)
@ -45,7 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result; return result;
} }
private MultiItemResolverResult ResolveMultipleInternal(Folder parent, private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files, List<FileSystemMetadata> files,
string collectionType, string collectionType,
IDirectoryService directoryService) IDirectoryService directoryService)
@ -90,7 +92,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null; 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() where T : Video, new()
{ {
var files = new List<FileSystemMetadata>(); 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 // This is a hack but currently no better way to resolve a sometimes ambiguous situation
if (string.IsNullOrEmpty(collectionType)) if (string.IsNullOrEmpty(collectionType))
{ {
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase) || if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase)) || string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
{ {
return null; return null;
} }
@ -114,11 +122,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{ {
leftOver.Add(child); leftOver.Add(child);
} }
else if (IsIgnored(child.Name)) else if (!IsIgnored(child.Name))
{
}
else
{ {
files.Add(child); files.Add(child);
} }
@ -167,17 +171,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private static bool IsIgnored(string filename) private static bool IsIgnored(string filename)
{ {
// Ignore samples // Ignore samples
var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase) Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
.Replace("-", " ", StringComparison.OrdinalIgnoreCase)
.Replace("_", " ", StringComparison.OrdinalIgnoreCase)
.Replace("!", " ", StringComparison.OrdinalIgnoreCase);
if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1) return m.Success;
{
return true;
}
return false;
} }
private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file) private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)

View File

@ -14,6 +14,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
{ {
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
private readonly ILibraryManager _libraryManager; 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) public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
{ {
@ -31,10 +43,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
if (!args.IsDirectory) if (!args.IsDirectory)
{ {
// Must be an image file within a photo collection // 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) || if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
(string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos)) || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
{ {
if (IsImageFile(args.Path, _imageProcessor)) 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) internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename)
{ => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
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"
};
internal static bool IsImageFile(string path, IImageProcessor imageProcessor) 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; 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 false;
} }
return imageProcessor.SupportedInputFormats.Contains(Path.GetExtension(path).TrimStart('.'), StringComparer.Ordinal); string extension = Path.GetExtension(path).TrimStart('.');
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparer.OrdinalIgnoreCase);
} }
} }
} }

View File

@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var path = args.Path; 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 var season = new Season
{ {

View File

@ -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> /// <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) private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager)
{ {
var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions(); var seasonNumber = new SeasonPathParser().Parse(path, isTvContentType, isTvContentType).SeasonNumber;
var seasonNumber = new SeasonPathParser(namingOptions).Parse(path, isTvContentType, isTvContentType).SeasonNumber;
return seasonNumber.HasValue; return seasonNumber.HasValue;
} }

View File

@ -79,6 +79,11 @@ namespace Emby.Server.Implementations.Library
private IAuthenticationProvider[] _authenticationProviders; private IAuthenticationProvider[] _authenticationProviders;
private DefaultAuthenticationProvider _defaultAuthenticationProvider; private DefaultAuthenticationProvider _defaultAuthenticationProvider;
private InvalidAuthProvider _invalidAuthProvider;
private IPasswordResetProvider[] _passwordResetProviders;
private DefaultPasswordResetProvider _defaultPasswordResetProvider;
public UserManager( public UserManager(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IServerConfigurationManager configurationManager, IServerConfigurationManager configurationManager,
@ -102,8 +107,6 @@ namespace Emby.Server.Implementations.Library
_fileSystem = fileSystem; _fileSystem = fileSystem;
ConfigurationManager = configurationManager; ConfigurationManager = configurationManager;
_users = Array.Empty<User>(); _users = Array.Empty<User>();
DeletePinFile();
} }
public NameIdPair[] GetAuthenticationProviders() public NameIdPair[] GetAuthenticationProviders()
@ -120,11 +123,31 @@ namespace Emby.Server.Implementations.Library
.ToArray(); .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(); _authenticationProviders = authenticationProviders.ToArray();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_passwordResetProviders = passwordResetProviders.ToArray();
_defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
} }
#region UserUpdated Event #region UserUpdated Event
@ -199,9 +222,8 @@ namespace Emby.Server.Implementations.Library
public void Initialize() public void Initialize()
{ {
_users = LoadUsers(); var users = LoadUsers();
_users = users.ToArray();
var users = Users.ToList();
// If there are no local users with admin rights, make them all admins // If there are no local users with admin rights, make them all admins
if (!users.Any(i => i.Policy.IsAdministrator)) 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)); .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
var success = false; var success = false;
string updatedUsername = null;
IAuthenticationProvider authenticationProvider = null; IAuthenticationProvider authenticationProvider = null;
if (user != null) if (user != null)
{ {
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false);
authenticationProvider = authResult.Item1; authenticationProvider = authResult.Item1;
success = authResult.Item2; updatedUsername = authResult.Item2;
success = authResult.Item3;
} }
else else
{ {
// user is null // user is null
var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false);
authenticationProvider = authResult.Item1; authenticationProvider = authResult.Item1;
success = authResult.Item2; updatedUsername = authResult.Item2;
success = authResult.Item3;
if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) 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; // Search the database for the user again; the authprovider might have created it
if (hasNewUserPolicy != null) user = Users
.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase));
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
{ {
var policy = hasNewUserPolicy.GetNewUserPolicy(); var policy = hasNewUserPolicy.GetNewUserPolicy();
UpdateUserPolicy(user, policy, true); UpdateUserPolicy(user, policy, true);
@ -342,11 +374,21 @@ namespace Emby.Server.Implementations.Library
return provider.GetType().FullName; return provider.GetType().FullName;
} }
private static string GetPasswordResetProviderId(IPasswordResetProvider provider)
{
return provider.GetType().FullName;
}
private IAuthenticationProvider GetAuthenticationProvider(User user) private IAuthenticationProvider GetAuthenticationProvider(User user)
{ {
return GetAuthenticationProviders(user).First(); return GetAuthenticationProviders(user).First();
} }
private IPasswordResetProvider GetPasswordResetProvider(User user)
{
return GetPasswordResetProviders(user)[0];
}
private IAuthenticationProvider[] GetAuthenticationProviders(User user) private IAuthenticationProvider[] GetAuthenticationProviders(User user)
{ {
var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId;
@ -360,38 +402,67 @@ namespace Emby.Server.Implementations.Library
if (providers.Length == 0) 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; 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 try
{ {
var requiresResolvedUser = provider as IRequiresResolvedUser; var requiresResolvedUser = provider as IRequiresResolvedUser;
ProviderAuthenticationResult authenticationResult = null;
if (requiresResolvedUser != null) if (requiresResolvedUser != null)
{ {
await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false);
} }
else 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) catch (Exception ex)
{ {
_logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); _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; bool success = false;
IAuthenticationProvider authenticationProvider = null; IAuthenticationProvider authenticationProvider = null;
@ -404,17 +475,20 @@ namespace Emby.Server.Implementations.Library
if (password == null) if (password == null)
{ {
// legacy // 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 else
{ {
foreach (var provider in GetAuthenticationProviders(user)) 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) if (success)
{ {
authenticationProvider = provider; authenticationProvider = provider;
username = updatedUsername;
break; break;
} }
} }
@ -427,16 +501,16 @@ namespace Emby.Server.Implementations.Library
if (password == null) if (password == null)
{ {
// legacy // 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 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) 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> /// <summary>
/// Loads the users from the repository /// Loads the users from the repository
/// </summary> /// </summary>
/// <returns>IEnumerable{User}.</returns> /// <returns>IEnumerable{User}.</returns>
private User[] LoadUsers() private List<User> LoadUsers()
{ {
var users = UserRepository.RetrieveAllUsers(); var users = UserRepository.RetrieveAllUsers();
// There always has to be at least one user. // There always has to be at least one user.
if (users.Count == 0) if (users.Count != 0)
{ {
var defaultName = Environment.UserName; return users;
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.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) 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 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) ? bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ?
hasConfiguredEasyPassword : hasConfiguredEasyPassword :
@ -814,17 +882,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(user)); throw new ArgumentNullException(nameof(user));
} }
if (newPassword != null) GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordHash);
{
newPasswordHash = _defaultAuthenticationProvider.GetHashedString(user, newPassword);
}
if (string.IsNullOrWhiteSpace(newPasswordHash))
{
throw new ArgumentNullException(nameof(newPasswordHash));
}
user.EasyPassword = newPasswordHash;
UpdateUser(user); UpdateUser(user);
@ -844,159 +902,51 @@ namespace Emby.Server.Implementations.Library
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
DateCreated = DateTime.UtcNow, DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow, DateModified = DateTime.UtcNow,
UsesIdForConfigurationPath = true, UsesIdForConfigurationPath = true
//Salt = BCrypt.GenerateSalt()
}; };
} }
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) public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
{ {
DeletePinFile();
var user = string.IsNullOrWhiteSpace(enteredUsername) ? var user = string.IsNullOrWhiteSpace(enteredUsername) ?
null : null :
GetUserByName(enteredUsername); GetUserByName(enteredUsername);
var action = ForgotPasswordAction.InNetworkRequired; 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 else
{ {
if (isInNetwork) return new ForgotPasswordResult
{ {
action = ForgotPasswordAction.PinCode; Action = action,
} PinFile = string.Empty
};
var result = await CreatePasswordResetPin().ConfigureAwait(false);
pinFile = result.PinFile;
expirationDate = result.ExpirationDate;
} }
return new ForgotPasswordResult
{
Action = action,
PinFile = pinFile,
PinExpirationDate = expirationDate
};
} }
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{ {
DeletePinFile(); foreach (var provider in _passwordResetProviders)
var usersReset = new List<string>();
var valid = !string.IsNullOrWhiteSpace(_lastPin) &&
string.Equals(_lastPin, pin, StringComparison.OrdinalIgnoreCase) &&
_lastPasswordPinCreationResult != null &&
_lastPasswordPinCreationResult.ExpirationDate > DateTime.UtcNow;
if (valid)
{ {
_lastPin = null; var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
_lastPasswordPinCreationResult = null; if (result.Success)
foreach (var user in Users)
{ {
await ResetPassword(user).ConfigureAwait(false); return result;
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 new PinRedeemResult return new PinRedeemResult
{ {
Success = valid, Success = false,
UsersReset = usersReset.ToArray() 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) public UserPolicy GetUserPolicy(User user)
{ {
var path = GetPolicyFilePath(user); var path = GetPolicyFilePath(user);

View File

@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
UserAgent = "Emby/3.0", UserAgent = "Emby/3.0",
// Shouldn't matter but may cause issues // Shouldn't matter but may cause issues
EnableHttpCompression = false DecompressionMethod = CompressionMethod.None
}; };
using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false)) using (var response = await _httpClient.SendAsync(httpRequestOptions, "GET").ConfigureAwait(false))

View File

@ -261,7 +261,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public string HomePageUrl => "https://github.com/jellyfin/jellyfin"; 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); 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); var timers = await GetTimersAsync(cancellationToken).ConfigureAwait(false);

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