- 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

@ -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=",
@ -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){
@ -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);
}
}
@ -904,7 +930,6 @@ 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")
);
@ -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));
@ -1215,6 +1248,13 @@ public class CrunchyrollManager{
ErrorText = "Hardsub not available"
};
}
} else{
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;
@ -1227,6 +1267,7 @@ public class CrunchyrollManager{
}
}
}
}
} else{
streams = streams.Where((s) => !s.IsHardsubbed).ToList();
@ -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
};
}
}
@ -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;
@ -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){
@ -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;
@ -151,6 +154,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;
@ -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;

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"
<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 };
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
@ -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
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 (kvp.Value.video != null)
existing.video.AddRange(kvp.Value.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,6 +71,7 @@ public class Merger{
index++;
}
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 })){
@ -94,15 +92,18 @@ public class Merger{
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");
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}\"");
@ -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
@ -452,10 +453,12 @@ public class Merger{
// Delete chapter files if any
options.Chapters?.ForEach(chapter => Helpers.DeleteFile(chapter.Path));
if (!options.SkipSubMux){
// Delete subtitle files
options.Subtitles.ForEach(subtitle => Helpers.DeleteFile(subtitle.File));
}
}
}
public class MergerInput{
public string Path{ get; set; }
@ -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

@ -151,6 +151,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