diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index 3dad234..ef018e0 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -42,7 +42,7 @@ public class CrunchyrollManager{ public ObservableCollection HistoryList = new(); public HistorySeries SelectedSeries = new HistorySeries{ - Seasons =[] + Seasons = [] }; #endregion @@ -107,8 +107,8 @@ public class CrunchyrollManager{ options.Partsize = 10; options.DlSubs = new List{ "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(); 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 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 notSyncedDubs =[]; + List 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( data.Data.Where(x => x.IsAudioRoleDescription).Select(x => x.Lang?.CrLocale ?? "err") ); - + bool HasDescriptionRole(IEnumerable? 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 hsLangs = new List(); - var pbStreams = pbData.Data; + var pbStreams = pbData?.Data; var streams = new List(); 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(), - Error = dlFailed, - FileName = "./unknown", - ErrorText = "No Hardsubs available" - }; + return new DownloadResponse{ + Data = new List(), + 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 streamPlaylistsReqResponseList =[]; + Dictionary 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 streamServers = new List(streamPlaylists.Data.Keys); if (streamPlaylistsReqResponseList.Count > 0){ - HashSet streamServers =[]; + HashSet streamServers = []; Dictionary playListData = new Dictionary(); 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 authDataDict = new Dictionary - { { "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 authDataDict = new Dictionary - { { "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 authDataDict = new Dictionary - { { "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 subsData = pbData.Meta.Subtitles?.Values.ToList() ??[]; - List capsData = pbData.Meta.Captions?.Values.ToList() ??[]; + List subsData = pbData.Meta.Subtitles?.Values.ToList() ?? []; + List 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() @@ -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 ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId, CrAuth authEndpoint){ + private async Task ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId, CrAuth authEndpoint, CrAuthSettings optionsStreamEndpointSettings){ var temppbData = new PlaybackData{ Total = 0, Data = new Dictionary() @@ -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 diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs index 1260a66..378a4e9 100644 --- a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs +++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs @@ -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 _selectedSubLang =[]; + private ObservableCollection _selectedSubLang = []; [ObservableProperty] private Color _listBoxColor; @@ -231,12 +246,12 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ new(){ Content = "ar-SA" } ]; - public ObservableCollection DubLangList{ get; } =[]; + public ObservableCollection DubLangList{ get; } = []; - public ObservableCollection DefaultDubLangList{ get; } =[]; + public ObservableCollection DefaultDubLangList{ get; } = []; - public ObservableCollection DefaultSubLangList{ get; } =[]; + public ObservableCollection DefaultSubLangList{ get; } = []; public ObservableCollection SubLangList{ get; } =[ @@ -277,7 +292,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ new(){ Content = "tv/android_tv" }, ]; - public ObservableCollection FFmpegHWAccel{ get; } =[]; + public ObservableCollection 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 MapHWAccelOptions(List accels){ diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml index e7545e2..d8f410f 100644 --- a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml +++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml @@ -75,6 +75,12 @@ + + + + + + @@ -249,12 +255,27 @@ - + - - + + + + + + + + + + + + + + + @@ -266,33 +287,44 @@ SelectedItem="{Binding SelectedStreamEndpointSecondary}"> + + + + + + + + + - - - - - diff --git a/CRD/Program.cs b/CRD/Program.cs index e8d2c19..62f2d04 100644 --- a/CRD/Program.cs +++ b/CRD/Program.cs @@ -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() .UsePlatformDetect() .WithInterFont() - .LogToTrace(); + .LogToTrace() + .UseReactiveUI() ; if (isHeadless){ Console.WriteLine("Running in headless mode..."); diff --git a/CRD/Utils/Helpers.cs b/CRD/Utils/Helpers.cs index 9d3dff8..c565cee 100644 --- a/CRD/Utils/Helpers.cs +++ b/CRD/Utils/Helpers.cs @@ -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 ComputeWordFrequency(string text){ var wordFrequency = new Dictionary(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 target, - Dictionary source){ + Dictionary 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(kvp.Value.audio) : new List(), - video = kvp.Value.video != null ? new List(kvp.Value.video) : new List() + target[key] = new ServerData{ + audio = (mergeAudio && src.audio != null) + ? new List(src.audio) + : new List(), + video = (mergeVideo && src.video != null) + ? new List(src.video) + : new List() }; } } } + private static readonly SemaphoreSlim ShutdownLock = new(1, 1); public static async Task ShutdownComputer(){ diff --git a/CRD/Utils/Http/HttpClientReq.cs b/CRD/Utils/Http/HttpClientReq.cs index e3119c7..e7b3479 100644 --- a/CRD/Utils/Http/HttpClientReq.cs +++ b/CRD/Utils/Http/HttpClientReq.cs @@ -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"; diff --git a/CRD/Utils/Muxing/Merger.cs b/CRD/Utils/Muxing/Merger.cs index 48becca..5f25c5f 100644 --- a/CRD/Utils/Muxing/Merger.cs +++ b/CRD/Utils/Muxing/Merger.cs @@ -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 DubLangList{ get; set; } = new List(); public List SubLangList{ get; set; } = new List(); 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 Fonts{ get; set; } = new List(); - 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; } diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs index e54e416..7e5da7f 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs @@ -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; } diff --git a/CRD/Utils/Structs/Crunchyroll/CrProfile.cs b/CRD/Utils/Structs/Crunchyroll/CrProfile.cs index 635834c..3df2146 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrProfile.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrProfile.cs @@ -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; } diff --git a/CRD/Utils/Structs/Crunchyroll/Playback.cs b/CRD/Utils/Structs/Crunchyroll/Playback.cs index 2949fa3..a8de3c3 100644 --- a/CRD/Utils/Structs/Crunchyroll/Playback.cs +++ b/CRD/Utils/Structs/Crunchyroll/Playback.cs @@ -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; } diff --git a/CRD/Utils/Structs/HelperClasses.cs b/CRD/Utils/Structs/HelperClasses.cs index 25967d6..5757cd5 100644 --- a/CRD/Utils/Structs/HelperClasses.cs +++ b/CRD/Utils/Structs/HelperClasses.cs @@ -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{ diff --git a/README.md b/README.md index 192e349..be68530 100644 --- a/README.md +++ b/README.md @@ -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