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

Print a log message if there may not be enough space to download/finalize a VOD #770

Merged
merged 1 commit into from
Aug 2, 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
36 changes: 36 additions & 0 deletions TwitchDownloaderCore/Tools/VideoSizeEstimator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;

namespace TwitchDownloaderCore.Tools
{
public static class VideoSizeEstimator
{
public static string StringifyByteCount(long sizeInBytes)
{
const long ONE_KIBIBYTE = 1024;
const long ONE_MEBIBYTE = 1_048_576;
const long ONE_GIBIBYTE = 1_073_741_824;

return sizeInBytes switch
{
< 1 => "",
< ONE_KIBIBYTE => $"{sizeInBytes}B",
< ONE_MEBIBYTE => $"{(float)sizeInBytes / ONE_KIBIBYTE:F1}KiB",
< ONE_GIBIBYTE => $"{(float)sizeInBytes / ONE_MEBIBYTE:F1}MiB",
_ => $"{(float)sizeInBytes / ONE_GIBIBYTE:F1}GiB",
};
}

/// <returns>An estimate of the final video download size in bytes.</returns>
public static long EstimateVideoSize(int bandwidth, TimeSpan startTime, TimeSpan endTime)
{
if (bandwidth < 1)
return 0;
if (endTime < startTime)
return 0;

var totalTime = endTime - startTime;
return (long)(bandwidth / 8d * totalTime.TotalSeconds);
}

}
}
47 changes: 42 additions & 5 deletions TwitchDownloaderCore/VideoDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,12 @@ public async Task DownloadAsync(CancellationToken cancellationToken)

GqlVideoChapterResponse videoChapterResponse = await TwitchHelper.GetVideoChapters(downloadOptions.Id);

string playlistUrl = await GetPlaylistUrl();
var (playlistUrl, bandwidth) = await GetPlaylistUrl();
string baseUrl = playlistUrl.Substring(0, playlistUrl.LastIndexOf('/') + 1);

var videoLength = TimeSpan.FromSeconds(videoInfoResponse.data.video.lengthSeconds);
CheckAvailableStorageSpace(bandwidth, videoLength);

List<KeyValuePair<string, double>> videoList = new List<KeyValuePair<string, double>>();
(List<string> videoPartsList, double vodAge) = await GetVideoPartsList(playlistUrl, videoList, cancellationToken);

Expand Down Expand Up @@ -132,6 +135,36 @@ await FfmpegMetadata.SerializeAsync(metadataPath, videoInfoResponse.data.video.o
}
}

private void CheckAvailableStorageSpace(int bandwidth, TimeSpan videoLength)
{
var videoSizeInBytes = VideoSizeEstimator.EstimateVideoSize(bandwidth,
downloadOptions.CropBeginning ? TimeSpan.FromSeconds(downloadOptions.CropBeginningTime) : TimeSpan.Zero,
downloadOptions.CropEnding ? TimeSpan.FromSeconds(downloadOptions.CropEndingTime) : videoLength);
var tempFolderDrive = DriveHelper.GetOutputDrive(downloadOptions.TempFolder);
var destinationDrive = DriveHelper.GetOutputDrive(downloadOptions.Filename);

if (tempFolderDrive.Name == destinationDrive.Name)
{
if (tempFolderDrive.AvailableFreeSpace < videoSizeInBytes * 2)
{
_progress.Report(new ProgressReport(ReportType.Log, $"The drive '{tempFolderDrive.Name}' may not have enough free space to complete the download."));
}
}
else
{
if (tempFolderDrive.AvailableFreeSpace < videoSizeInBytes)
{
// More drive space is needed by the raw ts files due to repeat metadata, but the amount of metadata packets can vary between files so we won't bother.
_progress.Report(new ProgressReport(ReportType.Log, $"The drive '{tempFolderDrive.Name}' may not have enough free space to complete the download."));
}

if (destinationDrive.AvailableFreeSpace < videoSizeInBytes)
{
_progress.Report(new ProgressReport(ReportType.Log, $"The drive '{destinationDrive.Name}' may not have enough free space to complete finalization."));
}
}
}

private async Task DownloadVideoPartsAsync(List<string> videoPartsList, string baseUrl, string downloadFolder, double vodAge, CancellationToken cancellationToken)
{
var partCount = videoPartsList.Count;
Expand Down Expand Up @@ -478,7 +511,7 @@ private async Task DownloadVideoPartAsync(string baseUrl, string videoPartName,
return (videoParts, vodAge);
}

private async Task<string> GetPlaylistUrl()
private async Task<(string url, int bandwidth)> GetPlaylistUrl()
{
GqlVideoTokenResponse accessToken = await TwitchHelper.GetVideoToken(downloadOptions.Id, downloadOptions.Oauth);

Expand All @@ -493,18 +526,22 @@ private async Task<string> GetPlaylistUrl()
throw new NullReferenceException("Insufficient access to VOD, OAuth may be required.");
}

List<KeyValuePair<string, string>> videoQualities = new List<KeyValuePair<string, string>>();
var videoQualities = new List<KeyValuePair<string, (string, int)>>();

for (int i = 0; i < videoPlaylist.Length; i++)
{
if (videoPlaylist[i].Contains("#EXT-X-MEDIA"))
{
string lastPart = videoPlaylist[i].Substring(videoPlaylist[i].IndexOf("NAME=\"") + 6);
string stringQuality = lastPart.Substring(0, lastPart.IndexOf("\""));
string stringQuality = lastPart.Substring(0, lastPart.IndexOf('"'));

var bandwidthStartIndex = videoPlaylist[i + 1].IndexOf("BANDWIDTH=") + 10;
var bandwidthEndIndex = videoPlaylist[i + 1].IndexOf(',') - bandwidthStartIndex;
int.TryParse(videoPlaylist[i + 1].Substring(bandwidthStartIndex, bandwidthEndIndex), out var bandwidth);

if (!videoQualities.Any(x => x.Key.Equals(stringQuality)))
{
videoQualities.Add(new KeyValuePair<string, string>(stringQuality, videoPlaylist[i + 2]));
videoQualities.Add(new KeyValuePair<string, (string, int)>(stringQuality, (videoPlaylist[i + 2], bandwidth)));
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion TwitchDownloaderWPF/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private async void Window_Loaded(object sender, RoutedEventArgs e)
}
catch (Exception ex)
{
if (MessageBox.Show(string.Format(Translations.Strings.UnableToDownloadFfmpegFull, "https://ffmpeg.org/download.html" , $"{Environment.CurrentDirectory}{Path.DirectorySeparatorChar}ffmpeg.exe"),
if (MessageBox.Show(string.Format(Translations.Strings.UnableToDownloadFfmpegFull, "https://ffmpeg.org/download.html", Path.Combine(Environment.CurrentDirectory, "ffmpeg.exe")),
Translations.Strings.UnableToDownloadFfmpeg, MessageBoxButton.OKCancel, MessageBoxImage.Information) == MessageBoxResult.OK)
{
Process.Start(new ProcessStartInfo("https://ffmpeg.org/download.html") { UseShellExecute = true });
Expand Down
43 changes: 7 additions & 36 deletions TwitchDownloaderWPF/PageVodDownload.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using TwitchDownloaderCore;
using TwitchDownloaderCore.Extensions;
using TwitchDownloaderCore.Options;
using TwitchDownloaderCore.Tools;
using TwitchDownloaderCore.TwitchObjects.Gql;
using TwitchDownloaderWPF.Properties;
using TwitchDownloaderWPF.Services;
Expand Down Expand Up @@ -76,7 +77,6 @@ private async void btnGetInfo_Click(object sender, RoutedEventArgs e)
await GetVideoInfo();
}

[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0018:Inline variable declaration")]
private async Task GetVideoInfo()
{
int videoId = ValidateUrl(textUrl.Text.Trim());
Expand Down Expand Up @@ -131,8 +131,7 @@ private async Task GetVideoInfo()

var bandwidthStartIndex = playlist[i + 1].IndexOf("BANDWIDTH=") + 10;
var bandwidthEndIndex = playlist[i + 1].IndexOf(',') - bandwidthStartIndex;
int bandwidth = 0; // Cannot be inlined if we want default value of 0
int.TryParse(playlist[i + 1].Substring(bandwidthStartIndex, bandwidthEndIndex), out bandwidth);
int.TryParse(playlist[i + 1].Substring(bandwidthStartIndex, bandwidthEndIndex), out var bandwidth);

if (!videoQualties.ContainsKey(stringQuality))
{
Expand Down Expand Up @@ -234,57 +233,29 @@ private void UpdateVideoSizeEstimates()
var cropEnd = checkEnd.IsChecked == true
? new TimeSpan((int)numEndHour.Value, (int)numEndMinute.Value, (int)numEndSecond.Value)
: vodLength;

for (int i = 0; i < comboQuality.Items.Count; i++)
{
var qualityWithSize = (string)comboQuality.Items[i];
var quality = GetQualityWithoutSize(qualityWithSize).ToString();
int bandwidth = videoQualties[quality].bandwidth;

var newVideoSize = EstimateVideoSize(bandwidth, cropStart, cropEnd);
comboQuality.Items[i] = $"{quality}{newVideoSize}";
var sizeInBytes = VideoSizeEstimator.EstimateVideoSize(bandwidth, cropStart, cropEnd);
var newVideoSize = VideoSizeEstimator.StringifyByteCount(sizeInBytes);
comboQuality.Items[i] = $"{quality} - {newVideoSize}";
}

comboQuality.SelectedIndex = selectedIndex;
}

private static ReadOnlySpan<char> GetQualityWithoutSize(string qualityWithSize)
{
int qualityIndex = qualityWithSize.LastIndexOf(" - ");
var qualityIndex = qualityWithSize.LastIndexOf(" - ", StringComparison.Ordinal);
return qualityIndex == -1
? qualityWithSize.AsSpan()
: qualityWithSize.AsSpan(0, qualityIndex);
}

// TODO: Move to Core to add support in CLI
private static string EstimateVideoSize(int bandwidth, TimeSpan startTime, TimeSpan endTime)
{
var sizeInBytes = EstimateVideoSizeBytes(bandwidth, startTime, endTime);

const long ONE_KIBIBYTE = 1024;
const long ONE_MEBIBYTE = 1_048_576;
const long ONE_GIBIBYTE = 1_073_741_824;

return sizeInBytes switch
{
< 1 => "",
< ONE_KIBIBYTE => $" - {sizeInBytes}B",
< ONE_MEBIBYTE => $" - {(float)sizeInBytes / ONE_KIBIBYTE:F1}KiB",
< ONE_GIBIBYTE => $" - {(float)sizeInBytes / ONE_MEBIBYTE:F1}MiB",
_ => $" - {(float)sizeInBytes / ONE_GIBIBYTE:F1}GiB",
};
}

private static long EstimateVideoSizeBytes(int bandwidth, TimeSpan startTime, TimeSpan endTime)
{
if (bandwidth < 1)
return 0;
if (endTime < startTime)
return 0;

var totalTime = endTime - startTime;
return (long)(bandwidth / 8d * totalTime.TotalSeconds);
}

private void OnProgressChanged(ProgressReport progress)
{
switch (progress.ReportType)
Expand Down
Loading