Skip to content

Commit 117af9c

Browse files
committed
fix: truncate download filenames to prevent 'filename too long' on Windows
Twitch VOD IDs (and other long platform IDs) can produce file names that, when combined with the download directory path, exceed Windows MAX_PATH (260 chars). This causes ffmpeg to fail with ERROR_FILENAME_EXCED_RANGE ("The filename or extension is too long") when writing the output file. Add TruncateFileName() extension that caps the base-name portion at 200 characters (preserving the extension), and apply it after SanitizeFileName() when constructing VideoFileName, AudioFileName, and SubtitleFileName. Fixes #875
1 parent 83aa8b8 commit 117af9c

2 files changed

Lines changed: 21 additions & 3 deletions

File tree

Grayjay.ClientServer/Extensions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,24 @@ public static string SanitizeFileName(this string fileName)
196196
fileName = fileName.Replace(invalidChar, '_');
197197
return fileName;
198198
}
199+
200+
// Windows MAX_PATH is 260; reserve some room for the directory prefix.
201+
private const int MaxFileNameLength = 200;
202+
/// <summary>
203+
/// Truncates a file name (after sanitization) so that the base name fits within
204+
/// <see cref="MaxFileNameLength"/> characters, preserving the extension.
205+
/// </summary>
206+
public static string TruncateFileName(this string fileName)
207+
{
208+
if (fileName.Length <= MaxFileNameLength)
209+
return fileName;
210+
211+
string ext = Path.GetExtension(fileName); // e.g. ".mp4"
212+
string baseName = Path.GetFileNameWithoutExtension(fileName);
213+
int allowedBase = MaxFileNameLength - ext.Length;
214+
if (allowedBase < 1) allowedBase = 1;
215+
return baseName.Substring(0, Math.Min(baseName.Length, allowedBase)) + ext;
216+
}
199217
public static string SanitizeFileNameWithPath(this string path)
200218
{
201219
string dirName = Path.GetDirectoryName(path);

Grayjay.ClientServer/Models/Downloads/VideoDownload.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ public async Task Download(ManagedHttpClient client, Action<double> onProgress,
321321
var representation = videoRepresentations?.FirstOrDefault();
322322
var mimeType = representation?.MimeType ?? VideoSource.Container;
323323

324-
VideoFileName = $"{VideoDetails.ID.Value} [{VideoSource.Width}x{VideoSource.Height}].{VideoHelper.VideoContainerToExtension(mimeType)}".SanitizeFileName();
324+
VideoFileName = $"{VideoDetails.ID.Value} [{VideoSource.Width}x{VideoSource.Height}].{VideoHelper.VideoContainerToExtension(mimeType)}".SanitizeFileName().TruncateFileName();
325325
VideoFilePath = Path.Combine(downloadDir, VideoFileName);
326326
}
327327
string audioDash = (AudioSource is DashManifestRawAudioSource dAudioSource) ? dAudioSource.Generate() : null;
@@ -331,12 +331,12 @@ public async Task Download(ManagedHttpClient client, Action<double> onProgress,
331331
var representation = audioRepresentations?.FirstOrDefault();
332332
var mimeType = representation?.MimeType ?? AudioSource.Container;
333333

334-
AudioFileName = $"{VideoDetails.ID.Value} [{AudioSource.Language}-{AudioSource.Bitrate}].{VideoHelper.AudioContainerToExtension(mimeType)}".SanitizeFileName();
334+
AudioFileName = $"{VideoDetails.ID.Value} [{AudioSource.Language}-{AudioSource.Bitrate}].{VideoHelper.AudioContainerToExtension(mimeType)}".SanitizeFileName().TruncateFileName();
335335
AudioFilePath = Path.Combine(downloadDir , AudioFileName);
336336
}
337337
if(SubtitleSourcetoUse != null)
338338
{
339-
SubtitleFileName = $"{VideoDetails.ID.Value} [{SubtitleSourcetoUse.Name}].{VideoHelper.SubtitleContainerToExtension(SubtitleSourcetoUse.Format)}".SanitizeFileName();
339+
SubtitleFileName = $"{VideoDetails.ID.Value} [{SubtitleSourcetoUse.Name}].{VideoHelper.SubtitleContainerToExtension(SubtitleSourcetoUse.Format)}".SanitizeFileName().TruncateFileName();
340340
SubtitleFilePath = Path.Combine(downloadDir, SubtitleFileName);
341341
}
342342
var progressLock = new Object();

0 commit comments

Comments
 (0)