diff --git a/CRD/Downloader/Crunchyroll/CrEpisode.cs b/CRD/Downloader/Crunchyroll/CrEpisode.cs index 3144176..179df5a 100644 --- a/CRD/Downloader/Crunchyroll/CrEpisode.cs +++ b/CRD/Downloader/Crunchyroll/CrEpisode.cs @@ -173,7 +173,7 @@ public class CrEpisode(){ } var epNum = episodeP.Key.StartsWith('E') ? episodeP.Key[1..] : episodeP.Key; - var images = (item.Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } }); + var images = (item.Images?.Thumbnail ??[new List{ new(){ Source = "/notFound.jpg" } }]); Regex dubPattern = new Regex(@"\(\w+ Dub\)"); @@ -190,6 +190,7 @@ public class CrEpisode(){ epMeta.SeriesId = item.SeriesId; epMeta.AbsolutEpisodeNumberE = epNum; epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source; + epMeta.ImageBig = images[images.Count / 2].LastOrDefault()?.Source; epMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = false, Done = false, diff --git a/CRD/Downloader/Crunchyroll/CrMovies.cs b/CRD/Downloader/Crunchyroll/CrMovies.cs index d30facc..592bb42 100644 --- a/CRD/Downloader/Crunchyroll/CrMovies.cs +++ b/CRD/Downloader/Crunchyroll/CrMovies.cs @@ -61,7 +61,7 @@ public class CrMovies{ return null; } - var images = (episodeP.Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } }); + var images = (episodeP.Images?.Thumbnail ??[new List(){ new(){ Source = "/notFound.jpg" } }]); var epMeta = new CrunchyEpMeta(); epMeta.Data = new List{ new(){ MediaId = episodeP.Id, Versions = null, IsSubbed = episodeP.IsSubbed, IsDubbed = episodeP.IsDubbed } }; @@ -74,6 +74,7 @@ public class CrMovies{ epMeta.SeriesId = ""; epMeta.AbsolutEpisodeNumberE = ""; epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source; + epMeta.ImageBig = images[images.Count / 2].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 ede28cf..193d1d4 100644 --- a/CRD/Downloader/Crunchyroll/CrMusic.cs +++ b/CRD/Downloader/Crunchyroll/CrMusic.cs @@ -165,7 +165,7 @@ public class CrMusic{ public CrunchyEpMeta EpisodeMeta(CrunchyMusicVideo episodeP){ - var images = (episodeP.Images?.Thumbnail ?? new List{ new Image{ Source = "/notFound.png" } }); + var images = (episodeP.Images?.Thumbnail ??[new Image{ Source = "/notFound.jpg" }]); var epMeta = new CrunchyEpMeta(); @@ -179,6 +179,7 @@ public class CrMusic{ epMeta.SeriesId = episodeP.GetSeriesId(); epMeta.AbsolutEpisodeNumberE = ""; epMeta.Image = images[images.Count / 2].Source; + epMeta.ImageBig = images[images.Count / 2].Source; epMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = false, Done = false, diff --git a/CRD/Downloader/Crunchyroll/CrSeries.cs b/CRD/Downloader/Crunchyroll/CrSeries.cs index 7d53662..926629f 100644 --- a/CRD/Downloader/Crunchyroll/CrSeries.cs +++ b/CRD/Downloader/Crunchyroll/CrSeries.cs @@ -56,7 +56,7 @@ public class CrSeries{ } var epNum = key.StartsWith('E') ? key[1..] : key; - var images = (item.Images?.Thumbnail ?? new List>{ new List{ new Image{ Source = "/notFound.png" } } }); + var images = (item.Images?.Thumbnail ??[new List{ new(){ Source = "/notFound.jpg" } }]); Regex dubPattern = new Regex(@"\(\w+ Dub\)"); @@ -71,6 +71,7 @@ public class CrSeries{ epMeta.SeriesId = item.SeriesId; epMeta.AbsolutEpisodeNumberE = epNum; epMeta.Image = images[images.Count / 2].FirstOrDefault()?.Source ?? ""; + epMeta.ImageBig = images[images.Count / 2].LastOrDefault()?.Source; epMeta.DownloadProgress = new DownloadProgress(){ IsDownloading = false, Done = false, @@ -265,7 +266,7 @@ 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 List{ new Image{ Source = "/notFound.png" } } }); + 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 langList = value.Langs.Select(a => a.CrLocale).ToList(); Languages.SortListByLangList(langList); diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index cc4272c..df400f5 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Globalization; using System.IO; -using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Runtime.InteropServices; @@ -108,8 +106,8 @@ public class CrunchyrollManager{ options.Partsize = 10; options.DlSubs = new List{ "en-US" }; options.SkipMuxing = false; - options.MkvmergeOptions = new List{ "--no-date", "--disable-track-statistics-tags", "--engage no_variable_data" }; - options.FfmpegOptions = new(); + options.MkvmergeOptions =[]; + options.FfmpegOptions =[]; options.DefaultAudio = "ja-JP"; options.DefaultSub = "en-US"; options.QualityAudio = "best"; @@ -339,6 +337,7 @@ public class CrunchyrollManager{ Mp4 = options.Mp4, Mp3 = options.AudioOnlyToMp3, MuxFonts = options.MuxFonts, + MuxCover = options.MuxCover, VideoTitle = res.VideoTitle, Novids = options.Novids, NoCleanup = options.Nocleanup, @@ -405,6 +404,7 @@ public class CrunchyrollManager{ Mp4 = options.Mp4, Mp3 = options.AudioOnlyToMp3, MuxFonts = options.MuxFonts, + MuxCover = options.MuxCover, VideoTitle = res.VideoTitle, Novids = options.Novids, NoCleanup = options.Nocleanup, @@ -520,8 +520,6 @@ public class CrunchyrollManager{ if (CrunOptions.ShutdownWhenQueueEmpty){ Helpers.ShutdownComputer(); } - - } return true; @@ -678,6 +676,7 @@ public class CrunchyrollManager{ CcSubsMuxingFlag = options.CcSubsMuxingFlag, SignsSubsAsForced = options.SignsSubsAsForced, Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() :[], + Cover = options.MuxCover ? data.Where(a => a.Type == DownloadMediaType.Cover).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() : [], }); if (!File.Exists(CfgManager.PathFFMPEG)){ @@ -1291,6 +1290,7 @@ public class CrunchyrollManager{ pssh = item.pssh, language = item.language, bandwidth = item.bandwidth, + audioSamplingRate = item.audioSamplingRate, resolutionText = $"{Math.Round(item.bandwidth / 1000.0)}kB/s" }).ToList(); @@ -1307,6 +1307,7 @@ public class CrunchyrollManager{ .GroupBy(a => new{ a.bandwidth, a.language }) // Add more properties if needed .Select(g => g.First()) .OrderBy(a => a.bandwidth) + .ThenBy(a => a.audioSamplingRate) .ToList(); if (string.IsNullOrEmpty(data.VideoQuality)){ @@ -1375,7 +1376,7 @@ public class CrunchyrollManager{ Console.WriteLine("Available Audio Qualities:"); for (int i = 0; i < audios.Count; i++){ - Console.WriteLine($"\t[{i + 1}] {audios[i].resolutionText}"); + Console.WriteLine($"\t[{i + 1}] {audios[i].resolutionText} / {audios[i].audioSamplingRate}"); } variables.Add(new Variable("height", chosenVideoSegments.quality.height, false)); @@ -1396,7 +1397,7 @@ public class CrunchyrollManager{ Console.WriteLine($"Selected quality:"); Console.WriteLine($"\tVideo: {chosenVideoSegments.resolutionText}"); - Console.WriteLine($"\tAudio: {chosenAudioSegments.resolutionText}"); + Console.WriteLine($"\tAudio: {chosenAudioSegments.resolutionText} / {chosenAudioSegments.audioSamplingRate}"); Console.WriteLine($"\tServer: {selectedServer}"); Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]); @@ -1448,6 +1449,20 @@ public class CrunchyrollManager{ } else if (options.Novids){ Console.WriteLine("Skipping video download..."); } else{ + await CrAuth.RefreshToken(true); + + Dictionary authDataDict = new Dictionary + { { "authorization", "Bearer " + Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } }; + + chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict); + + if (!string.IsNullOrEmpty(chosenVideoSegments.pssh) && !chosenVideoSegments.pssh.Equals(chosenAudioSegments.pssh)){ + if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){ + Console.WriteLine("Video and Audio PSSH different requesting Audio encryption keys"); + chosenAudioSegments.encryptionKeys = await _widevine.getKeys(chosenAudioSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict); + } + } + var videoDownloadResult = await DownloadVideo(chosenVideoSegments, options, outFile, tempTsFile, data, fileDir); tsFile = videoDownloadResult.tsFile; @@ -1467,6 +1482,20 @@ public class CrunchyrollManager{ if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){ + await CrAuth.RefreshToken(true); + + if (chosenVideoSegments.encryptionKeys.Count == 0){ + Dictionary authDataDict = new Dictionary + { { "authorization", "Bearer " + Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } }; + + chosenVideoSegments.encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict); + + if (!string.IsNullOrEmpty(chosenVideoSegments.pssh) && !chosenVideoSegments.pssh.Equals(chosenAudioSegments.pssh)){ + Console.WriteLine("Video and Audio PSSH different requesting Audio encryption keys"); + chosenAudioSegments.encryptionKeys = await _widevine.getKeys(chosenAudioSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict); + } + } + var audioDownloadResult = await DownloadAudio(chosenAudioSegments, options, outFile, tempTsFile, data, fileDir); tsFile = audioDownloadResult.tsFile; @@ -1517,20 +1546,25 @@ public class CrunchyrollManager{ Dictionary authDataDict = new Dictionary { { "authorization", "Bearer " + Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } }; - var encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict); + var encryptionKeys = chosenVideoSegments.encryptionKeys; if (encryptionKeys.Count == 0){ - Console.Error.WriteLine("Failed to get encryption keys"); - dlFailed = true; - return new DownloadResponse{ - Data = files, - Error = dlFailed, - FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", - ErrorText = "Couldn't get DRM encryption keys" - }; + encryptionKeys = await _widevine.getKeys(chosenVideoSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict); + + if (encryptionKeys.Count == 0){ + Console.Error.WriteLine("Failed to get encryption keys"); + dlFailed = true; + return new DownloadResponse{ + Data = files, + Error = dlFailed, + FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown", + ErrorText = "Couldn't get DRM encryption keys" + }; + } } - List encryptionKeysAudio =[]; + + List encryptionKeysAudio = chosenAudioSegments.encryptionKeys; if (!string.IsNullOrEmpty(chosenVideoSegments.pssh) && !chosenVideoSegments.pssh.Equals(chosenAudioSegments.pssh)){ Console.WriteLine("Video and Audio PSSH different requesting Audio encryption keys"); encryptionKeysAudio = await _widevine.getKeys(chosenAudioSegments.pssh, ApiUrls.WidevineLicenceUrl, authDataDict); @@ -1861,6 +1895,27 @@ public class CrunchyrollManager{ Console.WriteLine($"{fileName}.xml has been created with the description."); } + + if (options.MuxCover){ + if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(fileDir + "cover.png")){ + var bitmap = await Helpers.LoadImage(data.ImageBig); + if (bitmap != null){ + string coverPath = Path.Combine(fileDir, "cover.png"); + Helpers.EnsureDirectoriesExist(coverPath); + await using (var fs = File.OpenWrite(coverPath)){ + bitmap.Save(fs); // always saves PNG + } + bitmap.Dispose(); + + files.Add(new DownloadedMedia{ + Type = DownloadMediaType.Cover, + Lang = Languages.DEFAULT_lang, + Path = coverPath + }); + + } + } + } var tempFolderPath = ""; if (options.DownloadToTempFolder){ @@ -1928,7 +1983,6 @@ public class CrunchyrollManager{ var isCc = subsItem.isCC; var isDuplicate = false; - if ((!options.IncludeSignsSubs && isSigns) || (!options.IncludeCcSubs && isCc)){ continue; @@ -1945,8 +1999,9 @@ public class CrunchyrollManager{ continue; } } - - sxData.File = Languages.SubsFile(fileName, index + "", langItem, isDuplicate ? videoDownloadMedia.Lang.CrLocale : "",isCc, options.CcTag, isSigns, subsItem.format, !(data.DownloadSubs.Count == 1 && !data.DownloadSubs.Contains("all"))); + + sxData.File = Languages.SubsFile(fileName, index + "", langItem, isDuplicate ? videoDownloadMedia.Lang.CrLocale : "", isCc, options.CcTag, isSigns, subsItem.format, + !(data.DownloadSubs.Count == 1 && !data.DownloadSubs.Contains("all"))); sxData.Path = Path.Combine(fileDir, sxData.File); Helpers.EnsureDirectoriesExist(sxData.Path); @@ -1962,7 +2017,6 @@ public class CrunchyrollManager{ if (subsAssReqResponse.IsOk){ if (subsItem.format == "ass"){ - subsAssReqResponse.ResponseContent = '\ufeff' + subsAssReqResponse.ResponseContent; var sBodySplit = subsAssReqResponse.ResponseContent.Split(new[]{ "\r\n" }, StringSplitOptions.None).ToList(); if (sBodySplit.Count > 2){ @@ -1999,12 +2053,12 @@ public class CrunchyrollManager{ assBuilder.AppendLine(); assBuilder.AppendLine("[V4+ Styles]"); - assBuilder.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, " - + "Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding"); + assBuilder.AppendLine("Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour,BackColour,Bold,Italic," + + "Underline,Strikeout,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,Encoding"); assBuilder.AppendLine($"Style: Default,{options.CcSubsFont ?? "Trebuchet MS"},24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0018,1"); assBuilder.AppendLine(); assBuilder.AppendLine("[Events]"); - assBuilder.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"); + assBuilder.AppendLine("Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text"); // Parse the VTT content string normalizedContent = subsAssReqResponse.ResponseContent.Replace("\r\n", "\n").Replace("\r", "\n"); @@ -2100,10 +2154,22 @@ public class CrunchyrollManager{ FsRetryTime = options.RetryDelay * 1000, Retries = options.RetryAttempts, Override = options.Force, - }, data, true, false); + }, data, true, false, options.DownloadMethodeNew); + var defParts = new PartsData{ + First = 0, + Total = 0, + Completed = 0, + }; + + var videoDownloadResult = (Ok: false, Parts: defParts); + + try{ + videoDownloadResult = await videoDownloader.Download(); + } catch (Exception e){ + Console.WriteLine(e); + } - var videoDownloadResult = await videoDownloader.Download(); return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile); } @@ -2158,10 +2224,21 @@ public class CrunchyrollManager{ FsRetryTime = options.RetryDelay * 1000, Retries = options.RetryAttempts, Override = options.Force, - }, data, false, true); + }, data, false, true, options.DownloadMethodeNew); - var audioDownloadResult = await audioDownloader.Download(); + var defParts = new PartsData{ + First = 0, + Total = 0, + Completed = 0, + }; + var audioDownloadResult = (Ok: false, Parts: defParts); + + try{ + audioDownloadResult = await audioDownloader.Download(); + } catch (Exception e){ + Console.WriteLine(e); + } return (audioDownloadResult.Ok, audioDownloadResult.Parts, tsFile); } diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs index 4367077..5f78f62 100644 --- a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs +++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs @@ -69,6 +69,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ [ObservableProperty] private bool _muxFonts; + + [ObservableProperty] + private bool _muxCover; [ObservableProperty] private bool _syncTimings; @@ -359,6 +362,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ MuxToMp4 = options.Mp4; MuxToMp3 = options.AudioOnlyToMp3; MuxFonts = options.MuxFonts; + MuxCover = options.MuxCover; SyncTimings = options.SyncTiming; SkipSubMux = options.SkipSubsMux; LeadingNumbers = options.Numbers; @@ -428,6 +432,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4; CrunchyrollManager.Instance.CrunOptions.AudioOnlyToMp3 = MuxToMp3; CrunchyrollManager.Instance.CrunOptions.MuxFonts = MuxFonts; + CrunchyrollManager.Instance.CrunOptions.MuxCover = MuxCover; CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings; CrunchyrollManager.Instance.CrunOptions.SkipSubsMux = SkipSubMux; CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0), 0, 10); diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml index 12f434e..7fb0734 100644 --- a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml +++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml @@ -411,6 +411,12 @@ + + + + + + diff --git a/CRD/Downloader/ProgramManager.cs b/CRD/Downloader/ProgramManager.cs index 60135a4..2d598f4 100644 --- a/CRD/Downloader/ProgramManager.cs +++ b/CRD/Downloader/ProgramManager.cs @@ -15,7 +15,6 @@ using CRD.Utils; using CRD.Utils.Structs; using CRD.Utils.Structs.History; using CRD.Utils.Updater; -using ExtendedXmlSerializer.Core.Sources; using FluentAvalonia.Styling; namespace CRD.Downloader; diff --git a/CRD/Downloader/QueueManager.cs b/CRD/Downloader/QueueManager.cs index 3031dcc..7a1d97d 100644 --- a/CRD/Downloader/QueueManager.cs +++ b/CRD/Downloader/QueueManager.cs @@ -9,7 +9,6 @@ using CRD.Downloader.Crunchyroll; using CRD.Utils; using CRD.Utils.CustomList; using CRD.Utils.Structs; -using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.History; using CRD.ViewModels; using CRD.Views; @@ -182,12 +181,12 @@ public partial class QueueManager : ObservableObject{ case EpisodeDownloadMode.OnlyVideo: newOptions.Novids = false; newOptions.Noaudio = true; - selected.DownloadSubs = ["none"]; + selected.DownloadSubs =["none"]; break; case EpisodeDownloadMode.OnlyAudio: newOptions.Novids = true; newOptions.Noaudio = false; - selected.DownloadSubs = ["none"]; + selected.DownloadSubs =["none"]; break; case EpisodeDownloadMode.OnlySubs: newOptions.Novids = true; @@ -250,12 +249,12 @@ public partial class QueueManager : ObservableObject{ case EpisodeDownloadMode.OnlyVideo: newOptions.Novids = false; newOptions.Noaudio = true; - movieMeta.DownloadSubs = ["none"]; + movieMeta.DownloadSubs =["none"]; break; case EpisodeDownloadMode.OnlyAudio: newOptions.Novids = true; newOptions.Noaudio = false; - movieMeta.DownloadSubs = ["none"]; + movieMeta.DownloadSubs =["none"]; break; case EpisodeDownloadMode.OnlySubs: newOptions.Novids = true; @@ -345,7 +344,9 @@ public partial class QueueManager : ObservableObject{ public async Task CrAddSeriesToQueue(CrunchySeriesList list, CrunchyMultiDownload data){ var selected = CrunchyrollManager.Instance.CrSeries.ItemSelectMultiDub(list.Data, data.DubLang, data.But, data.AllEpisodes, data.E); - bool failed = false; + var failed = false; + var partialAdd = false; + foreach (var crunchyEpMeta in selected.Values.ToList()){ if (crunchyEpMeta.Data?.First() != null){ @@ -411,15 +412,30 @@ public partial class QueueManager : ObservableObject{ Queue.Add(crunchyEpMeta); + + if (crunchyEpMeta.Data.Count < data.DubLang.Count && !CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub){ + Console.WriteLine("Added Episode to Queue but couldn't find all selected dubs"); + Console.Error.WriteLine("Added Episode to Queue but couldn't find all selected dubs - Available dubs/subs: "); + + partialAdd = true; + + var languages = (crunchyEpMeta.Data.First().Versions ??[]).Select(version => $"{(version.IsPremiumOnly ? "+ " : "")}{version.AudioLocale}").ToArray(); + + Console.Error.WriteLine( + $"{crunchyEpMeta.SeasonTitle} - Season {crunchyEpMeta.Season} - {crunchyEpMeta.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", crunchyEpMeta.AvailableSubs ??[])}]"); + MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue but couldn't find all selected dubs", ToastType.Warning, 2)); + } } else{ failed = true; } } - if (failed){ + if (failed && !partialAdd){ MainWindow.Instance.ShowError("Not all episodes could be added – make sure that you are signed in with an account that has an active premium subscription?"); - } else{ + } else if (selected.Values.Count > 0 && !partialAdd){ MessageBus.Current.SendMessage(new ToastMessage($"Added episodes to the queue", ToastType.Information, 1)); + } else if (!partialAdd){ + MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode(s) to the queue with current dub settings", ToastType.Error, 2)); } } } \ No newline at end of file diff --git a/CRD/Utils/DRM/Widevine.cs b/CRD/Utils/DRM/Widevine.cs index f732aa8..d09ea0b 100644 --- a/CRD/Utils/DRM/Widevine.cs +++ b/CRD/Utils/DRM/Widevine.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.IO.IsolatedStorage; using System.Net.Http; using System.Text; using System.Threading.Tasks; diff --git a/CRD/Utils/Enums/EnumCollection.cs b/CRD/Utils/Enums/EnumCollection.cs index d036d1a..62432b0 100644 --- a/CRD/Utils/Enums/EnumCollection.cs +++ b/CRD/Utils/Enums/EnumCollection.cs @@ -182,6 +182,9 @@ public enum DownloadMediaType{ [EnumMember(Value = "Description")] Description, + + [EnumMember(Value = "Cover")] + Cover, } [JsonConverter(typeof(StringEnumConverter))] diff --git a/CRD/Utils/Files/CfgManager.cs b/CRD/Utils/Files/CfgManager.cs index 9fcfa23..22fca1d 100644 --- a/CRD/Utils/Files/CfgManager.cs +++ b/CRD/Utils/Files/CfgManager.cs @@ -4,13 +4,8 @@ using System.IO; using System.IO.Compression; using System.Reflection; using System.Runtime.InteropServices; -using CRD.Downloader; using CRD.Downloader.Crunchyroll; -using CRD.Utils.Structs.Crunchyroll; using Newtonsoft.Json; -using YamlDotNet.RepresentationModel; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; namespace CRD.Utils.Files; diff --git a/CRD/Utils/HLS/HLSDownloader.cs b/CRD/Utils/HLS/HLSDownloader.cs index df1f869..afd876f 100644 --- a/CRD/Utils/HLS/HLSDownloader.cs +++ b/CRD/Utils/HLS/HLSDownloader.cs @@ -4,6 +4,7 @@ using System.IO; using System.Net.Http; using System.Security.Cryptography; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using CRD.Downloader; using CRD.Utils.Parser.Utils; @@ -18,8 +19,9 @@ public class HlsDownloader{ private CrunchyEpMeta _currentEpMeta; private bool _isVideo; private bool _isAudio; + private bool _newDownloadMethode; - public HlsDownloader(HlsOptions options, CrunchyEpMeta meta, bool isVideo, bool isAudio){ + public HlsDownloader(HlsOptions options, CrunchyEpMeta meta, bool isVideo, bool isAudio, bool newDownloadMethode){ if (options == null || options.M3U8Json == null || options.M3U8Json.Segments == null){ throw new Exception("Playlist is empty"); } @@ -29,6 +31,8 @@ public class HlsDownloader{ _isVideo = isVideo; _isAudio = isAudio; + _newDownloadMethode = newDownloadMethode; + if (options?.M3U8Json != null) _data = new Data{ Parts = new PartsData{ @@ -152,6 +156,11 @@ public class HlsDownloader{ _data.Parts.Completed = _data.Offset; } + + if (_newDownloadMethode){ + return await DownloadSegmentsBufferedResumeAsync(segments, fn); + } + for (int p = 0; p < Math.Ceiling((double)segments.Count / _data.Threads); p++){ // Start time _data.DateStart = DateTimeOffset.Now.ToUnixTimeMilliseconds(); @@ -271,6 +280,7 @@ public class HlsDownloader{ File.Delete(downloadItemDownloadedFile); } } catch (Exception e){ + Console.Error.WriteLine(e.Message); } } } @@ -292,24 +302,182 @@ public class HlsDownloader{ return (Ok: true, _data.Parts); } - public static Info GetDownloadInfo(long dateStartUnix, int partsDownloaded, int partsTotal, long downloadedBytes, long totalDownloadedBytes){ - // Convert Unix timestamp to DateTime - DateTime dateStart = DateTimeOffset.FromUnixTimeMilliseconds(dateStartUnix).UtcDateTime; - double dateElapsed = (DateTime.UtcNow - dateStart).TotalMilliseconds; + private static readonly object _resumeLock = new object(); - // Calculate percentage - int percentFixed = (int)((double)partsDownloaded / partsTotal * 100); - int percent = percentFixed < 100 ? percentFixed : (partsTotal == partsDownloaded ? 100 : 99); + public async Task<(bool Ok, PartsData Parts)> DownloadSegmentsBufferedResumeAsync(List segments, string fn){ + var totalSeg = _data.Parts.Total; + string sessionId = Path.GetFileNameWithoutExtension(fn); + string tempDir = Path.Combine(Path.GetDirectoryName(fn), $"{sessionId}_temp"); - double downloadSpeed = downloadedBytes / (dateElapsed / 1000); + Directory.CreateDirectory(tempDir); - int partsLeft = partsTotal - partsDownloaded; - double remainingTime = (partsLeft * ((double)totalDownloadedBytes / partsDownloaded)) / downloadSpeed; + string resumeFile = $"{fn}.new.resume"; + int downloadedParts = 0; + int mergedParts = 0; + + if (File.Exists(resumeFile)){ + try{ + var resumeData = JsonConvert.DeserializeObject(File.ReadAllText(resumeFile)); + downloadedParts = (int?)resumeData?.DownloadedParts ?? 0; + mergedParts = (int?)resumeData?.MergedParts ?? 0; + } catch{ + } + } + + if (downloadedParts > totalSeg) downloadedParts = totalSeg; + if (mergedParts > downloadedParts) mergedParts = downloadedParts; + + var semaphore = new SemaphoreSlim(_data.Threads); + var downloadTasks = new List(); + bool errorOccurred = false; + + var _lastUiUpdate = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + for (int i = 0; i < segments.Count; i++){ + if (File.Exists(Path.Combine(tempDir, $"part_{i:D6}.tmp"))) + continue; + + int index = i; + await semaphore.WaitAsync(); + + downloadTasks.Add(Task.Run(async () => { + try{ + var segment = new Segment{ + Uri = ObjectUtilities.GetMemberValue(segments[index], "uri"), + Key = ObjectUtilities.GetMemberValue(segments[index], "key"), + ByteRange = ObjectUtilities.GetMemberValue(segments[index], "byteRange") + }; + + var data = await DownloadPart(segment, index, _data.Offset); + + string tempFile = Path.Combine(tempDir, $"part_{index:D6}.tmp"); + await File.WriteAllBytesAsync(tempFile, data); + + int currentDownloaded = Directory.GetFiles(tempDir, "part_*.tmp").Length; + lock (_resumeLock){ + File.WriteAllText(resumeFile, JsonConvert.SerializeObject(new{ + DownloadedParts = currentDownloaded, + MergedParts = mergedParts, + Total = totalSeg + })); + } + + if (DateTimeOffset.Now.ToUnixTimeMilliseconds() - _lastUiUpdate > 500){ + var dataLog = GetDownloadInfo( + _lastUiUpdate, + currentDownloaded, + totalSeg, + _data.BytesDownloaded, + _data.TotalBytes + ); + + _data.BytesDownloaded = 0; + _lastUiUpdate = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + Console.WriteLine($"{currentDownloaded}/{totalSeg} [{dataLog.Percent}%] Speed: {dataLog.DownloadSpeed / 1000000.0:F2} MB/s ETA: {FormatTime(dataLog.Time)}"); + + _currentEpMeta.DownloadProgress = new DownloadProgress{ + IsDownloading = true, + Percent = dataLog.Percent, + Time = dataLog.Time, + DownloadSpeed = dataLog.DownloadSpeed, + Doing = _isAudio ? "Downloading Audio" : (_isVideo ? "Downloading Video" : "") + }; + } + + if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)) + return; + + QueueManager.Instance.Queue.Refresh(); + + while (_currentEpMeta.Paused){ + await Task.Delay(500); + if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)) + return; + } + } catch (Exception ex){ + Console.Error.WriteLine($"Error downloading part {index}: {ex.Message}"); + errorOccurred = true; + } finally{ + semaphore.Release(); + } + })); + } + + await Task.WhenAll(downloadTasks); + + 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++){ + string tempFile = Path.Combine(tempDir, $"part_{i:D6}.tmp"); + if (!File.Exists(tempFile)){ + Console.Error.WriteLine($"Missing temp file for part {i}, aborting merge."); + return (false, _data.Parts); + } + + byte[] data = await File.ReadAllBytesAsync(tempFile); + await output.WriteAsync(data, 0, data.Length); + + mergedParts++; + + File.WriteAllText(resumeFile, JsonConvert.SerializeObject(new{ + DownloadedParts = totalSeg, + MergedParts = mergedParts, + Total = totalSeg + })); + + var dataLog = GetDownloadInfo(_data.DateStart, mergedParts, totalSeg, _data.BytesDownloaded, _data.TotalBytes); + Console.WriteLine($"{mergedParts}/{totalSeg} parts merged [{dataLog.Percent}%]"); + + _currentEpMeta.DownloadProgress = new DownloadProgress{ + IsDownloading = true, + Percent = dataLog.Percent, + Time = dataLog.Time, + DownloadSpeed = dataLog.DownloadSpeed, + Doing = _isAudio ? "Merging Audio" : (_isVideo ? "Merging Video" : "") + }; + + if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)) + return (false, _data.Parts); + + } + } + + // Cleanup temp files + Directory.Delete(tempDir, true); + File.Delete(resumeFile); + + return (true, _data.Parts); + } + + + public static Info GetDownloadInfo(long dateStartUnix, int partsDownloaded, int partsTotal, long incrementalBytes, long totalDownloadedBytes){ + DateTime lastStart = DateTimeOffset.FromUnixTimeMilliseconds(dateStartUnix).UtcDateTime; + double elapsedMs = (DateTime.UtcNow - lastStart).TotalMilliseconds; + if (elapsedMs <= 0) elapsedMs = 1; + + double speed = incrementalBytes / (elapsedMs / 1000); + if (speed < 1) speed = 1; + + int percent = (int)((double)partsDownloaded / partsTotal * 100); + if (percent > 100) percent = 100; + + double etaSec = 0; + if (partsDownloaded > 0){ + double avgPartSize = (double)totalDownloadedBytes / partsDownloaded; + double remainingBytes = avgPartSize * (partsTotal - partsDownloaded); + etaSec = remainingBytes / speed; + } + + if (etaSec > TimeSpan.MaxValue.TotalSeconds) + etaSec = TimeSpan.MaxValue.TotalSeconds; return new Info{ Percent = percent, - Time = remainingTime, - DownloadSpeed = downloadSpeed + Time = etaSec, + DownloadSpeed = speed }; } @@ -333,15 +501,15 @@ public class HlsDownloader{ } if (dec != null){ - _data.BytesDownloaded += dec.Length; - _data.TotalBytes += dec.Length; + Interlocked.Add(ref _data.BytesDownloaded, dec.Length); + Interlocked.Add(ref _data.TotalBytes, dec.Length); } } else{ part = await GetData(p, sUri, seg.ByteRange != null ? seg.ByteRange.ToDictionary() : new Dictionary(), segOffset, false, _data.Timeout, _data.Retries); dec = part; if (dec != null){ - _data.BytesDownloaded += dec.Length; - _data.TotalBytes += dec.Length; + Interlocked.Add(ref _data.BytesDownloaded, dec.Length); + Interlocked.Add(ref _data.TotalBytes, dec.Length); } } } catch (Exception ex){ @@ -455,12 +623,11 @@ public class HlsDownloader{ throw; // rethrow after last retry await Task.Delay(_data.WaitTime); - }catch (Exception ex) { - + } catch (Exception ex){ Console.Error.WriteLine($"Unexpected exception at part {partIndex + 1 + segOffset}:"); Console.Error.WriteLine($"\tType: {ex.GetType()}"); Console.Error.WriteLine($"\tMessage: {ex.Message}"); - throw; + throw; } } } @@ -591,11 +758,12 @@ public class Data{ public int Timeout{ get; set; } public bool CheckPartLength{ get; set; } public bool IsResume{ get; set; } - public long BytesDownloaded{ get; set; } public int WaitTime{ get; set; } public string? Override{ get; set; } public long DateStart{ get; set; } - public long TotalBytes{ get; set; } + + public long BytesDownloaded; + public long TotalBytes; } public class PartsData{ diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index 158cf84..c61c921 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -21,7 +21,6 @@ using CRD.Utils.Files; using CRD.Utils.HLS; using CRD.Utils.JsonConv; using CRD.Utils.Structs; -using CRD.Utils.Structs.Crunchyroll; using FluentAvalonia.UI.Controls; using Microsoft.Win32; using Newtonsoft.Json; @@ -852,12 +851,43 @@ public class Helpers{ } else{ throw new PlatformNotSupportedException(); } + + try{ + using (var process = new Process()){ + process.StartInfo.FileName = shutdownCmd; + process.StartInfo.Arguments = shutdownArgs; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; - Process.Start(new ProcessStartInfo{ - FileName = shutdownCmd, - Arguments = shutdownArgs, - CreateNoWindow = true, - UseShellExecute = false - }); + process.ErrorDataReceived += (sender, e) => { + if (!string.IsNullOrEmpty(e.Data)){ + Console.Error.WriteLine($"{e.Data}"); + } + }; + + process.OutputDataReceived += (sender, e) => { + if (!string.IsNullOrEmpty(e.Data)){ + Console.Error.WriteLine(e.Data); + } + }; + + process.Start(); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + process.WaitForExit(); + + if (process.ExitCode != 0){ + Console.Error.WriteLine($"Shutdown failed with exit code {process.ExitCode}"); + } + + } + } catch (Exception ex){ + Console.Error.WriteLine($"Failed to start shutdown process: {ex.Message}"); + } + } } \ No newline at end of file diff --git a/CRD/Utils/Muxing/FontsManager.cs b/CRD/Utils/Muxing/FontsManager.cs index b2bede7..4a87479 100644 --- a/CRD/Utils/Muxing/FontsManager.cs +++ b/CRD/Utils/Muxing/FontsManager.cs @@ -32,36 +32,84 @@ public class FontsManager{ #endregion - public Dictionary> Fonts{ get; private set; } = new(){ - { "Adobe Arabic", new List{ "AdobeArabic-Bold.otf" } }, - { "Andale Mono", new List{ "andalemo.ttf" } }, - { "Arial", new List{ "arial.ttf", "arialbd.ttf", "arialbi.ttf", "ariali.ttf" } }, - { "Arial Unicode MS", new List{ "arialuni.ttf" } }, - { "Arial Black", new List{ "ariblk.ttf" } }, - { "Comic Sans MS", new List{ "comic.ttf", "comicbd.ttf" } }, - { "Courier New", new List{ "cour.ttf", "courbd.ttf", "courbi.ttf", "couri.ttf" } }, - { "DejaVu LGC Sans Mono", new List{ "DejaVuLGCSansMono-Bold.ttf", "DejaVuLGCSansMono-BoldOblique.ttf", "DejaVuLGCSansMono-Oblique.ttf", "DejaVuLGCSansMono.ttf" } }, - { "DejaVu Sans", new List{ "DejaVuSans-Bold.ttf", "DejaVuSans-BoldOblique.ttf", "DejaVuSans-ExtraLight.ttf", "DejaVuSans-Oblique.ttf", "DejaVuSans.ttf" } }, - { "DejaVu Sans Condensed", new List{ "DejaVuSansCondensed-Bold.ttf", "DejaVuSansCondensed-BoldOblique.ttf", "DejaVuSansCondensed-Oblique.ttf", "DejaVuSansCondensed.ttf" } }, - { "DejaVu Sans Mono", new List{ "DejaVuSansMono-Bold.ttf", "DejaVuSansMono-BoldOblique.ttf", "DejaVuSansMono-Oblique.ttf", "DejaVuSansMono.ttf" } }, - { "Georgia", new List{ "georgia.ttf", "georgiab.ttf", "georgiai.ttf", "georgiaz.ttf" } }, - { "Impact", new List{ "impact.ttf" } }, - { "Rubik Black", new List{ "Rubik-Black.ttf", "Rubik-BlackItalic.ttf" } }, - { "Rubik", new List{ "Rubik-Bold.ttf", "Rubik-BoldItalic.ttf", "Rubik-Italic.ttf", "Rubik-Light.ttf", "Rubik-LightItalic.ttf", "Rubik-Medium.ttf", "Rubik-MediumItalic.ttf", "Rubik-Regular.ttf" } }, - { "Tahoma", new List{ "tahoma.ttf" } }, - { "Times New Roman", new List{ "times.ttf", "timesbd.ttf", "timesbi.ttf", "timesi.ttf" } }, - { "Trebuchet MS", new List{ "trebuc.ttf", "trebucbd.ttf", "trebucbi.ttf", "trebucit.ttf" } }, - { "Verdana", new List{ "verdana.ttf", "verdanab.ttf", "verdanai.ttf", "verdanaz.ttf" } }, - { "Vrinda", new List{ "vrinda.ttf", "vrindab.ttf"} }, - { "Webdings", new List{ "webdings.ttf" } }, + public Dictionary Fonts{ get; private set; } = new(){ + { "Adobe Arabic", "AdobeArabic-Bold.otf" }, + { "Andale Mono", "andalemo.ttf" }, + { "Arial", "arial.ttf" }, + { "Arial Black", "ariblk.ttf" }, + { "Arial Bold", "arialbd.ttf" }, + { "Arial Bold Italic", "arialbi.ttf" }, + { "Arial Italic", "ariali.ttf" }, + { "Arial Unicode MS", "arialuni.ttf" }, + { "Comic Sans MS", "comic.ttf" }, + { "Comic Sans MS Bold", "comicbd.ttf" }, + { "Courier New", "cour.ttf" }, + { "Courier New Bold", "courbd.ttf" }, + { "Courier New Bold Italic", "courbi.ttf" }, + { "Courier New Italic", "couri.ttf" }, + { "DejaVu LGC Sans Mono", "DejaVuLGCSansMono.ttf" }, + { "DejaVu LGC Sans Mono Bold", "DejaVuLGCSansMono-Bold.ttf" }, + { "DejaVu LGC Sans Mono Bold Oblique", "DejaVuLGCSansMono-BoldOblique.ttf" }, + { "DejaVu LGC Sans Mono Oblique", "DejaVuLGCSansMono-Oblique.ttf" }, + { "DejaVu Sans", "DejaVuSans.ttf" }, + { "DejaVu Sans Bold", "DejaVuSans-Bold.ttf" }, + { "DejaVu Sans Bold Oblique", "DejaVuSans-BoldOblique.ttf" }, + { "DejaVu Sans Condensed", "DejaVuSansCondensed.ttf" }, + { "DejaVu Sans Condensed Bold", "DejaVuSansCondensed-Bold.ttf" }, + { "DejaVu Sans Condensed Bold Oblique", "DejaVuSansCondensed-BoldOblique.ttf" }, + { "DejaVu Sans Condensed Oblique", "DejaVuSansCondensed-Oblique.ttf" }, + { "DejaVu Sans ExtraLight", "DejaVuSans-ExtraLight.ttf" }, + { "DejaVu Sans Mono", "DejaVuSansMono.ttf" }, + { "DejaVu Sans Mono Bold", "DejaVuSansMono-Bold.ttf" }, + { "DejaVu Sans Mono Bold Oblique", "DejaVuSansMono-BoldOblique.ttf" }, + { "DejaVu Sans Mono Oblique", "DejaVuSansMono-Oblique.ttf" }, + { "DejaVu Sans Oblique", "DejaVuSans-Oblique.ttf" }, + { "Gautami", "gautami.ttf" }, + { "Georgia", "georgia.ttf" }, + { "Georgia Bold", "georgiab.ttf" }, + { "Georgia Bold Italic", "georgiaz.ttf" }, + { "Georgia Italic", "georgiai.ttf" }, + { "Impact", "impact.ttf" }, + { "Mangal", "MANGAL.woff2" }, + { "Meera Inimai", "MeeraInimai-Regular.ttf" }, + { "Noto Sans Tamil", "NotoSansTamilVariable.ttf" }, + { "Noto Sans Telugu", "NotoSansTeluguVariable.ttf" }, + { "Noto Sans Thai", "NotoSansThai.ttf" }, + { "Rubik", "Rubik-Regular.ttf" }, + { "Rubik Black", "Rubik-Black.ttf" }, + { "Rubik Black Italic", "Rubik-BlackItalic.ttf" }, + { "Rubik Bold", "Rubik-Bold.ttf" }, + { "Rubik Bold Italic", "Rubik-BoldItalic.ttf" }, + { "Rubik Italic", "Rubik-Italic.ttf" }, + { "Rubik Light", "Rubik-Light.ttf" }, + { "Rubik Light Italic", "Rubik-LightItalic.ttf" }, + { "Rubik Medium", "Rubik-Medium.ttf" }, + { "Rubik Medium Italic", "Rubik-MediumItalic.ttf" }, + { "Tahoma", "tahoma.ttf" }, + { "Times New Roman", "times.ttf" }, + { "Times New Roman Bold", "timesbd.ttf" }, + { "Times New Roman Bold Italic", "timesbi.ttf" }, + { "Times New Roman Italic", "timesi.ttf" }, + { "Trebuchet MS", "trebuc.ttf" }, + { "Trebuchet MS Bold", "trebucbd.ttf" }, + { "Trebuchet MS Bold Italic", "trebucbi.ttf" }, + { "Trebuchet MS Italic", "trebucit.ttf" }, + { "Verdana", "verdana.ttf" }, + { "Verdana Bold", "verdanab.ttf" }, + { "Verdana Bold Italic", "verdanaz.ttf" }, + { "Verdana Italic", "verdanai.ttf" }, + { "Vrinda", "vrinda.ttf" }, + { "Vrinda Bold", "vrindab.ttf" }, + { "Webdings", "webdings.ttf" } }; + public string root = "https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fonts/"; public async Task GetFontsAsync(){ Console.WriteLine("Downloading fonts..."); - var fonts = Fonts.Values.SelectMany(f => f).ToList(); + var fonts = Fonts.Values.ToList(); foreach (var font in fonts){ var fontLoc = Path.Combine(CfgManager.PathFONTS_DIR, font); @@ -125,8 +173,8 @@ public class FontsManager{ return styles.Distinct().ToList(); // Using Linq to remove duplicates } - public Dictionary> GetDictFromKeyList(List keysList){ - Dictionary> filteredDictionary = new Dictionary>(); + public Dictionary GetDictFromKeyList(List keysList){ + Dictionary filteredDictionary = new Dictionary(); foreach (string key in keysList){ if (Fonts.TryGetValue(key, out var font)){ @@ -148,7 +196,7 @@ public class FontsManager{ } public List MakeFontsList(string fontsDir, List subs){ - Dictionary> fontsNameList = new Dictionary>(); + Dictionary fontsNameList = new Dictionary(); List subsList = new List(); List fontsList = new List(); bool isNstr = true; @@ -175,13 +223,11 @@ public class FontsManager{ List missingFonts = new List(); foreach (var f in fontsNameList){ - if (Fonts.TryGetValue(f.Key, out var fontFiles)){ - foreach (var fontFile in fontFiles){ - string fontPath = Path.Combine(fontsDir, fontFile); - string mime = GetFontMimeType(fontFile); - if (File.Exists(fontPath) && new FileInfo(fontPath).Length != 0){ - fontsList.Add(new ParsedFont{ Name = fontFile, Path = fontPath, Mime = mime }); - } + if (Fonts.TryGetValue(f.Key, out var fontFile)){ + string fontPath = Path.Combine(fontsDir, fontFile); + string mime = GetFontMimeType(fontFile); + if (File.Exists(fontPath) && new FileInfo(fontPath).Length != 0){ + fontsList.Add(new ParsedFont{ Name = fontFile, Path = fontPath, Mime = mime }); } } else{ missingFonts.Add(f.Key); @@ -198,5 +244,5 @@ public class FontsManager{ public class SubtitleFonts{ public LanguageItem Language{ get; set; } - public Dictionary> Fonts{ get; set; } + public Dictionary Fonts{ get; set; } } \ No newline at end of file diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index 76d0354..5efd26f 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; -using CRD.Downloader.Crunchyroll; using CRD.Utils.Files; using CRD.Utils.Structs; @@ -264,6 +262,14 @@ public class Merger{ if (options.Description is{ Count: > 0 }){ args.Add($"--global-tags \"{Helpers.AddUncPrefixIfNeeded(options.Description[0].Path)}\""); } + + if (options.Cover.Count > 0){ + if (File.Exists(options.Cover.First().Path)){ + args.Add($"--attach-file \"{options.Cover.First().Path}\""); + args.Add($"--attachment-mime-type image/png"); + args.Add($"--attachment-name cover.png"); + } + } return string.Join(" ", args); @@ -425,8 +431,11 @@ public class Merger{ .ToList(); allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path)); allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path + ".resume")); + allMediaFiles.ForEach(file => Helpers.DeleteFile(file.Path + ".new.resume")); options.Description?.ForEach(description => Helpers.DeleteFile(description.Path)); + + options.Cover?.ForEach(cover => Helpers.DeleteFile(cover.Path)); // Delete chapter files if any options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path)); @@ -471,6 +480,7 @@ public class CrunchyMuxOptions{ public bool Mp4{ get; set; } public bool Mp3{ get; set; } public bool MuxFonts{ get; set; } + public bool MuxCover{ get; set; } public bool MuxDescription{ get; set; } public string ForceMuxer{ get; set; } public bool? NoCleanup{ get; set; } @@ -511,6 +521,7 @@ public class MergerOptions{ public bool CcSubsMuxingFlag{ get; set; } public bool SignsSubsAsForced{ get; set; } public List Description{ get; set; } = new List(); + public List Cover{ get; set; } =[]; } public class MuxOptions{ diff --git a/CRD/Utils/Parser/M3u8/ToM3u8Class.cs b/CRD/Utils/Parser/M3u8/ToM3u8Class.cs index c558e8d..fb74a5a 100644 --- a/CRD/Utils/Parser/M3u8/ToM3u8Class.cs +++ b/CRD/Utils/Parser/M3u8/ToM3u8Class.cs @@ -179,6 +179,7 @@ public class ToM3u8Class{ playlist.attributes = new ExpandoObject(); playlist.attributes.NAME = item.attributes.id; playlist.attributes.BANDWIDTH = item.attributes.bandwidth; + playlist.attributes.AUDIOSAMPLINGRATE = item.attributes.audioSamplingRate; playlist.attributes.CODECS = item.attributes.codecs; playlist.uri = string.Empty; playlist.endList = item.attributes.type == "static"; diff --git a/CRD/Utils/Parser/MPDTransformer.cs b/CRD/Utils/Parser/MPDTransformer.cs index 9e8da0c..f5f17ea 100644 --- a/CRD/Utils/Parser/MPDTransformer.cs +++ b/CRD/Utils/Parser/MPDTransformer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; +using CRD.Utils.DRM; using CRD.Utils.HLS; using CRD.Utils.Parser; using CRD.Utils.Parser.Utils; @@ -28,12 +29,15 @@ public class Map{ public class PlaylistItem{ public string? pssh{ get; set; } + public List encryptionKeys{ get; set; } =[]; public int bandwidth{ get; set; } public List segments{ get; set; } } public class AudioPlaylist : PlaylistItem{ public LanguageItem? language{ get; set; } + + public int audioSamplingRate{ get; set; } public bool @default{ get; set; } } @@ -92,6 +96,7 @@ public static class MPDParser{ var pItem = new AudioPlaylist{ bandwidth = playlist.attributes.BANDWIDTH, + audioSamplingRate = ObjectUtilities.GetMemberValue(playlist.attributes ,"AUDIOSAMPLINGRATE") ?? 0, language = audioLang, @default = item.@default, segments = segments.Select(segment => new Segment{ diff --git a/CRD/Utils/Parser/Playlists/ParseAttribute.cs b/CRD/Utils/Parser/Playlists/ParseAttribute.cs index d0b1abd..84df7b3 100644 --- a/CRD/Utils/Parser/Playlists/ParseAttribute.cs +++ b/CRD/Utils/Parser/Playlists/ParseAttribute.cs @@ -18,6 +18,7 @@ public class ParseAttribute{ { "width", Width }, { "height", Height }, { "bandwidth", Bandwidth }, + { "audioSamplingRate", AudioSamplingRate }, { "frameRate", FrameRate }, { "startNumber", StartNumber }, { "timescale", Timescale }, @@ -40,6 +41,7 @@ public class ParseAttribute{ public static object Width(string value) => int.Parse(value); public static object Height(string value) => int.Parse(value); public static object Bandwidth(string value) => int.Parse(value); + public static object AudioSamplingRate(string value) => int.Parse(value); public static object FrameRate(string value) => DivisionValueParser.ParseDivisionValue(value); public static object StartNumber(string value) => int.Parse(value); public static object Timescale(string value) => int.Parse(value); diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs index d55dbbb..686d37a 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs @@ -2,7 +2,6 @@ using CRD.Utils.Sonarr; using CRD.ViewModels; using Newtonsoft.Json; -using YamlDotNet.Serialization; namespace CRD.Utils.Structs.Crunchyroll; @@ -29,6 +28,9 @@ public class CrDownloadOptions{ [JsonIgnore] public string Force{ get; set; } = ""; + + [JsonProperty("download_methode_new")] + public bool DownloadMethodeNew{ get; set; } [JsonProperty("simultaneous_downloads")] public int SimultaneousDownloads{ get; set; } @@ -208,6 +210,9 @@ public class CrDownloadOptions{ [JsonProperty("mux_fonts")] public bool MuxFonts{ get; set; } + + [JsonProperty("mux_cover")] + public bool MuxCover{ get; set; } [JsonProperty("mux_video_title")] public string? VideoTitle{ get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/CrMovie.cs b/CRD/Utils/Structs/Crunchyroll/CrMovie.cs index 150288f..a7c2c76 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrMovie.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrMovie.cs @@ -85,7 +85,7 @@ public class CrunchyMovie{ public string Description{ get; set; } - public Images? Images{ get; set; } + public Images Images{ get; set; } = new(); [JsonProperty("media_type")] public string? MediaType{ get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs index 9127745..90852e9 100644 --- a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs +++ b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs @@ -54,7 +54,7 @@ public class CrunchyEpisode : IHistorySource{ [JsonProperty("availability_starts")] public DateTime AvailabilityStarts{ get; set; } - public Images? Images{ get; set; } + public Images Images{ get; set; } = new(); [JsonProperty("season_id")] public string SeasonId{ get; set; } @@ -289,7 +289,7 @@ public class CrunchyEpisode : IHistorySource{ public EpisodeType GetEpisodeType(){ return EpisodeType; } - + public string GetImageUrl(){ if (Images != null){ return Images.Thumbnail?.First().First().Source ?? string.Empty; @@ -303,15 +303,15 @@ public class CrunchyEpisode : IHistorySource{ public class Images{ [JsonProperty("poster_tall")] - public List>? PosterTall{ get; set; } + public List> PosterTall{ get; set; } =[]; [JsonProperty("poster_wide")] - public List>? PosterWide{ get; set; } + public List> PosterWide{ get; set; } =[]; [JsonProperty("promo_image")] - public List>? PromoImage{ get; set; } + public List> PromoImage{ get; set; } =[]; - public List>? Thumbnail{ get; set; } + public List> Thumbnail{ get; set; } =[]; } public class Image{ @@ -368,6 +368,7 @@ public class CrunchyEpMeta{ public string? SeriesId{ get; set; } public string? AbsolutEpisodeNumberE{ get; set; } public string? Image{ get; set; } + public string? ImageBig{ get; set; } public bool Paused{ get; set; } public DownloadProgress DownloadProgress{ get; set; } = new(); @@ -389,7 +390,6 @@ public class CrunchyEpMeta{ public bool OnlySubs{ get; set; } public CrDownloadOptions? DownloadSettings; - } public class DownloadProgress{ @@ -410,7 +410,7 @@ public class CrunchyEpMetaData{ public List? Versions{ get; set; } public bool IsSubbed{ get; set; } public bool IsDubbed{ get; set; } - + public (string? seasonID, string? guid) GetOriginalIds(){ var version = Versions?.FirstOrDefault(a => a.Original); if (version != null && !string.IsNullOrEmpty(version.Guid) && !string.IsNullOrEmpty(version.SeasonGuid)){ @@ -419,7 +419,6 @@ public class CrunchyEpMetaData{ return (null, null); } - } public class CrunchyRollEpisodeData{ diff --git a/CRD/Utils/Structs/HelperClasses.cs b/CRD/Utils/Structs/HelperClasses.cs index b4f4c64..ec74e53 100644 --- a/CRD/Utils/Structs/HelperClasses.cs +++ b/CRD/Utils/Structs/HelperClasses.cs @@ -97,7 +97,7 @@ public class SxItem{ public string? Path{ get; set; } public string? File{ get; set; } public string? Title{ get; set; } - public Dictionary>? Fonts{ get; set; } + public Dictionary? Fonts{ get; set; } } public class FrameData{ diff --git a/CRD/Utils/Structs/History/HistorySeries.cs b/CRD/Utils/Structs/History/HistorySeries.cs index cf9c241..f62b66b 100644 --- a/CRD/Utils/Structs/History/HistorySeries.cs +++ b/CRD/Utils/Structs/History/HistorySeries.cs @@ -10,9 +10,7 @@ using Avalonia.Media.Imaging; using CRD.Downloader.Crunchyroll; using CRD.Utils.CustomList; using CRD.Utils.Files; -using CRD.Views; using Newtonsoft.Json; -using ReactiveUI; namespace CRD.Utils.Structs.History; diff --git a/CRD/ViewModels/Utils/ContentDialogFeaturedMusicViewModel.cs b/CRD/ViewModels/Utils/ContentDialogFeaturedMusicViewModel.cs index d5c1221..b9fcecc 100644 --- a/CRD/ViewModels/Utils/ContentDialogFeaturedMusicViewModel.cs +++ b/CRD/ViewModels/Utils/ContentDialogFeaturedMusicViewModel.cs @@ -4,7 +4,6 @@ using System.Collections.ObjectModel; using System.Linq; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using CRD.Downloader; using CRD.Downloader.Crunchyroll; using CRD.Utils.Structs.Crunchyroll.Music; using CRD.Utils.Structs.History; diff --git a/CRD/ViewModels/Utils/ContentDialogSonarrMatchEpisodeViewModel.cs b/CRD/ViewModels/Utils/ContentDialogSonarrMatchEpisodeViewModel.cs index c32833b..7db1ee5 100644 --- a/CRD/ViewModels/Utils/ContentDialogSonarrMatchEpisodeViewModel.cs +++ b/CRD/ViewModels/Utils/ContentDialogSonarrMatchEpisodeViewModel.cs @@ -3,9 +3,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; -using Avalonia; -using Avalonia.Media; -using Avalonia.Media.Imaging; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CRD.Downloader.Crunchyroll; diff --git a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs index dd79a3c..4a1e364 100644 --- a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs +++ b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs @@ -35,7 +35,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ [ObservableProperty] private bool _history; - + [ObservableProperty] private bool _historyCountMissing; @@ -54,12 +54,15 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ [ObservableProperty] private double? _simultaneousDownloads; + [ObservableProperty] + private bool _downloadMethodeNew; + [ObservableProperty] private double? _downloadSpeed; - + [ObservableProperty] private double? _retryAttempts; - + [ObservableProperty] private double? _retryDelay; @@ -268,8 +271,9 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ HistorySkipUnmonitored = options.HistorySkipUnmonitored; HistoryCountSonarr = options.HistoryCountSonarr; DownloadSpeed = options.DownloadSpeedLimit; - RetryAttempts = Math.Clamp((options.RetryAttempts), 1, 10); - RetryDelay = Math.Clamp((options.RetryDelay), 1, 30); + DownloadMethodeNew = options.DownloadMethodeNew; + RetryAttempts = Math.Clamp((options.RetryAttempts), 1, 10); + RetryDelay = Math.Clamp((options.RetryDelay), 1, 30); DownloadToTempFolder = options.DownloadToTempFolder; SimultaneousDownloads = options.SimultaneousDownloads; LogMode = options.LogMode; @@ -292,12 +296,14 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ } var settings = CrunchyrollManager.Instance.CrunOptions; - + settings.DownloadFinishedPlaySound = DownloadFinishedPlaySound; + settings.DownloadMethodeNew = DownloadMethodeNew; + settings.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40); settings.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1); - + settings.RetryAttempts = Math.Clamp((int)(RetryAttempts ?? 0), 1, 10); settings.RetryDelay = Math.Clamp((int)(RetryDelay ?? 0), 1, 30); diff --git a/CRD/Views/DownloadsPageView.axaml.cs b/CRD/Views/DownloadsPageView.axaml.cs index a2a439a..ab037ab 100644 --- a/CRD/Views/DownloadsPageView.axaml.cs +++ b/CRD/Views/DownloadsPageView.axaml.cs @@ -1,6 +1,4 @@ using Avalonia.Controls; -using Avalonia.Interactivity; -using CRD.ViewModels; namespace CRD.Views; diff --git a/CRD/Views/SettingsPageView.axaml.cs b/CRD/Views/SettingsPageView.axaml.cs index ee39889..bd21607 100644 --- a/CRD/Views/SettingsPageView.axaml.cs +++ b/CRD/Views/SettingsPageView.axaml.cs @@ -1,7 +1,5 @@ -using System.Linq; -using Avalonia.Controls; +using Avalonia.Controls; using Avalonia.Interactivity; -using Avalonia.VisualTree; using CRD.Utils.Sonarr; using CRD.ViewModels; diff --git a/CRD/Views/Utils/ContentDialogFeaturedMusicView.axaml.cs b/CRD/Views/Utils/ContentDialogFeaturedMusicView.axaml.cs index 33af031..0026024 100644 --- a/CRD/Views/Utils/ContentDialogFeaturedMusicView.axaml.cs +++ b/CRD/Views/Utils/ContentDialogFeaturedMusicView.axaml.cs @@ -1,7 +1,4 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using CRD.Utils.UI; +using Avalonia.Controls; namespace CRD.Views.Utils; diff --git a/CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml.cs b/CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml.cs index 3d14656..3b530f9 100644 --- a/CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml.cs +++ b/CRD/Views/Utils/ContentDialogSonarrMatchEpisodeView.axaml.cs @@ -1,7 +1,4 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using CRD.Utils.UI; +using Avalonia.Controls; namespace CRD.Views.Utils; diff --git a/CRD/Views/Utils/GeneralSettingsView.axaml b/CRD/Views/Utils/GeneralSettingsView.axaml index 5d5271e..ca0c72e 100644 --- a/CRD/Views/Utils/GeneralSettingsView.axaml +++ b/CRD/Views/Utils/GeneralSettingsView.axaml @@ -32,7 +32,7 @@ - + @@ -70,6 +70,12 @@ Description="Adjust download settings" IsExpanded="False"> + + + + + + @@ -595,13 +601,7 @@ VerticalAlignment="Center" DockPanel.Dock="Left" /> - diff --git a/README.md b/README.md index 03cbd6a..4c6ef6c 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ For detailed information on each feature, please refer to the [GitHub Wiki](http Place one of the following tools in the `./lib/` directory: -- **mp4decrypt:** Available at [Bento4](http://www.bento4.com/) +- **mp4decrypt:** Available at [Bento4](https://www.bento4.com/downloads/) - **shaka-packager:** Available at [Shaka Packager Releases](https://github.com/shaka-project/shaka-packager/releases/latest) ### 2. Acquire Widevine CDM Files