mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-01-11 20:10:26 +00:00
- 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:
parent
dc570bf420
commit
c5660a87e7
12 changed files with 276 additions and 132 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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...");
|
||||
|
|
|
|||
|
|
@ -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(){
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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:** 2015–2022
|
||||
|
||||
## 🖥️ Features
|
||||
|
|
|
|||
Loading…
Reference in a new issue