Add - Added a new download method, which can be enabled in the general download settings

Add - Added missing fonts
Chg - Changed to request encryption keys before downloading
Chg - Changed some log messages
This commit is contained in:
Elwador 2025-07-28 20:42:00 +00:00
parent 65200147a0
commit e80568cbb0
33 changed files with 522 additions and 161 deletions

View file

@ -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<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
var images = (item.Images?.Thumbnail ??[new List<Image>{ 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,

View file

@ -61,7 +61,7 @@ public class CrMovies{
return null;
}
var images = (episodeP.Images?.Thumbnail ?? new List<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
var images = (episodeP.Images?.Thumbnail ??[new List<Image>(){ new(){ Source = "/notFound.jpg" } }]);
var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ 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,

View file

@ -165,7 +165,7 @@ public class CrMusic{
public CrunchyEpMeta EpisodeMeta(CrunchyMusicVideo episodeP){
var images = (episodeP.Images?.Thumbnail ?? new List<Image>{ 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,

View file

@ -56,7 +56,7 @@ public class CrSeries{
}
var epNum = key.StartsWith('E') ? key[1..] : key;
var images = (item.Images?.Thumbnail ?? new List<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
var images = (item.Images?.Thumbnail ??[new List<Image>{ 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<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
var images = (value.Items[0].Images?.Thumbnail ??[new List<Image>{ 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);

View file

@ -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<string>{ "en-US" };
options.SkipMuxing = false;
options.MkvmergeOptions = new List<string>{ "--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<string, string> authDataDict = new Dictionary<string, string>
{ { "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<string, string> authDataDict = new Dictionary<string, string>
{ { "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<string, string> authDataDict = new Dictionary<string, string>
{ { "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<ContentKey> encryptionKeysAudio =[];
List<ContentKey> 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);
}

View file

@ -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);

View file

@ -411,6 +411,12 @@
<CheckBox IsChecked="{Binding MuxFonts}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Include episode thumbnail" Description="Embeds the episode thumbnail into the MKV as cover">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding MuxCover}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="File title"
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width} ${dubs}">

View file

@ -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;

View file

@ -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));
}
}
}

View file

@ -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;

View file

@ -182,6 +182,9 @@ public enum DownloadMediaType{
[EnumMember(Value = "Description")]
Description,
[EnumMember(Value = "Cover")]
Cover,
}
[JsonConverter(typeof(StringEnumConverter))]

View file

@ -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;

View file

@ -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<dynamic> 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<dynamic>(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<Task>();
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<string, string>(), 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{

View file

@ -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}");
}
}
}

View file

@ -32,36 +32,84 @@ public class FontsManager{
#endregion
public Dictionary<string, List<string>> Fonts{ get; private set; } = new(){
{ "Adobe Arabic", new List<string>{ "AdobeArabic-Bold.otf" } },
{ "Andale Mono", new List<string>{ "andalemo.ttf" } },
{ "Arial", new List<string>{ "arial.ttf", "arialbd.ttf", "arialbi.ttf", "ariali.ttf" } },
{ "Arial Unicode MS", new List<string>{ "arialuni.ttf" } },
{ "Arial Black", new List<string>{ "ariblk.ttf" } },
{ "Comic Sans MS", new List<string>{ "comic.ttf", "comicbd.ttf" } },
{ "Courier New", new List<string>{ "cour.ttf", "courbd.ttf", "courbi.ttf", "couri.ttf" } },
{ "DejaVu LGC Sans Mono", new List<string>{ "DejaVuLGCSansMono-Bold.ttf", "DejaVuLGCSansMono-BoldOblique.ttf", "DejaVuLGCSansMono-Oblique.ttf", "DejaVuLGCSansMono.ttf" } },
{ "DejaVu Sans", new List<string>{ "DejaVuSans-Bold.ttf", "DejaVuSans-BoldOblique.ttf", "DejaVuSans-ExtraLight.ttf", "DejaVuSans-Oblique.ttf", "DejaVuSans.ttf" } },
{ "DejaVu Sans Condensed", new List<string>{ "DejaVuSansCondensed-Bold.ttf", "DejaVuSansCondensed-BoldOblique.ttf", "DejaVuSansCondensed-Oblique.ttf", "DejaVuSansCondensed.ttf" } },
{ "DejaVu Sans Mono", new List<string>{ "DejaVuSansMono-Bold.ttf", "DejaVuSansMono-BoldOblique.ttf", "DejaVuSansMono-Oblique.ttf", "DejaVuSansMono.ttf" } },
{ "Georgia", new List<string>{ "georgia.ttf", "georgiab.ttf", "georgiai.ttf", "georgiaz.ttf" } },
{ "Impact", new List<string>{ "impact.ttf" } },
{ "Rubik Black", new List<string>{ "Rubik-Black.ttf", "Rubik-BlackItalic.ttf" } },
{ "Rubik", new List<string>{ "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<string>{ "tahoma.ttf" } },
{ "Times New Roman", new List<string>{ "times.ttf", "timesbd.ttf", "timesbi.ttf", "timesi.ttf" } },
{ "Trebuchet MS", new List<string>{ "trebuc.ttf", "trebucbd.ttf", "trebucbi.ttf", "trebucit.ttf" } },
{ "Verdana", new List<string>{ "verdana.ttf", "verdanab.ttf", "verdanai.ttf", "verdanaz.ttf" } },
{ "Vrinda", new List<string>{ "vrinda.ttf", "vrindab.ttf"} },
{ "Webdings", new List<string>{ "webdings.ttf" } },
public Dictionary<string, string> 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<string, List<string>> GetDictFromKeyList(List<string> keysList){
Dictionary<string, List<string>> filteredDictionary = new Dictionary<string, List<string>>();
public Dictionary<string, string> GetDictFromKeyList(List<string> keysList){
Dictionary<string, string> filteredDictionary = new Dictionary<string, string>();
foreach (string key in keysList){
if (Fonts.TryGetValue(key, out var font)){
@ -148,7 +196,7 @@ public class FontsManager{
}
public List<ParsedFont> MakeFontsList(string fontsDir, List<SubtitleFonts> subs){
Dictionary<string, List<string>> fontsNameList = new Dictionary<string, List<string>>();
Dictionary<string, string> fontsNameList = new Dictionary<string, string>();
List<string> subsList = new List<string>();
List<ParsedFont> fontsList = new List<ParsedFont>();
bool isNstr = true;
@ -175,13 +223,11 @@ public class FontsManager{
List<string> missingFonts = new List<string>();
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<string, List<string>> Fonts{ get; set; }
public Dictionary<string, string> Fonts{ get; set; }
}

View file

@ -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<MergerInput> Description{ get; set; } = new List<MergerInput>();
public List<MergerInput> Cover{ get; set; } =[];
}
public class MuxOptions{

View file

@ -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";

View file

@ -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<ContentKey> encryptionKeys{ get; set; } =[];
public int bandwidth{ get; set; }
public List<Segment> 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{

View file

@ -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);

View file

@ -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; }

View file

@ -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; }

View file

@ -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<List<Image>>? PosterTall{ get; set; }
public List<List<Image>> PosterTall{ get; set; } =[];
[JsonProperty("poster_wide")]
public List<List<Image>>? PosterWide{ get; set; }
public List<List<Image>> PosterWide{ get; set; } =[];
[JsonProperty("promo_image")]
public List<List<Image>>? PromoImage{ get; set; }
public List<List<Image>> PromoImage{ get; set; } =[];
public List<List<Image>>? Thumbnail{ get; set; }
public List<List<Image>> 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<EpisodeVersion>? 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{

View file

@ -97,7 +97,7 @@ public class SxItem{
public string? Path{ get; set; }
public string? File{ get; set; }
public string? Title{ get; set; }
public Dictionary<string, List<string>>? Fonts{ get; set; }
public Dictionary<string, string>? Fonts{ get; set; }
}
public class FrameData{

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -1,6 +1,4 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using CRD.ViewModels;
namespace CRD.Views;

View file

@ -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;

View file

@ -1,7 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using CRD.Utils.UI;
using Avalonia.Controls;
namespace CRD.Views.Utils;

View file

@ -1,7 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using CRD.Utils.UI;
using Avalonia.Controls;
namespace CRD.Views.Utils;

View file

@ -32,7 +32,7 @@
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History count Missing" Description="The missing count (orange corner) shows all missing episodes, not just new ones">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryCountMissing}"> </CheckBox>
@ -70,6 +70,12 @@
Description="Adjust download settings"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Enable New Download Method" Description="Enables the updated download handling logic. This may improve performance and stability.">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadMethodeNew}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Max Download Speed"
Description="Download in Kb/s - 0 is full speed">
<controls:SettingsExpanderItem.Footer>
@ -595,13 +601,7 @@
VerticalAlignment="Center"
DockPanel.Dock="Left" />
<controls:ColorPickerButton Color="{Binding CustomAccentColor}"
IsMoreButtonVisible="True"
UseSpectrum="True"
UseColorWheel="False"
UseColorTriangle="False"
UseColorPalette="False"
IsCompact="True" ShowAcceptDismissButtons="True"
<ColorPicker Color="{Binding CustomAccentColor}"
DockPanel.Dock="Right" />
</DockPanel>
</StackPanel>

View file

@ -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