Merge pull request #7 from jellyfin/master

nightly
This commit is contained in:
Artiume 2020-01-09 12:14:54 -05:00 committed by GitHub
commit a40cb7bbd8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1493 additions and 559 deletions

View file

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

View file

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

View file

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

View file

@ -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)
{

View file

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

View file

@ -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",

View file

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

View file

@ -96,7 +96,6 @@ namespace Jellyfin.Api.Controllers
public StartupUserDto GetFirstUser()
{
var user = _userManager.Users.First();
return new StartupUserDto
{
Name = user.Name,

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -38,6 +38,20 @@ namespace MediaBrowser.Controller.Library
/// <returns>IEnumerable&lt;MediaStream&gt;.</returns>
List<MediaStream> GetMediaStreams(MediaStreamQuery query);
/// <summary>
/// Gets the media attachments.
/// </summary>
/// <param name="itemId">The item identifier.</param>
/// <returns>IEnumerable&lt;MediaAttachment&gt;.</returns>
List<MediaAttachment> GetMediaAttachments(Guid itemId);
/// <summary>
/// Gets the media attachments.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>IEnumerable&lt;MediaAttachment&gt;.</returns>
List<MediaAttachment> GetMediaAttachments(MediaAttachmentQuery query);
/// <summary>
/// Gets the playack media sources.
/// </summary>

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

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

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

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

View file

@ -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))
{

View file

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

View file

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
</packages>

View file

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

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

View file

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