Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix chat jsons and mp4 metadata lacking any chapters for VODs with 1 chapter and clips #875

Merged
merged 4 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion TwitchDownloaderCore/ChatDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,8 @@ public async Task DownloadAsync(IProgress<ProgressReport> progress, Cancellation
viewCount = videoInfoResponse.data.video.viewCount;
game = videoInfoResponse.data.video.game?.displayName ?? "Unknown";

GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetVideoChapters(int.Parse(videoId));
GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetOrGenerateVideoChapters(int.Parse(videoId), videoInfoResponse.data.video);
chatRoot.video.chapters.EnsureCapacity(videoChapterResponse.data.video.moments.edges.Count);
foreach (var responseChapter in videoChapterResponse.data.video.moments.edges)
{
chatRoot.video.chapters.Add(new VideoChapter
Expand Down Expand Up @@ -329,6 +330,21 @@ public async Task DownloadAsync(IProgress<ProgressReport> progress, Cancellation
viewCount = clipInfoResponse.data.clip.viewCount;
game = clipInfoResponse.data.clip.game?.displayName ?? "Unknown";
connectionCount = 1;

var clipChapter = TwitchHelper.GenerateClipChapter(clipInfoResponse.data.clip);
chatRoot.video.chapters.Add(new VideoChapter
{
id = clipChapter.node.id,
startMilliseconds = clipChapter.node.positionMilliseconds,
lengthMilliseconds = clipChapter.node.durationMilliseconds,
_type = clipChapter.node._type,
description = clipChapter.node.description,
subDescription = clipChapter.node.subDescription,
thumbnailUrl = clipChapter.node.thumbnailURL,
gameId = clipChapter.node.details.game?.id,
gameDisplayName = clipChapter.node.details.game?.displayName,
gameBoxArtUrl = clipChapter.node.details.game?.boxArtURL
});
}

chatRoot.video.id = videoId;
Expand Down
7 changes: 4 additions & 3 deletions TwitchDownloaderCore/ClipDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ void DownloadProgressHandler(StreamCopyProgress streamProgress)
_progress.Report(new ProgressReport(ReportType.NewLineStatus, "Encoding Clip Metadata 0%"));
_progress.Report(new ProgressReport(0));

await EncodeClipWithMetadata(tempFile, downloadOptions.Filename, clipInfo.data.clip, cancellationToken);
var clipChapter = TwitchHelper.GenerateClipChapter(clipInfo.data.clip);
await EncodeClipWithMetadata(tempFile, downloadOptions.Filename, clipInfo.data.clip, clipChapter, cancellationToken);

_progress.Report(new ProgressReport(ReportType.SameLineStatus, "Encoding Clip Metadata 100%"));
_progress.Report(new ProgressReport(100));
Expand Down Expand Up @@ -137,14 +138,14 @@ private static async Task DownloadFileTaskAsync(string url, string destinationFi
}
}

private async Task EncodeClipWithMetadata(string inputFile, string destinationFile, Clip clipMetadata, CancellationToken cancellationToken)
private async Task EncodeClipWithMetadata(string inputFile, string destinationFile, Clip clipMetadata, VideoMomentEdge clipChapter, CancellationToken cancellationToken)
{
var metadataFile = $"{Path.GetFileName(inputFile)}_metadata.txt";

try
{
await FfmpegMetadata.SerializeAsync(metadataFile, clipMetadata.broadcaster.displayName, downloadOptions.Id, clipMetadata.title, clipMetadata.createdAt, clipMetadata.viewCount,
cancellationToken: cancellationToken);
videoMomentEdges: new[] { clipChapter }, cancellationToken: cancellationToken);

var process = new Process
{
Expand Down
4 changes: 2 additions & 2 deletions TwitchDownloaderCore/Tools/FfmpegMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ 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,
double startOffsetSeconds = 0, List<VideoMomentEdge> videoMomentEdges = null, CancellationToken cancellationToken = default)
double startOffsetSeconds = 0, IEnumerable<VideoMomentEdge> videoMomentEdges = null, CancellationToken cancellationToken = default)
{
await using var fs = new FileStream(filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None);
await using var sw = new StreamWriter(fs) { NewLine = LINE_FEED };
Expand Down Expand Up @@ -43,7 +43,7 @@ private static async Task SerializeGlobalMetadata(StreamWriter sw, string stream
await sw.WriteLineAsync(@$"Views: {viewCount}");
}

private static async Task SerializeChapters(StreamWriter sw, List<VideoMomentEdge> videoMomentEdges, double startOffsetSeconds)
private static async Task SerializeChapters(StreamWriter sw, IEnumerable<VideoMomentEdge> videoMomentEdges, double startOffsetSeconds)
{
if (videoMomentEdges is null)
{
Expand Down
55 changes: 53 additions & 2 deletions TwitchDownloaderCore/TwitchHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static async Task<GqlVideoResponse> GetVideoInfo(int videoId)
{
RequestUri = new Uri("https://gql.twitch.tv/gql"),
Method = HttpMethod.Post,
Content = new StringContent("{\"query\":\"query{video(id:\\\"" + videoId + "\\\"){title,thumbnailURLs(height:180,width:320),createdAt,lengthSeconds,owner{id,displayName},viewCount,game{id,displayName},description}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
Content = new StringContent("{\"query\":\"query{video(id:\\\"" + videoId + "\\\"){title,thumbnailURLs(height:180,width:320),createdAt,lengthSeconds,owner{id,displayName},viewCount,game{id,displayName,boxArtURL},description}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
};
request.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko");
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
Expand Down Expand Up @@ -72,7 +72,7 @@ public static async Task<GqlClipResponse> GetClipInfo(object clipId)
{
RequestUri = new Uri("https://gql.twitch.tv/gql"),
Method = HttpMethod.Post,
Content = new StringContent("{\"query\":\"query{clip(slug:\\\"" + clipId + "\\\"){title,thumbnailURL,createdAt,durationSeconds,broadcaster{id,displayName},videoOffsetSeconds,video{id},viewCount,game{id,displayName}}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
Content = new StringContent("{\"query\":\"query{clip(slug:\\\"" + clipId + "\\\"){title,thumbnailURL,createdAt,durationSeconds,broadcaster{id,displayName},videoOffsetSeconds,video{id},viewCount,game{id,displayName,boxArtURL}}}\",\"variables\":{}}", Encoding.UTF8, "application/json")
};
request.Headers.Add("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko");
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
Expand Down Expand Up @@ -912,6 +912,7 @@ public static async Task<GqlUserInfoResponse> GetUserInfo(List<string> idList)
return imageBytes;
}

/// <remarks>When a given video has only 1 chapter, data.video.moments.edges will be empty.</remarks>
public static async Task<GqlVideoChapterResponse> GetVideoChapters(int videoId)
{
var request = new HttpRequestMessage()
Expand All @@ -925,5 +926,55 @@ public static async Task<GqlVideoChapterResponse> GetVideoChapters(int videoId)
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<GqlVideoChapterResponse>();
}

public static async Task<GqlVideoChapterResponse> GetOrGenerateVideoChapters(int videoId, VideoInfo videoInfo)
{
var chapterResponse = await GetVideoChapters(videoId);

// Video has only 1 chapter, generate a bogus video chapter with the information we have available.
if (chapterResponse.data.video.moments.edges.Count == 0)
{
chapterResponse.data.video.moments.edges.Add(
GenerateVideoMomentEdge(0, videoInfo.lengthSeconds, videoInfo.game?.id, videoInfo.game?.displayName, videoInfo.game?.displayName, videoInfo.game?.boxArtURL
));
}

return chapterResponse;
}

public static VideoMomentEdge GenerateClipChapter(Clip clipInfo)
{
return GenerateVideoMomentEdge(0, clipInfo.durationSeconds, clipInfo.game?.id, clipInfo.game?.displayName, clipInfo.game?.displayName, clipInfo.game?.boxArtURL);
}

private static VideoMomentEdge GenerateVideoMomentEdge(int startSeconds, int lengthSeconds, string gameId = null, string gameDisplayName = null, string gameDescription = null, string gameBoxArtUrl = null)
{
gameId ??= "-1";
gameDisplayName ??= "Unknown";
gameDescription ??= "Unknown";
gameBoxArtUrl ??= "";

return new VideoMomentEdge
{
node = new VideoMoment
{
id = "",
_type = "GAME_CHANGE",
positionMilliseconds = startSeconds,
durationMilliseconds = lengthSeconds * 1000,
description = gameDescription,
subDescription = "",
details = new GameChangeMomentDetails
{
game = new Game
{
id = gameId,
displayName = gameDisplayName,
boxArtURL = gameBoxArtUrl.Replace("{width}", "40").Replace("{height}", "53")
}
}
}
};
}
}
}
8 changes: 1 addition & 7 deletions TwitchDownloaderCore/TwitchObjects/Gql/GqlClipResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ public class ClipVideo
public string id { get; set; }
}

public class ClipGame
{
public string id { get; set; }
public string displayName { get; set; }
}

public class Clip
{
public string title { get; set; }
Expand All @@ -29,7 +23,7 @@ public class Clip
public int? videoOffsetSeconds { get; set; }
public ClipVideo video { get; set; }
public int viewCount { get; set; }
public ClipGame game { get; set; }
public Game game { get; set; }
}

public class ClipData
Expand Down
8 changes: 1 addition & 7 deletions TwitchDownloaderCore/TwitchObjects/Gql/GqlVideoResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ public class VideoOwner
public string displayName { get; set; }
}

public class VideoGame
{
public string id { get; set; }
public string displayName { get; set; }
}

public class VideoInfo
{
public string title { get; set; }
Expand All @@ -23,7 +17,7 @@ public class VideoInfo
public int lengthSeconds { get; set; }
public VideoOwner owner { get; set; }
public int viewCount { get; set; }
public VideoGame game { get; set; }
public Game game { get; set; }
/// <remarks>
/// Some values, such as newlines, are repeated twice for some reason.
/// This can be filtered out with: <code>description?.Replace(" \n", "\n").Replace("\n\n", "\n").TrimEnd()</code>
Expand Down
2 changes: 1 addition & 1 deletion TwitchDownloaderCore/VideoDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task DownloadAsync(CancellationToken cancellationToken)
throw new NullReferenceException("Invalid VOD, deleted/expired VOD possibly?");
}

GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetVideoChapters(downloadOptions.Id);
GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetOrGenerateVideoChapters(downloadOptions.Id, videoInfoResponse.data.video);

var (playlistUrl, bandwidth) = await GetPlaylistUrl();
var baseUrl = new Uri(playlistUrl[..(playlistUrl.LastIndexOf('/') + 1)], UriKind.Absolute);
Expand Down
Loading