From 15c62193ca6081749e3719d3117c8bdf29fb722f Mon Sep 17 00:00:00 2001 From: Elwador <75888166+Elwador@users.noreply.github.com> Date: Sat, 6 Sep 2025 17:39:20 +0200 Subject: [PATCH] - Added **second endpoint settings** that can be freely adjusted - Added basic **poster (tall and wide) download** for history series - Changed **download list** so that episode titles are highlighted if possibly all dubs/subs are available --- CRD/Downloader/CalendarManager.cs | 4 +- CRD/Downloader/Crunchyroll/CRAuth.cs | 176 +++++++++------ CRD/Downloader/Crunchyroll/CrEpisode.cs | 10 +- CRD/Downloader/Crunchyroll/CrMovies.cs | 2 +- CRD/Downloader/Crunchyroll/CrMusic.cs | 4 +- CRD/Downloader/Crunchyroll/CrSeries.cs | 24 +- .../Crunchyroll/CrunchyrollManager.cs | 156 +++++++------ .../CrunchyrollSettingsViewModel.cs | 81 ++++++- .../Views/CrunchyrollSettingsView.axaml | 83 +++++-- CRD/Downloader/History.cs | 2 +- CRD/Downloader/QueueManager.cs | 20 +- CRD/Utils/Http/HttpClientReq.cs | 94 +++----- .../Structs/Crunchyroll/CrDownloadOptions.cs | 4 +- .../Crunchyroll/Episode/EpisodeStructs.cs | 3 + CRD/Utils/Structs/Crunchyroll/Playback.cs | 13 +- CRD/Utils/Structs/HelperClasses.cs | 8 + CRD/ViewModels/AccountPageViewModel.cs | 23 +- CRD/ViewModels/SeriesPageViewModel.cs | 24 ++ .../Utils/ContentDialogInputLoginViewModel.cs | 13 +- .../ContentDialogSeriesDetailsViewModel.cs | 157 ++++++++++++++ .../Utils/GeneralSettingsViewModel.cs | 2 +- CRD/Views/DownloadsPageView.axaml | 81 +++---- CRD/Views/SeriesPageView.axaml | 39 ++-- .../ContentDialogSeriesDetailsView.axaml | 205 ++++++++++++++++++ .../ContentDialogSeriesDetailsView.axaml.cs | 35 +++ 25 files changed, 952 insertions(+), 311 deletions(-) create mode 100644 CRD/ViewModels/Utils/ContentDialogSeriesDetailsViewModel.cs create mode 100644 CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml create mode 100644 CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml.cs diff --git a/CRD/Downloader/CalendarManager.cs b/CRD/Downloader/CalendarManager.cs index 093d342..6c1dc15 100644 --- a/CRD/Downloader/CalendarManager.cs +++ b/CRD/Downloader/CalendarManager.cs @@ -68,8 +68,8 @@ public class CalendarManager{ } var request = calendarLanguage.ContainsKey(CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us") - ? HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false, false, null) - : HttpClientReq.CreateRequestMessage($"{calendarLanguage["en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false, false, null); + ? HttpClientReq.CreateRequestMessage($"{calendarLanguage[CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage ?? "en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false) + : HttpClientReq.CreateRequestMessage($"{calendarLanguage["en-us"]}?filter=premium&date={weeksMondayDate}", HttpMethod.Get, false); request.Headers.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"); diff --git a/CRD/Downloader/Crunchyroll/CRAuth.cs b/CRD/Downloader/Crunchyroll/CRAuth.cs index 13f16cf..288ce40 100644 --- a/CRD/Downloader/Crunchyroll/CRAuth.cs +++ b/CRD/Downloader/Crunchyroll/CRAuth.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using CRD.Utils; @@ -13,13 +14,60 @@ using ReactiveUI; namespace CRD.Downloader.Crunchyroll; -public class CrAuth{ - private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; +public class CrAuth(CrunchyrollManager crunInstance, CrAuthSettings authSettings){ + + public CrToken? Token; + public CrProfile Profile = new(); - private readonly string authorization = ApiUrls.authBasicMob; - private readonly string userAgent = ApiUrls.MobileUserAgent; - private readonly string deviceType = "OnePlus CPH2449"; - private readonly string deviceName = "CPH2449"; + public CrAuthSettings AuthSettings = authSettings; + + public Dictionary cookieStore = new(); + + public void Init(){ + + Profile = new CrProfile{ + Username = "???", + Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png", + PreferredContentAudioLanguage = "ja-JP", + PreferredContentSubtitleLanguage = crunInstance.DefaultLocale, + HasPremium = false, + }; + + } + + private string GetTokenFilePath(){ + switch (AuthSettings.Endpoint){ + case "tv/samsung": + case "tv/vidaa": + case "tv/android_tv": + return CfgManager.PathCrToken.Replace(".json", "_tv.json"); + case "android/phone": + case "android/tablet": + return CfgManager.PathCrToken.Replace(".json", "_android.json"); + case "console/switch": + case "console/ps4": + case "console/ps5": + case "console/xbox_one": + return CfgManager.PathCrToken.Replace(".json", "_console.json"); + default: + return CfgManager.PathCrToken; + + } + } + + public async Task Auth(){ + if (CfgManager.CheckIfFileExists(GetTokenFilePath())){ + Token = CfgManager.ReadJsonFromFile(GetTokenFilePath()); + await LoginWithToken(); + } else{ + await AuthAnonymous(); + } + } + + public void SetETPCookie(string refreshToken){ + HttpClientReq.Instance.AddCookie(".crunchyroll.com", new Cookie("etp_rt", refreshToken),cookieStore); + HttpClientReq.Instance.AddCookie(".crunchyroll.com", new Cookie("c_locale", "en-US"),cookieStore); + } public async Task AuthAnonymous(){ string uuid = Guid.NewGuid().ToString(); @@ -28,18 +76,18 @@ public class CrAuth{ { "grant_type", "client_id" }, { "scope", "offline_access" }, { "device_id", uuid }, - { "device_type", deviceType }, + { "device_type", AuthSettings.Device_type }, }; - if (!string.IsNullOrEmpty(deviceName)){ - formData.Add("device_name", deviceName); + if (!string.IsNullOrEmpty(AuthSettings.Device_name)){ + formData.Add("device_name", AuthSettings.Device_name); } var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ - { "Authorization", authorization }, - { "User-Agent", userAgent } + { "Authorization", AuthSettings.Authorization }, + { "User-Agent", AuthSettings.UserAgent } }; var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ @@ -58,7 +106,7 @@ public class CrAuth{ Console.Error.WriteLine("Anonymous login failed"); } - crunInstance.Profile = new CrProfile{ + Profile = new CrProfile{ Username = "???", Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png", PreferredContentAudioLanguage = "ja-JP", @@ -67,13 +115,13 @@ public class CrAuth{ } private void JsonTokenToFileAndVariable(string content, string deviceId){ - crunInstance.Token = Helpers.Deserialize(content, crunInstance.SettingsJsonSerializerSettings); + Token = Helpers.Deserialize(content, crunInstance.SettingsJsonSerializerSettings); - if (crunInstance.Token is{ expires_in: not null }){ - crunInstance.Token.device_id = deviceId; - crunInstance.Token.expires = DateTime.Now.AddSeconds((double)crunInstance.Token.expires_in); + if (Token is{ expires_in: not null }){ + Token.device_id = deviceId; + Token.expires = DateTime.Now.AddSeconds((double)Token.expires_in); - CfgManager.WriteJsonToFile(CfgManager.PathCrToken, crunInstance.Token); + CfgManager.WriteJsonToFile(GetTokenFilePath(), Token); } } @@ -86,18 +134,18 @@ public class CrAuth{ { "grant_type", "password" }, { "scope", "offline_access" }, { "device_id", uuid }, - { "device_type", deviceType }, + { "device_type", AuthSettings.Device_type }, }; - if (!string.IsNullOrEmpty(deviceName)){ - formData.Add("device_name", deviceName); + if (!string.IsNullOrEmpty(AuthSettings.Device_name)){ + formData.Add("device_name", AuthSettings.Device_name); } var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ - { "Authorization", authorization }, - { "User-Agent", userAgent } + { "Authorization", AuthSettings.Authorization }, + { "User-Agent", AuthSettings.UserAgent } }; var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ @@ -128,20 +176,20 @@ public class CrAuth{ } } - if (crunInstance.Token?.refresh_token != null){ - HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token); + if (Token?.refresh_token != null){ + SetETPCookie(Token.refresh_token); await GetProfile(); } } public async Task GetProfile(){ - if (crunInstance.Token?.access_token == null){ + if (Token?.access_token == null){ Console.Error.WriteLine("Missing Access Token"); return; } - var request = HttpClientReq.CreateRequestMessage(ApiUrls.Profile, HttpMethod.Get, true, true, null); + var request = HttpClientReq.CreateRequestMessage(ApiUrls.Profile, HttpMethod.Get, true, Token.access_token, null); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -149,42 +197,42 @@ public class CrAuth{ var profileTemp = Helpers.Deserialize(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); if (profileTemp != null){ - crunInstance.Profile = profileTemp; + Profile = profileTemp; - var requestSubs = HttpClientReq.CreateRequestMessage(ApiUrls.Subscription + crunInstance.Token.account_id, HttpMethod.Get, true, false, null); + var requestSubs = HttpClientReq.CreateRequestMessage(ApiUrls.Subscription + Token.account_id, HttpMethod.Get, true, Token.access_token, null); var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs); if (responseSubs.IsOk){ var subsc = Helpers.Deserialize(responseSubs.ResponseContent, crunInstance.SettingsJsonSerializerSettings); - crunInstance.Profile.Subscription = subsc; + Profile.Subscription = subsc; if (subsc is{ SubscriptionProducts:{ Count: 0 }, ThirdPartySubscriptionProducts.Count: > 0 }){ var thirdPartySub = subsc.ThirdPartySubscriptionProducts.First(); var expiration = thirdPartySub.InGrace ? thirdPartySub.InGraceExpirationDate : thirdPartySub.ExpirationDate; var remaining = expiration - DateTime.Now; - crunInstance.Profile.HasPremium = true; - if (crunInstance.Profile.Subscription != null){ - crunInstance.Profile.Subscription.IsActive = remaining > TimeSpan.Zero; - crunInstance.Profile.Subscription.NextRenewalDate = expiration; + Profile.HasPremium = true; + if (Profile.Subscription != null){ + Profile.Subscription.IsActive = remaining > TimeSpan.Zero; + Profile.Subscription.NextRenewalDate = expiration; } } else if (subsc is{ SubscriptionProducts:{ Count: 0 }, NonrecurringSubscriptionProducts.Count: > 0 }){ var nonRecurringSub = subsc.NonrecurringSubscriptionProducts.First(); var remaining = nonRecurringSub.EndDate - DateTime.Now; - crunInstance.Profile.HasPremium = true; - if (crunInstance.Profile.Subscription != null){ - crunInstance.Profile.Subscription.IsActive = remaining > TimeSpan.Zero; - crunInstance.Profile.Subscription.NextRenewalDate = nonRecurringSub.EndDate; + Profile.HasPremium = true; + if (Profile.Subscription != null){ + Profile.Subscription.IsActive = remaining > TimeSpan.Zero; + Profile.Subscription.NextRenewalDate = nonRecurringSub.EndDate; } } else if (subsc is{ SubscriptionProducts:{ Count: 0 }, FunimationSubscriptions.Count: > 0 }){ - crunInstance.Profile.HasPremium = true; + Profile.HasPremium = true; } else if (subsc is{ SubscriptionProducts.Count: > 0 }){ - crunInstance.Profile.HasPremium = true; + Profile.HasPremium = true; } else{ - crunInstance.Profile.HasPremium = false; + Profile.HasPremium = false; Console.Error.WriteLine($"No subscription available:\n {JsonConvert.SerializeObject(subsc, Formatting.Indented)} "); } } else{ - crunInstance.Profile.HasPremium = false; + Profile.HasPremium = false; Console.Error.WriteLine("Failed to check premium subscription status"); } } @@ -192,31 +240,31 @@ public class CrAuth{ } public async Task LoginWithToken(){ - if (crunInstance.Token?.refresh_token == null){ + if (Token?.refresh_token == null){ Console.Error.WriteLine("Missing Refresh Token"); await AuthAnonymous(); return; } - string uuid = string.IsNullOrEmpty(crunInstance.Token.device_id) ? Guid.NewGuid().ToString() : crunInstance.Token.device_id; + string uuid = string.IsNullOrEmpty(Token.device_id) ? Guid.NewGuid().ToString() : Token.device_id; var formData = new Dictionary{ - { "refresh_token", crunInstance.Token.refresh_token }, + { "refresh_token", Token.refresh_token }, { "scope", "offline_access" }, { "device_id", uuid }, { "grant_type", "refresh_token" }, - { "device_type", deviceType }, + { "device_type", AuthSettings.Device_type }, }; - if (!string.IsNullOrEmpty(deviceName)){ - formData.Add("device_name", deviceName); + if (!string.IsNullOrEmpty(AuthSettings.Device_name)){ + formData.Add("device_name", AuthSettings.Device_name); } var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ - { "Authorization", authorization }, - { "User-Agent", userAgent } + { "Authorization", AuthSettings.Authorization }, + { "User-Agent", AuthSettings.UserAgent } }; var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ @@ -227,7 +275,7 @@ public class CrAuth{ request.Headers.Add(header.Key, header.Value); } - HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token); + SetETPCookie(Token.refresh_token); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -243,8 +291,8 @@ public class CrAuth{ if (response.IsOk){ JsonTokenToFileAndVariable(response.ResponseContent, uuid); - if (crunInstance.Token?.refresh_token != null){ - HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token); + if (Token?.refresh_token != null){ + SetETPCookie(Token.refresh_token); await GetProfile(); } @@ -258,38 +306,38 @@ public class CrAuth{ } public async Task RefreshToken(bool needsToken){ - if (crunInstance.Token?.access_token == null && crunInstance.Token?.refresh_token == null || - crunInstance.Token.access_token != null && crunInstance.Token.refresh_token == null){ + if (Token?.access_token == null && Token?.refresh_token == null || + Token.access_token != null && Token.refresh_token == null){ await AuthAnonymous(); } else{ - if (!(DateTime.Now > crunInstance.Token.expires) && needsToken){ + if (!(DateTime.Now > Token.expires) && needsToken){ return; } } - if (crunInstance.Profile.Username == "???"){ + if (Profile.Username == "???"){ return; } - string uuid = string.IsNullOrEmpty(crunInstance.Token?.device_id) ? Guid.NewGuid().ToString() : crunInstance.Token.device_id; + string uuid = string.IsNullOrEmpty(Token?.device_id) ? Guid.NewGuid().ToString() : Token.device_id; var formData = new Dictionary{ - { "refresh_token", crunInstance.Token?.refresh_token ?? "" }, + { "refresh_token", Token?.refresh_token ?? "" }, { "grant_type", "refresh_token" }, { "scope", "offline_access" }, { "device_id", uuid }, - { "device_type", deviceType }, + { "device_type", AuthSettings.Device_type }, }; - if (!string.IsNullOrEmpty(deviceName)){ - formData.Add("device_name", deviceName); + if (!string.IsNullOrEmpty(AuthSettings.Device_name)){ + formData.Add("device_name", AuthSettings.Device_name); } var requestContent = new FormUrlEncodedContent(formData); var crunchyAuthHeaders = new Dictionary{ - { "Authorization", authorization }, - { "User-Agent", userAgent } + { "Authorization", AuthSettings.Authorization }, + { "User-Agent", AuthSettings.UserAgent } }; var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Auth){ @@ -300,7 +348,7 @@ public class CrAuth{ request.Headers.Add(header.Key, header.Value); } - HttpClientReq.Instance.SetETPCookie(crunInstance.Token?.refresh_token ?? string.Empty); + SetETPCookie(Token?.refresh_token ?? string.Empty); var response = await HttpClientReq.Instance.SendHttpRequest(request); diff --git a/CRD/Downloader/Crunchyroll/CrEpisode.cs b/CRD/Downloader/Crunchyroll/CrEpisode.cs index 52f872a..de2ee39 100644 --- a/CRD/Downloader/Crunchyroll/CrEpisode.cs +++ b/CRD/Downloader/Crunchyroll/CrEpisode.cs @@ -27,7 +27,7 @@ public class CrEpisode(){ } - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/episodes/{id}", HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/episodes/{id}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -51,7 +51,7 @@ public class CrEpisode(){ //guid for episode id foreach (var episodeVersionse in list){ foreach (var version in episodeVersionse){ - var checkRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/episodes/{version.Guid}", HttpMethod.Get, true, true, query); + var checkRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/episodes/{version.Guid}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var checkResponse = await HttpClientReq.Instance.SendHttpRequest(checkRequest, true); if (!checkResponse.IsOk){ epsidoe.Data.First().Versions?.Remove(version); @@ -236,7 +236,7 @@ public class CrEpisode(){ } public async Task GetNewEpisodes(string? crLocale, int requestAmount, DateTime? firstWeekDay = null, bool forcedLang = false){ - await crunInstance.CrAuth.RefreshToken(true); + await crunInstance.CrAuthEndpoint1.RefreshToken(true); CrBrowseEpisodeBase? complete = new CrBrowseEpisodeBase(); complete.Data =[]; @@ -257,7 +257,7 @@ public class CrEpisode(){ query["sort_by"] = "newly_added"; query["type"] = "episode"; - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, false, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -290,7 +290,7 @@ public class CrEpisode(){ } public async Task MarkAsWatched(string episodeId){ - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true, false, null); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/discover/{crunInstance.CrAuthEndpoint1.Token?.account_id}/mark_as_watched/{episodeId}", HttpMethod.Post, true, crunInstance.CrAuthEndpoint1.Token?.access_token, null); var response = await HttpClientReq.Instance.SendHttpRequest(request); diff --git a/CRD/Downloader/Crunchyroll/CrMovies.cs b/CRD/Downloader/Crunchyroll/CrMovies.cs index 92e3bab..3133112 100644 --- a/CRD/Downloader/Crunchyroll/CrMovies.cs +++ b/CRD/Downloader/Crunchyroll/CrMovies.cs @@ -25,7 +25,7 @@ public class CrMovies{ } - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/objects/{id}", HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/objects/{id}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); diff --git a/CRD/Downloader/Crunchyroll/CrMusic.cs b/CRD/Downloader/Crunchyroll/CrMusic.cs index 629de02..a2f4696 100644 --- a/CRD/Downloader/Crunchyroll/CrMusic.cs +++ b/CRD/Downloader/Crunchyroll/CrMusic.cs @@ -106,7 +106,7 @@ public class CrMusic{ public async Task ParseArtistByIdAsync(string id, string crLocale, bool forcedLang = false){ var query = CreateQueryParameters(crLocale, forcedLang); - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/music/artists/{id}", HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Content}/music/artists/{id}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -136,7 +136,7 @@ public class CrMusic{ private async Task FetchMediaListAsync(string url, string crLocale, bool forcedLang){ var query = CreateQueryParameters(crLocale, forcedLang); - var request = HttpClientReq.CreateRequestMessage(url, HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage(url, HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); diff --git a/CRD/Downloader/Crunchyroll/CrSeries.cs b/CRD/Downloader/Crunchyroll/CrSeries.cs index ac16272..76669f1 100644 --- a/CRD/Downloader/Crunchyroll/CrSeries.cs +++ b/CRD/Downloader/Crunchyroll/CrSeries.cs @@ -28,7 +28,7 @@ public class CrSeries{ for (int index = 0; index < episode.Items.Count; index++){ var item = episode.Items[index]; - if (item.IsPremiumOnly && !crunInstance.Profile.HasPremium){ + if (item.IsPremiumOnly && !crunInstance.CrAuthEndpoint1.Profile.HasPremium){ MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode – make sure that you are signed in with an account that has an active premium subscription", ToastType.Error, 3)); continue; } @@ -120,7 +120,7 @@ public class CrSeries{ public async Task ListSeriesId(string id, string crLocale, CrunchyMultiDownload? data, bool forcedLocale = false){ - await crunInstance.CrAuth.RefreshToken(true); + await crunInstance.CrAuthEndpoint1.RefreshToken(true); bool serieshasversions = true; @@ -305,7 +305,7 @@ public class CrSeries{ } } - var showRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonId}", HttpMethod.Get, true, true, query); + var showRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonId}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(showRequest); @@ -326,7 +326,7 @@ public class CrSeries{ } } - var episodeRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonId}/episodes", HttpMethod.Get, true, true, query); + var episodeRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonId}/episodes", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var episodeRequestResponse = await HttpClientReq.Instance.SendHttpRequest(episodeRequest); @@ -345,7 +345,7 @@ public class CrSeries{ } public async Task ParseSeriesById(string id, string? crLocale, bool forced = false){ - await crunInstance.CrAuth.RefreshToken(true); + await crunInstance.CrAuthEndpoint1.RefreshToken(true); NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); query["preferred_audio_language"] = "ja-JP"; @@ -357,7 +357,7 @@ public class CrSeries{ } - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/series/{id}/seasons", HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/series/{id}/seasons", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -377,7 +377,7 @@ public class CrSeries{ } public async Task SeriesById(string id, string? crLocale, bool forced = false){ - await crunInstance.CrAuth.RefreshToken(true); + await crunInstance.CrAuthEndpoint1.RefreshToken(true); NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); query["preferred_audio_language"] = "ja-JP"; @@ -388,7 +388,7 @@ public class CrSeries{ } } - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/series/{id}", HttpMethod.Get, true, true, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/series/{id}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -409,7 +409,7 @@ public class CrSeries{ public async Task Search(string searchString, string? crLocale, bool forced = false){ - await crunInstance.CrAuth.RefreshToken(true); + await crunInstance.CrAuthEndpoint1.RefreshToken(true); NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query); if (!string.IsNullOrEmpty(crLocale)){ @@ -423,7 +423,7 @@ public class CrSeries{ query["n"] = "6"; query["type"] = "series"; - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Search}", HttpMethod.Get, true, false, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Search}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -468,7 +468,7 @@ public class CrSeries{ query["n"] = "50"; query["sort_by"] = "alphabetical"; - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, false, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); @@ -503,7 +503,7 @@ public class CrSeries{ query["seasonal_tag"] = season.ToLower() + "-" + year; query["n"] = "100"; - var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, false, query); + var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, crunInstance.CrAuthEndpoint1.Token?.access_token, query); var response = await HttpClientReq.Instance.SendHttpRequest(request); diff --git a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs index 794692a..0080d72 100644 --- a/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs +++ b/CRD/Downloader/Crunchyroll/CrunchyrollManager.cs @@ -33,9 +33,6 @@ using LanguageItem = CRD.Utils.Structs.LanguageItem; namespace CRD.Downloader.Crunchyroll; public class CrunchyrollManager{ - public CrToken? Token; - - public CrProfile Profile = new(); private readonly Lazy _optionsLazy; public CrDownloadOptions CrunOptions => _optionsLazy.Value; @@ -60,7 +57,9 @@ public class CrunchyrollManager{ private Widevine _widevine = Widevine.Instance; - public CrAuth CrAuth; + public CrAuth CrAuthEndpoint1; + public CrAuth CrAuthEndpoint2; + public CrEpisode CrEpisode; public CrSeries CrSeries; public CrMovies CrMovies; @@ -153,20 +152,16 @@ public class CrunchyrollManager{ public void InitOptions(){ _widevine = Widevine.Instance; - CrAuth = new CrAuth(); + CrAuthEndpoint1 = new CrAuth(this, new CrAuthSettings()); + CrAuthEndpoint1.Init(); + CrAuthEndpoint2 = new CrAuth(this, new CrAuthSettings()); + CrAuthEndpoint2.Init(); + CrEpisode = new CrEpisode(); CrSeries = new CrSeries(); CrMovies = new CrMovies(); CrMusic = new CrMusic(); History = new History(); - - Profile = new CrProfile{ - Username = "???", - Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png", - PreferredContentAudioLanguage = "ja-JP", - PreferredContentSubtitleLanguage = DefaultLocale, - HasPremium = false, - }; } public static async Task GetBase64EncodedTokenAsync(){ @@ -200,9 +195,34 @@ public class CrunchyrollManager{ } CrunOptions.StreamEndpoint = "tv/android_tv"; - CrunOptions.StreamEndpointSecondary = ""; - CfgManager.WriteCrSettings(); + CrAuthEndpoint1.AuthSettings = new CrAuthSettings(){ + Endpoint = "tv/android_tv", + Authorization = "Basic Ym1icmt4eXgzZDd1NmpzZnlsYTQ6QUlONEQ1VkVfY3Awd1Z6Zk5vUDBZcUhVcllGcDloU2c=", + UserAgent = "ANDROIDTV/3.42.1_22267 Android/16", + Device_name = "Android TV", + Device_type = "Android TV" + }; + + + if (CrunOptions.StreamEndpointSecondSettings == null){ + CrunOptions.StreamEndpointSecondSettings = new CrAuthSettings(){ + Endpoint = "android/phone", + Authorization = "Basic YmY3MHg2aWhjYzhoZ3p3c2J2eGk6eDJjc3BQZXQzWno1d0pDdEpyVUNPSVM5Ynpad1JDcGM=", + UserAgent = "Crunchyroll/3.90.0 Android/16 okhttp/4.12.0", + Device_name = "CPH2449", + Device_type = "OnePlus CPH2449" + }; + } + CrAuthEndpoint2.AuthSettings = CrunOptions.StreamEndpointSecondSettings; + + await CrAuthEndpoint1.Auth(); + if (!string.IsNullOrEmpty(CrAuthEndpoint2.AuthSettings.Endpoint)){ + await CrAuthEndpoint2.Auth(); + } + + CfgManager.WriteCrSettings(); + // var token = await GetBase64EncodedTokenAsync(); // // if (!string.IsNullOrEmpty(token)){ @@ -227,13 +247,6 @@ public class CrunchyrollManager{ } } - if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){ - Token = CfgManager.ReadJsonFromFile(CfgManager.PathCrToken); - await CrAuth.LoginWithToken(); - } else{ - await CrAuth.AuthAnonymous(); - } - if (CrunOptions.History){ if (File.Exists(CfgManager.PathCrHistory)){ @@ -680,7 +693,7 @@ public class CrunchyrollManager{ CcSubsMuxingFlag = options.CcSubsMuxingFlag, SignsSubsAsForced = options.SignsSubsAsForced, Description = muxDesc ? data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() :[], - Cover = options.MuxCover ? data.Where(a => a.Type == DownloadMediaType.Cover).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList() : [], + 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)){ @@ -760,7 +773,7 @@ public class CrunchyrollManager{ } private async Task DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){ - if (Profile.Username == "???"){ + if (CrAuthEndpoint1.Profile.Username == "???"){ MainWindow.Instance.ShowError($"User Account not recognized - are you signed in?"); return new DownloadResponse{ Data = new List(), @@ -886,7 +899,7 @@ public class CrunchyrollManager{ } - await CrAuth.RefreshToken(true); + await CrAuthEndpoint1.RefreshToken(true); EpisodeVersion currentVersion = new EpisodeVersion(); EpisodeVersion primaryVersion = new EpisodeVersion(); @@ -947,10 +960,10 @@ public class CrunchyrollManager{ #endregion - var fetchPlaybackData = await FetchPlaybackData(options.StreamEndpoint ?? "web/firefox", mediaId, mediaGuid, data.Music); + var fetchPlaybackData = await FetchPlaybackData(CrAuthEndpoint1, mediaId, mediaGuid, data.Music); (bool IsOk, PlaybackData pbData, string error) fetchPlaybackData2 = default; - if (!string.IsNullOrEmpty(options.StreamEndpointSecondary) && !(options.StreamEndpoint ?? "web/firefox").Equals(options.StreamEndpointSecondary)){ - fetchPlaybackData2 = await FetchPlaybackData(options.StreamEndpointSecondary, mediaId, mediaGuid, data.Music); + if (CrAuthEndpoint2.Profile.Username != "???"){ + fetchPlaybackData2 = await FetchPlaybackData(CrAuthEndpoint2, mediaId, mediaGuid, data.Music); } if (!fetchPlaybackData.IsOk){ @@ -1004,13 +1017,13 @@ public class CrunchyrollManager{ foreach (var keyValuePair in fetchPlaybackData2.pbData.Data){ var pbDataFirstEndpoint = fetchPlaybackData.pbData?.Data; if (pbDataFirstEndpoint != null && pbDataFirstEndpoint.TryGetValue(keyValuePair.Key, out var value)){ - var urlSecondEndpoint = keyValuePair.Value.Url.First() ?? ""; + var secondEndpoint = keyValuePair.Value.Url.First(); - var match = Regex.Match(urlSecondEndpoint, @"(https?:\/\/.*?\/(?:dash\/|\.urlset\/))"); - var shortendUrl = match.Success ? match.Value : urlSecondEndpoint; + var match = Regex.Match(secondEndpoint.Url ?? "", @"(https?:\/\/.*?\/(?:dash\/|\.urlset\/))"); + var shortendUrl = match.Success ? match.Value : secondEndpoint.Url; - if (!value.Url.Any(arrayUrl => arrayUrl != null && arrayUrl.Contains(shortendUrl))){ - value.Url.Add(urlSecondEndpoint); + if (!string.IsNullOrEmpty(shortendUrl) && !value.Url.Any(arrayUrl => arrayUrl.Url != null && arrayUrl.Url.Contains(shortendUrl))){ + value.Url.Add(secondEndpoint); } } else{ if (pbDataFirstEndpoint != null){ @@ -1189,7 +1202,7 @@ public class CrunchyrollManager{ Dictionary streamPlaylistsReqResponseList =[]; foreach (var streamUrl in curStream.Url){ - var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(streamUrl ?? string.Empty, HttpMethod.Get, true, true, null); + var streamPlaylistsReq = HttpClientReq.CreateRequestMessage(streamUrl.Url ?? string.Empty, HttpMethod.Get, true, streamUrl.CrAuth?.Token?.access_token); var streamPlaylistsReqResponse = await HttpClientReq.Instance.SendHttpRequest(streamPlaylistsReq); if (!streamPlaylistsReqResponse.IsOk){ @@ -1203,7 +1216,7 @@ public class CrunchyrollManager{ } if (streamPlaylistsReqResponse.ResponseContent.Contains("MPD")){ - streamPlaylistsReqResponseList[streamUrl ?? ""] = streamPlaylistsReqResponse.ResponseContent; + streamPlaylistsReqResponseList[streamUrl.Url ?? ""] = streamPlaylistsReqResponse.ResponseContent; } } @@ -1453,10 +1466,10 @@ public class CrunchyrollManager{ } else if (options.Novids){ Console.WriteLine("Skipping video download..."); } else{ - await CrAuth.RefreshToken(true); + await CrAuthEndpoint1.RefreshToken(true); Dictionary authDataDict = new Dictionary - { { "authorization", "Bearer " + Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } }; + { { "authorization", "Bearer " + CrAuthEndpoint1.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); @@ -1486,11 +1499,11 @@ public class CrunchyrollManager{ if (chosenAudioSegments.segments.Count > 0 && !options.Noaudio && !dlFailed){ - await CrAuth.RefreshToken(true); + await CrAuthEndpoint1.RefreshToken(true); if (chosenVideoSegments.encryptionKeys.Count == 0){ Dictionary authDataDict = new Dictionary - { { "authorization", "Bearer " + Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } }; + { { "authorization", "Bearer " + CrAuthEndpoint1.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); @@ -1545,10 +1558,10 @@ public class CrunchyrollManager{ }; } - await CrAuth.RefreshToken(true); + await CrAuthEndpoint1.RefreshToken(true); Dictionary authDataDict = new Dictionary - { { "authorization", "Bearer " + Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } }; + { { "authorization", "Bearer " + CrAuthEndpoint1.Token?.access_token },{ "x-cr-content-id", mediaGuid },{ "x-cr-video-token", pbData.Meta?.Token ?? string.Empty } }; var encryptionKeys = chosenVideoSegments.encryptionKeys; @@ -1899,7 +1912,7 @@ public class CrunchyrollManager{ Console.WriteLine($"{fileName}.xml has been created with the description."); } - + if (options.MuxCover){ if (!string.IsNullOrEmpty(data.ImageBig) && !File.Exists(fileDir + "cover.png")){ var bitmap = await Helpers.LoadImage(data.ImageBig); @@ -1909,14 +1922,14 @@ public class CrunchyrollManager{ await using (var fs = File.OpenWrite(coverPath)){ bitmap.Save(fs); // always saves PNG } + bitmap.Dispose(); - + files.Add(new DownloadedMedia{ Type = DownloadMediaType.Cover, Lang = Languages.DEFAULT_lang, Path = coverPath }); - } } } @@ -2004,7 +2017,8 @@ public class CrunchyrollManager{ } } - sxData.File = Languages.SubsFile(fileName, index + "", langItem, (isDuplicate || options is{ KeepDubsSeperate: true, DlVideoOnce: false }) ? videoDownloadMedia.Lang.CrLocale : "", isCc, options.CcTag, isSigns, subsItem.format, + sxData.File = Languages.SubsFile(fileName, index + "", langItem, (isDuplicate || options is{ KeepDubsSeperate: true, DlVideoOnce: false }) ? videoDownloadMedia.Lang.CrLocale : "", isCc, options.CcTag, + isSigns, subsItem.format, !(data.DownloadSubs.Count == 1 && !data.DownloadSubs.Contains("all"))); sxData.Path = Path.Combine(fileDir, sxData.File); @@ -2015,7 +2029,7 @@ public class CrunchyrollManager{ continue; } - var subsAssReq = HttpClientReq.CreateRequestMessage(subsItem.url, HttpMethod.Get, false, false, null); + var subsAssReq = HttpClientReq.CreateRequestMessage(subsItem.url, HttpMethod.Get, false, Instance.CrAuthEndpoint1.Token?.access_token, null); var subsAssReqResponse = await HttpClientReq.Instance.SendHttpRequest(subsAssReq); @@ -2249,32 +2263,34 @@ public class CrunchyrollManager{ #region Fetch Playback Data - private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(string streamEndpoint, string mediaId, string mediaGuidId, bool music){ + private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrAuth authEndpoint, string mediaId, string mediaGuidId, bool music){ var temppbData = new PlaybackData{ Total = 0, Data = new Dictionary() }; - var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{streamEndpoint}/play"; - var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); + await authEndpoint.RefreshToken(true); + + var playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/{authEndpoint.AuthSettings.Endpoint}/play?queue=false"; + var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint, authEndpoint); if (!playbackRequestResponse.IsOk){ - playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint); + playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint, authEndpoint); } if (playbackRequestResponse.IsOk){ - temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId); + temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint); } else{ Console.WriteLine("Request Stream URLs FAILED! Attempting fallback"); playbackEndpoint = $"{ApiUrls.Playback}/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play"; - playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); + playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint, authEndpoint); if (!playbackRequestResponse.IsOk){ - playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint); + playbackRequestResponse = await HandleStreamErrorsAsync(playbackRequestResponse, playbackEndpoint, authEndpoint); } if (playbackRequestResponse.IsOk){ - temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId); + temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId, authEndpoint); } else{ Console.Error.WriteLine("Fallback Request Stream URLs FAILED!"); } @@ -2283,28 +2299,28 @@ public class CrunchyrollManager{ return (playbackRequestResponse.IsOk, pbData: temppbData, error: playbackRequestResponse.IsOk ? "" : playbackRequestResponse.ResponseContent); } - private async Task<(bool IsOk, string ResponseContent, string error)> SendPlaybackRequestAsync(string endpoint){ - var request = HttpClientReq.CreateRequestMessage(endpoint, HttpMethod.Get, true, false, null); - request.Headers.UserAgent.ParseAdd("ANDROIDTV/3.42.1_22267 Android/16"); - return await HttpClientReq.Instance.SendHttpRequest(request); + private async Task<(bool IsOk, string ResponseContent, string error)> SendPlaybackRequestAsync(string endpoint, CrAuth authEndpoint){ + var request = HttpClientReq.CreateRequestMessage(endpoint, HttpMethod.Get, true, authEndpoint.Token?.access_token, null); + request.Headers.UserAgent.ParseAdd(authEndpoint.AuthSettings.UserAgent); + return await HttpClientReq.Instance.SendHttpRequest(request,false,authEndpoint.cookieStore); } - private async Task<(bool IsOk, string ResponseContent, string error)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent, string error) response, string endpoint){ + private async Task<(bool IsOk, string ResponseContent, string error)> HandleStreamErrorsAsync((bool IsOk, string ResponseContent, string error) response, string endpoint, CrAuth authEndpoint){ if (response.IsOk || string.IsNullOrEmpty(response.ResponseContent)) return response; var error = StreamError.FromJson(response.ResponseContent); if (error?.IsTooManyActiveStreamsError() == true){ foreach (var errorActiveStream in error.ActiveStreams){ - await HttpClientReq.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token); + await Instance.DeAuthVideo(errorActiveStream.ContentId, errorActiveStream.Token, authEndpoint); } - return await SendPlaybackRequestAsync(endpoint); + return await SendPlaybackRequestAsync(endpoint, authEndpoint); } return response; } - private async Task ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId){ + private async Task ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId, CrAuth authEndpoint){ var temppbData = new PlaybackData{ Total = 0, Data = new Dictionary() @@ -2314,7 +2330,7 @@ public class CrunchyrollManager{ if (playStream == null) return temppbData; if (!string.IsNullOrEmpty(playStream.Token)){ - await HttpClientReq.DeAuthVideo(mediaGuidId, playStream.Token); + await Instance.DeAuthVideo(mediaGuidId, playStream.Token, authEndpoint); } var derivedPlayCrunchyStreams = new CrunchyStreams(); @@ -2323,7 +2339,7 @@ public class CrunchyrollManager{ foreach (var hardsub in playStream.HardSubs){ var stream = hardsub.Value; derivedPlayCrunchyStreams[hardsub.Key] = new StreamDetails{ - Url =[stream.Url], + Url =[new UrlWithAuth(){ Url = stream.Url, CrAuth = authEndpoint }], IsHardsubbed = true, HardsubLocale = stream.Hlang, HardsubLang = Languages.FixAndFindCrLc((stream.Hlang ?? Locale.DefaulT).GetEnumMemberValue()) @@ -2332,7 +2348,7 @@ public class CrunchyrollManager{ } derivedPlayCrunchyStreams[""] = new StreamDetails{ - Url =[playStream.Url], + Url =[new UrlWithAuth(){ Url = playStream.Url, CrAuth = authEndpoint }], IsHardsubbed = false, HardsubLocale = Locale.DefaulT, HardsubLang = Languages.DEFAULT_lang @@ -2368,7 +2384,7 @@ public class CrunchyrollManager{ private async Task ParseChapters(string currentMediaId, List compiledChapters){ - var showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/skip-events/production/{currentMediaId}.json", HttpMethod.Get, true, true, null); + var showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/skip-events/production/{currentMediaId}.json", HttpMethod.Get, true, CrAuthEndpoint1.Token?.access_token, null); var showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true); @@ -2455,7 +2471,7 @@ public class CrunchyrollManager{ } else{ Console.WriteLine("Chapter request failed, attempting old API "); - showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/datalab-intro-v2/{currentMediaId}.json", HttpMethod.Get, true, true, null); + showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/datalab-intro-v2/{currentMediaId}.json", HttpMethod.Get, true, CrAuthEndpoint1.Token?.access_token, null); showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true); @@ -2488,6 +2504,12 @@ public class CrunchyrollManager{ } } + public async Task DeAuthVideo(string currentMediaId, string videoToken, CrAuth authEndoint){ + var deauthVideoToken = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/token/{currentMediaId}/{videoToken}/inactive", HttpMethod.Patch, true, + authEndoint.Token?.access_token, null); + var deauthVideoTokenResponse = await HttpClientReq.Instance.SendHttpRequest(deauthVideoToken); + } + private static string FormatKey(byte[] keyBytes) => BitConverter.ToString(keyBytes).Replace("-", "").ToLower(); diff --git a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs index b665d86..42eace0 100644 --- a/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs +++ b/CRD/Downloader/Crunchyroll/ViewModels/CrunchyrollSettingsViewModel.cs @@ -140,7 +140,22 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ private ComboBoxItem _selectedStreamEndpoint; [ObservableProperty] - private ComboBoxItem _selectedStreamEndpointSecondary; + private ComboBoxItem _SelectedStreamEndpointSecondary; + + [ObservableProperty] + private string _endpointAuthorization = ""; + + [ObservableProperty] + private string _endpointUserAgent = ""; + + [ObservableProperty] + private string _endpointDeviceName = ""; + + [ObservableProperty] + private string _endpointDeviceType = ""; + + [ObservableProperty] + private bool _endpointNotSignedWarning; [ObservableProperty] private ComboBoxItem _selectedDefaultDubLang; @@ -228,14 +243,14 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ public ObservableCollection StreamEndpointsSecondary{ get; } =[ new(){ Content = "" }, - new(){ Content = "web/firefox" }, + // new(){ Content = "web/firefox" }, new(){ Content = "console/switch" }, new(){ Content = "console/ps4" }, new(){ Content = "console/ps5" }, new(){ Content = "console/xbox_one" }, - new(){ Content = "web/edge" }, - new(){ Content = "web/chrome" }, - new(){ Content = "web/fallback" }, + // new(){ Content = "web/edge" }, + // new(){ Content = "web/chrome" }, + // new(){ Content = "web/fallback" }, new(){ Content = "android/phone" }, new(){ Content = "android/tablet" }, new(){ Content = "tv/samsung" }, @@ -314,8 +329,17 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null; SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0]; - ComboBoxItem? streamEndpointSecondary = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondary ?? "")) ?? null; - SelectedStreamEndpointSecondary = streamEndpointSecondary ?? StreamEndpointsSecondary[0]; + ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpointSecondSettings?.Endpoint ?? "")) ?? null; + SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0]; + + EndpointAuthorization = options.StreamEndpointSecondSettings?.Authorization ?? string.Empty; + EndpointUserAgent = options.StreamEndpointSecondSettings?.UserAgent ?? string.Empty; + EndpointDeviceName = options.StreamEndpointSecondSettings?.Device_name ?? string.Empty; + EndpointDeviceType = options.StreamEndpointSecondSettings?.Device_type ?? string.Empty; + + if (CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???"){ + EndpointNotSignedWarning = true; + } FFmpegHWAccel.AddRange(GetAvailableHWAccelOptions()); @@ -473,7 +497,16 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + ""; - CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondary = SelectedStreamEndpointSecondary.Content + ""; + + var endpointSettings = new CrAuthSettings(); + endpointSettings.Endpoint = SelectedStreamEndpointSecondary.Content + ""; + endpointSettings.Authorization = EndpointAuthorization; + endpointSettings.UserAgent = EndpointUserAgent; + endpointSettings.Device_name = EndpointDeviceName; + endpointSettings.Device_type = EndpointDeviceType; + + + CrunchyrollManager.Instance.CrunOptions.StreamEndpointSecondSettings = endpointSettings; List dubLangs = new List(); foreach (var listBoxItem in SelectedDubLang){ @@ -632,6 +665,38 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{ } } + [RelayCommand] + public void ResetEndpointSettings(){ + ComboBoxItem? streamEndpointSecondar = StreamEndpointsSecondary.FirstOrDefault(a => a.Content != null && (string)a.Content == ("android/phone")) ?? null; + SelectedStreamEndpointSecondary = streamEndpointSecondar ?? StreamEndpointsSecondary[0]; + + EndpointAuthorization = "Basic YmY3MHg2aWhjYzhoZ3p3c2J2eGk6eDJjc3BQZXQzWno1d0pDdEpyVUNPSVM5Ynpad1JDcGM="; + EndpointUserAgent = "Crunchyroll/3.90.0 Android/16 okhttp/4.12.0"; + EndpointDeviceName = "CPH2449"; + EndpointDeviceType = "OnePlus CPH2449"; + } + + [RelayCommand] + public async Task Login(){ + var dialog = new ContentDialog(){ + Title = "Login", + PrimaryButtonText = "Login", + CloseButtonText = "Close" + }; + + var viewModel = new ContentDialogInputLoginViewModel(dialog); + dialog.Content = new ContentDialogInputLoginView(){ + DataContext = viewModel + }; + + _ = await dialog.ShowAsync(); + + if (CrunchyrollManager.Instance.CrAuthEndpoint2.Profile.Username == "???"){ + EndpointNotSignedWarning = true; + } + + } + private List GetAvailableHWAccelOptions(){ try{ using (var process = new Process()){ diff --git a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml index 9862a7a..e5ab5d2 100644 --- a/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml +++ b/CRD/Downloader/Crunchyroll/Views/CrunchyrollSettingsView.axaml @@ -110,7 +110,7 @@ - + @@ -239,16 +239,73 @@ - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -325,7 +382,7 @@ HorizontalAlignment="Stretch" /> - + @@ -364,7 +421,7 @@ - + @@ -411,7 +468,7 @@ - + @@ -446,7 +503,7 @@ - + @@ -457,7 +514,7 @@ - + diff --git a/CRD/Downloader/History.cs b/CRD/Downloader/History.cs index f840b67..71d0f89 100644 --- a/CRD/Downloader/History.cs +++ b/CRD/Downloader/History.cs @@ -25,7 +25,7 @@ public class History{ return false; } - await crunInstance.CrAuth.RefreshToken(true); + await crunInstance.CrAuthEndpoint1.RefreshToken(true); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); diff --git a/CRD/Downloader/QueueManager.cs b/CRD/Downloader/QueueManager.cs index 7a1d97d..4fe488a 100644 --- a/CRD/Downloader/QueueManager.cs +++ b/CRD/Downloader/QueueManager.cs @@ -99,13 +99,13 @@ public partial class QueueManager : ObservableObject{ return; } - await CrunchyrollManager.Instance.CrAuth.RefreshToken(true); + await CrunchyrollManager.Instance.CrAuthEndpoint1.RefreshToken(true); var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale); if (episodeL != null){ - if (episodeL.IsPremiumOnly && !CrunchyrollManager.Instance.Profile.HasPremium){ + if (episodeL.IsPremiumOnly && !CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.HasPremium){ MessageBus.Current.SendMessage(new ToastMessage($"Episode is a premium episode – make sure that you are signed in with an account that has an active premium subscription", ToastType.Error, 3)); return; } @@ -197,6 +197,12 @@ public partial class QueueManager : ObservableObject{ break; } + if (!selected.DownloadSubs.Contains("none") && selected.DownloadSubs.All(item => (selected.AvailableSubs ??[]).Contains(item))){ + if (!(selected.Data.Count < dubLang.Count && !CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub)){ + selected.HighlightAllAvailable = true; + } + } + newOptions.DubLang = dubLang; selected.DownloadSettings = newOptions; @@ -293,7 +299,7 @@ public partial class QueueManager : ObservableObject{ } public async Task CrAddMusicVideoToQueue(string epId){ - await CrunchyrollManager.Instance.CrAuth.RefreshToken(true); + await CrunchyrollManager.Instance.CrAuthEndpoint1.RefreshToken(true); var musicVideo = await CrunchyrollManager.Instance.CrMusic.ParseMusicVideoByIdAsync(epId, ""); @@ -317,7 +323,7 @@ public partial class QueueManager : ObservableObject{ } public async Task CrAddConcertToQueue(string epId){ - await CrunchyrollManager.Instance.CrAuth.RefreshToken(true); + await CrunchyrollManager.Instance.CrAuthEndpoint1.RefreshToken(true); var concert = await CrunchyrollManager.Instance.CrMusic.ParseConcertByIdAsync(epId, ""); @@ -410,6 +416,12 @@ public partial class QueueManager : ObservableObject{ crunchyEpMeta.DownloadSettings = newOptions; + if (!crunchyEpMeta.DownloadSubs.Contains("none") && crunchyEpMeta.DownloadSubs.All(item => (crunchyEpMeta.AvailableSubs ??[]).Contains(item))){ + if (!(crunchyEpMeta.Data.Count < data.DubLang.Count && !CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub)){ + crunchyEpMeta.HighlightAllAvailable = true; + } + } + Queue.Add(crunchyEpMeta); diff --git a/CRD/Utils/Http/HttpClientReq.cs b/CRD/Utils/Http/HttpClientReq.cs index f6f4262..065368d 100644 --- a/CRD/Utils/Http/HttpClientReq.cs +++ b/CRD/Utils/Http/HttpClientReq.cs @@ -33,16 +33,10 @@ public class HttpClientReq{ } #endregion - - + private HttpClient client; - private Dictionary cookieStore; - - private HttpClientHandler handler; - + public HttpClientReq(){ - cookieStore = new Dictionary(); - IWebProxy systemProxy = WebRequest.DefaultWebProxy; HttpClientHandler handler = new HttpClientHandler(); @@ -130,43 +124,10 @@ public class HttpClientReq{ return handler; } - - public void SetETPCookie(string refreshToken){ - // var cookie = new Cookie("etp_rt", refreshToken){ - // Domain = "crunchyroll.com", - // Path = "/", - // }; - // - // var cookie2 = new Cookie("c_locale", "en-US"){ - // Domain = "crunchyroll.com", - // Path = "/", - // }; - // - // handler.CookieContainer.Add(cookie); - // handler.CookieContainer.Add(cookie2); - - AddCookie(".crunchyroll.com", new Cookie("etp_rt", refreshToken)); - AddCookie(".crunchyroll.com", new Cookie("c_locale", "en-US")); - } - - private void AddCookie(string domain, Cookie cookie){ - if (!cookieStore.ContainsKey(domain)){ - cookieStore[domain] = new CookieCollection(); - } - - var existingCookie = cookieStore[domain].FirstOrDefault(c => c.Name == cookie.Name); - - if (existingCookie != null){ - cookieStore[domain].Remove(existingCookie); - } - - cookieStore[domain].Add(cookie); - } - - public async Task<(bool IsOk, string ResponseContent,string error)> SendHttpRequest(HttpRequestMessage request, bool suppressError = false){ + public async Task<(bool IsOk, string ResponseContent, string error)> SendHttpRequest(HttpRequestMessage request, bool suppressError = false, Dictionary? cookieStore = null){ string content = string.Empty; try{ - AttachCookies(request); + AttachCookies(request, cookieStore); HttpResponseMessage response = await client.SendAsync(request); @@ -178,18 +139,22 @@ public class HttpClientReq{ response.EnsureSuccessStatusCode(); - return (IsOk: true, ResponseContent: content,error:""); + return (IsOk: true, ResponseContent: content, error: ""); } catch (Exception e){ // Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}"); if (!suppressError){ Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}"); } - return (IsOk: false, ResponseContent: content,error: e.Message); + return (IsOk: false, ResponseContent: content, error: e.Message); } } - private void AttachCookies(HttpRequestMessage request){ + private void AttachCookies(HttpRequestMessage request, Dictionary? cookieStore){ + if (cookieStore == null){ + return; + } + var cookieHeader = new StringBuilder(); if (request.Headers.TryGetValues("Cookie", out var existingCookies)){ @@ -214,13 +179,31 @@ public class HttpClientReq{ } } + public void AddCookie(string domain, Cookie cookie, Dictionary? cookieStore){ + if (cookieStore == null){ + return; + } - public static HttpRequestMessage CreateRequestMessage(string uri, HttpMethod requestMethod, bool authHeader, bool disableDrmHeader, NameValueCollection? query){ + if (!cookieStore.ContainsKey(domain)){ + cookieStore[domain] = new CookieCollection(); + } + + var existingCookie = cookieStore[domain].FirstOrDefault(c => c.Name == cookie.Name); + + if (existingCookie != null){ + cookieStore[domain].Remove(existingCookie); + } + + cookieStore[domain].Add(cookie); + } + + + public static HttpRequestMessage CreateRequestMessage(string uri, HttpMethod requestMethod, bool authHeader, string? accessToken = "", NameValueCollection? query = null){ if (string.IsNullOrEmpty(uri)){ Console.Error.WriteLine($" Request URI is empty"); return new HttpRequestMessage(HttpMethod.Get, "about:blank"); } - + UriBuilder uriBuilder = new UriBuilder(uri); if (query != null){ @@ -230,20 +213,13 @@ public class HttpClientReq{ var request = new HttpRequestMessage(requestMethod, uriBuilder.ToString()); if (authHeader){ - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", CrunchyrollManager.Instance.Token?.access_token); - } - - if (disableDrmHeader){ + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } return request; } - public static async Task DeAuthVideo(string currentMediaId, string token){ - var deauthVideoToken = HttpClientReq.CreateRequestMessage($"https://cr-play-service.prd.crunchyrollsvc.com/v1/token/{currentMediaId}/{token}/inactive", HttpMethod.Patch, true, false, null); - var deauthVideoTokenResponse = await HttpClientReq.Instance.SendHttpRequest(deauthVideoToken); - } public HttpClient GetHttpClient(){ return client; @@ -266,17 +242,17 @@ public static class ApiUrls{ public static string Playback => "https://cr-play-service.prd.crunchyrollsvc.com/v2"; //https://www.crunchyroll.com/playback/v2 //https://cr-play-service.prd.crunchyrollsvc.com/v2 - + public static string Subscription => (CrunchyrollManager.Instance.CrunOptions.UseCrBetaApi ? ApiBeta : ApiN) + "/subs/v3/subscriptions/"; public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse"; public static readonly string BetaCms = ApiBeta + "/cms/v2"; public static readonly string DRM = ApiBeta + "/drm/v1/auth"; - + public static readonly string WidevineLicenceUrl = "https://www.crunchyroll.com/license/v1/license/widevine"; //https://lic.drmtoday.com/license-proxy-widevine/cenc/ //https://lic.staging.drmtoday.com/license-proxy-widevine/cenc/ - + // public static string authBasicMob = "Basic djV3YnNsdGJueG5oeXk3cDN4ZmI6cFdKWkZMaHVTM0I2NFhPbk81bWVlWXpiTlBtZWsyRVU="; public static string authBasicMob = "Basic Ym1icmt4eXgzZDd1NmpzZnlsYTQ6QUlONEQ1VkVfY3Awd1Z6Zk5vUDBZcUhVcllGcDloU2c="; diff --git a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs index 686d37a..5c7ec83 100644 --- a/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs +++ b/CRD/Utils/Structs/Crunchyroll/CrDownloadOptions.cs @@ -292,8 +292,8 @@ public class CrDownloadOptions{ [JsonProperty("stream_endpoint")] public string? StreamEndpoint{ get; set; } - [JsonProperty("stream_endpoint_secondary")] - public string? StreamEndpointSecondary { get; set; } + [JsonProperty("stream_endpoint_secondary_settings")] + public CrAuthSettings? StreamEndpointSecondSettings { get; set; } [JsonProperty("search_fetch_featured_music")] public bool SearchFetchFeaturedMusic{ get; set; } diff --git a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs index dfa173b..1d0eaa8 100644 --- a/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs +++ b/CRD/Utils/Structs/Crunchyroll/Episode/EpisodeStructs.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; +using Avalonia.Media.Imaging; using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.History; using Newtonsoft.Json; @@ -389,6 +390,8 @@ public class CrunchyEpMeta{ public bool OnlySubs{ get; set; } public CrDownloadOptions? DownloadSettings; + + public bool HighlightAllAvailable{ get; set; } } public class DownloadProgress{ diff --git a/CRD/Utils/Structs/Crunchyroll/Playback.cs b/CRD/Utils/Structs/Crunchyroll/Playback.cs index 39a8812..2949fa3 100644 --- a/CRD/Utils/Structs/Crunchyroll/Playback.cs +++ b/CRD/Utils/Structs/Crunchyroll/Playback.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using CRD.Downloader.Crunchyroll; using Newtonsoft.Json; namespace CRD.Utils.Structs.Crunchyroll; @@ -13,7 +14,7 @@ public class StreamDetails{ [JsonProperty("hardsub_locale")] public Locale? HardsubLocale{ get; set; } - public List Url{ get; set; } + public List Url{ get; set; } [JsonProperty("hardsub_lang")] public required LanguageItem HardsubLang{ get; set; } @@ -26,9 +27,17 @@ public class StreamDetails{ public string? Type{ get; set; } } +public class UrlWithAuth{ + + public CrAuth? CrAuth{ get; set; } + + public string? Url{ get; set; } + +} + public class StreamDetailsPop{ public Locale? HardsubLocale{ get; set; } - public List Url{ get; set; } + public List Url{ get; set; } public required LanguageItem HardsubLang{ get; set; } public bool IsHardsubbed{ get; set; } diff --git a/CRD/Utils/Structs/HelperClasses.cs b/CRD/Utils/Structs/HelperClasses.cs index ec74e53..6c2cfd8 100644 --- a/CRD/Utils/Structs/HelperClasses.cs +++ b/CRD/Utils/Structs/HelperClasses.cs @@ -12,6 +12,14 @@ public class AuthData{ public string Password{ get; set; } } +public class CrAuthSettings{ + public string Endpoint{ get; set; } + public string Authorization{ get; set; } + public string UserAgent{ get; set; } + public string Device_type{ get; set; } + public string Device_name{ get; set; } +} + public class DrmAuthData{ [JsonProperty("custom_data")] public string? CustomData{ get; set; } diff --git a/CRD/ViewModels/AccountPageViewModel.cs b/CRD/ViewModels/AccountPageViewModel.cs index 2d6f9a8..34894f2 100644 --- a/CRD/ViewModels/AccountPageViewModel.cs +++ b/CRD/ViewModels/AccountPageViewModel.cs @@ -50,8 +50,8 @@ public partial class AccountPageViewModel : ViewModelBase{ RemainingTime = "Subscription maybe ended"; } - if (CrunchyrollManager.Instance.Profile.Subscription != null){ - Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.Profile.Subscription, Formatting.Indented)); + if (CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription != null){ + Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription, Formatting.Indented)); } } else{ RemainingTime = $"{(IsCancelled ? "Subscription ending in: " : "Subscription refreshing in: ")}{remaining:dd\\:hh\\:mm\\:ss}"; @@ -59,13 +59,13 @@ public partial class AccountPageViewModel : ViewModelBase{ } public void UpdatetProfile(){ - ProfileName = CrunchyrollManager.Instance.Profile.Username ?? CrunchyrollManager.Instance.Profile.ProfileName ?? "???"; // Default or fetched user name - LoginLogoutText = CrunchyrollManager.Instance.Profile.Username == "???" ? "Login" : "Logout"; // Default state + ProfileName = CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Username ?? CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.ProfileName ?? "???"; // Default or fetched user name + LoginLogoutText = CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Username == "???" ? "Login" : "Logout"; // Default state LoadProfileImage("https://static.crunchyroll.com/assets/avatar/170x170/" + - (string.IsNullOrEmpty(CrunchyrollManager.Instance.Profile.Avatar) ? "crbrand_avatars_logo_marks_mangagirl_taupe.png" : CrunchyrollManager.Instance.Profile.Avatar)); + (string.IsNullOrEmpty(CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Avatar) ? "crbrand_avatars_logo_marks_mangagirl_taupe.png" : CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Avatar)); - var subscriptions = CrunchyrollManager.Instance.Profile.Subscription; + var subscriptions = CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription; if (subscriptions != null){ if (subscriptions.SubscriptionProducts is{ Count: >= 1 }){ @@ -84,8 +84,8 @@ public partial class AccountPageViewModel : ViewModelBase{ UnknownEndDate = true; } - if (CrunchyrollManager.Instance.Profile.Subscription?.NextRenewalDate != null && !UnknownEndDate){ - _targetTime = CrunchyrollManager.Instance.Profile.Subscription.NextRenewalDate; + if (CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription?.NextRenewalDate != null && !UnknownEndDate){ + _targetTime = CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription.NextRenewalDate; _timer = new DispatcherTimer{ Interval = TimeSpan.FromSeconds(1) }; @@ -101,8 +101,8 @@ public partial class AccountPageViewModel : ViewModelBase{ RaisePropertyChanged(nameof(RemainingTime)); - if (CrunchyrollManager.Instance.Profile.Subscription != null){ - Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.Profile.Subscription, Formatting.Indented)); + if (CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription != null){ + Console.Error.WriteLine(JsonConvert.SerializeObject(CrunchyrollManager.Instance.CrAuthEndpoint1.Profile.Subscription, Formatting.Indented)); } } @@ -131,7 +131,8 @@ public partial class AccountPageViewModel : ViewModelBase{ _ = await dialog.ShowAsync(); } else{ - await CrunchyrollManager.Instance.CrAuth.AuthAnonymous(); + await CrunchyrollManager.Instance.CrAuthEndpoint1.AuthAnonymous(); + await CrunchyrollManager.Instance.CrAuthEndpoint2.AuthAnonymous(); UpdatetProfile(); } } diff --git a/CRD/ViewModels/SeriesPageViewModel.cs b/CRD/ViewModels/SeriesPageViewModel.cs index c2df0c3..632fabd 100644 --- a/CRD/ViewModels/SeriesPageViewModel.cs +++ b/CRD/ViewModels/SeriesPageViewModel.cs @@ -324,6 +324,30 @@ public partial class SeriesPageViewModel : ViewModelBase{ Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}"); } } + + [RelayCommand] + public async Task OpenSeriesDetails(){ + CrSeriesBase? parsedSeries = await CrunchyrollManager.Instance.CrSeries.SeriesById(SelectedSeries.SeriesId ?? string.Empty, CrunchyrollManager.Instance.CrunOptions.HistoryLang, true); + + if (parsedSeries is{ Data.Length: > 0 }){ + var dialog = new CustomContentDialog(){ + Title = "Series", + CloseButtonText = "Close", + FullSizeDesired = true + }; + + var viewModel = new ContentDialogSeriesDetailsViewModel(dialog, parsedSeries,SelectedSeries.SeriesFolderPath); + dialog.Content = new ContentDialogSeriesDetailsView(){ + DataContext = viewModel + }; + + var dialogResult = await dialog.ShowAsync(); + } else{ + MessageBus.Current.SendMessage(new ToastMessage($"Failed to get series details", ToastType.Warning, 3)); + } + + + } partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){ diff --git a/CRD/ViewModels/Utils/ContentDialogInputLoginViewModel.cs b/CRD/ViewModels/Utils/ContentDialogInputLoginViewModel.cs index b71fc3e..9c7a67f 100644 --- a/CRD/ViewModels/Utils/ContentDialogInputLoginViewModel.cs +++ b/CRD/ViewModels/Utils/ContentDialogInputLoginViewModel.cs @@ -17,7 +17,7 @@ public partial class ContentDialogInputLoginViewModel : ViewModelBase{ private AccountPageViewModel accountPageViewModel; - public ContentDialogInputLoginViewModel(ContentDialog dialog, AccountPageViewModel accountPageViewModel){ + public ContentDialogInputLoginViewModel(ContentDialog dialog, AccountPageViewModel accountPageViewModel = null){ if (dialog is null){ throw new ArgumentNullException(nameof(dialog)); } @@ -30,8 +30,15 @@ public partial class ContentDialogInputLoginViewModel : ViewModelBase{ private async void LoginButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){ dialog.PrimaryButtonClick -= LoginButton; - await CrunchyrollManager.Instance.CrAuth.Auth(new AuthData{Password = Password,Username = Email}); - accountPageViewModel.UpdatetProfile(); + await CrunchyrollManager.Instance.CrAuthEndpoint1.Auth(new AuthData{Password = Password,Username = Email}); + if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrAuthEndpoint2.AuthSettings.Endpoint)){ + await CrunchyrollManager.Instance.CrAuthEndpoint2.Auth(new AuthData{Password = Password,Username = Email}); + } + + if (accountPageViewModel != null){ + accountPageViewModel.UpdatetProfile(); + } + } private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){ diff --git a/CRD/ViewModels/Utils/ContentDialogSeriesDetailsViewModel.cs b/CRD/ViewModels/Utils/ContentDialogSeriesDetailsViewModel.cs new file mode 100644 index 0000000..bf96aff --- /dev/null +++ b/CRD/ViewModels/Utils/ContentDialogSeriesDetailsViewModel.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Media.Imaging; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CRD.Downloader.Crunchyroll; +using CRD.Utils; +using CRD.Utils.Files; +using CRD.Utils.Structs; +using CRD.Utils.Structs.Crunchyroll.Music; +using CRD.Utils.Structs.History; +using CRD.Utils.UI; +using CRD.Views; +using DynamicData; +using FluentAvalonia.UI.Controls; +using ReactiveUI; +using Image = CRD.Utils.Structs.Image; + +namespace CRD.ViewModels.Utils; + +public partial class ContentDialogSeriesDetailsViewModel : ViewModelBase{ + private readonly CustomContentDialog dialog; + + private CrSeriesBase seriesBase; + private string downloadPath = CfgManager.PathVIDEOS_DIR; + + [ObservableProperty] + private ObservableCollection _imagesListPosterTall = new(); + + [ObservableProperty] + private ObservableCollection _imagesListPosterWide = new(); + + [ObservableProperty] + private ObservableCollection _imagesListPromoImage = new(); + + [ObservableProperty] + private ObservableCollection _imagesListThumbnail = new(); + + [ObservableProperty] + private bool _isResolutionPopupOpenTall; + + [ObservableProperty] + private bool _isResolutionPopupOpenWide; + + [ObservableProperty] + private Image? _selectedImage; + + public ContentDialogSeriesDetailsViewModel(CustomContentDialog contentDialog, CrSeriesBase seriesBase, string downloadPath){ + ArgumentNullException.ThrowIfNull(contentDialog); + + this.seriesBase = seriesBase; + if (!string.IsNullOrEmpty(downloadPath)){ + this.downloadPath = downloadPath; + } + + + dialog = contentDialog; + dialog.Closed += DialogOnClosed; + dialog.PrimaryButtonClick += SaveButton; + + _ = LoadImages(); + } + + public async Task LoadImages(){ + var images = (seriesBase.Data ??[]).FirstOrDefault()?.Images; + if (images != null){ + foreach (var list in images.PosterTall){ + ImagesListPosterTall.Add(new SeriesDetailsImage(){ + images = list, + imagePreview = await Helpers.LoadImage(list.Last().Source) + }); + } + + foreach (var list in images.PosterWide){ + ImagesListPosterWide.Add(new SeriesDetailsImage(){ + images = list, + imagePreview = await Helpers.LoadImage(list.Last().Source) + }); + } + + foreach (var list in images.PromoImage){ + ImagesListPromoImage.Add(new SeriesDetailsImage(){ + images = list, + imagePreview = await Helpers.LoadImage(list.Last().Source) + }); + } + + foreach (var list in images.Thumbnail){ + ImagesListThumbnail.Add(new SeriesDetailsImage(){ + images = list, + imagePreview = await Helpers.LoadImage(list.Last().Source) + }); + } + } + } + + [RelayCommand] + public void ToggleButtonTall(){ + IsResolutionPopupOpenTall = !IsResolutionPopupOpenTall; + } + + [RelayCommand] + public void ToggleButtonWide(){ + IsResolutionPopupOpenWide = !IsResolutionPopupOpenWide; + } + + [RelayCommand] + public async Task DownloadImage(Image? image){ + if (image == null){ + return; + } + + IsResolutionPopupOpenTall = false; + IsResolutionPopupOpenWide = false; + SelectedImage = new Image(); + + string fileName = image.Type.GetEnumMemberValue() + "_" + image.Width + "_" + image.Height + ".png"; + + string coverPath = Path.Combine(downloadPath, fileName); + if (!string.IsNullOrEmpty(image.Source)){ + if (!File.Exists(coverPath)){ + var bitmap = await Helpers.LoadImage(image.Source); + if (bitmap != null){ + Helpers.EnsureDirectoriesExist(coverPath); + await using (var fs = File.OpenWrite(coverPath)){ + bitmap.Save(fs); // always saves PNG + } + + bitmap.Dispose(); + MessageBus.Current.SendMessage(new ToastMessage($"Image downloaded: " + coverPath, ToastType.Information, 1)); + } + } else{ + MessageBus.Current.SendMessage(new ToastMessage($"Image already exists with that name: " + coverPath, ToastType.Error, 3)); + } + } + } + + private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){ + dialog.PrimaryButtonClick -= SaveButton; + } + + private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){ + dialog.Closed -= DialogOnClosed; + } +} + +public class SeriesDetailsImage{ + public List images{ get; set; } + public Bitmap? imagePreview{ get; set; } +} \ No newline at end of file diff --git a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs index 4a1e364..7895053 100644 --- a/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs +++ b/CRD/ViewModels/Utils/GeneralSettingsViewModel.cs @@ -608,7 +608,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{ [RelayCommand] public async void CheckIp(){ - var result = await HttpClientReq.Instance.SendHttpRequest(HttpClientReq.CreateRequestMessage("https://icanhazip.com", HttpMethod.Get, false, false, null)); + var result = await HttpClientReq.Instance.SendHttpRequest(HttpClientReq.CreateRequestMessage("https://icanhazip.com", HttpMethod.Get, false)); Console.Error.WriteLine("Your IP: " + result.ResponseContent); if (result.IsOk){ CurrentIp = result.ResponseContent; diff --git a/CRD/Views/DownloadsPageView.axaml b/CRD/Views/DownloadsPageView.axaml index 0cd28df..7d8eb97 100644 --- a/CRD/Views/DownloadsPageView.axaml +++ b/CRD/Views/DownloadsPageView.axaml @@ -10,14 +10,14 @@ xmlns:ui="clr-namespace:CRD.Utils.UI"> - + - - + + - - + + @@ -28,7 +28,7 @@ - + - - - + @@ -69,16 +69,16 @@ - + - + - + @@ -90,13 +90,16 @@ - - - - + + - + - - - - - - - - + + + + + + @@ -132,15 +135,15 @@ - - - - - - + + + + + + diff --git a/CRD/Views/SeriesPageView.axaml b/CRD/Views/SeriesPageView.axaml index 449b87f..4174ebd 100644 --- a/CRD/Views/SeriesPageView.axaml +++ b/CRD/Views/SeriesPageView.axaml @@ -42,9 +42,13 @@ - - + @@ -341,31 +345,36 @@ - - diff --git a/CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml b/CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml new file mode 100644 index 0000000..947603f --- /dev/null +++ b/CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml.cs b/CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml.cs new file mode 100644 index 0000000..12cca56 --- /dev/null +++ b/CRD/Views/Utils/ContentDialogSeriesDetailsView.axaml.cs @@ -0,0 +1,35 @@ +using System.Linq; +using Avalonia.Controls; +using Avalonia.VisualTree; +using CRD.ViewModels.Utils; +using Image = CRD.Utils.Structs.Image; + +namespace CRD.Views.Utils; + +public partial class ContentDialogSeriesDetailsView : UserControl{ + public ContentDialogSeriesDetailsView(){ + InitializeComponent(); + } + + private void ImageSelectionChanged(object? sender, SelectionChangedEventArgs e){ + if (DataContext is ContentDialogSeriesDetailsViewModel viewModel && sender is ListBox listBox){ + _ = viewModel.DownloadImage((Image)listBox.SelectedItem ); + } + } + + private void ListBox_PointerWheelChanged(object sender, Avalonia.Input.PointerWheelEventArgs e){ + var listBox = sender as ListBox; + var scrollViewer = listBox?.GetVisualDescendants().OfType().FirstOrDefault(); + + if (scrollViewer != null){ + // Determine if the ListBox is at its bounds (top or bottom) + bool atTop = scrollViewer.Offset.Y <= 0 && e.Delta.Y > 0; + bool atBottom = scrollViewer.Offset.Y >= scrollViewer.Extent.Height - scrollViewer.Viewport.Height && e.Delta.Y < 0; + + if (atTop || atBottom){ + e.Handled = true; // Stop the event from propagating to the parent + } + } + } + +} \ No newline at end of file