Merge branch 'master' into release-10.3.z

This commit is contained in:
Bond-009 2019-07-06 23:08:52 +02:00 committed by GitHub
commit 82f041d050
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
139 changed files with 5130 additions and 7634 deletions

View file

@ -16,7 +16,7 @@ jobs:
- job: main_build
displayName: Main Build
pool:
vmImage: ubuntu-16.04
vmImage: ubuntu-latest
strategy:
matrix:
release:
@ -35,12 +35,14 @@ jobs:
inputs:
command: restore
projects: '$(RestoreBuildProjects)'
enabled: false
- task: DotNetCoreCLI@2
displayName: Build
inputs:
projects: '$(RestoreBuildProjects)'
arguments: '--configuration $(BuildConfiguration)'
enabled: false
- task: DotNetCoreCLI@2
displayName: Test
@ -66,38 +68,38 @@ jobs:
# artifactName: 'jellyfin-build-$(BuildConfiguration)'
# zipAfterPublish: true
- task: PublishBuildArtifacts@1
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Naming'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll'
artifactName: 'Jellyfin.Naming'
- task: PublishBuildArtifacts@1
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Controller'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll'
artifactName: 'Jellyfin.Controller'
- task: PublishBuildArtifacts@1
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Model'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll'
artifactName: 'Jellyfin.Model'
- task: PublishBuildArtifacts@1
- task: PublishPipelineArtifact@0
displayName: 'Publish Artifact Common'
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
inputs:
PathtoPublish: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll'
artifactName: 'Jellyfin.Common'
- job: dotnet_compat
displayName: Compatibility Check
pool:
vmImage: ubuntu-16.04
vmImage: ubuntu-latest
dependsOn: main_build
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber']) # Only execute if the pullrequest numer is defined. (So not for normal CI builds)
strategy:
@ -118,45 +120,23 @@ jobs:
steps:
- checkout: none
- task: DownloadBuildArtifacts@0
displayName: Download the Reference Assembly Build Artifact
inputs:
buildType: 'specific' # Options: current, specific
project: $(System.TeamProjectId) # Required when buildType == Specific
pipeline: $(System.DefinitionId) # Required when buildType == Specific, not sure if this will take a name too
#specificBuildWithTriggering: false # Optional
buildVersionToDownload: 'latestFromBranch' # Required when buildType == Specific# Options: latest, latestFromBranch, specific
allowPartiallySucceededBuilds: false # Optional
branchName: '$(System.PullRequest.TargetBranch)' # Required when buildType == Specific && BuildVersionToDownload == LatestFromBranch
#buildId: # Required when buildType == Specific && BuildVersionToDownload == Specific
#tags: # Optional
downloadType: 'single' # Options: single, specific
artifactName: '$(NugetPackageName)'# Required when downloadType == Single
#itemPattern: '**' # Optional
downloadPath: '$(System.ArtifactsDirectory)/current-artifacts'
#parallelizationLimit: '8' # Optional
- task: CopyFiles@2
displayName: Copy Nuget Assembly to current-release folder
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadBuildArtifacts@0
- task: DownloadPipelineArtifact@2
displayName: Download the New Assembly Build Artifact
inputs:
buildType: 'current' # Options: current, specific
allowPartiallySucceededBuilds: false # Optional
downloadType: 'single' # Options: single, specific
artifactName: '$(NugetPackageName)' # Required when downloadType == Single
downloadPath: '$(System.ArtifactsDirectory)/new-artifacts'
source: 'current' # Options: current, specific
#preferTriggeringPipeline: false # Optional
#tags: # Optional
artifact: '$(NugetPackageName)' # Optional
#patterns: '**' # Optional
path: '$(System.ArtifactsDirectory)/new-artifacts'
#project: # Required when source == Specific
#pipeline: # Required when source == Specific
runVersion: 'latest' # Required when source == Specific. Options: latest, latestFromBranch, specific
#runBranch: 'refs/heads/master' # Required when source == Specific && runVersion == LatestFromBranch
#runId: # Required when source == Specific && runVersion == Specific
- task: CopyFiles@2
displayName: Copy Artifact Assembly to new-release folder
displayName: Copy New Assembly to new-release folder
inputs:
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
contents: '**/*.dll'
@ -165,6 +145,31 @@ jobs:
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadPipelineArtifact@2
displayName: Download the Reference Assembly Build Artifact
inputs:
source: 'specific' # Options: current, specific
#preferTriggeringPipeline: false # Optional
#tags: # Optional
artifact: '$(NugetPackageName)' # Optional
#patterns: '**' # Optional
path: '$(System.ArtifactsDirectory)/current-artifacts'
project: '$(System.TeamProjectId)' # Required when source == Specific
pipeline: '$(System.DefinitionId)' # Required when source == Specific
runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific
runBranch: 'refs/heads/$(System.PullRequest.TargetBranch)' # Required when source == Specific && runVersion == LatestFromBranch
#runId: # Required when source == Specific && runVersion == Specific
- task: CopyFiles@2
displayName: Copy Reference Assembly to current-release folder
inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional
contents: '**/*.dll'
targetFolder: $(System.ArtifactsDirectory)/current-release
cleanTargetFolder: true # Optional
overWrite: true # Optional
flattenFolders: true # Optional
- task: DownloadGitHubRelease@0
displayName: Download ABI compatibility check tool from GitHub
inputs:

2
.gitattributes vendored
View file

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

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]

7
.gitignore vendored
View file

@ -239,11 +239,6 @@ pip-log.txt
##########
.idea/
##########
# Visual Studio Code
##########
.vscode/
#########################
# Build artifacts
#########################
@ -268,4 +263,4 @@ jellyfin_version.ini
ci/
# Doxygen
doc/
doc/

5130
Doxyfile

File diff suppressed because it is too large Load diff

View file

@ -33,27 +33,29 @@ namespace Emby.Naming.Audio
// Normalize
// Remove whitespace
filename = filename.Replace("-", " ");
filename = filename.Replace(".", " ");
filename = filename.Replace("(", " ");
filename = filename.Replace(")", " ");
filename = filename.Replace('-', ' ');
filename = filename.Replace('.', ' ');
filename = filename.Replace('(', ' ');
filename = filename.Replace(')', ' ');
filename = Regex.Replace(filename, @"\s+", " ");
filename = filename.TrimStart();
foreach (var prefix in _options.AlbumStackingPrefixes)
{
if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) == 0)
if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) != 0)
{
var tmp = filename.Substring(prefix.Length);
continue;
}
tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty;
var tmp = filename.Substring(prefix.Length);
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
result.IsMultiPart = true;
break;
}
tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty;
if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
{
result.IsMultiPart = true;
break;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -7,33 +7,40 @@ namespace Emby.Naming.AudioBook
/// </summary>
public class AudioBookInfo
{
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
public int? Year { get; set; }
/// <summary>
/// Gets or sets the files.
/// </summary>
/// <value>The files.</value>
public List<AudioBookFileInfo> Files { get; set; }
/// <summary>
/// Gets or sets the extras.
/// </summary>
/// <value>The extras.</value>
public List<AudioBookFileInfo> Extras { get; set; }
/// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public List<AudioBookFileInfo> AlternateVersions { get; set; }
public AudioBookInfo()
{
Files = new List<AudioBookFileInfo>();
Extras = new List<AudioBookFileInfo>();
AlternateVersions = new List<AudioBookFileInfo>();
}
/// <summary>
/// Gets or sets the name.
/// </summary>
/// <value>The name.</value>
public string Name { get; set; }
/// <summary>
/// Gets or sets the year.
/// </summary>
public int? Year { get; set; }
/// <summary>
/// Gets or sets the files.
/// </summary>
/// <value>The files.</value>
public List<AudioBookFileInfo> Files { get; set; }
/// <summary>
/// Gets or sets the extras.
/// </summary>
/// <value>The extras.</value>
public List<AudioBookFileInfo> Extras { get; set; }
/// <summary>
/// Gets or sets the alternate versions.
/// </summary>
/// <value>The alternate versions.</value>
public List<AudioBookFileInfo> AlternateVersions { get; set; }
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
@ -10,7 +10,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
</ItemGroup>
<PropertyGroup>
@ -18,6 +18,18 @@
<PackageId>Jellyfin.Naming</PackageId>
<PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
</Project>

View file

@ -5,6 +5,7 @@ namespace Emby.Naming.Extensions
{
public static class StringExtensions
{
// TODO: @bond remove this when moving to netstandard2.1
public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison)
{
var sb = new StringBuilder();

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>
/// <value>The path.</value>
public string Path { get; set; }
/// <summary>
/// Gets or sets the language.
/// </summary>
/// <value>The language.</value>
public string Language { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is default.
/// </summary>
/// <value><c>true</c> if this instance is default; otherwise, <c>false</c>.</value>
public bool IsDefault { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is forced.
/// </summary>

View file

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

View file

@ -15,12 +15,12 @@ namespace Emby.Naming.TV
_options = options;
}
public EpisodePathParserResult Parse(string path, bool IsDirectory, bool? isNamed = null, bool? isOptimistic = null, bool? supportsAbsoluteNumbers = null, bool fillExtendedInfo = true)
public EpisodePathParserResult Parse(string path, bool isDirectory, bool? isNamed = null, bool? isOptimistic = null, bool? supportsAbsoluteNumbers = null, bool fillExtendedInfo = true)
{
// Added to be able to use regex patterns which require a file extension.
// There were no failed tests without this block, but to be safe, we can keep it until
// the regex which require file extensions are modified so that they don't need them.
if (IsDirectory)
if (isDirectory)
{
path += ".mp4";
}
@ -29,28 +29,20 @@ namespace Emby.Naming.TV
foreach (var expression in _options.EpisodeExpressions)
{
if (supportsAbsoluteNumbers.HasValue)
if (supportsAbsoluteNumbers.HasValue
&& expression.SupportsAbsoluteEpisodeNumbers != supportsAbsoluteNumbers.Value)
{
if (expression.SupportsAbsoluteEpisodeNumbers != supportsAbsoluteNumbers.Value)
{
continue;
}
continue;
}
if (isNamed.HasValue)
if (isNamed.HasValue && expression.IsNamed != isNamed.Value)
{
if (expression.IsNamed != isNamed.Value)
{
continue;
}
continue;
}
if (isOptimistic.HasValue)
if (isOptimistic.HasValue && expression.IsOptimistic != isOptimistic.Value)
{
if (expression.IsOptimistic != isOptimistic.Value)
{
continue;
}
continue;
}
var currentResult = Parse(path, expression);
@ -97,7 +89,8 @@ namespace Emby.Naming.TV
DateTime date;
if (expression.DateTimeFormats.Length > 0)
{
if (DateTime.TryParseExact(match.Groups[0].Value,
if (DateTime.TryParseExact(
match.Groups[0].Value,
expression.DateTimeFormats,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
@ -109,15 +102,12 @@ namespace Emby.Naming.TV
result.Success = true;
}
}
else
else if (DateTime.TryParse(match.Groups[0].Value, out date))
{
if (DateTime.TryParse(match.Groups[0].Value, out date))
{
result.Year = date.Year;
result.Month = date.Month;
result.Day = date.Day;
result.Success = true;
}
result.Year = date.Year;
result.Month = date.Month;
result.Day = date.Day;
result.Success = true;
}
// TODO: Only consider success if date successfully parsed?
@ -142,7 +132,8 @@ namespace Emby.Naming.TV
// or a 'p' or 'i' as what you would get with a pixel resolution specification.
// It avoids erroneous parsing of something like "series-s09e14-1080p.mkv" as a multi-episode from E14 to E108
int nextIndex = endingNumberGroup.Index + endingNumberGroup.Length;
if (nextIndex >= name.Length || "0123456789iIpP".IndexOf(name[nextIndex]) == -1)
if (nextIndex >= name.Length
|| "0123456789iIpP".IndexOf(name[nextIndex]) == -1)
{
if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
@ -160,6 +151,7 @@ namespace Emby.Naming.TV
{
result.SeasonNumber = num;
}
if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EpisodeNumber = num;
@ -171,8 +163,11 @@ namespace Emby.Naming.TV
// Invalidate match when the season is 200 through 1927 or above 2500
// because it is an error unless the TV show is intentionally using false season numbers.
// It avoids erroneous parsing of something like "Series Special (1920x1080).mkv" as being season 1920 episode 1080.
if (result.SeasonNumber >= 200 && result.SeasonNumber < 1928 || result.SeasonNumber > 2500)
if ((result.SeasonNumber >= 200 && result.SeasonNumber < 1928)
|| result.SeasonNumber > 2500)
{
result.Success = false;
}
result.IsByDate = expression.IsByDate;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -15,9 +15,9 @@ namespace Emby.Naming.Video
Files = new List<string>();
}
public bool ContainsFile(string file, bool IsDirectory)
public bool ContainsFile(string file, bool isDirectory)
{
if (IsDirectoryStack == IsDirectory)
if (IsDirectoryStack == isDirectory)
{
return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
}

View file

@ -15,10 +15,12 @@ namespace Emby.Naming.Video
public Format3DResult Parse(string path)
{
var delimeters = _options.VideoFlagDelimiters.ToList();
delimeters.Add(' ');
int oldLen = _options.VideoFlagDelimiters.Length;
var delimeters = new char[oldLen + 1];
_options.VideoFlagDelimiters.CopyTo(delimeters, 0);
delimeters[oldLen] = ' ';
return Parse(new FlagParser(_options).GetFlags(path, delimeters.ToArray()));
return Parse(new FlagParser(_options).GetFlags(path, delimeters));
}
internal Format3DResult Parse(string[] videoFlags)
@ -66,8 +68,10 @@ namespace Emby.Naming.Video
format = flag;
result.Tokens.Add(rule.Token);
}
break;
}
foundPrefix = string.Equals(flag, rule.PreceedingToken, StringComparison.OrdinalIgnoreCase);
}

View file

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

View file

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

View file

@ -9,24 +9,32 @@ namespace Emby.Naming.Video
{
public static StubResult ResolveFile(string path, NamingOptions options)
{
var result = new StubResult();
var extension = Path.GetExtension(path) ?? string.Empty;
if (options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
if (path == null)
{
result.IsStub = true;
return default(StubResult);
}
path = Path.GetFileNameWithoutExtension(path);
var extension = Path.GetExtension(path);
var token = (Path.GetExtension(path) ?? string.Empty).TrimStart('.');
if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
{
return default(StubResult);
}
foreach (var rule in options.StubTypes)
var result = new StubResult()
{
IsStub = true
};
path = Path.GetFileNameWithoutExtension(path);
var token = Path.GetExtension(path).TrimStart('.');
foreach (var rule in options.StubTypes)
{
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
{
result.StubType = rule.StubType;
break;
}
result.StubType = rule.StubType;
break;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -155,11 +155,6 @@ namespace Emby.Server.Implementations
/// </summary>
public event EventHandler HasPendingRestartChanged;
/// <summary>
/// Occurs when [application updated].
/// </summary>
public event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
/// <summary>
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary>
@ -237,11 +232,6 @@ namespace Emby.Server.Implementations
/// <value>The server configuration manager.</value>
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
protected virtual IResourceFileManager CreateResourceFileManager()
{
return new ResourceFileManager(HttpResultFactory, LoggerFactory, FileSystemManager);
}
/// <summary>
/// Gets or sets the user manager.
/// </summary>
@ -621,8 +611,6 @@ namespace Emby.Server.Implementations
DiscoverTypes();
SetHttpLimit();
await RegisterResources(serviceCollection).ConfigureAwait(false);
FindParts();
@ -765,10 +753,6 @@ namespace Emby.Server.Implementations
UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager);
serviceCollection.AddSingleton(UserDataManager);
UserRepository = GetUserRepository();
// This is only needed for disposal purposes. If removing this, make sure to have the manager handle disposing it
serviceCollection.AddSingleton(UserRepository);
var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager);
serviceCollection.AddSingleton<IDisplayPreferencesRepository>(displayPreferencesRepo);
@ -778,6 +762,8 @@ namespace Emby.Server.Implementations
AuthenticationRepository = GetAuthenticationRepository();
serviceCollection.AddSingleton(AuthenticationRepository);
UserRepository = GetUserRepository();
UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager);
serviceCollection.AddSingleton(UserManager);
@ -818,7 +804,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(TVSeriesManager);
DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
serviceCollection.AddSingleton(DeviceManager);
MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
@ -833,10 +818,10 @@ namespace Emby.Server.Implementations
DtoService = new DtoService(LoggerFactory, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ProviderManager, this, () => MediaSourceManager, () => LiveTvManager);
serviceCollection.AddSingleton(DtoService);
ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager);
ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager);
serviceCollection.AddSingleton(ChannelManager);
SessionManager = new SessionManager(UserDataManager, LoggerFactory, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, JsonSerializer, this, HttpClient, AuthenticationRepository, DeviceManager, MediaSourceManager);
SessionManager = new SessionManager(UserDataManager, LoggerFactory, LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, this, AuthenticationRepository, DeviceManager, MediaSourceManager);
serviceCollection.AddSingleton(SessionManager);
serviceCollection.AddSingleton<IDlnaManager>(
@ -893,7 +878,7 @@ namespace Emby.Server.Implementations
SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(LibraryManager, LoggerFactory, ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory);
serviceCollection.AddSingleton(SubtitleEncoder);
serviceCollection.AddSingleton(CreateResourceFileManager());
serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager));
displayPreferencesRepo.Initialize();
@ -918,8 +903,7 @@ namespace Emby.Server.Implementations
.Distinct();
logger.LogInformation("Arguments: {Args}", commandLineArgs);
// FIXME: @bond this logs the kernel version, not the OS version
logger.LogInformation("Operating system: {OS} {OSVersion}", OperatingSystem.Name, Environment.OSVersion.Version);
logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
@ -929,19 +913,6 @@ namespace Emby.Server.Implementations
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
}
private void SetHttpLimit()
{
try
{
// Increase the max http request limit
ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error setting http limit");
}
}
private X509Certificate2 GetCertificate(CertificateInfo info)
{
var certificateLocation = info?.Path;
@ -1035,7 +1006,6 @@ namespace Emby.Server.Implementations
Video.LiveTvManager = LiveTvManager;
Folder.UserViewManager = UserViewManager;
UserView.TVSeriesManager = TVSeriesManager;
UserView.PlaylistManager = PlaylistManager;
UserView.CollectionManager = CollectionManager;
BaseItem.MediaSourceManager = MediaSourceManager;
CollectionFolder.XmlSerializer = XmlSerializer;
@ -1441,9 +1411,9 @@ namespace Emby.Server.Implementations
public async Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken)
{
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
string wanAddress;
string wanAddress;
if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns))
{
wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false);
@ -1499,10 +1469,10 @@ namespace Emby.Server.Implementations
public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken)
{
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
string wanAddress;
if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns))
{
wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false);
@ -1572,6 +1542,7 @@ namespace Emby.Server.Implementations
{
Logger.LogError(ex, "Error getting WAN Ip address information");
}
return null;
}
@ -1618,9 +1589,9 @@ namespace Emby.Server.Implementations
}
return string.Format("http://{0}:{1}",
host,
ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture));
ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture));
}
public Task<List<IpAddressInfo>> GetLocalIpAddresses(CancellationToken cancellationToken)
{
return GetLocalIpAddressesInternal(true, 0, cancellationToken);
@ -1718,10 +1689,8 @@ namespace Emby.Server.Implementations
LogErrors = LogPing,
LogRequest = LogPing,
BufferContent = false,
CancellationToken = cancellationToken
}, HttpMethod.Post).ConfigureAwait(false))
{
using (var reader = new StreamReader(response.Content))
{
@ -1874,24 +1843,6 @@ namespace Emby.Server.Implementations
{
}
/// <summary>
/// Called when [application updated].
/// </summary>
/// <param name="package">The package.</param>
protected void OnApplicationUpdated(PackageVersionInfo package)
{
Logger.LogInformation("Application has been updated to version {0}", package.versionStr);
ApplicationUpdated?.Invoke(
this,
new GenericEventArgs<PackageVersionInfo>()
{
Argument = package
});
NotifyPendingRestart();
}
private bool _disposed = false;
/// <summary>
@ -1936,8 +1887,12 @@ namespace Emby.Server.Implementations
Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name);
}
}
UserRepository.Dispose();
}
UserRepository = null;
_disposed = true;
}
}

View file

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

View file

@ -1,183 +1,141 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.Logging;
using SQLitePCL;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
public abstract class BaseSqliteRepository : IDisposable
{
protected string DbFilePath { get; set; }
protected ReaderWriterLockSlim WriteLock;
protected ILogger Logger { get; private set; }
private bool _disposed = false;
protected BaseSqliteRepository(ILogger logger)
{
Logger = logger;
WriteLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
}
/// <summary>
/// Gets or sets the path to the DB file.
/// </summary>
/// <value>Path to the DB file.</value>
protected string DbFilePath { get; set; }
/// <summary>
/// Gets the logger.
/// </summary>
/// <value>The logger.</value>
protected ILogger Logger { get; }
/// <summary>
/// Gets the default connection flags.
/// </summary>
/// <value>The default connection flags.</value>
protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex;
/// <summary>
/// Gets the transaction mode.
/// </summary>
/// <value>The transaction mode.</value>>
protected TransactionMode TransactionMode => TransactionMode.Deferred;
/// <summary>
/// Gets the transaction mode for read-only operations.
/// </summary>
/// <value>The transaction mode.</value>
protected TransactionMode ReadTransactionMode => TransactionMode.Deferred;
internal static int ThreadSafeMode { get; set; }
/// <summary>
/// Gets the cache size.
/// </summary>
/// <value>The cache size or null.</value>
protected virtual int? CacheSize => null;
static BaseSqliteRepository()
/// <summary>
/// Gets the journal mode.
/// </summary>
/// <value>The journal mode.</value>
protected virtual string JournalMode => "WAL";
/// <summary>
/// Gets the page size.
/// </summary>
/// <value>The page size or null.</value>
protected virtual int? PageSize => null;
/// <summary>
/// Gets the temp store mode.
/// </summary>
/// <value>The temp store mode.</value>
/// <see cref="TempStoreMode"/>
protected virtual TempStoreMode TempStore => TempStoreMode.Default;
/// <summary>
/// Gets the synchronous mode.
/// </summary>
/// <value>The synchronous mode or null.</value>
/// <see cref="SynchronousMode"/>
protected virtual SynchronousMode? Synchronous => null;
/// <summary>
/// Gets or sets the write lock.
/// </summary>
/// <value>The write lock.</value>
protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1);
/// <summary>
/// Gets or sets the write connection.
/// </summary>
/// <value>The write connection.</value>
protected SQLiteDatabaseConnection WriteConnection { get; set; }
protected ManagedConnection GetConnection(bool _ = false)
{
SQLite3.EnableSharedCache = false;
int rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MEMSTATUS, 0);
//CheckOk(rc);
rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MULTITHREAD, 1);
//rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SINGLETHREAD, 1);
//rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SERIALIZED, 1);
//CheckOk(rc);
rc = raw.sqlite3_enable_shared_cache(1);
ThreadSafeMode = raw.sqlite3_threadsafe();
}
private static bool _versionLogged;
private string _defaultWal;
protected ManagedConnection _connection;
protected virtual bool EnableSingleConnection => true;
protected ManagedConnection CreateConnection(bool isReadOnly = false)
{
if (_connection != null)
WriteLock.Wait();
if (WriteConnection != null)
{
return _connection;
return new ManagedConnection(WriteConnection, WriteLock);
}
lock (WriteLock)
WriteConnection = SQLite3.Open(
DbFilePath,
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
null);
if (CacheSize.HasValue)
{
if (!_versionLogged)
{
_versionLogged = true;
Logger.LogInformation("Sqlite version: " + SQLite3.Version);
Logger.LogInformation("Sqlite compiler options: " + string.Join(",", SQLite3.CompilerOptions.ToArray()));
}
ConnectionFlags connectionFlags;
if (isReadOnly)
{
//Logger.LogInformation("Opening read connection");
//connectionFlags = ConnectionFlags.ReadOnly;
connectionFlags = ConnectionFlags.Create;
connectionFlags |= ConnectionFlags.ReadWrite;
}
else
{
//Logger.LogInformation("Opening write connection");
connectionFlags = ConnectionFlags.Create;
connectionFlags |= ConnectionFlags.ReadWrite;
}
if (EnableSingleConnection)
{
connectionFlags |= ConnectionFlags.PrivateCache;
}
else
{
connectionFlags |= ConnectionFlags.SharedCached;
}
connectionFlags |= ConnectionFlags.NoMutex;
var db = SQLite3.Open(DbFilePath, connectionFlags, null);
try
{
if (string.IsNullOrWhiteSpace(_defaultWal))
{
_defaultWal = db.Query("PRAGMA journal_mode").SelectScalarString().First();
Logger.LogInformation("Default journal_mode for {0} is {1}", DbFilePath, _defaultWal);
}
var queries = new List<string>
{
//"PRAGMA cache size=-10000"
//"PRAGMA read_uncommitted = true",
"PRAGMA synchronous=Normal"
};
if (CacheSize.HasValue)
{
queries.Add("PRAGMA cache_size=" + CacheSize.Value.ToString(CultureInfo.InvariantCulture));
}
if (EnableTempStoreMemory)
{
queries.Add("PRAGMA temp_store = memory");
}
else
{
queries.Add("PRAGMA temp_store = file");
}
foreach (var query in queries)
{
db.Execute(query);
}
}
catch
{
using (db)
{
}
throw;
}
_connection = new ManagedConnection(db, false);
return _connection;
WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
}
if (!string.IsNullOrWhiteSpace(JournalMode))
{
WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode);
}
if (Synchronous.HasValue)
{
WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
}
if (PageSize.HasValue)
{
WriteConnection.Execute("PRAGMA page_size=" + PageSize.Value);
}
WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
return new ManagedConnection(WriteConnection, WriteLock);
}
public IStatement PrepareStatement(ManagedConnection connection, string sql)
{
return connection.PrepareStatement(sql);
}
public IStatement PrepareStatementSafe(ManagedConnection connection, string sql)
{
return connection.PrepareStatement(sql);
}
=> connection.PrepareStatement(sql);
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
{
return connection.PrepareStatement(sql);
}
=> connection.PrepareStatement(sql);
public IStatement PrepareStatementSafe(IDatabaseConnection connection, string sql)
{
return connection.PrepareStatement(sql);
}
public List<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql)
{
return PrepareAllSafe(connection, sql);
}
public List<IStatement> PrepareAllSafe(IDatabaseConnection connection, IEnumerable<string> sql)
{
return sql.Select(connection.PrepareStatement).ToList();
}
public IEnumerable<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql)
=> sql.Select(connection.PrepareStatement);
protected bool TableExists(ManagedConnection connection, string name)
{
@ -199,105 +157,9 @@ namespace Emby.Server.Implementations.Data
}, ReadTransactionMode);
}
protected void RunDefaultInitialization(ManagedConnection db)
{
var queries = new List<string>
{
"PRAGMA journal_mode=WAL",
"PRAGMA page_size=4096",
"PRAGMA synchronous=Normal"
};
if (EnableTempStoreMemory)
{
queries.AddRange(new List<string>
{
"pragma default_temp_store = memory",
"pragma temp_store = memory"
});
}
else
{
queries.AddRange(new List<string>
{
"pragma temp_store = file"
});
}
// Configuration and pragmas can affect VACUUM so it needs to be last.
queries.Add("VACUUM");
db.ExecuteAll(string.Join(";", queries));
Logger.LogInformation("PRAGMA synchronous=" + db.Query("PRAGMA synchronous").SelectScalarString().First());
}
protected virtual bool EnableTempStoreMemory => false;
protected virtual int? CacheSize => null;
private bool _disposed;
protected void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name, "Object has been disposed and cannot be accessed.");
}
}
public void Dispose()
{
_disposed = true;
Dispose(true);
}
private readonly object _disposeLock = new object();
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
if (dispose)
{
DisposeConnection();
}
}
private void DisposeConnection()
{
try
{
lock (_disposeLock)
{
using (WriteLock.Write())
{
if (_connection != null)
{
using (_connection)
{
_connection.Close();
}
_connection = null;
}
CloseConnection();
}
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error disposing database");
}
}
protected virtual void CloseConnection()
{
}
protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
{
var list = new List<string>();
var columnNames = new List<string>();
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
{
@ -305,11 +167,13 @@ namespace Emby.Server.Implementations.Data
{
var name = row[1].ToString();
list.Add(name);
columnNames.Add(name);
}
}
// Configuration and pragmas can affect VACUUM so it needs to be last.
queries.Add("VACUUM");
return list;
return columnNames;
}
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
@ -321,61 +185,103 @@ namespace Emby.Server.Implementations.Data
connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL");
}
protected void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name, "Object has been disposed and cannot be accessed.");
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool dispose)
{
if (_disposed)
{
return;
}
if (dispose)
{
WriteLock.Wait();
try
{
WriteConnection.Dispose();
}
finally
{
WriteLock.Release();
}
WriteLock.Dispose();
}
WriteConnection = null;
WriteLock = null;
_disposed = true;
}
}
public static class ReaderWriterLockSlimExtensions
/// <summary>
/// The disk synchronization mode, controls how aggressively SQLite will write data
/// all the way out to physical storage.
/// </summary>
public enum SynchronousMode
{
private sealed class ReadLockToken : IDisposable
{
private ReaderWriterLockSlim _sync;
public ReadLockToken(ReaderWriterLockSlim sync)
{
_sync = sync;
sync.EnterReadLock();
}
public void Dispose()
{
if (_sync != null)
{
_sync.ExitReadLock();
_sync = null;
}
}
}
private sealed class WriteLockToken : IDisposable
{
private ReaderWriterLockSlim _sync;
public WriteLockToken(ReaderWriterLockSlim sync)
{
_sync = sync;
sync.EnterWriteLock();
}
public void Dispose()
{
if (_sync != null)
{
_sync.ExitWriteLock();
_sync = null;
}
}
}
/// <summary>
/// SQLite continues without syncing as soon as it has handed data off to the operating system
/// </summary>
Off = 0,
public static IDisposable Read(this ReaderWriterLockSlim obj)
{
//if (BaseSqliteRepository.ThreadSafeMode > 0)
//{
// return new DummyToken();
//}
return new WriteLockToken(obj);
}
/// <summary>
/// SQLite database engine will still sync at the most critical moments
/// </summary>
Normal = 1,
public static IDisposable Write(this ReaderWriterLockSlim obj)
{
//if (BaseSqliteRepository.ThreadSafeMode > 0)
//{
// return new DummyToken();
//}
return new WriteLockToken(obj);
}
/// <summary>
/// SQLite database engine will use the xSync method of the VFS
/// to ensure that all content is safely written to the disk surface prior to continuing.
/// </summary>
Full = 2,
/// <summary>
/// EXTRA synchronous is like FULL with the addition that the directory containing a rollback journal
/// is synced after that journal is unlinked to commit a transaction in DELETE mode.
/// </summary>
Extra = 3
}
/// <summary>
/// Storage mode used by temporary database files.
/// </summary>
public enum TempStoreMode
{
/// <summary>
/// The compile-time C preprocessor macro SQLITE_TEMP_STORE
/// is used to determine where temporary tables and indices are stored.
/// </summary>
Default = 0,
/// <summary>
/// Temporary tables and indices are stored in a file.
/// </summary>
File = 1,
/// <summary>
/// Temporary tables and indices are kept in as if they were pure in-memory databases memory.
/// </summary>
Memory = 2
}
}

View file

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

View file

@ -18,13 +18,13 @@ namespace Emby.Server.Implementations.Data
/// </summary>
public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
{
protected IFileSystem FileSystem { get; private set; }
private readonly IFileSystem _fileSystem;
public SqliteDisplayPreferencesRepository(ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IFileSystem fileSystem)
: base(loggerFactory.CreateLogger(nameof(SqliteDisplayPreferencesRepository)))
{
_jsonSerializer = jsonSerializer;
FileSystem = fileSystem;
_fileSystem = fileSystem;
DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
}
@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Data
{
Logger.LogError(ex, "Error loading database file. Will reset and retry.");
FileSystem.DeleteFile(DbFilePath);
_fileSystem.DeleteFile(DbFilePath);
InitializeInternal();
}
@ -61,10 +61,8 @@ namespace Emby.Server.Implementations.Data
/// <returns>Task.</returns>
private void InitializeInternal()
{
using (var connection = CreateConnection())
using (var connection = GetConnection())
{
RunDefaultInitialization(connection);
string[] queries = {
"create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
@ -98,15 +96,12 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested();
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
{
SaveDisplayPreferences(displayPreferences, userId, client, db);
}, TransactionMode);
}
SaveDisplayPreferences(displayPreferences, userId, client, db);
}, TransactionMode);
}
}
@ -142,18 +137,15 @@ namespace Emby.Server.Implementations.Data
cancellationToken.ThrowIfCancellationRequested();
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
foreach (var displayPreference in displayPreferences)
{
foreach (var displayPreference in displayPreferences)
{
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
}
}, TransactionMode);
}
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
}
}, TransactionMode);
}
}
@ -174,27 +166,24 @@ namespace Emby.Server.Implementations.Data
var guidId = displayPreferencesId.GetMD5();
using (WriteLock.Read())
using (var connection = GetConnection(true))
{
using (var connection = CreateConnection(true))
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
{
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
{
statement.TryBind("@id", guidId.ToGuidBlob());
statement.TryBind("@userId", userId.ToGuidBlob());
statement.TryBind("@client", client);
statement.TryBind("@id", guidId.ToGuidBlob());
statement.TryBind("@userId", userId.ToGuidBlob());
statement.TryBind("@client", client);
foreach (var row in statement.ExecuteQuery())
{
return Get(row);
}
foreach (var row in statement.ExecuteQuery())
{
return Get(row);
}
return new DisplayPreferences
{
Id = guidId.ToString("N")
};
}
return new DisplayPreferences
{
Id = guidId.ToString("N")
};
}
}
@ -208,18 +197,15 @@ namespace Emby.Server.Implementations.Data
{
var list = new List<DisplayPreferences>();
using (WriteLock.Read())
using (var connection = GetConnection(true))
{
using (var connection = CreateConnection(true))
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
{
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
{
statement.TryBind("@userId", userId.ToGuidBlob());
statement.TryBind("@userId", userId.ToGuidBlob());
foreach (var row in statement.ExecuteQuery())
{
list.Add(Get(row));
}
foreach (var row in statement.ExecuteQuery())
{
list.Add(Get(row));
}
}
}

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);

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.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@ -33,14 +32,14 @@ namespace Emby.Server.Implementations.Data
/// Opens the connection to the database
/// </summary>
/// <returns>Task.</returns>
public void Initialize(ReaderWriterLockSlim writeLock, ManagedConnection managedConnection, IUserManager userManager)
public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
{
_connection = managedConnection;
WriteLock.Dispose();
WriteLock = writeLock;
WriteLock = dbLock;
WriteConnection?.Dispose();
WriteConnection = dbConnection;
using (var connection = CreateConnection())
using (var connection = GetConnection())
{
var userDatasTableExists = TableExists(connection, "UserDatas");
var userDataTableExists = TableExists(connection, "userdata");
@ -129,8 +128,6 @@ namespace Emby.Server.Implementations.Data
return list;
}
protected override bool EnableTempStoreMemory => true;
/// <summary>
/// Saves the user data.
/// </summary>
@ -178,15 +175,12 @@ namespace Emby.Server.Implementations.Data
{
cancellationToken.ThrowIfCancellationRequested();
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
{
SaveUserData(db, internalUserId, key, userData);
}, TransactionMode);
}
SaveUserData(db, internalUserId, key, userData);
}, TransactionMode);
}
}
@ -249,18 +243,15 @@ namespace Emby.Server.Implementations.Data
{
cancellationToken.ThrowIfCancellationRequested();
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
foreach (var userItemData in userDataList)
{
foreach (var userItemData in userDataList)
{
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
}
}, TransactionMode);
}
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
}
}, TransactionMode);
}
}
@ -281,28 +272,26 @@ namespace Emby.Server.Implementations.Data
{
throw new ArgumentNullException(nameof(internalUserId));
}
if (string.IsNullOrEmpty(key))
{
throw new ArgumentNullException(nameof(key));
}
using (WriteLock.Read())
using (var connection = GetConnection(true))
{
using (var connection = CreateConnection(true))
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
statement.TryBind("@UserId", internalUserId);
statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery())
{
statement.TryBind("@UserId", internalUserId);
statement.TryBind("@Key", key);
foreach (var row in statement.ExecuteQuery())
{
return ReadRow(row);
}
return ReadRow(row);
}
return null;
}
return null;
}
}
@ -335,18 +324,15 @@ namespace Emby.Server.Implementations.Data
var list = new List<UserItemData>();
using (WriteLock.Read())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
{
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
{
statement.TryBind("@UserId", internalUserId);
statement.TryBind("@UserId", internalUserId);
foreach (var row in statement.ExecuteQuery())
{
list.Add(ReadRow(row));
}
foreach (var row in statement.ExecuteQuery())
{
list.Add(ReadRow(row));
}
}
}
@ -392,15 +378,5 @@ namespace Emby.Server.Implementations.Data
return userData;
}
protected override void Dispose(bool dispose)
{
// handled by library database
}
protected override void CloseConnection()
{
// handled by library database
}
}
}

View file

@ -40,10 +40,8 @@ namespace Emby.Server.Implementations.Data
/// <returns>Task.</returns>
public void Initialize()
{
using (var connection = CreateConnection())
using (var connection = GetConnection())
{
RunDefaultInitialization(connection);
var localUsersTableExists = TableExists(connection, "LocalUsersv2");
connection.RunQueries(new[] {
@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Data
TryMigrateToLocalUsersTable(connection);
}
RemoveEmptyPasswordHashes();
RemoveEmptyPasswordHashes(connection);
}
}
@ -75,9 +73,9 @@ namespace Emby.Server.Implementations.Data
}
}
private void RemoveEmptyPasswordHashes()
private void RemoveEmptyPasswordHashes(ManagedConnection connection)
{
foreach (var user in RetrieveAllUsers())
foreach (var user in RetrieveAllUsers(connection))
{
// If the user password is the sha1 hash of the empty string, remove it
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
@ -89,22 +87,16 @@ namespace Emby.Server.Implementations.Data
user.Password = null;
var serialized = _jsonSerializer.SerializeToBytes(user);
using (WriteLock.Write())
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
{
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
{
statement.TryBind("@InternalId", user.InternalId);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
}, TransactionMode);
}
statement.TryBind("@InternalId", user.InternalId);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
}, TransactionMode);
}
}
/// <summary>
@ -119,31 +111,28 @@ namespace Emby.Server.Implementations.Data
var serialized = _jsonSerializer.SerializeToBytes(user);
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
{
using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
{
statement.TryBind("@guid", user.Id.ToGuidBlob());
statement.TryBind("@data", serialized);
statement.TryBind("@guid", user.Id.ToGuidBlob());
statement.TryBind("@data", serialized);
statement.MoveNext();
}
statement.MoveNext();
}
var createdUser = GetUser(user.Id, false);
var createdUser = GetUser(user.Id, connection);
if (createdUser == null)
{
throw new ApplicationException("created user should never be null");
}
if (createdUser == null)
{
throw new ApplicationException("created user should never be null");
}
user.InternalId = createdUser.InternalId;
user.InternalId = createdUser.InternalId;
}, TransactionMode);
}
}, TransactionMode);
}
}
@ -156,39 +145,30 @@ namespace Emby.Server.Implementations.Data
var serialized = _jsonSerializer.SerializeToBytes(user);
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
{
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
{
statement.TryBind("@InternalId", user.InternalId);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
statement.TryBind("@InternalId", user.InternalId);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
}, TransactionMode);
}
}, TransactionMode);
}
}
private User GetUser(Guid guid, bool openLock)
private User GetUser(Guid guid, ManagedConnection connection)
{
using (openLock ? WriteLock.Read() : null)
using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
{
using (var connection = CreateConnection(true))
{
using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
{
statement.TryBind("@guid", guid);
statement.TryBind("@guid", guid);
foreach (var row in statement.ExecuteQuery())
{
return GetUser(row);
}
}
foreach (var row in statement.ExecuteQuery())
{
return GetUser(row);
}
}
@ -216,20 +196,22 @@ namespace Emby.Server.Implementations.Data
/// <returns>IEnumerable{User}.</returns>
public List<User> RetrieveAllUsers()
{
var list = new List<User>();
using (WriteLock.Read())
using (var connection = GetConnection(true))
{
using (var connection = CreateConnection(true))
{
foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
{
list.Add(GetUser(row));
}
}
return new List<User>(RetrieveAllUsers(connection));
}
}
return list;
/// <summary>
/// Retrieve all users from the database
/// </summary>
/// <returns>IEnumerable{User}.</returns>
private IEnumerable<User> RetrieveAllUsers(ManagedConnection connection)
{
foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
{
yield return GetUser(row);
}
}
/// <summary>
@ -245,19 +227,16 @@ namespace Emby.Server.Implementations.Data
throw new ArgumentNullException(nameof(user));
}
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id"))
{
using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id"))
{
statement.TryBind("@id", user.InternalId);
statement.MoveNext();
}
}, TransactionMode);
}
statement.TryBind("@id", user.InternalId);
statement.MoveNext();
}
}, TransactionMode);
}
}
}

View file

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

View file

@ -31,10 +31,9 @@
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.4.0" />
<PackageReference Include="sharpcompress" Version="0.22.0" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.5.0" />
<PackageReference Include="sharpcompress" Version="0.23.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="1.0.0" />
<PackageReference Include="UTF.Unknown" Version="1.0.0-beta1" />
</ItemGroup>
<ItemGroup>
@ -52,8 +51,8 @@
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
</ItemGroup>

View file

@ -4,6 +4,8 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Sockets;
@ -11,7 +10,6 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Net;
using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.SocketSharp;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
@ -127,12 +125,12 @@ namespace Emby.Server.Implementations.HttpServer
private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
{
var attributes = requestDtoType.GetTypeInfo().GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
var serviceType = GetServiceTypeByRequest(requestDtoType);
if (serviceType != null)
{
attributes.AddRange(serviceType.GetTypeInfo().GetCustomAttributes(true).OfType<IHasRequestFilter>());
attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
}
attributes.Sort((x, y) => x.Priority - y.Priority);
@ -154,7 +152,7 @@ namespace Emby.Server.Implementations.HttpServer
QueryString = e.QueryString ?? new QueryCollection()
};
connection.Closed += Connection_Closed;
connection.Closed += OnConnectionClosed;
lock (_webSocketConnections)
{
@ -164,7 +162,7 @@ namespace Emby.Server.Implementations.HttpServer
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
}
private void Connection_Closed(object sender, EventArgs e)
private void OnConnectionClosed(object sender, EventArgs e)
{
lock (_webSocketConnections)
{
@ -323,14 +321,14 @@ namespace Emby.Server.Implementations.HttpServer
private static string NormalizeConfiguredLocalAddress(string address)
{
var index = address.Trim('/').IndexOf('/');
var add = address.AsSpan().Trim('/');
int index = add.IndexOf('/');
if (index != -1)
{
address = address.Substring(index + 1);
add = add.Slice(index + 1);
}
return address.Trim('/');
return add.TrimStart('/').ToString();
}
private bool ValidateHost(string host)
@ -400,8 +398,8 @@ namespace Emby.Server.Implementations.HttpServer
if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
{
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 ||
urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
|| urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
@ -573,7 +571,7 @@ namespace Emby.Server.Implementations.HttpServer
if (handler != null)
{
await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, httpReq.OperationName, cancellationToken).ConfigureAwait(false);
await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, cancellationToken).ConfigureAwait(false);
}
else
{
@ -614,21 +612,11 @@ namespace Emby.Server.Implementations.HttpServer
{
var pathInfo = httpReq.PathInfo;
var pathParts = pathInfo.TrimStart('/').Split('/');
if (pathParts.Length == 0)
{
Logger.LogError("Path parts empty for PathInfo: {PathInfo}, Url: {RawUrl}", pathInfo, httpReq.RawUrl);
return null;
}
var restPath = ServiceHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out string contentType);
pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
if (restPath != null)
{
return new ServiceHandler
{
RestPath = restPath,
ResponseContentType = contentType
};
return new ServiceHandler(restPath, contentType);
}
Logger.LogError("Could not find handler for {PathInfo}", pathInfo);
@ -657,11 +645,6 @@ namespace Emby.Server.Implementations.HttpServer
}
else
{
// TODO what is this?
var httpsUrl = url
.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase)
.Replace(":" + _config.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture), ":" + _config.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase);
RedirectToUrl(httpRes, url);
}
}
@ -686,10 +669,7 @@ namespace Emby.Server.Implementations.HttpServer
UrlPrefixes = urlPrefixes.ToArray();
ServiceController = new ServiceController();
Logger.LogInformation("Calling ServiceStack AppHost.Init");
var types = services.Select(r => r.GetType()).ToArray();
var types = services.Select(r => r.GetType());
ServiceController.Init(this, types);
ResponseFilters = new Action<IRequest, IResponse, object>[]
@ -825,19 +805,15 @@ namespace Emby.Server.Implementations.HttpServer
Logger.LogDebug("Websocket message received: {0}", result.MessageType);
var tasks = _webSocketListeners.Select(i => Task.Run(async () =>
IEnumerable<Task> GetTasks()
{
try
foreach (var x in _webSocketListeners)
{
await i.ProcessMessage(result).ConfigureAwait(false);
yield return x.ProcessMessageAsync(result);
}
catch (Exception ex)
{
Logger.LogError(ex, "{0} failed processing WebSocket message {1}", i.GetType().Name, result.MessageType ?? string.Empty);
}
}));
}
return Task.WhenAll(tasks);
return Task.WhenAll(GetTasks());
}
public void Dispose()

View file

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

View file

@ -9,9 +9,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
using Microsoft.Extensions.Logging;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
namespace Emby.Server.Implementations.IO
{
@ -21,6 +19,7 @@ namespace Emby.Server.Implementations.IO
/// The file system watchers
/// </summary>
private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// The affected paths
/// </summary>
@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.IO
throw new ArgumentNullException(nameof(path));
}
// This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to.
// This is an arbitrary amount of time, but delay it because file system writes often trigger events long after the file was actually written to.
// Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
// But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
await Task.Delay(45000).ConfigureAwait(false);
@ -162,10 +161,10 @@ namespace Emby.Server.Implementations.IO
public void Start()
{
LibraryManager.ItemAdded += LibraryManager_ItemAdded;
LibraryManager.ItemRemoved += LibraryManager_ItemRemoved;
LibraryManager.ItemAdded += OnLibraryManagerItemAdded;
LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
var pathsToWatch = new List<string> { };
var pathsToWatch = new List<string>();
var paths = LibraryManager
.RootFolder
@ -204,7 +203,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
@ -217,7 +216,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
@ -244,7 +243,7 @@ namespace Emby.Server.Implementations.IO
return lst.Any(str =>
{
//this should be a little quicker than examining each actual parent folder...
// this should be a little quicker than examining each actual parent folder...
var compare = str.TrimEnd(Path.DirectorySeparatorChar);
return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
@ -260,19 +259,10 @@ namespace Emby.Server.Implementations.IO
if (!Directory.Exists(path))
{
// Seeing a crash in the mono runtime due to an exception being thrown on a different thread
Logger.LogInformation("Skipping realtime monitor for {0} because the path does not exist", path);
Logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
return;
}
if (OperatingSystem.Id != OperatingSystemId.Windows)
{
if (path.StartsWith("\\\\", StringComparison.OrdinalIgnoreCase) || path.StartsWith("smb://", StringComparison.OrdinalIgnoreCase))
{
// not supported
return;
}
}
// Already being watched
if (_fileSystemWatchers.ContainsKey(path))
{
@ -286,23 +276,21 @@ namespace Emby.Server.Implementations.IO
{
var newWatcher = new FileSystemWatcher(path, "*")
{
IncludeSubdirectories = true
IncludeSubdirectories = true,
InternalBufferSize = 65536,
NotifyFilter = NotifyFilters.CreationTime |
NotifyFilters.DirectoryName |
NotifyFilters.FileName |
NotifyFilters.LastWrite |
NotifyFilters.Size |
NotifyFilters.Attributes
};
newWatcher.InternalBufferSize = 65536;
newWatcher.NotifyFilter = NotifyFilters.CreationTime |
NotifyFilters.DirectoryName |
NotifyFilters.FileName |
NotifyFilters.LastWrite |
NotifyFilters.Size |
NotifyFilters.Attributes;
newWatcher.Created += watcher_Changed;
newWatcher.Deleted += watcher_Changed;
newWatcher.Renamed += watcher_Changed;
newWatcher.Changed += watcher_Changed;
newWatcher.Error += watcher_Error;
newWatcher.Created += OnWatcherChanged;
newWatcher.Deleted += OnWatcherChanged;
newWatcher.Renamed += OnWatcherChanged;
newWatcher.Changed += OnWatcherChanged;
newWatcher.Error += OnWatcherError;
if (_fileSystemWatchers.TryAdd(path, newWatcher))
{
@ -343,32 +331,16 @@ namespace Emby.Server.Implementations.IO
{
using (watcher)
{
Logger.LogInformation("Stopping directory watching for path {path}", watcher.Path);
Logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
watcher.Created -= watcher_Changed;
watcher.Deleted -= watcher_Changed;
watcher.Renamed -= watcher_Changed;
watcher.Changed -= watcher_Changed;
watcher.Error -= watcher_Error;
watcher.Created -= OnWatcherChanged;
watcher.Deleted -= OnWatcherChanged;
watcher.Renamed -= OnWatcherChanged;
watcher.Changed -= OnWatcherChanged;
watcher.Error -= OnWatcherError;
try
{
watcher.EnableRaisingEvents = false;
}
catch (InvalidOperationException)
{
// Seeing this under mono on linux sometimes
// Collection was modified; enumeration operation may not execute.
}
watcher.EnableRaisingEvents = false;
}
}
catch (NotImplementedException)
{
// the dispose method on FileSystemWatcher is sometimes throwing NotImplementedException on Xamarin Android
}
catch
{
}
finally
{
@ -385,7 +357,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="watcher">The watcher.</param>
private void RemoveWatcherFromList(FileSystemWatcher watcher)
{
_fileSystemWatchers.TryRemove(watcher.Path, out var removed);
_fileSystemWatchers.TryRemove(watcher.Path, out _);
}
/// <summary>
@ -393,12 +365,12 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
void watcher_Error(object sender, ErrorEventArgs e)
private void OnWatcherError(object sender, ErrorEventArgs e)
{
var ex = e.GetException();
var dw = (FileSystemWatcher)sender;
Logger.LogError(ex, "Error in Directory watcher for: {path}", dw.Path);
Logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
DisposeWatcher(dw, true);
}
@ -408,15 +380,11 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
void watcher_Changed(object sender, FileSystemEventArgs e)
private void OnWatcherChanged(object sender, FileSystemEventArgs e)
{
try
{
//logger.LogDebug("Changed detected of type " + e.ChangeType + " to " + e.FullPath);
var path = e.FullPath;
ReportFileSystemChanged(path);
ReportFileSystemChanged(e.FullPath);
}
catch (Exception ex)
{
@ -446,25 +414,22 @@ namespace Emby.Server.Implementations.IO
{
if (_fileSystem.AreEqual(i, path))
{
Logger.LogDebug("Ignoring change to {path}", path);
Logger.LogDebug("Ignoring change to {Path}", path);
return true;
}
if (_fileSystem.ContainsSubPath(i, path))
{
Logger.LogDebug("Ignoring change to {path}", path);
Logger.LogDebug("Ignoring change to {Path}", path);
return true;
}
// Go up a level
var parent = Path.GetDirectoryName(i);
if (!string.IsNullOrEmpty(parent))
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
{
if (_fileSystem.AreEqual(parent, path))
{
Logger.LogDebug("Ignoring change to {path}", path);
return true;
}
Logger.LogDebug("Ignoring change to {Path}", path);
return true;
}
return false;
@ -487,8 +452,7 @@ namespace Emby.Server.Implementations.IO
lock (_activeRefreshers)
{
var refreshers = _activeRefreshers.ToList();
foreach (var refresher in refreshers)
foreach (var refresher in _activeRefreshers)
{
// Path is already being refreshed
if (_fileSystem.AreEqual(path, refresher.Path))
@ -536,8 +500,8 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public void Stop()
{
LibraryManager.ItemAdded -= LibraryManager_ItemAdded;
LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved;
LibraryManager.ItemAdded -= OnLibraryManagerItemAdded;
LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
foreach (var watcher in _fileSystemWatchers.Values.ToList())
{
@ -565,17 +529,20 @@ namespace Emby.Server.Implementations.IO
{
refresher.Dispose();
}
_activeRefreshers.Clear();
}
}
private bool _disposed;
private bool _disposed = false;
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>

View file

@ -19,24 +19,17 @@ namespace Emby.Server.Implementations.IO
{
protected ILogger Logger;
private readonly bool _supportsAsyncFileStreams;
private char[] _invalidFileNameChars;
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath;
private readonly bool _isEnvironmentCaseInsensitive;
public ManagedFileSystem(
ILoggerFactory loggerFactory,
ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths)
{
Logger = loggerFactory.CreateLogger("FileSystem");
_supportsAsyncFileStreams = true;
Logger = logger;
_tempPath = applicationPaths.TempDirectory;
SetInvalidFileNameChars(OperatingSystem.Id == OperatingSystemId.Windows);
_isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
}
@ -45,20 +38,6 @@ namespace Emby.Server.Implementations.IO
_shortcutHandlers.Add(handler);
}
protected void SetInvalidFileNameChars(bool enableManagedInvalidFileNameChars)
{
if (enableManagedInvalidFileNameChars)
{
_invalidFileNameChars = Path.GetInvalidFileNameChars();
}
else
{
// Be consistent across platforms because the windows server will fail to query network shares that don't follow windows conventions
// https://referencesource.microsoft.com/#mscorlib/system/io/path.cs
_invalidFileNameChars = new char[] { '\"', '<', '>', '|', '\0', (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, (char)31, ':', '*', '?', '\\', '/' };
}
}
/// <summary>
/// Determines whether the specified filename is shortcut.
/// </summary>
@ -92,20 +71,22 @@ namespace Emby.Server.Implementations.IO
var extension = Path.GetExtension(filename);
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
if (handler != null)
{
return handler.Resolve(filename);
}
return null;
return handler?.Resolve(filename);
}
public virtual string MakeAbsolutePath(string folderPath, string filePath)
{
if (string.IsNullOrWhiteSpace(filePath)) return filePath;
if (string.IsNullOrWhiteSpace(filePath)
// stream
|| filePath.Contains("://"))
{
return filePath;
}
if (filePath.Contains(@"://")) return filePath; //stream
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/') return filePath; //absolute local path
if (filePath.Length > 3 && filePath[1] == ':' && filePath[2] == '/')
{
return filePath; // absolute local path
}
// unc path
if (filePath.StartsWith("\\\\"))
@ -125,9 +106,7 @@ namespace Emby.Server.Implementations.IO
}
try
{
string path = System.IO.Path.Combine(folderPath, filePath);
path = System.IO.Path.GetFullPath(path);
return path;
return Path.Combine(Path.GetFullPath(folderPath), filePath);
}
catch (ArgumentException)
{
@ -166,7 +145,7 @@ namespace Emby.Server.Implementations.IO
}
var extension = Path.GetExtension(shortcutPath);
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
if (handler != null)
{
@ -244,12 +223,13 @@ namespace Emby.Server.Implementations.IO
private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info)
{
var result = new FileSystemMetadata();
result.Exists = info.Exists;
result.FullName = info.FullName;
result.Extension = info.Extension;
result.Name = info.Name;
var result = new FileSystemMetadata
{
Exists = info.Exists,
FullName = info.FullName,
Extension = info.Extension,
Name = info.Name
};
if (result.Exists)
{
@ -260,8 +240,7 @@ namespace Emby.Server.Implementations.IO
// result.IsHidden = (info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
//}
var fileInfo = info as FileInfo;
if (fileInfo != null)
if (info is FileInfo fileInfo)
{
result.Length = fileInfo.Length;
result.DirectoryName = fileInfo.DirectoryName;
@ -307,7 +286,7 @@ namespace Emby.Server.Implementations.IO
{
var builder = new StringBuilder(filename);
foreach (var c in _invalidFileNameChars)
foreach (var c in Path.GetInvalidFileNameChars())
{
builder = builder.Replace(c, ' ');
}
@ -394,7 +373,7 @@ namespace Emby.Server.Implementations.IO
/// <returns>FileStream.</returns>
public virtual Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false)
{
if (_supportsAsyncFileStreams && isAsync)
if (isAsync)
{
return GetFileStream(path, mode, access, share, FileOpenOptions.Asynchronous);
}
@ -666,7 +645,6 @@ namespace Emby.Server.Implementations.IO
public virtual bool IsPathFile(string path)
{
// Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\
if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 &&
!path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
@ -674,8 +652,6 @@ namespace Emby.Server.Implementations.IO
}
return true;
//return Path.IsPathRooted(path);
}
public virtual void DeleteFile(string path)
@ -686,13 +662,14 @@ namespace Emby.Server.Implementations.IO
public virtual List<FileSystemMetadata> GetDrives()
{
// Only include drives in the ready state or this method could end up being very slow, waiting for drives to timeout
return DriveInfo.GetDrives().Where(d => d.IsReady).Select(d => new FileSystemMetadata
// check for ready state to avoid waiting for drives to timeout
// some drives on linux have no actual size or are used for other purposes
return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram)
.Select(d => new FileSystemMetadata
{
Name = d.Name,
FullName = d.RootDirectory.FullName,
IsDirectory = true
}).ToList();
}

View file

@ -17,11 +17,11 @@ namespace Emby.Server.Implementations.IO
try
{
int read;
while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
cancellationToken.ThrowIfCancellationRequested();
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false);
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
if (onStarted != null)
{
@ -44,11 +44,11 @@ namespace Emby.Server.Implementations.IO
if (emptyReadLimit <= 0)
{
int read;
while ((read = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) != 0)
while ((read = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
cancellationToken.ThrowIfCancellationRequested();
await destination.WriteAsync(buffer, 0, read).ConfigureAwait(false);
await destination.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
}
return;
@ -60,7 +60,7 @@ namespace Emby.Server.Implementations.IO
{
cancellationToken.ThrowIfCancellationRequested();
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
var bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
if (bytesRead == 0)
{
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.IO
{
eofCount = 0;
await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
}
}
}
@ -109,64 +109,6 @@ namespace Emby.Server.Implementations.IO
}
}
public async Task<int> CopyToAsyncWithSyncRead(Stream source, Stream destination, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
try
{
int bytesRead;
int totalBytesRead = 0;
while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0)
{
var bytesToWrite = bytesRead;
if (bytesToWrite > 0)
{
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
}
}
return totalBytesRead;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public async Task CopyToAsyncWithSyncRead(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
try
{
int bytesRead;
while ((bytesRead = source.Read(buffer, 0, buffer.Length)) != 0)
{
var bytesToWrite = Math.Min(bytesRead, copyLength);
if (bytesToWrite > 0)
{
await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
}
copyLength -= bytesToWrite;
if (copyLength <= 0)
{
break;
}
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize);
@ -208,7 +150,7 @@ namespace Emby.Server.Implementations.IO
if (bytesRead == 0)
{
await Task.Delay(100).ConfigureAwait(false);
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
}
}
}
@ -225,7 +167,7 @@ namespace Emby.Server.Implementations.IO
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(buffer, 0, bytesRead).ConfigureAwait(false);
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
}

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,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
@ -148,15 +149,9 @@ namespace Emby.Server.Implementations.Library
}
// Ignore samples
var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase)
.Replace("-", " ", StringComparison.OrdinalIgnoreCase)
.Replace("_", " ", StringComparison.OrdinalIgnoreCase)
.Replace("!", " ", StringComparison.OrdinalIgnoreCase);
Match m = Regex.Match(filename,@"\bsample\b",RegexOptions.IgnoreCase);
if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
return m.Success;
}
return false;

View file

@ -2368,7 +2368,7 @@ namespace Emby.Server.Implementations.Library
public int? GetSeasonNumberFromPath(string path)
{
return new SeasonPathParser(GetNamingOptions()).Parse(path, true, true).SeasonNumber;
return new SeasonPathParser().Parse(path, true, true).SeasonNumber;
}
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Video;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@ -167,17 +168,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private static bool IsIgnored(string filename)
{
// Ignore samples
var sampleFilename = " " + filename.Replace(".", " ", StringComparison.OrdinalIgnoreCase)
.Replace("-", " ", StringComparison.OrdinalIgnoreCase)
.Replace("_", " ", StringComparison.OrdinalIgnoreCase)
.Replace("!", " ", StringComparison.OrdinalIgnoreCase);
Match m = Regex.Match(filename,@"\bsample\b",RegexOptions.IgnoreCase);
if (sampleFilename.IndexOf(" sample ", StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
return false;
return m.Success;
}
private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)

View file

@ -14,6 +14,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
private readonly IImageProcessor _imageProcessor;
private readonly ILibraryManager _libraryManager;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"folder",
"thumb",
"landscape",
"fanart",
"backdrop",
"poster",
"cover",
"logo",
"default"
};
public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
{
@ -31,10 +43,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
if (!args.IsDirectory)
{
// Must be an image file within a photo collection
var collectionType = args.GetCollectionType();
var collectionType = args.CollectionType;
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) ||
(string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
|| (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
{
if (IsImageFile(args.Path, _imageProcessor))
{
@ -74,43 +86,29 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, LibraryOptions libraryOptions, string file, string imageFilename)
{
if (imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
private static readonly HashSet<string> IgnoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"folder",
"thumb",
"landscape",
"fanart",
"backdrop",
"poster",
"cover",
"logo",
"default"
};
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{
var filename = Path.GetFileNameWithoutExtension(path) ?? string.Empty;
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (IgnoreFiles.Contains(filename))
var filename = Path.GetFileNameWithoutExtension(path);
if (_ignoreFiles.Contains(filename))
{
return false;
}
if (IgnoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
{
return false;
}
return imageProcessor.SupportedInputFormats.Contains(Path.GetExtension(path).TrimStart('.'), StringComparer.Ordinal);
string extension = Path.GetExtension(path).TrimStart('.');
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparer.OrdinalIgnoreCase);
}
}
}

View file

@ -52,7 +52,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var path = args.Path;
var seasonParserResult = new SeasonPathParser(namingOptions).Parse(path, true, true);
var seasonParserResult = new SeasonPathParser().Parse(path, true, true);
var season = new Season
{

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

View file

@ -222,9 +222,8 @@ namespace Emby.Server.Implementations.Library
public void Initialize()
{
_users = LoadUsers();
var users = Users.ToList();
var users = LoadUsers();
_users = users.ToArray();
// If there are no local users with admin rights, make them all admins
if (!users.Any(i => i.Policy.IsAdministrator))
@ -555,35 +554,36 @@ namespace Emby.Server.Implementations.Library
/// Loads the users from the repository
/// </summary>
/// <returns>IEnumerable{User}.</returns>
private User[] LoadUsers()
private List<User> LoadUsers()
{
var users = UserRepository.RetrieveAllUsers();
// There always has to be at least one user.
if (users.Count == 0)
if (users.Count != 0)
{
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName))
{
defaultName = "MyJellyfinUser";
}
var name = MakeValidUsername(defaultName);
var user = InstantiateNewUser(name);
user.DateLastSaved = DateTime.UtcNow;
UserRepository.CreateUser(user);
users.Add(user);
user.Policy.IsAdministrator = true;
user.Policy.EnableContentDeletion = true;
user.Policy.EnableRemoteControlOfOtherUsers = true;
UpdateUserPolicy(user, user.Policy, false);
return users;
}
return users.ToArray();
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName))
{
defaultName = "MyJellyfinUser";
}
var name = MakeValidUsername(defaultName);
var user = InstantiateNewUser(name);
user.DateLastSaved = DateTime.UtcNow;
UserRepository.CreateUser(user);
user.Policy.IsAdministrator = true;
user.Policy.EnableContentDeletion = true;
user.Policy.EnableRemoteControlOfOtherUsers = true;
UpdateUserPolicy(user, user.Policy, false);
return new List<User> { user };
}
public UserDto GetUserDto(User user, string remoteEndPoint = null)

View file

@ -46,8 +46,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Url = url,
CancellationToken = CancellationToken.None,
BufferContent = false,
DecompressionMethod = CompressionMethod.None,
DecompressionMethod = CompressionMethod.None
};
foreach (var header in mediaSource.RequiredHttpHeaders)

View file

@ -18,11 +18,11 @@
"HeaderAlbumArtists": "Artistas del Álbum",
"HeaderCameraUploads": "Subidos desde Camara",
"HeaderContinueWatching": "Continuar Viendo",
"HeaderFavoriteAlbums": "Álbumes Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
"HeaderFavoriteEpisodes": "Episodios Preferidos",
"HeaderFavoriteShows": "Programas Preferidos",
"HeaderFavoriteSongs": "Canciones Favoritas",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en Vivo",
"HeaderNextUp": "A Continuación",
"HeaderRecordingGroups": "Grupos de Grabaciones",

View file

@ -21,7 +21,7 @@
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteShows": "Series favoritas",
"HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en directo",
"HeaderNextUp": "Siguiendo",

View file

@ -0,0 +1,96 @@
{
"Albums": "アルバム",
"AppDeviceValues": "アプリ: {0}, デバイス: {1}",
"Application": "アプリケーション",
"Artists": "アーティスト",
"AuthenticationSucceededWithUserName": "{0} 認証に成功しました",
"Books": "ブック",
"CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました",
"Channels": "チャンネル",
"ChapterNameValue": "チャプター {0}",
"Collections": "コレクション",
"DeviceOfflineWithName": "{0} が切断されました",
"DeviceOnlineWithName": "{0} が接続されました",
"FailedLoginAttemptWithUserName": "ログインを試行しましたが {0}によって失敗しました",
"Favorites": "お気に入り",
"Folders": "フォルダ",
"Genres": "ジャンル",
"HeaderAlbumArtists": "アルバムアーティスト",
"HeaderCameraUploads": "カメラアップロード",
"HeaderContinueWatching": "視聴中",
"HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト",
"HeaderFavoriteEpisodes": "お気に入りのエピソード",
"HeaderFavoriteShows": "お気に入りの番組",
"HeaderFavoriteSongs": "お気に入りの曲",
"HeaderLiveTV": "ライブ テレビ",
"HeaderNextUp": "次",
"HeaderRecordingGroups": "レコーディンググループ",
"HomeVideos": "ホームビデオ",
"Inherit": "継承",
"ItemAddedWithName": "{0} をライブラリに追加しました",
"ItemRemovedWithName": "{0} をライブラリから削除しました",
"LabelIpAddressValue": "IPアドレス: {0}",
"LabelRunningTimeValue": "稼働時間: {0}",
"Latest": "最新",
"MessageApplicationUpdated": "Jellyfin Server が更新されました",
"MessageApplicationUpdatedTo": "Jellyfin Server が {0}に更新されました",
"MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました",
"MessageServerConfigurationUpdated": "サーバー設定が更新されました",
"MixedContent": "ミックスコンテンツ",
"Movies": "ムービー",
"Music": "ミュージック",
"MusicVideos": "ミュージックビデオ",
"NameInstallFailed": "{0}のインストールに失敗しました",
"NameSeasonNumber": "シーズン {0}",
"NameSeasonUnknown": "不明なシーズン",
"NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。",
"NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります",
"NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です",
"NotificationOptionAudioPlayback": "オーディオの再生を開始",
"NotificationOptionAudioPlaybackStopped": "オーディオの再生をストップしました",
"NotificationOptionCameraImageUploaded": "カメライメージがアップロードされました",
"NotificationOptionInstallationFailed": "インストール失敗",
"NotificationOptionNewLibraryContent": "新しいコンテンツを追加しました",
"NotificationOptionPluginError": "プラグインに障害が発生しました",
"NotificationOptionPluginInstalled": "プラグインがインストールされました",
"NotificationOptionPluginUninstalled": "プラグインがアンインストールされました",
"NotificationOptionPluginUpdateInstalled": "プラグインのアップデートをインストールしました",
"NotificationOptionServerRestartRequired": "サーバーを再起動してください",
"NotificationOptionTaskFailed": "スケジュールされていたタスクの失敗",
"NotificationOptionUserLockedOut": "ユーザーはロックされています",
"NotificationOptionVideoPlayback": "ビデオの再生を開始しました",
"NotificationOptionVideoPlaybackStopped": "ビデオを停止しました",
"Photos": "フォト",
"Playlists": "プレイリスト",
"Plugin": "プラグイン",
"PluginInstalledWithName": "{0} がインストールされました",
"PluginUninstalledWithName": "{0} がアンインストールされました",
"PluginUpdatedWithName": "{0} が更新されました",
"ProviderValue": "プロバイダ: {0}",
"ScheduledTaskFailedWithName": "{0} が失敗しました",
"ScheduledTaskStartedWithName": "{0} が開始されました",
"ServerNameNeedsToBeRestarted": "{0} を再起動してください",
"Shows": "番組",
"Songs": "曲",
"StartupEmbyServerIsLoading": "Jellyfin Server は現在読み込み中です。しばらくしてからもう一度お試しください。",
"SubtitleDownloadFailureFromForItem": "{0} から {1}の字幕のダウンロードに失敗しました",
"SubtitlesDownloadedForItem": "{0} の字幕がダウンロードされました",
"Sync": "同期",
"System": "システム",
"TvShows": "テレビ番組",
"User": "ユーザー",
"UserCreatedWithName": "ユーザー {0} が作成されました",
"UserDeletedWithName": "User {0} を削除しました",
"UserDownloadingItemWithValues": "{0} が {1} をダウンロードしています",
"UserLockedOutWithName": "ユーザー {0} はロックされています",
"UserOfflineFromDevice": "{0} は {1} から切断しました",
"UserOnlineFromDevice": "{0} は {1} からオンラインになりました",
"UserPasswordChangedWithName": "ユーザー {0} のパスワードは変更されました",
"UserPolicyUpdatedWithName": "ユーザーポリシーが{0}に更新されました",
"UserStartedPlayingItemWithValues": "{0} は {2}で{1} を再生しています",
"UserStoppedPlayingItemWithValues": "{0} は{2}で{1} の再生が終わりました",
"ValueHasBeenAddedToLibrary": "{0}はあなたのメディアライブラリに追加されました",
"ValueSpecialEpisodeName": "スペシャル - {0}",
"VersionNumber": "バージョン {0}"
}

View file

@ -1,88 +1,88 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Device: {1}",
"Application": "Application",
"Artists": "Artists",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
"Channels": "Channels",
"ChapterNameValue": "Chapter {0}",
"Collections": "Collections",
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites",
"Folders": "Folders",
"Genres": "Genres",
"Albums": "앨범",
"AppDeviceValues": "앱: {0}, 디바이스: {1}",
"Application": "애플리케이션",
"Artists": "아티스트",
"AuthenticationSucceededWithUserName": "{0} 인증에 성공했습니다.",
"Books": "",
"CameraImageUploadedFrom": "새로운 카메라 이미지가 {0}에서 업로드되었습니다.",
"Channels": "채널",
"ChapterNameValue": "챕터 {0}",
"Collections": "컬렉션",
"DeviceOfflineWithName": "{0}가 접속이 끊어졌습니다.",
"DeviceOnlineWithName": "{0}가 접속되었습니다.",
"FailedLoginAttemptWithUserName": "{0}에서 로그인이 실패했습니다.",
"Favorites": "즐겨찾기",
"Folders": "폴더",
"Genres": "장르",
"HeaderAlbumArtists": "앨범 아티스트",
"HeaderCameraUploads": "Camera Uploads",
"HeaderCameraUploads": "카메라 업로드",
"HeaderContinueWatching": "계속 시청하기",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
"HeaderFavoriteAlbums": "좋아하는 앨범",
"HeaderFavoriteArtists": "좋아하는 아티스트",
"HeaderFavoriteEpisodes": "Favorite Episodes",
"HeaderFavoriteShows": "즐겨찾는 쇼",
"HeaderFavoriteSongs": "Favorite Songs",
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Next Up",
"HeaderRecordingGroups": "Recording Groups",
"HomeVideos": "Home videos",
"Inherit": "Inherit",
"ItemAddedWithName": "{0} was added to the library",
"ItemRemovedWithName": "{0} was removed from the library",
"LabelIpAddressValue": "Ip address: {0}",
"LabelRunningTimeValue": "Running time: {0}",
"Latest": "Latest",
"MessageApplicationUpdated": "Jellyfin Server has been updated",
"MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated",
"MessageServerConfigurationUpdated": "Server configuration has been updated",
"MixedContent": "Mixed content",
"Movies": "Movies",
"Music": "Music",
"MusicVideos": "Music videos",
"NameInstallFailed": "{0} installation failed",
"NameSeasonNumber": "Season {0}",
"NameSeasonUnknown": "Season Unknown",
"NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.",
"NotificationOptionApplicationUpdateAvailable": "Application update available",
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
"NotificationOptionAudioPlayback": "Audio playback started",
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
"NotificationOptionInstallationFailed": "Installation failure",
"NotificationOptionNewLibraryContent": "New content added",
"NotificationOptionPluginError": "Plugin failure",
"NotificationOptionPluginInstalled": "Plugin installed",
"NotificationOptionPluginUninstalled": "Plugin uninstalled",
"NotificationOptionPluginUpdateInstalled": "Plugin update installed",
"NotificationOptionServerRestartRequired": "Server restart required",
"NotificationOptionTaskFailed": "Scheduled task failure",
"NotificationOptionUserLockedOut": "User locked out",
"NotificationOptionVideoPlayback": "Video playback started",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
"Photos": "Photos",
"Playlists": "Playlists",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed",
"PluginUninstalledWithName": "{0} was uninstalled",
"PluginUpdatedWithName": "{0} was updated",
"ProviderValue": "Provider: {0}",
"ScheduledTaskFailedWithName": "{0} failed",
"ScheduledTaskStartedWithName": "{0} started",
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
"Shows": "Shows",
"Songs": "Songs",
"HeaderFavoriteSongs": "좋아하는 노래",
"HeaderLiveTV": "TV 방송",
"HeaderNextUp": "다음으로",
"HeaderRecordingGroups": "녹화 그룹",
"HomeVideos": "홈 비디오",
"Inherit": "상속",
"ItemAddedWithName": "{0} 라이브러리에 추가됨",
"ItemRemovedWithName": "{0} 라이브러리에서 제거됨",
"LabelIpAddressValue": "IP 주소: {0}",
"LabelRunningTimeValue": "상영 시간: {0}",
"Latest": "최근",
"MessageApplicationUpdated": "Jellyfin 서버 업데이트됨",
"MessageApplicationUpdatedTo": "Jellyfin 서버가 {0}로 업데이트됨",
"MessageNamedServerConfigurationUpdatedWithValue": "서버 환경 설정 {0} 섹션 업데이트 됨",
"MessageServerConfigurationUpdated": "서버 환경 설정 업데이드됨",
"MixedContent": "혼합 콘텐츠",
"Movies": "영화",
"Music": "음악",
"MusicVideos": "뮤직 비디오",
"NameInstallFailed": "{0} 설치 실패.",
"NameSeasonNumber": "시즌 {0}",
"NameSeasonUnknown": "알 수 없는 시즌",
"NewVersionIsAvailable": "새 버전의 Jellyfin 서버를 사용할 수 있습니다.",
"NotificationOptionApplicationUpdateAvailable": "애플리케이션 업데이트 사용 가능",
"NotificationOptionApplicationUpdateInstalled": "애플리케이션 업데이트가 설치됨",
"NotificationOptionAudioPlayback": "오디오 재생을 시작함",
"NotificationOptionAudioPlaybackStopped": "오디오 재생이 중지됨",
"NotificationOptionCameraImageUploaded": "카메라 이미지가 업로드됨",
"NotificationOptionInstallationFailed": "설치 실패",
"NotificationOptionNewLibraryContent": "새 콘텐트가 추가됨",
"NotificationOptionPluginError": "플러그인 실패",
"NotificationOptionPluginInstalled": "플러그인이 설치됨",
"NotificationOptionPluginUninstalled": "플러그인이 설치 제거됨",
"NotificationOptionPluginUpdateInstalled": "플러그인 업데이트가 설치됨",
"NotificationOptionServerRestartRequired": "서버를 다시 시작하십시오",
"NotificationOptionTaskFailed": "예약 작업 실패",
"NotificationOptionUserLockedOut": "사용자가 잠겼습니다",
"NotificationOptionVideoPlayback": "비디오 재생을 시작함",
"NotificationOptionVideoPlaybackStopped": "비디오 재생이 중지됨",
"Photos": "사진",
"Playlists": "재생목록",
"Plugin": "플러그인",
"PluginInstalledWithName": "{0} 설치됨",
"PluginUninstalledWithName": "{0} 설치 제거됨",
"PluginUpdatedWithName": "{0} 업데이트됨",
"ProviderValue": "제공자: {0}",
"ScheduledTaskFailedWithName": "{0} 실패",
"ScheduledTaskStartedWithName": "{0} 시작",
"ServerNameNeedsToBeRestarted": "{0} 를 재시작하십시오",
"Shows": "프로그램",
"Songs": "노래",
"StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시후 다시시도 해주세요.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
"SubtitlesDownloadedForItem": "Subtitles downloaded for {0}",
"Sync": "Sync",
"System": "System",
"TvShows": "TV Shows",
"User": "User",
"UserCreatedWithName": "User {0} has been created",
"UserDeletedWithName": "User {0} has been deleted",
"SubtitleDownloadFailureFromForItem": "{0}에서 {1} 자막 다운로드에 실패했습니다",
"SubtitlesDownloadedForItem": "{0} 자막을 다운로드했습니다",
"Sync": "동기화",
"System": "시스템",
"TvShows": "TV ",
"User": "사용자",
"UserCreatedWithName": "사용자 {0} 생성됨",
"UserDeletedWithName": "사용자 {0} 삭제됨",
"UserDownloadingItemWithValues": "{0} is downloading {1}",
"UserLockedOutWithName": "User {0} has been locked out",
"UserOfflineFromDevice": "{0} has disconnected from {1}",

View file

@ -1,21 +1,21 @@
{
"Albums": "Albums",
"Albums": "Albumai",
"AppDeviceValues": "App: {0}, Device: {1}",
"Application": "Application",
"Artists": "Artists",
"Artists": "Atlikėjai",
"AuthenticationSucceededWithUserName": "{0} successfully authenticated",
"Books": "Books",
"Books": "Knygos",
"CameraImageUploadedFrom": "A new camera image has been uploaded from {0}",
"Channels": "Channels",
"Channels": "Kanalai",
"ChapterNameValue": "Chapter {0}",
"Collections": "Collections",
"Collections": "Kolekcijos",
"DeviceOfflineWithName": "{0} has disconnected",
"DeviceOnlineWithName": "{0} is connected",
"FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
"Favorites": "Favorites",
"Folders": "Folders",
"Favorites": "Mėgstami",
"Folders": "Katalogai",
"Genres": "Žanrai",
"HeaderAlbumArtists": "Album Artists",
"HeaderAlbumArtists": "Albumo atlikėjai",
"HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Žiūrėti toliau",
"HeaderFavoriteAlbums": "Favorite Albums",

View file

@ -89,5 +89,8 @@
"UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 播放 {1}",
"ValueHasBeenAddedToLibrary": "{0} 已新增至您的媒體庫",
"ValueSpecialEpisodeName": "特典 - {0}",
"VersionNumber": "版本 {0}"
"VersionNumber": "版本 {0}",
"HeaderRecordingGroups": "錄製組",
"Inherit": "繼承",
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕"
}

View file

@ -1,10 +1,8 @@
using System;
using System.IO;
using System.Threading.Tasks;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations
@ -13,34 +11,14 @@ namespace Emby.Server.Implementations
{
private readonly IFileSystem _fileSystem;
private readonly ILogger _logger;
private readonly IHttpResultFactory _resultFactory;
public ResourceFileManager(
IHttpResultFactory resultFactory,
ILoggerFactory loggerFactory,
IFileSystem fileSystem)
public ResourceFileManager(ILogger<ResourceFileManager> logger, IFileSystem fileSystem)
{
_resultFactory = resultFactory;
_logger = loggerFactory.CreateLogger("ResourceManager");
_logger = logger;
_fileSystem = fileSystem;
}
public Stream GetResourceFileStream(string basePath, string virtualPath)
{
return _fileSystem.GetFileStream(GetResourcePath(basePath, virtualPath), FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, true);
}
public Task<object> GetStaticFileResult(IRequest request, string basePath, string virtualPath, string contentType, TimeSpan? cacheDuration)
{
return _resultFactory.GetStaticFileResult(request, GetResourcePath(basePath, virtualPath));
}
public string ReadAllText(string basePath, string virtualPath)
{
return File.ReadAllText(GetResourcePath(basePath, virtualPath));
}
private string GetResourcePath(string basePath, string virtualPath)
public string GetResourcePath(string basePath, string virtualPath)
{
var fullPath = Path.Combine(basePath, virtualPath.Replace('/', Path.DirectorySeparatorChar));
@ -50,7 +28,7 @@ namespace Emby.Server.Implementations
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in Path.GetFullPath");
_logger.LogError(ex, "Error retrieving full path");
}
// Don't allow file system access outside of the source folder

View file

@ -15,22 +15,16 @@ namespace Emby.Server.Implementations.Security
{
public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository
{
private readonly IServerConfigurationManager _config;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
public AuthenticationRepository(ILoggerFactory loggerFactory, IServerConfigurationManager config)
: base(loggerFactory.CreateLogger(nameof(AuthenticationRepository)))
{
_config = config;
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db");
}
public void Initialize()
{
using (var connection = CreateConnection())
using (var connection = GetConnection())
{
RunDefaultInitialization(connection);
var tableNewlyCreated = !TableExists(connection, "Tokens");
string[] queries = {
@ -91,31 +85,28 @@ namespace Emby.Server.Implementations.Security
throw new ArgumentNullException(nameof(info));
}
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)"))
{
using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)"))
{
statement.TryBind("@AccessToken", info.AccessToken);
statement.TryBind("@AccessToken", info.AccessToken);
statement.TryBind("@DeviceId", info.DeviceId);
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N")));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@IsActive", true);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
statement.TryBind("@DeviceId", info.DeviceId);
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N")));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@IsActive", true);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
statement.MoveNext();
}
statement.MoveNext();
}
}, TransactionMode);
}
}, TransactionMode);
}
}
@ -126,31 +117,28 @@ namespace Emby.Server.Implementations.Security
throw new ArgumentNullException(nameof(info));
}
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id"))
{
using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id"))
{
statement.TryBind("@Id", info.Id);
statement.TryBind("@Id", info.Id);
statement.TryBind("@AccessToken", info.AccessToken);
statement.TryBind("@AccessToken", info.AccessToken);
statement.TryBind("@DeviceId", info.DeviceId);
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N")));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
statement.TryBind("@DeviceId", info.DeviceId);
statement.TryBind("@AppName", info.AppName);
statement.TryBind("@AppVersion", info.AppVersion);
statement.TryBind("@DeviceName", info.DeviceName);
statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N")));
statement.TryBind("@UserName", info.UserName);
statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue());
statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue());
statement.MoveNext();
}
}, TransactionMode);
}
statement.MoveNext();
}
}, TransactionMode);
}
}
@ -161,20 +149,17 @@ namespace Emby.Server.Implementations.Security
throw new ArgumentNullException(nameof(info));
}
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id"))
{
using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id"))
{
statement.TryBind("@Id", info.Id);
statement.TryBind("@Id", info.Id);
statement.MoveNext();
}
}, TransactionMode);
}
statement.MoveNext();
}
}, TransactionMode);
}
}
@ -261,45 +246,42 @@ namespace Emby.Server.Implementations.Security
var list = new List<AuthenticationInfo>();
using (WriteLock.Read())
using (var connection = GetConnection(true))
{
using (var connection = CreateConnection(true))
return connection.RunInTransaction(db =>
{
return connection.RunInTransaction(db =>
var result = new QueryResult<AuthenticationInfo>();
var statementTexts = new List<string>();
statementTexts.Add(commandText);
statementTexts.Add("select count (Id) from Tokens" + whereTextWithoutPaging);
var statements = PrepareAll(db, statementTexts)
.ToList();
using (var statement = statements[0])
{
var result = new QueryResult<AuthenticationInfo>();
BindAuthenticationQueryParams(query, statement);
var statementTexts = new List<string>();
statementTexts.Add(commandText);
statementTexts.Add("select count (Id) from Tokens" + whereTextWithoutPaging);
var statements = PrepareAllSafe(db, statementTexts)
.ToList();
using (var statement = statements[0])
foreach (var row in statement.ExecuteQuery())
{
BindAuthenticationQueryParams(query, statement);
foreach (var row in statement.ExecuteQuery())
{
list.Add(Get(row));
}
using (var totalCountStatement = statements[1])
{
BindAuthenticationQueryParams(query, totalCountStatement);
result.TotalRecordCount = totalCountStatement.ExecuteQuery()
.SelectScalarInt()
.First();
}
list.Add(Get(row));
}
result.Items = list.ToArray();
return result;
using (var totalCountStatement = statements[1])
{
BindAuthenticationQueryParams(query, totalCountStatement);
}, ReadTransactionMode);
}
result.TotalRecordCount = totalCountStatement.ExecuteQuery()
.SelectScalarInt()
.First();
}
}
result.Items = list.ToArray();
return result;
}, ReadTransactionMode);
}
}
@ -362,31 +344,28 @@ namespace Emby.Server.Implementations.Security
public DeviceOptions GetDeviceOptions(string deviceId)
{
using (WriteLock.Read())
using (var connection = GetConnection(true))
{
using (var connection = CreateConnection(true))
return connection.RunInTransaction(db =>
{
return connection.RunInTransaction(db =>
using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId"))
{
using (var statement = PrepareStatementSafe(db, "select CustomName from Devices where Id=@DeviceId"))
statement.TryBind("@DeviceId", deviceId);
var result = new DeviceOptions();
foreach (var row in statement.ExecuteQuery())
{
statement.TryBind("@DeviceId", deviceId);
var result = new DeviceOptions();
foreach (var row in statement.ExecuteQuery())
if (row[0].SQLiteType != SQLiteType.Null)
{
if (row[0].SQLiteType != SQLiteType.Null)
{
result.CustomName = row[0].ToString();
}
result.CustomName = row[0].ToString();
}
return result;
}
}, ReadTransactionMode);
}
return result;
}
}, ReadTransactionMode);
}
}
@ -397,30 +376,27 @@ namespace Emby.Server.Implementations.Security
throw new ArgumentNullException(nameof(options));
}
using (WriteLock.Write())
using (var connection = GetConnection())
{
using (var connection = CreateConnection())
connection.RunInTransaction(db =>
{
connection.RunInTransaction(db =>
using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))"))
{
using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))"))
statement.TryBind("@Id", deviceId);
if (string.IsNullOrWhiteSpace(options.CustomName))
{
statement.TryBind("@Id", deviceId);
if (string.IsNullOrWhiteSpace(options.CustomName))
{
statement.TryBindNull("@CustomName");
}
else
{
statement.TryBind("@CustomName", options.CustomName);
}
statement.MoveNext();
statement.TryBindNull("@CustomName");
}
else
{
statement.TryBind("@CustomName", options.CustomName);
}
}, TransactionMode);
}
statement.MoveNext();
}
}, TransactionMode);
}
}
}

View file

@ -1,26 +1,17 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Model.Services;
namespace Emby.Server.Implementations.Services
{
public delegate Task<object> InstanceExecFn(IRequest requestContext, object intance, object request);
public delegate object ActionInvokerFn(object intance, object request);
public delegate void VoidActionInvokerFn(object intance, object request);
public class ServiceController
{
public static ServiceController Instance;
public ServiceController()
{
Instance = this;
}
public void Init(HttpListenerHost appHost, Type[] serviceTypes)
public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes)
{
foreach (var serviceType in serviceTypes)
{
@ -37,7 +28,11 @@ namespace Emby.Server.Implementations.Services
foreach (var mi in serviceType.GetActions())
{
var requestType = mi.GetParameters()[0].ParameterType;
if (processedReqs.Contains(requestType)) continue;
if (processedReqs.Contains(requestType))
{
continue;
}
processedReqs.Add(requestType);
ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions);
@ -55,18 +50,6 @@ namespace Emby.Server.Implementations.Services
}
}
public static Type FirstGenericType(Type type)
{
while (type != null)
{
if (type.GetTypeInfo().IsGenericType)
return type;
type = type.GetTypeInfo().BaseType;
}
return null;
}
public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap();
public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType)
@ -84,17 +67,24 @@ namespace Emby.Server.Implementations.Services
public void RegisterRestPath(RestPath restPath)
{
if (!restPath.Path.StartsWith("/"))
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
if (!RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
if (restPath.Path[0] != '/')
{
pathsAtFirstMatch = new List<RestPath>();
RestPathMap[restPath.FirstMatchHashKey] = pathsAtFirstMatch;
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
}
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
{
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
}
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
{
pathsAtFirstMatch.Add(restPath);
}
else
{
RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath };
}
pathsAtFirstMatch.Add(restPath);
}
public RestPath GetRestPathForRequest(string httpMethod, string pathInfo)
@ -155,17 +145,15 @@ namespace Emby.Server.Implementations.Services
return null;
}
public Task<object> Execute(HttpListenerHost appHost, object requestDto, IRequest req)
public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req)
{
req.Dto = requestDto;
var requestType = requestDto.GetType();
req.OperationName = requestType.Name;
var serviceType = appHost.GetServiceTypeByRequest(requestType);
var serviceType = httpHost.GetServiceTypeByRequest(requestType);
var service = appHost.CreateInstance(serviceType);
//var service = typeFactory.CreateInstance(serviceType);
var service = httpHost.CreateInstance(serviceType);
var serviceRequiresContext = service as IRequiresRequest;
if (serviceRequiresContext != null)

View file

@ -11,6 +11,16 @@ namespace Emby.Server.Implementations.Services
{
public class ServiceHandler
{
public RestPath RestPath { get; }
public string ResponseContentType { get; }
internal ServiceHandler(RestPath restPath, string responseContentType)
{
RestPath = restPath;
ResponseContentType = responseContentType;
}
protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType)
{
if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0)
@ -18,24 +28,18 @@ namespace Emby.Server.Implementations.Services
var deserializer = RequestHelper.GetRequestReader(host, contentType);
if (deserializer != null)
{
return deserializer(requestType, httpReq.InputStream);
return deserializer.Invoke(requestType, httpReq.InputStream);
}
}
return Task.FromResult(host.CreateInstance(requestType));
}
public static RestPath FindMatchingRestPath(string httpMethod, string pathInfo, out string contentType)
{
pathInfo = GetSanitizedPathInfo(pathInfo, out contentType);
return ServiceController.Instance.GetRestPathForRequest(httpMethod, pathInfo);
}
public static string GetSanitizedPathInfo(string pathInfo, out string contentType)
{
contentType = null;
var pos = pathInfo.LastIndexOf('.');
if (pos >= 0)
if (pos != -1)
{
var format = pathInfo.Substring(pos + 1);
contentType = GetFormatContentType(format);
@ -44,58 +48,38 @@ namespace Emby.Server.Implementations.Services
pathInfo = pathInfo.Substring(0, pos);
}
}
return pathInfo;
}
private static string GetFormatContentType(string format)
{
//built-in formats
if (format == "json")
return "application/json";
if (format == "xml")
return "application/xml";
return null;
switch (format)
{
case "json": return "application/json";
case "xml": return "application/xml";
default: return null;
}
}
public RestPath GetRestPath(string httpMethod, string pathInfo)
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, IResponse httpRes, ILogger logger, CancellationToken cancellationToken)
{
if (this.RestPath == null)
{
this.RestPath = FindMatchingRestPath(httpMethod, pathInfo, out string contentType);
if (contentType != null)
ResponseContentType = contentType;
}
return this.RestPath;
}
public RestPath RestPath { get; set; }
// Set from SSHHF.GetHandlerForPathInfo()
public string ResponseContentType { get; set; }
public async Task ProcessRequestAsync(HttpListenerHost appHost, IRequest httpReq, IResponse httpRes, ILogger logger, string operationName, CancellationToken cancellationToken)
{
var restPath = GetRestPath(httpReq.Verb, httpReq.PathInfo);
if (restPath == null)
{
throw new NotSupportedException("No RestPath found for: " + httpReq.Verb + " " + httpReq.PathInfo);
}
SetRoute(httpReq, restPath);
httpReq.Items["__route"] = RestPath;
if (ResponseContentType != null)
{
httpReq.ResponseContentType = ResponseContentType;
}
var request = httpReq.Dto = await CreateRequest(appHost, httpReq, restPath, logger).ConfigureAwait(false);
var request = httpReq.Dto = await CreateRequest(httpHost, httpReq, RestPath, logger).ConfigureAwait(false);
appHost.ApplyRequestFilters(httpReq, httpRes, request);
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
var response = await appHost.ServiceController.Execute(appHost, request, httpReq).ConfigureAwait(false);
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
// Apply response filters
foreach (var responseFilter in appHost.ResponseFilters)
foreach (var responseFilter in httpHost.ResponseFilters)
{
responseFilter(httpReq, httpRes, response);
}
@ -152,7 +136,11 @@ namespace Emby.Server.Implementations.Services
foreach (var name in request.QueryString.Keys)
{
if (name == null) continue; //thank you ASP.NET
if (name == null)
{
// thank you ASP.NET
continue;
}
var values = request.QueryString[name];
if (values.Count == 1)
@ -175,7 +163,11 @@ namespace Emby.Server.Implementations.Services
{
foreach (var name in formData.Keys)
{
if (name == null) continue; //thank you ASP.NET
if (name == null)
{
// thank you ASP.NET
continue;
}
var values = formData.GetValues(name);
if (values.Count == 1)
@ -210,7 +202,12 @@ namespace Emby.Server.Implementations.Services
foreach (var name in request.QueryString.Keys)
{
if (name == null) continue; //thank you ASP.NET
if (name == null)
{
// thank you ASP.NET
continue;
}
map[name] = request.QueryString[name];
}
@ -221,7 +218,12 @@ namespace Emby.Server.Implementations.Services
{
foreach (var name in formData.Keys)
{
if (name == null) continue; //thank you ASP.NET
if (name == null)
{
// thank you ASP.NET
continue;
}
map[name] = formData[name];
}
}
@ -229,17 +231,5 @@ namespace Emby.Server.Implementations.Services
return map;
}
private static void SetRoute(IRequest req, RestPath route)
{
req.Items["__route"] = route;
}
private static RestPath GetRoute(IRequest req)
{
req.Items.TryGetValue("__route", out var route);
return route as RestPath;
}
}
}

View file

@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Services
foreach (var propertyInfo in RestPath.GetSerializableProperties(type))
{
var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo);
var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo);
var propertyType = propertyInfo.PropertyType;
var propertyParseStringFn = GetParseFn(propertyType);
var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType);
@ -110,9 +110,9 @@ namespace Emby.Server.Implementations.Services
}
}
internal class TypeAccessor
internal static class TypeAccessor
{
public static Action<object, object> GetSetPropertyMethod(Type type, PropertyInfo propertyInfo)
public static Action<object, object> GetSetPropertyMethod(PropertyInfo propertyInfo)
{
if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0)
{

View file

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services;
using Emby.Server.Implementations.HttpServer;
namespace Emby.Server.Implementations.Services
{
@ -109,10 +109,16 @@ namespace Emby.Server.Implementations.Services
public class SwaggerService : IService, IRequiresRequest
{
private readonly IHttpServer _httpServer;
private SwaggerSpec _spec;
public IRequest Request { get; set; }
public SwaggerService(IHttpServer httpServer)
{
_httpServer = httpServer;
}
public object Get(GetSwaggerSpec request)
{
return _spec ?? (_spec = GetSpec());
@ -181,7 +187,8 @@ namespace Emby.Server.Implementations.Services
{
var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>();
var all = ServiceController.Instance.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
// REVIEW: this can be done better
var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
foreach (var current in all)
{
@ -192,11 +199,8 @@ namespace Emby.Server.Implementations.Services
continue;
}
if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)
|| info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
{
continue;
}

View file

@ -7,7 +7,6 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Devices;
@ -25,7 +24,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Logging;
@ -53,8 +51,6 @@ namespace Emby.Server.Implementations.Session
private readonly IImageProcessor _imageProcessor;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IHttpClient _httpClient;
private readonly IJsonSerializer _jsonSerializer;
private readonly IServerApplicationHost _appHost;
private readonly IAuthenticationRepository _authRepo;
@ -96,9 +92,7 @@ namespace Emby.Server.Implementations.Session
IMusicManager musicManager,
IDtoService dtoService,
IImageProcessor imageProcessor,
IJsonSerializer jsonSerializer,
IServerApplicationHost appHost,
IHttpClient httpClient,
IAuthenticationRepository authRepo,
IDeviceManager deviceManager,
IMediaSourceManager mediaSourceManager)
@ -110,9 +104,7 @@ namespace Emby.Server.Implementations.Session
_musicManager = musicManager;
_dtoService = dtoService;
_imageProcessor = imageProcessor;
_jsonSerializer = jsonSerializer;
_appHost = appHost;
_httpClient = httpClient;
_authRepo = authRepo;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
@ -347,8 +339,7 @@ namespace Emby.Server.Implementations.Session
var runtimeTicks = libraryItem.RunTimeTicks;
MediaSourceInfo mediaSource = null;
var hasMediaSources = libraryItem as IHasMediaSources;
if (hasMediaSources != null)
if (libraryItem is IHasMediaSources hasMediaSources)
{
mediaSource = await GetMediaSource(libraryItem, info.MediaSourceId, info.LiveStreamId).ConfigureAwait(false);
@ -1046,6 +1037,24 @@ namespace Emby.Server.Implementations.Session
}
}
private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, string name, T data, CancellationToken cancellationToken)
{
IEnumerable<Task> GetTasks()
{
var messageId = Guid.NewGuid().ToString("N");
foreach (var session in sessions)
{
var controllers = session.SessionControllers;
foreach (var controller in controllers)
{
yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken);
}
}
}
return Task.WhenAll(GetTasks());
}
public async Task SendPlayCommand(string controllingSessionId, string sessionId, PlayRequest command, CancellationToken cancellationToken)
{
CheckDisposed();
@ -1232,12 +1241,13 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSession(session, "Playstate", command, cancellationToken);
}
private void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
{
if (session == null)
{
throw new ArgumentNullException(nameof(session));
}
if (controllingSession == null)
{
throw new ArgumentNullException(nameof(controllingSession));
@ -1249,26 +1259,11 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
public async Task SendRestartRequiredNotification(CancellationToken cancellationToken)
public Task SendRestartRequiredNotification(CancellationToken cancellationToken)
{
CheckDisposed();
var sessions = Sessions.ToList();
var tasks = sessions.Select(session => Task.Run(async () =>
{
try
{
await SendMessageToSession(session, "RestartRequired", string.Empty, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError("Error in SendRestartRequiredNotification.", ex);
}
}, cancellationToken)).ToArray();
await Task.WhenAll(tasks).ConfigureAwait(false);
return SendMessageToSessions(Sessions, "RestartRequired", string.Empty, cancellationToken);
}
/// <summary>
@ -1280,22 +1275,7 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
var sessions = Sessions.ToList();
var tasks = sessions.Select(session => Task.Run(async () =>
{
try
{
await SendMessageToSession(session, "ServerShuttingDown", string.Empty, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError("Error in SendServerShutdownNotification.", ex);
}
}, cancellationToken)).ToArray();
return Task.WhenAll(tasks);
return SendMessageToSessions(Sessions, "ServerShuttingDown", string.Empty, cancellationToken);
}
/// <summary>
@ -1309,22 +1289,7 @@ namespace Emby.Server.Implementations.Session
_logger.LogDebug("Beginning SendServerRestartNotification");
var sessions = Sessions.ToList();
var tasks = sessions.Select(session => Task.Run(async () =>
{
try
{
await SendMessageToSession(session, "ServerRestarting", string.Empty, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError("Error in SendServerRestartNotification.", ex);
}
}, cancellationToken)).ToArray();
return Task.WhenAll(tasks);
return SendMessageToSessions(Sessions, "ServerRestarting", string.Empty, cancellationToken);
}
/// <summary>
@ -1841,64 +1806,23 @@ namespace Emby.Server.Implementations.Session
var data = dataFn();
var tasks = sessions.Select(session => Task.Run(async () =>
{
try
{
await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError("Error sending message", ex);
}
}, cancellationToken)).ToArray();
return Task.WhenAll(tasks);
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser)).ToList();
var tasks = sessions.Select(session => Task.Run(async () =>
{
try
{
await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError("Error sending message", ex);
}
}, cancellationToken)).ToArray();
return Task.WhenAll(tasks);
var sessions = Sessions.Where(i => userIds.Any(i.ContainsUser));
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)).ToList();
var tasks = sessions.Select(session => Task.Run(async () =>
{
try
{
await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError("Error sending message", ex);
}
}, cancellationToken)).ToArray();
return Task.WhenAll(tasks);
var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
public Task SendMessageToUserDeviceAndAdminSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
@ -1906,23 +1830,8 @@ namespace Emby.Server.Implementations.Session
CheckDisposed();
var sessions = Sessions
.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i))
.ToList();
var tasks = sessions.Select(session => Task.Run(async () =>
{
try
{
await SendMessageToSession(session, name, data, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError("Error sending message", ex);
}
}, cancellationToken)).ToArray();
return Task.WhenAll(tasks);
.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i));
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
private bool IsAdminSession(SessionInfo s)

View file

@ -89,10 +89,8 @@ namespace Emby.Server.Implementations.Session
/// </summary>
/// <param name="message">The message.</param>
/// <returns>Task.</returns>
public Task ProcessMessage(WebSocketMessageInfo message)
{
return Task.CompletedTask;
}
public Task ProcessMessageAsync(WebSocketMessageInfo message)
=> Task.CompletedTask;
private void EnsureController(SessionInfo session, IWebSocketConnection connection)
{

View file

@ -86,8 +86,7 @@ namespace Emby.Server.Implementations.SocketSharp
else
{
// We use a substream, as in 2.x we will support large uploads streamed to disk,
var sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length);
files[e.Name] = sub;
files[e.Name] = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length);
}
}
}
@ -374,7 +373,7 @@ namespace Emby.Server.Implementations.SocketSharp
var elem = new Element();
ReadOnlySpan<char> header;
while ((header = ReadHeaders().AsSpan()) != null)
while ((header = ReadLine().AsSpan()).Length != 0)
{
if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
@ -513,17 +512,6 @@ namespace Emby.Server.Implementations.SocketSharp
return false;
}
private string ReadHeaders()
{
string s = ReadLine();
if (s.Length == 0)
{
return null;
}
return s;
}
private static bool CompareBytes(byte[] orig, byte[] other)
{
for (int i = orig.Length - 1; i >= 0; i--)

View file

@ -3,7 +3,9 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Linq;
using System.Text;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
@ -25,8 +27,6 @@ namespace Emby.Server.Implementations.SocketSharp
this.OperationName = operationName;
this.request = httpContext;
this.Response = new WebSocketSharpResponse(logger, response);
// HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes[0]);
}
public HttpRequest HttpRequest => request;
@ -40,16 +40,9 @@ namespace Emby.Server.Implementations.SocketSharp
public string RawUrl => request.GetEncodedPathAndQuery();
public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/');
// Header[name] returns "" when undefined
public string XForwardedFor
=> StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"].ToString();
public int? XForwardedPort
=> StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"], CultureInfo.InvariantCulture);
public string XForwardedProtocol => StringValues.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"].ToString();
public string XRealIp => StringValues.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"].ToString();
private string GetHeader(string name) => request.Headers[name].ToString();
private string remoteIp;
public string RemoteIp
@ -61,107 +54,27 @@ namespace Emby.Server.Implementations.SocketSharp
return remoteIp;
}
var temp = CheckBadChars(XForwardedFor.AsSpan());
if (temp.Length != 0)
IPAddress ip;
// "Real" remote ip might be in X-Forwarded-For of X-Real-Ip
// (if the server is behind a reverse proxy for example)
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip))
{
return remoteIp = temp.ToString();
if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
{
ip = request.HttpContext.Connection.RemoteIpAddress;
}
}
temp = CheckBadChars(XRealIp.AsSpan());
if (temp.Length != 0)
{
return remoteIp = NormalizeIp(temp).ToString();
}
return remoteIp = NormalizeIp(request.HttpContext.Connection.RemoteIpAddress.ToString().AsSpan()).ToString();
return remoteIp = NormalizeIp(ip).ToString();
}
}
private static readonly char[] HttpTrimCharacters = new char[] { (char)0x09, (char)0xA, (char)0xB, (char)0xC, (char)0xD, (char)0x20 };
// CheckBadChars - throws on invalid chars to be not found in header name/value
internal static ReadOnlySpan<char> CheckBadChars(ReadOnlySpan<char> name)
private static IPAddress NormalizeIp(IPAddress ip)
{
if (name.Length == 0)
if (ip.IsIPv4MappedToIPv6)
{
return name;
}
// VALUE check
// Trim spaces from both ends
name = name.Trim(HttpTrimCharacters);
// First, check for correctly formed multi-line value
// Second, check for absence of CTL characters
int crlf = 0;
for (int i = 0; i < name.Length; ++i)
{
char c = (char)(0x000000ff & (uint)name[i]);
switch (crlf)
{
case 0:
{
if (c == '\r')
{
crlf = 1;
}
else if (c == '\n')
{
// Technically this is bad HTTP. But it would be a breaking change to throw here.
// Is there an exploit?
crlf = 2;
}
else if (c == 127 || (c < ' ' && c != '\t'))
{
throw new ArgumentException("net_WebHeaderInvalidControlChars", nameof(name));
}
break;
}
case 1:
{
if (c == '\n')
{
crlf = 2;
break;
}
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
case 2:
{
if (c == ' ' || c == '\t')
{
crlf = 0;
break;
}
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
}
}
if (crlf != 0)
{
throw new ArgumentException("net_WebHeaderInvalidCRLFChars", nameof(name));
}
return name;
}
private ReadOnlySpan<char> NormalizeIp(ReadOnlySpan<char> ip)
{
if (ip.Length != 0 && !ip.IsWhiteSpace())
{
// Handle ipv4 mapped to ipv6
const string srch = "::ffff:";
var index = ip.IndexOf(srch.AsSpan(), StringComparison.OrdinalIgnoreCase);
if (index == 0)
{
ip = ip.Slice(srch.Length);
}
return ip.MapToIPv4();
}
return ip;
@ -312,97 +225,7 @@ namespace Emby.Server.Implementations.SocketSharp
return pos == -1 ? strVal : strVal.Slice(0, pos);
}
public static string HandlerFactoryPath;
private string pathInfo;
public string PathInfo
{
get
{
if (this.pathInfo == null)
{
var mode = HandlerFactoryPath;
var pos = RawUrl.IndexOf("?", StringComparison.Ordinal);
if (pos != -1)
{
var path = RawUrl.Substring(0, pos);
this.pathInfo = GetPathInfo(
path,
mode,
mode ?? string.Empty);
}
else
{
this.pathInfo = RawUrl;
}
this.pathInfo = WebUtility.UrlDecode(pathInfo);
this.pathInfo = NormalizePathInfo(pathInfo, mode).ToString();
}
return this.pathInfo;
}
}
private static string GetPathInfo(string fullPath, string mode, string appPath)
{
var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode);
if (!string.IsNullOrEmpty(pathInfo))
{
return pathInfo;
}
// Wildcard mode relies on this to work out the handlerPath
pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath);
if (!string.IsNullOrEmpty(pathInfo))
{
return pathInfo;
}
return fullPath;
}
private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot)
{
if (mappedPathRoot == null)
{
return null;
}
var sbPathInfo = new StringBuilder();
var fullPathParts = fullPath.Split('/');
var mappedPathRootParts = mappedPathRoot.Split('/');
var fullPathIndexOffset = mappedPathRootParts.Length - 1;
var pathRootFound = false;
for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++)
{
if (pathRootFound)
{
sbPathInfo.Append("/" + fullPathParts[fullPathIndex]);
}
else if (fullPathIndex - fullPathIndexOffset >= 0)
{
pathRootFound = true;
for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++)
{
if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase))
{
pathRootFound = false;
break;
}
}
}
}
if (!pathRootFound)
{
return null;
}
return sbPathInfo.Length > 1 ? sbPathInfo.ToString().TrimEnd('/') : "/";
}
public string PathInfo => this.request.Path.Value;
public string UserAgent => request.Headers[HeaderNames.UserAgent];
@ -474,45 +297,32 @@ namespace Emby.Server.Implementations.SocketSharp
{
get
{
if (httpFiles == null)
if (httpFiles != null)
{
if (files == null)
{
return httpFiles = Array.Empty<IHttpFile>();
}
return httpFiles;
}
httpFiles = new IHttpFile[files.Count];
var i = 0;
foreach (var pair in files)
if (files == null)
{
return httpFiles = Array.Empty<IHttpFile>();
}
var values = files.Values;
httpFiles = new IHttpFile[values.Count];
for (int i = 0; i < values.Count; i++)
{
var reqFile = values.ElementAt(i);
httpFiles[i] = new HttpFile
{
var reqFile = pair.Value;
httpFiles[i] = new HttpFile
{
ContentType = reqFile.ContentType,
ContentLength = reqFile.ContentLength,
FileName = reqFile.FileName,
InputStream = reqFile.InputStream,
};
i++;
}
ContentType = reqFile.ContentType,
ContentLength = reqFile.ContentLength,
FileName = reqFile.FileName,
InputStream = reqFile.InputStream,
};
}
return httpFiles;
}
}
public static ReadOnlySpan<char> NormalizePathInfo(string pathInfo, string handlerPath)
{
if (handlerPath != null)
{
var trimmed = pathInfo.AsSpan().TrimStart('/');
if (trimmed.StartsWith(handlerPath.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return trimmed.Slice(handlerPath.Length).ToString().AsSpan();
}
}
return pathInfo.AsSpan();
}
}
}

View file

@ -2,14 +2,11 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using SkiaSharp;
@ -18,24 +15,28 @@ namespace Jellyfin.Drawing.Skia
public class SkiaEncoder : IImageEncoder
{
private readonly ILogger _logger;
private static IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private static ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
private readonly ILocalizationManager _localizationManager;
private static readonly HashSet<string> _transparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
public SkiaEncoder(
ILoggerFactory loggerFactory,
ILogger<SkiaEncoder> logger,
IApplicationPaths appPaths,
IFileSystem fileSystem,
ILocalizationManager localizationManager)
{
_logger = loggerFactory.CreateLogger("ImageEncoder");
_logger = logger;
_appPaths = appPaths;
_fileSystem = fileSystem;
_localizationManager = localizationManager;
LogVersion();
}
public string Name => "Skia";
public bool SupportsImageCollageCreation => true;
public bool SupportsImageEncoding => true;
public IReadOnlyCollection<string> SupportedInputFormats =>
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
@ -66,17 +67,15 @@ namespace Jellyfin.Drawing.Skia
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats
=> new HashSet<ImageFormat>() { ImageFormat.Webp, ImageFormat.Jpg, ImageFormat.Png };
private void LogVersion()
/// <summary>
/// Test to determine if the native lib is available
/// </summary>
public static void TestSkia()
{
// test an operation that requires the native library
SKPMColor.PreMultiply(SKColors.Black);
_logger.LogInformation("SkiaSharp version: " + GetVersion());
}
public static Version GetVersion()
=> typeof(SKBitmap).GetTypeInfo().Assembly.GetName().Version;
private static bool IsTransparent(SKColor color)
=> (color.Red == 255 && color.Green == 255 && color.Blue == 255) || color.Alpha == 0;
@ -106,6 +105,7 @@ namespace Jellyfin.Drawing.Skia
return false;
}
}
return true;
}
@ -118,6 +118,7 @@ namespace Jellyfin.Drawing.Skia
return false;
}
}
return true;
}
@ -202,7 +203,7 @@ namespace Jellyfin.Drawing.Skia
private static bool HasDiacritics(string text)
=> !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
private static bool RequiresSpecialCharacterHack(string path)
private bool RequiresSpecialCharacterHack(string path)
{
if (_localizationManager.HasUnicodeCategory(path, UnicodeCategory.OtherLetter))
{
@ -217,7 +218,7 @@ namespace Jellyfin.Drawing.Skia
return false;
}
private static string NormalizePath(string path, IFileSystem fileSystem)
private string NormalizePath(string path)
{
if (!RequiresSpecialCharacterHack(path))
{
@ -260,21 +261,18 @@ namespace Jellyfin.Drawing.Skia
}
}
private static readonly HashSet<string> TransparentImageTypes
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".gif", ".webp" };
internal static SKBitmap Decode(string path, bool forceCleanBitmap, IFileSystem fileSystem, ImageOrientation? orientation, out SKEncodedOrigin origin)
internal SKBitmap Decode(string path, bool forceCleanBitmap, ImageOrientation? orientation, out SKEncodedOrigin origin)
{
if (!File.Exists(path))
{
throw new FileNotFoundException("File not found", path);
}
var requiresTransparencyHack = TransparentImageTypes.Contains(Path.GetExtension(path));
var requiresTransparencyHack = _transparentImageTypes.Contains(Path.GetExtension(path));
if (requiresTransparencyHack || forceCleanBitmap)
{
using (var stream = new SKFileStream(NormalizePath(path, fileSystem)))
using (var stream = new SKFileStream(NormalizePath(path)))
using (var codec = SKCodec.Create(stream))
{
if (codec == null)
@ -295,11 +293,11 @@ namespace Jellyfin.Drawing.Skia
}
}
var resultBitmap = SKBitmap.Decode(NormalizePath(path, fileSystem));
var resultBitmap = SKBitmap.Decode(NormalizePath(path));
if (resultBitmap == null)
{
return Decode(path, true, fileSystem, orientation, out origin);
return Decode(path, true, orientation, out origin);
}
// If we have to resize these they often end up distorted
@ -307,7 +305,7 @@ namespace Jellyfin.Drawing.Skia
{
using (resultBitmap)
{
return Decode(path, true, fileSystem, orientation, out origin);
return Decode(path, true, orientation, out origin);
}
}
@ -319,13 +317,13 @@ namespace Jellyfin.Drawing.Skia
{
if (cropWhitespace)
{
using (var bitmap = Decode(path, forceAnalyzeBitmap, _fileSystem, orientation, out origin))
using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin))
{
return CropWhiteSpace(bitmap);
}
}
return Decode(path, forceAnalyzeBitmap, _fileSystem, orientation, out origin);
return Decode(path, forceAnalyzeBitmap, orientation, out origin);
}
private SKBitmap GetBitmap(string path, bool cropWhitespace, bool autoOrient, ImageOrientation? orientation)
@ -612,16 +610,16 @@ namespace Jellyfin.Drawing.Skia
if (ratio >= 1.4)
{
new StripCollageBuilder(_appPaths, _fileSystem).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
}
else if (ratio >= .9)
{
new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
}
else
{
// TODO: Create Poster collage capability
new StripCollageBuilder(_appPaths, _fileSystem).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
new StripCollageBuilder(this).BuildSquareCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
}
}
@ -650,11 +648,5 @@ namespace Jellyfin.Drawing.Skia
_logger.LogError(ex, "Error drawing indicator overlay");
}
}
public string Name => "Skia";
public bool SupportsImageCollageCreation => true;
public bool SupportsImageEncoding => true;
}
}

View file

@ -1,21 +1,17 @@
using System;
using System.Collections.Generic;
using System.IO;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
using SkiaSharp;
namespace Jellyfin.Drawing.Skia
{
public class StripCollageBuilder
{
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
private readonly SkiaEncoder _skiaEncoder;
public StripCollageBuilder(IApplicationPaths appPaths, IFileSystem fileSystem)
public StripCollageBuilder(SkiaEncoder skiaEncoder)
{
_appPaths = appPaths;
_fileSystem = fileSystem;
_skiaEncoder = skiaEncoder;
}
public static SKEncodedImageFormat GetEncodedFormat(string outputPath)
@ -25,19 +21,28 @@ namespace Jellyfin.Drawing.Skia
throw new ArgumentNullException(nameof(outputPath));
}
var ext = Path.GetExtension(outputPath).ToLowerInvariant();
var ext = Path.GetExtension(outputPath);
if (ext == ".jpg" || ext == ".jpeg")
if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
|| string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
{
return SKEncodedImageFormat.Jpeg;
}
if (ext == ".webp")
if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
{
return SKEncodedImageFormat.Webp;
}
if (ext == ".gif")
if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
{
return SKEncodedImageFormat.Gif;
}
if (ext == ".bmp")
if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
{
return SKEncodedImageFormat.Bmp;
}
// default to png
return SKEncodedImageFormat.Png;
@ -47,25 +52,19 @@ namespace Jellyfin.Drawing.Skia
{
using (var bitmap = BuildSquareCollageBitmap(paths, width, height))
using (var outputStream = new SKFileWStream(outputPath))
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
{
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
{
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
}
public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
{
using (var bitmap = BuildThumbCollageBitmap(paths, width, height))
using (var outputStream = new SKFileWStream(outputPath))
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
{
using (var outputStream = new SKFileWStream(outputPath))
{
using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()))
{
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
}
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
}
@ -127,7 +126,7 @@ namespace Jellyfin.Drawing.Skia
currentIndex = 0;
}
bitmap = SkiaEncoder.Decode(paths[currentIndex], false, _fileSystem, null, out var origin);
bitmap = _skiaEncoder.Decode(paths[currentIndex], false, null, out var origin);
imagesTested[currentIndex] = 0;
@ -156,7 +155,6 @@ namespace Jellyfin.Drawing.Skia
{
for (var y = 0; y < 2; y++)
{
using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex))
{
imageIndex = newIndex;

View file

@ -1,7 +1,8 @@
using System.Collections.Generic;
using System.Reflection;
using Emby.Server.Implementations;
using Emby.Server.Implementations.HttpServer;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
@ -15,8 +16,8 @@ namespace Jellyfin.Server
ILoggerFactory loggerFactory,
StartupOptions options,
IFileSystem fileSystem,
MediaBrowser.Controller.Drawing.IImageEncoder imageEncoder,
MediaBrowser.Common.Net.INetworkManager networkManager,
IImageEncoder imageEncoder,
INetworkManager networkManager,
IConfiguration configuration)
: base(
applicationPaths,

View file

@ -12,7 +12,7 @@
<!-- We need C# 7.1 for async main-->
<LangVersion>latest</LangVersion>
<!-- Disable documentation warnings (for now) -->
<NoWarn>SA1600;SA1601;CS1591</NoWarn>
<NoWarn>SA1600;SA1601;SA1629;CS1591</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
@ -26,8 +26,8 @@
<!-- Code analysers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
</ItemGroup>
@ -36,17 +36,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.4.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.0" />
<PackageReference Include="CommandLineParser" Version="2.5.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="2.1.1" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.3.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />
<PackageReference Include="SkiaSharp" Version="1.68.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.13" />
<PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.13" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.14" />
<PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" />
</ItemGroup>
<ItemGroup>

View file

@ -24,6 +24,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.AspNetCore;
using SQLitePCL;
using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace Jellyfin.Server
@ -34,9 +35,8 @@ namespace Jellyfin.Server
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
private static ILogger _logger;
private static bool _restartOnShutdown;
private static IConfiguration appConfig;
public static async Task Main(string[] args)
public static Task Main(string[] args)
{
// For backwards compatibility.
// Modify any input arguments now which start with single-hyphen to POSIX standard
@ -50,8 +50,8 @@ namespace Jellyfin.Server
}
// Parse the command line arguments and either start the app or exit indicating error
await Parser.Default.ParseArguments<StartupOptions>(args)
.MapResult(StartApp, _ => Task.CompletedTask).ConfigureAwait(false);
return Parser.Default.ParseArguments<StartupOptions>(args)
.MapResult(StartApp, _ => Task.CompletedTask);
}
public static void Shutdown()
@ -76,7 +76,7 @@ namespace Jellyfin.Server
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false);
IConfiguration appConfig = await CreateConfiguration(appPaths).ConfigureAwait(false);
CreateLogger(appConfig, appPaths);
@ -116,7 +116,12 @@ namespace Jellyfin.Server
ApplicationHost.LogEnvironmentInfo(_logger, appPaths);
SQLitePCL.Batteries_V2.Init();
// Increase the max http request limit
// The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit);
// CA5359: Do Not Disable Certificate Validation
#pragma warning disable CA5359
// Increase the max http request limit
// The default connection limit is 10 for ASP.NET hosted applications and 2 for all others.
@ -133,20 +138,24 @@ namespace Jellyfin.Server
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
#pragma warning restore CA5359
var fileSystem = new ManagedFileSystem(_loggerFactory, appPaths);
Batteries_V2.Init();
if (raw.sqlite3_enable_shared_cache(1) != raw.SQLITE_OK)
{
Console.WriteLine("WARN: Failed to enable shared cache for SQLite");
}
using (var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
fileSystem,
new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths),
new NullImageEncoder(),
new NetworkManager(_loggerFactory),
appConfig))
{
await appHost.InitAsync(new ServiceCollection()).ConfigureAwait(false);
appHost.ImageProcessor.ImageEncoder = GetImageEncoder(fileSystem, appPaths, appHost.LocalizationManager);
appHost.ImageProcessor.ImageEncoder = GetImageEncoder(appPaths, appHost.LocalizationManager);
await appHost.RunStartupTasksAsync().ConfigureAwait(false);
@ -169,7 +178,7 @@ namespace Jellyfin.Server
/// <summary>
/// Create the data, config and log paths from the variety of inputs(command line args,
/// environment variables) or decide on what default to use. For Windows it's %AppPath%
/// environment variables) or decide on what default to use. For Windows it's %AppPath%
/// for everything else the XDG approach is followed:
/// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
/// </summary>
@ -191,7 +200,9 @@ namespace Jellyfin.Server
if (string.IsNullOrEmpty(dataDir))
{
// LocalApplicationData follows the XDG spec on unix machines
dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "jellyfin");
dataDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"jellyfin");
}
}
@ -210,20 +221,26 @@ namespace Jellyfin.Server
if (string.IsNullOrEmpty(configDir))
{
if (options.DataDir != null || Directory.Exists(Path.Combine(dataDir, "config")) || RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (options.DataDir != null
|| Directory.Exists(Path.Combine(dataDir, "config"))
|| RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Hang config folder off already set dataDir
configDir = Path.Combine(dataDir, "config");
}
else
{
// $XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored.
// $XDG_CONFIG_HOME defines the base directory relative to which
// user specific configuration files should be stored.
configDir = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
// If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME /.config should be used.
// If $XDG_CONFIG_HOME is either not set or empty,
// a default equal to $HOME /.config should be used.
if (string.IsNullOrEmpty(configDir))
{
configDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
configDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".config");
}
configDir = Path.Combine(configDir, "jellyfin");
@ -251,13 +268,17 @@ namespace Jellyfin.Server
}
else
{
// $XDG_CACHE_HOME defines the base directory relative to which user specific non-essential data files should be stored.
// $XDG_CACHE_HOME defines the base directory relative to which
// user specific non-essential data files should be stored.
cacheDir = Environment.GetEnvironmentVariable("XDG_CACHE_HOME");
// If $XDG_CACHE_HOME is either not set or empty, a default equal to $HOME/.cache should be used.
// If $XDG_CACHE_HOME is either not set or empty,
// a default equal to $HOME/.cache should be used.
if (string.IsNullOrEmpty(cacheDir))
{
cacheDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cache");
cacheDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".cache");
}
cacheDir = Path.Combine(cacheDir, "jellyfin");
@ -366,17 +387,22 @@ namespace Jellyfin.Server
}
private static IImageEncoder GetImageEncoder(
IFileSystem fileSystem,
IApplicationPaths appPaths,
ILocalizationManager localizationManager)
{
try
{
return new SkiaEncoder(_loggerFactory, appPaths, fileSystem, localizationManager);
// Test if the native lib is available
SkiaEncoder.TestSkia();
return new SkiaEncoder(
_loggerFactory.CreateLogger<SkiaEncoder>(),
appPaths,
localizationManager);
}
catch (Exception ex)
{
_logger.LogInformation(ex, "Skia not available. Will fallback to NullIMageEncoder. {0}");
_logger.LogWarning(ex, "Skia not available. Will fallback to NullIMageEncoder.");
}
return new NullImageEncoder();
@ -390,7 +416,7 @@ namespace Jellyfin.Server
if (string.IsNullOrWhiteSpace(module))
{
module = Environment.GetCommandLineArgs().First();
module = Environment.GetCommandLineArgs()[0];
}
string commandLineArgsString;
@ -402,7 +428,7 @@ namespace Jellyfin.Server
else
{
commandLineArgsString = string.Join(
" ",
' ',
Environment.GetCommandLineArgs().Skip(1).Select(NormalizeCommandLineArgument));
}

View file

@ -415,7 +415,7 @@ namespace MediaBrowser.Api
public void OnTranscodeEndRequest(TranscodingJob job)
{
job.ActiveRequestCount--;
//Logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
Logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={0}", job.ActiveRequestCount);
if (job.ActiveRequestCount <= 0)
{
PingTimer(job, false);
@ -428,7 +428,7 @@ namespace MediaBrowser.Api
throw new ArgumentNullException(nameof(playSessionId));
}
//Logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
Logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused);
List<TranscodingJob> jobs;
@ -443,7 +443,7 @@ namespace MediaBrowser.Api
{
if (isUserPaused.HasValue)
{
//Logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
Logger.LogDebug("Setting job.IsUserPaused to {0}. jobId: {1}", isUserPaused, job.Id);
job.IsUserPaused = isUserPaused.Value;
}
PingTimer(job, true);
@ -601,7 +601,6 @@ namespace MediaBrowser.Api
{
Logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path);
//process.Kill();
process.StandardInput.WriteLine("q");
// Need to wait because killing is asynchronous
@ -701,7 +700,7 @@ namespace MediaBrowser.Api
{
try
{
//Logger.LogDebug("Deleting HLS file {0}", file);
Logger.LogDebug("Deleting HLS file {0}", file);
_fileSystem.DeleteFile(file);
}
catch (FileNotFoundException)
@ -840,12 +839,12 @@ namespace MediaBrowser.Api
{
if (KillTimer == null)
{
//Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite);
}
else
{
//Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
KillTimer.Change(intervalMs, Timeout.Infinite);
}
}
@ -864,7 +863,7 @@ namespace MediaBrowser.Api
{
var intervalMs = PingTimeout;
//Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId);
KillTimer.Change(intervalMs, Timeout.Infinite);
}
}

View file

@ -490,18 +490,6 @@ namespace MediaBrowser.Api.Library
{
return false;
}
else if (string.Equals(name, "FanArt", StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return true;
}
else if (string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase))
{
return true;
@ -999,19 +987,16 @@ namespace MediaBrowser.Api.Library
/// Posts the specified request.
/// </summary>
/// <param name="request">The request.</param>
public void Post(RefreshLibrary request)
public async Task Post(RefreshLibrary request)
{
Task.Run(() =>
try
{
try
{
_libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error refreshing library");
}
});
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error refreshing library");
}
}
/// <summary>

View file

@ -8,7 +8,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna;
@ -16,7 +15,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Diagnostics;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@ -32,6 +30,8 @@ namespace MediaBrowser.Api.Playback
/// </summary>
public abstract class BaseStreamingService : BaseApiService
{
protected virtual bool EnableOutputInSubFolder => false;
/// <summary>
/// Gets or sets the application paths.
/// </summary>
@ -65,15 +65,25 @@ namespace MediaBrowser.Api.Playback
protected IFileSystem FileSystem { get; private set; }
protected IDlnaManager DlnaManager { get; private set; }
protected IDeviceManager DeviceManager { get; private set; }
protected ISubtitleEncoder SubtitleEncoder { get; private set; }
protected IMediaSourceManager MediaSourceManager { get; private set; }
protected IJsonSerializer JsonSerializer { get; private set; }
protected IAuthorizationContext AuthorizationContext { get; private set; }
protected EncodingHelper EncodingHelper { get; set; }
/// <summary>
/// Gets the type of the transcoding job.
/// </summary>
/// <value>The type of the transcoding job.</value>
protected abstract TranscodingJobType TranscodingJobType { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BaseStreamingService" /> class.
/// </summary>
@ -112,12 +122,6 @@ namespace MediaBrowser.Api.Playback
/// </summary>
protected abstract string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding);
/// <summary>
/// Gets the type of the transcoding job.
/// </summary>
/// <value>The type of the transcoding job.</value>
protected abstract TranscodingJobType TranscodingJobType { get; }
/// <summary>
/// Gets the output file extension.
/// </summary>
@ -133,31 +137,24 @@ namespace MediaBrowser.Api.Playback
/// </summary>
private string GetOutputFilePath(StreamState state, EncodingOptions encodingOptions, string outputFileExtension)
{
var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath;
var data = GetCommandLineArguments("dummy\\dummy", encodingOptions, state, false);
data += "-" + (state.Request.DeviceId ?? string.Empty);
data += "-" + (state.Request.PlaySessionId ?? string.Empty);
data += "-" + (state.Request.DeviceId ?? string.Empty)
+ "-" + (state.Request.PlaySessionId ?? string.Empty);
var dataHash = data.GetMD5().ToString("N");
var filename = data.GetMD5().ToString("N");
var ext = outputFileExtension.ToLowerInvariant();
var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath;
if (EnableOutputInSubFolder)
{
return Path.Combine(folder, dataHash, dataHash + (outputFileExtension ?? string.Empty).ToLowerInvariant());
return Path.Combine(folder, filename, filename + ext);
}
return Path.Combine(folder, dataHash + (outputFileExtension ?? string.Empty).ToLowerInvariant());
return Path.Combine(folder, filename + ext);
}
protected virtual bool EnableOutputInSubFolder => false;
protected readonly CultureInfo UsCulture = new CultureInfo("en-US");
protected virtual string GetDefaultH264Preset()
{
return "superfast";
}
protected virtual string GetDefaultH264Preset() => "superfast";
private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource)
{
@ -171,7 +168,6 @@ namespace MediaBrowser.Api.Playback
var liveStreamResponse = await MediaSourceManager.OpenLiveStream(new LiveStreamRequest
{
OpenToken = state.MediaSource.OpenToken
}, cancellationTokenSource.Token).ConfigureAwait(false);
EncodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
@ -209,22 +205,16 @@ namespace MediaBrowser.Api.Playback
if (state.VideoRequest != null && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
if (auth.User != null)
if (auth.User != null && !auth.User.Policy.EnableVideoPlaybackTranscoding)
{
if (!auth.User.Policy.EnableVideoPlaybackTranscoding)
{
ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);
throw new ArgumentException("User does not have access to video transcoding");
}
throw new ArgumentException("User does not have access to video transcoding");
}
}
var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
var transcodingId = Guid.NewGuid().ToString("N");
var commandLineArgs = GetCommandLineArguments(outputPath, encodingOptions, state, true);
var process = new Process()
{
StartInfo = new ProcessStartInfo()
@ -239,7 +229,7 @@ namespace MediaBrowser.Api.Playback
RedirectStandardInput = true,
FileName = MediaEncoder.EncoderPath,
Arguments = commandLineArgs,
Arguments = GetCommandLineArguments(outputPath, encodingOptions, state, true),
WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? null : workingDirectory,
ErrorDialog = false
@ -250,7 +240,7 @@ namespace MediaBrowser.Api.Playback
var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath,
state.Request.PlaySessionId,
state.MediaSource.LiveStreamId,
transcodingId,
Guid.NewGuid().ToString("N"),
TranscodingJobType,
process,
state.Request.DeviceId,
@ -261,27 +251,26 @@ namespace MediaBrowser.Api.Playback
Logger.LogInformation(commandLineLogMessage);
var logFilePrefix = "ffmpeg-transcode";
if (state.VideoRequest != null)
if (state.VideoRequest != null
&& string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
logFilePrefix = "ffmpeg-directstream";
}
else if (string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
else
{
logFilePrefix = "ffmpeg-remux";
}
}
var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
state.LogFileStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true);
Stream logStream = FileSystem.GetFileStream(logFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(Request.AbsoluteUri + Environment.NewLine + Environment.NewLine + JsonSerializer.SerializeToString(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
await state.LogFileStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
await logStream.WriteAsync(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length, cancellationTokenSource.Token).ConfigureAwait(false);
process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state);
@ -298,13 +287,10 @@ namespace MediaBrowser.Api.Playback
throw;
}
// MUST read both stdout and stderr asynchronously or a deadlock may occurr
//process.BeginOutputReadLine();
state.TranscodingJob = transcodingJob;
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, state.LogFileStream);
_ = new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
// Wait for the file to exist before proceeeding
while (!File.Exists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited)
@ -368,25 +354,16 @@ namespace MediaBrowser.Api.Playback
Logger.LogDebug("Disposing stream resources");
state.Dispose();
try
if (process.ExitCode == 0)
{
Logger.LogInformation("FFMpeg exited with code {0}", process.ExitCode);
Logger.LogInformation("FFMpeg exited with code 0");
}
catch
else
{
Logger.LogError("FFMpeg exited with an error.");
Logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
}
// This causes on exited to be called twice:
//try
//{
// // Dispose the process
// process.Dispose();
//}
//catch (Exception ex)
//{
// Logger.LogError(ex, "Error disposing ffmpeg.");
//}
process.Dispose();
}
/// <summary>
@ -439,55 +416,55 @@ namespace MediaBrowser.Api.Playback
{
if (videoRequest != null)
{
videoRequest.AudioStreamIndex = int.Parse(val, UsCulture);
videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
}
}
else if (i == 7)
{
if (videoRequest != null)
{
videoRequest.SubtitleStreamIndex = int.Parse(val, UsCulture);
videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture);
}
}
else if (i == 8)
{
if (videoRequest != null)
{
videoRequest.VideoBitRate = int.Parse(val, UsCulture);
videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture);
}
}
else if (i == 9)
{
request.AudioBitRate = int.Parse(val, UsCulture);
request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture);
}
else if (i == 10)
{
request.MaxAudioChannels = int.Parse(val, UsCulture);
request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
}
else if (i == 11)
{
if (videoRequest != null)
{
videoRequest.MaxFramerate = float.Parse(val, UsCulture);
videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture);
}
}
else if (i == 12)
{
if (videoRequest != null)
{
videoRequest.MaxWidth = int.Parse(val, UsCulture);
videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture);
}
}
else if (i == 13)
{
if (videoRequest != null)
{
videoRequest.MaxHeight = int.Parse(val, UsCulture);
videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture);
}
}
else if (i == 14)
{
request.StartTimeTicks = long.Parse(val, UsCulture);
request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture);
}
else if (i == 15)
{
@ -500,14 +477,14 @@ namespace MediaBrowser.Api.Playback
{
if (videoRequest != null)
{
videoRequest.MaxRefFrames = int.Parse(val, UsCulture);
videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture);
}
}
else if (i == 17)
{
if (videoRequest != null)
{
videoRequest.MaxVideoBitDepth = int.Parse(val, UsCulture);
videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture);
}
}
else if (i == 18)
@ -556,7 +533,7 @@ namespace MediaBrowser.Api.Playback
}
else if (i == 26)
{
request.TranscodingMaxAudioChannels = int.Parse(val, UsCulture);
request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture);
}
else if (i == 27)
{
@ -643,16 +620,25 @@ namespace MediaBrowser.Api.Playback
return null;
}
if (value.IndexOf("npt=", StringComparison.OrdinalIgnoreCase) != 0)
const string Npt = "npt=";
if (!value.StartsWith(Npt, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException("Invalid timeseek header");
}
value = value.Substring(4).Split(new[] { '-' }, 2)[0];
int index = value.IndexOf('-');
if (index == -1)
{
value = value.Substring(Npt.Length);
}
else
{
value = value.Substring(Npt.Length, index);
}
if (value.IndexOf(':') == -1)
{
// Parses npt times in the format of '417.33'
if (double.TryParse(value, NumberStyles.Any, UsCulture, out var seconds))
if (double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
{
return TimeSpan.FromSeconds(seconds).Ticks;
}
@ -667,7 +653,7 @@ namespace MediaBrowser.Api.Playback
foreach (var time in tokens)
{
if (double.TryParse(time, NumberStyles.Any, UsCulture, out var digit))
if (double.TryParse(time, NumberStyles.Any, CultureInfo.InvariantCulture, out var digit))
{
secondsSum += digit * timeFactor;
}
@ -707,7 +693,7 @@ namespace MediaBrowser.Api.Playback
var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) /*||
string.Equals(Request.Headers.Get("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase)*/;
var state = new StreamState(MediaSourceManager, Logger, TranscodingJobType)
var state = new StreamState(MediaSourceManager, TranscodingJobType)
{
Request = request,
RequestedUrl = url,
@ -728,13 +714,10 @@ namespace MediaBrowser.Api.Playback
// state.SegmentLength = 6;
//}
if (state.VideoRequest != null)
if (state.VideoRequest != null && !string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
{
if (!string.IsNullOrWhiteSpace(state.VideoRequest.VideoCodec))
{
state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
}
state.SupportedVideoCodecs = state.VideoRequest.VideoCodec.Split(',').Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
state.VideoRequest.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
}
if (!string.IsNullOrWhiteSpace(request.AudioCodec))
@ -779,12 +762,12 @@ namespace MediaBrowser.Api.Playback
var mediaSources = (await MediaSourceManager.GetPlayackMediaSources(LibraryManager.GetItemById(request.Id), null, false, false, cancellationToken).ConfigureAwait(false)).ToList();
mediaSource = string.IsNullOrEmpty(request.MediaSourceId)
? mediaSources.First()
: mediaSources.FirstOrDefault(i => string.Equals(i.Id, request.MediaSourceId));
? mediaSources[0]
: mediaSources.Find(i => string.Equals(i.Id, request.MediaSourceId));
if (mediaSource == null && request.MediaSourceId.Equals(request.Id))
{
mediaSource = mediaSources.First();
mediaSource = mediaSources[0];
}
}
}
@ -834,11 +817,11 @@ namespace MediaBrowser.Api.Playback
if (state.OutputVideoBitrate.HasValue && !string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase))
{
var resolution = ResolutionNormalizer.Normalize(
state.VideoStream == null ? (int?)null : state.VideoStream.BitRate,
state.VideoStream == null ? (int?)null : state.VideoStream.Width,
state.VideoStream == null ? (int?)null : state.VideoStream.Height,
state.VideoStream?.BitRate,
state.VideoStream?.Width,
state.VideoStream?.Height,
state.OutputVideoBitrate.Value,
state.VideoStream == null ? null : state.VideoStream.Codec,
state.VideoStream?.Codec,
state.OutputVideoCodec,
videoRequest.MaxWidth,
videoRequest.MaxHeight);
@ -846,17 +829,13 @@ namespace MediaBrowser.Api.Playback
videoRequest.MaxWidth = resolution.MaxWidth;
videoRequest.MaxHeight = resolution.MaxHeight;
}
}
ApplyDeviceProfileSettings(state);
}
else
{
ApplyDeviceProfileSettings(state);
}
ApplyDeviceProfileSettings(state);
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
? GetOutputFileExtension(state)
: ("." + state.OutputContainer);
: ('.' + state.OutputContainer);
var encodingOptions = ApiEntryPoint.Instance.GetEncodingOptions();
@ -970,18 +949,18 @@ namespace MediaBrowser.Api.Playback
responseHeaders["transferMode.dlna.org"] = string.IsNullOrEmpty(transferMode) ? "Streaming" : transferMode;
responseHeaders["realTimeInfo.dlna.org"] = "DLNA.ORG_TLAG=*";
if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase))
if (state.RunTimeTicks.HasValue)
{
if (state.RunTimeTicks.HasValue)
if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase))
{
var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds;
responseHeaders["MediaInfo.sec"] = string.Format("SEC_Duration={0};", Convert.ToInt32(ms).ToString(CultureInfo.InvariantCulture));
}
}
if (state.RunTimeTicks.HasValue && !isStaticallyStreamed && profile != null)
{
AddTimeSeekResponseHeaders(state, responseHeaders);
if (!isStaticallyStreamed && profile != null)
{
AddTimeSeekResponseHeaders(state, responseHeaders);
}
}
if (profile == null)
@ -1046,8 +1025,8 @@ namespace MediaBrowser.Api.Playback
private void AddTimeSeekResponseHeaders(StreamState state, IDictionary<string, string> responseHeaders)
{
var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(UsCulture);
var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(UsCulture);
var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture);
var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture);
responseHeaders["TimeSeekRange.dlna.org"] = string.Format("npt={0}-{1}/{1}", startSeconds, runtimeSeconds);
responseHeaders["X-AvailableSeekRange"] = string.Format("1 npt={0}-{1}", startSeconds, runtimeSeconds);

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
@ -143,10 +144,10 @@ namespace MediaBrowser.Api.Playback.Hls
text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT");
var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(UsCulture);
var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(UsCulture), newDuration, StringComparison.OrdinalIgnoreCase);
//text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(UsCulture), newDuration, StringComparison.OrdinalIgnoreCase);
text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
//text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
return text;
}
@ -163,7 +164,7 @@ namespace MediaBrowser.Api.Playback.Hls
var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
// Main stream
builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(UsCulture));
builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(CultureInfo.InvariantCulture));
var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
builder.AppendLine(playlistUrl);
@ -231,7 +232,7 @@ namespace MediaBrowser.Api.Playback.Hls
{
var itsOffsetMs = 0;
var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(UsCulture));
var itsOffset = itsOffsetMs == 0 ? string.Empty : string.Format("-itsoffset {0} ", TimeSpan.FromMilliseconds(itsOffsetMs).TotalSeconds.ToString(CultureInfo.InvariantCulture));
var videoCodec = EncodingHelper.GetVideoEncoder(state, encodingOptions);
@ -240,7 +241,7 @@ namespace MediaBrowser.Api.Playback.Hls
var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions);
// If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(UsCulture) : "0";
var startNumberParam = isEncoding ? GetStartNumber(state).ToString(CultureInfo.InvariantCulture) : "0";
var baseUrlParam = string.Empty;
@ -272,7 +273,7 @@ namespace MediaBrowser.Api.Playback.Hls
EncodingHelper.GetMapArgs(state),
GetVideoArguments(state, encodingOptions),
GetAudioArguments(state, encodingOptions),
state.SegmentLength.ToString(UsCulture),
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
startNumberParam,
outputPath,
outputTsArg,
@ -293,9 +294,9 @@ namespace MediaBrowser.Api.Playback.Hls
EncodingHelper.GetMapArgs(state),
GetVideoArguments(state, encodingOptions),
GetAudioArguments(state, encodingOptions),
state.SegmentLength.ToString(UsCulture),
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
startNumberParam,
state.HlsListSize.ToString(UsCulture),
state.HlsListSize.ToString(CultureInfo.InvariantCulture),
baseUrlParam,
outputPath
).Trim();

View file

@ -177,7 +177,7 @@ namespace MediaBrowser.Api.Playback.Hls
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, UsCulture);
var requestedIndex = int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture);
var state = await GetState(request, cancellationToken).ConfigureAwait(false);
@ -364,7 +364,7 @@ namespace MediaBrowser.Api.Playback.Hls
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
return int.Parse(indexString, NumberStyles.Integer, UsCulture);
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
@ -438,7 +438,7 @@ namespace MediaBrowser.Api.Playback.Hls
segmentId = segmentRequest.SegmentId;
}
return int.Parse(segmentId, NumberStyles.Integer, UsCulture);
return int.Parse(segmentId, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
private string GetSegmentPath(StreamState state, string playlist, int index)
@ -447,7 +447,7 @@ namespace MediaBrowser.Api.Playback.Hls
var filename = Path.GetFileNameWithoutExtension(playlist);
return Path.Combine(folder, filename + index.ToString(UsCulture) + GetSegmentFileExtension(state.Request));
return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request));
}
private async Task<object> GetSegmentResult(StreamState state,
@ -628,8 +628,8 @@ namespace MediaBrowser.Api.Playback.Hls
private string ReplaceBitrate(string url, int oldValue, int newValue)
{
return url.Replace(
"videobitrate=" + oldValue.ToString(UsCulture),
"videobitrate=" + newValue.ToString(UsCulture),
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
StringComparison.OrdinalIgnoreCase);
}
@ -648,8 +648,8 @@ namespace MediaBrowser.Api.Playback.Hls
var url = string.Format("{0}/Subtitles/{1}/subtitles.m3u8?SegmentLength={2}&api_key={3}",
state.Request.MediaSourceId,
stream.Index.ToString(UsCulture),
30.ToString(UsCulture),
stream.Index.ToString(CultureInfo.InvariantCulture),
30.ToString(CultureInfo.InvariantCulture),
AuthorizationContext.GetAuthorizationInfo(Request).Token);
var line = string.Format(format,
@ -705,7 +705,7 @@ namespace MediaBrowser.Api.Playback.Hls
private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup)
{
var header = "#EXT-X-STREAM-INF:BANDWIDTH=" + bitrate.ToString(UsCulture) + ",AVERAGE-BANDWIDTH=" + bitrate.ToString(UsCulture);
var header = "#EXT-X-STREAM-INF:BANDWIDTH=" + bitrate.ToString(CultureInfo.InvariantCulture) + ",AVERAGE-BANDWIDTH=" + bitrate.ToString(CultureInfo.InvariantCulture);
// tvos wants resolution, codecs, framerate
//if (state.TargetFramerate.HasValue)
@ -770,7 +770,7 @@ namespace MediaBrowser.Api.Playback.Hls
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
builder.AppendLine("#EXT-X-VERSION:3");
builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(UsCulture));
builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
var queryStringIndex = Request.RawUrl.IndexOf('?');
@ -785,12 +785,12 @@ namespace MediaBrowser.Api.Playback.Hls
foreach (var length in segmentLengths)
{
builder.AppendLine("#EXTINF:" + length.ToString("0.0000", UsCulture) + ", nodesc");
builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}",
name,
index.ToString(UsCulture),
index.ToString(CultureInfo.InvariantCulture),
GetSegmentFileExtension(request),
queryString));
@ -821,17 +821,17 @@ namespace MediaBrowser.Api.Playback.Hls
if (state.OutputAudioBitrate.HasValue)
{
audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(UsCulture));
audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
}
if (state.OutputAudioChannels.HasValue)
{
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(UsCulture));
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
}
if (state.OutputAudioSampleRate.HasValue)
{
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture));
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
}
audioTranscodeParams.Add("-vn");
@ -863,12 +863,12 @@ namespace MediaBrowser.Api.Playback.Hls
if (bitrate.HasValue)
{
args += " -ab " + bitrate.Value.ToString(UsCulture);
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
{
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture);
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
@ -905,7 +905,7 @@ namespace MediaBrowser.Api.Playback.Hls
else
{
var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
state.SegmentLength.ToString(UsCulture));
state.SegmentLength.ToString(CultureInfo.InvariantCulture));
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
@ -953,7 +953,7 @@ namespace MediaBrowser.Api.Playback.Hls
// If isEncoding is true we're actually starting ffmpeg
var startNumber = GetStartNumber(state);
var startNumberParam = isEncoding ? startNumber.ToString(UsCulture) : "0";
var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
var mapArgs = state.IsOutputVideo ? EncodingHelper.GetMapArgs(state) : string.Empty;
@ -984,7 +984,7 @@ namespace MediaBrowser.Api.Playback.Hls
mapArgs,
GetVideoArguments(state, encodingOptions),
GetAudioArguments(state, encodingOptions),
state.SegmentLength.ToString(UsCulture),
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
startNumberParam,
outputPath,
outputTsArg,

View file

@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@ -55,12 +56,12 @@ namespace MediaBrowser.Api.Playback.Hls
if (bitrate.HasValue)
{
args += " -ab " + bitrate.Value.ToString(UsCulture);
args += " -ab " + bitrate.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
{
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(UsCulture);
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
args += " " + EncodingHelper.GetAudioFilterParam(state, encodingOptions, true);
@ -100,7 +101,7 @@ namespace MediaBrowser.Api.Playback.Hls
else
{
var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
state.SegmentLength.ToString(UsCulture));
state.SegmentLength.ToString(CultureInfo.InvariantCulture));
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;

View file

@ -1,17 +1,14 @@
using System;
using System.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Api.Playback
{
public class StreamState : EncodingJobInfo, IDisposable
{
private readonly ILogger _logger;
private readonly IMediaSourceManager _mediaSourceManager;
private bool _disposed = false;
public string RequestedUrl { get; set; }
@ -30,11 +27,6 @@ namespace MediaBrowser.Api.Playback
public VideoStreamRequest VideoRequest => Request as VideoStreamRequest;
/// <summary>
/// Gets or sets the log file stream.
/// </summary>
/// <value>The log file stream.</value>
public Stream LogFileStream { get; set; }
public IDirectStreamProvider DirectStreamProvider { get; set; }
public string WaitForPath { get; set; }
@ -72,6 +64,7 @@ namespace MediaBrowser.Api.Playback
{
return 3;
}
return 6;
}
@ -94,82 +87,57 @@ namespace MediaBrowser.Api.Playback
public string UserAgent { get; set; }
public StreamState(IMediaSourceManager mediaSourceManager, ILogger logger, TranscodingJobType transcodingType)
: base(transcodingType)
{
_mediaSourceManager = mediaSourceManager;
_logger = logger;
}
public bool EstimateContentLength { get; set; }
public TranscodeSeekInfo TranscodeSeekInfo { get; set; }
public bool EnableDlnaHeaders { get; set; }
public override void Dispose()
{
DisposeTranscodingThrottler();
DisposeLogStream();
DisposeLiveStream();
TranscodingJob = null;
}
private void DisposeTranscodingThrottler()
{
if (TranscodingThrottler != null)
{
try
{
TranscodingThrottler.Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error disposing TranscodingThrottler");
}
TranscodingThrottler = null;
}
}
private void DisposeLogStream()
{
if (LogFileStream != null)
{
try
{
LogFileStream.Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error disposing log stream");
}
LogFileStream = null;
}
}
private async void DisposeLiveStream()
{
if (MediaSource.RequiresClosing && string.IsNullOrWhiteSpace(Request.LiveStreamId) && !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
{
try
{
await _mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error closing media source");
}
}
}
public DeviceProfile DeviceProfile { get; set; }
public TranscodingJob TranscodingJob;
public TranscodingJob TranscodingJob { get; set; }
public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType)
: base(transcodingType)
{
_mediaSourceManager = mediaSourceManager;
}
public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float framerate, double? percentComplete, long bytesTranscoded, int? bitRate)
{
ApiEntryPoint.Instance.ReportTranscodingProgress(TranscodingJob, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// REVIEW: Is this the right place for this?
if (MediaSource.RequiresClosing
&& string.IsNullOrWhiteSpace(Request.LiveStreamId)
&& !string.IsNullOrWhiteSpace(MediaSource.LiveStreamId))
{
_mediaSourceManager.CloseLiveStream(MediaSource.LiveStreamId).GetAwaiter().GetResult();
}
TranscodingThrottler?.Dispose();
}
TranscodingThrottler = null;
TranscodingJob = null;
_disposed = true;
}
}
}

View file

@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Events;

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;

View file

@ -214,6 +214,9 @@ namespace MediaBrowser.Api
{
[ApiMember(Name = "Name", IsRequired = true, DataType = "string", ParameterType = "body", Verb = "POST")]
public string Name { get; set; }
[ApiMember(Name = "Password", IsRequired = false, DataType = "string", ParameterType = "body", Verb = "POST")]
public string Password { get; set; }
}
[Route("/Users/ForgotPassword", "POST", Summary = "Initiates the forgot password process for a local user")]
@ -406,7 +409,6 @@ namespace MediaBrowser.Api
PasswordSha1 = request.Password,
RemoteEndPoint = Request.RemoteIp,
Username = request.Username
}).ConfigureAwait(false);
return ToOptimizedResult(result);
@ -501,27 +503,21 @@ namespace MediaBrowser.Api
}
}
/// <summary>
/// Posts the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public async Task<object> Post(CreateUserByName request)
{
var dtoUser = request;
var newUser = await _userManager.CreateUser(request.Name).ConfigureAwait(false);
var newUser = await _userManager.CreateUser(dtoUser.Name).ConfigureAwait(false);
// no need to authenticate password for new user
if (request.Password != null)
{
await _userManager.ChangePassword(newUser, request.Password).ConfigureAwait(false);
}
var result = _userManager.GetUserDto(newUser, Request.RemoteIp);
return ToOptimizedResult(result);
}
/// <summary>
/// Posts the specified request.
/// </summary>
/// <param name="request">The request.</param>
/// <returns>System.Object.</returns>
public async Task<object> Post(ForgotPassword request)
{
var isLocal = Request.IsLocal || _networkManager.IsInLocalNetwork(Request.RemoteIp);

View file

@ -25,11 +25,6 @@ namespace MediaBrowser.Common
/// <value>The device identifier.</value>
string SystemId { get; }
/// <summary>
/// Occurs when [application updated].
/// </summary>
event EventHandler<GenericEventArgs<PackageVersionInfo>> ApplicationUpdated;
/// <summary>
/// Gets or sets a value indicating whether this instance has pending kernel reload.
/// </summary>

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