diff --git a/TwitchDownloaderCore/ClipDownloader.cs b/TwitchDownloaderCore/ClipDownloader.cs index 1a85ccbf..0130f5fe 100644 --- a/TwitchDownloaderCore/ClipDownloader.cs +++ b/TwitchDownloaderCore/ClipDownloader.cs @@ -224,8 +224,7 @@ private async Task EncodeClipWithMetadata(string inputFile, string destinationFi Process process = null; try { - await FfmpegMetadata.SerializeAsync(metadataFile, clipMetadata.broadcaster?.displayName, downloadOptions.Id, clipMetadata.title, clipMetadata.createdAt, clipMetadata.viewCount, - videoMomentEdges: new[] { clipChapter }, cancellationToken: cancellationToken); + await FfmpegMetadata.SerializeAsync(metadataFile, downloadOptions.Id, clipMetadata, new[] { clipChapter }); process = new Process { diff --git a/TwitchDownloaderCore/Tools/FfmpegMetadata.cs b/TwitchDownloaderCore/Tools/FfmpegMetadata.cs index 12017f75..7d29df09 100644 --- a/TwitchDownloaderCore/Tools/FfmpegMetadata.cs +++ b/TwitchDownloaderCore/Tools/FfmpegMetadata.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Text; -using System.Threading; using System.Threading.Tasks; using TwitchDownloaderCore.TwitchObjects.Gql; @@ -13,38 +14,56 @@ public static class FfmpegMetadata { private const string LINE_FEED = "\u000A"; - public static async Task SerializeAsync(string filePath, string streamerName, string videoId, string videoTitle, DateTime videoCreation, int viewCount, string videoDescription = null, - TimeSpan startOffset = default, TimeSpan videoLength = default, IEnumerable videoMomentEdges = null, CancellationToken cancellationToken = default) + public static async Task SerializeAsync(string filePath, string videoId, VideoInfo videoInfo, TimeSpan startOffset, TimeSpan videoLength, IEnumerable videoMomentEdges) { await using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read); await using var sw = new StreamWriter(fs) { NewLine = LINE_FEED }; - await SerializeGlobalMetadata(sw, streamerName, videoId, videoTitle, videoCreation, viewCount, videoDescription); - await fs.FlushAsync(cancellationToken); + var streamer = GetUserName(videoInfo.owner.displayName, videoInfo.owner.login); + var description = videoInfo.description?.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd(); + await SerializeGlobalMetadata(sw, streamer, videoId, videoInfo.title, videoInfo.createdAt, videoInfo.viewCount, description, videoInfo.game?.displayName); await SerializeChapters(sw, videoMomentEdges, startOffset, videoLength); - await fs.FlushAsync(cancellationToken); } - private static async Task SerializeGlobalMetadata(StreamWriter sw, string streamerName, string videoId, string videoTitle, DateTime videoCreation, int viewCount, string videoDescription) + public static async Task SerializeAsync(string filePath, string videoId, Clip clip, IEnumerable videoMomentEdges) { + await using var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read); + await using var sw = new StreamWriter(fs) { NewLine = LINE_FEED }; + + var streamer = GetUserName(clip.broadcaster.displayName, clip.broadcaster.login); + var clipper = GetUserName(clip.curator.displayName, clip.curator.login); + await SerializeGlobalMetadata(sw, streamer, videoId, clip.title, clip.createdAt, clip.viewCount, game: clip.game?.displayName, clipper: clipper); + + await SerializeChapters(sw, videoMomentEdges); + } + + private static async Task SerializeGlobalMetadata(StreamWriter sw, [AllowNull] string streamer, string id, string title, DateTime createdAt, int viewCount, [AllowNull] string description = null, [AllowNull] string game = null, + [AllowNull] string clipper = null) + { + // ReSharper disable once StringLiteralTypo await sw.WriteLineAsync(";FFMETADATA1"); - await sw.WriteLineAsync($"title={SanitizeKeyValue(videoTitle)} ({SanitizeKeyValue(videoId)})"); - if (!string.IsNullOrWhiteSpace(streamerName)) - await sw.WriteLineAsync($"artist={SanitizeKeyValue(streamerName)}"); - await sw.WriteLineAsync($"date={videoCreation:yyyy}"); // The 'date' key becomes 'year' in most formats + await sw.WriteLineAsync($"title={EscapeMetadataValue(title)} ({EscapeMetadataValue(id)})"); + if (!string.IsNullOrWhiteSpace(streamer)) + await sw.WriteLineAsync($"artist={EscapeMetadataValue(streamer)}"); + await sw.WriteLineAsync($"date={createdAt:yyyy}"); // The 'date' key becomes 'year' in most formats + if (!string.IsNullOrWhiteSpace(game)) + await sw.WriteLineAsync($"genre={game}"); await sw.WriteAsync(@"comment="); - if (!string.IsNullOrWhiteSpace(videoDescription)) + if (!string.IsNullOrWhiteSpace(description)) { - await sw.WriteLineAsync(@$"{SanitizeKeyValue(videoDescription.TrimEnd())}\"); + // We could use the 'description' key, but so few media players support mp4 descriptions that users would probably think it was missing + await sw.WriteLineAsync(@$"{EscapeMetadataValue(description.TrimEnd())}\"); await sw.WriteLineAsync(@"------------------------\"); } - await sw.WriteLineAsync(@$"Originally aired: {SanitizeKeyValue(videoCreation.ToString("u"))}\"); - await sw.WriteLineAsync(@$"Video id: {SanitizeKeyValue(videoId)}\"); + if (!string.IsNullOrWhiteSpace(clipper)) + await sw.WriteLineAsync($@"Clipped by: {EscapeMetadataValue(clipper)}\"); + await sw.WriteLineAsync(@$"Created at: {EscapeMetadataValue(createdAt.ToString("u"))}\"); + await sw.WriteLineAsync(@$"Video id: {EscapeMetadataValue(id)}\"); await sw.WriteLineAsync(@$"Views: {viewCount}"); } - private static async Task SerializeChapters(StreamWriter sw, IEnumerable videoMomentEdges, TimeSpan startOffset, TimeSpan videoLength) + private static async Task SerializeChapters(StreamWriter sw, IEnumerable videoMomentEdges, TimeSpan startOffset = default, TimeSpan videoLength = default) { if (videoMomentEdges is null) { @@ -83,12 +102,35 @@ private static async Task SerializeChapters(StreamWriter sw, IEnumerable