- Added **fallback for hardsub** to use **no-hardsub video** if enabled

- Added **video/audio toggle for endpoints** to control which media type is used from each endpoint
- **Updated packages** to the latest versions
- **Updated Android phone token**
This commit is contained in:
Elwador 2025-12-01 11:43:55 +01:00
parent dc570bf420
commit c5660a87e7
12 changed files with 276 additions and 132 deletions

View file

@ -42,7 +42,7 @@ public class CrunchyrollManager{
public ObservableCollection<HistorySeries> HistoryList = new();
public HistorySeries SelectedSeries = new HistorySeries{
Seasons =[]
Seasons = []
};
#endregion
@ -107,8 +107,8 @@ public class CrunchyrollManager{
options.Partsize = 10;
options.DlSubs = new List<string>{ "en-US" };
options.SkipMuxing = false;
options.MkvmergeOptions =[];
options.FfmpegOptions =[];
options.MkvmergeOptions = [];
options.FfmpegOptions = [];
options.DefaultAudio = "ja-JP";
options.DefaultSub = "en-US";
options.QualityAudio = "best";
@ -128,7 +128,7 @@ public class CrunchyrollManager{
options.CalendarDubFilter = "none";
options.CustomCalendar = true;
options.DlVideoOnce = true;
options.StreamEndpoint = "web/firefox";
options.StreamEndpoint = new CrAuthSettings(){ Endpoint = "tv/android_tv", Audio = true, Video = true };
options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd;
options.HistoryLang = DefaultLocale;
options.FixCccSubtitles = true;
@ -201,13 +201,17 @@ public class CrunchyrollManager{
DefaultAndroidAuthSettings = new CrAuthSettings(){
Endpoint = "android/phone",
Authorization = "Basic YmY3MHg2aWhjYzhoZ3p3c2J2eGk6eDJjc3BQZXQzWno1d0pDdEpyVUNPSVM5Ynpad1JDcGM=",
UserAgent = "Crunchyroll/3.90.0 Android/16 okhttp/4.12.0",
Client_ID = "pd6uw3dfyhzghs0wxae3",
Authorization = "Basic cGQ2dXczZGZ5aHpnaHMwd3hhZTM6NXJ5SjJFQXR3TFc0UklIOEozaWk1anVqbnZrRWRfTkY=",
UserAgent = "Crunchyroll/3.95.2 Android/16 okhttp/4.12.0",
Device_name = "CPH2449",
Device_type = "OnePlus CPH2449"
Device_type = "OnePlus CPH2449",
Audio = true,
Video = true,
};
CrunOptions.StreamEndpoint = "tv/android_tv";
CrunOptions.StreamEndpoint ??= new CrAuthSettings(){ Endpoint = "tv/android_tv", Audio = true, Video = true };
CrunOptions.StreamEndpoint.Endpoint = "tv/android_tv";
CrAuthEndpoint1.AuthSettings = new CrAuthSettings(){
Endpoint = "tv/android_tv",
Authorization = "Basic ZGsxYndzemRyc3lkeTR1N2xvenE6bDl0SU1BdTlzTGc4ZjA4ajlfQkQ4eWZmQmZTSms0R0o=",
@ -236,7 +240,7 @@ public class CrunchyrollManager{
// ApiUrls.authBasicMob = "Basic " + token;
// }
var jsonFiles = Directory.Exists(CfgManager.PathENCODING_PRESETS_DIR) ? Directory.GetFiles(CfgManager.PathENCODING_PRESETS_DIR, "*.json") :[];
var jsonFiles = Directory.Exists(CfgManager.PathENCODING_PRESETS_DIR) ? Directory.GetFiles(CfgManager.PathENCODING_PRESETS_DIR, "*.json") : [];
foreach (var file in jsonFiles){
try{
@ -276,13 +280,13 @@ public class CrunchyrollManager{
}
});
} else{
HistoryList =[];
HistoryList = [];
}
} else{
HistoryList =[];
HistoryList = [];
}
} else{
HistoryList =[];
HistoryList = [];
}
@ -303,7 +307,14 @@ public class CrunchyrollManager{
Doing = "Starting"
};
QueueManager.Instance.Queue.Refresh();
var res = await DownloadMediaList(data, options);
var res = new DownloadResponse();
try{
res = await DownloadMediaList(data, options);
} catch (Exception e){
Console.WriteLine(e);
res.Error = true;
}
if (res.Error){
QueueManager.Instance.DecrementDownloads();
@ -356,7 +367,7 @@ public class CrunchyrollManager{
var fileNameAndPath = options.DownloadToTempFolder
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
if (options is{ DlVideoOnce: false, KeepDubsSeperate: true } && (!options.Noaudio || !options.Novids)){
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
var mergers = new List<Merger>();
foreach (var keyValue in groupByDub){
@ -421,7 +432,7 @@ public class CrunchyrollManager{
if (preset != null) await Helpers.RunFFmpegWithPresetAsync(merger.options.Output, preset, data);
}
if (options.DownloadToTempFolder){
await MoveFromTempFolder(merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, merger.options.Subtitles);
}
@ -481,7 +492,22 @@ public class CrunchyrollManager{
}
if (options.DownloadToTempFolder){
await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath ?? CfgManager.PathTEMP_DIR, result.merger?.options.Subtitles ?? []);
var tempFolder = res.TempFolderPath ?? CfgManager.PathTEMP_DIR;
List<SubtitleInput> subtitles =
result.merger?.options.Subtitles
?? res.Data
.Where(d => d.Type == DownloadMediaType.Subtitle)
.Select(d => new SubtitleInput{
File = d.Path ?? string.Empty,
Language = d.Language,
ClosedCaption = d.Cc ?? false,
Signs = d.Signs ?? false,
RelatedVideoDownloadMedia = d.RelatedVideoDownloadMedia
})
.ToList();
await MoveFromTempFolder(result.merger, data, options, tempFolder, subtitles);
}
}
@ -663,7 +689,7 @@ public class CrunchyrollManager{
foreach (var downloadedMedia in subs){
var subt = new SubtitleFonts();
subt.Language = downloadedMedia.Language;
subt.Fonts = downloadedMedia.Fonts ??[];
subt.Fonts = downloadedMedia.Fonts ?? [];
subsList.Add(subt);
}
@ -701,7 +727,7 @@ public class CrunchyrollManager{
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
KeepAllVideos = options.KeepAllVideos,
Fonts = options.MuxFonts ? FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList) :[],
Fonts = options.MuxFonts ? FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList) : [],
Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
VideoTitle = options.VideoTitle,
Options = new MuxOptions(){
@ -718,8 +744,8 @@ public class CrunchyrollManager{
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay,
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() :[],
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)){
@ -731,7 +757,7 @@ public class CrunchyrollManager{
}
bool isMuxed, syncError = false;
List<string> notSyncedDubs =[];
List<string> notSyncedDubs = [];
if (options is{ SyncTiming: true, DlVideoOnce: true } && merger.options.OnlyVid.Count > 0 && merger.options.OnlyAudio.Count > 0){
@ -904,11 +930,10 @@ public class CrunchyrollManager{
options.Partsize = options.Partsize > 0 ? options.Partsize : 1;
if (options.DownloadDescriptionAudio){
var alreadyAdr = new HashSet<string>(
data.Data.Where(x => x.IsAudioRoleDescription).Select(x => x.Lang?.CrLocale ?? "err")
);
bool HasDescriptionRole(IEnumerable<string>? roles) =>
roles?.Any(r => string.Equals(r, "description", StringComparison.OrdinalIgnoreCase)) == true;
@ -918,7 +943,7 @@ public class CrunchyrollManager{
.Where(m => m.Versions?.Any(v => (v.AudioLocale == (m.Lang?.CrLocale ?? "err"))
&& HasDescriptionRole(v.roles)) == true)
.ToList();
var additions = toDuplicate.Select(m => new CrunchyEpMetaData{
MediaId = m.MediaId,
Lang = m.Lang,
@ -1024,13 +1049,18 @@ public class CrunchyrollManager{
#endregion
var fetchPlaybackData = await FetchPlaybackData(CrAuthEndpoint1, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription);
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData = default;
(bool IsOk, PlaybackData pbData, string error) fetchPlaybackData2 = default;
if (CrAuthEndpoint2.Profile.Username != "???"){
fetchPlaybackData2 = await FetchPlaybackData(CrAuthEndpoint2, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription);
if (CrAuthEndpoint1.Profile.Username != "???" && options.StreamEndpoint != null && (options.StreamEndpoint.Video || options.StreamEndpoint.Audio)){
fetchPlaybackData = await FetchPlaybackData(CrAuthEndpoint1, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription, options.StreamEndpoint);
}
if (!fetchPlaybackData.IsOk){
if (CrAuthEndpoint2.Profile.Username != "???" && options.StreamEndpointSecondSettings != null && (options.StreamEndpointSecondSettings.Video || options.StreamEndpointSecondSettings.Audio)){
fetchPlaybackData2 = await FetchPlaybackData(CrAuthEndpoint2, mediaId, mediaGuid, data.Music, epMeta.IsAudioRoleDescription, options.StreamEndpointSecondSettings);
}
if (!fetchPlaybackData.IsOk && !fetchPlaybackData2.IsOk){
var errorJson = fetchPlaybackData.error;
if (!string.IsNullOrEmpty(errorJson)){
var error = StreamError.FromJson(errorJson);
@ -1077,7 +1107,7 @@ public class CrunchyrollManager{
}
if (fetchPlaybackData2.IsOk){
if (fetchPlaybackData.pbData.Data != null && fetchPlaybackData2.pbData?.Data != null)
if (fetchPlaybackData.pbData?.Data != null && fetchPlaybackData2.pbData?.Data != null){
foreach (var keyValuePair in fetchPlaybackData2.pbData.Data){
var pbDataFirstEndpoint = fetchPlaybackData.pbData?.Data;
if (pbDataFirstEndpoint != null && pbDataFirstEndpoint.TryGetValue(keyValuePair.Key, out var value)){
@ -1101,13 +1131,16 @@ public class CrunchyrollManager{
}
}
}
} else{
fetchPlaybackData = fetchPlaybackData2;
}
}
var pbData = fetchPlaybackData.pbData;
List<string> hsLangs = new List<string>();
var pbStreams = pbData.Data;
var pbStreams = pbData?.Data;
var streams = new List<StreamDetailsPop>();
variables.Add(new Variable("title", data.EpisodeTitle ?? string.Empty, true));
@ -1116,12 +1149,12 @@ public class CrunchyrollManager{
variables.Add(new Variable("seriesTitle", data.SeriesTitle ?? string.Empty, true));
variables.Add(new Variable("seasonTitle", data.SeasonTitle ?? string.Empty, true));
variables.Add(new Variable("season", !string.IsNullOrEmpty(data.Season) ? Math.Round(double.Parse(data.Season, CultureInfo.InvariantCulture), 1) : 0, false));
variables.Add(new Variable("dubs", string.Join(", ", data.SelectedDubs ??[]), true));
variables.Add(new Variable("dubs", string.Join(", ", data.SelectedDubs ?? []), true));
if (pbStreams?.Keys != null){
var pb = pbStreams.Select(v => {
if (v.Key != "none" && v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){
if (v.Key != "none" && v.Value is{ IsHardsubbed: true, HardsubLocale: not null } && v.Value.HardsubLocale != Locale.DefaulT && !hsLangs.Contains(v.Value.HardsubLang.CrLocale)){
hsLangs.Add(v.Value.HardsubLang.CrLocale);
}
@ -1216,14 +1249,22 @@ public class CrunchyrollManager{
};
}
} else{
dlFailed = true;
if (options.HsRawFallback){
streams = streams.Where((s) => !s.IsHardsubbed).ToList();
if (streams.Count < 1){
Console.Error.WriteLine("Raw streams not available!");
dlFailed = true;
}
} else{
dlFailed = true;
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = dlFailed,
FileName = "./unknown",
ErrorText = "No Hardsubs available"
};
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = dlFailed,
FileName = "./unknown",
ErrorText = "No Hardsubs available"
};
}
}
}
}
@ -1263,7 +1304,7 @@ public class CrunchyrollManager{
var videoDownloadMedia = new DownloadedMedia(){ Lang = Languages.DEFAULT_lang };
if (!dlFailed && curStream != null && options is not{ Novids: true, Noaudio: true }){
Dictionary<string, string> streamPlaylistsReqResponseList =[];
Dictionary<string, StreamInfo> streamPlaylistsReqResponseList = [];
foreach (var streamUrl in curStream.Url){
var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(streamUrl.Url ?? string.Empty, HttpMethod.Get, true, streamUrl.CrAuth?.Token?.access_token);
@ -1280,7 +1321,11 @@ public class CrunchyrollManager{
}
if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){
streamPlaylistsReqResponseList[streamUrl.Url ?? ""] = streamPlaylistsReqResponse.ResponseContent;
streamPlaylistsReqResponseList[streamUrl.Url ?? ""] = new StreamInfo(){
Playlist = streamPlaylistsReqResponse.ResponseContent,
Audio = streamUrl.Audio,
Video = streamUrl.Video
};
}
}
@ -1315,7 +1360,7 @@ public class CrunchyrollManager{
//
// List<string> streamServers = new List<string>(streamPlaylists.Data.Keys);
if (streamPlaylistsReqResponseList.Count > 0){
HashSet<string> streamServers =[];
HashSet<string> streamServers = [];
Dictionary<string, ServerData> playListData = new Dictionary<string, ServerData>();
foreach (var curStreams in streamPlaylistsReqResponseList){
@ -1328,9 +1373,10 @@ public class CrunchyrollManager{
}
try{
MPDParsed streamPlaylists = MPDParser.Parse(curStreams.Value, Languages.FindLang(crLocal), matchedUrl);
var entry = curStreams.Value;
MPDParsed streamPlaylists = MPDParser.Parse(entry.Playlist, Languages.FindLang(crLocal), matchedUrl);
streamServers.UnionWith(streamPlaylists.Data.Keys);
Helpers.MergePlaylistData(playListData, streamPlaylists.Data);
Helpers.MergePlaylistData(playListData, streamPlaylists.Data, entry.Audio, entry.Video);
} catch (Exception e){
Console.Error.WriteLine(e);
}
@ -1543,9 +1589,10 @@ public class CrunchyrollManager{
Console.WriteLine("Skipping video download...");
} else{
await CrAuthEndpoint1.RefreshToken(true);
await CrAuthEndpoint2.RefreshToken(true);
Dictionary<string, string> authDataDict = new Dictionary<string, string>
{ { "authorization", "Bearer " + CrAuthEndpoint1.Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
{ { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true } ) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.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);
@ -1576,10 +1623,11 @@ public class CrunchyrollManager{
if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){
await CrAuthEndpoint1.RefreshToken(true);
await CrAuthEndpoint2.RefreshToken(true);
if (chosenVideoSegments.encryptionKeys.Count == 0){
Dictionary<string, string> authDataDict = new Dictionary<string, string>
{ { "authorization", "Bearer " + CrAuthEndpoint1.Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
{ { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true } ) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.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);
@ -1635,9 +1683,10 @@ public class CrunchyrollManager{
}
await CrAuthEndpoint1.RefreshToken(true);
await CrAuthEndpoint2.RefreshToken(true);
Dictionary<string, string> authDataDict = new Dictionary<string, string>
{ { "authorization", "Bearer " + CrAuthEndpoint1.Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
{ { "authorization", "Bearer " + ((options.StreamEndpoint is { Audio: true } or{ Video: true } ) ? CrAuthEndpoint1.Token?.access_token : CrAuthEndpoint2.Token?.access_token) },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } };
var encryptionKeys = chosenVideoSegments.encryptionKeys;
@ -1895,7 +1944,7 @@ public class CrunchyrollManager{
var isAbsolute = Path.IsPathRooted(outFile);
// Get all directory parts of the path except the last segment (assuming it's a file)
var directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ??[];
var directories = Path.GetDirectoryName(outFile)?.Split(Path.DirectorySeparatorChar) ?? [];
// Initialize the cumulative path based on whether the original path is absolute or not
var cumulativePath = isAbsolute ? "" : fileDir;
@ -1990,7 +2039,7 @@ public class CrunchyrollManager{
Console.WriteLine($"{fileName}.xml has been created with the description.");
}
if (options.MuxCover){
if (options is{ MuxCover: true, Noaudio: false, Novids: false }){
if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(fileDir + "cover.png")){
var bitmap = await Helpers.LoadImage(data.ImageBig);
if (bitmap != null){
@ -2040,8 +2089,8 @@ public class CrunchyrollManager{
videoDownloadMedia.Lang = pbData.Meta.AudioLocale;
}
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ??[];
List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ??[];
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ?? [];
List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ?? [];
var subsDataMapped = subsData.Select(s => {
var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue());
return new{
@ -2348,7 +2397,8 @@ public class CrunchyrollManager{
#region Fetch Playback Data
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrAuth authEndpoint, string mediaId, string mediaGuidId, bool music, bool auioRoleDesc){
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrAuth authEndpoint, string mediaId, string mediaGuidId, bool music, bool auioRoleDesc,
CrAuthSettings optionsStreamEndpointSettings){
var temppbData = new PlaybackData{
Total = 0,
Data = new Dictionary<string, StreamDetails>()
@ -2364,7 +2414,7 @@ public class CrunchyrollManager{
}
if (playbackRequestResponse.IsOk){
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint);
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint, optionsStreamEndpointSettings);
} else{
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play{(auioRoleDesc ? "?audioRole=description" : "")}";
@ -2375,7 +2425,7 @@ public class CrunchyrollManager{
}
if (playbackRequestResponse.IsOk){
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint);
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint, optionsStreamEndpointSettings);
} else{
Console.Error.WriteLine("Fallback Request Stream URLs FAILED!");
}
@ -2405,7 +2455,7 @@ public class CrunchyrollManager{
return response;
}
private async Task<PlaybackData> ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId, CrAuth authEndpoint){
private async Task<PlaybackData> ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId, CrAuth authEndpoint, CrAuthSettings optionsStreamEndpointSettings){
var temppbData = new PlaybackData{
Total = 0,
Data = new Dictionary<string, StreamDetails>()
@ -2424,7 +2474,7 @@ public class CrunchyrollManager{
foreach (var hardsub in playStream.HardSubs){
var stream = hardsub.Value;
derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{
Url =[new UrlWithAuth(){ Url = stream.Url, CrAuth = authEndpoint }],
Url = [new UrlWithAuth(){ Url = stream.Url, CrAuth = authEndpoint, Audio = optionsStreamEndpointSettings.Audio, Video = optionsStreamEndpointSettings.Video }],
IsHardsubbed = true,
HardsubLocale = stream.Hlang,
HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue())
@ -2433,7 +2483,7 @@ public class CrunchyrollManager{
}
derivedPlayCrunchyStreams[""] = new StreamDetails{
Url =[new UrlWithAuth(){ Url = playStream.Url, CrAuth = authEndpoint }],
Url = [new UrlWithAuth(){ Url = playStream.Url, CrAuth = authEndpoint, Audio = optionsStreamEndpointSettings.Audio, Video = optionsStreamEndpointSettings.Video }],
IsHardsubbed = false,
HardsubLocale = Locale.DefaulT,
HardsubLang = Languages.DEFAULT_lang

View file

@ -139,6 +139,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private ComboBoxItem _selectedHSLang;
[ObservableProperty]
private bool _hsRawFallback;
[ObservableProperty]
private ComboBoxItem _selectedDescriptionLang;
@ -150,6 +153,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private ComboBoxItem _selectedStreamEndpoint;
[ObservableProperty]
private bool _firstEndpointVideo;
[ObservableProperty]
private bool _firstEndpointAudio;
[ObservableProperty]
private ComboBoxItem _SelectedStreamEndpointSecondary;
@ -169,6 +178,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private string _endpointDeviceType = "";
[ObservableProperty]
private bool _endpointVideo;
[ObservableProperty]
private bool _endpointAudio;
[ObservableProperty]
private bool _isLoggingIn;
@ -188,7 +203,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
private ComboBoxItem? _selectedAudioQuality;
[ObservableProperty]
private ObservableCollection<ListBoxItem> _selectedSubLang =[];
private ObservableCollection<ListBoxItem> _selectedSubLang = [];
[ObservableProperty]
private Color _listBoxColor;
@ -231,12 +246,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
new(){ Content = "ar-SA" }
];
public ObservableCollection<ListBoxItem> DubLangList{ get; } =[];
public ObservableCollection<ListBoxItem> DubLangList{ get; } = [];
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } =[];
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } = [];
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } =[];
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } = [];
public ObservableCollection<ListBoxItem> SubLangList{ get; } =[
@ -277,7 +292,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
new(){ Content = "tv/android_tv" },
];
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } =[];
public ObservableCollection<StringItemWithDisplayName> FFmpegHWAccel{ get; } = [];
[ObservableProperty]
private StringItemWithDisplayName _selectedFFmpegHWAccel;
@ -345,7 +360,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null;
SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0];
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint?.Endpoint ?? "")) ?? null;
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null;
@ -356,6 +371,11 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty;
EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty;
EndpointDeviceType = options.StreamEndpointSecondSettings?.Device_type ?? string.Empty;
EndpointVideo = options.StreamEndpointSecondSettings?.Video ?? true;
EndpointAudio = options.StreamEndpointSecondSettings?.Audio ?? true;
FirstEndpointVideo = options.StreamEndpoint?.Video ?? true;
FirstEndpointAudio = options.StreamEndpoint?.Audio ?? true;
if (CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???"){
EndpointNotSignedWarning = true;
@ -390,6 +410,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
HsRawFallback = options.HsRawFallback;
FixCccSubtitles = options.FixCccSubtitles;
ConvertVtt2Ass = options.ConvertVtt2Ass;
SubsDownloadDuplicate = options.SubsDownloadDuplicate;
@ -519,12 +540,16 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
CrunchyrollManager.Instance.CrunOptions.Hslang = SelectedHSLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.HsRawFallback = HsRawFallback;
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + "";
var endpointSettingsFirst = new CrAuthSettings();
endpointSettingsFirst.Endpoint = SelectedStreamEndpoint.Content + "";
endpointSettingsFirst.Video = FirstEndpointVideo;
endpointSettingsFirst.Audio = FirstEndpointAudio;
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = endpointSettingsFirst;
var endpointSettings = new CrAuthSettings();
endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + "";
@ -533,6 +558,8 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
endpointSettings.UserAgent = EndpointUserAgent;
endpointSettings.Device_name = EndpointDeviceName;
endpointSettings.Device_type = EndpointDeviceType;
endpointSettings.Video = EndpointVideo;
endpointSettings.Audio = EndpointAudio;
CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondSettings = endpointSettings;
@ -657,13 +684,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
}
}
} else{
CrunchyrollManager.Instance.HistoryList =[];
CrunchyrollManager.Instance.HistoryList = [];
}
}
_ = SonarrClient.Instance.RefreshSonarrLite();
} else{
CrunchyrollManager.Instance.HistoryList =[];
CrunchyrollManager.Instance.HistoryList = [];
}
}
}
@ -763,7 +790,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
}
return[];
return [];
}
private List<StringItemWithDisplayName> MapHWAccelOptions(List<string> accels){

View file

@ -75,6 +75,12 @@
</ComboBox>
</controls:SettingsExpander.Footer>
<controls:SettingsExpanderItem Content="No hardsubs fallback" Description="If no hardsubs are available, automatically download the no-hardsub (raw) video.">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HsRawFallback}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
@ -249,12 +255,27 @@
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Stream Endpoint " IsEnabled="False">
<controls:SettingsExpanderItem Content="Stream Endpoint ">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding StreamEndpoints}"
SelectedItem="{Binding SelectedStreamEndpoint}">
</ComboBox>
<StackPanel>
<ComboBox IsEnabled="False" HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding StreamEndpoints}"
SelectedItem="{Binding SelectedStreamEndpoint}">
</ComboBox>
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Video" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
IsChecked="{Binding FirstEndpointVideo}" />
</StackPanel>
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Audio" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
IsChecked="{Binding FirstEndpointAudio}" />
</StackPanel>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
@ -266,33 +287,44 @@
SelectedItem="{Binding SelectedStreamEndpointSecondary}">
</ComboBox>
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Video" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
IsChecked="{Binding EndpointVideo}" />
</StackPanel>
<StackPanel Margin="0,5" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Audio" Margin="0 , 0 , 5 , 0" VerticalAlignment="Center"/>
<CheckBox HorizontalAlignment="Left" MinWidth="250" VerticalAlignment="Center"
IsChecked="{Binding EndpointAudio}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Authorization" />
<TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250"
<TextBox Name="AuthorizationTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointAuthorization}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Client Id" />
<TextBox Name="ClientIdTextBox" HorizontalAlignment="Left" MinWidth="250"
<TextBox Name="ClientIdTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointClientId}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="User Agent" />
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250"
<TextBox Name="UserAgentTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointUserAgent}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Device Type" />
<TextBox Name="DeviceTypeTextBox" HorizontalAlignment="Left" MinWidth="250"
<TextBox Name="DeviceTypeTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointDeviceType}" />
</StackPanel>
<StackPanel Margin="0,5">
<TextBlock Text="Device Name" />
<TextBox Name="DeviceNameTextBox" HorizontalAlignment="Left" MinWidth="250"
<TextBox Name="DeviceNameTextBox" HorizontalAlignment="Left" MinWidth="250" MaxWidth="250" TextWrapping="Wrap"
Text="{Binding EndpointDeviceName}" />
</StackPanel>

View file

@ -1,6 +1,7 @@
using System;
using Avalonia;
using System.Linq;
using ReactiveUI.Avalonia;
namespace CRD;
@ -26,7 +27,8 @@ sealed class Program{
var builder = AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
.LogToTrace()
.UseReactiveUI() ;
if (isHeadless){
Console.WriteLine("Running in headless mode...");

View file

@ -73,10 +73,12 @@ public class Helpers{
}
public static int ToKbps(int bps) => (int)Math.Round(bps / 1000.0);
public static int SnapToAudioBucket(int kbps){
int[] buckets ={ 64, 96, 128, 192,256 };
int[] buckets = { 64, 96, 128, 192, 256 };
return buckets.OrderBy(b => Math.Abs(b - kbps)).First();
}
public static int WidthBucket(int width, int height){
int expected = (int)Math.Round(height * 16 / 9.0);
int tol = Math.Max(8, (int)(expected * 0.02)); // ~2% or ≥8 px
@ -555,7 +557,7 @@ public class Helpers{
return CosineSimilarity(vector1, vector2);
}
private static readonly char[] Delimiters ={ ' ', ',', '.', ';', ':', '-', '_', '\'' };
private static readonly char[] Delimiters = { ' ', ',', '.', ';', ':', '-', '_', '\'' };
public static Dictionary<string, double> ComputeWordFrequency(string text){
var wordFrequency = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
@ -718,7 +720,7 @@ public class Helpers{
bool isValid = !folderName.Any(c => invalidChars.Contains(c));
// Check for reserved names on Windows
string[] reservedNames =["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
string[] reservedNames = ["CON", "PRN", "AUX", "NUL", "COM1", "LPT1"];
bool isReservedName = reservedNames.Contains(folderName.ToUpperInvariant());
if (isValid && !isReservedName && folderName.Length <= 255){
@ -847,28 +849,39 @@ public class Helpers{
public static void MergePlaylistData(
Dictionary<string, ServerData> target,
Dictionary<string, ServerData> source){
Dictionary<string, ServerData> source,
bool mergeAudio,
bool mergeVideo){
foreach (var kvp in source){
if (target.TryGetValue(kvp.Key, out var existing)){
// Merge audio
existing.audio ??=[];
if (kvp.Value.audio != null)
existing.audio.AddRange(kvp.Value.audio);
var key = kvp.Key;
var src = kvp.Value;
// Merge video
existing.video ??=[];
if (kvp.Value.video != null)
existing.video.AddRange(kvp.Value.video);
if (target.TryGetValue(key, out var existing)){
if (mergeAudio){
existing.audio ??= [];
if (src.audio != null)
existing.audio.AddRange(src.audio);
}
if (mergeVideo){
existing.video ??= [];
if (src.video != null)
existing.video.AddRange(src.video);
}
} else{
// Add new entry (clone lists to avoid reference issues)
target[kvp.Key] = new ServerData{
audio = kvp.Value.audio != null ? new List<AudioPlaylist>(kvp.Value.audio) : new List<AudioPlaylist>(),
video = kvp.Value.video != null ? new List<VideoPlaylist>(kvp.Value.video) : new List<VideoPlaylist>()
target[key] = new ServerData{
audio = (mergeAudio && src.audio != null)
? new List<AudioPlaylist>(src.audio)
: new List<AudioPlaylist>(),
video = (mergeVideo && src.video != null)
? new List<VideoPlaylist>(src.video)
: new List<VideoPlaylist>()
};
}
}
}
private static readonly SemaphoreSlim ShutdownLock = new(1, 1);
public static async Task ShutdownComputer(){

View file

@ -270,6 +270,7 @@ public static class ApiUrls{
public static string Auth => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/auth/v1/token";
public static string Profile => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/profile";
public static string Profiles => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/accounts/v1/me/multiprofile";
public static string CmsToken => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/index/v2";
public static string Search => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/search";
public static string Browse => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/content/v2/discover/browse";

View file

@ -16,9 +16,6 @@ public class Merger{
public Merger(MergerOptions options){
this.options = options;
if (this.options.SkipSubMux != null && this.options.SkipSubMux == true){
this.options.Subtitles = new();
}
if (this.options.VideoTitle != null && this.options.VideoTitle.Length > 0){
this.options.VideoTitle = this.options.VideoTitle.Replace("\"", "'");
@ -74,35 +71,39 @@ public class Merger{
index++;
}
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
if (!options.SkipSubMux){
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
if (sub.value.Delay != null && sub.value.Delay != 0){
double delay = sub.value.Delay / 1000.0 ?? 0;
args.Add($"-itsoffset {delay.ToString(CultureInfo.InvariantCulture)}");
foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
if (sub.value.Delay != null && sub.value.Delay != 0){
double delay = sub.value.Delay / 1000.0 ?? 0;
args.Add($"-itsoffset {delay.ToString(CultureInfo.InvariantCulture)}");
}
args.Add($"-i \"{sub.value.File}\"");
metaData.Add($"-map {index}:s");
if (options.Defaults.Sub.Code == sub.value.Language.Code &&
(options.DefaultSubSigns == sub.value.Signs || options.DefaultSubSigns && !hasSignsSub)
&& sub.value.ClosedCaption == false){
metaData.Add($"-disposition:s:{sub.i} default");
} else{
metaData.Add($"-disposition:s:{sub.i} 0");
}
index++;
}
args.Add($"-i \"{sub.value.File}\"");
metaData.Add($"-map {index}:s");
if (options.Defaults.Sub.Code == sub.value.Language.Code &&
(options.DefaultSubSigns == sub.value.Signs || options.DefaultSubSigns && !hasSignsSub)
&& sub.value.ClosedCaption == false){
metaData.Add($"-disposition:s:{sub.i} default");
} else{
metaData.Add($"-disposition:s:{sub.i} 0");
}
index++;
}
args.AddRange(metaData);
// args.AddRange(options.Subtitles.Select((sub, subIndex) => $"-map {subIndex + index}"));
args.Add("-c:v copy");
args.Add("-c:a copy");
args.Add(options.Output.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) ? "-c:s mov_text" : "-c:s ass");
args.AddRange(options.Subtitles.Select((sub, subindex) =>
$"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}"));
if (!options.SkipSubMux){
args.AddRange(options.Subtitles.Select((sub, subindex) =>
$"-metadata:s:s:{subindex} title=\"{sub.Language.Language ?? sub.Language.Name}{(sub.ClosedCaption == true ? $" {options.CcTag}" : "")}{(sub.Signs == true ? " Signs" : "")}\" -metadata:s:s:{subindex} language={sub.Language.Code}"));
}
if (!string.IsNullOrEmpty(options.VideoTitle)){
args.Add($"-metadata title=\"{options.VideoTitle}\"");
@ -134,9 +135,9 @@ public class Merger{
}
var audio = options.OnlyAudio.First();
args.Add($"-i \"{audio.Path}\"");
args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : "") );
args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : ""));
args.Add($"\"{options.Output}\"");
return string.Join(" ", args);
}
@ -170,7 +171,7 @@ public class Merger{
// var sortedAudio = options.OnlyAudio
// .OrderBy(sub => options.DubLangList.IndexOf(sub.Language.CrLocale) != -1 ? options.DubLangList.IndexOf(sub.Language.CrLocale) : int.MaxValue)
// .ToList();
var rank = options.DubLangList
.Select((val, i) => new{ val, i })
.ToDictionary(x => x.val, x => x.i, StringComparer.OrdinalIgnoreCase);
@ -204,7 +205,7 @@ public class Merger{
args.Add($"\"{Helpers.AddUncPrefixIfNeeded(aud.Path)}\"");
}
if (options.Subtitles.Count > 0){
if (options.Subtitles.Count > 0 && !options.SkipSubMux){
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
var sortedSubtitles = options.Subtitles
@ -274,7 +275,7 @@ 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}\"");
@ -446,14 +447,16 @@ public class Merger{
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));
// Delete subtitle files
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File));
if (!options.SkipSubMux){
// Delete subtitle files
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File));
}
}
}
@ -486,7 +489,7 @@ public class CrunchyMuxOptions{
public List<string> DubLangList{ get; set; } = new List<string>();
public List<string> SubLangList{ get; set; } = new List<string>();
public string Output{ get; set; }
public bool? SkipSubMux{ get; set; }
public bool SkipSubMux{ get; set; }
public bool? KeepAllVideos{ get; set; }
public bool? Novids{ get; set; }
public bool Mp4{ get; set; }
@ -524,7 +527,7 @@ public class MergerOptions{
public string VideoTitle{ get; set; }
public bool? KeepAllVideos{ get; set; }
public List<ParsedFont> Fonts{ get; set; } = new List<ParsedFont>();
public bool? SkipSubMux{ get; set; }
public bool SkipSubMux{ get; set; }
public MuxOptions Options{ get; set; }
public Defaults Defaults{ get; set; }
public bool mp3{ get; set; }

View file

@ -150,6 +150,9 @@ public class CrDownloadOptions{
[JsonProperty("hard_sub_lang")]
public string Hslang{ get; set; } = "";
[JsonProperty("hard_sub_raw_fallback")]
public bool HsRawFallback{ get; set; }
[JsonIgnore]
public int Kstream{ get; set; }
@ -304,8 +307,8 @@ public class CrDownloadOptions{
[JsonProperty("calendar_show_upcoming_episodes")]
public bool CalendarShowUpcomingEpisodes{ get; set; }
[JsonProperty("stream_endpoint")]
public string? StreamEndpoint{ get; set; }
[JsonProperty("stream_endpoint_settings")]
public CrAuthSettings? StreamEndpoint{ get; set; }
[JsonProperty("stream_endpoint_secondary_settings")]
public CrAuthSettings? StreamEndpointSecondSettings{ get; set; }

View file

@ -11,6 +11,9 @@ public class CrProfile{
[JsonProperty("profile_name")]
public string? ProfileName{ get; set; }
[JsonProperty("profile_id")]
public string? ProfileId{ get; set; }
[JsonProperty("preferred_content_audio_language")]
public string? PreferredContentAudioLanguage{ get; set; }

View file

@ -30,6 +30,8 @@ public class StreamDetails{
public class UrlWithAuth{
public CrAuth? CrAuth{ get; set; }
public bool Video{ get; set; }
public bool Audio{ get; set; }
public string? Url{ get; set; }

View file

@ -20,6 +20,14 @@ public class CrAuthSettings{
public string Device_type{ get; set; }
public string Device_name{ get; set; }
public bool Video{ get; set; }
public bool Audio{ get; set; }
}
public class StreamInfo{
public string Playlist { get; set; }
public bool Audio { get; set; }
public bool Video { get; set; }
}
public class DrmAuthData{

View file

@ -29,7 +29,7 @@ A simple crunchyroll downloader that allows you to download your favorite series
## 🛠️ System Requirements
- **Operating System:** Windows 10 or Windows 11
- **.NET Desktop Runtime:** Version 8.0
- **.NET Desktop Runtime:** Version 10.0
- **Visual C++ Redistributable:** 20152022
## 🖥️ Features