Skip to content

Commit

Permalink
Fix chat jsons and mp4 metadata lacking any chapters for VODs with 1 …
Browse files Browse the repository at this point in the history
…chapter and clips (#875)

* Create GetOrGenerateVideoChapters and update GetVideoInfo query

* Make ChatDownloader & VideoDownloader use GetOrGenerateVideoChapters

* Use the same Game object for Video & Clip info responses

* Generate a bogus chapter for clips
  • Loading branch information
ScrubN authored Oct 27, 2023
1 parent 8db2d26 commit 8104934
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 23 deletions.
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

0 comments on commit 8104934

Please sign in to comment.