commit
a40cb7bbd8
|
@ -19,9 +19,9 @@ jobs:
|
|||
vmImage: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
release:
|
||||
Release:
|
||||
BuildConfiguration: Release
|
||||
debug:
|
||||
Debug:
|
||||
BuildConfiguration: Debug
|
||||
maxParallel: 2
|
||||
steps:
|
||||
|
@ -31,32 +31,32 @@ jobs:
|
|||
persistCredentials: true
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: "Check out web"
|
||||
displayName: "Clone Web Client (Master, Release, or Tag)"
|
||||
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||
inputs:
|
||||
script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: "Check out web (PR)"
|
||||
displayName: "Clone Web Client (PR)"
|
||||
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
|
||||
inputs:
|
||||
script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
|
||||
|
||||
- task: NodeTool@0
|
||||
displayName: 'Install Node.js'
|
||||
displayName: 'Install Node'
|
||||
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: "Build Web UI"
|
||||
displayName: "Build Web Client"
|
||||
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||
inputs:
|
||||
script: yarn install
|
||||
workingDirectory: $(Agent.TempDirectory)/jellyfin-web
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy the web UI
|
||||
displayName: 'Copy Web Client'
|
||||
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||
inputs:
|
||||
sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional
|
||||
|
@ -66,8 +66,14 @@ jobs:
|
|||
overWrite: true # Optional
|
||||
flattenFolders: false # Optional
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Update DotNet'
|
||||
inputs:
|
||||
packageType: sdk
|
||||
version: 3.1.100
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Publish
|
||||
displayName: 'Publish Server'
|
||||
inputs:
|
||||
command: publish
|
||||
publishWebProjects: false
|
||||
|
@ -135,62 +141,20 @@ jobs:
|
|||
!**\obj\**
|
||||
!**\xunit.runner.visualstudio.testadapter.dll
|
||||
!**\xunit.runner.visualstudio.dotnetcore.testadapter.dll
|
||||
#testPlan: # Required when testSelector == TestPlan
|
||||
#testSuite: # Required when testSelector == TestPlan
|
||||
#testConfiguration: # Required when testSelector == TestPlan
|
||||
#tcmTestRun: '$(test.RunId)' # Optional
|
||||
searchFolder: '$(System.DefaultWorkingDirectory)'
|
||||
#testFiltercriteria: # Optional
|
||||
#runOnlyImpactedTests: False # Optional
|
||||
#runAllTestsAfterXBuilds: '50' # Optional
|
||||
#uiTests: false # Optional
|
||||
#vstestLocationMethod: 'version' # Optional. Options: version, location
|
||||
#vsTestVersion: 'latest' # Optional. Options: latest, 16.0, 15.0, 14.0, toolsInstaller
|
||||
#vstestLocation: # Optional
|
||||
#runSettingsFile: # Optional
|
||||
#overrideTestrunParameters: # Optional
|
||||
#pathtoCustomTestAdapters: # Optional
|
||||
runInParallel: True # Optional
|
||||
runTestsInIsolation: True # Optional
|
||||
codeCoverageEnabled: True # Optional
|
||||
#otherConsoleOptions: # Optional
|
||||
#distributionBatchType: 'basedOnTestCases' # Optional. Options: basedOnTestCases, basedOnExecutionTime, basedOnAssembly
|
||||
#batchingBasedOnAgentsOption: 'autoBatchSize' # Optional. Options: autoBatchSize, customBatchSize
|
||||
#customBatchSizeValue: '10' # Required when distributionBatchType == BasedOnTestCases && BatchingBasedOnAgentsOption == CustomBatchSize
|
||||
#batchingBasedOnExecutionTimeOption: 'autoBatchSize' # Optional. Options: autoBatchSize, customTimeBatchSize
|
||||
#customRunTimePerBatchValue: '60' # Required when distributionBatchType == BasedOnExecutionTime && BatchingBasedOnExecutionTimeOption == CustomTimeBatchSize
|
||||
#dontDistribute: False # Optional
|
||||
#testRunTitle: # Optional
|
||||
#platform: # Optional
|
||||
configuration: 'Debug' # Optional
|
||||
publishRunAttachments: true # Optional
|
||||
#diagnosticsEnabled: false # Optional
|
||||
#collectDumpOn: 'onAbortOnly' # Optional. Options: onAbortOnly, always, never
|
||||
#rerunFailedTests: False # Optional
|
||||
#rerunType: 'basedOnTestFailurePercentage' # Optional. Options: basedOnTestFailurePercentage, basedOnTestFailureCount
|
||||
#rerunFailedThreshold: '30' # Optional
|
||||
#rerunFailedTestCasesMaxLimit: '5' # Optional
|
||||
#rerunMaxAttempts: '3' # Optional
|
||||
|
||||
# - task: PublishTestResults@2
|
||||
# inputs:
|
||||
# testResultsFormat: 'VSTest' # Options: JUnit, NUnit, VSTest, xUnit, cTest
|
||||
# testResultsFiles: '**/*.trx'
|
||||
# #searchFolder: '$(System.DefaultWorkingDirectory)' # Optional
|
||||
# mergeTestResults: true # Optional
|
||||
# #failTaskOnFailedTests: false # Optional
|
||||
# #testRunTitle: # Optional
|
||||
# #buildPlatform: # Optional
|
||||
# #buildConfiguration: # Optional
|
||||
# #publishRunAttachments: true # Optional
|
||||
|
||||
- job: main_build_win
|
||||
displayName: Main Build Windows
|
||||
displayName: Publish Windows
|
||||
pool:
|
||||
vmImage: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
release:
|
||||
Release:
|
||||
BuildConfiguration: Release
|
||||
maxParallel: 2
|
||||
steps:
|
||||
|
@ -200,32 +164,32 @@ jobs:
|
|||
persistCredentials: true
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: "Check out web (master, release or tag)"
|
||||
displayName: "Clone Web Client (Master, Release, or Tag)"
|
||||
condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||
inputs:
|
||||
script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: "Check out web (PR)"
|
||||
displayName: "Clone Web Client (PR)"
|
||||
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest'))
|
||||
inputs:
|
||||
script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web'
|
||||
|
||||
- task: NodeTool@0
|
||||
displayName: 'Install Node.js'
|
||||
displayName: 'Install Node'
|
||||
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: "Build Web UI"
|
||||
displayName: "Build Web Client"
|
||||
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||
inputs:
|
||||
script: yarn install
|
||||
workingDirectory: $(Agent.TempDirectory)/jellyfin-web
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy the web UI
|
||||
displayName: 'Copy Web Client'
|
||||
condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')) ,eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion'))
|
||||
inputs:
|
||||
sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional
|
||||
|
@ -236,25 +200,21 @@ jobs:
|
|||
flattenFolders: false # Optional
|
||||
|
||||
- task: CmdLine@2
|
||||
displayName: Clone the UX repository
|
||||
displayName: 'Clone UX Repository'
|
||||
inputs:
|
||||
script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux
|
||||
|
||||
- task: PowerShell@2
|
||||
displayName: Build the NSIS Installer
|
||||
displayName: 'Build NSIS Installer'
|
||||
inputs:
|
||||
targetType: 'filePath' # Optional. Options: filePath, inline
|
||||
filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath
|
||||
arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory)
|
||||
#script: '# Write your PowerShell commands here.Write-Host Hello World' # Required when targetType == Inline
|
||||
errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue
|
||||
#failOnStderr: false # Optional
|
||||
#ignoreLASTEXITCODE: false # Optional
|
||||
#pwsh: false # Optional
|
||||
workingDirectory: $(Build.SourcesDirectory) # Optional
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: Copy the NSIS Installer to the artifact directory
|
||||
displayName: 'Copy NSIS Installer'
|
||||
inputs:
|
||||
sourceFolder: $(Build.SourcesDirectory)/deployment/windows/ # Optional
|
||||
contents: 'jellyfin*.exe'
|
||||
|
@ -264,7 +224,7 @@ jobs:
|
|||
flattenFolders: true # Optional
|
||||
|
||||
- task: PublishPipelineArtifact@0
|
||||
displayName: 'Publish Setup Artifact'
|
||||
displayName: 'Publish Artifact Setup'
|
||||
condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded())
|
||||
inputs:
|
||||
targetPath: '$(build.artifactstagingdirectory)/setup'
|
||||
|
@ -275,7 +235,8 @@ jobs:
|
|||
pool:
|
||||
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)
|
||||
# only execute for pull requests
|
||||
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
|
||||
strategy:
|
||||
matrix:
|
||||
Naming:
|
||||
|
@ -293,24 +254,23 @@ jobs:
|
|||
maxParallel: 2
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Update DotNet'
|
||||
inputs:
|
||||
packageType: sdk
|
||||
version: 3.1.100
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download the New Assembly Build Artifact
|
||||
displayName: 'Download New Assembly Build Artifact'
|
||||
inputs:
|
||||
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 New Assembly to new-release folder
|
||||
displayName: 'Copy New Assembly Build Artifact'
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts # Optional
|
||||
contents: '**/*.dll'
|
||||
|
@ -320,22 +280,18 @@ jobs:
|
|||
flattenFolders: true # Optional
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download the Reference Assembly Build Artifact
|
||||
displayName: 'Download 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
|
||||
displayName: 'Copy Reference Assembly Build Artifact'
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts # Optional
|
||||
contents: '**/*.dll'
|
||||
|
@ -345,27 +301,24 @@ jobs:
|
|||
flattenFolders: true # Optional
|
||||
|
||||
- task: DownloadGitHubRelease@0
|
||||
displayName: Download ABI compatibility check tool from GitHub
|
||||
displayName: 'Download ABI Compatibility Check Tool'
|
||||
inputs:
|
||||
connection: Jellyfin Release Download
|
||||
userRepository: EraYaN/dotnet-compatibility
|
||||
defaultVersionType: 'latest' # Options: latest, specificVersion, specificTag
|
||||
#version: # Required when defaultVersionType != Latest
|
||||
itemPattern: '**-ci.zip' # Optional
|
||||
downloadPath: '$(System.ArtifactsDirectory)'
|
||||
|
||||
- task: ExtractFiles@1
|
||||
displayName: Extract ABI compatibility check tool
|
||||
displayName: 'Extract ABI Compatibility Check Tool'
|
||||
inputs:
|
||||
archiveFilePatterns: '$(System.ArtifactsDirectory)/*-ci.zip'
|
||||
destinationFolder: $(System.ArtifactsDirectory)/tools
|
||||
cleanDestinationFolder: true
|
||||
|
||||
# The `--warnings-only` switch will swallow the return code and not emit any errors.
|
||||
- task: CmdLine@2
|
||||
displayName: Execute ABI compatibility check tool
|
||||
displayName: 'Execute ABI Compatibility Check Tool'
|
||||
inputs:
|
||||
script: 'dotnet tools/CompatibilityCheckerCoreCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines'
|
||||
script: 'dotnet tools/CompatibilityCheckerCLI.dll current-release/$(AssemblyFileName) new-release/$(AssemblyFileName) --azure-pipelines --warnings-only'
|
||||
workingDirectory: $(System.ArtifactsDirectory) # Optional
|
||||
#failOnStderr: false # Optional
|
||||
|
||||
|
||||
|
|
|
@ -103,14 +103,11 @@ using MediaBrowser.Providers.Subtitles;
|
|||
using MediaBrowser.Providers.TV.TheTVDB;
|
||||
using MediaBrowser.WebDashboard.Api;
|
||||
using MediaBrowser.XbmcMetadata.Providers;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
||||
|
||||
namespace Emby.Server.Implementations
|
||||
|
@ -878,6 +875,8 @@ namespace Emby.Server.Implementations
|
|||
serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager));
|
||||
serviceCollection.AddSingleton<EncodingHelper>();
|
||||
|
||||
serviceCollection.AddSingleton(typeof(IAttachmentExtractor), typeof(MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor));
|
||||
|
||||
_displayPreferencesRepository.Initialize();
|
||||
|
||||
var userDataRepo = new SqliteUserDataRepository(LoggerFactory.CreateLogger<SqliteUserDataRepository>(), ApplicationPaths);
|
||||
|
@ -1478,7 +1477,7 @@ namespace Emby.Server.Implementations
|
|||
/// </summary>
|
||||
/// <param name="address">The IPv6 address.</param>
|
||||
/// <returns>The IPv6 address without the scope id.</returns>
|
||||
private string RemoveScopeId(string address)
|
||||
private ReadOnlySpan<char> RemoveScopeId(ReadOnlySpan<char> address)
|
||||
{
|
||||
var index = address.IndexOf('%');
|
||||
if (index == -1)
|
||||
|
@ -1486,33 +1485,50 @@ namespace Emby.Server.Implementations
|
|||
return address;
|
||||
}
|
||||
|
||||
return address.Substring(0, index);
|
||||
return address.Slice(0, index);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetLocalApiUrl(IPAddress ipAddress)
|
||||
{
|
||||
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
var str = RemoveScopeId(ipAddress.ToString());
|
||||
Span<char> span = new char[str.Length + 2];
|
||||
span[0] = '[';
|
||||
str.CopyTo(span.Slice(1));
|
||||
span[^1] = ']';
|
||||
|
||||
return GetLocalApiUrl("[" + str + "]");
|
||||
return GetLocalApiUrl(span);
|
||||
}
|
||||
|
||||
return GetLocalApiUrl(ipAddress.ToString());
|
||||
}
|
||||
|
||||
public string GetLocalApiUrl(string host)
|
||||
/// <inheritdoc />
|
||||
public string GetLocalApiUrl(ReadOnlySpan<char> host)
|
||||
{
|
||||
var url = new StringBuilder(64);
|
||||
if (EnableHttps)
|
||||
{
|
||||
return string.Format("https://{0}:{1}",
|
||||
host,
|
||||
HttpsPort.ToString(CultureInfo.InvariantCulture));
|
||||
url.Append("https://");
|
||||
}
|
||||
else
|
||||
{
|
||||
url.Append("http://");
|
||||
}
|
||||
|
||||
return string.Format("http://{0}:{1}",
|
||||
host,
|
||||
HttpPort.ToString(CultureInfo.InvariantCulture));
|
||||
url.Append(host)
|
||||
.Append(':')
|
||||
.Append(HttpPort);
|
||||
|
||||
string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
|
||||
if (baseUrl.Length != 0)
|
||||
{
|
||||
url.Append('/').Append(baseUrl);
|
||||
}
|
||||
|
||||
return url.ToString();
|
||||
}
|
||||
|
||||
public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
|
||||
|
|
|
@ -49,6 +49,21 @@ namespace Emby.Server.Implementations.Data
|
|||
private readonly TypeMapper _typeMapper;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
static SqliteItemRepository()
|
||||
{
|
||||
var queryPrefixText = new StringBuilder();
|
||||
queryPrefixText.Append("insert into mediaattachments (");
|
||||
foreach (var column in _mediaAttachmentSaveColumns)
|
||||
{
|
||||
queryPrefixText.Append(column)
|
||||
.Append(',');
|
||||
}
|
||||
|
||||
queryPrefixText.Length -= 1;
|
||||
queryPrefixText.Append(") values ");
|
||||
_mediaAttachmentInsertPrefix = queryPrefixText.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqliteItemRepository"/> class.
|
||||
/// </summary>
|
||||
|
@ -92,6 +107,8 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
const string CreateMediaStreamsTableCommand
|
||||
= "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))";
|
||||
const string CreateMediaAttachmentsTableCommand
|
||||
= "create table if not exists mediaattachments (ItemId GUID, AttachmentIndex INT, Codec TEXT, CodecTag TEXT NULL, Comment TEXT NULL, Filename TEXT NULL, MIMEType TEXT NULL, PRIMARY KEY (ItemId, AttachmentIndex))";
|
||||
|
||||
string[] queries =
|
||||
{
|
||||
|
@ -114,6 +131,7 @@ namespace Emby.Server.Implementations.Data
|
|||
"create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))",
|
||||
|
||||
CreateMediaStreamsTableCommand,
|
||||
CreateMediaAttachmentsTableCommand,
|
||||
|
||||
"pragma shrink_memory"
|
||||
};
|
||||
|
@ -421,6 +439,19 @@ namespace Emby.Server.Implementations.Data
|
|||
"ColorTransfer"
|
||||
};
|
||||
|
||||
private static readonly string[] _mediaAttachmentSaveColumns =
|
||||
{
|
||||
"ItemId",
|
||||
"AttachmentIndex",
|
||||
"Codec",
|
||||
"CodecTag",
|
||||
"Comment",
|
||||
"Filename",
|
||||
"MIMEType"
|
||||
};
|
||||
|
||||
private static readonly string _mediaAttachmentInsertPrefix;
|
||||
|
||||
private static string GetSaveItemCommandText()
|
||||
{
|
||||
var saveColumns = new []
|
||||
|
@ -6136,5 +6167,175 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
|
||||
return item;
|
||||
}
|
||||
|
||||
public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
if (query == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
var cmdText = "select "
|
||||
+ string.Join(",", _mediaAttachmentSaveColumns)
|
||||
+ " from mediaattachments where"
|
||||
+ " ItemId=@ItemId";
|
||||
|
||||
if (query.Index.HasValue)
|
||||
{
|
||||
cmdText += " AND AttachmentIndex=@AttachmentIndex";
|
||||
}
|
||||
|
||||
cmdText += " order by AttachmentIndex ASC";
|
||||
|
||||
var list = new List<MediaAttachment>();
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = PrepareStatement(connection, cmdText))
|
||||
{
|
||||
statement.TryBind("@ItemId", query.ItemId.ToByteArray());
|
||||
|
||||
if (query.Index.HasValue)
|
||||
{
|
||||
statement.TryBind("@AttachmentIndex", query.Index.Value);
|
||||
}
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(GetMediaAttachment(row));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public void SaveMediaAttachments(
|
||||
Guid id,
|
||||
IReadOnlyList<MediaAttachment> attachments,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
CheckDisposed();
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
throw new ArgumentException(nameof(id));
|
||||
}
|
||||
|
||||
if (attachments == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attachments));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
var itemIdBlob = id.ToByteArray();
|
||||
|
||||
db.Execute("delete from mediaattachments where ItemId=@ItemId", itemIdBlob);
|
||||
|
||||
InsertMediaAttachments(itemIdBlob, attachments, db, cancellationToken);
|
||||
|
||||
}, TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertMediaAttachments(
|
||||
byte[] idBlob,
|
||||
IReadOnlyList<MediaAttachment> attachments,
|
||||
IDatabaseConnection db,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
const int InsertAtOnce = 10;
|
||||
|
||||
for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
|
||||
{
|
||||
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
|
||||
|
||||
var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
var index = i.ToString(CultureInfo.InvariantCulture);
|
||||
insertText.Append("(@ItemId, ");
|
||||
|
||||
foreach (var column in _mediaAttachmentSaveColumns.Skip(1))
|
||||
{
|
||||
insertText.Append("@" + column + index + ",");
|
||||
}
|
||||
|
||||
insertText.Length -= 1;
|
||||
|
||||
insertText.Append("),");
|
||||
}
|
||||
|
||||
insertText.Length--;
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var statement = PrepareStatement(db, insertText.ToString()))
|
||||
{
|
||||
statement.TryBind("@ItemId", idBlob);
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
var index = i.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
var attachment = attachments[i];
|
||||
|
||||
statement.TryBind("@AttachmentIndex" + index, attachment.Index);
|
||||
statement.TryBind("@Codec" + index, attachment.Codec);
|
||||
statement.TryBind("@CodecTag" + index, attachment.CodecTag);
|
||||
statement.TryBind("@Comment" + index, attachment.Comment);
|
||||
statement.TryBind("@FileName" + index, attachment.FileName);
|
||||
statement.TryBind("@MimeType" + index, attachment.MimeType);
|
||||
}
|
||||
|
||||
statement.Reset();
|
||||
statement.MoveNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the attachment.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <returns>MediaAttachment</returns>
|
||||
private MediaAttachment GetMediaAttachment(IReadOnlyList<IResultSetValue> reader)
|
||||
{
|
||||
var item = new MediaAttachment
|
||||
{
|
||||
Index = reader[1].ToInt()
|
||||
};
|
||||
|
||||
if (reader[2].SQLiteType != SQLiteType.Null)
|
||||
{
|
||||
item.Codec = reader[2].ToString();
|
||||
}
|
||||
|
||||
if (reader[2].SQLiteType != SQLiteType.Null)
|
||||
{
|
||||
item.CodecTag = reader[3].ToString();
|
||||
}
|
||||
|
||||
if (reader[4].SQLiteType != SQLiteType.Null)
|
||||
{
|
||||
item.Comment = reader[4].ToString();
|
||||
}
|
||||
|
||||
if (reader[6].SQLiteType != SQLiteType.Null)
|
||||
{
|
||||
item.FileName = reader[5].ToString();
|
||||
}
|
||||
|
||||
if (reader[6].SQLiteType != SQLiteType.Null)
|
||||
{
|
||||
item.MimeType = reader[6].ToString();
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
try
|
||||
{
|
||||
return Path.Combine(Path.GetFullPath(folderPath), filePath);
|
||||
return Path.GetFullPath(Path.Combine(folderPath, filePath));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
|
|
|
@ -130,6 +130,21 @@ namespace Emby.Server.Implementations.Library
|
|||
return streams;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query)
|
||||
{
|
||||
return _itemRepo.GetMediaAttachments(query);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<MediaAttachment> GetMediaAttachments(Guid itemId)
|
||||
{
|
||||
return GetMediaAttachments(new MediaAttachmentQuery
|
||||
{
|
||||
ItemId = itemId
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<List<MediaSourceInfo>> GetPlayackMediaSources(BaseItem item, User user, bool allowMediaProbe, bool enablePathSubstitution, CancellationToken cancellationToken)
|
||||
{
|
||||
var mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user);
|
||||
|
|
|
@ -19,10 +19,10 @@
|
|||
"HeaderCameraUploads": "Photos transférées",
|
||||
"HeaderContinueWatching": "Continuer à regarder",
|
||||
"HeaderFavoriteAlbums": "Albums favoris",
|
||||
"HeaderFavoriteArtists": "Artistes favoris",
|
||||
"HeaderFavoriteArtists": "Artistes préférés",
|
||||
"HeaderFavoriteEpisodes": "Épisodes favoris",
|
||||
"HeaderFavoriteShows": "Séries favorites",
|
||||
"HeaderFavoriteSongs": "Chansons favorites",
|
||||
"HeaderFavoriteSongs": "Chansons préférées",
|
||||
"HeaderLiveTV": "TV en direct",
|
||||
"HeaderNextUp": "À suivre",
|
||||
"HeaderRecordingGroups": "Groupes d'enregistrements",
|
||||
|
|
|
@ -1 +1,96 @@
|
|||
{}
|
||||
{
|
||||
"HeaderLiveTV": "TV ao Vivo",
|
||||
"Collections": "Colecções",
|
||||
"Books": "Livros",
|
||||
"Artists": "Artistas",
|
||||
"Albums": "Álbuns",
|
||||
"HeaderNextUp": "A Seguir",
|
||||
"HeaderFavoriteSongs": "Músicas Favoritas",
|
||||
"HeaderFavoriteArtists": "Artistas Favoritos",
|
||||
"HeaderFavoriteAlbums": "Álbuns Favoritos",
|
||||
"HeaderFavoriteEpisodes": "Episódios Favoritos",
|
||||
"HeaderFavoriteShows": "Séries Favoritas",
|
||||
"HeaderContinueWatching": "Continuar a Ver",
|
||||
"HeaderAlbumArtists": "Artistas do Álbum",
|
||||
"Genres": "Géneros",
|
||||
"Folders": "Pastas",
|
||||
"Favorites": "Favoritos",
|
||||
"Channels": "Canais",
|
||||
"UserDownloadingItemWithValues": "{0} está a transferir {1}",
|
||||
"VersionNumber": "Versão {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca multimédia",
|
||||
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} está a reproduzir {1} em {2}",
|
||||
"UserPolicyUpdatedWithName": "A política do utilizador {0} foi alterada",
|
||||
"UserPasswordChangedWithName": "A palavra-passe do utilizador {0} foi alterada",
|
||||
"UserOnlineFromDevice": "{0} ligou-se a partir de {1}",
|
||||
"UserOfflineFromDevice": "{0} desligou-se a partir de {1}",
|
||||
"UserLockedOutWithName": "Utilizador {0} bloqueado",
|
||||
"UserDeletedWithName": "Utilizador {0} removido",
|
||||
"UserCreatedWithName": "Utilizador {0} criado",
|
||||
"User": "Utilizador",
|
||||
"TvShows": "Programas",
|
||||
"System": "Sistema",
|
||||
"SubtitlesDownloadedForItem": "Legendas transferidas para {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}",
|
||||
"StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente dentro de momentos.",
|
||||
"ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciado",
|
||||
"ScheduledTaskStartedWithName": "{0} iniciou",
|
||||
"ScheduledTaskFailedWithName": "{0} falhou",
|
||||
"ProviderValue": "Fornecedor: {0}",
|
||||
"PluginUpdatedWithName": "{0} foi actualizado",
|
||||
"PluginUninstalledWithName": "{0} foi desinstalado",
|
||||
"PluginInstalledWithName": "{0} foi instalado",
|
||||
"Plugin": "Extensão",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodução de vídeo parada",
|
||||
"NotificationOptionVideoPlayback": "Reprodução de vídeo iniciada",
|
||||
"NotificationOptionUserLockedOut": "Utilizador bloqueado",
|
||||
"NotificationOptionTaskFailed": "Falha em tarefa agendada",
|
||||
"NotificationOptionServerRestartRequired": "É necessário reiniciar o servidor",
|
||||
"NotificationOptionPluginUpdateInstalled": "Extensão actualizada",
|
||||
"NotificationOptionPluginUninstalled": "Extensão desinstalada",
|
||||
"NotificationOptionPluginInstalled": "Extensão instalada",
|
||||
"NotificationOptionPluginError": "Falha na extensão",
|
||||
"NotificationOptionNewLibraryContent": "Novo conteúdo adicionado",
|
||||
"NotificationOptionInstallationFailed": "Falha de instalação",
|
||||
"NotificationOptionCameraImageUploaded": "Imagem da câmara enviada",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodução Parada",
|
||||
"NotificationOptionAudioPlayback": "Reprodução Iniciada",
|
||||
"NotificationOptionApplicationUpdateInstalled": "A actualização da aplicação foi instalada",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Uma actualização da aplicação está disponível",
|
||||
"NewVersionIsAvailable": "Uma nova versão do servidor Jellyfin está disponível para transferência.",
|
||||
"NameSeasonUnknown": "Temporada Desconhecida",
|
||||
"NameSeasonNumber": "Temporada {0}",
|
||||
"NameInstallFailed": "Falha na instalação de {0}",
|
||||
"MusicVideos": "Videoclips",
|
||||
"Music": "Música",
|
||||
"MixedContent": "Conteúdo Misto",
|
||||
"MessageServerConfigurationUpdated": "A configuração do servidor foi actualizada",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Configurações do servidor na secção {0} foram atualizadas",
|
||||
"MessageApplicationUpdatedTo": "O servidor Jellyfin foi actualizado para a versão {0}",
|
||||
"MessageApplicationUpdated": "O servidor Jellyfin foi actualizado",
|
||||
"Latest": "Mais Recente",
|
||||
"LabelRunningTimeValue": "Duração: {0}",
|
||||
"LabelIpAddressValue": "Endereço IP: {0}",
|
||||
"ItemRemovedWithName": "{0} foi removido da biblioteca",
|
||||
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
|
||||
"Inherit": "Herdar",
|
||||
"HomeVideos": "Vídeos Caseiros",
|
||||
"HeaderRecordingGroups": "Grupos de Gravação",
|
||||
"ValueSpecialEpisodeName": "Especial - {0}",
|
||||
"Sync": "Sincronização",
|
||||
"Songs": "Músicas",
|
||||
"Shows": "Séries",
|
||||
"Playlists": "Listas de Reprodução",
|
||||
"Photos": "Fotografias",
|
||||
"Movies": "Filmes",
|
||||
"HeaderCameraUploads": "Envios a partir da câmara",
|
||||
"FailedLoginAttemptWithUserName": "Tentativa de ligação a partir de {0} falhou",
|
||||
"DeviceOnlineWithName": "{0} ligou-se",
|
||||
"DeviceOfflineWithName": "{0} desligou-se",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticado com sucesso",
|
||||
"Application": "Aplicação",
|
||||
"AppDeviceValues": "Aplicação {0}, Dispositivo: {1}"
|
||||
}
|
||||
|
|
|
@ -96,7 +96,6 @@ namespace Jellyfin.Api.Controllers
|
|||
public StartupUserDto GetFirstUser()
|
||||
{
|
||||
var user = _userManager.Users.First();
|
||||
|
||||
return new StartupUserDto
|
||||
{
|
||||
Name = user.Name,
|
||||
|
|
63
MediaBrowser.Api/Attachments/AttachmentService.cs
Normal file
63
MediaBrowser.Api/Attachments/AttachmentService.cs
Normal file
|
@ -0,0 +1,63 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Api.Attachments
|
||||
{
|
||||
[Route("/Videos/{Id}/{MediaSourceId}/Attachments/{Index}", "GET", Summary = "Gets specified attachment.")]
|
||||
public class GetAttachment
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[ApiMember(Name = "MediaSourceId", Description = "MediaSourceId", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
[ApiMember(Name = "Index", Description = "The attachment stream index", IsRequired = true, DataType = "int", ParameterType = "path", Verb = "GET")]
|
||||
public int Index { get; set; }
|
||||
}
|
||||
|
||||
public class AttachmentService : BaseApiService
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IAttachmentExtractor _attachmentExtractor;
|
||||
|
||||
public AttachmentService(
|
||||
ILogger<AttachmentService> logger,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IHttpResultFactory httpResultFactory,
|
||||
ILibraryManager libraryManager,
|
||||
IAttachmentExtractor attachmentExtractor)
|
||||
: base(logger, serverConfigurationManager, httpResultFactory)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_attachmentExtractor = attachmentExtractor;
|
||||
}
|
||||
|
||||
public async Task<object> Get(GetAttachment request)
|
||||
{
|
||||
var (attachment, attachmentStream) = await GetAttachment(request).ConfigureAwait(false);
|
||||
var mime = string.IsNullOrWhiteSpace(attachment.MimeType) ? "application/octet-stream" : attachment.MimeType;
|
||||
|
||||
return ResultFactory.GetResult(Request, attachmentStream, mime);
|
||||
}
|
||||
|
||||
private Task<(MediaAttachment, Stream)> GetAttachment(GetAttachment request)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(request.Id);
|
||||
|
||||
return _attachmentExtractor.GetAttachment(item,
|
||||
request.MediaSourceId,
|
||||
request.Index,
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -572,6 +572,16 @@ namespace MediaBrowser.Api.Playback
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var attachment in mediaSource.MediaAttachments)
|
||||
{
|
||||
attachment.DeliveryUrl = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"/Videos/{0}/{1}/Attachments/{2}",
|
||||
item.Id,
|
||||
mediaSource.Id,
|
||||
attachment.Index);
|
||||
}
|
||||
}
|
||||
|
||||
private long? GetMaxBitrate(long? clientMaxBitrate, User user)
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace MediaBrowser.Common.Json.Converters
|
|||
/// <summary>
|
||||
/// Converts a GUID object or value to/from JSON.
|
||||
/// </summary>
|
||||
public class GuidConverter : JsonConverter<Guid>
|
||||
public class JsonGuidConverter : JsonConverter<Guid>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
53
MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs
Normal file
53
MediaBrowser.Common/Json/Converters/JsonInt32Converter.cs
Normal file
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.Common.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a GUID object or value to/from JSON.
|
||||
/// </summary>
|
||||
public class JsonInt32Converter : JsonConverter<int>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
static void ThrowFormatException() => throw new FormatException("Invalid format for an integer.");
|
||||
ReadOnlySpan<byte> span = stackalloc byte[0];
|
||||
|
||||
if (reader.HasValueSequence)
|
||||
{
|
||||
long sequenceLength = reader.ValueSequence.Length;
|
||||
Span<byte> stackSpan = stackalloc byte[(int)sequenceLength];
|
||||
reader.ValueSequence.CopyTo(stackSpan);
|
||||
span = stackSpan;
|
||||
}
|
||||
else
|
||||
{
|
||||
span = reader.ValueSpan;
|
||||
}
|
||||
|
||||
if (!Utf8Parser.TryParse(span, out int number, out _))
|
||||
{
|
||||
ThrowFormatException();
|
||||
}
|
||||
|
||||
return number;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
|
||||
{
|
||||
static void ThrowInvalidOperationException() => throw new InvalidOperationException();
|
||||
Span<byte> span = stackalloc byte[16];
|
||||
if (Utf8Formatter.TryFormat(value, span, out int bytesWritten))
|
||||
{
|
||||
writer.WriteStringValue(span.Slice(0, bytesWritten));
|
||||
}
|
||||
|
||||
ThrowInvalidOperationException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ namespace MediaBrowser.Common.Json
|
|||
WriteIndented = false
|
||||
};
|
||||
|
||||
options.Converters.Add(new GuidConverter());
|
||||
options.Converters.Add(new JsonGuidConverter());
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
|
||||
return options;
|
||||
|
|
|
@ -1098,6 +1098,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
Id = item.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
Protocol = protocol ?? MediaProtocol.File,
|
||||
MediaStreams = MediaSourceManager.GetMediaStreams(item.Id),
|
||||
MediaAttachments = MediaSourceManager.GetMediaAttachments(item.Id),
|
||||
Name = GetMediaSourceName(item),
|
||||
Path = enablePathSubstitution ? GetMappedPath(item, item.Path, protocol) : item.Path,
|
||||
RunTimeTicks = item.RunTimeTicks,
|
||||
|
|
|
@ -71,13 +71,15 @@ namespace MediaBrowser.Controller
|
|||
/// <summary>
|
||||
/// Gets the local API URL.
|
||||
/// </summary>
|
||||
/// <param name="host">The host.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string GetLocalApiUrl(string host);
|
||||
/// <param name="hostname">The hostname.</param>
|
||||
/// <returns>The local API URL.</returns>
|
||||
string GetLocalApiUrl(ReadOnlySpan<char> hostname);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local API URL.
|
||||
/// </summary>
|
||||
/// <param name="address">The IP address.</param>
|
||||
/// <returns>The local API URL.</returns>
|
||||
string GetLocalApiUrl(IPAddress address);
|
||||
|
||||
void LaunchUrl(string url);
|
||||
|
|
|
@ -38,6 +38,20 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <returns>IEnumerable<MediaStream>.</returns>
|
||||
List<MediaStream> GetMediaStreams(MediaStreamQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media attachments.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item identifier.</param>
|
||||
/// <returns>IEnumerable<MediaAttachment>.</returns>
|
||||
List<MediaAttachment> GetMediaAttachments(Guid itemId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media attachments.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>IEnumerable<MediaAttachment>.</returns>
|
||||
List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the playack media sources.
|
||||
/// </summary>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
public interface IAttachmentExtractor
|
||||
{
|
||||
Task<(MediaAttachment attachment, Stream stream)> GetAttachment(
|
||||
BaseItem item,
|
||||
string mediaSourceId,
|
||||
int attachmentStreamIndex,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
|
@ -78,6 +78,21 @@ namespace MediaBrowser.Controller.Persistence
|
|||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
void SaveMediaStreams(Guid id, List<MediaStream> streams, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media attachments.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>IEnumerable{MediaAttachment}.</returns>
|
||||
List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the media attachments.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="attachments">The attachments.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
void SaveMediaAttachments(Guid id, IReadOnlyList<MediaAttachment> attachments, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item ids.
|
||||
/// </summary>
|
||||
|
|
20
MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs
Normal file
20
MediaBrowser.Controller/Persistence/MediaAttachmentQuery.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using System;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Persistence
|
||||
{
|
||||
public class MediaAttachmentQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the index.
|
||||
/// </summary>
|
||||
/// <value>The index.</value>
|
||||
public int? Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the item identifier.
|
||||
/// </summary>
|
||||
/// <value>The item identifier.</value>
|
||||
public Guid ItemId { get; set; }
|
||||
}
|
||||
}
|
281
MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
Normal file
281
MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
Normal file
|
@ -0,0 +1,281 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Attachments
|
||||
{
|
||||
public class AttachmentExtractor : IAttachmentExtractor, IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
||||
new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public AttachmentExtractor(
|
||||
ILogger<AttachmentExtractor> logger,
|
||||
IApplicationPaths appPaths,
|
||||
IFileSystem fileSystem,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(MediaAttachment attachment, Stream stream)> GetAttachment(BaseItem item, string mediaSourceId, int attachmentStreamIndex, CancellationToken cancellationToken)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(mediaSourceId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(mediaSourceId));
|
||||
}
|
||||
|
||||
var mediaSources = await _mediaSourceManager.GetPlayackMediaSources(item, null, true, false, cancellationToken).ConfigureAwait(false);
|
||||
var mediaSource = mediaSources
|
||||
.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase));
|
||||
if (mediaSource == null)
|
||||
{
|
||||
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} not found");
|
||||
}
|
||||
|
||||
var mediaAttachment = mediaSource.MediaAttachments
|
||||
.FirstOrDefault(i => i.Index == attachmentStreamIndex);
|
||||
if (mediaAttachment == null)
|
||||
{
|
||||
throw new ResourceNotFoundException($"MediaSource {mediaSourceId} has no attachment with stream index {attachmentStreamIndex}");
|
||||
}
|
||||
|
||||
var attachmentStream = await GetAttachmentStream(mediaSource, mediaAttachment, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return (mediaAttachment, attachmentStream);
|
||||
}
|
||||
|
||||
private async Task<Stream> GetAttachmentStream(
|
||||
MediaSourceInfo mediaSource,
|
||||
MediaAttachment mediaAttachment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var attachmentPath = await GetReadableFile(mediaSource.Path, mediaSource.Path, mediaSource.Protocol, mediaAttachment, cancellationToken).ConfigureAwait(false);
|
||||
return File.OpenRead(attachmentPath);
|
||||
}
|
||||
|
||||
private async Task<string> GetReadableFile(
|
||||
string mediaPath,
|
||||
string inputFile,
|
||||
MediaProtocol protocol,
|
||||
MediaAttachment mediaAttachment,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var outputPath = GetAttachmentCachePath(mediaPath, protocol, mediaAttachment.Index);
|
||||
await ExtractAttachment(inputFile, protocol, mediaAttachment.Index, outputPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
private async Task ExtractAttachment(
|
||||
string inputFile,
|
||||
MediaProtocol protocol,
|
||||
int attachmentStreamIndex,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
|
||||
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
await ExtractAttachmentInternal(
|
||||
_mediaEncoder.GetInputArgument(new[] { inputFile }, protocol),
|
||||
attachmentStreamIndex,
|
||||
outputPath,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAttachmentInternal(
|
||||
string inputPath,
|
||||
int attachmentStreamIndex,
|
||||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(inputPath))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(inputPath));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(outputPath))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(outputPath));
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-dump_attachment:{1} {2} -i {0} -t 0 -f null null",
|
||||
inputPath,
|
||||
attachmentStreamIndex,
|
||||
outputPath);
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
Arguments = processArgs,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
};
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = startInfo
|
||||
};
|
||||
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
process.Start();
|
||||
|
||||
var processTcs = new TaskCompletionSource<bool>();
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += (sender, args) => processTcs.TrySetResult(true);
|
||||
var unregister = cancellationToken.Register(() => processTcs.TrySetResult(process.HasExited));
|
||||
var ranToCompletion = await processTcs.Task.ConfigureAwait(false);
|
||||
unregister.Dispose();
|
||||
|
||||
if (!ranToCompletion)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg attachment extraction process");
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing attachment extraction process");
|
||||
}
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
|
||||
process.Dispose();
|
||||
|
||||
var failed = false;
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
failed = true;
|
||||
|
||||
_logger.LogWarning("Deleting extracted attachment {Path} due to failure: {ExitCode}", outputPath, exitCode);
|
||||
try
|
||||
{
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted attachment {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
else if (!File.Exists(outputPath))
|
||||
{
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (failed)
|
||||
{
|
||||
var msg = $"ffmpeg attachment extraction failed for {inputPath} to {outputPath}";
|
||||
|
||||
_logger.LogError(msg);
|
||||
|
||||
throw new InvalidOperationException(msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("ffmpeg attachment extraction completed for {Path} to {Path}", inputPath, outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetAttachmentCachePath(string mediaPath, MediaProtocol protocol, int attachmentStreamIndex)
|
||||
{
|
||||
string filename;
|
||||
if (protocol == MediaProtocol.File)
|
||||
{
|
||||
var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
|
||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D");
|
||||
}
|
||||
else
|
||||
{
|
||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D");
|
||||
}
|
||||
|
||||
var prefix = filename.Substring(0, 1);
|
||||
return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -397,7 +397,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
try
|
||||
{
|
||||
result = await JsonSerializer.DeserializeAsync<InternalMediaInfoResult>(
|
||||
process.StandardOutput.BaseStream).ConfigureAwait(false);
|
||||
process.StandardOutput.BaseStream,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -406,24 +407,24 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
throw;
|
||||
}
|
||||
|
||||
if (result == null || (result.streams == null && result.format == null))
|
||||
if (result == null || (result.Streams == null && result.Format == null))
|
||||
{
|
||||
throw new Exception("ffprobe failed - streams and format are both null.");
|
||||
}
|
||||
|
||||
if (result.streams != null)
|
||||
if (result.Streams != null)
|
||||
{
|
||||
// Normalize aspect ratio if invalid
|
||||
foreach (var stream in result.streams)
|
||||
foreach (var stream in result.Streams)
|
||||
{
|
||||
if (string.Equals(stream.display_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(stream.DisplayAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.display_aspect_ratio = string.Empty;
|
||||
stream.DisplayAspectRatio = string.Empty;
|
||||
}
|
||||
|
||||
if (string.Equals(stream.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(stream.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.sample_aspect_ratio = string.Empty;
|
||||
stream.SampleAspectRatio = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -778,6 +779,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
_runningProcesses.Add(process);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopProcess(ProcessWrapper process, int waitTimeMs)
|
||||
{
|
||||
try
|
||||
|
@ -786,18 +788,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in WaitForExit");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Killing ffmpeg process");
|
||||
|
||||
process.Process.Kill();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// The process has already exited or
|
||||
// there is no process associated with this Process object.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing process");
|
||||
|
|
|
@ -16,24 +16,19 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
throw new ArgumentNullException(nameof(result));
|
||||
}
|
||||
|
||||
if (result.format != null && result.format.tags != null)
|
||||
if (result.Format != null && result.Format.Tags != null)
|
||||
{
|
||||
result.format.tags = ConvertDictionaryToCaseInSensitive(result.format.tags);
|
||||
result.Format.Tags = ConvertDictionaryToCaseInsensitive(result.Format.Tags);
|
||||
}
|
||||
|
||||
if (result.streams != null)
|
||||
if (result.Streams != null)
|
||||
{
|
||||
// Convert all dictionaries to case insensitive
|
||||
foreach (var stream in result.streams)
|
||||
foreach (var stream in result.Streams)
|
||||
{
|
||||
if (stream.tags != null)
|
||||
if (stream.Tags != null)
|
||||
{
|
||||
stream.tags = ConvertDictionaryToCaseInSensitive(stream.tags);
|
||||
}
|
||||
|
||||
if (stream.disposition != null)
|
||||
{
|
||||
stream.disposition = ConvertDictionaryToCaseInSensitive(stream.disposition);
|
||||
stream.Tags = ConvertDictionaryToCaseInsensitive(stream.Tags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +40,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string GetDictionaryValue(Dictionary<string, string> tags, string key)
|
||||
public static string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
|
@ -103,7 +98,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
/// </summary>
|
||||
/// <param name="dict">The dict.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private static Dictionary<string, string> ConvertDictionaryToCaseInSensitive(Dictionary<string, string> dict)
|
||||
private static Dictionary<string, string> ConvertDictionaryToCaseInsensitive(IReadOnlyDictionary<string, string> dict)
|
||||
{
|
||||
return new Dictionary<string, string>(dict, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaInfoResult
|
||||
/// Class MediaInfoResult.
|
||||
/// </summary>
|
||||
public class InternalMediaInfoResult
|
||||
{
|
||||
|
@ -11,331 +12,21 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
/// Gets or sets the streams.
|
||||
/// </summary>
|
||||
/// <value>The streams.</value>
|
||||
public MediaStreamInfo[] streams { get; set; }
|
||||
[JsonPropertyName("streams")]
|
||||
public IReadOnlyList<MediaStreamInfo> Streams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format.
|
||||
/// </summary>
|
||||
/// <value>The format.</value>
|
||||
public MediaFormatInfo format { get; set; }
|
||||
[JsonPropertyName("format")]
|
||||
public MediaFormatInfo Format { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the chapters.
|
||||
/// </summary>
|
||||
/// <value>The chapters.</value>
|
||||
public MediaChapter[] Chapters { get; set; }
|
||||
}
|
||||
|
||||
public class MediaChapter
|
||||
{
|
||||
public int id { get; set; }
|
||||
public string time_base { get; set; }
|
||||
public long start { get; set; }
|
||||
public string start_time { get; set; }
|
||||
public long end { get; set; }
|
||||
public string end_time { get; set; }
|
||||
public Dictionary<string, string> tags { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a stream within the output
|
||||
/// </summary>
|
||||
public class MediaStreamInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the index.
|
||||
/// </summary>
|
||||
/// <value>The index.</value>
|
||||
public int index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile.
|
||||
/// </summary>
|
||||
/// <value>The profile.</value>
|
||||
public string profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_name.</value>
|
||||
public string codec_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_long_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_long_name.</value>
|
||||
public string codec_long_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_type.
|
||||
/// </summary>
|
||||
/// <value>The codec_type.</value>
|
||||
public string codec_type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_rate.
|
||||
/// </summary>
|
||||
/// <value>The sample_rate.</value>
|
||||
public string sample_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channels.
|
||||
/// </summary>
|
||||
/// <value>The channels.</value>
|
||||
public int channels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channel_layout.
|
||||
/// </summary>
|
||||
/// <value>The channel_layout.</value>
|
||||
public string channel_layout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the avg_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The avg_frame_rate.</value>
|
||||
public string avg_frame_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
public string duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
public string bit_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
/// <value>The width.</value>
|
||||
public int width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the refs.
|
||||
/// </summary>
|
||||
/// <value>The refs.</value>
|
||||
public int refs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
/// <value>The height.</value>
|
||||
public int height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The display_aspect_ratio.</value>
|
||||
public string display_aspect_ratio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
public Dictionary<string, string> tags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bits_per_sample.
|
||||
/// </summary>
|
||||
/// <value>The bits_per_sample.</value>
|
||||
public int bits_per_sample { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bits_per_raw_sample.
|
||||
/// </summary>
|
||||
/// <value>The bits_per_raw_sample.</value>
|
||||
public int bits_per_raw_sample { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the r_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The r_frame_rate.</value>
|
||||
public string r_frame_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the has_b_frames.
|
||||
/// </summary>
|
||||
/// <value>The has_b_frames.</value>
|
||||
public int has_b_frames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The sample_aspect_ratio.</value>
|
||||
public string sample_aspect_ratio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pix_fmt.
|
||||
/// </summary>
|
||||
/// <value>The pix_fmt.</value>
|
||||
public string pix_fmt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// </summary>
|
||||
/// <value>The level.</value>
|
||||
public int level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time_base.
|
||||
/// </summary>
|
||||
/// <value>The time_base.</value>
|
||||
public string time_base { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
public string start_time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_time_base.
|
||||
/// </summary>
|
||||
/// <value>The codec_time_base.</value>
|
||||
public string codec_time_base { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag.</value>
|
||||
public string codec_tag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag_string.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag_string.</value>
|
||||
public string codec_tag_string { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_fmt.
|
||||
/// </summary>
|
||||
/// <value>The sample_fmt.</value>
|
||||
public string sample_fmt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the dmix_mode.
|
||||
/// </summary>
|
||||
/// <value>The dmix_mode.</value>
|
||||
public string dmix_mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_pts.
|
||||
/// </summary>
|
||||
/// <value>The start_pts.</value>
|
||||
public string start_pts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the is_avc.
|
||||
/// </summary>
|
||||
/// <value>The is_avc.</value>
|
||||
public string is_avc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nal_length_size.
|
||||
/// </summary>
|
||||
/// <value>The nal_length_size.</value>
|
||||
public string nal_length_size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_cmixlev.</value>
|
||||
public string ltrt_cmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_surmixlev.</value>
|
||||
public string ltrt_surmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_cmixlev.</value>
|
||||
public string loro_cmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_surmixlev.</value>
|
||||
public string loro_surmixlev { get; set; }
|
||||
|
||||
public string field_order { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the disposition.
|
||||
/// </summary>
|
||||
/// <value>The disposition.</value>
|
||||
public Dictionary<string, string> disposition { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class MediaFormat
|
||||
/// </summary>
|
||||
public class MediaFormatInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the filename.
|
||||
/// </summary>
|
||||
/// <value>The filename.</value>
|
||||
public string filename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nb_streams.
|
||||
/// </summary>
|
||||
/// <value>The nb_streams.</value>
|
||||
public int nb_streams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format_name.
|
||||
/// </summary>
|
||||
/// <value>The format_name.</value>
|
||||
public string format_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format_long_name.
|
||||
/// </summary>
|
||||
/// <value>The format_long_name.</value>
|
||||
public string format_long_name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
public string start_time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
public string duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size.
|
||||
/// </summary>
|
||||
/// <value>The size.</value>
|
||||
public string size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
public string bit_rate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the probe_score.
|
||||
/// </summary>
|
||||
/// <value>The probe_score.</value>
|
||||
public int probe_score { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
public Dictionary<string, string> tags { get; set; }
|
||||
[JsonPropertyName("chapters")]
|
||||
public IReadOnlyList<MediaChapter> Chapters { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
32
MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
Normal file
32
MediaBrowser.MediaEncoding/Probing/MediaChapter.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaChapter.
|
||||
/// </summary>
|
||||
public class MediaChapter
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("time_base")]
|
||||
public string TimeBase { get; set; }
|
||||
|
||||
[JsonPropertyName("start")]
|
||||
public long Start { get; set; }
|
||||
|
||||
[JsonPropertyName("start_time")]
|
||||
public string StartTime { get; set; }
|
||||
|
||||
[JsonPropertyName("end")]
|
||||
public long End { get; set; }
|
||||
|
||||
[JsonPropertyName("end_time")]
|
||||
public string EndTime { get; set; }
|
||||
|
||||
[JsonPropertyName("tags")]
|
||||
public IReadOnlyDictionary<string, string> Tags { get; set; }
|
||||
}
|
||||
}
|
81
MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs
Normal file
81
MediaBrowser.MediaEncoding/Probing/MediaFormatInfo.cs
Normal file
|
@ -0,0 +1,81 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaFormat.
|
||||
/// </summary>
|
||||
public class MediaFormatInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the filename.
|
||||
/// </summary>
|
||||
/// <value>The filename.</value>
|
||||
[JsonPropertyName("filename")]
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nb_streams.
|
||||
/// </summary>
|
||||
/// <value>The nb_streams.</value>
|
||||
[JsonPropertyName("nb_streams")]
|
||||
public int NbStreams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format_name.
|
||||
/// </summary>
|
||||
/// <value>The format_name.</value>
|
||||
[JsonPropertyName("format_name")]
|
||||
public string FormatName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format_long_name.
|
||||
/// </summary>
|
||||
/// <value>The format_long_name.</value>
|
||||
[JsonPropertyName("format_long_name")]
|
||||
public string FormatLongName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
[JsonPropertyName("start_time")]
|
||||
public string StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
[JsonPropertyName("duration")]
|
||||
public string Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size.
|
||||
/// </summary>
|
||||
/// <value>The size.</value>
|
||||
[JsonPropertyName("size")]
|
||||
public string Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
[JsonPropertyName("bit_rate")]
|
||||
public string BitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the probe_score.
|
||||
/// </summary>
|
||||
/// <value>The probe_score.</value>
|
||||
[JsonPropertyName("probe_score")]
|
||||
public int ProbeScore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
[JsonPropertyName("tags")]
|
||||
public IReadOnlyDictionary<string, string> Tags { get; set; }
|
||||
}
|
||||
}
|
282
MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
Normal file
282
MediaBrowser.MediaEncoding/Probing/MediaStreamInfo.cs
Normal file
|
@ -0,0 +1,282 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using MediaBrowser.Common.Json.Converters;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Probing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a stream within the output.
|
||||
/// </summary>
|
||||
public class MediaStreamInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the index.
|
||||
/// </summary>
|
||||
/// <value>The index.</value>
|
||||
[JsonPropertyName("index")]
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile.
|
||||
/// </summary>
|
||||
/// <value>The profile.</value>
|
||||
[JsonPropertyName("profile")]
|
||||
public string Profile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_name.</value>
|
||||
[JsonPropertyName("codec_name")]
|
||||
public string CodecName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_long_name.
|
||||
/// </summary>
|
||||
/// <value>The codec_long_name.</value>
|
||||
[JsonPropertyName("codec_long_name")]
|
||||
public string CodecLongName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_type.
|
||||
/// </summary>
|
||||
/// <value>The codec_type.</value>
|
||||
[JsonPropertyName("codec_type")]
|
||||
public string CodecType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_rate.
|
||||
/// </summary>
|
||||
/// <value>The sample_rate.</value>
|
||||
[JsonPropertyName("sample_rate")]
|
||||
public string SampleRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channels.
|
||||
/// </summary>
|
||||
/// <value>The channels.</value>
|
||||
[JsonPropertyName("channels")]
|
||||
public int Channels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the channel_layout.
|
||||
/// </summary>
|
||||
/// <value>The channel_layout.</value>
|
||||
[JsonPropertyName("channel_layout")]
|
||||
public string ChannelLayout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the avg_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The avg_frame_rate.</value>
|
||||
[JsonPropertyName("avg_frame_rate")]
|
||||
public string AverageFrameRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the duration.
|
||||
/// </summary>
|
||||
/// <value>The duration.</value>
|
||||
[JsonPropertyName("duration")]
|
||||
public string Duration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bit_rate.
|
||||
/// </summary>
|
||||
/// <value>The bit_rate.</value>
|
||||
[JsonPropertyName("bit_rate")]
|
||||
public string BitRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
/// <value>The width.</value>
|
||||
[JsonPropertyName("width")]
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the refs.
|
||||
/// </summary>
|
||||
/// <value>The refs.</value>
|
||||
[JsonPropertyName("refs")]
|
||||
public int Refs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
/// <value>The height.</value>
|
||||
[JsonPropertyName("height")]
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The display_aspect_ratio.</value>
|
||||
[JsonPropertyName("display_aspect_ratio")]
|
||||
public string DisplayAspectRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tags.
|
||||
/// </summary>
|
||||
/// <value>The tags.</value>
|
||||
[JsonPropertyName("tags")]
|
||||
public IReadOnlyDictionary<string, string> Tags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bits_per_sample.
|
||||
/// </summary>
|
||||
/// <value>The bits_per_sample.</value>
|
||||
[JsonPropertyName("bits_per_sample")]
|
||||
public int BitsPerSample { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bits_per_raw_sample.
|
||||
/// </summary>
|
||||
/// <value>The bits_per_raw_sample.</value>
|
||||
[JsonPropertyName("bits_per_raw_sample")]
|
||||
[JsonConverter(typeof(JsonInt32Converter))]
|
||||
public int BitsPerRawSample { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the r_frame_rate.
|
||||
/// </summary>
|
||||
/// <value>The r_frame_rate.</value>
|
||||
[JsonPropertyName("r_frame_rate")]
|
||||
public string RFrameRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the has_b_frames.
|
||||
/// </summary>
|
||||
/// <value>The has_b_frames.</value>
|
||||
[JsonPropertyName("has_b_frames")]
|
||||
public int HasBFrames { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_aspect_ratio.
|
||||
/// </summary>
|
||||
/// <value>The sample_aspect_ratio.</value>
|
||||
[JsonPropertyName("sample_aspect_ratio")]
|
||||
public string SampleAspectRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pix_fmt.
|
||||
/// </summary>
|
||||
/// <value>The pix_fmt.</value>
|
||||
[JsonPropertyName("pix_fmt")]
|
||||
public string PixelFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the level.
|
||||
/// </summary>
|
||||
/// <value>The level.</value>
|
||||
[JsonPropertyName("level")]
|
||||
public int Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time_base.
|
||||
/// </summary>
|
||||
/// <value>The time_base.</value>
|
||||
[JsonPropertyName("time_base")]
|
||||
public string TimeBase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_time.
|
||||
/// </summary>
|
||||
/// <value>The start_time.</value>
|
||||
[JsonPropertyName("start_time")]
|
||||
public string StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_time_base.
|
||||
/// </summary>
|
||||
/// <value>The codec_time_base.</value>
|
||||
[JsonPropertyName("codec_time_base")]
|
||||
public string CodecTimeBase { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag.</value>
|
||||
[JsonPropertyName("codec_tag")]
|
||||
public string CodecTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec_tag_string.
|
||||
/// </summary>
|
||||
/// <value>The codec_tag_string.</value>
|
||||
[JsonPropertyName("codec_tag_string")]
|
||||
public string CodecTagString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sample_fmt.
|
||||
/// </summary>
|
||||
/// <value>The sample_fmt.</value>
|
||||
[JsonPropertyName("sample_fmt")]
|
||||
public string SampleFmt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the dmix_mode.
|
||||
/// </summary>
|
||||
/// <value>The dmix_mode.</value>
|
||||
[JsonPropertyName("dmix_mode")]
|
||||
public string DmixMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start_pts.
|
||||
/// </summary>
|
||||
/// <value>The start_pts.</value>
|
||||
[JsonPropertyName("start_pts")]
|
||||
public int StartPts { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the is_avc.
|
||||
/// </summary>
|
||||
/// <value>The is_avc.</value>
|
||||
[JsonPropertyName("is_avc")]
|
||||
public string IsAvc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nal_length_size.
|
||||
/// </summary>
|
||||
/// <value>The nal_length_size.</value>
|
||||
[JsonPropertyName("nal_length_size")]
|
||||
public string NalLengthSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_cmixlev.</value>
|
||||
[JsonPropertyName("ltrt_cmixlev")]
|
||||
public string LtrtCmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ltrt_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The ltrt_surmixlev.</value>
|
||||
[JsonPropertyName("ltrt_surmixlev")]
|
||||
public string LtrtSurmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_cmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_cmixlev.</value>
|
||||
[JsonPropertyName("loro_cmixlev")]
|
||||
public string LoroCmixlev { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the loro_surmixlev.
|
||||
/// </summary>
|
||||
/// <value>The loro_surmixlev.</value>
|
||||
[JsonPropertyName("loro_surmixlev")]
|
||||
public string LoroSurmixlev { get; set; }
|
||||
|
||||
[JsonPropertyName("field_order")]
|
||||
public string FieldOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the disposition.
|
||||
/// </summary>
|
||||
/// <value>The disposition.</value>
|
||||
[JsonPropertyName("disposition")]
|
||||
public IReadOnlyDictionary<string, int> Disposition { get; set; }
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ using System.Xml;
|
|||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Extensions;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
@ -41,21 +40,25 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
FFProbeHelpers.NormalizeFFProbeResult(data);
|
||||
SetSize(data, info);
|
||||
|
||||
var internalStreams = data.streams ?? new MediaStreamInfo[] { };
|
||||
var internalStreams = data.Streams ?? new MediaStreamInfo[] { };
|
||||
|
||||
info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.format))
|
||||
info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format))
|
||||
.Where(i => i != null)
|
||||
// Drop subtitle streams if we don't know the codec because it will just cause failures if we don't know how to handle them
|
||||
.Where(i => i.Type != MediaStreamType.Subtitle || !string.IsNullOrWhiteSpace(i.Codec))
|
||||
.ToList();
|
||||
|
||||
if (data.format != null)
|
||||
{
|
||||
info.Container = NormalizeFormat(data.format.format_name);
|
||||
info.MediaAttachments = internalStreams.Select(s => GetMediaAttachment(s))
|
||||
.Where(i => i != null)
|
||||
.ToList();
|
||||
|
||||
if (!string.IsNullOrEmpty(data.format.bit_rate))
|
||||
if (data.Format != null)
|
||||
{
|
||||
info.Container = NormalizeFormat(data.Format.FormatName);
|
||||
|
||||
if (!string.IsNullOrEmpty(data.Format.BitRate))
|
||||
{
|
||||
if (int.TryParse(data.format.bit_rate, NumberStyles.Any, _usCulture, out var value))
|
||||
if (int.TryParse(data.Format.BitRate, NumberStyles.Any, _usCulture, out var value))
|
||||
{
|
||||
info.Bitrate = value;
|
||||
}
|
||||
|
@ -65,22 +68,22 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
var tags = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var tagStreamType = isAudio ? "audio" : "video";
|
||||
|
||||
if (data.streams != null)
|
||||
if (data.Streams != null)
|
||||
{
|
||||
var tagStream = data.streams.FirstOrDefault(i => string.Equals(i.codec_type, tagStreamType, StringComparison.OrdinalIgnoreCase));
|
||||
var tagStream = data.Streams.FirstOrDefault(i => string.Equals(i.CodecType, tagStreamType, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (tagStream != null && tagStream.tags != null)
|
||||
if (tagStream != null && tagStream.Tags != null)
|
||||
{
|
||||
foreach (var pair in tagStream.tags)
|
||||
foreach (var pair in tagStream.Tags)
|
||||
{
|
||||
tags[pair.Key] = pair.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.format != null && data.format.tags != null)
|
||||
if (data.Format != null && data.Format.Tags != null)
|
||||
{
|
||||
foreach (var pair in data.format.tags)
|
||||
foreach (var pair in data.Format.Tags)
|
||||
{
|
||||
tags[pair.Key] = pair.Value;
|
||||
}
|
||||
|
@ -153,9 +156,9 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
FetchFromItunesInfo(itunesXml, info);
|
||||
}
|
||||
|
||||
if (data.format != null && !string.IsNullOrEmpty(data.format.duration))
|
||||
if (data.Format != null && !string.IsNullOrEmpty(data.Format.Duration))
|
||||
{
|
||||
info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.format.duration, _usCulture)).Ticks;
|
||||
info.RunTimeTicks = TimeSpan.FromSeconds(double.Parse(data.Format.Duration, _usCulture)).Ticks;
|
||||
}
|
||||
|
||||
FetchWtvInfo(info, data);
|
||||
|
@ -513,6 +516,39 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
return codec;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts ffprobe stream info to our MediaAttachment class
|
||||
/// </summary>
|
||||
/// <param name="streamInfo">The stream info.</param>
|
||||
/// <returns>MediaAttachments.</returns>
|
||||
private MediaAttachment GetMediaAttachment(MediaStreamInfo streamInfo)
|
||||
{
|
||||
if (!string.Equals(streamInfo.CodecType, "attachment", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var attachment = new MediaAttachment
|
||||
{
|
||||
Codec = streamInfo.CodecName,
|
||||
Index = streamInfo.Index
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString))
|
||||
{
|
||||
attachment.CodecTag = streamInfo.CodecTagString;
|
||||
}
|
||||
|
||||
if (streamInfo.Tags != null)
|
||||
{
|
||||
attachment.FileName = GetDictionaryValue(streamInfo.Tags, "filename");
|
||||
attachment.MimeType = GetDictionaryValue(streamInfo.Tags, "mimetype");
|
||||
attachment.Comment = GetDictionaryValue(streamInfo.Tags, "comment");
|
||||
}
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts ffprobe stream info to our MediaStream class
|
||||
/// </summary>
|
||||
|
@ -523,7 +559,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
private MediaStream GetMediaStream(bool isAudio, MediaStreamInfo streamInfo, MediaFormatInfo formatInfo)
|
||||
{
|
||||
// These are mp4 chapters
|
||||
if (string.Equals(streamInfo.codec_name, "mov_text", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(streamInfo.CodecName, "mov_text", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Edit: but these are also sometimes subtitles?
|
||||
//return null;
|
||||
|
@ -531,71 +567,71 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
|
||||
var stream = new MediaStream
|
||||
{
|
||||
Codec = streamInfo.codec_name,
|
||||
Profile = streamInfo.profile,
|
||||
Level = streamInfo.level,
|
||||
Index = streamInfo.index,
|
||||
PixelFormat = streamInfo.pix_fmt,
|
||||
NalLengthSize = streamInfo.nal_length_size,
|
||||
TimeBase = streamInfo.time_base,
|
||||
CodecTimeBase = streamInfo.codec_time_base
|
||||
Codec = streamInfo.CodecName,
|
||||
Profile = streamInfo.Profile,
|
||||
Level = streamInfo.Level,
|
||||
Index = streamInfo.Index,
|
||||
PixelFormat = streamInfo.PixelFormat,
|
||||
NalLengthSize = streamInfo.NalLengthSize,
|
||||
TimeBase = streamInfo.TimeBase,
|
||||
CodecTimeBase = streamInfo.CodecTimeBase
|
||||
};
|
||||
|
||||
if (string.Equals(streamInfo.is_avc, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(streamInfo.is_avc, "1", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(streamInfo.IsAvc, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(streamInfo.IsAvc, "1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.IsAVC = true;
|
||||
}
|
||||
else if (string.Equals(streamInfo.is_avc, "false", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(streamInfo.is_avc, "0", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(streamInfo.IsAvc, "false", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(streamInfo.IsAvc, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.IsAVC = false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.field_order) && !string.Equals(streamInfo.field_order, "progressive", StringComparison.OrdinalIgnoreCase))
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.FieldOrder) && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.IsInterlaced = true;
|
||||
}
|
||||
|
||||
// Filter out junk
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.codec_tag_string) && streamInfo.codec_tag_string.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && streamInfo.CodecTagString.IndexOf("[0]", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
stream.CodecTag = streamInfo.codec_tag_string;
|
||||
stream.CodecTag = streamInfo.CodecTagString;
|
||||
}
|
||||
|
||||
if (streamInfo.tags != null)
|
||||
if (streamInfo.Tags != null)
|
||||
{
|
||||
stream.Language = GetDictionaryValue(streamInfo.tags, "language");
|
||||
stream.Comment = GetDictionaryValue(streamInfo.tags, "comment");
|
||||
stream.Title = GetDictionaryValue(streamInfo.tags, "title");
|
||||
stream.Language = GetDictionaryValue(streamInfo.Tags, "language");
|
||||
stream.Comment = GetDictionaryValue(streamInfo.Tags, "comment");
|
||||
stream.Title = GetDictionaryValue(streamInfo.Tags, "title");
|
||||
}
|
||||
|
||||
if (string.Equals(streamInfo.codec_type, "audio", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Audio;
|
||||
|
||||
stream.Channels = streamInfo.channels;
|
||||
stream.Channels = streamInfo.Channels;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.sample_rate))
|
||||
if (!string.IsNullOrEmpty(streamInfo.SampleRate))
|
||||
{
|
||||
if (int.TryParse(streamInfo.sample_rate, NumberStyles.Any, _usCulture, out var value))
|
||||
if (int.TryParse(streamInfo.SampleRate, NumberStyles.Any, _usCulture, out var value))
|
||||
{
|
||||
stream.SampleRate = value;
|
||||
}
|
||||
}
|
||||
|
||||
stream.ChannelLayout = ParseChannelLayout(streamInfo.channel_layout);
|
||||
stream.ChannelLayout = ParseChannelLayout(streamInfo.ChannelLayout);
|
||||
|
||||
if (streamInfo.bits_per_sample > 0)
|
||||
if (streamInfo.BitsPerSample > 0)
|
||||
{
|
||||
stream.BitDepth = streamInfo.bits_per_sample;
|
||||
stream.BitDepth = streamInfo.BitsPerSample;
|
||||
}
|
||||
else if (streamInfo.bits_per_raw_sample > 0)
|
||||
else if (streamInfo.BitsPerRawSample > 0)
|
||||
{
|
||||
stream.BitDepth = streamInfo.bits_per_raw_sample;
|
||||
stream.BitDepth = streamInfo.BitsPerRawSample;
|
||||
}
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(streamInfo.CodecType, "subtitle", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = MediaStreamType.Subtitle;
|
||||
stream.Codec = NormalizeSubtitleCodec(stream.Codec);
|
||||
|
@ -603,14 +639,14 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
stream.localizedDefault = _localization.GetLocalizedString("Default");
|
||||
stream.localizedForced = _localization.GetLocalizedString("Forced");
|
||||
}
|
||||
else if (string.Equals(streamInfo.codec_type, "video", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stream.Type = isAudio || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) || string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
||||
? MediaStreamType.EmbeddedImage
|
||||
: MediaStreamType.Video;
|
||||
|
||||
stream.AverageFrameRate = GetFrameRate(streamInfo.avg_frame_rate);
|
||||
stream.RealFrameRate = GetFrameRate(streamInfo.r_frame_rate);
|
||||
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
|
||||
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
|
||||
|
||||
if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
|
||||
|
@ -635,17 +671,17 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
stream.Type = MediaStreamType.Video;
|
||||
}
|
||||
|
||||
stream.Width = streamInfo.width;
|
||||
stream.Height = streamInfo.height;
|
||||
stream.Width = streamInfo.Width;
|
||||
stream.Height = streamInfo.Height;
|
||||
stream.AspectRatio = GetAspectRatio(streamInfo);
|
||||
|
||||
if (streamInfo.bits_per_sample > 0)
|
||||
if (streamInfo.BitsPerSample > 0)
|
||||
{
|
||||
stream.BitDepth = streamInfo.bits_per_sample;
|
||||
stream.BitDepth = streamInfo.BitsPerSample;
|
||||
}
|
||||
else if (streamInfo.bits_per_raw_sample > 0)
|
||||
else if (streamInfo.BitsPerRawSample > 0)
|
||||
{
|
||||
stream.BitDepth = streamInfo.bits_per_raw_sample;
|
||||
stream.BitDepth = streamInfo.BitsPerRawSample;
|
||||
}
|
||||
|
||||
//stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase) ||
|
||||
|
@ -653,11 +689,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
// string.Equals(stream.AspectRatio, "2.40:1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe
|
||||
stream.IsAnamorphic = string.Equals(streamInfo.sample_aspect_ratio, "0:1", StringComparison.OrdinalIgnoreCase);
|
||||
stream.IsAnamorphic = string.Equals(streamInfo.SampleAspectRatio, "0:1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (streamInfo.refs > 0)
|
||||
if (streamInfo.Refs > 0)
|
||||
{
|
||||
stream.RefFrames = streamInfo.refs;
|
||||
stream.RefFrames = streamInfo.Refs;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -668,18 +704,18 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
// Get stream bitrate
|
||||
var bitrate = 0;
|
||||
|
||||
if (!string.IsNullOrEmpty(streamInfo.bit_rate))
|
||||
if (!string.IsNullOrEmpty(streamInfo.BitRate))
|
||||
{
|
||||
if (int.TryParse(streamInfo.bit_rate, NumberStyles.Any, _usCulture, out var value))
|
||||
if (int.TryParse(streamInfo.BitRate, NumberStyles.Any, _usCulture, out var value))
|
||||
{
|
||||
bitrate = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (bitrate == 0 && formatInfo != null && !string.IsNullOrEmpty(formatInfo.bit_rate) && stream.Type == MediaStreamType.Video)
|
||||
if (bitrate == 0 && formatInfo != null && !string.IsNullOrEmpty(formatInfo.BitRate) && stream.Type == MediaStreamType.Video)
|
||||
{
|
||||
// If the stream info doesn't have a bitrate get the value from the media format info
|
||||
if (int.TryParse(formatInfo.bit_rate, NumberStyles.Any, _usCulture, out var value))
|
||||
if (int.TryParse(formatInfo.BitRate, NumberStyles.Any, _usCulture, out var value))
|
||||
{
|
||||
bitrate = value;
|
||||
}
|
||||
|
@ -690,14 +726,18 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
stream.BitRate = bitrate;
|
||||
}
|
||||
|
||||
if (streamInfo.disposition != null)
|
||||
var disposition = streamInfo.Disposition;
|
||||
if (disposition != null)
|
||||
{
|
||||
var isDefault = GetDictionaryValue(streamInfo.disposition, "default");
|
||||
var isForced = GetDictionaryValue(streamInfo.disposition, "forced");
|
||||
if (disposition.GetValueOrDefault("default") == 1)
|
||||
{
|
||||
stream.IsDefault = true;
|
||||
}
|
||||
|
||||
stream.IsDefault = string.Equals(isDefault, "1", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
stream.IsForced = string.Equals(isForced, "1", StringComparison.OrdinalIgnoreCase);
|
||||
if (disposition.GetValueOrDefault("forced") == 1)
|
||||
{
|
||||
stream.IsForced = true;
|
||||
}
|
||||
}
|
||||
|
||||
NormalizeStreamTitle(stream);
|
||||
|
@ -724,7 +764,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
/// <param name="tags">The tags.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetDictionaryValue(Dictionary<string, string> tags, string key)
|
||||
private string GetDictionaryValue(IReadOnlyDictionary<string, string> tags, string key)
|
||||
{
|
||||
if (tags == null)
|
||||
{
|
||||
|
@ -747,7 +787,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
|
||||
private string GetAspectRatio(MediaStreamInfo info)
|
||||
{
|
||||
var original = info.display_aspect_ratio;
|
||||
var original = info.DisplayAspectRatio;
|
||||
|
||||
var parts = (original ?? string.Empty).Split(':');
|
||||
if (!(parts.Length == 2 &&
|
||||
|
@ -756,8 +796,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
width > 0 &&
|
||||
height > 0))
|
||||
{
|
||||
width = info.width;
|
||||
height = info.height;
|
||||
width = info.Width;
|
||||
height = info.Height;
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0)
|
||||
|
@ -850,20 +890,20 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
|
||||
private void SetAudioRuntimeTicks(InternalMediaInfoResult result, MediaInfo data)
|
||||
{
|
||||
if (result.streams != null)
|
||||
if (result.Streams != null)
|
||||
{
|
||||
// Get the first info stream
|
||||
var stream = result.streams.FirstOrDefault(s => string.Equals(s.codec_type, "audio", StringComparison.OrdinalIgnoreCase));
|
||||
var stream = result.Streams.FirstOrDefault(s => string.Equals(s.CodecType, "audio", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (stream != null)
|
||||
{
|
||||
// Get duration from stream properties
|
||||
var duration = stream.duration;
|
||||
var duration = stream.Duration;
|
||||
|
||||
// If it's not there go into format properties
|
||||
if (string.IsNullOrEmpty(duration))
|
||||
{
|
||||
duration = result.format.duration;
|
||||
duration = result.Format.Duration;
|
||||
}
|
||||
|
||||
// If we got something, parse it
|
||||
|
@ -877,11 +917,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
|
||||
private void SetSize(InternalMediaInfoResult data, MediaInfo info)
|
||||
{
|
||||
if (data.format != null)
|
||||
if (data.Format != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(data.format.size))
|
||||
if (!string.IsNullOrEmpty(data.Format.Size))
|
||||
{
|
||||
info.Size = long.Parse(data.format.size, _usCulture);
|
||||
info.Size = long.Parse(data.Format.Size, _usCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1194,16 +1234,16 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
{
|
||||
var info = new ChapterInfo();
|
||||
|
||||
if (chapter.tags != null)
|
||||
if (chapter.Tags != null)
|
||||
{
|
||||
if (chapter.tags.TryGetValue("title", out string name))
|
||||
if (chapter.Tags.TryGetValue("title", out string name))
|
||||
{
|
||||
info.Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
// Limit accuracy to milliseconds to match xml saving
|
||||
var secondsString = chapter.start_time;
|
||||
var secondsString = chapter.StartTime;
|
||||
|
||||
if (double.TryParse(secondsString, NumberStyles.Any, CultureInfo.InvariantCulture, out var seconds))
|
||||
{
|
||||
|
@ -1218,12 +1258,12 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
|
||||
private void FetchWtvInfo(MediaInfo video, InternalMediaInfoResult data)
|
||||
{
|
||||
if (data.format == null || data.format.tags == null)
|
||||
if (data.Format == null || data.Format.Tags == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var genres = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/Genre");
|
||||
var genres = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/Genre");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genres))
|
||||
{
|
||||
|
@ -1239,14 +1279,14 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
}
|
||||
}
|
||||
|
||||
var officialRating = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/ParentalRating");
|
||||
var officialRating = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/ParentalRating");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(officialRating))
|
||||
{
|
||||
video.OfficialRating = officialRating;
|
||||
}
|
||||
|
||||
var people = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaCredits");
|
||||
var people = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaCredits");
|
||||
|
||||
if (!string.IsNullOrEmpty(people))
|
||||
{
|
||||
|
@ -1256,7 +1296,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
.ToArray();
|
||||
}
|
||||
|
||||
var year = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/OriginalReleaseTime");
|
||||
var year = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/OriginalReleaseTime");
|
||||
if (!string.IsNullOrWhiteSpace(year))
|
||||
{
|
||||
if (int.TryParse(year, NumberStyles.Integer, _usCulture, out var val))
|
||||
|
@ -1265,7 +1305,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
}
|
||||
}
|
||||
|
||||
var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/MediaOriginalBroadcastDateTime");
|
||||
var premiereDateString = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/MediaOriginalBroadcastDateTime");
|
||||
if (!string.IsNullOrWhiteSpace(premiereDateString))
|
||||
{
|
||||
// Credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
|
||||
|
@ -1276,9 +1316,9 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
}
|
||||
}
|
||||
|
||||
var description = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitleDescription");
|
||||
var description = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitleDescription");
|
||||
|
||||
var subTitle = FFProbeHelpers.GetDictionaryValue(data.format.tags, "WM/SubTitle");
|
||||
var subTitle = FFProbeHelpers.GetDictionaryValue(data.Format.Tags, "WM/SubTitle");
|
||||
|
||||
// For below code, credit to MCEBuddy: https://mcebuddy2x.codeplex.com/
|
||||
|
||||
|
@ -1334,24 +1374,25 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
{
|
||||
video.Timestamp = GetMpegTimestamp(video.Path);
|
||||
|
||||
_logger.LogDebug("Video has {timestamp} timestamp", video.Timestamp);
|
||||
_logger.LogDebug("Video has {Timestamp} timestamp", video.Timestamp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error extracting timestamp info from {path}", video.Path);
|
||||
_logger.LogError(ex, "Error extracting timestamp info from {Path}", video.Path);
|
||||
video.Timestamp = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// REVIEW: find out why the byte array needs to be 197 bytes long and comment the reason
|
||||
private TransportStreamTimestamp GetMpegTimestamp(string path)
|
||||
{
|
||||
var packetBuffer = new byte['Å'];
|
||||
var packetBuffer = new byte[197];
|
||||
|
||||
using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read))
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
fs.Read(packetBuffer, 0, packetBuffer.Length);
|
||||
fs.Read(packetBuffer);
|
||||
}
|
||||
|
||||
if (packetBuffer[0] == 71)
|
||||
|
@ -1359,7 +1400,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
return TransportStreamTimestamp.None;
|
||||
}
|
||||
|
||||
if ((packetBuffer[4] == 71) && (packetBuffer['Ä'] == 71))
|
||||
if ((packetBuffer[4] == 71) && (packetBuffer[196] == 71))
|
||||
{
|
||||
if ((packetBuffer[0] == 0) && (packetBuffer[1] == 0) && (packetBuffer[2] == 0) && (packetBuffer[3] == 0))
|
||||
{
|
||||
|
|
|
@ -16,6 +16,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
using (var writer = new Utf8JsonWriter(stream))
|
||||
{
|
||||
var trackevents = info.TrackEvents;
|
||||
writer.WriteStartObject();
|
||||
writer.WriteStartArray("TrackEvents");
|
||||
|
||||
for (int i = 0; i < trackevents.Count; i++)
|
||||
|
@ -33,7 +34,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
|
||||
writer.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
</packages>
|
|
@ -57,6 +57,8 @@ namespace MediaBrowser.Model.Dto
|
|||
|
||||
public List<MediaStream> MediaStreams { get; set; }
|
||||
|
||||
public IReadOnlyList<MediaAttachment> MediaAttachments { get; set; }
|
||||
|
||||
public string[] Formats { get; set; }
|
||||
|
||||
public int? Bitrate { get; set; }
|
||||
|
|
50
MediaBrowser.Model/Entities/MediaAttachment.cs
Normal file
50
MediaBrowser.Model/Entities/MediaAttachment.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
namespace MediaBrowser.Model.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaAttachment
|
||||
/// </summary>
|
||||
public class MediaAttachment
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the codec.
|
||||
/// </summary>
|
||||
/// <value>The codec.</value>
|
||||
public string Codec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec tag.
|
||||
/// </summary>
|
||||
/// <value>The codec tag.</value>
|
||||
public string CodecTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the comment.
|
||||
/// </summary>
|
||||
/// <value>The comment.</value>
|
||||
public string Comment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index.
|
||||
/// </summary>
|
||||
/// <value>The index.</value>
|
||||
public int Index { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the filename.
|
||||
/// </summary>
|
||||
/// <value>The filename.</value>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the MIME type.
|
||||
/// </summary>
|
||||
/// <value>The MIME type.</value>
|
||||
public string MimeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the delivery URL.
|
||||
/// </summary>
|
||||
/// <value>The delivery URL.</value>
|
||||
public string DeliveryUrl { get; set; }
|
||||
}
|
||||
}
|
|
@ -158,11 +158,13 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
MetadataRefreshOptions options)
|
||||
{
|
||||
List<MediaStream> mediaStreams;
|
||||
IReadOnlyList<MediaAttachment> mediaAttachments;
|
||||
List<ChapterInfo> chapters;
|
||||
|
||||
if (mediaInfo != null)
|
||||
{
|
||||
mediaStreams = mediaInfo.MediaStreams;
|
||||
mediaAttachments = mediaInfo.MediaAttachments;
|
||||
|
||||
video.TotalBitrate = mediaInfo.Bitrate;
|
||||
//video.FormatName = (mediaInfo.Container ?? string.Empty)
|
||||
|
@ -198,6 +200,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
else
|
||||
{
|
||||
mediaStreams = new List<MediaStream>();
|
||||
mediaAttachments = Array.Empty<MediaAttachment>();
|
||||
chapters = new List<ChapterInfo>();
|
||||
}
|
||||
|
||||
|
@ -210,19 +213,20 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
FetchEmbeddedInfo(video, mediaInfo, options, libraryOptions);
|
||||
FetchPeople(video, mediaInfo, options);
|
||||
video.Timestamp = mediaInfo.Timestamp;
|
||||
video.Video3DFormat = video.Video3DFormat ?? mediaInfo.Video3DFormat;
|
||||
video.Video3DFormat ??= mediaInfo.Video3DFormat;
|
||||
}
|
||||
|
||||
var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
|
||||
|
||||
video.Height = videoStream == null ? 0 : videoStream.Height ?? 0;
|
||||
video.Width = videoStream == null ? 0 : videoStream.Width ?? 0;
|
||||
video.Height = videoStream?.Height ?? 0;
|
||||
video.Width = videoStream?.Width ?? 0;
|
||||
|
||||
video.DefaultVideoStreamIndex = videoStream == null ? (int?)null : videoStream.Index;
|
||||
|
||||
video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle);
|
||||
|
||||
_itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken);
|
||||
_itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken);
|
||||
|
||||
if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh ||
|
||||
options.MetadataRefreshMode == MetadataRefreshMode.Default)
|
||||
|
|
Loading…
Reference in New Issue
Block a user