From e3eb2f2056ec7ccb0b36d4c2136e4fac4743ccbf Mon Sep 17 00:00:00 2001 From: Weirdo Date: Tue, 21 Dec 2021 00:10:59 +0100 Subject: [PATCH 1/6] Added MetaDataArgument --- .../FFMpeg/Arguments/MetaDataArgument.cs | 27 +++++++++++++++++++ FFMpegCore/FFMpeg/FFMpegArguments.cs | 1 + 2 files changed, 28 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs diff --git a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs new file mode 100644 index 0000000..7e9ffc6 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs @@ -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); + } +} diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 847e68c..45750c6 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -38,6 +38,7 @@ public FFMpegArguments WithGlobalOptions(Action configure public FFMpegArguments AddFileInput(FileInfo fileInfo, Action? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments); public FFMpegArguments AddUrlInput(Uri uri, Action? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments); + public FFMpegArguments AddMetaData(string content, Action? addArguments = null) => WithInput(new MetaDataArgument(content), addArguments); private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments) { From 2605ac1a54dcc5b76325bcc4045bc20e4b64fba4 Mon Sep 17 00:00:00 2001 From: Weirdo Date: Tue, 21 Dec 2021 00:34:33 +0100 Subject: [PATCH 2/6] Added MetadataBuilder --- .../FFMpeg/Builders/MetaData/ChapterData.cs | 18 ++++ .../Builders/MetaData/IReadOnlyMetaData.cs | 11 +++ .../FFMpeg/Builders/MetaData/MetaData.cs | 33 ++++++++ .../Builders/MetaData/MetaDataBuilder.cs | 82 +++++++++++++++++++ .../Builders/MetaData/MetaDataSerializer.cs | 38 +++++++++ .../Builders/MetaData/ReadOnlyMetaData.cs | 25 ++++++ FFMpegCore/FFMpeg/FFMpegArguments.cs | 2 + 7 files changed, 209 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs create mode 100644 FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs new file mode 100644 index 0000000..24ad2b6 --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs new file mode 100644 index 0000000..fd55ea7 --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace FFMpegCore.Builders.MetaData +{ + + public interface IReadOnlyMetaData + { + IReadOnlyList Chapters { get; } + IReadOnlyDictionary Entries { get; } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs new file mode 100644 index 0000000..2efc696 --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FFMpegCore.Builders.MetaData +{ + public class MetaData : IReadOnlyMetaData + { + public Dictionary Entries { get; private set; } + public List Chapters { get; private set; } + + IReadOnlyList IReadOnlyMetaData.Chapters => this.Chapters; + IReadOnlyDictionary IReadOnlyMetaData.Entries => this.Entries; + + public MetaData() + { + Entries = new Dictionary(); + Chapters = new List(); + } + + public MetaData(MetaData cloneSource) + { + Entries = new Dictionary(cloneSource.Entries); + Chapters = cloneSource.Chapters + .Select(x => new ChapterData + ( + start: x.Start, + end: x.End, + title: x.Title + )) + .ToList(); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs new file mode 100644 index 0000000..90513ac --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; + +namespace FFMpegCore.Builders.MetaData +{ + public class MetaDataBuilder + { + private MetaData _metaData = new MetaData(); + + public MetaDataBuilder WithEntry(string key, string value) + { + _metaData.Entries[key] = value; + return this; + } + + public MetaDataBuilder AddChapter(ChapterData chapterData) + { + _metaData.Chapters.Add(chapterData); + 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 + )); + + 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 WithArtist(string value) => WithEntry("artist", value); + + //composer=J. K. Rowling + public MetaDataBuilder WithComposer(string value) => WithEntry("composer", value); + + //album_artist=Dennis E. Taylor + public MetaDataBuilder WithAlbumArtist(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 WithGenre(string value) => WithEntry("genre", value); + + //comment=Chapter 200 + public MetaDataBuilder WithComment(string value) => WithEntry("comment", value); + + //encoder=Lavf58.47.100 + public MetaDataBuilder WithEncoder(string value) => WithEntry("encoder", value); + + public ReadOnlyMetaData Build() + { + return new MetaData(_metaData); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs new file mode 100644 index 0000000..1a6f176 --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs @@ -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()) + { + 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(); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs new file mode 100644 index 0000000..ff9bae9 --- /dev/null +++ b/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; + +namespace FFMpegCore.Builders.MetaData +{ + public class ReadOnlyMetaData : IReadOnlyMetaData + { + public IReadOnlyDictionary Entries { get; private set; } + public IReadOnlyList Chapters { get; private set; } + + public ReadOnlyMetaData(MetaData metaData) + { + Entries = new Dictionary(metaData.Entries); + Chapters = metaData.Chapters + .Select(x => new ChapterData + ( + start: x.Start, + end: x.End, + title: x.Title + )) + .ToList() + .AsReadOnly(); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 45750c6..6c9784d 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using FFMpegCore.Arguments; +using FFMpegCore.Builders.MetaData; using FFMpegCore.Pipes; namespace FFMpegCore @@ -39,6 +40,7 @@ public FFMpegArguments WithGlobalOptions(Action configure public FFMpegArguments AddUrlInput(Uri uri, Action? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments); public FFMpegArguments AddMetaData(string content, Action? addArguments = null) => WithInput(new MetaDataArgument(content), addArguments); + public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action? addArguments = null) => WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments); private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments) { From e2dc91660cef004cba6ca3ccba9ef9c822f874d3 Mon Sep 17 00:00:00 2001 From: Weirdo Date: Tue, 21 Dec 2021 00:40:45 +0100 Subject: [PATCH 3/6] Implemented AddChapters for convenience --- .../FFMpeg/Builders/MetaData/MetaDataBuilder.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs index 90513ac..7409543 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace FFMpegCore.Builders.MetaData @@ -19,6 +20,17 @@ public MetaDataBuilder AddChapter(ChapterData chapterData) return this; } + public MetaDataBuilder AddChapters(IEnumerable values, Func 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; From 6194dc4e4b3eb80875b0d2331b7a1c230f0cda9f Mon Sep 17 00:00:00 2001 From: Weirdo Date: Tue, 21 Dec 2021 00:42:20 +0100 Subject: [PATCH 4/6] Returning ReadOnlyMetaData instead of MetaData --- FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs index 7409543..8c7360b 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs @@ -88,7 +88,7 @@ public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) public ReadOnlyMetaData Build() { - return new MetaData(_metaData); + return new ReadOnlyMetaData(_metaData); } } } \ No newline at end of file From 9b670f11b28705831375a927ca78687d257fb274 Mon Sep 17 00:00:00 2001 From: Weirdo Date: Tue, 4 Jan 2022 16:02:47 +0100 Subject: [PATCH 5/6] Revert "Added string escape for DemuxConcatArgument" This reverts commit ecbdae40af53d99581204342f6f44318774b495e. From f560244628a147dd24a8a257f2e15cd4b5833881 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker Date: Thu, 6 Jan 2022 19:38:37 +0100 Subject: [PATCH 6/6] Refactored, added unit test --- FFMpegCore.Test/MetaDataBuilderTests.cs | 54 +++++++++++++++++++ .../Builders/MetaData/MetaDataBuilder.cs | 39 +++++++++----- 2 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 FFMpegCore.Test/MetaDataBuilderTests.cs diff --git a/FFMpegCore.Test/MetaDataBuilderTests.cs b/FFMpegCore.Test/MetaDataBuilderTests.cs new file mode 100644 index 0000000..5f0a144 --- /dev/null +++ b/FFMpegCore.Test/MetaDataBuilderTests.cs @@ -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)); + } + } +} diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs index 8c7360b..29c13c2 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs @@ -8,12 +8,23 @@ public class MetaDataBuilder { private MetaData _metaData = new MetaData(); - public MetaDataBuilder WithEntry(string key, string value) + public MetaDataBuilder WithEntry(string key, string entry) { - _metaData.Entries[key] = value; + 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 values) + => this.WithEntry(key, String.Join("; ", values)); + public MetaDataBuilder AddChapter(ChapterData chapterData) { _metaData.Chapters.Add(chapterData); @@ -41,7 +52,7 @@ public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) ( start: start, end: end, - title: title + title: title ?? String.Empty )); return this; @@ -63,13 +74,16 @@ public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) public MetaDataBuilder WithTitle(string value) => WithEntry("title", value); //artist=Dennis E. Taylor - public MetaDataBuilder WithArtist(string value) => WithEntry("artist", value); + public MetaDataBuilder WithArtists(params string[] value) => WithEntry("artist", value); + public MetaDataBuilder WithArtists(IEnumerable value) => WithEntry("artist", value); //composer=J. K. Rowling - public MetaDataBuilder WithComposer(string value) => WithEntry("composer", value); + public MetaDataBuilder WithComposers(params string[] value) => WithEntry("composer", value); + public MetaDataBuilder WithComposers(IEnumerable value) => WithEntry("composer", value); //album_artist=Dennis E. Taylor - public MetaDataBuilder WithAlbumArtist(string value) => WithEntry("album_artist", value); + public MetaDataBuilder WithAlbumArtists(params string[] value) => WithEntry("album_artist", value); + public MetaDataBuilder WithAlbumArtists(IEnumerable value) => WithEntry("album_artist", value); //album=Alle diese Welten: Bobiverse 3 public MetaDataBuilder WithAlbum(string value) => WithEntry("album", value); @@ -78,17 +92,18 @@ public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) public MetaDataBuilder WithDate(string value) => WithEntry("date", value); //genre=Hörbuch - public MetaDataBuilder WithGenre(string value) => WithEntry("genre", value); + public MetaDataBuilder WithGenres(params string[] value) => WithEntry("genre", value); + public MetaDataBuilder WithGenres(IEnumerable value) => WithEntry("genre", value); //comment=Chapter 200 - public MetaDataBuilder WithComment(string value) => WithEntry("comment", value); + public MetaDataBuilder WithComments(params string[] value) => WithEntry("comment", value); + public MetaDataBuilder WithComments(IEnumerable value) => WithEntry("comment", value); //encoder=Lavf58.47.100 public MetaDataBuilder WithEncoder(string value) => WithEntry("encoder", value); - public ReadOnlyMetaData Build() - { - return new ReadOnlyMetaData(_metaData); - } + + + public ReadOnlyMetaData Build() => new ReadOnlyMetaData(_metaData); } } \ No newline at end of file