From 28c452d537295eb18b0762071f92c231568f821f Mon Sep 17 00:00:00 2001 From: Elwador <75888166+Elwador@users.noreply.github.com> Date: Tue, 12 Aug 2025 22:30:31 +0200 Subject: [PATCH] Fix - Fix download issues --- CRD/Downloader/Crunchyroll/CrEpisode.cs | 18 ++-- CRD/Downloader/Crunchyroll/CrMovies.cs | 4 +- CRD/Downloader/Crunchyroll/CrMusic.cs | 4 +- CRD/Downloader/Crunchyroll/CrSeries.cs | 24 +++--- CRD/Utils/HLS/HLSDownloader.cs | 85 ++++++++++++++++--- CRD/Utils/Helpers.cs | 11 ++- .../Crunchyroll/Episode/EpisodeStructs.cs | 9 +- 7 files changed, 112 insertions(+), 43 deletions(-) diff --git a/CRD/Downloader/Crunchyroll/CrEpisode.cs b/CRD/Downloader/Crunchyroll/CrEpisode.cs index 179df5a..52f872a 100644 --- a/CRD/Downloader/Crunchyroll/CrEpisode.cs +++ b/CRD/Downloader/Crunchyroll/CrEpisode.cs @@ -43,16 +43,16 @@ public class CrEpisode(){ } if (epsidoe is{ Total: 1, Data: not null } && - (epsidoe.Data.First().Versions ?? []) + (epsidoe.Data.First().Versions ??[]) .GroupBy(v => v.AudioLocale) .Any(g => g.Count() > 1)){ Console.Error.WriteLine("Episode has Duplicate Audio Locales"); - var list = (epsidoe.Data.First().Versions ?? []).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList(); + var list = (epsidoe.Data.First().Versions ??[]).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList(); //guid for episode id foreach (var episodeVersionse in list){ foreach (var version in episodeVersionse){ var checkRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/episodes/{version.Guid}", HttpMethod.Get, true, true, query); - var checkResponse = await HttpClientReq.Instance.SendHttpRequest(checkRequest,true); + var checkResponse = await HttpClientReq.Instance.SendHttpRequest(checkRequest, true); if (!checkResponse.IsOk){ epsidoe.Data.First().Versions?.Remove(version); } @@ -189,8 +189,8 @@ public class CrEpisode(){ epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + ""; epMeta.SeriesId = item.SeriesId; epMeta.AbsolutEpisodeNumberE = epNum; - epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source; - epMeta.ImageBig = images[images.Count / 2].LastOrDefault()?.Source; + epMeta.Image = images.FirstOrDefault()?.FirstOrDefault()?.Source ?? string.Empty; + epMeta.ImageBig = images.FirstOrDefault()?.LastOrDefault()?.Source ?? string.Empty; epMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = false, Done = false, @@ -288,18 +288,14 @@ public class CrEpisode(){ return complete; } - + public async Task MarkAsWatched(string episodeId){ - - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true,false,null); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true, false, null); var response = await HttpClientReq.Instance.SendHttpRequest(request); if (!response.IsOk){ Console.Error.WriteLine($"Mark as watched for {episodeId} failed"); } - } - - } \ No newline at end of file diff --git a/CRD/Downloader/Crunchyroll/CrMovies.cs b/CRD/Downloader/Crunchyroll/CrMovies.cs index 592bb42..92e3bab 100644 --- a/CRD/Downloader/Crunchyroll/CrMovies.cs +++ b/CRD/Downloader/Crunchyroll/CrMovies.cs @@ -73,8 +73,8 @@ public class CrMovies{ epMeta.Season = ""; epMeta.SeriesId = ""; epMeta.AbsolutEpisodeNumberE = ""; - epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source; - epMeta.ImageBig = images[images.Count / 2].LastOrDefault()?.Source; + epMeta.Image = images.FirstOrDefault()?.FirstOrDefault()?.Source; + epMeta.ImageBig = images.FirstOrDefault()?.LastOrDefault()?.Source; epMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = false, Done = false, diff --git a/CRD/Downloader/Crunchyroll/CrMusic.cs b/CRD/Downloader/Crunchyroll/CrMusic.cs index 193d1d4..629de02 100644 --- a/CRD/Downloader/Crunchyroll/CrMusic.cs +++ b/CRD/Downloader/Crunchyroll/CrMusic.cs @@ -178,8 +178,8 @@ public class CrMusic{ epMeta.Season = ""; epMeta.SeriesId = episodeP.GetSeriesId(); epMeta.AbsolutEpisodeNumberE = ""; - epMeta.Image = images[images.Count / 2].Source; - epMeta.ImageBig = images[images.Count / 2].Source; + epMeta.Image = images.FirstOrDefault()?.Source ?? string.Empty; + epMeta.ImageBig = images.FirstOrDefault()?.Source ?? string.Empty; epMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = false, Done = false, diff --git a/CRD/Downloader/Crunchyroll/CrSeries.cs b/CRD/Downloader/Crunchyroll/CrSeries.cs index 926629f..ac16272 100644 --- a/CRD/Downloader/Crunchyroll/CrSeries.cs +++ b/CRD/Downloader/Crunchyroll/CrSeries.cs @@ -70,8 +70,8 @@ public class CrSeries{ epMeta.Season = Helpers.ExtractNumberAfterS(item.Identifier) ?? item.SeasonNumber + ""; epMeta.SeriesId = item.SeriesId; epMeta.AbsolutEpisodeNumberE = epNum; - epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source ?? ""; - epMeta.ImageBig = images[images.Count / 2].LastOrDefault()?.Source; + epMeta.Image = images.FirstOrDefault()?.FirstOrDefault()?.Source ?? string.Empty; + epMeta.ImageBig = images.FirstOrDefault()?.LastOrDefault()?.Source ?? string.Empty; epMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = false, Done = false, @@ -266,22 +266,22 @@ public class CrSeries{ crunchySeriesList.List = sortedEpisodes.Select(kvp => { var key = kvp.Key; var value = kvp.Value; - var images = (value.Items[0].Images?.Thumbnail ??[new List{ new(){ Source = "/notFound.jpg" } }]); - var seconds = (int)Math.Floor(value.Items[0].DurationMs / 1000.0); + var images = (value.Items.FirstOrDefault()?.Images?.Thumbnail ??[new List{ new(){ Source = "/notFound.jpg" } }]); + var seconds = (int)Math.Floor((value.Items.FirstOrDefault()?.DurationMs ?? 0) / 1000.0); var langList = value.Langs.Select(a => a.CrLocale).ToList(); Languages.SortListByLangList(langList); return new Episode{ E = key.StartsWith("E") ? key.Substring(1) : key, Lang = langList, - Name = value.Items[0].Title, - Season = Helpers.ExtractNumberAfterS(value.Items[0].Identifier) ?? value.Items[0].SeasonNumber.ToString(), - SeriesTitle = Regex.Replace(value.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(), - SeasonTitle = Regex.Replace(value.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd(), - EpisodeNum = key.StartsWith("SP") ? key : value.Items[0].EpisodeNumber?.ToString() ?? value.Items[0].Episode ?? "?", - Id = value.Items[0].SeasonId, - Img = images[images.Count / 2].FirstOrDefault()?.Source ?? "", - Description = value.Items[0].Description, + Name = value.Items.FirstOrDefault()?.Title ?? string.Empty, + Season = (Helpers.ExtractNumberAfterS(value.Items.FirstOrDefault()?.Identifier?? string.Empty) ?? value.Items.FirstOrDefault()?.SeasonNumber.ToString()) ?? string.Empty, + SeriesTitle = Regex.Replace(value.Items.FirstOrDefault()?.SeriesTitle?? string.Empty, @"\(\w+ Dub\)", "").TrimEnd(), + SeasonTitle = Regex.Replace(value.Items.FirstOrDefault()?.SeasonTitle?? string.Empty, @"\(\w+ Dub\)", "").TrimEnd(), + EpisodeNum = key.StartsWith("SP") ? key : value.Items.FirstOrDefault()?.EpisodeNumber?.ToString() ?? value.Items.FirstOrDefault()?.Episode ?? "?", + Id = value.Items.FirstOrDefault()?.SeasonId ?? string.Empty, + Img = images.FirstOrDefault()?.FirstOrDefault()?.Source ?? string.Empty, + Description = value.Items.FirstOrDefault()?.Description ?? string.Empty, EpisodeType = EpisodeType.Episode, Time = $"{seconds / 60}:{seconds % 60:D2}" // Ensures two digits for seconds. }; diff --git a/CRD/Utils/HLS/HLSDownloader.cs b/CRD/Utils/HLS/HLSDownloader.cs index afd876f..19058ec 100644 --- a/CRD/Utils/HLS/HLSDownloader.cs +++ b/CRD/Utils/HLS/HLSDownloader.cs @@ -333,15 +333,27 @@ public class HlsDownloader{ var _lastUiUpdate = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + var cts = new CancellationTokenSource(); + var token = cts.Token; + for (int i = 0; i < segments.Count; i++){ - if (File.Exists(Path.Combine(tempDir, $"part_{i:D6}.tmp"))) + try{ + await semaphore.WaitAsync(token); + } catch (OperationCanceledException){ + break; + } + + if (File.Exists(Path.Combine(tempDir, $"part_{i:D6}.tmp"))){ + semaphore.Release(); continue; + } int index = i; - await semaphore.WaitAsync(); + downloadTasks.Add(Task.Run(async () => { try{ + token.ThrowIfCancellationRequested(); var segment = new Segment{ Uri = ObjectUtilities.GetMemberValue(segments[index], "uri"), Key = ObjectUtilities.GetMemberValue(segments[index], "key"), @@ -385,32 +397,53 @@ public class HlsDownloader{ }; } - if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)) + if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)){ + cts.Cancel(); return; + } + QueueManager.Instance.Queue.Refresh(); while (_currentEpMeta.Paused){ await Task.Delay(500); - if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)) + if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)){ + cts.Cancel(); return; + } } } catch (Exception ex){ Console.Error.WriteLine($"Error downloading part {index}: {ex.Message}"); errorOccurred = true; + cts.Cancel(); } finally{ semaphore.Release(); } - })); + }, token)); } - await Task.WhenAll(downloadTasks); + try{ + await Task.WhenAll(downloadTasks); + } catch (OperationCanceledException){ + if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)){ + if (!_currentEpMeta.DownloadProgress.Done){ + CleanupNewDownloadMethod(tempDir, resumeFile, true); + } + } else{ + Console.Error.WriteLine("Download cancelled due to error."); + } + + return (false, _data.Parts); + } if (errorOccurred) return (false, _data.Parts); using (var output = new FileStream(fn, FileMode.Append, FileAccess.Write, FileShare.None)){ for (int i = mergedParts; i < segments.Count; i++){ + if (token.IsCancellationRequested) + return (false, _data.Parts); + string tempFile = Path.Combine(tempDir, $"part_{i:D6}.tmp"); if (!File.Exists(tempFile)){ Console.Error.WriteLine($"Missing temp file for part {i}, aborting merge."); @@ -439,19 +472,51 @@ public class HlsDownloader{ Doing = _isAudio ? "Merging Audio" : (_isVideo ? "Merging Video" : "") }; - if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)) + if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)){ + if (!_currentEpMeta.DownloadProgress.Done){ + CleanupNewDownloadMethod(tempDir, resumeFile, true); + } + return (false, _data.Parts); - + } } } // Cleanup temp files - Directory.Delete(tempDir, true); - File.Delete(resumeFile); + CleanupNewDownloadMethod(tempDir, resumeFile); return (true, _data.Parts); } + private void CleanupNewDownloadMethod(string tempDir, string resumeFile, bool cleanAll = false){ + if (cleanAll){ + // Delete downloaded files + foreach (var file in _currentEpMeta.downloadedFiles){ + try{ + File.Delete(file); // Safe: File.Delete does nothing if file doesn't exist + } catch (Exception ex){ + Console.Error.WriteLine($"Failed to delete file '{file}': {ex.Message}"); + } + } + } + + // Delete temp directory + try{ + if (Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); + } catch (Exception ex){ + Console.Error.WriteLine($"Failed to delete temp dir '{tempDir}': {ex.Message}"); + } + + // Delete resume file + try{ + if (File.Exists(resumeFile)) + File.Delete(resumeFile); + } catch (Exception ex){ + Console.Error.WriteLine($"Failed to delete resume file '{resumeFile}': {ex.Message}"); + } + } + public static Info GetDownloadInfo(long dateStartUnix, int partsDownloaded, int partsTotal, long incrementalBytes, long totalDownloadedBytes){ DateTime lastStart = DateTimeOffset.FromUnixTimeMilliseconds(dateStartUnix).UtcDateTime; diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index c61c921..34e6733 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -623,7 +623,7 @@ public class Helpers{ public static Dictionary> GroupByLanguageWithSubtitles(List allMedia){ //Group by language var languageGroups = allMedia - .Where(media => media.Type != DownloadMediaType.Description && + .Where(media => media.Type != DownloadMediaType.Description && media.Type != DownloadMediaType.Cover && (!string.IsNullOrEmpty(media.Lang?.CrLocale) || (media is{ Type: DownloadMediaType.Subtitle, RelatedVideoDownloadMedia: not null } && !string.IsNullOrEmpty(media.RelatedVideoDownloadMedia.Lang?.CrLocale))) @@ -645,6 +645,15 @@ public class Helpers{ group.Add(descriptionMedia[0]); } } + + //Find and add Cover media to each group + var coverMedia = allMedia.Where(media => media.Type == DownloadMediaType.Cover).ToList(); + + if (coverMedia.Count > 0){ + foreach (var group in languageGroups.Values){ + group.Add(coverMedia[0]); + } + } return languageGroups; } diff --git a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs index 90852e9..dfa173b 100644 --- a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs +++ b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs @@ -291,11 +291,10 @@ public class CrunchyEpisode : IHistorySource{ } public string GetImageUrl(){ - if (Images != null){ - return Images.Thumbnail?.First().First().Source ?? string.Empty; - } - - return string.Empty; + return Images?.Thumbnail? + .FirstOrDefault()? + .FirstOrDefault()? + .Source ?? string.Empty; } #endregion