mirror of
https://github.com/rosenbjerg/FFMpegCore.git
synced 2024-11-10 08:34:12 +01:00
Merge pull request #287 from JKamsker/MetaDataArgument
Added MetaDataArgument
This commit is contained in:
commit
a6c825ea55
9 changed files with 318 additions and 0 deletions
54
FFMpegCore.Test/MetaDataBuilderTests.cs
Normal file
54
FFMpegCore.Test/MetaDataBuilderTests.cs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
using FFMpegCore.Builders.MetaData;
|
||||||
|
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Test
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class MetaDataBuilderTests
|
||||||
|
{
|
||||||
|
[TestMethod]
|
||||||
|
public void TestMetaDataBuilderIntegrity()
|
||||||
|
{
|
||||||
|
var source = new
|
||||||
|
{
|
||||||
|
Album = "Kanon und Gigue",
|
||||||
|
Artist = "Pachelbel",
|
||||||
|
Title = "Kanon und Gigue in D-Dur",
|
||||||
|
Copyright = "Copyright Lol",
|
||||||
|
Composer = "Pachelbel",
|
||||||
|
Genres = new[] { "Synthwave", "Classics" },
|
||||||
|
Tracks = new[]
|
||||||
|
{
|
||||||
|
new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 01" },
|
||||||
|
new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 02" },
|
||||||
|
new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 03" },
|
||||||
|
new { Duration = TimeSpan.FromSeconds(10), Title = "Chapter 04" },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var builder = new MetaDataBuilder()
|
||||||
|
.WithTitle(source.Title)
|
||||||
|
.WithArtists(source.Artist)
|
||||||
|
.WithComposers(source.Composer)
|
||||||
|
.WithAlbumArtists(source.Artist)
|
||||||
|
.WithGenres(source.Genres)
|
||||||
|
.WithCopyright(source.Copyright)
|
||||||
|
.AddChapters(source.Tracks, x => (x.Duration, x.Title));
|
||||||
|
|
||||||
|
var metadata = builder.Build();
|
||||||
|
var serialized = MetaDataSerializer.Instance.Serialize(metadata);
|
||||||
|
|
||||||
|
Assert.IsTrue(serialized.StartsWith(";FFMETADATA1", StringComparison.OrdinalIgnoreCase));
|
||||||
|
Assert.IsTrue(serialized.Contains("genre=Synthwave; Classics", StringComparison.OrdinalIgnoreCase));
|
||||||
|
Assert.IsTrue(serialized.Contains("title=Chapter 01", StringComparison.OrdinalIgnoreCase));
|
||||||
|
Assert.IsTrue(serialized.Contains("album_artist=Pachelbel", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs
Normal file
27
FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Arguments
|
||||||
|
{
|
||||||
|
public class MetaDataArgument : IInputArgument
|
||||||
|
{
|
||||||
|
private readonly string _metaDataContent;
|
||||||
|
private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"metadata_{Guid.NewGuid()}.txt");
|
||||||
|
|
||||||
|
public MetaDataArgument(string metaDataContent)
|
||||||
|
{
|
||||||
|
_metaDataContent = metaDataContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Text => $"-i \"{_tempFileName}\" -map_metadata 1";
|
||||||
|
|
||||||
|
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||||
|
|
||||||
|
|
||||||
|
public void Pre() => File.WriteAllText(_tempFileName, _metaDataContent);
|
||||||
|
|
||||||
|
public void Post() => File.Delete(_tempFileName);
|
||||||
|
}
|
||||||
|
}
|
18
FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs
Normal file
18
FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Builders.MetaData
|
||||||
|
{
|
||||||
|
public class ChapterData
|
||||||
|
{
|
||||||
|
public string Title { get; private set; }
|
||||||
|
public TimeSpan Start { get; private set; }
|
||||||
|
public TimeSpan End { get; private set; }
|
||||||
|
|
||||||
|
public ChapterData(string title, TimeSpan start, TimeSpan end)
|
||||||
|
{
|
||||||
|
Title = title;
|
||||||
|
Start = start;
|
||||||
|
End = end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs
Normal file
11
FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Builders.MetaData
|
||||||
|
{
|
||||||
|
|
||||||
|
public interface IReadOnlyMetaData
|
||||||
|
{
|
||||||
|
IReadOnlyList<ChapterData> Chapters { get; }
|
||||||
|
IReadOnlyDictionary<string, string> Entries { get; }
|
||||||
|
}
|
||||||
|
}
|
33
FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs
Normal file
33
FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Builders.MetaData
|
||||||
|
{
|
||||||
|
public class MetaData : IReadOnlyMetaData
|
||||||
|
{
|
||||||
|
public Dictionary<string, string> Entries { get; private set; }
|
||||||
|
public List<ChapterData> Chapters { get; private set; }
|
||||||
|
|
||||||
|
IReadOnlyList<ChapterData> IReadOnlyMetaData.Chapters => this.Chapters;
|
||||||
|
IReadOnlyDictionary<string, string> IReadOnlyMetaData.Entries => this.Entries;
|
||||||
|
|
||||||
|
public MetaData()
|
||||||
|
{
|
||||||
|
Entries = new Dictionary<string, string>();
|
||||||
|
Chapters = new List<ChapterData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetaData(MetaData cloneSource)
|
||||||
|
{
|
||||||
|
Entries = new Dictionary<string, string>(cloneSource.Entries);
|
||||||
|
Chapters = cloneSource.Chapters
|
||||||
|
.Select(x => new ChapterData
|
||||||
|
(
|
||||||
|
start: x.Start,
|
||||||
|
end: x.End,
|
||||||
|
title: x.Title
|
||||||
|
))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
109
FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs
Normal file
109
FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Builders.MetaData
|
||||||
|
{
|
||||||
|
public class MetaDataBuilder
|
||||||
|
{
|
||||||
|
private MetaData _metaData = new MetaData();
|
||||||
|
|
||||||
|
public MetaDataBuilder WithEntry(string key, string entry)
|
||||||
|
{
|
||||||
|
if (_metaData.Entries.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
entry = String.Concat(value, "; ", entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
_metaData.Entries[key] = entry;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetaDataBuilder WithEntry(string key, params string[] values)
|
||||||
|
=> this.WithEntry(key, String.Join("; ", values));
|
||||||
|
|
||||||
|
public MetaDataBuilder WithEntry(string key, IEnumerable<string> values)
|
||||||
|
=> this.WithEntry(key, String.Join("; ", values));
|
||||||
|
|
||||||
|
public MetaDataBuilder AddChapter(ChapterData chapterData)
|
||||||
|
{
|
||||||
|
_metaData.Chapters.Add(chapterData);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetaDataBuilder AddChapters<T>(IEnumerable<T> values, Func<T, (TimeSpan duration, string title)> chapterGetter)
|
||||||
|
{
|
||||||
|
foreach (T value in values)
|
||||||
|
{
|
||||||
|
var (duration, title) = chapterGetter(value);
|
||||||
|
AddChapter(duration, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null)
|
||||||
|
{
|
||||||
|
var start = _metaData.Chapters.LastOrDefault()?.End ?? TimeSpan.Zero;
|
||||||
|
var end = start + duration;
|
||||||
|
title = String.IsNullOrEmpty(title) ? $"Chapter {_metaData.Chapters.Count + 1}" : title;
|
||||||
|
|
||||||
|
_metaData.Chapters.Add(new ChapterData
|
||||||
|
(
|
||||||
|
start: start,
|
||||||
|
end: end,
|
||||||
|
title: title ?? String.Empty
|
||||||
|
));
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
//major_brand=M4A
|
||||||
|
public MetaDataBuilder WithMajorBrand(string value) => WithEntry("major_brand", value);
|
||||||
|
|
||||||
|
//minor_version=512
|
||||||
|
public MetaDataBuilder WithMinorVersion(string value) => WithEntry("minor_version", value);
|
||||||
|
|
||||||
|
//compatible_brands=M4A isomiso2
|
||||||
|
public MetaDataBuilder WithCompatibleBrands(string value) => WithEntry("compatible_brands", value);
|
||||||
|
|
||||||
|
//copyright=©2017 / 2019 Dennis E. Taylor / Random House Audio / Wilhelm Heyne Verlag. Übersetzung von Urban Hofstetter (P)2019 Random House Audio
|
||||||
|
public MetaDataBuilder WithCopyright(string value) => WithEntry("copyright", value);
|
||||||
|
|
||||||
|
//title=Alle diese Welten: Bobiverse 3
|
||||||
|
public MetaDataBuilder WithTitle(string value) => WithEntry("title", value);
|
||||||
|
|
||||||
|
//artist=Dennis E. Taylor
|
||||||
|
public MetaDataBuilder WithArtists(params string[] value) => WithEntry("artist", value);
|
||||||
|
public MetaDataBuilder WithArtists(IEnumerable<string> value) => WithEntry("artist", value);
|
||||||
|
|
||||||
|
//composer=J. K. Rowling
|
||||||
|
public MetaDataBuilder WithComposers(params string[] value) => WithEntry("composer", value);
|
||||||
|
public MetaDataBuilder WithComposers(IEnumerable<string> value) => WithEntry("composer", value);
|
||||||
|
|
||||||
|
//album_artist=Dennis E. Taylor
|
||||||
|
public MetaDataBuilder WithAlbumArtists(params string[] value) => WithEntry("album_artist", value);
|
||||||
|
public MetaDataBuilder WithAlbumArtists(IEnumerable<string> value) => WithEntry("album_artist", value);
|
||||||
|
|
||||||
|
//album=Alle diese Welten: Bobiverse 3
|
||||||
|
public MetaDataBuilder WithAlbum(string value) => WithEntry("album", value);
|
||||||
|
|
||||||
|
//date=2019
|
||||||
|
public MetaDataBuilder WithDate(string value) => WithEntry("date", value);
|
||||||
|
|
||||||
|
//genre=Hörbuch
|
||||||
|
public MetaDataBuilder WithGenres(params string[] value) => WithEntry("genre", value);
|
||||||
|
public MetaDataBuilder WithGenres(IEnumerable<string> value) => WithEntry("genre", value);
|
||||||
|
|
||||||
|
//comment=Chapter 200
|
||||||
|
public MetaDataBuilder WithComments(params string[] value) => WithEntry("comment", value);
|
||||||
|
public MetaDataBuilder WithComments(IEnumerable<string> value) => WithEntry("comment", value);
|
||||||
|
|
||||||
|
//encoder=Lavf58.47.100
|
||||||
|
public MetaDataBuilder WithEncoder(string value) => WithEntry("encoder", value);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public ReadOnlyMetaData Build() => new ReadOnlyMetaData(_metaData);
|
||||||
|
}
|
||||||
|
}
|
38
FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs
Normal file
38
FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Builders.MetaData
|
||||||
|
{
|
||||||
|
public class MetaDataSerializer
|
||||||
|
{
|
||||||
|
public static readonly MetaDataSerializer Instance = new MetaDataSerializer();
|
||||||
|
|
||||||
|
public string Serialize(IReadOnlyMetaData metaData)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder()
|
||||||
|
.AppendLine(";FFMETADATA1");
|
||||||
|
|
||||||
|
foreach (var value in metaData.Entries)
|
||||||
|
{
|
||||||
|
sb.AppendLine($"{value.Key}={value.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
int chapterNumber = 0;
|
||||||
|
foreach (var chapter in metaData.Chapters ?? Enumerable.Empty<ChapterData>())
|
||||||
|
{
|
||||||
|
chapterNumber++;
|
||||||
|
var title = string.IsNullOrEmpty(chapter.Title) ? $"Chapter {chapterNumber}" : chapter.Title;
|
||||||
|
|
||||||
|
sb
|
||||||
|
.AppendLine("[CHAPTER]")
|
||||||
|
.AppendLine($"TIMEBASE=1/1000")
|
||||||
|
.AppendLine($"START={(int)chapter.Start.TotalMilliseconds}")
|
||||||
|
.AppendLine($"END={(int)chapter.End.TotalMilliseconds}")
|
||||||
|
.AppendLine($"title={title}")
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs
Normal file
25
FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Builders.MetaData
|
||||||
|
{
|
||||||
|
public class ReadOnlyMetaData : IReadOnlyMetaData
|
||||||
|
{
|
||||||
|
public IReadOnlyDictionary<string, string> Entries { get; private set; }
|
||||||
|
public IReadOnlyList<ChapterData> Chapters { get; private set; }
|
||||||
|
|
||||||
|
public ReadOnlyMetaData(MetaData metaData)
|
||||||
|
{
|
||||||
|
Entries = new Dictionary<string, string>(metaData.Entries);
|
||||||
|
Chapters = metaData.Chapters
|
||||||
|
.Select(x => new ChapterData
|
||||||
|
(
|
||||||
|
start: x.Start,
|
||||||
|
end: x.End,
|
||||||
|
title: x.Title
|
||||||
|
))
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FFMpegCore.Arguments;
|
using FFMpegCore.Arguments;
|
||||||
|
using FFMpegCore.Builders.MetaData;
|
||||||
using FFMpegCore.Pipes;
|
using FFMpegCore.Pipes;
|
||||||
|
|
||||||
namespace FFMpegCore
|
namespace FFMpegCore
|
||||||
|
@ -38,6 +39,8 @@ public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalArguments> configure
|
||||||
public FFMpegArguments AddFileInput(FileInfo fileInfo, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments);
|
public FFMpegArguments AddFileInput(FileInfo fileInfo, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments);
|
||||||
public FFMpegArguments AddUrlInput(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments);
|
public FFMpegArguments AddUrlInput(Uri uri, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments);
|
||||||
public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments);
|
public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments);
|
||||||
|
public FFMpegArguments AddMetaData(string content, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new MetaDataArgument(content), addArguments);
|
||||||
|
public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments);
|
||||||
|
|
||||||
private FFMpegArguments WithInput(IInputArgument inputArgument, Action<FFMpegArgumentOptions>? addArguments)
|
private FFMpegArguments WithInput(IInputArgument inputArgument, Action<FFMpegArgumentOptions>? addArguments)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue